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

Kopieren mit F5 – die erste Fracht

Jetzt wird der Fährmann nützlich: Ein Druck auf F5, und die markierte Datei wird ins andere Panel kopiert. Damit der Fährmann auch sagen kann, was er gerade getan hat (oder warum nicht), bekommt er eine Meldungszeile. Und weil beim Kopieren einiges schiefgehen kann, lernst du, wie man in Rust ehrlich mit Fehlern umgeht, statt sie zu verstecken.

4 Schritte · ca. 40 Minuten · baut auf Etappe 4 auf

Der Fährmann mit zwei Panels und einer Meldungszeile, die eine erfolgreich kopierte Datei meldet
So sieht der Fährmann am Ende dieser Etappe aus.
Vorab

Bevor wir loslegen

Wir machen da weiter, wo Etappe 4 aufgehört hat. Den kompletten Stand findest du dort unter Zwischenstand.

Kopieren klingt einfach: Datei nehmen, woanders hinlegen. Aber stell dir vor, was dabei alles passieren kann. Die Datei ist in Wahrheit ein Ordner. Am Ziel liegt schon eine Datei mit demselben Namen. Du darfst im Zielordner gar nicht schreiben. Die Festplatte ist voll. Ein gutes Programm stürzt in solchen Fällen nicht ab und tut auch nicht so, als wäre alles gut. Es sagt freundlich, was los ist. Genau dafür bauen wir zuerst die Meldungszeile.

Damit du gefahrlos üben kannst, leg dir am besten einen Spielordner an, zum Beispiel ~/faehrmann-test mit zwei Unterordnern links und rechts und ein paar Testdateien darin. Dann startest du den Fährmann dort und kopierst nach Herzenslust, ohne dass echte Dateien in Gefahr geraten.

Schritt 1

Eine Meldungszeile

Über der Funktionstastenleiste soll eine schwarze Zeile stehen, in der der Fährmann mit uns spricht. Im Original war an dieser Stelle die Befehlszeile von DOS. Zuerst bekommt das Gedächtnis ein neues Feld für den Text:

src/main.rsdas struct Faehrmann
    rechts: Panel,
    aktiv: Seite,    meldung: String, // was der Fährmann zuletzt zu sagen hatte}

Beim Start begrüßt der Fährmann seine Fahrgäste gleich mit einer kleinen Anleitung:

src/main.rsin der Funktion neu
            rechts: Panel::neu(ordner),
            aktiv: Seite::Links,            meldung: String::from("Willkommen an Bord! Tab wechselt das Panel, F5 kopiert, F10 beendet."),        }

In view bauen wir die Zeile direkt über der Tastenleiste und nehmen sie mit in die große Spalte auf:

src/main.rsin der Funktion view
                ..container::Style::default()
            });        let meldungszeile = container(text(self.meldung.as_str()).color(WEISS))
            .width(Fill)
            .padding([0, 6])
            .style(|_theme: &Theme| container::Style {
                background: Some(Background::Color(SCHWARZ)),
                ..container::Style::default()
            });        let tastenleiste = row![    ⋮        column![menuzeile, arbeitsflaeche, tastenleiste].into()        column![menuzeile, arbeitsflaeche, meldungszeile, tastenleiste].into()

.padding([0, 6]) ist eine Kurzschreibweise für zwei verschiedene Abstände: 0 Pixel oben und unten, 6 Pixel links und rechts. So klebt der Text nicht am Fensterrand.

Probelauf Speichern, dann im Terminal:
cargo run
Über der Tastenleiste steht jetzt in Weiß auf Schwarz der Willkommensgruß. Die Panels sind dafür ein kleines Stück kürzer geworden, haben aber immer noch Platz für 24 Zeilen.
Schritt 2

Ein paar Handgriffe vorbereiten

Fürs Kopieren brauchen wir ein paar kleine Helfer. Das Panel soll uns sagen können, auf welchem Eintrag der Balken steht, und es soll seinen Ordner neu einlesen können, denn nach dem Kopieren liegt im Zielordner ja eine Datei mehr. Beides kommt in impl Panel, hinter runter:

src/main.rsin impl Panel, direkt hinter runter
        if self.cursor + 1 < self.eintraege.len() {
            self.cursor += 1;
        }
        self.sichtbar_halten();
    }    // Liefert den Eintrag, auf dem der Balken steht, falls es einen gibt.
    fn markierter_eintrag(&self) -> Option<&Eintrag> {
        self.eintraege.get(self.cursor)
    }

    // Liest den Ordner neu ein, zum Beispiel nachdem eine Datei dazugekommen ist.
    fn neu_lesen(&mut self) {
        self.eintraege = ordner_lesen(&self.ordner);
        if self.cursor >= self.eintraege.len() {
            self.cursor = self.eintraege.len().saturating_sub(1);
        }
        self.sichtbar_halten();
    }
  • markierter_eintrag liefert eine Option<&Eintrag>: vielleicht einen ausgeliehenen Eintrag, vielleicht keinen (in einem leeren Ordner).
  • neu_lesen liest den Ordner frisch ein. Falls die Liste dabei kürzer geworden ist, zum Beispiel weil jemand anderes eine Datei gelöscht hat, rückt der Balken auf den letzten Eintrag. saturating_sub(1) zieht eins ab, bleibt aber bei 0 stehen, wenn die Liste leer ist. Ein normales - 1 würde dort abstürzen.

Und Faehrmann bekommt zwei Helfer hinter aktives_panel:

src/main.rsin impl Faehrmann, hinter aktives_panel
            Seite::Links => &mut self.links,
            Seite::Rechts => &mut self.rechts,
        }
    }    // Liefert das Panel, das gerade NICHT aktiv ist, zum Verändern.
    fn inaktives_panel(&mut self) -> &mut Panel {
        match self.aktiv {
            Seite::Links => &mut self.rechts,
            Seite::Rechts => &mut self.links,
        }
    }

    // Liefert beide Panels zum Anschauen: zuerst das aktive, dann das andere.
    fn quelle_und_ziel(&self) -> (&Panel, &Panel) {
        match self.aktiv {
            Seite::Links => (&self.links, &self.rechts),
            Seite::Rechts => (&self.rechts, &self.links),
        }
    }

inaktives_panel ist das Spiegelbild von aktives_panel. quelle_und_ziel liefert gleich beide Panels auf einmal, als Tupel: zwei Werte in runden Klammern, die zusammen zurückgegeben werden. Diesmal nur zum Anschauen (&, nicht &mut), denn beim Kopieren lesen wir aus beiden Panels nur den Ordner und den Eintrag.

Probelauf Speichern, dann im Terminal:
cargo run
Äußerlich ändert sich nichts. Rust warnt, dass die vier neuen Helfer nie benutzt werden. Im nächsten Schritt kommen sie alle zum Einsatz.
Schritt 3

Das Kopieren selbst

Jetzt das Herzstück der Etappe. Die Funktion kopieren kommt hinter quelle_und_ziel:

src/main.rsin impl Faehrmann, hinter quelle_und_ziel
            Seite::Links => (&self.links, &self.rechts),
            Seite::Rechts => (&self.rechts, &self.links),
        }
    }    // Kopiert die markierte Datei vom aktiven Panel in den Ordner des anderen Panels.
    fn kopieren(&mut self) {
        let (quelle, ziel) = self.quelle_und_ziel();
        let Some(eintrag) = quelle.markierter_eintrag() else {
            return;
        };
        if eintrag.name == ".." {
            return;
        }

        // Alles, was wir brauchen, herausschreiben, solange wir die Panels ausleihen
        let name = eintrag.name.clone();
        let ist_ordner = eintrag.ist_ordner;
        let von = quelle.ordner.join(&name);
        let nach = ziel.ordner.join(&name);
        let zielordner = ziel.ordner.display().to_string();

        self.meldung = if ist_ordner {
            format!("„{}“ ist ein Ordner. Ordner kann der Fährmann noch nicht kopieren.", name)
        } else if nach.exists() {
            format!("„{}“ gibt es in {} schon. Nichts überschrieben.", name, zielordner)
        } else {
            match fs::copy(&von, &nach) {
                Ok(_) => format!("Kopiert: {} → {}", name, zielordner),
                Err(fehler) => format!("Kopieren fehlgeschlagen: {}", fehler),
            }
        };

        self.inaktives_panel().neu_lesen();
    }

Gehen wir die Geschichte durch:

  • Was ist markiert? Wir holen uns beide Panels und den markierten Eintrag der Quelle. Ist nichts markiert oder steht der Balken auf .., gibt es nichts zu kopieren, und wir hören leise auf.
  • Herausschreiben: Wir kopieren Name, Ordner-Eigenschaft und die beiden fertigen Pfade in eigene Variablen. Warum, erklärt der Kasten unten.
  • Drei Fälle prüfen: Ist es ein Ordner? Dann sagen wir ehrlich, dass das (noch) nicht geht. Gibt es die Datei am Ziel schon? Dann überschreiben wir sie nicht, denn das könnte wertvolle Daten zerstören. Erst wenn beides nicht zutrifft, kopieren wir.
  • Kopieren: fs::copy(&von, &nach) kopiert die Datei. Auch das liefert ein Result, und diesmal behandeln wir beide Fälle mit match: Ok ergibt eine Erfolgsmeldung, Err eine Fehlermeldung, in die wir den Fehler selbst einsetzen. Was Ok mitliefert (die Zahl der kopierten Bytes), brauchen wir nicht, daher Ok(_).
  • Neu einlesen: Zum Schluss liest das Zielpanel seinen Ordner neu ein, damit die neue Datei auch zu sehen ist.
Warum erst alles herausschreiben? quelle, ziel und eintrag sind Ausleihen aus self. Solange wir sie benutzen, ist self sozusagen verliehen, und Rust erlaubt nicht, gleichzeitig etwas an self zu ändern, etwa self.meldung. Deshalb schreiben wir uns alles heraus, was wir später noch brauchen, als eigene Werte. Nach der Zeile mit zielordner benutzen wir keine der Ausleihen mehr, und Rust weiß: Ab hier ist self wieder frei. Dieses „Erst lesen, dann schreiben“ wirst du in Rust noch oft sehen.
Worauf du achten musst Die Anführungszeichen „ und “ in den Meldungen sind deutsche Anführungszeichen, keine normalen ". Rust stört sich nicht daran, denn innerhalb eines Textes sind alle Zeichen erlaubt. Tippst du dort aus Versehen ein normales ", endet der Text an dieser Stelle, und Rust meldet einen Fehler.
Probelauf Speichern, dann im Terminal:
cargo run
Rust warnt jetzt, dass kopieren nie aufgerufen wird, und damit natürlich auch die Helfer aus Schritt 2 nicht. Es fehlt nur noch ein Tastendruck.
Schritt 4

F5 drücken

Die letzten Handgriffe sind dir inzwischen vertraut: Briefsorte, Weiterleitung, Taste.

src/main.rsdas enum Message
    Wechseln,    Kopieren,    Beenden,
src/main.rsin der Funktion update
                    Seite::Rechts => Seite::Links,
                };
            }            Message::Kopieren => self.kopieren(),            Message::Beenden => return iced::exit(),
src/main.rsin der Funktion taste
        Key::Named(Named::Tab) => Some(Message::Wechseln),        Key::Named(Named::F5) => Some(Message::Kopieren),
Probelauf Speichern, dann im Terminal:
cargo run
Jetzt wird's spannend. Geh im rechten Panel in einen anderen Ordner, wechsle mit Tab nach links, setz den Balken auf eine Datei und drück F5. Unten steht Kopiert: …, und im rechten Panel taucht die Datei auf. Probier auch die Sonderfälle aus: F5 auf einem Ordner, und F5 noch einmal auf derselben Datei. Jedes Mal sagt dir der Fährmann freundlich, was los ist.
Zwei Panels und eine Erfolgsmeldung nach dem Kopieren
Stand nach Schritt 4: die erste Fracht ist angekommen
Halbzeit

Was du bis hierher gebaut hast

Halte kurz inne und schau, was aus dem blauen Fenster aus Etappe 1 geworden ist: ein Dateimanager mit zwei Panels, Tastatursteuerung, mitlaufender Liste und einer Kopierfunktion, die ehrlich sagt, was sie tut. Das ist ein Programm, das man wirklich benutzen kann, und du hast jede Zeile davon selbst geschrieben.

Bevor die Fähre ablegt, bekommt sie noch zwei wichtige Fähigkeiten: Sie kopiert ganze Ordner, und sie kennt und bewahrt die Zugriffsrechte von Dateien. Danach geht sie aufs Wasser. Das rechte Panel zeigt dann nicht mehr einen Ordner auf deinem Rechner, sondern einen auf einem Server, erst per FTP und später verschlüsselt per SFTP. Damit kommt eine neue Herausforderung: Ein Server antwortet nicht sofort, und während der Fährmann wartet, darf das Fenster nicht einfrieren. Dafür lernen wir asynchrones Programmieren kennen, und das Gute ist: Du hast mit Task schon den ersten Schritt dahin gemacht.

Zwischenstand

Der komplette Code nach Etappe 5

Hier ist der vollständige Stand nach Etappe 5. 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::keyboard::{self, Key, key::Named};
use iced::{Background, Border, Color, Element, Fill, Font, Subscription, Task, 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)
        .subscription(Faehrmann::subscription)
        .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 {
    links: Panel,
    rechts: Panel,
    aktiv: Seite,
    meldung: String, // was der Fährmann zuletzt zu sagen hatte
}

// Welches der beiden Panels ist gerade aktiv?
#[derive(PartialEq)]
enum Seite {
    Links,
    Rechts,
}

#[derive(Debug, Clone)]
enum Message {
    Hoch,
    Runter,
    Oeffnen,
    Wechseln,
    Kopieren,
    Beenden,
}

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("/"));
        Faehrmann {
            links: Panel::neu(ordner.clone()),
            rechts: Panel::neu(ordner),
            aktiv: Seite::Links,
            meldung: String::from("Willkommen an Bord! Tab wechselt das Panel, F5 kopiert, F10 beendet."),
        }
    }

    fn update(&mut self, message: Message) -> Task<Message> {
        match message {
            Message::Hoch => self.aktives_panel().hoch(),
            Message::Runter => self.aktives_panel().runter(),
            Message::Oeffnen => self.aktives_panel().oeffnen(),
            Message::Wechseln => {
                self.aktiv = match self.aktiv {
                    Seite::Links => Seite::Rechts,
                    Seite::Rechts => Seite::Links,
                };
            }
            Message::Kopieren => self.kopieren(),
            Message::Beenden => return iced::exit(),
        }
        Task::none()
    }

    // Abonniert die Tastatur: Jeder Tastendruck geht durch die Funktion taste.
    fn subscription(&self) -> Subscription<Message> {
        keyboard::listen().filter_map(taste)
    }

    // Liefert das Panel, das gerade aktiv ist, zum Verändern.
    fn aktives_panel(&mut self) -> &mut Panel {
        match self.aktiv {
            Seite::Links => &mut self.links,
            Seite::Rechts => &mut self.rechts,
        }
    }

    // Liefert das Panel, das gerade NICHT aktiv ist, zum Verändern.
    fn inaktives_panel(&mut self) -> &mut Panel {
        match self.aktiv {
            Seite::Links => &mut self.rechts,
            Seite::Rechts => &mut self.links,
        }
    }

    // Liefert beide Panels zum Anschauen: zuerst das aktive, dann das andere.
    fn quelle_und_ziel(&self) -> (&Panel, &Panel) {
        match self.aktiv {
            Seite::Links => (&self.links, &self.rechts),
            Seite::Rechts => (&self.rechts, &self.links),
        }
    }

    // Kopiert die markierte Datei vom aktiven Panel in den Ordner des anderen Panels.
    fn kopieren(&mut self) {
        let (quelle, ziel) = self.quelle_und_ziel();
        let Some(eintrag) = quelle.markierter_eintrag() else {
            return;
        };
        if eintrag.name == ".." {
            return;
        }

        // Alles, was wir brauchen, herausschreiben, solange wir die Panels ausleihen
        let name = eintrag.name.clone();
        let ist_ordner = eintrag.ist_ordner;
        let von = quelle.ordner.join(&name);
        let nach = ziel.ordner.join(&name);
        let zielordner = ziel.ordner.display().to_string();

        self.meldung = if ist_ordner {
            format!("„{}“ ist ein Ordner. Ordner kann der Fährmann noch nicht kopieren.", name)
        } else if nach.exists() {
            format!("„{}“ gibt es in {} schon. Nichts überschrieben.", name, zielordner)
        } else {
            match fs::copy(&von, &nach) {
                Ok(_) => format!("Kopiert: {} → {}", name, zielordner),
                Err(fehler) => format!("Kopieren fehlgeschlagen: {}", fehler),
            }
        };

        self.inaktives_panel().neu_lesen();
    }

    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 panels = row![
            self.links.ansicht(self.aktiv == Seite::Links),
            self.rechts.ansicht(self.aktiv == Seite::Rechts),
        ]
        .spacing(6);

        let arbeitsflaeche = container(panels)
            .width(Fill)
            .height(Fill)
            .padding(6)
            .style(|_theme: &Theme| container::Style {
                background: Some(Background::Color(BLAU)),
                text_color: Some(WEISS),
                ..container::Style::default()
            });

        let meldungszeile = container(text(self.meldung.as_str()).color(WEISS))
            .width(Fill)
            .padding([0, 6])
            .style(|_theme: &Theme| container::Style {
                background: Some(Background::Color(SCHWARZ)),
                ..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, meldungszeile, tastenleiste].into()
    }
}

// Ein Panel: ein Ordner, seine Einträge und der Auswahlbalken.
struct Panel {
    ordner: PathBuf,
    eintraege: Vec<Eintrag>,
    cursor: usize,      // Nummer des markierten Eintrags
    erste_zeile: usize, // Nummer des obersten sichtbaren Eintrags
}

impl Panel {
    // Legt ein Panel an, das den angegebenen Ordner zeigt.
    fn neu(ordner: PathBuf) -> Self {
        let eintraege = ordner_lesen(&ordner);
        Panel {
            ordner,
            eintraege,
            cursor: 0,
            erste_zeile: 0,
        }
    }

    // Balken eine Zeile nach oben
    fn hoch(&mut self) {
        if self.cursor > 0 {
            self.cursor -= 1;
        }
        self.sichtbar_halten();
    }

    // Balken eine Zeile nach unten
    fn runter(&mut self) {
        if self.cursor + 1 < self.eintraege.len() {
            self.cursor += 1;
        }
        self.sichtbar_halten();
    }

    // Liefert den Eintrag, auf dem der Balken steht, falls es einen gibt.
    fn markierter_eintrag(&self) -> Option<&Eintrag> {
        self.eintraege.get(self.cursor)
    }

    // Liest den Ordner neu ein, zum Beispiel nachdem eine Datei dazugekommen ist.
    fn neu_lesen(&mut self) {
        self.eintraege = ordner_lesen(&self.ordner);
        if self.cursor >= self.eintraege.len() {
            self.cursor = self.eintraege.len().saturating_sub(1);
        }
        self.sichtbar_halten();
    }

    // Schiebt den sichtbaren Ausschnitt so, dass der Balken immer zu sehen ist.
    fn sichtbar_halten(&mut self) {
        if self.cursor < self.erste_zeile {
            self.erste_zeile = self.cursor;
        }
        if self.cursor >= self.erste_zeile + ZEILEN {
            self.erste_zeile = self.cursor + 1 - ZEILEN;
        }
    }

    // Betritt den markierten Ordner, oder geht bei ".." eine Ebene hoch.
    fn oeffnen(&mut self) {
        let Some(eintrag) = self.eintraege.get(self.cursor) else {
            return;
        };
        if !eintrag.ist_ordner {
            return; // Dateien öffnen wir (noch) nicht
        }

        let neuer_ordner = if eintrag.name == ".." {
            match self.ordner.parent() {
                Some(oben) => oben.to_path_buf(),
                None => return,
            }
        } else {
            self.ordner.join(&eintrag.name)
        };

        self.eintraege = ordner_lesen(&neuer_ordner);
        self.ordner = neuer_ordner;
        self.cursor = 0;
        self.erste_zeile = 0;
    }

    // Zeichnet das Panel. Nur das aktive Panel zeigt den Balken und einen hellen Kopf.
    fn ansicht(&self, aktiv: bool) -> Element<'_, Message> {
        let kopffarbe = if aktiv { SCHWARZ } else { GELB };
        let kopf = container(text(self.ordner.display().to_string()).color(kopffarbe))
            .style(move |_theme: &Theme| container::Style {
                background: if aktiv {
                    Some(Background::Color(TUERKIS))
                } else {
                    None
                },
                ..container::Style::default()
            });

        let mut liste = column![];
        for (nr, eintrag) in self
            .eintraege
            .iter()
            .enumerate()
            .skip(self.erste_zeile)
            .take(ZEILEN)
        {
            liste = liste.push(zeile(eintrag, aktiv && nr == self.cursor));
        }

        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, markiert: bool) -> Element<'_, Message> {
    let farbe = if markiert {
        SCHWARZ
    } else if eintrag.ist_ordner {
        WEISS
    } else {
        HELLTUERKIS
    };
    let groesse = if eintrag.ist_ordner {
        String::from("<ORDNER>")
    } else {
        groesse_text(eintrag.groesse)
    };

    let inhalt = row![
        text(eintrag.name.as_str())
            .color(farbe)
            .width(Fill)
            .wrapping(text::Wrapping::None),
        text(groesse).color(farbe),
    ]
    .spacing(12);

    // Der markierte Eintrag bekommt den türkisen Balken
    container(inhalt)
        .width(Fill)
        .style(move |_theme: &Theme| container::Style {
            background: if markiert {
                Some(Background::Color(TUERKIS))
            } else {
                None
            },
            ..container::Style::default()
        })
        .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('.', ",")
}

// Übersetzt einen Tastendruck in eine Nachricht, oder in gar nichts.
fn taste(ereignis: keyboard::Event) -> Option<Message> {
    let keyboard::Event::KeyPressed { key, .. } = ereignis else {
        return None; // Taste losgelassen oder Ähnliches: interessiert uns nicht
    };

    match key {
        Key::Named(Named::ArrowUp) => Some(Message::Hoch),
        Key::Named(Named::ArrowDown) => Some(Message::Runter),
        Key::Named(Named::Enter) => Some(Message::Oeffnen),
        Key::Named(Named::Tab) => Some(Message::Wechseln),
        Key::Named(Named::F5) => Some(Message::Kopieren),
        Key::Named(Named::F10) => Some(Message::Beenden),
        _ => None,
    }
}
ZusammenfassungEin gutes Programm behandelt Fehler sichtbar: Mit match über ein Result machen wir aus Ok und Err verständliche Meldungen. Gefährliche Aktionen wie Überschreiben prüfen wir vorher. Option<&T> liefert vielleicht eine Ausleihe, saturating_sub rechnet sicher an der Null, und ein Tupel gibt mehrere Werte auf einmal zurück. Und wenn Ausleihen und Ändern sich in die Quere kommen, hilft „erst alles herauslesen, dann ändern“.
Ü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. F8 löscht: Bau eine Löschfunktion für F8 mit fs::remove_file. Achtung: Lösch dabei nur in deinem Spielordner! Wie meldest du Erfolg und Fehler?
  2. Wie viel? fs::copy liefert im Erfolgsfall die Zahl der kopierten Bytes. Bau sie in die Erfolgsmeldung ein, schön lesbar mit groesse_text.
  3. Gleicher Ordner: Was passiert, wenn beide Panels denselben Ordner zeigen und du F5 drückst? Warum ist das gut so?
Lösungen anzeigen

1. Eine Nachricht Loeschen, Named::F8 in taste, und eine Funktion, die wie kopieren beginnt (nur ohne Ziel), dann match fs::remove_file(&pfad) { Ok(()) => format!("Gelöscht: {}", name), Err(fehler) => format!("Löschen fehlgeschlagen: {}", fehler) } und danach self.aktives_panel().neu_lesen();. Ein echter Commander würde vorher nachfragen. Dafür bräuchten wir einen Dialog, eine schöne Aufgabe für später.

2. Ok(bytes) => format!("Kopiert: {} ({}) → {}", name, groesse_text(bytes), zielordner),. Statt Ok(_) geben wir dem Wert einen Namen, und schon können wir ihn benutzen.

3. Die Datei gibt es am Ziel natürlich schon, denn es ist ja dieselbe. Der Fährmann meldet „gibt es … schon“ und kopiert nichts. Ohne diese Prüfung würde fs::copy versuchen, eine Datei auf sich selbst zu kopieren, und das kann je nach Betriebssystem die Datei leeren. Die Vorsichtsmaßnahme schützt also gleich doppelt.

Etappe 5 geschafft 📦

Die erste Fracht ist angekommen: Der Fährmann kopiert Dateien zwischen zwei Panels, prüft vorher, ob das sicher ist, und erzählt dir in der Meldungszeile, was er getan hat. In der nächsten Etappe lernt er, auch ganze Ordner zu kopieren, mit allem, was darin liegt.

Hat dir das Mitbauen Spaß gemacht? Ein Kaffee hilft, dass die nächste Etappe ein Stück schneller fertig wird.

☕ Kaffee spendieren