Ein Panel zeigt deinen Ordner
In Etappe 1 haben wir das Commander-Fenster gebaut, aber die blaue Fläche ist noch leer. Heute kommt Leben hinein: Ein Panel zeigt die Dateien und Ordner aus dem Ordner an, in dem du das Programm startest, sortiert, mit Größenangaben und mit dem typischen türkisen Rahmen. Dabei lernst du, wie ein Rust-Programm Ordner auf deiner Festplatte liest und was passiert, wenn dabei etwas schiefgeht.
Bevor wir loslegen
Wir machen da weiter, wo Etappe 1 aufgehört hat. Falls du neu einsteigst: Den kompletten Stand findest du am Ende von Etappe 1 unter Zwischenstand. Wie die Code-Kästen funktionieren, steht ebenfalls dort, im Abschnitt „So liest du die Code-Kästen“.
Heute bauen wir von unten nach oben, so wie man ein Haus baut: Erst beschreiben wir, was ein einzelner Eintrag in einem Ordner ist. Dann schreiben wir eine Funktion, die einen ganzen Ordner liest. Danach bekommt das Programm ein Gedächtnis, und erst zum Schluss zeichnen wir das Panel. Unterwegs wird Rust ein paarmal gelb warnen, dass wir etwas gebaut, aber noch nicht benutzt haben. Das ist bei dieser Bauweise ganz normal.
Was ist ein Eintrag?
Ein Ordner enthält Dateien und weitere Ordner. Für unser Panel ist an jedem dieser
Einträge dreierlei wichtig: wie er heißt, ob er ein Ordner ist und wie groß
er ist. Für Dinge, die zusammengehören, gibt es in Rust das struct. Wir
schreiben es direkt unter unsere Farben:
const WEISS: Color = Color::WHITE;// Ein Eintrag in einem Ordner: eine Datei oder ein Unterordner. struct Eintrag { name: String, ist_ordner: bool, groesse: u64, }fn main() -> iced::Result {
name: Stringist der Name, zum BeispielUrlaubsvideo.mp4. EinStringist ein Text, der dem Eintrag selbst gehört und beliebig lang sein darf.ist_ordner: boolist entwedertrue(Ordner) oderfalse(Datei).groesse: u64ist die Größe in Bytes.u64ist eine sehr große ganze Zahl ohne Vorzeichen. Sie reicht für Dateien bis zu 18 Trillionen Bytes, also locker für jedes Urlaubsvideo.
cargo run
Das Fenster sieht aus wie bisher. Rust meldet gelb, dass Eintrag nie
benutzt wird. Stimmt, das kommt im nächsten Schritt.Einen Ordner lesen
Jetzt bringen wir dem Fährmann bei, in einen Ordner hineinzuschauen. Werkzeuge dafür hat
Rust schon eingebaut, in seiner Standardbibliothek std.
fs steht für file system, das Dateisystem, und Path
beschreibt einen Pfad wie /home/volker/Dokumente. Beides holen wir ganz oben
in die Datei:
use std::fs; use std::path::Path;use iced::widget::{column, container, row, text};
Die Leerzeile trennt die Standardbibliothek von iced. Das ist kein Muss, aber so sieht man
auf einen Blick, was woher kommt. Jetzt die eigentliche Funktion. Sie kommt ganz ans Ende
der Datei, hinter f_taste:
row![zahl, feld].width(Fill).into() }// Liest einen Ordner und liefert seine Einträge: oben "..", dann Ordner, dann Dateien. fn ordner_lesen(pfad: &Path) -> Vec<Eintrag> { let mut eintraege = Vec::new(); let Ok(inhalt) = fs::read_dir(pfad) else { return eintraege; // Ordner nicht lesbar: leere Liste }; for eintrag in inhalt.flatten() { let name = eintrag.file_name().to_string_lossy().to_string(); if name.starts_with('.') { continue; // versteckte Dateien überspringen } let Ok(info) = eintrag.metadata() else { continue; // keine Infos lesbar: Eintrag überspringen }; eintraege.push(Eintrag { name, ist_ordner: info.is_dir(), groesse: info.len(), }); } // Ordner vor Dateien, innerhalb davon alphabetisch eintraege.sort_by(|a, b| { b.ist_ordner .cmp(&a.ist_ordner) .then(a.name.to_lowercase().cmp(&b.name.to_lowercase())) }); // Der Weg nach oben steht immer ganz oben if pfad.parent().is_some() { let hoch = Eintrag { name: String::from(".."), ist_ordner: true, groesse: 0, }; eintraege.insert(0, hoch); } eintraege }
Das ist ein großer Brocken, aber er erzählt eine einfache Geschichte. Gehen wir sie gemeinsam durch:
- Ordner öffnen:
fs::read_dir(pfad)versucht, den Ordner zu öffnen. Versucht, denn das kann schiefgehen: Vielleicht gibt es den Ordner nicht, oder du darfst nicht hineinschauen. Rust zwingt uns, an diesen Fall zu denken. Die Antwort ist einResult, entwederOk(…)mit dem Inhalt oderErr(…)mit einem Fehler. - let … else:
let Ok(inhalt) = … else { … };heißt: „Wenn das geklappt hat, nenn das Ergebnisinhaltund mach weiter. Sonst führ denelse-Block aus.“ Und der verlässt die Funktion mit einer leeren Liste. Ein unlesbarer Ordner sieht also einfach leer aus, statt das Programm abstürzen zu lassen. - Alle Einträge durchgehen: Auch jeder einzelne Eintrag kann beim Lesen
scheitern.
.flatten()lässt die kaputten einfach weg und gibt uns nur die guten. Den Namen wandeln wir in einenStringum. Dasto_string_lossysorgt dafür, dass auch Dateinamen mit seltsamen Zeichen lesbar werden. - Versteckte Dateien: Unter Linux und macOS beginnen versteckte Dateien
mit einem Punkt. Davon gibt es in jedem Home-Ordner Dutzende. Mit
continueüberspringen wir sie. - Infos holen:
metadata()liefert, ob es ein Ordner ist und wie groß er ist. Auch das kann scheitern, also wiederlet … else. Dann legen wir den fertigen Eintrag mitpushin die Liste. Steht dort nurname,, ist das die Kurzform vonname: name,. - Sortieren:
sort_bybekommt eine Closure, die zwei Einträgeaundbvergleicht. Zuerst nach „Ordner oder nicht“ (Ordner nach oben), und.then(…)sagt: „Bei Gleichstand nach dem Namen.“to_lowercase()sorgt dafür, dass Groß- und Kleinschreibung beim Sortieren keine Rolle spielt. - Der Weg nach oben: Ganz oben in jedem Panel steht im Commander
.., der Eintrag für den übergeordneten Ordner. Nur ganz oben im Dateisystem gibt es keinen, deshalb fragen wir mitpfad.parent().is_some(), ob es einen gibt.insert(0, …)setzt ihn an Platz 0, also ganz an den Anfang.
a.cmp(&b), und
false kommt vor true. Wir wollen aber Ordner, also
true, zuerst. Darum drehen wir es um: b.ist_ordner.cmp(&a.ist_ordner).
Beim Namen dagegen wollen wir die normale Reihenfolge A bis Z, deshalb dort
a vor b.& in pfad: &Path. Die Funktion
bekommt den Pfad nur ausgeliehen, sie muss ihn ja nur anschauen, nicht
behalten. Mehr übers Ausleihen: Buch, Kapitel 45.cargo run
Alles sieht aus wie vorher, und Rust meldet jetzt, dass
ordner_lesen nie benutzt wird. Das Werkzeug liegt bereit.Der Fährmann bekommt ein Gedächtnis
Bisher war struct Faehrmann leer. Jetzt soll es sich zwei Dinge merken:
welcher Ordner gerade offen ist und welche Einträge darin liegen. Für den Ordner brauchen
wir einen Pfad, der dem Fährmann gehört, nicht nur ausgeliehen ist. Das ist der
PathBuf. Das Verhältnis ist dasselbe wie zwischen String und
&str: PathBuf besitzt den Pfad, &Path leiht
ihn sich nur aus.
use std::fs;use std::path::Path;use std::path::{Path, PathBuf};
Dann ersetzen wir das alte, leere struct, und zwar samt der Zeile
#[derive(Default)] darüber:
#[derive(Default)] struct Faehrmann {}// Das Gedächtnis des Fährmanns: welcher Ordner offen ist und was darin liegt. struct Faehrmann { ordner: PathBuf, eintraege: Vec<Eintrag>, }
Warum muss Default weg? Rust kann von allein nur dann einen Startwert
erzeugen, wenn klar ist, wie der aussehen soll. Bei einem Ordner ist das nicht klar. Also
schreiben wir selbst eine Funktion neu, ganz oben in impl Faehrmann:
impl Faehrmann {// Startet im Ordner, aus dem das Programm aufgerufen wurde. fn neu() -> Self { let ordner = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("/")); let eintraege = ordner_lesen(&ordner); Faehrmann { ordner, eintraege } }fn update(&mut self, message: Message) {
std::env::current_dir() fragt das Betriebssystem nach dem Ordner, in dem das
Programm gestartet wurde. Auch das kann theoretisch schiefgehen, deshalb liefert es ein
Result. unwrap_or_else(…) heißt: „Nimm das Ergebnis, und falls es
nicht geklappt hat, nimm stattdessen das hier.“ Im Notfall starten wir also ganz oben im
Dateisystem bei /. Mit &ordner leihen wir den Pfad an
ordner_lesen aus.
Zum Schluss sagen wir iced, dass es neu statt default zum
Starten verwenden soll:
iced::application(Faehrmann::default, Faehrmann::update, Faehrmann::view)iced::application(Faehrmann::neu, Faehrmann::update, Faehrmann::view)
cargo run
Äußerlich ändert sich noch nichts. Rust warnt aber jetzt, dass die Felder
ordner und eintraege nie gelesen werden, und ebenso
groesse in Eintrag. Der Fährmann hat sich den Ordner gemerkt, zeigt ihn aber noch nicht.Eine Zeile pro Eintrag
Bevor wir das ganze Panel zeichnen, bauen wir wieder erst die kleinste Einheit: eine
Zeile. Links steht der Name, rechts die Größe. Ordner zeigen statt einer Größe
<ORDNER>, genau wie im Original. Und damit man Ordner und Dateien schon
an der Farbe unterscheidet, sind Ordner weiß und Dateien hell-türkis. Die neue Farbe kommt
zu den anderen:
const WEISS: Color = Color::WHITE;const HELLTUERKIS: Color = Color::from_rgb8(85, 255, 255);
Die beiden neuen Funktionen kommen ans Ende der Datei:
eintraege }// Baut eine Zeile im Panel: links der Name, rechts die Größe. fn zeile(eintrag: &Eintrag) -> Element<'_, Message> { let farbe = if eintrag.ist_ordner { WEISS } else { HELLTUERKIS }; let groesse = if eintrag.ist_ordner { String::from("<ORDNER>") } else { groesse_text(eintrag.groesse) }; row![ text(eintrag.name.as_str()) .color(farbe) .width(Fill) .wrapping(text::Wrapping::None), text(groesse).color(farbe), ] .spacing(12) .into() } // Macht aus einer Zahl von Bytes etwas Lesbares, zum Beispiel "12,3 KB". fn groesse_text(bytes: u64) -> String { let b = bytes as f64; let text = if bytes < 1024 { format!("{} B", bytes) } else if b < 1024.0 * 1024.0 { format!("{:.1} KB", b / 1024.0) } else if b < 1024.0 * 1024.0 * 1024.0 { format!("{:.1} MB", b / 1024.0 / 1024.0) } else { format!("{:.1} GB", b / 1024.0 / 1024.0 / 1024.0) }; text.replace('.', ",") }
Zuerst zeile:
- Die Funktion bekommt den Eintrag ausgeliehen (
&Eintrag) und liefert einElement. Das'_darin bedeutet: Das Element leiht sich seinerseits etwas aus dem Eintrag, nämlich den Namen. Rust passt auf, dass der Eintrag so lange lebt wie die Zeile. eintrag.name.as_str()zeigt den Namen an, ohne ihn zu kopieren..wrapping(text::Wrapping::None)verbietet Zeilenumbrüche. Ein sehr langer Dateiname würde sonst in eine zweite Zeile rutschen und die ganze Liste durcheinander bringen. Hier zeigt sich übrigens, dasstextzwei Dinge ist: die Funktion, die einen Text baut, und ein Modul, in dem Zubehör wieWrappingliegt.
Und groesse_text rechnet Bytes in lesbare Einheiten um. Ein Kilobyte sind
dabei 1024 Bytes, ein Megabyte 1024 Kilobyte. {:.1} im
format! heißt „mit genau einer Nachkommastelle“. Weil Rust Kommazahlen mit
Punkt schreibt, wir in Deutschland aber mit Komma, tauschen wir am Ende mit
replace den Punkt gegen ein Komma.
as f64 wandelt die ganze Zahl in eine Kommazahl um, damit wir
teilen können, ohne dass die Nachkommastellen verloren gehen. Teilt man in Rust zwei ganze
Zahlen, wird nämlich abgeschnitten: 1500 / 1024 ergibt 1, nicht
1,46.cargo run
Noch immer keine sichtbare Änderung, dafür ein ganzer Schwung gelber
Warnungen: Lauter fertige Bausteine, die noch niemand benutzt. Im nächsten Schritt setzen wir
sie zusammen, und dann verschwinden alle Warnungen auf einmal.Das Panel
Jetzt setzen wir alles zusammen. Das Panel hat oben eine Kopfzeile mit dem Pfad in Gelb, darunter die Liste der Einträge, und rundherum den türkisen Rahmen des Commanders. Zwei Konstanten brauchen wir dafür: die Farbe Gelb und die Zahl der Zeilen, die in ein Panel passen:
const HELLTUERKIS: Color = Color::from_rgb8(85, 255, 255);const GELB: Color = Color::from_rgb8(255, 255, 85); // So viele Einträge passen untereinander in ein Panel const ZEILEN: usize = 24;
Warum 24? Bei unserer Fenstergröße von 640 Pixeln Höhe passen 24 Zeilen in das Panel, wenn Menüzeile, Tastenleiste, Kopfzeile und später noch eine Meldungszeile Platz haben sollen. Ist ein Ordner voller, zeigen wir vorerst nur die ersten 24 Einträge. In Etappe 3 lernt das Panel blättern.
Für den Rahmen brauchen wir Border aus iced:
use iced::{Background, Color, Element, Fill, Font, Theme};use iced::{Background, Border, Color, Element, Fill, Font, Theme};
Das Panel bekommt eine eigene Funktion in impl Faehrmann, direkt hinter
view. Sie muss in das Gedächtnis schauen, darum steht sie in
impl und bekommt &self:
column![menuzeile, arbeitsflaeche, tastenleiste].into() }// Zeichnet das Panel: oben der Pfad, darunter die Einträge, rundherum ein Rahmen. fn panel_ansicht(&self) -> Element<'_, Message> { let kopf = text(self.ordner.display().to_string()).color(GELB); let mut liste = column![]; for eintrag in self.eintraege.iter().take(ZEILEN) { liste = liste.push(zeile(eintrag)); } container(column![kopf, liste].spacing(6)) .width(Fill) .height(Fill) .padding(8) .clip(true) .style(|_theme: &Theme| container::Style { border: Border { color: TUERKIS, width: 2.0, radius: 0.0.into(), }, ..container::Style::default() }) .into() }}
self.ordner.display().to_string()macht aus dem Pfad einen anzeigbaren Text.self.eintraege.iter().take(ZEILEN)geht die Einträge durch, aber höchstens 24 Stück. Für jeden hängen wir eine Zeile an die Liste, genau wie die Tasten an die Tastenleiste..clip(true)schneidet alles ab, was über den Rand hinausragen würde. So bleibt der Rahmen sauber, auch wenn ein Name sehr lang ist.- Der
Borderzeichnet den türkisen Rahmen, 2 Pixel dick und mit eckigen Ecken (radius: 0.0), wie es sich für die 80er gehört.
Zum Schluss ersetzen wir in view den Begrüßungstext durch das Panel. Statt
.center(Fill) sagen wir jetzt, dass die Arbeitsfläche alles ausfüllt, und
geben ihr 6 Pixel Innenabstand, damit der Rahmen nicht am Fensterrand klebt:
let arbeitsflaeche = container(text("Ahoi! Hier entsteht der Fährmann.")) .center(Fill)let arbeitsflaeche = container(self.panel_ansicht()) .width(Fill) .height(Fill) .padding(6).style(|_theme: &Theme| container::Style { background: Some(Background::Color(BLAU)),
.padding(6) und die anderen neuen Zeilen gehören zur
arbeitsflaeche, die .style(…)-Zeile darunter bleibt unverändert.
Wenn du nicht sicher bist, ob alles stimmt, vergleich mit dem
Zwischenstand.cargo run
Da ist es: Oben im Panel steht in Gelb der Pfad deines Ordners, darunter
.., dann die Ordner in Weiß und die Dateien in Türkis, jeweils mit Größe. Und
im Terminal steht keine einzige Warnung mehr. Tipp: Starte das Programm auch mal aus einem anderen Ordner, zum Beispiel mit
cd ~ und dann cargo run --manifest-path und dem Pfad zu deiner
Cargo.toml. Dann siehst du deinen Home-Ordner.
Der komplette Code nach Etappe 2
Hier ist der vollständige Stand nach Etappe 2. Nimm ihn zum Vergleichen, wenn irgendwo ein Fehler steckt, den du nicht findest, oder als Einstiegspunkt, wenn du erst bei der nächsten Etappe dazustößt.
Cargo.toml und src/main.rs anzeigen
Cargo.toml
[package]
name = "faehrmann"
version = "0.1.0"
edition = "2024"
[dependencies]
iced = "0.14"
src/main.rs
use std::fs;
use std::path::{Path, PathBuf};
use iced::widget::{column, container, row, text};
use iced::{Background, Border, Color, Element, Fill, Font, Theme};
// Die Farben des Norton Commander
const BLAU: Color = Color::from_rgb8(0, 0, 170);
const TUERKIS: Color = Color::from_rgb8(0, 170, 170);
const SCHWARZ: Color = Color::BLACK;
const WEISS: Color = Color::WHITE;
const HELLTUERKIS: Color = Color::from_rgb8(85, 255, 255);
const GELB: Color = Color::from_rgb8(255, 255, 85);
// So viele Einträge passen untereinander in ein Panel
const ZEILEN: usize = 24;
// Ein Eintrag in einem Ordner: eine Datei oder ein Unterordner.
struct Eintrag {
name: String,
ist_ordner: bool,
groesse: u64,
}
fn main() -> iced::Result {
iced::application(Faehrmann::neu, Faehrmann::update, Faehrmann::view)
.title("Fährmann")
.default_font(Font::MONOSPACE)
.window_size((1000.0, 640.0))
.run()
}
// Das Gedächtnis des Fährmanns: welcher Ordner offen ist und was darin liegt.
struct Faehrmann {
ordner: PathBuf,
eintraege: Vec<Eintrag>,
}
#[derive(Debug, Clone)]
enum Message {}
impl Faehrmann {
// Startet im Ordner, aus dem das Programm aufgerufen wurde.
fn neu() -> Self {
let ordner = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("/"));
let eintraege = ordner_lesen(&ordner);
Faehrmann { ordner, eintraege }
}
fn update(&mut self, message: Message) {
match message {}
}
fn view(&self) -> Element<'_, Message> {
let menuzeile = container(text(" Links Datei Befehle Optionen Rechts"))
.width(Fill)
.style(|_theme: &Theme| container::Style {
background: Some(Background::Color(TUERKIS)),
text_color: Some(SCHWARZ),
..container::Style::default()
});
let arbeitsflaeche = container(self.panel_ansicht())
.width(Fill)
.height(Fill)
.padding(6)
.style(|_theme: &Theme| container::Style {
background: Some(Background::Color(BLAU)),
text_color: Some(WEISS),
..container::Style::default()
});
let tastenleiste = row![
f_taste(1, "Hilfe"),
f_taste(2, "Menü"),
f_taste(3, "Ansicht"),
f_taste(4, "Bearb."),
f_taste(5, "Kopie"),
f_taste(6, "Versch."),
f_taste(7, "Ordner"),
f_taste(8, "Lösch."),
f_taste(9, "Leiste"),
f_taste(10, "Ende"),
]
.spacing(4);
let tastenleiste = container(tastenleiste)
.width(Fill)
.style(|_theme: &Theme| container::Style {
background: Some(Background::Color(SCHWARZ)),
..container::Style::default()
});
column![menuzeile, arbeitsflaeche, tastenleiste].into()
}
// Zeichnet das Panel: oben der Pfad, darunter die Einträge, rundherum ein Rahmen.
fn panel_ansicht(&self) -> Element<'_, Message> {
let kopf = text(self.ordner.display().to_string()).color(GELB);
let mut liste = column![];
for eintrag in self.eintraege.iter().take(ZEILEN) {
liste = liste.push(zeile(eintrag));
}
container(column![kopf, liste].spacing(6))
.width(Fill)
.height(Fill)
.padding(8)
.clip(true)
.style(|_theme: &Theme| container::Style {
border: Border {
color: TUERKIS,
width: 2.0,
radius: 0.0.into(),
},
..container::Style::default()
})
.into()
}
}
// Baut eine einzelne Taste der Funktionstastenleiste, z. B. "5 Kopie".
fn f_taste(nummer: u8, beschriftung: &'static str) -> Element<'static, Message> {
let zahl = text(nummer).color(WEISS);
let feld = container(text(beschriftung))
.width(Fill)
.style(|_theme: &Theme| container::Style {
background: Some(Background::Color(TUERKIS)),
text_color: Some(SCHWARZ),
..container::Style::default()
});
row![zahl, feld].width(Fill).into()
}
// Liest einen Ordner und liefert seine Einträge: oben "..", dann Ordner, dann Dateien.
fn ordner_lesen(pfad: &Path) -> Vec<Eintrag> {
let mut eintraege = Vec::new();
let Ok(inhalt) = fs::read_dir(pfad) else {
return eintraege; // Ordner nicht lesbar: leere Liste
};
for eintrag in inhalt.flatten() {
let name = eintrag.file_name().to_string_lossy().to_string();
if name.starts_with('.') {
continue; // versteckte Dateien überspringen
}
let Ok(info) = eintrag.metadata() else {
continue; // keine Infos lesbar: Eintrag überspringen
};
eintraege.push(Eintrag {
name,
ist_ordner: info.is_dir(),
groesse: info.len(),
});
}
// Ordner vor Dateien, innerhalb davon alphabetisch
eintraege.sort_by(|a, b| {
b.ist_ordner
.cmp(&a.ist_ordner)
.then(a.name.to_lowercase().cmp(&b.name.to_lowercase()))
});
// Der Weg nach oben steht immer ganz oben
if pfad.parent().is_some() {
let hoch = Eintrag {
name: String::from(".."),
ist_ordner: true,
groesse: 0,
};
eintraege.insert(0, hoch);
}
eintraege
}
// Baut eine Zeile im Panel: links der Name, rechts die Größe.
fn zeile(eintrag: &Eintrag) -> Element<'_, Message> {
let farbe = if eintrag.ist_ordner { WEISS } else { HELLTUERKIS };
let groesse = if eintrag.ist_ordner {
String::from("<ORDNER>")
} else {
groesse_text(eintrag.groesse)
};
row![
text(eintrag.name.as_str())
.color(farbe)
.width(Fill)
.wrapping(text::Wrapping::None),
text(groesse).color(farbe),
]
.spacing(12)
.into()
}
// Macht aus einer Zahl von Bytes etwas Lesbares, zum Beispiel "12,3 KB".
fn groesse_text(bytes: u64) -> String {
let b = bytes as f64;
let text = if bytes < 1024 {
format!("{} B", bytes)
} else if b < 1024.0 * 1024.0 {
format!("{:.1} KB", b / 1024.0)
} else if b < 1024.0 * 1024.0 * 1024.0 {
format!("{:.1} MB", b / 1024.0 / 1024.0)
} else {
format!("{:.1} GB", b / 1024.0 / 1024.0 / 1024.0)
};
text.replace('.', ",")
}
std::fs und std::path alles mit, um Ordner zu lesen. Dinge, die
schiefgehen können, liefern ein Result, und mit let … else
behandeln wir den Fehlerfall gleich an Ort und Stelle. PathBuf besitzt einen
Pfad, &Path leiht ihn sich aus. Mit sort_by sortieren wir nach
eigenen Regeln, und wer kein Default hat, bekommt eine eigene Funktion
neu.Zum Ausprobieren
Die Übungen sind freiwillig, aber sie helfen enorm. Mach nach jeder Übung einen Probelauf, und stell danach den alten Zustand wieder her, damit die nächste Etappe bei dir genauso weitergeht wie bei allen anderen.
- Alles zeigen: Lass den Fährmann auch versteckte Dateien anzeigen. Welche Zeilen musst du dafür löschen? Starte ihn danach in deinem Home-Ordner und staune.
- Großbuchstaben: Im Original standen Ordnernamen in GROSSBUCHSTABEN. Bau
das in
zeileein. Tipp: Es gibtto_uppercase(). - Zähler: Häng an den Pfad in der Kopfzeile die Anzahl der Einträge an,
etwa
/home/volker (31). Zählt der Eintrag..mit?
Lösungen anzeigen
1. Die vier Zeilen mit if name.starts_with('.') { continue; } samt Kommentar.
Im Home-Ordner tauchen dann Dutzende Einträge wie .bashrc oder
.config auf. Gut zu wissen, wo die Programme ihre Einstellungen
verstecken.
2. Den Namen vorher in eine Variable stecken: let name = if eintrag.ist_ordner {
eintrag.name.to_uppercase() } else { eintrag.name.clone() }; und dann
text(name) statt text(eintrag.name.as_str()). Weil
to_uppercase() einen neuen String baut, müssen wir für den anderen
Fall mit clone() auch einen bauen, denn beide Zweige müssen denselben Typ
liefern.
3. format!("{} ({})", self.ordner.display(), self.eintraege.len()). Ja,
.. zählt mit, denn er steht ja in der Liste. Wer ihn nicht mitzählen will,
zieht eins ab, aber Vorsicht: Ganz oben im Dateisystem gibt es keinen
..-Eintrag.
Etappe 2 geschafft 📂
Der Fährmann schaut jetzt in echte Ordner, sortiert, was er findet, und zeigt es im Commander-Look an. In der nächsten Etappe kommt Bewegung hinein: Mit den Pfeiltasten wanderst du durch die Liste, mit Enter öffnest du Ordner, und F10 beendet das Programm.
Hat dir das Mitbauen Spaß gemacht? Ein Kaffee hilft, dass die nächste Etappe ein Stück schneller fertig wird.
☕ Kaffee spendieren