Rust-Werkstatt
Werkstatt · Fährmann · Etappe 2 von 13

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.

5 Schritte · ca. 45 Minuten · baut auf Etappe 1 auf

Der Fährmann mit einem Panel, das Ordner in Weiß und Dateien in Türkis mit Größenangaben auflistet
So sieht der Fährmann am Ende dieser Etappe aus.
Vorab

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.

Schritt 1

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:

src/main.rszwischen den Farben und fn main()
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: String ist der Name, zum Beispiel Urlaubsvideo.mp4. Ein String ist ein Text, der dem Eintrag selbst gehört und beliebig lang sein darf.
  • ist_ordner: bool ist entweder true (Ordner) oder false (Datei).
  • groesse: u64 ist die Größe in Bytes. u64 ist eine sehr große ganze Zahl ohne Vorzeichen. Sie reicht für Dateien bis zu 18 Trillionen Bytes, also locker für jedes Urlaubsvideo.
Probelauf Speichern, dann im Terminal:
cargo run
Das Fenster sieht aus wie bisher. Rust meldet gelb, dass Eintrag nie benutzt wird. Stimmt, das kommt im nächsten Schritt.
Schritt 2

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:

src/main.rsganz oben, vor den bisherigen use-Zeilen
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:

src/main.rsganz am Ende der Datei
    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 ein Result, entweder Ok(…) mit dem Inhalt oder Err(…) mit einem Fehler.
  • let … else: let Ok(inhalt) = … else { … }; heißt: „Wenn das geklappt hat, nenn das Ergebnis inhalt und mach weiter. Sonst führ den else-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 einen String um. Das to_string_lossy sorgt 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 wieder let … else. Dann legen wir den fertigen Eintrag mit push in die Liste. Steht dort nur name,, ist das die Kurzform von name: name,.
  • Sortieren: sort_by bekommt eine Closure, die zwei Einträge a und b vergleicht. 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 mit pfad.parent().is_some(), ob es einen gibt. insert(0, …) setzt ihn an Platz 0, also ganz an den Anfang.
Warum b vor a? Beim Sortieren steht normalerweise 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.
Worauf du achten musst Achte auf das & in pfad: &Path. Die Funktion bekommt den Pfad nur ausgeliehen, sie muss ihn ja nur anschauen, nicht behalten. Mehr übers Ausleihen: Buch, Kapitel 45.
Probelauf Speichern, dann im Terminal:
cargo run
Alles sieht aus wie vorher, und Rust meldet jetzt, dass ordner_lesen nie benutzt wird. Das Werkzeug liegt bereit.
Schritt 3

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.

src/main.rsganz oben, zweite Zeile
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:

src/main.rsanstelle des alten struct Faehrmann
#[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:

src/main.rsin impl Faehrmann, als erste Funktion
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:

src/main.rsin fn main()
    iced::application(Faehrmann::default, Faehrmann::update, Faehrmann::view)    iced::application(Faehrmann::neu, Faehrmann::update, Faehrmann::view)
Probelauf Speichern, dann im Terminal:
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.
Schritt 4

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:

src/main.rsbei den Farben
const WEISS: Color = Color::WHITE;const HELLTUERKIS: Color = Color::from_rgb8(85, 255, 255);

Die beiden neuen Funktionen kommen ans Ende der Datei:

src/main.rsganz am 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 ein Element. 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, dass text zwei Dinge ist: die Funktion, die einen Text baut, und ein Modul, in dem Zubehör wie Wrapping liegt.

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.

Worauf du achten musst 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.
Probelauf Speichern, dann im Terminal:
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.
Schritt 5

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:

src/main.rsbei den Farben
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:

src/main.rsganz oben, zweite iced-Zeile
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:

src/main.rsin impl Faehrmann, hinter der Funktion view
        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 Border zeichnet 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:

src/main.rsin der Funktion view
        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)),
Worauf du achten musst .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.
Probelauf Speichern, dann im Terminal:
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 Fährmann mit einem Panel voller Dateien
Stand nach Schritt 5
Zwischenstand

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('.', ",")
}
ZusammenfassungRusts Standardbibliothek bringt mit 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.
Übungen

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.

  1. 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.
  2. Großbuchstaben: Im Original standen Ordnernamen in GROSSBUCHSTABEN. Bau das in zeile ein. Tipp: Es gibt to_uppercase().
  3. 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