Rechte anzeigen und ändern
Wer darf eine Datei lesen, wer darf sie ändern, und wer darf sie als Programm starten? Das regeln unter Linux und macOS die Zugriffsrechte. Heute zeigt der Fährmann sie in jeder Zeile an, und mit F9 öffnet sich ein kleiner Dialog im Commander-Stil, in dem du sie per Tastatur ändern kannst. Dabei lernst du, wie ein Programm auf verschiedenen Betriebssystemen unterschiedlichen Code benutzen kann, und wie man in iced etwas über etwas anderes legt.
Bevor wir loslegen
Wir machen da weiter, wo Etappe 6 aufgehört hat. Den kompletten Stand findest du dort unter Zwischenstand.
Wie Rechte funktionieren
Jede Datei hat drei Gruppen von Menschen, die etwas mit ihr tun wollen: ihren Besitzer, eine Gruppe von Benutzern und alle Anderen. Und für jede dieser drei Gruppen gibt es drei Erlaubnisse: Lesen (r wie read), Schreiben (w wie write) und Ausführen (x wie execute). Drei mal drei, macht neun Schalter, jeder an oder aus.
Im Terminal zeigt ls -l diese neun Schalter als Text, zum Beispiel
rw-r--r--: Der Besitzer darf lesen und schreiben, alle anderen nur lesen. Oft
sieht man sie auch als dreistellige Zahl, etwa 644 oder 755. Jede
Ziffer steht für eine Gruppe: 4 heißt lesen, 2 schreiben, 1 ausführen, und die Ziffer ist die
Summe. 6 ist also 4 + 2, lesen und schreiben. Diese Zahlen sind
Oktalzahlen (Achtersystem), und Rust schreibt sie mit 0o davor:
0o644.
Für den Computer sind die neun Schalter einfach neun Bits in einer Zahl. Das erste Bit (ganz links) ist „Besitzer darf lesen“, das letzte „Andere dürfen ausführen“. Damit werden wir heute rechnen.
Und Windows?
Windows regelt Rechte ganz anders und viel feiner. Was Rust auf allen Systemen gleich anbietet, ist nur ein einziger Schalter: schreibgeschützt oder nicht. Unser Fährmann zeigt deshalb unter Windows einfache Rechte an, und beim Ändern zählt dort nur das Schreibrecht des Besitzers. Wie man das in einem Programm für beide Welten unterbringt, zeigt Schritt 1.
Rechte in jeder Zeile
Zuerst soll jede Zeile ihre Rechte zeigen, zwischen Name und Größe. Dafür muss sich jeder Eintrag seine Rechte merken:
ist_ordner: bool, groesse: u64,rechte: u32, // Zugriffsrechte, zum Beispiel 0o644}
u32 ist eine ganze Zahl mit 32 Bits, mehr als genug für unsere neun
Schalter. In ordner_lesen füllen wir das neue Feld an beiden Stellen, an denen
ein Eintrag gebaut wird. Der Eintrag .. bekommt einfach 0:
ist_ordner: info.is_dir(), groesse: info.len(),rechte: rechte_lesen(&info),});⋮name: String::from(".."), ist_ordner: true, groesse: 0,rechte: 0,};
Jetzt die drei Hilfsfunktionen, ans Ende der Datei:
fs::set_permissions(nach, fs::metadata(von)?.permissions())?; Ok(dateien) }// Liefert die Nummer eines Rechte-Bits: 0 ist "Besitzer darf lesen", 8 ist "Andere dürfen ausführen". fn bit(nr: usize) -> u32 { 1 << (8 - nr) } // Macht aus den Rechte-Bits einen Text wie "rwxr-xr-x". fn rechte_text(rechte: u32) -> String { let zeichen = ['r', 'w', 'x']; let mut text = String::new(); for nr in 0..9 { if rechte & bit(nr) != 0 { text.push(zeichen[nr % 3]); } else { text.push('-'); } } text } // Liest die Rechte aus den Infos einer Datei. fn rechte_lesen(info: &fs::Metadata) -> u32 { #[cfg(unix)] { use std::os::unix::fs::PermissionsExt; info.permissions().mode() & 0o777 } #[cfg(not(unix))] { // Windows kennt nur "schreibgeschützt" oder nicht if info.permissions().readonly() { 0o444 } else { 0o666 } } }
bit(nr)rechnet aus, welches Bit zu Schalter Nummernrgehört.1 << 8schiebt eine 1 um acht Stellen nach links, das ist das Bit ganz links, „Besitzer darf lesen“. Für Schalter 8 wird nicht geschoben: das Bit ganz rechts.rechte_textgeht die neun Schalter durch. Mitrechte & bit(nr)(das einfache&ist hier ein „bitweises Und“) prüft es, ob ein Schalter an ist. Wenn ja, kommt der passende Buchstabe in den Text, sonst ein Strich.nr % 3wählt dabei immer reihumr,w,x.rechte_lesenholt die Rechte aus den Datei-Infos. Hier steht zweimal fast derselbe Block, einmal mit#[cfg(unix)]und einmal mit#[cfg(not(unix))]. Das ist bedingtes Übersetzen: Auf Linux und macOS übersetzt Rust nur den ersten Block, auf Windows nur den zweiten. Der andere ist für den Compiler einfach nicht da. So kann ein Programm auf jedem System genau den Code benutzen, den es dort gibt.PermissionsExtmit seinemmode()gibt es nämlich nur auf Unix-Systemen. Mit& 0o777behalten wir nur unsere neun Schalter und werfen andere Bits weg, die dort auch noch gespeichert sind.
Die Zeile bekommt eine neue Spalte für die Rechte. Damit die Größen ordentlich untereinander
stehen, geben wir ihrer Spalte eine feste Breite und richten sie rechtsbündig aus. Dafür
brauchen wir Right aus iced:
use iced::{Background, Border, Color, Element, Fill, Font, Subscription, Task, Theme};use iced::{Background, Border, Color, Element, Fill, Font, Right, Subscription, Task, Theme};
let inhalt = row![ text(eintrag.name.as_str()) .color(farbe) .width(Fill) .wrapping(text::Wrapping::None), text(groesse).color(farbe), ]let rechte = if eintrag.name == ".." { String::new() } else { rechte_text(eintrag.rechte) }; let inhalt = row![ text(eintrag.name.as_str()) .color(farbe) .width(Fill) .wrapping(text::Wrapping::None), text(rechte).color(farbe), text(groesse).color(farbe).width(90).align_x(Right), ].spacing(12);
#[cfg(unix)] stehen die geschweiften Klammern direkt darunter, ohne
if davor. Es sind einfache Blöcke, und der letzte Wert im übersetzten Block ist
das Ergebnis der Funktion. Deshalb steht hinter info.permissions().mode() & 0o777
auch kein Semikolon.cargo run
In beiden Panels steht jetzt bei jeder Datei und jedem Ordner eine Spalte wie
rw-r--r-- oder rwxr-xr-x. Ordner haben fast immer ein
x: Bei Ordnern bedeutet „ausführen“ nämlich „hineingehen dürfen“.Der Dialog im Gedächtnis
Ein Dialog ist ein kleines Fenster, das sich über alles andere legt und eine Frage stellt.
Unser Rechte-Dialog muss sich vier Dinge merken: für welche Datei er gilt (Name und Pfad),
welche Rechte gerade eingestellt sind und welcher der neun Schalter markiert ist. Dafür
bekommt er ein eigenes struct, zwischen Seite und
Message:
enum Seite { Links, Rechts, }// Der Dialog zum Ändern der Rechte: für welche Datei, welche Rechte, welcher Schalter gewählt ist. struct RechteDialog { name: String, pfad: PathBuf, rechte: u32, auswahl: usize, // 0 bis 8, welcher der neun Schalter gerade markiert ist }#[derive(Debug, Clone)]
Mal ist ein Dialog offen, mal nicht. „Vielleicht ein Dialog, vielleicht keiner“, das ist ein
Fall für Option:
meldung: String, // was der Fährmann zuletzt zu sagen hattedialog: Option<RechteDialog>, // offener Dialog, falls es einen gibt}
meldung: String::from("Willkommen an Bord! Tab wechselt das Panel, F5 kopiert, F10 beendet."),dialog: None,}
Geöffnet wird der Dialog mit F9. Eine Briefsorte, eine Weiterleitung, eine Taste:
Kopieren,RechteZeigen,Beenden,
Message::Kopieren => self.kopieren(),Message::RechteZeigen => self.rechte_zeigen(),Message::Beenden => return iced::exit(),
Key::Named(Named::F5) => Some(Message::Kopieren),Key::Named(Named::F9) => Some(Message::RechteZeigen),
Und weil F9 jetzt eine Aufgabe hat, bekommt die Taste unten auch eine passende Beschriftung:
f_taste(9, "Leiste"),f_taste(9, "Rechte"),
Fehlt noch die Funktion, die den Dialog öffnet. Sie kommt in impl Faehrmann,
direkt vor kopieren:
// Öffnet den Rechte-Dialog für den markierten Eintrag. fn rechte_zeigen(&mut self) { let panel = self.aktives_panel(); let Some(eintrag) = panel.markierter_eintrag() else { return; }; if eintrag.name == ".." { return; } let dialog = RechteDialog { name: eintrag.name.clone(), pfad: panel.ordner.join(&eintrag.name), rechte: eintrag.rechte, auswahl: 0, }; self.dialog = Some(dialog); }// Kopiert die markierte Datei vom aktiven Panel in den Ordner des anderen Panels.
Sie arbeitet wie der Anfang von kopieren: markierten Eintrag holen,
.. ausschließen, und dann einen neuen RechteDialog mit den aktuellen
Rechten des Eintrags anlegen. Some(dialog) macht daraus einen offenen
Dialog.
cargo run
Sichtbar ist noch nichts, denn gezeichnet wird der Dialog erst im nächsten
Schritt. Rust warnt, dass die Felder des Dialogs nie gelesen werden. Unten in der
Tastenleiste steht aber schon 9 Rechte.Den Dialog zeichnen
Im Original waren Dialoge graue Kästen mit schwarzem Rahmen, die mitten über den Panels
erschienen. Für „etwas über etwas anderes legen“ hat iced das Widget stack, einen
Stapel. Was im Stapel später kommt, liegt oben. Dazu eine neue Farbe:
use iced::widget::{column, container, row, text};use iced::widget::{column, container, row, stack, text};
const GELB: Color = Color::from_rgb8(255, 255, 85);const GRAU: Color = Color::from_rgb8(170, 170, 170);
Am Ende von view bauen wir die bisherige Spalte, geben ihr einen Namen und
entscheiden dann: Ist ein Dialog offen, kommt er mittig obendrauf:
column![menuzeile, arbeitsflaeche, meldungszeile, tastenleiste].into()let hauptteil = column![menuzeile, arbeitsflaeche, meldungszeile, tastenleiste]; // Ist ein Dialog offen, liegt er mittig über allem anderen match &self.dialog { Some(dialog) => stack![hauptteil, container(dialog_ansicht(dialog)).center(Fill)].into(), None => hauptteil.into(), }}
match &self.dialog schaut in die Option hinein, ohne sie zu
verändern (daher das &). container(…).center(Fill) kennst du
schon aus Etappe 1: Er nimmt den ganzen Platz ein und stellt den Dialog in die Mitte.
Und nun der Dialog selbst, wieder als eigene Funktion ans Ende der Datei:
if info.permissions().readonly() { 0o444 } else { 0o666 } } }// Zeichnet den Rechte-Dialog als graue Box im Commander-Stil. fn dialog_ansicht(dialog: &RechteDialog) -> Element<'_, Message> { let mut gitter = column![row![ text("").width(110), text("Lesen").width(110), text("Schreiben").width(110), text("Ausführen").width(110), ]] .spacing(4); for (reihe_nr, wer) in ["Besitzer", "Gruppe", "Andere"].iter().enumerate() { let mut reihe = row![text(*wer).width(110)]; for spalte in 0..3 { let nr = reihe_nr * 3 + spalte; let an = dialog.rechte & bit(nr) != 0; let gewaehlt = nr == dialog.auswahl; let kaestchen = text(if an { "[x]" } else { "[ ]" }) .color(if gewaehlt { WEISS } else { SCHWARZ }); reihe = reihe.push(container(kaestchen).width(110).style(move |_theme: &Theme| { container::Style { background: if gewaehlt { Some(Background::Color(SCHWARZ)) } else { None }, ..container::Style::default() } })); } gitter = gitter.push(reihe); } let inhalt = column![ text(format!("Rechte für „{}“", dialog.name)), gitter, text(format!("Zahlencode: {:03o}", dialog.rechte)), text("Pfeile: wählen · Leertaste: an/aus · Enter: übernehmen · Esc: abbrechen").size(13), ] .spacing(14); container(inhalt) .padding(18) .style(|_theme: &Theme| container::Style { background: Some(Background::Color(GRAU)), text_color: Some(SCHWARZ), border: Border { color: SCHWARZ, width: 2.0, radius: 0.0.into(), }, ..container::Style::default() }) .into() }
- Oben steht eine Kopfzeile mit den drei Spaltennamen. Alle Spalten sind 110 Pixel breit, damit alles schön untereinander steht.
- Darunter kommen drei Reihen, für Besitzer, Gruppe und Andere.
.iter().enumerate()liefert dabei die Nummer der Reihe mit. Aus Reihe und Spalte rechnen wir die Nummer des Schalters aus: Reihe mal drei plus Spalte. So bekommt jedes Kästchen eine Nummer von 0 bis 8, Zeile für Zeile durchgezählt. - Jeder Schalter erscheint als
[x](an) oder[ ](aus). Der gewählte Schalter bekommt einen schwarzen Hintergrund mit weißer Schrift, so sieht man, wo man gerade ist. {:03o}imformat!zeigt die Rechte als dreistellige Oktalzahl, also als den Zahlencode, den man aus dem Terminal kennt. Dasosteht für oktal, die03für „mindestens drei Stellen, vorne mit Nullen auffüllen“.
cargo run
Setz den Balken auf eine Datei und drück F9: Mitten über den Panels
erscheint ein grauer Dialog mit neun Kästchen und dem Zahlencode. Er zeigt genau die Rechte
an, die auch in der Zeile stehen. Rust warnt nur noch, dass pfad nie gelesen
wird. Den brauchen wir erst beim Übernehmen. Aber Vorsicht: Bedienen lässt er sich noch nicht, und
schließen auch nicht. Beende das Programm mit F10, das funktioniert noch.Den Dialog bedienen
Jetzt erwecken wir den Dialog zum Leben. Mit den Pfeiltasten wanderst du durch die neun Kästchen, die Leertaste schaltet ein Kästchen an oder aus, Enter übernimmt die Rechte, und Esc bricht ab. Dafür brauchen wir vier neue Briefsorten:
RechteZeigen,PfeilLinks, PfeilRechts, Umschalten, Abbrechen,Beenden,
Wir nennen sie PfeilLinks und PfeilRechts, nicht
Links und Rechts. Das wäre zwar erlaubt, weil sie in einem anderen
enum stehen als die Seiten, aber man käme leicht durcheinander. Die Tasten dazu:
Key::Named(Named::F9) => Some(Message::RechteZeigen),Key::Named(Named::ArrowLeft) => Some(Message::PfeilLinks), Key::Named(Named::ArrowRight) => Some(Message::PfeilRechts), Key::Named(Named::Space) => Some(Message::Umschalten), Key::Named(Named::Escape) => Some(Message::Abbrechen),
Jetzt die wichtigste Idee dieses Schritts: Solange der Dialog offen ist, gehören ihm alle
Tasten. Die Pfeiltasten sollen dann nicht den Balken im Panel bewegen, sondern die Auswahl im
Dialog. Deshalb fängt update ganz am Anfang alle Briefe ab, wenn ein Dialog
offen ist:
fn update(&mut self, message: Message) -> Task<Message> {// Ist der Rechte-Dialog offen, gehen alle Tasten an ihn if let Some(dialog) = &mut self.dialog { match message { Message::Hoch => dialog.auswahl = (dialog.auswahl + 6) % 9, Message::Runter => dialog.auswahl = (dialog.auswahl + 3) % 9, Message::PfeilLinks => dialog.auswahl = (dialog.auswahl + 8) % 9, Message::PfeilRechts => dialog.auswahl = (dialog.auswahl + 1) % 9, Message::Umschalten => dialog.rechte ^= bit(dialog.auswahl), Message::Oeffnen => self.rechte_anwenden(), Message::Abbrechen => self.dialog = None, Message::Beenden => return iced::exit(), _ => {} } return Task::none(); }match message {
if let Some(dialog) = &mut self.dialogheißt: „Falls ein Dialog offen ist, leih ihn mir zum Verändern aus und nenn ihndialog.“- Die Pfeiltasten rechnen mit
%, dem Rest beim Teilen durch 9. So läuft die Auswahl am Rand einfach auf der anderen Seite weiter. Eine Reihe hoch heißt drei Schalter zurück. Weil man beiusizenicht unter 0 rechnen darf, zählen wir stattdessen 6 vor: Bei neun Schaltern im Kreis kommt man so an derselben Stelle an. dialog.rechte ^= bit(…)schaltet ein Bit um. Das^ist das „bitweise Entweder-oder“: Ein Bit, das an war, geht aus, und eins, das aus war, geht an. Genau wie ein Lichtschalter.Oeffnenist der Brief der Enter-Taste. Im Dialog bedeutet er „übernehmen“._ => {}fängt alle übrigen Briefe ab, zum Beispiel Tab oder F5: Bei offenem Dialog bewirken sie nichts.- Zum Schluss verlassen wir
updatesofort, damit der Brief nicht auch noch im normalenmatchdarunter landet.
Das normale match darunter muss die neuen Briefe auch kennen, sonst meckert
Rust, dass nicht alle Fälle abgedeckt sind. Ohne Dialog haben sie keine Bedeutung. Der
senkrechte Strich | fasst mehrere Fälle zu einem Zweig zusammen:
Message::RechteZeigen => self.rechte_zeigen(),Message::PfeilLinks | Message::PfeilRechts | Message::Umschalten | Message::Abbrechen => {} // ohne Dialog ohne BedeutungMessage::Beenden => return iced::exit(),
Bleibt das Übernehmen. Erst die Funktion, die wirklich Rechte setzt, wieder in zwei Ausführungen für Unix und Windows, ans Ende der Datei:
if info.permissions().readonly() { 0o444 } else { 0o666 } } }// Setzt die Rechte einer Datei oder eines Ordners. fn rechte_setzen(pfad: &Path, rechte: u32) -> io::Result<()> { #[cfg(unix)] { use std::os::unix::fs::PermissionsExt; fs::set_permissions(pfad, fs::Permissions::from_mode(rechte)) } #[cfg(not(unix))] { let mut erlaubnis = fs::metadata(pfad)?.permissions(); erlaubnis.set_readonly(rechte & 0o200 == 0); // ohne Schreibrecht: schreibgeschützt fs::set_permissions(pfad, erlaubnis) } }
Unter Unix baut Permissions::from_mode aus unseren Bits die passende Erlaubnis.
Unter Windows holen wir die bisherige Erlaubnis und setzen nur „schreibgeschützt“ an oder
aus, je nachdem, ob der Besitzer schreiben darf (0o200 ist genau dieses Bit).
Und dann die Funktion in impl Faehrmann, hinter rechte_zeigen:
auswahl: 0, }; self.dialog = Some(dialog); }// Setzt die im Dialog gewählten Rechte und schließt den Dialog. fn rechte_anwenden(&mut self) { let Some(dialog) = self.dialog.take() else { return; }; self.meldung = match rechte_setzen(&dialog.pfad, dialog.rechte) { Ok(()) => format!( "Rechte von „{}“ sind jetzt {} ({:03o}).", dialog.name, rechte_text(dialog.rechte), dialog.rechte ), Err(fehler) => format!("Rechte ändern fehlgeschlagen: {}", fehler), }; self.links.neu_lesen(); self.rechts.neu_lesen(); }
self.dialog.take() ist ein praktischer Trick: Es nimmt den Dialog aus der
Option heraus und lässt None zurück. In einem Rutsch haben wir also
den Dialog in der Hand und ihn geschlossen. Danach setzen wir die Rechte, melden das
Ergebnis und lesen beide Panels neu ein, denn beide könnten den Ordner zeigen.
update im Zweig Oeffnen
self.rechte_anwenden() auf, obwohl wir uns dialog gerade aus
self ausgeliehen haben. Rust erlaubt das, weil es genau mitverfolgt, dass
dialog in diesem Zweig nicht mehr benutzt wird. Würdest du dort aber etwa
dialog.rechte noch einmal anfassen, käme der Fehler
cannot borrow `*self` as mutable more than once.cargo run
Leg dir im Spielordner eine Testdatei an, setz den Balken darauf und drück
F9. Wandere mit den Pfeiltasten zu „Andere · Lesen“, drück die Leertaste, und
das Kreuz verschwindet. Der Zahlencode ändert sich mit. Drück Enter: Der Dialog
schließt sich, die Meldungszeile bestätigt die neuen Rechte, und in der Zeile steht jetzt
zum Beispiel rw-r-----. Probier auch Esc: Dann bleibt alles, wie es
war.
Der komplette Code nach Etappe 7
Hier ist der vollständige Stand nach Etappe 7. 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::io;
use std::path::{Path, PathBuf};
use iced::widget::{column, container, row, stack, text};
use iced::keyboard::{self, Key, key::Named};
use iced::{Background, Border, Color, Element, Fill, Font, Right, 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);
const GRAU: Color = Color::from_rgb8(170, 170, 170);
// 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,
rechte: u32, // Zugriffsrechte, zum Beispiel 0o644
}
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
dialog: Option<RechteDialog>, // offener Dialog, falls es einen gibt
}
// Welches der beiden Panels ist gerade aktiv?
#[derive(PartialEq)]
enum Seite {
Links,
Rechts,
}
// Der Dialog zum Ändern der Rechte: für welche Datei, welche Rechte, welcher Schalter gewählt ist.
struct RechteDialog {
name: String,
pfad: PathBuf,
rechte: u32,
auswahl: usize, // 0 bis 8, welcher der neun Schalter gerade markiert ist
}
#[derive(Debug, Clone)]
enum Message {
Hoch,
Runter,
Oeffnen,
Wechseln,
Kopieren,
RechteZeigen,
PfeilLinks,
PfeilRechts,
Umschalten,
Abbrechen,
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."),
dialog: None,
}
}
fn update(&mut self, message: Message) -> Task<Message> {
// Ist der Rechte-Dialog offen, gehen alle Tasten an ihn
if let Some(dialog) = &mut self.dialog {
match message {
Message::Hoch => dialog.auswahl = (dialog.auswahl + 6) % 9,
Message::Runter => dialog.auswahl = (dialog.auswahl + 3) % 9,
Message::PfeilLinks => dialog.auswahl = (dialog.auswahl + 8) % 9,
Message::PfeilRechts => dialog.auswahl = (dialog.auswahl + 1) % 9,
Message::Umschalten => dialog.rechte ^= bit(dialog.auswahl),
Message::Oeffnen => self.rechte_anwenden(),
Message::Abbrechen => self.dialog = None,
Message::Beenden => return iced::exit(),
_ => {}
}
return Task::none();
}
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::RechteZeigen => self.rechte_zeigen(),
Message::PfeilLinks
| Message::PfeilRechts
| Message::Umschalten
| Message::Abbrechen => {} // ohne Dialog ohne Bedeutung
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),
}
}
// Öffnet den Rechte-Dialog für den markierten Eintrag.
fn rechte_zeigen(&mut self) {
let panel = self.aktives_panel();
let Some(eintrag) = panel.markierter_eintrag() else {
return;
};
if eintrag.name == ".." {
return;
}
let dialog = RechteDialog {
name: eintrag.name.clone(),
pfad: panel.ordner.join(&eintrag.name),
rechte: eintrag.rechte,
auswahl: 0,
};
self.dialog = Some(dialog);
}
// Setzt die im Dialog gewählten Rechte und schließt den Dialog.
fn rechte_anwenden(&mut self) {
let Some(dialog) = self.dialog.take() else {
return;
};
self.meldung = match rechte_setzen(&dialog.pfad, dialog.rechte) {
Ok(()) => format!(
"Rechte von „{}“ sind jetzt {} ({:03o}).",
dialog.name,
rechte_text(dialog.rechte),
dialog.rechte
),
Err(fehler) => format!("Rechte ändern fehlgeschlagen: {}", fehler),
};
self.links.neu_lesen();
self.rechts.neu_lesen();
}
// 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 nach.exists() {
format!("„{}“ gibt es in {} schon. Nichts überschrieben.", name, zielordner)
} else if ist_ordner && nach.starts_with(&von) {
format!("„{}“ kann nicht in sich selbst kopiert werden.", name)
} else if ist_ordner {
match ordner_kopieren(&von, &nach) {
Ok(anzahl) => format!(
"Ordner kopiert: {} mit {} Dateien → {}",
name, anzahl, zielordner
),
Err(fehler) => format!("Ordner kopieren fehlgeschlagen: {}", fehler),
}
} 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, "Rechte"),
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()
});
let hauptteil = column![menuzeile, arbeitsflaeche, meldungszeile, tastenleiste];
// Ist ein Dialog offen, liegt er mittig über allem anderen
match &self.dialog {
Some(dialog) => stack![hauptteil, container(dialog_ansicht(dialog)).center(Fill)].into(),
None => hauptteil.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(),
rechte: rechte_lesen(&info),
});
}
// 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,
rechte: 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 rechte = if eintrag.name == ".." {
String::new()
} else {
rechte_text(eintrag.rechte)
};
let inhalt = row![
text(eintrag.name.as_str())
.color(farbe)
.width(Fill)
.wrapping(text::Wrapping::None),
text(rechte).color(farbe),
text(groesse).color(farbe).width(90).align_x(Right),
]
.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('.', ",")
}
// Kopiert einen ganzen Ordner mit allem, was darin liegt, und liefert die Zahl der Dateien.
// Die Rechte jeder Datei und jedes Ordners werden dabei mitgenommen.
fn ordner_kopieren(von: &Path, nach: &Path) -> io::Result<u64> {
fs::create_dir(nach)?;
let mut dateien = 0;
for eintrag in fs::read_dir(von)? {
let eintrag = eintrag?;
let art = eintrag.file_type()?;
let ziel = nach.join(eintrag.file_name());
if art.is_symlink() {
continue; // Verknüpfungen lassen wir aus
} else if art.is_dir() {
dateien += ordner_kopieren(&eintrag.path(), &ziel)?;
} else {
fs::copy(eintrag.path(), &ziel)?; // nimmt die Rechte der Datei mit
dateien += 1;
}
}
// Die Rechte des Ordners erst zum Schluss setzen: Ein schreibgeschützter
// Ordner ließe sich sonst gar nicht mehr befüllen.
fs::set_permissions(nach, fs::metadata(von)?.permissions())?;
Ok(dateien)
}
// Liefert die Nummer eines Rechte-Bits: 0 ist "Besitzer darf lesen", 8 ist "Andere dürfen ausführen".
fn bit(nr: usize) -> u32 {
1 << (8 - nr)
}
// Macht aus den Rechte-Bits einen Text wie "rwxr-xr-x".
fn rechte_text(rechte: u32) -> String {
let zeichen = ['r', 'w', 'x'];
let mut text = String::new();
for nr in 0..9 {
if rechte & bit(nr) != 0 {
text.push(zeichen[nr % 3]);
} else {
text.push('-');
}
}
text
}
// Liest die Rechte aus den Infos einer Datei.
fn rechte_lesen(info: &fs::Metadata) -> u32 {
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
info.permissions().mode() & 0o777
}
#[cfg(not(unix))]
{
// Windows kennt nur "schreibgeschützt" oder nicht
if info.permissions().readonly() { 0o444 } else { 0o666 }
}
}
// Setzt die Rechte einer Datei oder eines Ordners.
fn rechte_setzen(pfad: &Path, rechte: u32) -> io::Result<()> {
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
fs::set_permissions(pfad, fs::Permissions::from_mode(rechte))
}
#[cfg(not(unix))]
{
let mut erlaubnis = fs::metadata(pfad)?.permissions();
erlaubnis.set_readonly(rechte & 0o200 == 0); // ohne Schreibrecht: schreibgeschützt
fs::set_permissions(pfad, erlaubnis)
}
}
// Zeichnet den Rechte-Dialog als graue Box im Commander-Stil.
fn dialog_ansicht(dialog: &RechteDialog) -> Element<'_, Message> {
let mut gitter = column![row![
text("").width(110),
text("Lesen").width(110),
text("Schreiben").width(110),
text("Ausführen").width(110),
]]
.spacing(4);
for (reihe_nr, wer) in ["Besitzer", "Gruppe", "Andere"].iter().enumerate() {
let mut reihe = row![text(*wer).width(110)];
for spalte in 0..3 {
let nr = reihe_nr * 3 + spalte;
let an = dialog.rechte & bit(nr) != 0;
let gewaehlt = nr == dialog.auswahl;
let kaestchen = text(if an { "[x]" } else { "[ ]" })
.color(if gewaehlt { WEISS } else { SCHWARZ });
reihe = reihe.push(container(kaestchen).width(110).style(move |_theme: &Theme| {
container::Style {
background: if gewaehlt {
Some(Background::Color(SCHWARZ))
} else {
None
},
..container::Style::default()
}
}));
}
gitter = gitter.push(reihe);
}
let inhalt = column![
text(format!("Rechte für „{}“", dialog.name)),
gitter,
text(format!("Zahlencode: {:03o}", dialog.rechte)),
text("Pfeile: wählen · Leertaste: an/aus · Enter: übernehmen · Esc: abbrechen").size(13),
]
.spacing(14);
container(inhalt)
.padding(18)
.style(|_theme: &Theme| container::Style {
background: Some(Background::Color(GRAU)),
text_color: Some(SCHWARZ),
border: Border {
color: SCHWARZ,
width: 2.0,
radius: 0.0.into(),
},
..container::Style::default()
})
.into()
}
// Ü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::F9) => Some(Message::RechteZeigen),
Key::Named(Named::ArrowLeft) => Some(Message::PfeilLinks),
Key::Named(Named::ArrowRight) => Some(Message::PfeilRechts),
Key::Named(Named::Space) => Some(Message::Umschalten),
Key::Named(Named::Escape) => Some(Message::Abbrechen),
Key::Named(Named::F10) => Some(Message::Beenden),
_ => None,
}
}
& prüft und mit ^ umschaltet. Mit #[cfg(unix)] und
#[cfg(not(unix))] übersetzt Rust je nach Betriebssystem unterschiedlichen Code.
stack legt in iced Widgets übereinander, und eine Option im Zustand
steht für „vielleicht offen, vielleicht nicht“. Wenn ein Dialog alle Tasten bekommen soll,
fängt update sie mit if let Some(…) = &mut … vorn ab.
take() holt den Inhalt aus einer Option und lässt
None zurück.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.
- Für alles darin: Bei Ordnern wäre es praktisch, die Rechte auf Wunsch auch
für alles darin zu setzen. Schreib eine rekursive Funktion
rechte_rekursiv_setzennach dem Vorbild vonordner_kopieren. - Zifferneingabe: Profis tippen Rechte als Zahl. Lass im Dialog die Tasten
0 bis 7 die Ziffer der aktuellen Reihe setzen. Tipp: In
tasteliefertKey::Character(c)die getippte Taste als Text. - Schreibgeschützt? Nimm einer Testdatei mit dem Dialog alle Schreibrechte und versuch dann, mit F5 eine gleichnamige Datei darüber zu kopieren. Was passiert, und warum?
Lösungen anzeigen
1. Wie ordner_kopieren, nur ohne Ziel: Für jeden Eintrag in
fs::read_dir(pfad)? bei Ordnern sich selbst aufrufen und bei Dateien
rechte_setzen(&eintrag.path(), rechte)?. Zum Schluss den Ordner selbst.
Vorsicht: Ordner brauchen das x-Recht, sonst kommt man nicht mehr hinein.
Darum setzen Profiprogramme für Ordner und Dateien oft unterschiedliche Rechte.
2. Eine neue Nachricht Ziffer(u32), in taste:
Key::Character(c) => c.parse::<u32>().ok().filter(|z| *z <= 7).map(Message::Ziffer),
und im Dialog-match: Message::Ziffer(z) => { let schieben = 3 * (2 -
dialog.auswahl / 3); dialog.rechte = (dialog.rechte & !(0o7 << schieben)) | (z
<< schieben); }. Erst werden die drei Bits der Reihe gelöscht, dann die neue
Ziffer hineingeschoben.
3. Gar nichts wird überschrieben. Der Fährmann meldet schon vorher, dass es die Datei am Ziel gibt, und rührt sie nicht an. Seine Vorsicht aus Etappe 5 greift also, bevor die Rechte überhaupt eine Rolle spielen. Das Schreibrecht würde erst wichtig, wenn wir Überschreiben einmal erlauben.
Etappe 7 geschafft 🔐
Der Fährmann zeigt Rechte an, kopiert sie mit und lässt dich sie in einem richtigen Commander-Dialog ändern, auf Linux, macOS und Windows. Damit ist er bereit für die große Fahrt: In der nächsten Etappe richten wir einen Testserver auf deinem eigenen Rechner ein, und danach legt die Fähre zum ersten Mal per FTP ab.
Hat dir das Mitbauen Spaß gemacht? Ein Kaffee hilft, dass die nächste Etappe ein Stück schneller fertig wird.
☕ Kaffee spendieren