diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..61cf3bd --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,38 @@ +# Changelog + +All notable changes to this project will be documented in this file. +Format based on [Keep a Changelog](https://keepachangelog.com/). + +## [0.1.1] - 2026-03-28 + +### Fixed + +- Use absolute paths for all system binaries (`systemctl`, `loginctl`, `niri`, `moonlock`) to prevent PATH hijacking +- Implement `POWER_TIMEOUT` (30s) via `try_wait()` polling — previously declared but unused, leaving power actions able to block indefinitely +- Prevent panic in `load_background_texture` when GResource path contains non-UTF-8 bytes — now falls back to known wallpaper resource +- Fix fallback user UID from `0` (root) to `u32::MAX` as a safe sentinel value +- Fix CSS comment incorrectly describing circular buttons as "square card" + +### Changed + +- Compress wallpaper in GResource bundle (`compressed="true"`) to reduce binary size +- Merge double `idle_add_local_once` into single idle cycle for faster keyboard focus on launch +- Centralize `GRESOURCE_PREFIX` as `pub(crate) const` in `main.rs` (was duplicated in `config.rs`, `users.rs`, and literal strings in `panel.rs`) +- Translate README.md and config comments from German to English +- Remove stale `journal.md` (one-time development notes, not actively maintained) + +## [0.1.0] - 2026-03-27 + +### Added + +- Rust rewrite of the Python power menu (gtk4-rs + gtk4-layer-shell) +- 5 power actions: Lock, Logout, Hibernate, Reboot, Shutdown +- Inline confirmation for destructive actions (all except Lock) +- Multi-monitor wallpaper support via shared `gdk::Texture` +- DE/EN localization with automatic locale detection +- TOML configuration for custom wallpaper path +- GResource bundle for CSS, wallpaper, and default avatar +- Async power actions via `glib::spawn_future_local` + `gio::spawn_blocking` +- Async avatar loading (file-based avatars decoded off UI thread) +- Cached icon loading at startup +- 45 unit tests diff --git a/CLAUDE.md b/CLAUDE.md index 2b6beef..dfd9c1c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -17,7 +17,7 @@ Lock, Logout, Hibernate, Reboot, Shutdown. ## Projektstruktur - `src/` — Rust-Quellcode (main.rs, power.rs, i18n.rs, config.rs, users.rs, panel.rs) -- `resources/` — GResource-Assets (style.css, wallpaper.jpg, default-avatar.svg) +- `resources/` — GResource-Assets (style.css, wallpaper.jpg komprimiert, default-avatar.svg) - `config/` — Beispiel-Konfigurationsdateien ## Kommandos @@ -35,20 +35,24 @@ LD_PRELOAD=/usr/lib/libgtk4-layer-shell.so ./target/release/moonset ## Architektur -- `power.rs` — 5 Power-Action-Wrapper (lock, logout, hibernate, reboot, shutdown) +- `main.rs` — Entry Point, GTK App, Layer Shell Setup, Multi-Monitor, zentrale `GRESOURCE_PREFIX`-Konstante +- `power.rs` — 5 Power-Action-Wrapper mit absoluten Pfaden und 30s Timeout (lock, logout, hibernate, reboot, shutdown) - `i18n.rs` — Locale-Erkennung und String-Tabellen (DE/EN) - `config.rs` — TOML-Config + Wallpaper-Fallback - `panel.rs` — GTK4 UI (Action-Buttons, Inline-Confirmation, WallpaperWindow) -- `main.rs` — Entry Point, GTK App, Layer Shell Setup, Multi-Monitor -- `resources/style.css` — Catppuccin Mocha Theme (aus Python-Version übernommen) +- `users.rs` — User-Erkennung, Avatar-Loading (AccountsService, ~/.face, GResource-Fallback) +- `resources/style.css` — GTK-Theme-Colors für Konsistenz mit dem aktiven Desktop-Theme ## Design Decisions +Siehe `DECISIONS.md` für das vollständige Entscheidungsprotokoll. + +Kurzfassung der wichtigsten Entscheidungen: - **OVERLAY statt TOP Layer**: Waybar liegt auf TOP, moonset muss darüber - **Niri-spezifischer Logout** (`niri msg action quit`): Moonarch setzt fest auf Niri - **Einmal-Start per Keybind**: Kein Daemon, GTK `application_id` verhindert Doppelstart - **System-Icons**: Adwaita/Catppuccin liefern alle benötigten symbolischen Icons - **Lock ohne Confirmation**: Lock ist sofort reversibel, braucht kein Confirm -- **Icon-Scaling**: 22px Theme-Variante laden, auf 64px skalieren via GdkPixbuf -- **GResource-Bundle**: CSS, Wallpaper und Default-Avatar sind in die Binary kompiliert -- **Async Power Actions**: `glib::spawn_future_local` + `gio::spawn_blocking` statt raw Threads +- **Absolute Pfade für Binaries**: `/usr/bin/systemctl` etc. statt relativer Pfade (Security) +- **GResource-Bundle**: CSS, Wallpaper (komprimiert) und Default-Avatar sind in die Binary kompiliert +- **Async Power Actions**: `glib::spawn_future_local` + `gio::spawn_blocking` mit 30s Timeout diff --git a/Cargo.toml b/Cargo.toml index b87a3a6..3c4728e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "moonset" -version = "0.1.0" +version = "0.1.1" edition = "2024" description = "Wayland session power menu with GTK4 and Layer Shell" license = "MIT" diff --git a/DECISIONS.md b/DECISIONS.md new file mode 100644 index 0000000..7d8dd51 --- /dev/null +++ b/DECISIONS.md @@ -0,0 +1,52 @@ +# Decisions + +Architectural and design decisions for Moonset, in reverse chronological order. + +## 2026-03-28 – Use absolute paths for system binaries + +- **Who**: Hekate, Dom +- **Why**: Security audit flagged PATH hijacking risk — relative binary names allow a malicious `$PATH` entry to intercept `systemctl`, `loginctl`, etc. +- **Tradeoffs**: Hardcoded paths reduce portability to non-Arch distros where binaries may live elsewhere (e.g. `/sbin/`). Acceptable because Moonarch targets Arch Linux exclusively. +- **How**: All five power action wrappers now use `/usr/bin/` prefixed paths. + +## 2026-03-28 – Implement power action timeout via try_wait polling + +- **Who**: Hekate, Dom +- **Why**: `POWER_TIMEOUT` and `PowerError::Timeout` were declared but never wired up. A hanging `systemctl hibernate` (e.g. blocked NFS mount) would freeze the power menu indefinitely. +- **Tradeoffs**: Polling with `try_wait()` + 100ms sleep is slightly less efficient than a dedicated timeout crate, but avoids adding a dependency for a single use case. +- **How**: `run_command` now polls `child.try_wait()` against a 30s deadline, kills the child on timeout. + +## 2026-03-28 – Centralize GRESOURCE_PREFIX + +- **Who**: Hekate, Dom +- **Why**: The string `/dev/moonarch/moonset` was duplicated in `config.rs`, `users.rs`, and as literal strings in `panel.rs` and `main.rs`. Changing the application ID would require edits in 4+ locations. +- **Tradeoffs**: Modules now depend on `crate::GRESOURCE_PREFIX` — tighter coupling to main.rs, but acceptable for an internal constant. +- **How**: Single `pub(crate) const GRESOURCE_PREFIX` in `main.rs`, referenced everywhere else. + +## 2026-03-28 – Remove journal.md + +- **Who**: Hekate, Dom +- **Why**: One-time development notes from the Rust rewrite, never updated after initial session. Overlapped with memory system and git history. +- **Tradeoffs**: Historical context lost from the file, but the information is preserved in git history and the memory system. +- **How**: Deleted. Useful technical learnings migrated to persistent memory. + +## 2026-03-27 – OVERLAY layer instead of TOP + +- **Who**: Hekate, Dom +- **Why**: Waybar occupies the TOP layer. The power menu must appear above it. +- **Tradeoffs**: OVERLAY is the highest layer — nothing can render above moonset while it's open. This is intentional for a session power menu. +- **How**: `setup_layer_shell` uses `gtk4_layer_shell::Layer::Overlay` for the panel window. + +## 2026-03-27 – Lock without confirmation + +- **Who**: Hekate, Dom +- **Why**: Lock is immediately reversible (just unlock). All other actions (logout, hibernate, reboot, shutdown) are destructive or disruptive. +- **Tradeoffs**: One less click for the most common action. Risk of accidental lock is negligible since unlocking is trivial. +- **How**: `ActionDef.needs_confirm = false` for lock; all others require inline confirmation. + +## 2026-03-27 – Niri-specific logout via `niri msg action quit` + +- **Who**: Hekate, Dom +- **Why**: Moonarch is built exclusively for the Niri compositor. Generic Wayland logout mechanisms don't exist — each compositor has its own. +- **Tradeoffs**: Hard dependency on Niri. If the compositor changes, `power::logout()` must be updated. +- **How**: `Command::new("/usr/bin/niri").args(["msg", "action", "quit"])`.