From 664298b6b4ddc297f980f8194f065531ef1aad69 Mon Sep 17 00:00:00 2001 From: Breadway Date: Sun, 5 Jul 2026 09:16:14 +0800 Subject: [PATCH] Extract bos-settings to its own repo; add breadhelp; JSON-driven Hyprland config bos-settings moves to git.breadway.dev/Breadway/bos-settings (full history preserved via git-filter-repo) so its release cadence is decoupled from BOS's own. breadhelp takes its place as this repo's workspace member: a GTK4 onboarding/help center replacing the old bos-welcome/bos-keybinds bash scripts with searchable guides, an interactive keybind viewer (sourced from the new keybinds.toml, not parsed out of hyprland.lua or hardcoded), a troubleshooting wizard with one-click fixes, and a proper first-run tour. bos-netcheck extracts bos-welcome's network-check half, which still needs to run every login independent of breadhelp's own first-run gating. hyprland.lua's keybinds/settings/monitors/autostart are now JSON-driven (binds.json/settings.json/monitors.json/autostart.json) with every loader pcall-wrapped and falling back to hardcoded defaults per field on bad or missing config, so bread* apps (bos-settings' new editors, and breadhelp's keybind viewer) can read/write this config without ever being able to leave the compositor unable to start. CI's package.yml now builds breadhelp instead of bos-settings on tag push; bos-settings needs its own equivalent workflow in its new repo (not yet set up). --- .forgejo/workflows/package.yml | 14 +- Cargo.lock | 26 +- Cargo.toml | 2 +- README.md | 75 ++- bos-settings/Cargo.toml | 19 - bos-settings/src/config/mod.rs | 213 -------- bos-settings/src/main.rs | 13 - bos-settings/src/theme.rs | 56 -- bos-settings/src/ui/mod.rs | 4 - bos-settings/src/ui/sidebar.rs | 141 ----- bos-settings/src/ui/views/about.rs | 172 ------ bos-settings/src/ui/views/aur.rs | 169 ------ bos-settings/src/ui/views/bread.rs | 160 ------ bos-settings/src/ui/views/breadbar.rs | 93 ---- bos-settings/src/ui/views/breadbox.rs | 210 -------- bos-settings/src/ui/views/breadclip.rs | 33 -- bos-settings/src/ui/views/breadcrumbs.rs | 467 ---------------- bos-settings/src/ui/views/breadpad.rs | 146 ----- bos-settings/src/ui/views/breadpaper.rs | 163 ------ bos-settings/src/ui/views/breadsearch.rs | 115 ---- bos-settings/src/ui/views/datetime.rs | 107 ---- bos-settings/src/ui/views/firewall.rs | 246 --------- bos-settings/src/ui/views/firmware.rs | 159 ------ bos-settings/src/ui/views/hyprland.rs | 85 --- bos-settings/src/ui/views/mod.rs | 20 - bos-settings/src/ui/views/network.rs | 328 ------------ bos-settings/src/ui/views/packages.rs | 205 ------- bos-settings/src/ui/views/power.rs | 193 ------- bos-settings/src/ui/views/snapshots.rs | 256 --------- bos-settings/src/ui/views/sound.rs | 167 ------ bos-settings/src/ui/views/users.rs | 293 ---------- bos-settings/src/ui/widgets.rs | 506 ------------------ bos-settings/src/ui/window.rs | 74 --- breadhelp/Cargo.toml | 17 + breadhelp/content/README.md | 28 + .../01-snapshots-and-rollback/content.md | 13 + .../01-snapshots-and-rollback/meta.toml | 5 + .../apps/01-notes-and-tasks/content.md | 10 + .../content/apps/01-notes-and-tasks/meta.toml | 5 + .../01-bar-and-launcher/content.md | 7 + .../01-bar-and-launcher/meta.toml | 5 + .../01-searching-your-files/content.md | 9 + .../01-searching-your-files/meta.toml | 5 + .../daily-use/02-clipboard-history/content.md | 7 + .../daily-use/02-clipboard-history/meta.toml | 5 + .../01-what-is-bos/content.beginner.md | 10 + .../getting-started/01-what-is-bos/content.md | 21 + .../getting-started/01-what-is-bos/meta.toml | 5 + .../02-essential-keybinds/content.md | 23 + .../02-essential-keybinds/meta.toml | 5 + .../03-personalizing-bos/content.md | 18 + .../03-personalizing-bos/meta.toml | 5 + .../troubleshooting/01-fixing-wifi/content.md | 13 + .../troubleshooting/01-fixing-wifi/meta.toml | 5 + .../troubleshooting/_symptoms/no-sound.toml | 35 ++ .../_symptoms/wifi-wont-connect.toml | 25 + breadhelp/src/cli.rs | 37 ++ breadhelp/src/config.rs | 135 +++++ breadhelp/src/content/keybinds.rs | 126 +++++ breadhelp/src/content/markdown.rs | 230 ++++++++ breadhelp/src/content/meta.rs | 28 + breadhelp/src/content/mod.rs | 145 +++++ breadhelp/src/content/troubleshoot.rs | 66 +++ breadhelp/src/main.rs | 34 ++ breadhelp/src/services/breadd.rs | 16 + breadhelp/src/services/exec.rs | 28 + breadhelp/src/services/hyprland.rs | 15 + breadhelp/src/services/mod.rs | 3 + breadhelp/src/theme.rs | 39 ++ breadhelp/src/ui/ask.rs | 173 ++++++ breadhelp/src/ui/guide_view.rs | 20 + breadhelp/src/ui/home.rs | 181 +++++++ breadhelp/src/ui/keybind_viewer.rs | 115 ++++ breadhelp/src/ui/learn.rs | 100 ++++ breadhelp/src/ui/mod.rs | 10 + breadhelp/src/ui/modes.rs | 25 + breadhelp/src/ui/onboarding/mod.rs | 196 +++++++ breadhelp/src/ui/onboarding/steps.rs | 51 ++ breadhelp/src/ui/tabs.rs | 14 + breadhelp/src/ui/troubleshoot_wizard.rs | 144 +++++ breadhelp/src/ui/window.rs | 142 +++++ .../bread/modules/breadhelp-suggest.lua | 16 + .../etc/skel/.config/hypr/autostart.json | 8 + iso/airootfs/etc/skel/.config/hypr/binds.json | 88 +++ .../etc/skel/.config/hypr/hyprland.lua | 206 +++---- .../etc/skel/.config/hypr/monitors.json | 5 + .../.config/hypr/scripts/display/monitors.lua | 56 ++ .../skel/.config/hypr/scripts/input/binds.lua | 35 ++ .../.config/hypr/scripts/input/keybinds.lua | 124 +++++ .../skel/.config/hypr/scripts/lib/json.lua | 180 +++++++ .../.config/hypr/scripts/system/autostart.lua | 45 ++ .../skel/.config/hypr/scripts/ui/settings.lua | 121 +++++ .../etc/skel/.config/hypr/settings.json | 19 + iso/airootfs/usr/local/bin/bos-keybinds | 4 - iso/airootfs/usr/local/bin/bos-netcheck | 35 ++ iso/airootfs/usr/local/bin/bos-welcome | 48 -- iso/airootfs/usr/share/bos/keybinds.txt | 53 -- iso/airootfs/usr/share/bos/welcome.txt | 24 - iso/packages.x86_64 | 5 +- iso/profiledef.sh | 3 +- packaging/arch/README.md | 24 +- packaging/arch/bos-settings.desktop | 9 - packaging/arch/{ => breadhelp}/PKGBUILD | 18 +- packaging/arch/breadhelp/breadhelp.desktop | 9 + scripts/smoke-test.sh | 10 + 105 files changed, 3280 insertions(+), 5149 deletions(-) delete mode 100644 bos-settings/Cargo.toml delete mode 100644 bos-settings/src/config/mod.rs delete mode 100644 bos-settings/src/main.rs delete mode 100644 bos-settings/src/theme.rs delete mode 100644 bos-settings/src/ui/mod.rs delete mode 100644 bos-settings/src/ui/sidebar.rs delete mode 100644 bos-settings/src/ui/views/about.rs delete mode 100644 bos-settings/src/ui/views/aur.rs delete mode 100644 bos-settings/src/ui/views/bread.rs delete mode 100644 bos-settings/src/ui/views/breadbar.rs delete mode 100644 bos-settings/src/ui/views/breadbox.rs delete mode 100644 bos-settings/src/ui/views/breadclip.rs delete mode 100644 bos-settings/src/ui/views/breadcrumbs.rs delete mode 100644 bos-settings/src/ui/views/breadpad.rs delete mode 100644 bos-settings/src/ui/views/breadpaper.rs delete mode 100644 bos-settings/src/ui/views/breadsearch.rs delete mode 100644 bos-settings/src/ui/views/datetime.rs delete mode 100644 bos-settings/src/ui/views/firewall.rs delete mode 100644 bos-settings/src/ui/views/firmware.rs delete mode 100644 bos-settings/src/ui/views/hyprland.rs delete mode 100644 bos-settings/src/ui/views/mod.rs delete mode 100644 bos-settings/src/ui/views/network.rs delete mode 100644 bos-settings/src/ui/views/packages.rs delete mode 100644 bos-settings/src/ui/views/power.rs delete mode 100644 bos-settings/src/ui/views/snapshots.rs delete mode 100644 bos-settings/src/ui/views/sound.rs delete mode 100644 bos-settings/src/ui/views/users.rs delete mode 100644 bos-settings/src/ui/widgets.rs delete mode 100644 bos-settings/src/ui/window.rs create mode 100644 breadhelp/Cargo.toml create mode 100644 breadhelp/content/README.md create mode 100644 breadhelp/content/advanced/01-snapshots-and-rollback/content.md create mode 100644 breadhelp/content/advanced/01-snapshots-and-rollback/meta.toml create mode 100644 breadhelp/content/apps/01-notes-and-tasks/content.md create mode 100644 breadhelp/content/apps/01-notes-and-tasks/meta.toml create mode 100644 breadhelp/content/customization/01-bar-and-launcher/content.md create mode 100644 breadhelp/content/customization/01-bar-and-launcher/meta.toml create mode 100644 breadhelp/content/daily-use/01-searching-your-files/content.md create mode 100644 breadhelp/content/daily-use/01-searching-your-files/meta.toml create mode 100644 breadhelp/content/daily-use/02-clipboard-history/content.md create mode 100644 breadhelp/content/daily-use/02-clipboard-history/meta.toml create mode 100644 breadhelp/content/getting-started/01-what-is-bos/content.beginner.md create mode 100644 breadhelp/content/getting-started/01-what-is-bos/content.md create mode 100644 breadhelp/content/getting-started/01-what-is-bos/meta.toml create mode 100644 breadhelp/content/getting-started/02-essential-keybinds/content.md create mode 100644 breadhelp/content/getting-started/02-essential-keybinds/meta.toml create mode 100644 breadhelp/content/getting-started/03-personalizing-bos/content.md create mode 100644 breadhelp/content/getting-started/03-personalizing-bos/meta.toml create mode 100644 breadhelp/content/troubleshooting/01-fixing-wifi/content.md create mode 100644 breadhelp/content/troubleshooting/01-fixing-wifi/meta.toml create mode 100644 breadhelp/content/troubleshooting/_symptoms/no-sound.toml create mode 100644 breadhelp/content/troubleshooting/_symptoms/wifi-wont-connect.toml create mode 100644 breadhelp/src/cli.rs create mode 100644 breadhelp/src/config.rs create mode 100644 breadhelp/src/content/keybinds.rs create mode 100644 breadhelp/src/content/markdown.rs create mode 100644 breadhelp/src/content/meta.rs create mode 100644 breadhelp/src/content/mod.rs create mode 100644 breadhelp/src/content/troubleshoot.rs create mode 100644 breadhelp/src/main.rs create mode 100644 breadhelp/src/services/breadd.rs create mode 100644 breadhelp/src/services/exec.rs create mode 100644 breadhelp/src/services/hyprland.rs create mode 100644 breadhelp/src/services/mod.rs create mode 100644 breadhelp/src/theme.rs create mode 100644 breadhelp/src/ui/ask.rs create mode 100644 breadhelp/src/ui/guide_view.rs create mode 100644 breadhelp/src/ui/home.rs create mode 100644 breadhelp/src/ui/keybind_viewer.rs create mode 100644 breadhelp/src/ui/learn.rs create mode 100644 breadhelp/src/ui/mod.rs create mode 100644 breadhelp/src/ui/modes.rs create mode 100644 breadhelp/src/ui/onboarding/mod.rs create mode 100644 breadhelp/src/ui/onboarding/steps.rs create mode 100644 breadhelp/src/ui/tabs.rs create mode 100644 breadhelp/src/ui/troubleshoot_wizard.rs create mode 100644 breadhelp/src/ui/window.rs create mode 100644 iso/airootfs/etc/skel/.config/bread/modules/breadhelp-suggest.lua create mode 100644 iso/airootfs/etc/skel/.config/hypr/autostart.json create mode 100644 iso/airootfs/etc/skel/.config/hypr/binds.json create mode 100644 iso/airootfs/etc/skel/.config/hypr/monitors.json create mode 100644 iso/airootfs/etc/skel/.config/hypr/scripts/display/monitors.lua create mode 100644 iso/airootfs/etc/skel/.config/hypr/scripts/input/binds.lua create mode 100644 iso/airootfs/etc/skel/.config/hypr/scripts/input/keybinds.lua create mode 100644 iso/airootfs/etc/skel/.config/hypr/scripts/lib/json.lua create mode 100644 iso/airootfs/etc/skel/.config/hypr/scripts/system/autostart.lua create mode 100644 iso/airootfs/etc/skel/.config/hypr/scripts/ui/settings.lua create mode 100644 iso/airootfs/etc/skel/.config/hypr/settings.json delete mode 100644 iso/airootfs/usr/local/bin/bos-keybinds create mode 100755 iso/airootfs/usr/local/bin/bos-netcheck delete mode 100644 iso/airootfs/usr/local/bin/bos-welcome delete mode 100644 iso/airootfs/usr/share/bos/keybinds.txt delete mode 100644 iso/airootfs/usr/share/bos/welcome.txt delete mode 100644 packaging/arch/bos-settings.desktop rename packaging/arch/{ => breadhelp}/PKGBUILD (59%) create mode 100644 packaging/arch/breadhelp/breadhelp.desktop diff --git a/.forgejo/workflows/package.yml b/.forgejo/workflows/package.yml index aaf7eb7..8f763e5 100644 --- a/.forgejo/workflows/package.yml +++ b/.forgejo/workflows/package.yml @@ -24,15 +24,15 @@ jobs: git clone --branch "${GITHUB_REF_NAME}" --depth 1 \ "https://git.breadway.dev/${GITHUB_REPOSITORY}.git" /home/builder/src cd /home/builder/src - git archive --format=tar.gz --prefix="bos-settings-${VERSION}/" HEAD \ - > packaging/arch/bos-settings-${VERSION}.tar.gz - SHA=$(sha256sum packaging/arch/bos-settings-${VERSION}.tar.gz | awk '{print $1}') - sed -i "s/^pkgver=.*/pkgver=${VERSION}/" packaging/arch/PKGBUILD - sed -i "s/^sha256sums=.*/sha256sums=('${SHA}')/" packaging/arch/PKGBUILD + git archive --format=tar.gz --prefix="breadhelp-${VERSION}/" HEAD \ + > packaging/arch/breadhelp/breadhelp-${VERSION}.tar.gz + SHA=$(sha256sum packaging/arch/breadhelp/breadhelp-${VERSION}.tar.gz | awk '{print $1}') + sed -i "s/^pkgver=.*/pkgver=${VERSION}/" packaging/arch/breadhelp/PKGBUILD + sed -i "s/^sha256sums=.*/sha256sums=('${SHA}')/" packaging/arch/breadhelp/PKGBUILD chown -R builder:builder /home/builder/src # --nocheck: packaging builds the artifact; tests belong in a CI job. - su builder -c "cd /home/builder/src/packaging/arch && makepkg -f --noconfirm --nocheck" - PKG=$(find /home/builder/src/packaging/arch -name '*.pkg.tar.zst' | head -1) + su builder -c "cd /home/builder/src/packaging/arch/breadhelp && makepkg -f --noconfirm --nocheck" + PKG=$(find /home/builder/src/packaging/arch/breadhelp -name '*.pkg.tar.zst' | head -1) curl -fsS -X PUT \ -H "Authorization: token ${PUBLISH_TOKEN}" \ -H "Content-Type: application/octet-stream" \ diff --git a/Cargo.lock b/Cargo.lock index bc9c049..d8bf35b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -27,8 +27,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" [[package]] -name = "bos-settings" -version = "0.6.0" +name = "bread-theme" +version = "0.2.3" +source = "git+https://github.com/Breadway/bread-ecosystem?tag=v0.2.10#17d1bb85801b9a8c195b64c02d288cd662c9c780" +dependencies = [ + "dirs", + "gtk4", + "serde", + "serde_json", +] + +[[package]] +name = "breadhelp" +version = "0.1.0" dependencies = [ "async-channel", "bread-theme", @@ -40,17 +51,6 @@ dependencies = [ "toml_edit 0.22.27", ] -[[package]] -name = "bread-theme" -version = "0.2.3" -source = "git+https://github.com/Breadway/bread-ecosystem?tag=v0.2.10#17d1bb85801b9a8c195b64c02d288cd662c9c780" -dependencies = [ - "dirs", - "gtk4", - "serde", - "serde_json", -] - [[package]] name = "cairo-rs" version = "0.22.0" diff --git a/Cargo.toml b/Cargo.toml index af9532d..9279d56 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,3 +1,3 @@ [workspace] -members = ["bos-settings"] +members = ["breadhelp"] resolver = "2" diff --git a/README.md b/README.md index b736e5d..7b1be40 100644 --- a/README.md +++ b/README.md @@ -14,9 +14,15 @@ wiring up dotfiles, no per-tool bakery installs. - **Compositor**: Hyprland with a native-Lua config (`hyprland.lua`), curated keybinds, snappy animations, blur, and pywal-driven colours on a black base. - **bread ecosystem**, baked into `/etc/skel` from bakery-managed binaries - (no network needed at install time): `bread`/`breadd`, `breadbar` (status bar - + notification daemon), `breadbox` (launcher), `breadcrumbs` (Wi-Fi profiles), - `breadpad` (notes/reminders), `breadman`, and the `bakery` package manager. + (no network needed at install time): the `bread`/`breadd` automation daemon, + `breadbar` (status bar + notifications), `breadbox` (launcher), `breadclip` + (clipboard history), `breadcrumbs` (Wi-Fi profiles), `breadpad`/`breadman` + (notes), `breadpaper` (wallpaper + theme), `breadsearch` (system search), + `breadmon` (monitor layout TUI), `breadshot` (screenshots), `bread-theme` + (the shared palette engine), and the `bakery` package manager. `breadlock` + (lock screen + greeter) ships as its own pacman package alongside + `bos-settings`, not through bakery. See [below](#the-bread-ecosystem) for + what each one actually does. - **bos-settings**: a GTK4 control panel that configures every bread\* app's config from a GUI (non-destructively), plus snapshot rollback and bakery updates. See below. @@ -162,18 +168,49 @@ cargo build --release -p bos-settings cargo test -p bos-settings # includes config round-trip tests ``` -## The bread ecosystem at a glance +## The bread ecosystem + +Everything below is a separate bakery-distributed project with its own repo +and release cadence, baked into `/etc/skel` at ISO build time so a fresh +install has them all with no network round-trip. Some ship more than one +binary from a single package — that's noted where it applies. Most have a +corresponding **bos-settings** panel for configuration; this table is about +*using* the app directly. + +**Desktop shell** | Tool | Role | Launch | |------|------|--------| -| `bread` / `breadd` | Reactive automation daemon — normalises hardware/compositor signals into events dispatched to Lua modules | runs at login | -| `breadbar` | Top status bar (workspaces, clock, stats, tray) **and** the notification daemon | runs at login | -| `breadbox` | Application launcher | `SUPER+Space` | -| `breadpad` | Notes & reminders (AI-classified, optional CalDAV sync) | `SUPER+U` | -| `breadman` | Package-manager UI | `SUPER+M` | -| `breadcrumbs` | Wi-Fi profile state machine (location-aware) | CLI / BOS Settings | -| `bakery` | CLI package manager for the ecosystem | `bakery` | -| `bos-settings` | Unified GTK4 control panel for all of the above + snapshots + updates | `SUPER+,` | +| `bread` / `breadd` | Reactive automation daemon — normalises hardware/compositor/power/network signals into events dispatched to Lua modules (`~/.config/bread/`). Everything else in the ecosystem can subscribe to its events. | runs at login (`breadd.service`) | +| `breadbar` | Top status bar: workspaces, clock, system stats, tray, **and** the notification daemon — one process, not two | runs at login | +| `breadbox` | Application launcher (fuzzy search, per-context results via `breadbox-sync`) | `SUPER+Space` | +| `breadlock` | Idle lock screen. Also provides `breadgreet`, the login greeter hosted under `cage` via greetd — same project, two binaries, one visual identity from login to lock | `SUPER+L` (via `loginctl lock-session`, picked up by `hypridle`); `breadgreet` runs automatically at boot | +| `bread-theme` | The shared palette engine every bread app renders through: fixed dark base colors, with only the accent slots following the current wallpaper's pywal palette. `bread-theme generate` regenerates the stylesheet; hyprland.lua calls it automatically on wallpaper change. | invoked automatically, rarely run by hand | + +**Productivity** + +| Tool | Role | Launch | +|------|------|--------| +| `breadpad` | Quick-capture scratchpad/notes popup with AI classification and optional CalDAV calendar sync | `SUPER+U` | +| `breadman` | The fuller notes manager view (browse/organize) — ships from the same `breadpad` package as a second binary | `SUPER+M` | +| `breadclip` | Clipboard history. `breadclipd` is the background daemon that actually records history; `breadclip` is the GTK4 popup that browses it | `SUPER+V` / `SUPER+Shift+V` | +| `breadsearch` | Semantic system-wide search (indexes files/notes, embeds locally — CPU/ROCm/CUDA backend configurable). `breadmill` is its indexing daemon. | via breadbox, or BOS Settings → File Search | + +**System** + +| Tool | Role | Launch | +|------|------|--------| +| `breadcrumbs` | Location-aware Wi-Fi profile state machine, with optional Tailscale integration — switches network behavior based on which saved network you're on | CLI, or BOS Settings → Wi-Fi Profiles | +| `breadpaper` | Wallpaper manager — sets the wallpaper via `awww`, generates the pywal accent palette from it, and reloads every bread-theme app | BOS Settings → Wallpaper | +| `breadmon` | TUI monitor layout manager (resolution/position/scaling) — the interactive counterpart to BOS Settings' read-only Display panel | `breadmon` in a terminal | +| `breadshot` | Screenshot utility wrapping `grim`/`slurp`/`wl-copy` with Hyprland-aware geometry (multi-monitor-safe region select) | `breadshot`, or `SUPER+Shift+S/C/P` | + +**Tooling** + +| Tool | Role | Launch | +|------|------|--------| +| `bakery` | CLI package manager for the whole ecosystem — install/update/list, tracks installed binaries + versions independently of pacman | `bakery` | +| `bos-settings` | Unified GTK4 control panel: live system state + control (network, power, firewall, users, packages, firmware, AUR, snapshots) plus non-destructive config editing for every app above | `SUPER+,` | ## Keyboard shortcuts @@ -185,12 +222,16 @@ cheatsheet in-session; first boot shows a short welcome (once). | `SUPER+Return` | Terminal (kitty) | | `SUPER+Space` | App launcher (breadbox) | | `SUPER+E` / `SUPER+B` | Files (nautilus) / Browser (zen) | -| `SUPER+U` / `SUPER+M` | breadpad / breadman | +| `SUPER+U` / `SUPER+M` | Notes (breadpad) / notes manager (breadman) | | `SUPER+,` / `SUPER+/` | BOS Settings / keybind cheatsheet | | `SUPER+L` / `SUPER+N` | Lock / log out | | `SUPER+Backspace` | Close window | -| `SUPER+F` / `SUPER+V` / `SUPER+T` | Fullscreen / float / toggle split | -| `SUPER+Shift+V` | Clipboard history | +| `SUPER+F` | Fullscreen | +| `SUPER+I` | Toggle floating | +| `SUPER+P` | Toggle pseudotile | +| `SUPER+R` | Resize mode | +| `SUPER+T` | Toggle split direction | +| `SUPER+V` / `SUPER+Shift+V` | Clipboard history (breadclip) | | `SUPER+Tab` | Last window | | `SUPER+Shift+S/C/P` | Screenshot region→file / region→clipboard / screen→file | | `SUPER+arrows` | Move focus | @@ -199,7 +240,11 @@ cheatsheet in-session; first boot shows a short welcome (once). | `SUPER+1..0` | Switch to workspace 1–10 | | `SUPER+Shift+1..0` | Move window to workspace | | `SUPER+[ / ]` | Previous / next workspace | +| `SUPER+Shift+[ / ]` | Move window to previous / next workspace | +| `SUPER+scroll` | Cycle workspaces | | `SUPER+left/right-drag` | Move / resize window with the mouse | +| Volume / brightness / play-pause / next / prev | Media keys — work even on the lock screen | +| Calculator key | Opens gnome-calculator | ## Known limitations diff --git a/bos-settings/Cargo.toml b/bos-settings/Cargo.toml deleted file mode 100644 index 61bb665..0000000 --- a/bos-settings/Cargo.toml +++ /dev/null @@ -1,19 +0,0 @@ -[package] -name = "bos-settings" -version = "0.6.0" -edition = "2021" - -[dependencies] -gtk4 = { version = "0.11", features = ["v4_12"] } -glib = "0.22" -# Shared ecosystem theming — bos-settings loads the same generated stylesheet as -# breadbar/breadbox/breadpad so the whole desktop looks consistent. -bread-theme = { git = "https://github.com/Breadway/bread-ecosystem", tag = "v0.2.10", features = ["gtk"] } -serde = { version = "1", features = ["derive"] } -serde_json = "1" -toml = "0.8" -# toml_edit drives non-destructive config editing: it preserves comments and -# any keys the UI doesn't model, so saving a single field never rewrites or -# drops the rest of the user's config file. -toml_edit = "0.22" -async-channel = "2" diff --git a/bos-settings/src/config/mod.rs b/bos-settings/src/config/mod.rs deleted file mode 100644 index 4cc3266..0000000 --- a/bos-settings/src/config/mod.rs +++ /dev/null @@ -1,213 +0,0 @@ -//! Non-destructive config editing. -//! -//! Every bread* app owns a TOML config that may contain keys, sections, and -//! comments this settings app does not model (e.g. breadpad's calendar -//! credentials, breadcrumbs' saved-network passwords). To edit safely we parse -//! the file into a `toml_edit::DocumentMut`, mutate only the specific keys the -//! UI exposes, and write the document back — preserving everything else, -//! formatting and comments included. - -use std::error::Error; -use std::path::{Path, PathBuf}; - -use toml_edit::{value, Array, DocumentMut, Item, Table, Value}; - -/// Load a TOML file into an editable document. A missing file yields an -/// empty document so the UI still renders with defaults — normal for a fresh -/// install. A file that *exists* but fails to parse is far more dangerous: -/// falling back to an empty document there means the next Save (see -/// `save_doc`) overwrites it with only the UI-modelled keys, silently -/// destroying anything else in the file (breadpad's calendar credentials, -/// breadcrumbs' saved network passwords, ...). Back up the unparseable file -/// once before falling back, so a bad edit is always recoverable. -pub fn load_doc(path: &Path) -> DocumentMut { - let Ok(text) = std::fs::read_to_string(path) else { - return DocumentMut::default(); - }; - match text.parse::() { - Ok(doc) => doc, - Err(e) => { - let backup = PathBuf::from(format!("{}.bak", path.display())); - eprintln!( - "bos-settings: {} failed to parse ({e}); backed up to {} before falling back to defaults", - path.display(), - backup.display() - ); - let _ = std::fs::write(&backup, &text); - DocumentMut::default() - } - } -} - -/// Write the document back to disk, creating parent dirs as needed. -pub fn save_doc(path: &Path, doc: &DocumentMut) -> Result<(), Box> { - if let Some(parent) = path.parent() { - std::fs::create_dir_all(parent)?; - } - std::fs::write(path, doc.to_string())?; - Ok(()) -} - -pub fn config_dir() -> PathBuf { - // Honour XDG_CONFIG_HOME if set; otherwise fall back to $HOME/.config. - if let Ok(xdg) = std::env::var("XDG_CONFIG_HOME") { - let p = PathBuf::from(xdg); - if p.is_absolute() { - return p; - } - } - let home = std::env::var("HOME").unwrap_or_else(|_| "/root".to_string()); - PathBuf::from(home).join(".config") -} - -// --- typed readers (walk a dotted path, return None if absent/wrong type) --- - -fn get<'a>(doc: &'a DocumentMut, path: &[&str]) -> Option<&'a Item> { - let mut tbl = doc.as_table(); - let (last, parents) = path.split_last()?; - for key in parents { - tbl = tbl.get(key)?.as_table()?; - } - tbl.get(last) -} - -pub fn get_bool(doc: &DocumentMut, path: &[&str]) -> Option { - get(doc, path)?.as_bool() -} -pub fn get_str(doc: &DocumentMut, path: &[&str]) -> Option { - get(doc, path)?.as_str().map(str::to_string) -} -pub fn get_i64(doc: &DocumentMut, path: &[&str]) -> Option { - get(doc, path)?.as_integer() -} -pub fn get_f64(doc: &DocumentMut, path: &[&str]) -> Option { - let item = get(doc, path)?; - item.as_float().or_else(|| item.as_integer().map(|i| i as f64)) -} -/// Read an array of strings (e.g. modules.disable, contexts[].priority). -pub fn get_str_list(doc: &DocumentMut, path: &[&str]) -> Vec { - match get(doc, path).and_then(Item::as_array) { - Some(arr) => arr - .iter() - .filter_map(|v| v.as_str().map(str::to_string)) - .collect(), - None => Vec::new(), - } -} - -// --- setters (auto-create intermediate tables, replace only the leaf) --- - -fn table_at_mut<'a>(doc: &'a mut DocumentMut, parents: &[&str]) -> &'a mut Table { - let mut tbl = doc.as_table_mut(); - for key in parents { - let entry = tbl.entry(key).or_insert_with(|| Item::Table(Table::new())); - if !entry.is_table() { - *entry = Item::Table(Table::new()); - } - tbl = entry.as_table_mut().expect("just ensured table"); - } - tbl -} - -fn set_item(doc: &mut DocumentMut, path: &[&str], item: Item) { - let Some((last, parents)) = path.split_last() else { - return; - }; - table_at_mut(doc, parents).insert(last, item); -} - -pub fn set_bool(doc: &mut DocumentMut, path: &[&str], v: bool) { - set_item(doc, path, value(v)); -} -pub fn set_str(doc: &mut DocumentMut, path: &[&str], v: &str) { - set_item(doc, path, value(v)); -} -pub fn set_i64(doc: &mut DocumentMut, path: &[&str], v: i64) { - set_item(doc, path, value(v)); -} -pub fn set_f64(doc: &mut DocumentMut, path: &[&str], v: f64) { - set_item(doc, path, value(v)); -} -pub fn set_str_list(doc: &mut DocumentMut, path: &[&str], items: &[String]) { - let mut arr = Array::new(); - for s in items { - arr.push(s.as_str()); - } - set_item(doc, path, Item::Value(Value::Array(arr))); -} - -/// Set a string key, or remove it entirely when the value is empty — keeps -/// optional fields out of the file rather than persisting `key = ""`. -pub fn set_str_or_remove(doc: &mut DocumentMut, path: &[&str], v: &str) { - if v.is_empty() { - remove(doc, path); - } else { - set_str(doc, path, v); - } -} - -pub fn remove(doc: &mut DocumentMut, path: &[&str]) { - if let Some((last, parents)) = path.split_last() { - let mut tbl = doc.as_table_mut(); - for key in parents { - match tbl.get_mut(key).and_then(Item::as_table_mut) { - Some(t) => tbl = t, - None => return, - } - } - tbl.remove(last); - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn edits_preserve_unmodelled_keys_and_comments() { - let src = "\ -# a leading comment -[daemon] -log_level = \"info\" - -[calendar] -password = \"secret\" # keep me -"; - let mut doc: DocumentMut = src.parse().unwrap(); - // Modify a single modelled key. - set_str(&mut doc, &["daemon", "log_level"], "debug"); - // A key/section the UI never touches must survive untouched. - let out = doc.to_string(); - assert!(out.contains("log_level = \"debug\"")); - assert!(out.contains("password = \"secret\"")); - assert!(out.contains("# keep me")); - assert!(out.contains("# a leading comment")); - } - - #[test] - fn setters_create_missing_tables() { - let mut doc = DocumentMut::new(); - set_bool(&mut doc, &["adapters", "power", "enabled"], false); - set_i64(&mut doc, &["adapters", "power", "poll_interval_secs"], 45); - assert_eq!(get_bool(&doc, &["adapters", "power", "enabled"]), Some(false)); - assert_eq!( - get_i64(&doc, &["adapters", "power", "poll_interval_secs"]), - Some(45) - ); - } - - #[test] - fn empty_string_removes_key() { - let mut doc: DocumentMut = "[calendar]\nurl = \"x\"\n".parse().unwrap(); - set_str_or_remove(&mut doc, &["calendar", "url"], ""); - assert_eq!(get_str(&doc, &["calendar", "url"]), None); - } - - #[test] - fn str_list_roundtrips() { - let mut doc = DocumentMut::new(); - let items = vec!["a".to_string(), "b".to_string()]; - set_str_list(&mut doc, &["modules", "disable"], &items); - assert_eq!(get_str_list(&doc, &["modules", "disable"]), items); - } -} diff --git a/bos-settings/src/main.rs b/bos-settings/src/main.rs deleted file mode 100644 index a5e73fd..0000000 --- a/bos-settings/src/main.rs +++ /dev/null @@ -1,13 +0,0 @@ -mod config; -mod theme; -mod ui; - -use gtk4::prelude::*; - -fn main() { - let app = gtk4::Application::builder() - .application_id("com.breadway.bos-settings") - .build(); - app.connect_activate(ui::window::build_ui); - app.run(); -} diff --git a/bos-settings/src/theme.rs b/bos-settings/src/theme.rs deleted file mode 100644 index 5b57ca8..0000000 --- a/bos-settings/src/theme.rs +++ /dev/null @@ -1,56 +0,0 @@ -//! Theming for bos-settings. -//! -//! bos-settings deliberately owns almost no styling: it loads the ecosystem's -//! shared stylesheet (the same one breadbar/breadbox/breadpad use, generated by -//! `bread-theme` from the pywal palette) and adds only the few layout rules -//! specific to this app's sidebar + content shell. This keeps it visually -//! identical to the rest of the bread desktop and live-recolouring for free. - -use gtk4::CssProvider; -use std::cell::RefCell; - -// App-specific layout only — everything visual (colours, buttons, entries, -// switches, sidebar/row styling, cards, scrollbars) comes from the shared sheet. -const APP_CSS: &str = "\ -.view-content { padding: 24px; }\n\ -.view-content > label.title { margin-bottom: 16px; }\n\ -/* Sidebar row sub-labels (the underlying binary/config name under a row's \ - human label) — smaller than the shared sheet's default dim-label size. */\n\ -.caption { font-size: 11px; }\n\ -/* bread-theme's shared sheet only overrides background-color on \ - suggested/destructive buttons, not background-image — so Adwaita's \ - built-in gradient (bright blue/red) paints over our flat colour \ - underneath it. Belongs upstream in bread-theme; patched locally here \ - until that's worth its own release. */\n\ -button.suggested-action, button.destructive-action { background-image: none; }\n\ -/* Same upstream gap for scale (Sound's volume sliders) — the shared sheet \ - has no `scale` rules at all, so they render in Adwaita's default blue. */\n\ -scale trough { background-color: alpha(@on-surface, 0.15); border-radius: 999px; min-height: 6px; background-image: none; }\n\ -scale highlight { background-color: @accent; background-image: none; border-radius: 999px; }\n\ -scale slider { background-color: @on-surface; border-radius: 999px; }\n\ -/* Destructive actions must not follow the wallpaper palette: @red is \ - pywal's color1, which can land on gold/yellow/anything depending on the \ - wallpaper (it did, this session) — making Delete/Remove look like a \ - primary action instead of a dangerous one. Fixed regardless of palette. */\n\ -button.destructive-action { background-color: #c0392b; color: #ffffff; }\n\ -button.destructive-action:hover { background-color: #d64535; }\n\ -/* Adwaita's default switch slider (the knob) carries a box-shadow used for \ - its 3D bevel look — bread-theme's override only sets background-color, \ - so that shadow still renders as a pale ring around the knob on top of \ - our flat colour. */\n\ -switch slider { box-shadow: none; outline: none; border: none; background-image: none; }\n\ -switch { box-shadow: none; outline: none; border: none; background-image: none; }\n\ -"; - -thread_local! { - static APP_PROVIDER: RefCell> = const { RefCell::new(None) }; -} - -pub fn load(_display: >k4::gdk::Display) { - // Shared ecosystem stylesheet (loads the generated file or a rendered - // fallback, and live-reloads when the palette changes). - bread_theme::gtk::apply_shared(); - - // bos-settings layout, layered on top at APPLICATION priority. - APP_PROVIDER.with(|cell| bread_theme::gtk::apply_css(APP_CSS, cell)); -} diff --git a/bos-settings/src/ui/mod.rs b/bos-settings/src/ui/mod.rs deleted file mode 100644 index 3867fcd..0000000 --- a/bos-settings/src/ui/mod.rs +++ /dev/null @@ -1,4 +0,0 @@ -pub mod sidebar; -pub mod views; -pub mod widgets; -pub mod window; diff --git a/bos-settings/src/ui/sidebar.rs b/bos-settings/src/ui/sidebar.rs deleted file mode 100644 index 5b2ef8e..0000000 --- a/bos-settings/src/ui/sidebar.rs +++ /dev/null @@ -1,141 +0,0 @@ -use gtk4::prelude::*; -use gtk4::{Box as GBox, Image, Label, ListBox, ListBoxRow, Orientation}; - -pub struct SidebarItem { - /// Must match the `Stack` page name registered in `window.rs`. - pub id: &'static str, - pub label: &'static str, - /// Dim second line — the underlying binary/config name, for items whose - /// human label doesn't already make that obvious. - pub sublabel: Option<&'static str>, - /// A `-symbolic` icon name from the system icon theme (Papirus-Dark ships - /// the full Adwaita-compatible symbolic set this app relies on). - pub icon: &'static str, -} - -const fn item(id: &'static str, label: &'static str, icon: &'static str) -> SidebarItem { - SidebarItem { id, label, sublabel: None, icon } -} - -const fn item_sub( - id: &'static str, - label: &'static str, - sublabel: &'static str, - icon: &'static str, -) -> SidebarItem { - SidebarItem { id, label, sublabel: Some(sublabel), icon } -} - -// Grouped by task, not by "app vs system internals" — a user thinks "I want -// to change my Wi-Fi" or "I want to change my wallpaper", not "which of -// these is a bread-ecosystem app". breadcrumbs (Wi-Fi profiles) moves out of -// the old "Apps" bucket into System for the same reason. -pub const SYSTEM_ITEMS: &[SidebarItem] = &[ - item("network", "Network", "network-wireless-symbolic"), - item_sub("breadcrumbs", "Wi-Fi Profiles", "breadcrumbs", "network-workgroup-symbolic"), - item("firewall", "Firewall", "security-high-symbolic"), - item("sound", "Sound", "audio-volume-high-symbolic"), - item("power", "Power", "battery-good-symbolic"), - item("datetime", "Date & Time", "preferences-system-time-symbolic"), - item_sub("hyprland", "Display", "hyprland.lua", "video-display-symbolic"), - item("users", "Users", "system-users-symbolic"), -]; - -pub const PERSONALIZATION_ITEMS: &[SidebarItem] = &[ - item_sub("breadpaper", "Wallpaper", "breadpaper", "preferences-desktop-wallpaper-symbolic"), - item_sub("breadbar", "Bar", "breadbar", "view-grid-symbolic"), - item_sub("breadbox", "Launcher", "breadbox", "view-app-grid-symbolic"), - item_sub("breadclip", "Clipboard", "breadclipd", "edit-paste-symbolic"), - item_sub("breadpad", "Notes", "breadpad", "text-editor-symbolic"), - item_sub("breadsearch", "File Search", "breadsearch", "system-search-symbolic"), - item_sub("bread", "Daemon", "breadd", "applications-system-symbolic"), -]; - -pub const MAINTENANCE_ITEMS: &[SidebarItem] = &[ - item("packages", "Packages", "package-x-generic-symbolic"), - item("aur", "AUR", "system-search-symbolic"), - item("firmware", "Firmware", "software-update-available-symbolic"), - item("snapshots", "Snapshots", "document-open-recent-symbolic"), -]; - -pub const ABOUT_ITEMS: &[SidebarItem] = &[item("about", "About", "help-about-symbolic")]; - -/// `default_id` must match whatever page `window.rs` sets as the `Stack`'s -/// initial visible child — previously these were two independent hardcoded -/// "about" literals in different files with no link between them, so -/// changing one without the other silently desynced the sidebar highlight -/// from the actually-displayed page. -pub fn build(default_id: &str) -> (GBox, ListBox) { - let vbox = GBox::new(Orientation::Vertical, 0); - vbox.add_css_class("sidebar"); - vbox.set_width_request(210); - - let list = ListBox::new(); - list.set_selection_mode(gtk4::SelectionMode::Single); - list.add_css_class("sidebar"); - - append_section(&list, "System", SYSTEM_ITEMS); - append_section(&list, "Personalization", PERSONALIZATION_ITEMS); - append_section(&list, "Maintenance", MAINTENANCE_ITEMS); - append_section(&list, None, ABOUT_ITEMS); - - let mut i = 0; - loop { - match list.row_at_index(i) { - None => break, - Some(row) if row.widget_name() == default_id => { - list.select_row(Some(&row)); - break; - } - _ => i += 1, - } - } - - vbox.append(&list); - (vbox, list) -} - -fn append_section(list: &ListBox, title: impl Into>, items: &[SidebarItem]) { - if let Some(title) = title.into() { - let header_row = ListBoxRow::new(); - header_row.set_selectable(false); - header_row.set_activatable(false); - let header_lbl = Label::new(Some(title)); - header_lbl.add_css_class("section-header"); - header_lbl.set_xalign(0.0); - header_row.set_child(Some(&header_lbl)); - list.append(&header_row); - } - - for entry in items { - let row = ListBoxRow::new(); - row.set_widget_name(entry.id); - - let hbox = GBox::new(Orientation::Horizontal, 10); - hbox.set_margin_top(4); - hbox.set_margin_bottom(4); - - let icon = Image::from_icon_name(entry.icon); - icon.set_pixel_size(16); - hbox.append(&icon); - - let labels = GBox::new(Orientation::Vertical, 0); - let lbl = Label::new(Some(entry.label)); - lbl.set_xalign(0.0); - labels.append(&lbl); - if let Some(sub) = entry.sublabel { - let sub_lbl = Label::new(Some(sub)); - sub_lbl.add_css_class("dim-label"); - sub_lbl.set_xalign(0.0); - // Match the sidebar's smaller "section-header" scale rather than - // the shared sheet's default dim-label size, so it reads as a - // caption under the row label, not a second full-size label. - sub_lbl.add_css_class("caption"); - labels.append(&sub_lbl); - } - hbox.append(&labels); - - row.set_child(Some(&hbox)); - list.append(&row); - } -} diff --git a/bos-settings/src/ui/views/about.rs b/bos-settings/src/ui/views/about.rs deleted file mode 100644 index 8f4cdcb..0000000 --- a/bos-settings/src/ui/views/about.rs +++ /dev/null @@ -1,172 +0,0 @@ -//! Read-only system info, plus the one thing worth making writable: hostname. -//! BOS is a rolling release (no fixed version number to show — `os-release` -//! ships `BUILD_ID=rolling` on purpose), so there's no "BOS 1.2.3" readout -//! here the way a point-release distro's About panel would have one. - -use gtk4::prelude::*; -use gtk4::{Box as GBox, Button, Entry, Label, Orientation}; -use std::fs; -use std::process::Command; - -use crate::ui::widgets as w; - -fn os_pretty_name() -> String { - fs::read_to_string("/etc/os-release") - .ok() - .and_then(|s| { - s.lines() - .find_map(|l| l.strip_prefix("PRETTY_NAME=").map(|v| v.trim_matches('"').to_string())) - }) - .unwrap_or_else(|| "BOS".to_string()) -} - -fn hostname() -> String { - fs::read_to_string("/etc/hostname") - .map(|s| s.trim().to_string()) - .unwrap_or_else(|_| "unknown".to_string()) -} - -fn kernel() -> String { - Command::new("uname") - .arg("-r") - .output() - .ok() - .map(|o| String::from_utf8_lossy(&o.stdout).trim().to_string()) - .unwrap_or_else(|| "unknown".to_string()) -} - -fn cpu() -> String { - let model = fs::read_to_string("/proc/cpuinfo") - .ok() - .and_then(|s| { - s.lines() - .find_map(|l| l.strip_prefix("model name").map(|v| v.trim_start_matches([':', ' ', '\t']).to_string())) - }) - .unwrap_or_else(|| "unknown".to_string()); - let cores = std::thread::available_parallelism().map(|n| n.get()).unwrap_or(0); - if cores > 0 { - format!("{model} ({cores} threads)") - } else { - model - } -} - -fn memory() -> String { - let kb = fs::read_to_string("/proc/meminfo") - .ok() - .and_then(|s| { - s.lines() - .find(|l| l.starts_with("MemTotal:")) - .and_then(|l| l.split_whitespace().nth(1)) - .and_then(|v| v.parse::().ok()) - }); - match kb { - Some(kb) => format!("{:.1} GiB", kb as f64 / 1024.0 / 1024.0), - None => "unknown".to_string(), - } -} - -fn gpu() -> String { - let Ok(output) = Command::new("lspci").output() else { - return "unknown".to_string(); - }; - let text = String::from_utf8_lossy(&output.stdout); - text.lines() - // "Display controller" covers integrated GPUs some laptop chipsets - // (this dev laptop's AMD Radeon 860M included) report under instead - // of "VGA compatible controller" — without it those show "unknown". - .find(|l| { - l.contains("VGA compatible controller") - || l.contains("3D controller") - || l.contains("Display controller") - }) - .and_then(|l| l.split(": ").nth(1)) - .unwrap_or("unknown") - .to_string() -} - -fn disk_usage() -> String { - let Ok(output) = Command::new("df").args(["-h", "--output=used,size,pcent", "/"]).output() else { - return "unknown".to_string(); - }; - let text = String::from_utf8_lossy(&output.stdout); - text.lines() - .nth(1) - .map(|l| { - let cols: Vec<&str> = l.split_whitespace().collect(); - match cols.as_slice() { - [used, size, pcent] => format!("{used} of {size} used ({pcent})"), - _ => l.trim().to_string(), - } - }) - .unwrap_or_else(|| "unknown".to_string()) -} - -fn uptime() -> String { - Command::new("uptime") - .arg("-p") - .output() - .ok() - .map(|o| String::from_utf8_lossy(&o.stdout).trim().to_string()) - .unwrap_or_else(|| "unknown".to_string()) -} - -pub fn build() -> GBox { - let (outer, content) = w::view_scaffold("About"); - - content.append(&w::info_row("Operating system", &os_pretty_name())); - content.append(&w::info_row("Kernel", &kernel())); - content.append(&w::info_row("CPU", &cpu())); - content.append(&w::info_row("GPU", &gpu())); - content.append(&w::info_row("Memory", &memory())); - content.append(&w::info_row("Disk (/)", &disk_usage())); - content.append(&w::info_row("Uptime", &uptime())); - - content.append(&w::section("Hostname")); - content.append(&w::hint( - "Changes the machine's network name. Takes effect immediately; \ - needs your password (polkit).", - )); - - let hn_row = GBox::new(Orientation::Horizontal, 12); - let entry = Entry::new(); - entry.set_text(&hostname()); - entry.set_hexpand(true); - let apply_btn = Button::with_label("Apply"); - let status = Label::new(None); - status.add_css_class("dim-label"); - - { - let entry = entry.clone(); - let status = status.clone(); - apply_btn.connect_clicked(move |_| { - let name = entry.text().to_string(); - if name.trim().is_empty() { - status.set_text("Hostname can't be empty"); - return; - } - let log_buf = gtk4::TextBuffer::new(None); - let status2 = status.clone(); - status.set_text("Applying…"); - w::stream_command_then( - &["pkexec", "hostnamectl", "set-hostname", name.trim()], - log_buf.clone(), - move || { - let text = log_buf.text(&log_buf.start_iter(), &log_buf.end_iter(), false); - if text.trim().is_empty() { - status2.set_text("Applied"); - } else { - status2.set_text(&format!("Error: {}", text.trim())); - } - }, - ); - }); - } - - hn_row.append(&entry); - hn_row.append(&apply_btn); - content.append(&hn_row); - content.append(&status); - - outer -} diff --git a/bos-settings/src/ui/views/aur.rs b/bos-settings/src/ui/views/aur.rs deleted file mode 100644 index b526224..0000000 --- a/bos-settings/src/ui/views/aur.rs +++ /dev/null @@ -1,169 +0,0 @@ -//! AUR search via yay — graphical discovery for the wider AUR beyond -//! bakery's bread ecosystem and [breadway]'s own republished packages. -//! -//! Search and browsing are fully graphical; actually installing a package -//! opens a terminal running `yay -S ` instead of a silent `--noconfirm` -//! install. That's deliberate, not a shortcut we didn't get around to: -//! AUR packages run arbitrary maintainer-supplied build scripts, and yay's -//! interactive PKGBUILD diff review (plus the sudo password prompt) is the -//! actual safety mechanism against a malicious/compromised AUR package — -//! automating it away in the name of "no terminal" would remove the one -//! step that exists to catch that. - -use gtk4::prelude::*; -use gtk4::{Box as GBox, Button, Entry, Label, ListBox, ListBoxRow, Orientation, ScrolledWindow}; -use std::process::Command; - -use crate::ui::widgets as w; - -#[derive(Clone)] -struct AurResult { - name: String, - version: String, - description: String, -} - -fn search(query: &str) -> Vec { - let Ok(output) = Command::new("yay").args(["-Ss", "--aur", query]).output() else { - return Vec::new(); - }; - let text = String::from_utf8_lossy(&output.stdout); - let mut results = Vec::new(); - let mut lines = text.lines().peekable(); - while let Some(header) = lines.next() { - // "aur/name version (+votes score) [Orphaned]" — name/version are - // always the first two whitespace-separated fields after the repo/. - let Some(rest) = header.strip_prefix("aur/") else { continue }; - let mut parts = rest.split_whitespace(); - let Some(name) = parts.next() else { continue }; - let version = parts.next().unwrap_or("").to_string(); - let description = lines.next().unwrap_or("").trim().to_string(); - results.push(AurResult { name: name.to_string(), version, description }); - } - results -} - -fn install_in_terminal(pkg: &str) { - let _ = Command::new("kitty").args(["-e", "yay", "-S", pkg]).spawn(); -} - -pub fn build() -> GBox { - let (outer, content) = w::view_scaffold("AUR"); - content.append(&w::hint( - "Search the Arch User Repository via yay. Installing opens a \ - terminal — AUR packages run arbitrary build scripts, and reviewing \ - what yay is about to do (and entering your password) is a real \ - safety step, not just a formality.", - )); - - let search_row = GBox::new(Orientation::Horizontal, 8); - let search_entry = Entry::new(); - search_entry.set_hexpand(true); - search_entry.set_placeholder_text(Some("Search the AUR…")); - let search_btn = Button::with_label("Search"); - search_btn.add_css_class("suggested-action"); - search_row.append(&search_entry); - search_row.append(&search_btn); - content.append(&search_row); - - let status = w::hint("Search for a package to see results here."); - content.append(&status); - - let list = ListBox::new(); - list.set_selection_mode(gtk4::SelectionMode::None); - let scroll = ScrolledWindow::new(); - scroll.set_vexpand(true); - scroll.set_min_content_height(320); - scroll.set_child(Some(&list)); - content.append(&scroll); - - let run_search = { - let list = list.clone(); - let status = status.clone(); - let search_entry = search_entry.clone(); - move |btn: Option