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.
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.
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:
.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.hochundruntersind das, was bisher direkt inupdatestand. Als eigene Funktionen gehören sie jetzt zum Panel: Ein Panel weiß selbst, wie man seinen Balken bewegt.ansicht(aktiv)ist die altepanel_ansichtmit einem Zusatz: Der Wertaktivsagt, ob dieses Panel gerade das aktive ist. Nur dann bekommt der Pfad oben einen türkisen Hintergrund, und nur dann wird der Balken gezeichnet. Dasaktiv && nr == self.cursorheiß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.
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.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:
// 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 Eintragspanel: Panel,}
neu wird dadurch kürzer. Den Startordner ermitteln wir wie bisher, aber das
Einlesen erledigt jetzt Panel::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:
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:
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:
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:
let arbeitsflaeche = container(self.panel_ansicht())let arbeitsflaeche = container(self.panel.ansicht(true)).width(Fill)
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.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.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:
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:
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:
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:
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:
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)
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.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:
Oeffnen,Wechseln,Beenden,
Was beim Brief passiert:
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:
Key::Named(Named::Enter) => Some(Message::Oeffnen),Key::Named(Named::Tab) => Some(Message::Wechseln),
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.
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,
}
}
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.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.
- Rechts startet zu Hause: Lass das rechte Panel im Home-Ordner starten
statt im Startordner. Tipp:
std::env::home_dir()liefert eineOption<PathBuf>. - 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.
- Wo bin ich? Schreib eine Funktion
inaktives_panel, die das jeweils andere Panel liefert. Wie unterscheidet sie sich vonaktives_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