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

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.

4 Schritte · ca. 50 Minuten · baut auf Etappe 6 auf

Der Fährmann mit Rechte-Spalte in beiden Panels und einem grauen Dialog, in dem Lese-, Schreib- und Ausführrechte angekreuzt sind
So sieht der Fährmann am Ende dieser Etappe aus.
Vorab

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.

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:

src/main.rsdas struct Eintrag
    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:

src/main.rsin der Funktion ordner_lesen
            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:

src/main.rsganz am 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 Nummer nr gehört. 1 << 8 schiebt 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_text geht die neun Schalter durch. Mit rechte & 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 % 3 wählt dabei immer reihum r, w, x.
  • rechte_lesen holt 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. PermissionsExt mit seinem mode() gibt es nämlich nur auf Unix-Systemen. Mit & 0o777 behalten 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:

src/main.rsganz oben, die zweite iced-Zeile
use iced::{Background, Border, Color, Element, Fill, Font, Subscription, Task, Theme};use iced::{Background, Border, Color, Element, Fill, Font, Right, Subscription, Task, Theme};
src/main.rsin der Funktion zeile
    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);
Worauf du achten musst Bei #[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.
Probelauf Speichern, dann im Terminal:
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“.
Schritt 2

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:

src/main.rszwischen enum Seite und enum 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:

src/main.rsdas struct Faehrmann
    meldung: String, // was der Fährmann zuletzt zu sagen hatte    dialog: Option<RechteDialog>, // offener Dialog, falls es einen gibt}
src/main.rsin der Funktion neu
            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:

src/main.rsdas enum Message
    Kopieren,    RechteZeigen,    Beenden,
src/main.rsin der Funktion update
            Message::Kopieren => self.kopieren(),            Message::RechteZeigen => self.rechte_zeigen(),            Message::Beenden => return iced::exit(),
src/main.rsin der Funktion taste
        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:

src/main.rsin der Funktion view, die Tastenleiste
            f_taste(9, "Leiste"),            f_taste(9, "Rechte"),

Fehlt noch die Funktion, die den Dialog öffnet. Sie kommt in impl Faehrmann, direkt vor kopieren:

src/main.rsin impl Faehrmann, direkt vor der Funktion 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.

Probelauf Speichern, dann im Terminal:
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.
Schritt 3

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:

src/main.rsganz oben, die erste iced-Zeile
use iced::widget::{column, container, row, text};use iced::widget::{column, container, row, stack, text};
src/main.rsbei den Farben
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:

src/main.rsam Ende der Funktion view
        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:

src/main.rsganz am 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} im format! zeigt die Rechte als dreistellige Oktalzahl, also als den Zahlencode, den man aus dem Terminal kennt. Das o steht für oktal, die 03 für „mindestens drei Stellen, vorne mit Nullen auffüllen“.
Probelauf Speichern, dann im Terminal:
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.
Schritt 4

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:

src/main.rsdas enum Message
    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:

src/main.rsin der Funktion taste
        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:

src/main.rsganz am Anfang der Funktion update
    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.dialog heißt: „Falls ein Dialog offen ist, leih ihn mir zum Verändern aus und nenn ihn dialog.“
  • 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 bei usize nicht 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.
  • Oeffnen ist 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 update sofort, damit der Brief nicht auch noch im normalen match darunter 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:

src/main.rsin der Funktion update, der eigentliche match
            Message::RechteZeigen => self.rechte_zeigen(),            Message::PfeilLinks
            | Message::PfeilRechts
            | Message::Umschalten
            | Message::Abbrechen => {} // ohne Dialog ohne Bedeutung            Message::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:

src/main.rsganz am Ende der Datei, hinter rechte_lesen
        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:

src/main.rsin 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.

Worauf du achten musst Bei offenem Dialog ruft 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.
Probelauf Speichern, dann im Terminal:
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 Rechte-Dialog mitten über den Panels
Stand nach Schritt 4: der Rechte-Dialog in Aktion
Zwischenstand

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,
    }
}
ZusammenfassungZugriffsrechte sind neun Bits, die man mit & 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.
Ü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. 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_setzen nach dem Vorbild von ordner_kopieren.
  2. Zifferneingabe: Profis tippen Rechte als Zahl. Lass im Dialog die Tasten 0 bis 7 die Ziffer der aktuellen Reihe setzen. Tipp: In taste liefert Key::Character(c) die getippte Taste als Text.
  3. 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