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.
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.
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:
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:
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:
..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.
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.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:
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_eintragliefert eineOption<&Eintrag>: vielleicht einen ausgeliehenen Eintrag, vielleicht keinen (in einem leeren Ordner).neu_lesenliest 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- 1würde dort abstürzen.
Und Faehrmann bekommt zwei Helfer 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.
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.Das Kopieren selbst
Jetzt das Herzstück der Etappe. Die Funktion kopieren kommt 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 einResult, und diesmal behandeln wir beide Fälle mitmatch:Okergibt eine Erfolgsmeldung,Erreine Fehlermeldung, in die wir den Fehler selbst einsetzen. WasOkmitliefert (die Zahl der kopierten Bytes), brauchen wir nicht, daherOk(_). - Neu einlesen: Zum Schluss liest das Zielpanel seinen Ordner neu ein, damit die neue Datei auch zu sehen ist.
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.". 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.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.F5 drücken
Die letzten Handgriffe sind dir inzwischen vertraut: Briefsorte, Weiterleitung, Taste.
Wechseln,Kopieren,Beenden,
Seite::Rechts => Seite::Links, }; }Message::Kopieren => self.kopieren(),Message::Beenden => return iced::exit(),
Key::Named(Named::Tab) => Some(Message::Wechseln),Key::Named(Named::F5) => Some(Message::Kopieren),
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.
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.
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,
}
}
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“.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.
- 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? - Wie viel?
fs::copyliefert im Erfolgsfall die Zahl der kopierten Bytes. Bau sie in die Erfolgsmeldung ein, schön lesbar mitgroesse_text. - 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