Sie sind offline — einige Inhalte sind eventuell nicht aktuell.
Zurück zum Blog
Entwicklung

Unix vs. Windows — erklärt an einer einzigen Geste

UnixLinuxWindowsmacOSArchitekturHIDBetriebssysteme

Unix vs. Windows — erklärt an einer einzigen Geste

Ein scheinbar winziges Feature — „drei Finger über das Touchpad nach oben = lauter” — legt fast die gesamte Designphilosophie zweier Betriebssystem-Welten offen. Dieser Beitrag verfolgt diese eine Geste vom Glas unter Ihren Fingern bis zur Aktion „Lautstärke +1” und zeigt an jeder Station, wo sich Unix (Linux, im Kern auch macOS) und Windows unterscheiden — und warum.

Der Text ist bewusst für Leser geschrieben, die keine Treiberentwickler sind. Fachbegriffe werden eingeführt, nicht vorausgesetzt. Aber er bleibt nicht an der Oberfläche.

Die Ausgangslage: Was muss überhaupt passieren?

Zwischen „Finger berührt Touchpad” und „Lautstärke wird lauter” liegt eine erstaunlich lange Kette:

  1. Das Touchpad muss merken, dass drei Finger drauf sind (nicht einer, nicht zwei).
  2. Es muss merken, dass die sich nach oben bewegen — und nicht nur zucken.
  3. Irgendeine Schicht muss aus „drei Punkte wandern hoch” die Bedeutung „Swipe up mit 3 Fingern” machen.
  4. Diese Bedeutung muss bei Ihrem Programm ankommen.
  5. Das Programm löst die Aktion aus.

Die entscheidende Frage, an der sich alles aufhängt, ist Punkt 3 und 4: Wer macht aus rohem Gezappel eine fertige Geste — und ab welcher Schicht darf ein normales Programm überhaupt mithören?

Genau hier trennen sich die Welten.

Die gemeinsame Basis: HID

Beide Welten starten gleich. Moderne Laptop-Touchpads sind HID-Geräte (Human Interface Device — derselbe Standard wie bei Tastaturen und Mäusen). Das Touchpad schickt viele Male pro Sekunde kleine Datenpakete, sogenannte HID-Reports. In so einem Report steht roh: „Kontakt 1 bei X=812, Y=440, Finger liegt auf; Kontakt 2 bei …; Anzahl gleichzeitiger Kontakte = 3.”

Wichtig: Auf dieser Ebene gibt es noch keine Geste. Da sind nur Koordinaten und eine Kontaktanzahl, Frame für Frame. Aus diesem Strom „drei wandernde Punkte” eine Geste zu machen, ist die eigentliche Arbeit — und genau die machen die beiden Systeme komplett unterschiedlich.

Microsoft hat übrigens einen Standard namens Precision Touchpad definiert, damit alle Hersteller ihre HID-Reports einheitlich aufbauen. Linux profitiert davon mit, weil es denselben HID-Standard liest. So weit Burgfrieden.

Der Linux-Weg: ein Stapel offener, austauschbarer Schichten

Linux verarbeitet das Touchpad in klar getrennten Etagen, von unten nach oben:

Etage 1 — Der Kernel macht aus Hersteller-Wirrwarr ein einheitliches Vokabular

Im Linux-Kernel sitzt ein Treiber namens hid-multitouch. Er liest die rohen HID-Reports und übersetzt sie in evdev-Events — ein standardisiertes, herstellerunabhängiges Ereignis-Vokabular. Statt „Firma X codiert Finger so, Firma Y anders” gibt es ab hier einheitliche Bausteine wie ABS_MT_POSITION_X (Position eines Kontakts), ABS_MT_SLOT (welcher Finger), ABS_MT_TRACKING_ID und — das ist hübsch — fertige Signale wie BTN_TOOL_TRIPLETAP, das schlicht bedeutet: „genau drei Finger liegen auf.”

Schon der Kernel sagt also die Fingeranzahl. Und — sehr Unix-typisch — das Gerät erscheint als Datei im System: /dev/input/event5 oder ähnlich. „Everything is a file” ist keine Floskel: Sie können diesen Ereignisstrom mit Standard-Werkzeugen (evtest) direkt mitlesen und sich live ansehen, was Ihre Finger auslösen. Die Schnittstelle ist ein les- und inspizierbarer Strom, kein verschlossener Kasten.

Etage 2 — libinput macht aus Bewegung eine Geste

Darüber liegt eine eigenständige Bibliothek namens libinput. Sie liest die evdev-Events aus /dev/input/, hält pro Gerät Buch, macht Dinge wie Handballen-Erkennung und Zeigerbeschleunigung — und vor allem: Sie hat eine Gestenerkennung eingebaut. libinput beobachtet die Finger-Slots, zählt die Kontakte, integriert deren Bewegung und erzeugt daraus fertige, benannte Ereignisse:

LIBINPUT_EVENT_GESTURE_SWIPE_BEGIN…_UPDATE…_END, jeweils mit finger_count (= 3) und aufsummiertem dx/dy (= wie weit, in welche Richtung).

Das ist der entscheidende Punkt: Die schwierige Arbeit — aus Gezappel eine Geste machen — ist hier schon erledigt, in einem fertigen Baustein. Sie müssen nur noch hinhören: „finger_count == 3 und dy ist negativ” = drei Finger nach oben.

Und dieser Baustein ist geteilt. Dieselbe libinput wird vom alten X11-Grafikserver (über xf86-input-libinput) und von jedem modernen Wayland-Compositor benutzt. Eine Implementierung, von allen wiederverwendet. Das ist der Unix-Gedanke in Reinform: ein Werkzeug, das eine Sache gut kann, an einer sauberen Schnittstelle, von allen genutzt.

Etage 3 — Der Compositor verteilt (und hier lauert der Konflikt)

Über libinput sitzt die grafische Oberfläche (z. B. GNOMEs Compositor „Mutter”). Sie bekommt dieselben Gesten und macht ihre eigene Politik daraus — auf GNOME ist 3-Finger-Wischen oft schon mit Workspace-Wechsel belegt.

Wo Ihr Programm andockt

Sie haben zwei Möglichkeiten:

  • Direkt an libinput andocken: einen eigenen „Sitz” öffnen und die Gesten-Events mitlesen — exakt das machen Tools wie libinput-gestures oder fusuma. Sie brauchen dafür Leserechte auf /dev/input/event* (über die input-Gruppe bzw. eine udev-Regel). Sie hören dann parallel zum Compositor mit — beobachtend, nicht blockierend.

Der Aufwand für die Lautstärke-Geste ist damit klein: Sie klinken sich in eine bestehende Schicht ein, die Ihnen die Geste fertig liefert. Ein Wochenende, inklusive Feintuning.

Der Windows-Weg: eine integrierte Black Box — und ein roher Notausgang

Windows verarbeitet exakt dieselbe Hardware, aber die Architektur dahinter ist gegensätzlich.

Die Geste gehört dem System — nicht Ihnen

Der Windows-eigene Precision-Touchpad-Treiber liest die HID-Reports und füttert die Kontakte direkt in die Gesten-Engine des Betriebssystems. Diese Engine entscheidet selbst: 3-Finger-Wisch = App-Umschalter / Task-Ansicht, 4-Finger = virtuelle Desktops. Die Interpretation „das war ein 3-Finger-Swipe” passiert fest verdrahtet im OS — und wird nicht als wiederverwendbares Ereignis nach außen gereicht, an dem Sie sich anhängen könnten.

Es gibt zwar App-Schnittstellen für Berührungen — WM_GESTURE, WM_TOUCH, die WM_POINTER*-Nachrichten —, aber die stammen aus der Touchscreen-Welt (Tablet, Stift, Finger auf dem Bildschirm) und sind an ein Fenster gebunden. Für „Mehr-Finger-Geste irgendwo auf dem Touchpad, auch wenn mein Programm gar nicht im Vordergrund ist” gibt es schlicht keine fertige, hohe API. Das hat sich das System für sich behalten.

Der einzige robuste Weg: die Raw Input API — also rohe HID-Daten selbst zerlegen

Bleibt der „Notausgang”: Sie gehen eine Etage tiefer und lesen die rohen HID-Reports selbst — über die Raw Input API. Das sieht konkret so aus:

  1. Registrieren: Mit RegisterRawInputDevices melden Sie Interesse an „Usage Page 0x0D (Digitizer), Usage 0x05 (Touch Pad)” an. Mit dem Flag RIDEV_INPUTSINK bekommen Sie die Daten auch dann, wenn Ihr Fenster gar nicht den Fokus hat — nötig, weil Ihr Programm ja im Tray im Hintergrund läuft. (Das Ganze hängt an einem Fenster-Handle, HWND.)
  2. Empfangen: Windows schickt Ihnen die Nachricht WM_INPUT in die Nachrichtenschleife. Mit GetRawInputData holen Sie sich ein RAWINPUT-Paket, in dem die rohen HID-Bytes (RAWHID) stecken.
  3. Entschlüsseln: Diese Bytes sind ohne Bauplan bedeutungslos. Den Bauplan — den HID Report Descriptor — holen Sie sich mit GetRawInputDeviceInfo (Flag RIDI_PREPARSEDDATA) und zerlegen die Reports dann Feld für Feld mit dem HID-Parser des Systems: HidP_GetCaps, HidP_GetValueCaps, HidP_GetUsageValue, HidP_GetUsages. Erst dadurch kommen Sie pro Frame an: X, Y, Contact ID, „Finger liegt auf?” (Tip Switch) und die Kontaktanzahl. Oft sind mehrere Kontakte in einem Report gepackt, manchmal kommen mehrere Reports pro physischem Frame — das müssen Sie korrekt zusammensetzen.
  4. Die Geste selbst bauen: Jetzt erst beginnt die Arbeit, die libinput unter Linux geschenkt liefert. Sie bauen die Erkennungs-Zustandsmaschine: Frames über die Zeit sammeln, gleichzeitige Kontakte zählen (3?), Bewegungsvektoren integrieren, Geschwindigkeit berechnen, Richtung klassifizieren, entprellen, und für den Tap erkennen: drei Kontakte, kurze Dauer, kaum Bewegung. Alle Schwellwerte müssen Sie selbst justieren, damit sich die Geste nicht „klebrig” oder zickig anfühlt.

Zwei harte Einschränkungen, die die Philosophie entlarven

  • Raw Input ist nur lesend. Sie beobachten — Sie blockieren nicht. Windows feuert seine eigene 3-Finger-Geste parallel weiter. Es gibt keine öffentliche Möglichkeit, dem System die Geste „wegzunehmen”. Praktisch heißt das: Der Nutzer muss in den Touchpad-Einstellungen die 3-/4-Finger-Gesten auf „Nichts” stellen, sonst löst beides gleichzeitig aus.
  • Nur echte Precision Touchpads. Ältere Synaptics-/ELAN-Pads mit Proprietär-Treiber liefern die standardisierten HID-Strukturen nicht sauber. Dann bräuchten Sie die geschlossene Hersteller-SDK — die es meist gar nicht öffentlich gibt. Ende der Fahnenstange.

Für dasselbe Feature bauen Sie auf Windows also die komplette Gestenerkennung von Hand, kämpfen mit gerätespezifischen Eigenheiten und können die Doppel-Auslösung nicht sauber unterbinden. Deutlich mehr Aufwand — für ein identisches Ergebnis.

Was diese eine Geste über die zwei Welten verrät

1. Wo lebt die Abstraktion — und wem gehört sie?

  • Unix/Linux: Die wertvolle Abstraktion (Gestenerkennung) lebt in einem offenen, austauschbaren Baustein im Userspace (libinput), der auf einer dünnen Kernel-Mechanik (evdev) aufsetzt. Sie dürfen auf jeder Etage einsteigen.
  • Windows: Die Abstraktion ist ins OS integriert und für das OS reserviert. Fremde Programme bekommen entweder eine fertige API nur für die vom Hersteller vorgesehenen Fälle — oder den rohen Datenschlauch ganz unten. Die schöne Mitte gehört dem System.

2. Der Stil der Schnittstelle

  • Unix: Gerät = Datei, Ereignisse = lesbarer, inspizierbarer Strom (/dev/input, mit evtest live mitlesbar). Offen, nachvollziehbar.
  • Windows: Registrieren, Fenster-Nachrichten empfangen, eine C-API gegen undurchsichtige „preparsed data” aufrufen. Rechte-gebunden, fenster-gebunden, an die Nachrichtenschleife geknüpft.

3. Offenheit & Wiederverwendung

  • Unix: Jede Schicht ist Open Source. libinput wird von X11 und allen Wayland-Compositors geteilt. Sie können exakt nachlesen, wie ein Swipe erkannt wird — und es notfalls forken.
  • Windows: Die Gesten-Engine ist geschlossen. Sie arbeiten mit dem, was dokumentiert ist, oder reverse-engineeren das Verhalten.

4. Der eigentliche Kern: Komposition vs. Integration

  • Unix-Philosophie: kleine Teile, jedes macht eine Sache, über stabile Schnittstellen kombinierbar. Mechanismus (Kernel liefert Kontakte; libinput liefert Gesten) ist von Politik (Compositor/Sie entscheiden, was die Geste tut) getrennt. Sie dürfen Ihre eigene Politik einsetzen.
  • Windows-Philosophie: ein zusammenhängendes, poliertes, integriertes Ganzes, in dem das System die Politik vorgibt und fertiges Verhalten präsentiert. Hervorragende Standard-Erfahrung ab Werk — aber wenig Platz, sich dazwischenzuschieben.

5. Die ehrliche Nuance: macOS bricht das simple Schema

Es wäre zu billig, daraus „offen = gut, geschlossen = schlecht” zu machen. macOS ist im Kern selbst Unix — und reicht an der Oberfläche trotzdem eine sehr saubere, hohe Gesten-API (NSEvent/NSTouch) hinüber, schöner noch als libinput. Apple kann das, weil ihm Hardware und OS gehören. Das zeigt: Die wahre Achse ist nicht „Unix gegen Windows”, sondern:

Wie viel fertige Abstraktion reicht die Plattform an fremde Programme weiter — und auf welcher Etage darf man sich einklinken?

Linux gibt eine fertige, offene Etage zum Andocken. macOS gibt eine fertige, geschlossene, aber komfortable Etage. Windows behält die fertige Etage für sich und lässt einen nur ganz unten an den rohen Strom — oder ganz oben an seine vordefinierten Knöpfe.

Zusammengefasst in einer Tabelle

FrageLinux (Unix)WindowsmacOS (Unix-Kern)
Wer erkennt die Geste?libinput (offener, geteilter Baustein)das OS selbst, interndas OS selbst, sauber als API
Bekommen Sie die fertige Geste?Ja, direkt aus libinputNein — nur rohe HID-FramesJa, über NSEvent
Wo klinken Sie sich ein?Mittlere Schicht (libinput / evdev)Ganz unten (Raw Input) oder ganz obenHohe, fertige Schicht
Eigenen Code zur Erkennung nötig?KaumJa, komplette ZustandsmaschineKaum
SchnittstelleDatei-/Ereignisstrom, inspizierbarFenster-Nachrichten + HID-Parser-APIObjektorientierte Event-API
Quellcode einsehbar?Ja, jede SchichtNeinTeilweise (Darwin-Kern), GUI nein
Aufwand für die Lautstärke-Gestekleingroßklein

Wenn man das in Code gießen will: eine plattformübergreifende Gesten-Engine

Theorie ist das eine. Was passiert, wenn Sie eine echte Desktop-App bauen, die auf allen drei Systemen dieselbe 3-Finger-Geste zur Lautstärke-Steuerung anbieten soll? Genau diese Frage habe ich für eine Tauri-/Rust-Anwendung durchgespielt — und die Antwort ist überraschend stark vom Architektur-Stil abhängig, den Sie vor den OS-Eigenheiten wählen.

Architektur: Trigger ≠ Aktion

Der Schlüssel ist eine entkoppelte Pipeline, die Mechanismus von Politik trennt — genau das Unix-Pattern, das wir oben gesehen haben, nur jetzt in den App-Code übernommen:

[Trigger]  →  emittiert TriggerEvent  →  [Mapping/Config]  →  dispatcht  →  [SystemCommand]  →  [Executor]
 Geste                                    (Geste → Befehl)                    Befehls-Katalog        OS-spezifisch
 (später: Hotkey,
  Command Palette)
  • Trigger (Touchpad-Geste) wissen nichts von Aktionen — sie feuern nur ein normalisiertes TriggerEvent.
  • System-Befehle (SystemCommand) wissen nichts von Triggern — sie sind ein eigenständiger Katalog.
  • Eine Config (zur Laufzeit änderbar) verbindet beides: Geste X → Befehl Y.

Ergebnis: Derselbe Befehl ist per Geste, später per Hotkey und per Command-Palette aufrufbar — ohne Code-Duplizierung. Genau das, was libinput auf Linux schon für die Geste selbst macht: einen geteilten Baustein an einer sauberen Schnittstelle.

Linux-Trigger: die input-Crate als libinput-Wrapper

Auf Linux ist die Trigger-Schicht in Rust angenehm kompakt. Die input-Crate wickelt libinput in einen idiomatischen Iterator:

use input::{Libinput, LibinputInterface};
use input::event::{Event, GestureEvent};
use input::event::gesture::{GestureSwipeEvent, GestureEventCoordinates, GestureEventTrait};

let mut li = Libinput::new_with_udev(MyInterface);
li.udev_assign_seat("seat0").unwrap();

for event in li.by_ref() {
    if let Event::Gesture(GestureEvent::Swipe(swipe)) = event {
        match swipe {
            GestureSwipeEvent::Begin(b)  if b.finger_count() == 3 => start_state_machine(),
            GestureSwipeEvent::Update(u) if u.finger_count() == 3 => accumulate(u.dx(), u.dy()),
            GestureSwipeEvent::End(_)                              => emit_trigger_event(),
            _ => {}
        }
    }
}

finger_count, dx, dy werden Ihnen direkt geliefert. Sie müssen nichts integrieren, nichts entprellen, keine Zustandsmaschine schreiben. Die Berechtigung auf /dev/input/event* regelt eine udev-Regel oder die Mitgliedschaft in der input-Gruppe — der App-Bundle dokumentiert das in einer README.

3-Finger-Tap ist eine eigene Geschichte: libinput exponiert ihn nicht als fertiges Event, sondern Sie greifen auf GESTURE_HOLD_* zurück oder lesen rohe Touch-Slots und prüfen „drei Kontakte, kurze Dauer, kaum Bewegung”. Trotzdem deutlich näher an „business logic” als an „protocol parsing”.

Windows-Trigger: Raw Input API + HID-Parser im Detail

Auf Windows beginnt die Arbeit, die libinput unter Linux geschenkt liefert. Vier konkrete Schritte:

1. Registrieren

RAWINPUTDEVICE rid = {0};
rid.usUsagePage = 0x0D;          // Digitizer
rid.usUsage     = 0x05;          // Touch Pad
rid.dwFlags     = RIDEV_INPUTSINK; // auch ohne Fokus empfangen — App lebt im Tray
rid.hwndTarget  = hwnd;            // an ein (auch unsichtbares) Fenster gebunden
RegisterRawInputDevices(&rid, 1, sizeof(rid));

Das RIDEV_INPUTSINK-Flag ist der Knackpunkt: Eine Tray-App ohne Fokus bekommt sonst keine Events. Und ja — das Ganze ist an ein Fenster-Handle gebunden, selbst wenn das Fenster nie sichtbar wird.

2. Empfangen

Windows ruft die Nachrichtenschleife auf:

case WM_INPUT: {
    UINT size = 0;
    GetRawInputData((HRAWINPUT)lParam, RID_INPUT, nullptr, &size, sizeof(RAWINPUTHEADER));
    std::vector<BYTE> buf(size);
    GetRawInputData((HRAWINPUT)lParam, RID_INPUT, buf.data(), &size, sizeof(RAWINPUTHEADER));
    auto* ri = reinterpret_cast<RAWINPUT*>(buf.data());
    auto* hid = &ri->data.hid;
    // hid->bRawData enthält dwSizeHid * dwCount Bytes
}

3. Entschlüsseln

Diese Bytes sind ohne Bauplan bedeutungslos. Den HID Report Descriptor holen Sie sich separat — und übergeben ihn dem HID-Parser:

// Pre-parsed data einmal pro Gerät cachen
UINT ppdSize = 0;
GetRawInputDeviceInfo(ri->header.hDevice, RIDI_PREPARSEDDATA, nullptr, &ppdSize);
PHIDP_PREPARSED_DATA ppd = (PHIDP_PREPARSED_DATA)malloc(ppdSize);
GetRawInputDeviceInfo(ri->header.hDevice, RIDI_PREPARSEDDATA, ppd, &ppdSize);

HIDP_CAPS caps;
HidP_GetCaps(ppd, &caps);

// Pro Report: X/Y/Contact-ID/Tip-Switch + Contact-Count auslesen
ULONG x, y;
HidP_GetUsageValue(HidP_Input, 0x01, 0, 0x30, &x, ppd, (PCHAR)hid->bRawData, hid->dwSizeHid);
HidP_GetUsageValue(HidP_Input, 0x01, 0, 0x31, &y, ppd, (PCHAR)hid->bRawData, hid->dwSizeHid);
// Tip-Switch (Usage Page 0x0D, Usage 0x42) via HidP_GetUsages
// Contact-Count (Usage 0x54) via HidP_GetUsageValue

Oft sind mehrere Kontakte in einem Report gepackt; manchmal kommen mehrere Reports pro physischem Frame. Sie müssen die korrekt zusammensetzen, sonst flackert die erkannte Anzahl der Finger.

4. Die Geste selbst bauen

Erst jetzt beginnt die Zustandsmaschine, die libinput Ihnen geschenkt hat:

// Pseudocode der Erkennungs-Logik
match state {
    Idle                              if frame.contacts == 3 => state = Tracking { start_t, accum_dy: 0.0 },
    Tracking { start_t, accum_dy }    if frame.contacts == 3 => accum_dy += avg_dy(frame),
    Tracking { start_t, accum_dy }    if frame.contacts < 3  => {
        let dur = now() - start_t;
        if dur < TAP_MAX_MS && accum_dy.abs() < TAP_DRIFT_PX {
            emit(TriggerEvent::ThreeFingerTap);
        } else if accum_dy < -SWIPE_MIN_PX {
            emit(TriggerEvent::ThreeFingerSwipeUp);
        } else if accum_dy >  SWIPE_MIN_PX {
            emit(TriggerEvent::ThreeFingerSwipeDown);
        }
        state = Idle;
    }
    _ => {}
}

Alle Schwellwerte — SWIPE_MIN_PX, TAP_MAX_MS, TAP_DRIFT_PX — müssen Sie selbst justieren. Zu eng → die Geste fühlt sich zickig an. Zu locker → jedes Zucken löst aus. Das ist die Arbeit, die libinput unter Linux einmal für alle erledigt hat.

Zwei harte Einschränkungen, die bleiben

  • Raw Input ist nur lesend. Windows feuert seine eigene 3-Finger-Geste parallel weiter. Sie können dem System die Geste nicht „wegnehmen”. Praktisch heißt das: Der Nutzer muss in den Touchpad-Einstellungen die 3-/4-Finger-Gesten auf „Nichts” stellen, sonst löst beides gleichzeitig aus. Das muss der App-Onboarding-Schritt dokumentieren.
  • Nur echte Precision Touchpads. Ältere Synaptics-/ELAN-Pads mit Proprietär-Treiber liefern die standardisierten HID-Strukturen nicht sauber. Dann bräuchten Sie das geschlossene Hersteller-SDK — das es öffentlich meist nicht gibt. Ende der Fahnenstange. Auch das gehört in die README.

Wayland: graceful degradation als Pflicht

Linux ist nicht überall gleich. Unter X11 bekommen Sie über xcb/xdotool bequem an aktive Fenster, können sie verschieben, schließen, kacheln. Unter Wayland ist genau das generisch nicht möglich — jeder Compositor entscheidet selbst, ob er einer App Fenster-Management erlaubt, und die meisten erlauben es nicht.

Die saubere Lösung im App-Design ist eine Capability-Map: Jeder Befehl trägt Plattform-Flags (linux_x11, linux_wayland, windows, macos). Nicht unterstützte Befehle werden in der Settings-UI ausgegraut, mit Tooltip „unter Wayland nicht verfügbar”. Ein nicht unterstützter Befehl darf niemals crashen — er liefert einen klaren CommandError::Unsupported. Wo es geht, wird auf Key-Emulation degradiert (z. B. Super + ←/→ statt direktem Snap-Call).

Der portable Befehls-Katalog: SystemCommand

Auf der anderen Seite der Pipeline steht das, was nach dem Trigger passiert. Ein typisches Enum sieht so aus:

// serde-serialisierbar, da in Config persistiert
#[derive(Serialize, Deserialize, Clone)]
pub enum SystemCommand {
    // Audio
    VolumeUp, VolumeDown, VolumeSet(u8), MuteToggle,
    MediaPlayPause, MediaNext, MediaPrev,
    // Display
    BrightnessUp, BrightnessDown,
    // Fenster (aktives Fenster, plattformabhängig)
    WindowMinimize, WindowMaximizeRestore, WindowClose,
    WindowSnapLeft, WindowSnapRight, ShowDesktop,
    // App-/Desktop-Wechsel
    AppSwitchNext, DesktopNext, DesktopPrev,
    // System
    LockScreen, Sleep, ScreenshotRegion, ScreenshotFull,
    // Generische Editier-/Navigationsbefehle (via Key-Emulation)
    Copy, Paste, Undo, Redo, ZoomIn, ZoomOut,
    BrowserBack, BrowserForward, TabNext, TabPrev,
    // Parametrierte/erweiterbare Befehle
    LaunchApp(String),
    RunShell(String),              // NUR aus lokaler User-Config — nie aus Fremdquellen
    SendHotkey(Vec<KeySpec>),
}

Dahinter steht ein CommandExecutor-Trait. Drei Executor-Implementierungen verteilen die Arbeit:

  • Portabler Executor über enigo für alles, was reine Key-Emulation ist: Copy/Paste/Undo, Media-Keys, beliebige Hotkeys.
  • Windows-Executor (windows-rs): GetForegroundWindow + ShowWindow(SW_MINIMIZE/MAXIMIZE) für Fenster, LockWorkStation für Sperren, IAudioEndpointVolume für Audio, WmiMonitorBrightnessMethods für Helligkeit. Virtual-Desktop-Wechsel besser über Win+Ctrl+←/→ per Key-Emulation als über die instabile COM-API.
  • Linux-Executor: X11-Fenster-API wo möglich, sonst CLI-Fallbacks (brightnessctl, loginctl lock-session, systemctl suspend), Wayland → Key-Emulation oder Capability-Off.

RunShell(String) und LaunchApp(String) werden ausschließlich aus der lokalen User-Config gespeist — nie aus Clipboard, Netzwerk oder Fremdquellen. Das ist nicht nur eine Code-Konvention, das ist eine harte Sicherheitsregel: Sobald irgendeine externe Stelle einen Shell-Befehl einspielen darf, haben Sie effektiv einen Shell-Injection-Vektor in eine App eingebaut, die mit OS-Berechtigung Lautstärke regeln darf. Im Kommentar steht das genauso drin.

Was das in Aufwand-Zahlen heißt

PlattformTrigger-ErkennungExecutor-Komplexität
Linux X11input-Crate liefert fertige Swipe-Events — < 50 LOCFenster-API + CLI-Tools — überschaubar
Linux WaylandditoWindow-Management eingeschränkt, Fallback auf Keys
WindowsRaw Input API + HID-Parser + State-Machine — ~500 LOCwindows-rs deckt fast alles ab, nur instabile Virtual-Desktops
macOSNSEvent / NSTouchesEvent — ~30 LOCAccessibility-API für Fenster, sonst Key-Emulation

Linux: ein Wochenende. Windows: zwei bis drei Wochen, mit Mess-Sessions am echten Touchpad-Gerät, weil sich die Schwellwerte sonst nie richtig anfühlen. macOS: ein Nachmittag, sobald Sie die Accessibility-Berechtigung haben.

Die Tabelle zementiert die Erkenntnis von oben in konkreten Aufwand. Die Architektur — Trigger ≠ Aktion, Capability-Map, graceful degradation — ist dabei OS-übergreifend identisch. Nur eine einzige Schicht (die Trigger-Erkennung) trägt die ganze Last des Unterschieds.


Eine einzige Drei-Finger-Wischbewegung — und man hat den Unterschied zwischen „aus Bausteinen selbst zusammensetzen” und „fertige, geschlossene Erfahrung konsumieren” einmal komplett durchdekliniert. In der Theorie an der Architektur, in der Praxis an der HID-Parser-State-Machine, die Sie auf Windows selbst schreiben — und auf Linux nie sehen müssen.