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

Zwei Panels und die Tab-Taste

Ein Commander mit nur einem Panel ist wie eine Fähre mit nur einem Ufer. Heute bekommt der Fährmann sein zweites Panel. Dafür räumen wir zuerst auf: Alles, was ein Panel ausmacht, ziehen wir in einen eigenen Baustein um. Danach ist ein zweites Panel nur noch eine Zeile Arbeit. Und mit der Tab-Taste springst du zwischen links und rechts hin und her.

4 Schritte · ca. 45 Minuten · baut auf Etappe 3 auf

Der Fährmann mit zwei Panels nebeneinander, das linke aktiv mit hervorgehobenem Pfad und Auswahlbalken
So sieht der Fährmann am Ende dieser Etappe aus.
Vorab

Bevor wir loslegen

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

Diese Etappe ist etwas Besonderes: In den ersten beiden Schritten bauen wir um, ohne dass sich am Programm irgendetwas sichtbar ändert. Das nennt man Refactoring, auf Deutsch etwa „Umbauen“. Es ist wie beim Umzug in eine größere Wohnung: Die Möbel bleiben dieselben, sie stehen nur an einem besseren Ort. Am Ende des Umzugs muss alles genauso funktionieren wie vorher. Und erst dann stellen wir neue Möbel dazu.

Warum der Aufwand? Im Moment speichert Faehrmann direkt einen Ordner, einen Balken und so weiter. Für zwei Panels bräuchten wir alles doppelt: ordner_links, ordner_rechts, cursor_links… Das wird schnell unübersichtlich. Besser ist ein eigener Bauplan Panel, von dem wir dann einfach zwei Stück bauen.

Schritt 1

Ein Bauplan für Panels

Wir schreiben ein neues struct Panel mit eigenem impl. Das meiste davon kennst du schon: Die Felder und die Funktionen sichtbar_halten und oeffnen sind genau dieselben wie in Faehrmann, Wort für Wort. Sie bekommen nur ein neues Zuhause. Tipp also ruhig ab, was neu ist, und kopier, was gleich bleibt. Der ganze Block kommt hinter die letzte geschweifte Klammer von impl Faehrmann, vor f_taste:

src/main.rszwischen impl Faehrmann und der Funktion f_taste
            .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();
    }

    // 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".

Was ist neu?

  • neu(ordner) bekommt den Ordner, den das Panel zeigen soll, als Wert mit. So können wir später zwei Panels mit verschiedenen Ordnern anlegen.
  • hoch und runter sind das, was bisher direkt in update stand. Als eigene Funktionen gehören sie jetzt zum Panel: Ein Panel weiß selbst, wie man seinen Balken bewegt.
  • ansicht(aktiv) ist die alte panel_ansicht mit einem Zusatz: Der Wert aktiv sagt, ob dieses Panel gerade das aktive ist. Nur dann bekommt der Pfad oben einen türkisen Hintergrund, und nur dann wird der Balken gezeichnet. Das aktiv && nr == self.cursor heißt: „markiert ist eine Zeile nur, wenn das Panel aktiv ist und der Balken auf ihr steht.“ So war es auch im Original: Man sieht sofort, wo man gerade ist.
Probelauf Speichern, dann im Terminal:
cargo run
Das Programm läuft genauso wie vorher, denn das neue Panel benutzt noch niemand. Rust warnt, dass Panel nie gebaut wird und seine Funktionen nie aufgerufen werden. Das ändern wir jetzt mit dem eigentlichen Umzug.
Schritt 2

Der Umzug

Jetzt ziehen wir um. Faehrmann merkt sich nicht mehr selbst Ordner, Einträge und Balken, sondern hat ein Panel, und das Panel kümmert sich um den Rest:

src/main.rsdas struct Faehrmann
// Das Gedächtnis des Fährmanns: welcher Ordner offen ist und was darin liegt.
struct Faehrmann {    ordner: PathBuf,
    eintraege: Vec<Eintrag>,
    cursor: usize,      // Nummer des markierten Eintrags
    erste_zeile: usize, // Nummer des obersten sichtbaren Eintrags    panel: Panel,}

neu wird dadurch kürzer. Den Startordner ermitteln wir wie bisher, aber das Einlesen erledigt jetzt Panel::neu:

src/main.rsin impl Faehrmann, die Funktion neu
        let ordner = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("/"));        let eintraege = ordner_lesen(&ordner);
        Faehrmann {
            ordner,
            eintraege,
            cursor: 0,
            erste_zeile: 0,
        }        Faehrmann {
            panel: Panel::neu(ordner),
        }    }

In update leiten wir die Briefe an das Panel weiter. Aus jeweils fünf Zeilen wird eine:

src/main.rsin impl Faehrmann, die Funktion update
        match message {            Message::Hoch => {
                if self.cursor > 0 {
                    self.cursor -= 1;
                }
                self.sichtbar_halten();
            }
            Message::Runter => {
                if self.cursor + 1 < self.eintraege.len() {
                    self.cursor += 1;
                }
                self.sichtbar_halten();
            }
            Message::Oeffnen => self.oeffnen(),            Message::Hoch => self.panel.hoch(),
            Message::Runter => self.panel.runter(),
            Message::Oeffnen => self.panel.oeffnen(),            Message::Beenden => return iced::exit(),

Die Funktionen sichtbar_halten und oeffnen in impl Faehrmann werden nicht mehr gebraucht, das Panel hat ja seine eigenen. Lösch sie:

src/main.rsin impl Faehrmann, hinter subscription
        keyboard::listen().filter_map(taste)
    }    // 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;
    }    fn view(&self) -> Element<'_, Message> {

Genauso panel_ansicht ganz unten in impl Faehrmann:

src/main.rsin impl Faehrmann, ganz am Ende
        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 (nr, eintrag) in self
            .eintraege
            .iter()
            .enumerate()
            .skip(self.erste_zeile)
            .take(ZEILEN)
        {
            liste = liste.push(zeile(eintrag, 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()
    }}

Und zum Schluss zeichnet view das neue Panel. Weil es im Moment das einzige ist, ist es natürlich aktiv:

src/main.rsin der Funktion view
        let arbeitsflaeche = container(self.panel_ansicht())        let arbeitsflaeche = container(self.panel.ansicht(true))            .width(Fill)
Der Compiler als Umzugshelfer Bei einem Umzug wie diesem ist Rusts strenger Compiler ein Segen. Vergisst du eine Stelle, zum Beispiel in update noch self.cursor statt self.panel…, meldet er sofort no field `cursor` on type `&mut Faehrmann`. Du kannst dich also von Fehler zu Fehler hangeln, bis alles wieder passt. In vielen anderen Sprachen merkt man so etwas erst, wenn das Programm läuft, oder gar nicht.
Probelauf Speichern, dann im Terminal:
cargo run
Alles funktioniert genauso wie am Ende von Etappe 3: Pfeiltasten, Enter, F10. Der Pfad oben ist jetzt türkis hinterlegt, weil das Panel aktiv ist. Und im Terminal steht keine einzige Warnung. Genau so soll ein Umzug enden: Alles steht an seinem neuen Platz, und nichts ist kaputt.
Schritt 3

Zwei Panels

Jetzt zahlt sich der Umzug aus. Der Fährmann bekommt zwei Panels und muss sich merken, welches davon gerade aktiv ist. „Links oder rechts“ ist wieder eine Sache mit festen Möglichkeiten, also ein enum:

src/main.rszwischen struct Faehrmann und enum Message
    panel: Panel,
}// Welches der beiden Panels ist gerade aktiv?
#[derive(PartialEq)]
enum Seite {
    Links,
    Rechts,
}#[derive(Debug, Clone)]
enum Message {    ⋮    panel: Panel,    links: Panel,
    rechts: Panel,
    aktiv: Seite,}

Mit #[derive(PartialEq)] können wir später mit == fragen, ob eine Seite aktiv ist. Beide Panels starten im selben Ordner, das linke ist aktiv:

src/main.rsin impl Faehrmann, die Funktion neu
        Faehrmann {            panel: Panel::neu(ordner),            links: Panel::neu(ordner.clone()),
            rechts: Panel::neu(ordner),
            aktiv: Seite::Links,        }
    }

Warum ordner.clone() beim linken Panel? Weil Panel::neu den Pfad behält. Gäben wir dem linken Panel den Pfad selbst, gehörte er danach dem linken Panel, und für das rechte bliebe keiner übrig. Mit clone() bekommt das linke eine Kopie, und das rechte bekommt das Original.

In update soll jede Taste auf das aktive Panel wirken. Dafür schreiben wir eine kleine Hilfsfunktion, die genau dieses Panel liefert:

src/main.rsin impl Faehrmann, hinter subscription
        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,
        }
    }

Sie gibt &mut Panel zurück, also eine Ausleihe zum Verändern, entweder vom linken oder vom rechten Panel. Damit wird update so lesbar wie ein Satz:

src/main.rsin der Funktion update
            Message::Hoch => self.panel.hoch(),
            Message::Runter => self.panel.runter(),
            Message::Oeffnen => self.panel.oeffnen(),            Message::Hoch => self.aktives_panel().hoch(),
            Message::Runter => self.aktives_panel().runter(),
            Message::Oeffnen => self.aktives_panel().oeffnen(),

Und view stellt beide Panels nebeneinander. Jedes bekommt mitgeteilt, ob es aktiv ist:

src/main.rsin der Funktion view
        let arbeitsflaeche = container(self.panel.ansicht(true))        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)
Probelauf Speichern, dann im Terminal:
cargo run
Zwei Panels! Beide zeigen denselben Ordner, aber nur das linke hat den hellen Pfad und den Balken. Pfeiltasten und Enter wirken auf das linke Panel. Zum rechten kommst du noch nicht, das ist der nächste Schritt.
Schritt 4

Hin und her mit Tab

Im Commander wechselt man mit der Tab-Taste zwischen den Panels. Das sind drei kleine Ergänzungen, die du inzwischen fast im Schlaf kennst. Eine Briefsorte:

src/main.rsdas enum Message
    Oeffnen,    Wechseln,    Beenden,

Was beim Brief passiert:

src/main.rsin der Funktion update
            Message::Oeffnen => self.aktives_panel().oeffnen(),            Message::Wechseln => {
                self.aktiv = match self.aktiv {
                    Seite::Links => Seite::Rechts,
                    Seite::Rechts => Seite::Links,
                };
            }            Message::Beenden => return iced::exit(),

Das match liefert hier einen Wert: Ist links aktiv, wird rechts aktiv, und umgekehrt. Und die Übersetzung der Taste:

src/main.rsin der Funktion taste
        Key::Named(Named::Enter) => Some(Message::Oeffnen),        Key::Named(Named::Tab) => Some(Message::Wechseln),
Probelauf Speichern, dann im Terminal:
cargo run
Drück Tab: Der helle Pfad und der Balken springen ins rechte Panel. Geh dort in einen anderen Ordner. Drück wieder Tab, und du bist links, genau dort, wo du warst. Jedes Panel merkt sich seinen eigenen Ordner und seinen eigenen Balken. Das ist der Lohn für den Umzug.
Zwei Panels mit verschiedenen Ordnern
Stand nach Schritt 4: links der Startordner, rechts ein Unterordner
Zwischenstand

Der komplette Code nach Etappe 4

Hier ist der vollständige Stand nach Etappe 4. 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,
}

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

#[derive(Debug, Clone)]
enum Message {
    Hoch,
    Runter,
    Oeffnen,
    Wechseln,
    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,
        }
    }

    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::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,
        }
    }

    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 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()
    }
}

// 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();
    }

    // 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::F10) => Some(Message::Beenden),
        _ => None,
    }
}
ZusammenfassungEin Refactoring baut Code um, ohne sein Verhalten zu ändern, und Rusts Compiler zeigt dabei jede Stelle, die noch angepasst werden muss. Ein eigenes struct mit eigenem impl bündelt alles, was zu einem Ding gehört, sodass man es mehrfach bauen kann. clone() macht eine Kopie, wenn zwei Besitzer dasselbe brauchen. Und eine Funktion kann mit &mut eine veränderbare Ausleihe zurückgeben, je nach Lage von verschiedenen Feldern.
Ü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. Rechts startet zu Hause: Lass das rechte Panel im Home-Ordner starten statt im Startordner. Tipp: std::env::home_dir() liefert eine Option<PathBuf>.
  2. Gleicher Ordner: Bau eine Taste (zum Beispiel F9), die das inaktive Panel auf den Ordner des aktiven setzt. Praktisch, wenn man in beiden Panels am selben Ort weiterarbeiten will.
  3. Wo bin ich? Schreib eine Funktion inaktives_panel, die das jeweils andere Panel liefert. Wie unterscheidet sie sich von aktives_panel?
Lösungen anzeigen

1. In neu: let zuhause = std::env::home_dir().unwrap_or(ordner.clone()); und dann rechts: Panel::neu(zuhause),. unwrap_or nimmt im Notfall den Startordner. Die Funktion home_dir war in älteren Rust-Versionen als veraltet markiert. In aktuellen Versionen ist das nicht mehr so, und Rust warnt nicht.

2. Eine Nachricht Angleichen, eine Zeile in taste mit Named::F9, und in update: let ordner = self.aktives_panel().ordner.clone(); und danach je nach self.aktiv self.rechts = Panel::neu(ordner) oder self.links = Panel::neu(ordner). Das clone() ist nötig, weil wir den Ordner aus dem einen Panel lesen und damit ein anderes bauen.

3. Fast gleich, nur mit vertauschten Zweigen: Seite::Links => &mut self.rechts, Seite::Rechts => &mut self.links,. Brauchen werden wir sie in der nächsten Etappe beim Kopieren, dann allerdings in einer etwas anderen Form.

Etappe 4 geschafft ⇆

Der Fährmann hat zwei Ufer: zwei unabhängige Panels, zwischen denen du mit Tab hin- und herspringst. Und nebenbei hast du deinen ersten großen Umbau gemeistert. In der nächsten Etappe nimmt der Fährmann zum ersten Mal Fracht an Bord: Mit F5 kopierst du Dateien von einem Panel ins andere.

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

☕ Kaffee spendieren