From 26d3bd82665fab8be77513aae623975ad21301f2 Mon Sep 17 00:00:00 2001 From: Breadway Date: Fri, 12 Jun 2026 21:16:09 +0800 Subject: [PATCH 1/7] initial --- can-you-begin-a-composed-beacon.md | 195 +++++++++++++++++++++++++++++ 1 file changed, 195 insertions(+) create mode 100644 can-you-begin-a-composed-beacon.md diff --git a/can-you-begin-a-composed-beacon.md b/can-you-begin-a-composed-beacon.md new file mode 100644 index 0000000..d6d6585 --- /dev/null +++ b/can-you-begin-a-composed-beacon.md @@ -0,0 +1,195 @@ +# BOS — Bread Operating System Plan + +## Context + +The bread ecosystem (bread, breadbar, breadbox, breadcrumbs, breadpad/breadman, bakery) is a cohesive set of Arch/Hyprland-specific tools with a shared theme system, unified package manager, and consistent config conventions. Currently, getting to a working system requires installing Arch, Hyprland, each tool via bakery, and wiring up dotfiles manually. BOS eliminates that — one ISO install produces a fully working desktop with everything preconfigured. + +Goals: +- **Install and be done**: Calamares GUI installer → reboot → working Hyprland + full bread stack +- **Rollback safety**: Btrfs subvolumes + snapper + snap-pac; every pacman transaction is snapshotted +- **Unified config**: `bos-settings` GTK4 app surfaces all app configs + snapshot management + bakery updates +- **Future-compatible**: Btrfs layout is designed to allow A/B partition migration later (SteamOS model) + +--- + +## Repo Structure + +Single new repo: `Breadway/bos` — a Cargo workspace. + +``` +bos/ +├── Cargo.toml # Workspace (members: [bos-settings]) +├── bos-settings/ # GTK4 unified settings app +│ ├── Cargo.toml +│ └── src/ +│ ├── main.rs +│ ├── state.rs +│ ├── theme.rs +│ ├── ui/ +│ │ ├── window.rs # Sidebar + content shell (port breadman pattern) +│ │ ├── sidebar.rs +│ │ └── views/ +│ │ ├── bread.rs +│ │ ├── breadbar.rs +│ │ ├── breadbox.rs +│ │ ├── breadcrumbs.rs +│ │ ├── breadpad.rs +│ │ ├── snapshots.rs +│ │ ├── packages.rs +│ │ └── hyprland.rs +│ └── config/ +│ └── mod.rs # Per-app config loaders +├── iso/ # archiso profile +│ ├── profiledef.sh +│ ├── packages.x86_64 # Live ISO + installed system package list +│ ├── airootfs/ # Files overlaid onto live ISO root +│ │ └── etc/ +│ │ ├── calamares/ # Calamares YAML configuration +│ │ └── skel/ # Default user dotfiles +└── dotfiles/ # Default configs deployed at install time + ├── hyprland/ # hyprland.conf, keybinds, autostart + ├── bread/ # breadd.toml, init.lua, devices.lua + ├── breadbar/ # (no config needed; zero-config by default) + ├── breadbox/ # config.toml with default context priorities + └── breadcrumbs/ # breadcrumbs.toml with default home profile +``` + +--- + +## Component 1: Btrfs Layout + Snapshot Infrastructure + +### Partition/subvolume layout (set up by Calamares) + +| Subvolume | Mount point | Notes | +|-----------|-------------|-------| +| `@` | `/` | Root — snapshotted by snapper | +| `@home` | `/home` | User data — separate from root snapshots | +| `@snapshots` | `/.snapshots` | Snapper snapshot dir | +| `@log` | `/var/log` | Excluded from root snapshots (prevents bloat) | +| `@cache` | `/var/cache` | Excluded from root snapshots | + +Mount options: `noatime,compress=zstd,space_cache=v2` on all subvolumes. + +**A/B compatibility note:** The `@` subvolume is self-contained and can be swapped atomically — this is the design property needed for a future A/B upgrade path. The layout does not need to change to adopt it. + +### Snapshot tooling (installed + configured during post-install) + +- `snapper` — snapshot manager; configured for root (`snapper -c root create-config /`) +- `snap-pac` — pacman hooks that call `snapper pre`/`snapper post` around every transaction +- `grub-btrfs` — regenerates GRUB entries from snapper snapshots; hook runs on `snapper post` + +**snapper root config defaults** (written to `/etc/snapper/configs/root`): +``` +TIMELINE_CREATE="no" # timeline snapshots off; snap-pac handles it +NUMBER_CLEANUP="yes" +NUMBER_MIN_AGE="1800" +NUMBER_LIMIT="10" # keep last 10 pacman snapshots +NUMBER_LIMIT_IMPORTANT="5" +``` + +No user-facing CLI needed for this component — `bos-settings` is the interface. + +--- + +## Component 2: ISO + Calamares Installer + +### archiso profile (`iso/`) + +- Derives from `/usr/share/archiso/configs/releng/` (the standard baseline) +- `packages.x86_64` includes: base, linux, grub, btrfs-progs, snapper, snap-pac, grub-btrfs, hyprland, pipewire, wireplumber, networkmanager, gtk4, gtk4-layer-shell, iw, librsvg, libpulse, bluez, bluez-utils, calamares, calamares-qt6 +- `airootfs/etc/skel/` contains the default dotfiles (symlinked from `dotfiles/`) +- Live session autologs into a `liveuser` and launches Calamares automatically + +### Calamares modules (in order) + +1. **welcome** — system checks (RAM ≥ 2GB, internet, disk space) +2. **locale** — timezone + locale selection +3. **keyboard** — layout selection +4. **partition** — custom `btrfs` mode: creates EFI partition + single btrfs pool with the subvolume layout above +5. **users** — create main user, set password +6. **packages** — install package list (reuses `packages.x86_64`) +7. **bootloader** — install GRUB to EFI, `grub-mkconfig` with grub-btrfs hook +8. **shellprocess (post-install)** — runs `iso/post-install.sh`: + - Configures snapper root config + - Enables services: `NetworkManager`, `bluetooth`, `breadd` (user), `breadbox-sync` (user) + - Runs `bakery install bread breadbar breadbox breadcrumbs breadpad` (or `bakery install --all`) + - Copies `dotfiles/` into `/home/$USER/.config/` (skips any file that already exists) +9. **finished** — reboot prompt + +--- + +## Component 3: `bos-settings` GTK4 App + +### Tech choices +- **gtk4-rs** (v0.11, v4_12 feature), no relm4 — plain GTK4 following breadman's pattern +- **bread-theme** for palette + CSS (git dep: `github.com/Breadway/bread-ecosystem`) +- Reads/writes each tool's own config file directly (no unified intermediate config) +- Window: 960×640, sidebar 190px, `gtk4::Stack` for view switching — identical structure to breadman + +### Sidebar sections + views + +| Section | View | What it does | +|---------|------|--------------| +| **Apps** | bread | Edit `~/.config/bread/breadd.toml` | +| | breadbar | Edit `~/.config/breadbar/` (style.css override, no TOML needed) | +| | breadbox | Edit `~/.config/breadbox/config.toml` (context priority lists) | +| | breadcrumbs | Edit `~/.config/breadcrumbs/breadcrumbs.toml` (profiles, networks) | +| | breadpad | Edit `~/.config/breadpad/breadpad.toml` (model, reminders, calendar) | +| **System** | Snapshots | `snapper list` output; rollback button calls `snapper rollback N` | +| | Packages | `bakery list --installed`; update buttons call `bakery update ` | +| | Hyprland | "Open config in editor" + monitor list from `bread.state.monitors()` | + +### Config loading pattern + +Each view has a dedicated `load_config(path) -> Result` and `save_config(path, T) -> Result<()>` using `toml` crate. Config structs mirror each app's existing types (no duplication — import the `*-shared` crate where it exists, e.g. `breadpad-shared`). For apps without a shared crate (breadbox, breadcrumbs), define minimal local structs. + +### Snapshots view specifics + +- On open: runs `snapper list --output-cols number,date,description,pre-post` via `std::process::Command`, parses into table rows +- Rollback: confirmation dialog → `snapper rollback ` → notify user to reboot +- Delete: `snapper delete ` +- No write access to `/` needed for list/rollback since snapper is configured with `ALLOW_USERS` for the main user + +### Packages view specifics + +- On open: reads `~/.local/state/bakery/installed.json` directly (no network) +- "Check for updates": runs `bakery list` (triggers index refresh), compares versions +- "Update all": runs `bakery update --all` in a subprocess, streams stdout to a log TextView + +### Distribution + +`bos-settings` gets a `bakery.toml` and is added to the `bread-ecosystem` registry — installable standalone on any Arch/Hyprland system via `bakery install bos-settings`, not only as part of a BOS install. + +--- + +## Component 4: Default Dotfiles + +Minimal but functional defaults deployed at install time. These are opinionated starting points, not locked configs — users edit freely after install. + +| File | Key content | +|------|-------------| +| `dotfiles/hyprland/hyprland.conf` | Monitor auto-detect, default keybinds, `exec-once` for breadd/breadbar/breadbox-sync | +| `dotfiles/hyprland/keybinds.conf` | `$mod+Space` → breadbox, `$mod+N` → breadpad, `$mod+M` → breadman, `$mod+S` → bos-settings | +| `dotfiles/bread/breadd.toml` | All adapters enabled, log_level=info | +| `dotfiles/bread/init.lua` | Minimal: activates "default" profile on startup | +| `dotfiles/breadbox/config.toml` | Single default context with common apps | +| `dotfiles/breadcrumbs/breadcrumbs.toml` | Placeholder home profile (user fills in SSIDs) | + +--- + +## Build Order + +1. **Dotfiles** — write default configs; these unblock installer testing immediately +2. **Btrfs + snapper config** — write `post-install.sh`; test in a VM with `archiso` livecdbase +3. **ISO profile** — archiso profiledef + package list + Calamares YAML; iterate in a VM +4. **bos-settings** — start with Snapshots and Packages views (highest value, no app-specific config parsing needed), then add per-app views one at a time + +--- + +## Verification + +- **ISO**: Build with `mkarchiso -v -w /tmp/bos-work -o /tmp/bos-out iso/`; boot in QEMU (`qemu-system-x86_64 -cdrom bos.iso -m 4G -enable-kvm`); complete install; reboot into installed system; confirm all services running and bakery packages present +- **btrfs layout**: `btrfs subvolume list /` after install; confirm `@`, `@home`, `@snapshots`, `@log`, `@cache` exist +- **snapper**: `snapper list`; run `pacman -Syu` and confirm two new snapshots appear +- **grub-btrfs**: Reboot and confirm snapshot submenu in GRUB +- **bos-settings**: `cargo build --release`; launch; confirm each view loads its config file; edit a value, save, re-open and confirm persistence; test rollback button in Snapshots view From 0ff3998c844e6da4b5df166e2c721f31f87177f7 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 12 Jun 2026 13:27:25 +0000 Subject: [PATCH 2/7] Scaffold BOS repo: dotfiles, ISO profile, and bos-settings GTK4 app Implements all four components from the BOS spec: - dotfiles/: default Hyprland, bread, breadbox, breadcrumbs configs - iso/: archiso profiledef, package list, Calamares YAML modules, post-install.sh - bos-settings/: Cargo workspace with GTK4 settings app (8 views: snapshots, packages, bread, breadbar, breadbox, breadcrumbs, breadpad, hyprland) https://claude.ai/code/session_01WszGHvCmxgcyTwNSkfLF9P --- Cargo.lock | 816 ++++++++++++++++++ Cargo.toml | 3 + bos-settings/Cargo.toml | 11 + bos-settings/src/config/mod.rs | 24 + bos-settings/src/main.rs | 12 + bos-settings/src/state.rs | 11 + bos-settings/src/theme.rs | 88 ++ bos-settings/src/ui/mod.rs | 3 + bos-settings/src/ui/sidebar.rs | 66 ++ bos-settings/src/ui/views/bread.rs | 149 ++++ bos-settings/src/ui/views/breadbar.rs | 58 ++ bos-settings/src/ui/views/breadbox.rs | 134 +++ bos-settings/src/ui/views/breadcrumbs.rs | 134 +++ bos-settings/src/ui/views/breadpad.rs | 118 +++ bos-settings/src/ui/views/hyprland.rs | 92 ++ bos-settings/src/ui/views/mod.rs | 8 + bos-settings/src/ui/views/packages.rs | 171 ++++ bos-settings/src/ui/views/snapshots.rs | 167 ++++ bos-settings/src/ui/window.rs | 57 ++ dotfiles/bread/breadd.toml | 8 + dotfiles/bread/init.lua | 1 + dotfiles/breadbox/config.toml | 3 + dotfiles/breadcrumbs/breadcrumbs.toml | 3 + dotfiles/hyprland/hyprland.conf | 54 ++ dotfiles/hyprland/keybinds.conf | 58 ++ .../etc/calamares/modules/bootloader.conf | 9 + .../etc/calamares/modules/finished.conf | 5 + .../etc/calamares/modules/keyboard.conf | 2 + .../etc/calamares/modules/locale.conf | 5 + .../etc/calamares/modules/packages.conf | 10 + .../etc/calamares/modules/partition.conf | 29 + .../etc/calamares/modules/shellprocess.conf | 3 + iso/airootfs/etc/calamares/modules/users.conf | 40 + .../etc/calamares/modules/welcome.conf | 11 + iso/airootfs/etc/calamares/settings.conf | 36 + iso/packages.x86_64 | 84 ++ iso/post-install.sh | 40 + iso/profiledef.sh | 24 + 38 files changed, 2547 insertions(+) create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 bos-settings/Cargo.toml create mode 100644 bos-settings/src/config/mod.rs create mode 100644 bos-settings/src/main.rs create mode 100644 bos-settings/src/state.rs create mode 100644 bos-settings/src/theme.rs create mode 100644 bos-settings/src/ui/mod.rs create mode 100644 bos-settings/src/ui/sidebar.rs create mode 100644 bos-settings/src/ui/views/bread.rs create mode 100644 bos-settings/src/ui/views/breadbar.rs create mode 100644 bos-settings/src/ui/views/breadbox.rs create mode 100644 bos-settings/src/ui/views/breadcrumbs.rs create mode 100644 bos-settings/src/ui/views/breadpad.rs create mode 100644 bos-settings/src/ui/views/hyprland.rs create mode 100644 bos-settings/src/ui/views/mod.rs create mode 100644 bos-settings/src/ui/views/packages.rs create mode 100644 bos-settings/src/ui/views/snapshots.rs create mode 100644 bos-settings/src/ui/window.rs create mode 100644 dotfiles/bread/breadd.toml create mode 100644 dotfiles/bread/init.lua create mode 100644 dotfiles/breadbox/config.toml create mode 100644 dotfiles/breadcrumbs/breadcrumbs.toml create mode 100644 dotfiles/hyprland/hyprland.conf create mode 100644 dotfiles/hyprland/keybinds.conf create mode 100644 iso/airootfs/etc/calamares/modules/bootloader.conf create mode 100644 iso/airootfs/etc/calamares/modules/finished.conf create mode 100644 iso/airootfs/etc/calamares/modules/keyboard.conf create mode 100644 iso/airootfs/etc/calamares/modules/locale.conf create mode 100644 iso/airootfs/etc/calamares/modules/packages.conf create mode 100644 iso/airootfs/etc/calamares/modules/partition.conf create mode 100644 iso/airootfs/etc/calamares/modules/shellprocess.conf create mode 100644 iso/airootfs/etc/calamares/modules/users.conf create mode 100644 iso/airootfs/etc/calamares/modules/welcome.conf create mode 100644 iso/airootfs/etc/calamares/settings.conf create mode 100644 iso/packages.x86_64 create mode 100644 iso/post-install.sh create mode 100644 iso/profiledef.sh diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..a8c150b --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,816 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "bitflags" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" + +[[package]] +name = "bos-settings" +version = "0.1.0" +dependencies = [ + "glib", + "gtk4", + "serde", + "serde_json", + "toml 0.8.23", +] + +[[package]] +name = "cairo-rs" +version = "0.20.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e3bd0f4e25afa9cabc157908d14eeef9067d6448c49414d17b3fb55f0eadd0" +dependencies = [ + "bitflags", + "cairo-sys-rs", + "glib", + "libc", +] + +[[package]] +name = "cairo-sys-rs" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "059cc746549898cbfd9a47754288e5a958756650ef4652bbb6c5f71a6bda4f8b" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + +[[package]] +name = "cfg-expr" +version = "0.20.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb693542bcafa528e198be0ebd9d3632ca5b7c93dbe7237460e199910835997c" +dependencies = [ + "smallvec", + "target-lexicon", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "field-offset" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38e2275cc4e4fc009b0669731a1e5ab7ebf11f469eaede2bab9309a5b4d6057f" +dependencies = [ + "memoffset", + "rustc_version", +] + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-executor" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[package]] +name = "futures-macro" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-core", + "futures-macro", + "futures-task", + "pin-project-lite", + "slab", +] + +[[package]] +name = "gdk-pixbuf" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fd242894c084f4beed508a56952750bce3e96e85eb68fdc153637daa163e10c" +dependencies = [ + "gdk-pixbuf-sys", + "gio", + "glib", + "libc", +] + +[[package]] +name = "gdk-pixbuf-sys" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b34f3b580c988bd217e9543a2de59823fafae369d1a055555e5f95a8b130b96" +dependencies = [ + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gdk4" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4850c9d9c1aecd1a3eb14fadc1cdb0ac0a2298037e116264c7473e1740a32d60" +dependencies = [ + "cairo-rs", + "gdk-pixbuf", + "gdk4-sys", + "gio", + "glib", + "libc", + "pango", +] + +[[package]] +name = "gdk4-sys" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f6eb95798e2b46f279cf59005daf297d5b69555428f185650d71974a910473a" +dependencies = [ + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "pkg-config", + "system-deps", +] + +[[package]] +name = "gio" +version = "0.20.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e27e276e7b6b8d50f6376ee7769a71133e80d093bdc363bd0af71664228b831" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "gio-sys", + "glib", + "libc", + "pin-project-lite", + "smallvec", +] + +[[package]] +name = "gio-sys" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521e93a7e56fc89e84aea9a52cfc9436816a4b363b030260b699950ff1336c83" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", + "windows-sys", +] + +[[package]] +name = "glib" +version = "0.20.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffc4b6e352d4716d84d7dde562dd9aee2a7d48beb872dd9ece7f2d1515b2d683" +dependencies = [ + "bitflags", + "futures-channel", + "futures-core", + "futures-executor", + "futures-task", + "futures-util", + "gio-sys", + "glib-macros", + "glib-sys", + "gobject-sys", + "libc", + "memchr", + "smallvec", +] + +[[package]] +name = "glib-macros" +version = "0.20.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8084af62f09475a3f529b1629c10c429d7600ee1398ae12dd3bf175d74e7145" +dependencies = [ + "heck", + "proc-macro-crate", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "glib-sys" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ab79e1ed126803a8fb827e3de0e2ff95191912b8db65cee467edb56fc4cc215" +dependencies = [ + "libc", + "system-deps", +] + +[[package]] +name = "gobject-sys" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec9aca94bb73989e3cfdbf8f2e0f1f6da04db4d291c431f444838925c4c63eda" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + +[[package]] +name = "graphene-rs" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b86dfad7d14251c9acaf1de63bc8754b7e3b4e5b16777b6f5a748208fe9519b" +dependencies = [ + "glib", + "graphene-sys", + "libc", +] + +[[package]] +name = "graphene-sys" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df583a85ba2d5e15e1797e40d666057b28bc2f60a67c9c24145e6db2cc3861ea" +dependencies = [ + "glib-sys", + "libc", + "pkg-config", + "system-deps", +] + +[[package]] +name = "gsk4" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61f5e72f931c8c9f65fbfc89fe0ddc7746f147f822f127a53a9854666ac1f855" +dependencies = [ + "cairo-rs", + "gdk4", + "glib", + "graphene-rs", + "gsk4-sys", + "libc", + "pango", +] + +[[package]] +name = "gsk4-sys" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "755059de55fa6f85a46bde8caf03e2184c96bfda1f6206163c72fb0ea12436dc" +dependencies = [ + "cairo-sys-rs", + "gdk4-sys", + "glib-sys", + "gobject-sys", + "graphene-sys", + "libc", + "pango-sys", + "system-deps", +] + +[[package]] +name = "gtk4" +version = "0.9.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f274dd0102c21c47bbfa8ebcb92d0464fab794a22fad6c3f3d5f165139a326d6" +dependencies = [ + "cairo-rs", + "field-offset", + "futures-channel", + "gdk-pixbuf", + "gdk4", + "gio", + "glib", + "graphene-rs", + "gsk4", + "gtk4-macros", + "gtk4-sys", + "libc", + "pango", +] + +[[package]] +name = "gtk4-macros" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ed1786c4703dd196baf7e103525ce0cf579b3a63a0570fe653b7ee6bac33999" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "gtk4-sys" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41e03b01e54d77c310e1d98647d73f996d04b2f29b9121fe493ea525a7ec03d6" +dependencies = [ + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gdk4-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "graphene-sys", + "gsk4-sys", + "libc", + "pango-sys", + "system-deps", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "memchr" +version = "2.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4" + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "pango" +version = "0.20.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6576b311f6df659397043a5fa8a021da8f72e34af180b44f7d57348de691ab5c" +dependencies = [ + "gio", + "glib", + "libc", + "pango-sys", +] + +[[package]] +name = "pango-sys" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "186909673fc09be354555c302c0b3dcf753cd9fa08dcb8077fa663c80fb243fa" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pkg-config" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" + +[[package]] +name = "proc-macro-crate" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +dependencies = [ + "toml_edit 0.25.12+spec-1.1.0", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.150" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_spanned" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" +dependencies = [ + "serde_core", +] + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "system-deps" +version = "7.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "396a35feb67335377e0251fcbc1092fc85c484bd4e3a7a54319399da127796e7" +dependencies = [ + "cfg-expr", + "heck", + "pkg-config", + "toml 1.1.2+spec-1.1.0", + "version-compare", +] + +[[package]] +name = "target-lexicon" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca" + +[[package]] +name = "toml" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" +dependencies = [ + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.11", + "toml_edit 0.22.27", +] + +[[package]] +name = "toml" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81f3d15e84cbcd896376e6730314d59fb5a87f31e4b038454184435cd57defee" +dependencies = [ + "indexmap", + "serde_core", + "serde_spanned 1.1.1", + "toml_datetime 1.1.1+spec-1.1.0", + "toml_parser", + "toml_writer", + "winnow 1.0.3", +] + +[[package]] +name = "toml_datetime" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.22.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" +dependencies = [ + "indexmap", + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.11", + "toml_write", + "winnow 0.7.15", +] + +[[package]] +name = "toml_edit" +version = "0.25.12+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2153edc6955a6c354fad8f5efd38b6a8769bdccf9fe50f8e1329f81b0baa5d7" +dependencies = [ + "indexmap", + "toml_datetime 1.1.1+spec-1.1.0", + "toml_parser", + "winnow 1.0.3", +] + +[[package]] +name = "toml_parser" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" +dependencies = [ + "winnow 1.0.3", +] + +[[package]] +name = "toml_write" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" + +[[package]] +name = "toml_writer" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "version-compare" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03c2856837ef78f57382f06b2b8563a2f512f7185d732608fd9176cb3b8edf0e" + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "winnow" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" +dependencies = [ + "memchr", +] + +[[package]] +name = "winnow" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1" +dependencies = [ + "memchr", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..af9532d --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,3 @@ +[workspace] +members = ["bos-settings"] +resolver = "2" diff --git a/bos-settings/Cargo.toml b/bos-settings/Cargo.toml new file mode 100644 index 0000000..24dabe0 --- /dev/null +++ b/bos-settings/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "bos-settings" +version = "0.1.0" +edition = "2021" + +[dependencies] +gtk4 = { version = "0.9", features = ["v4_12"] } +glib = "0.20" +serde = { version = "1", features = ["derive"] } +serde_json = "1" +toml = "0.8" diff --git a/bos-settings/src/config/mod.rs b/bos-settings/src/config/mod.rs new file mode 100644 index 0000000..08b005b --- /dev/null +++ b/bos-settings/src/config/mod.rs @@ -0,0 +1,24 @@ +use std::error::Error; +use std::path::Path; + +pub fn load serde::Deserialize<'de>>(path: &Path) -> Result> { + let text = std::fs::read_to_string(path)?; + Ok(toml::from_str(&text)?) +} + +pub fn save(path: &Path, val: &T) -> Result<(), Box> { + if let Some(parent) = path.parent() { + std::fs::create_dir_all(parent)?; + } + std::fs::write(path, toml::to_string_pretty(val)?)?; + Ok(()) +} + +pub fn config_dir() -> std::path::PathBuf { + dirs_path() +} + +fn dirs_path() -> std::path::PathBuf { + let home = std::env::var("HOME").unwrap_or_else(|_| "/root".to_string()); + std::path::PathBuf::from(home).join(".config") +} diff --git a/bos-settings/src/main.rs b/bos-settings/src/main.rs new file mode 100644 index 0000000..c93a8d2 --- /dev/null +++ b/bos-settings/src/main.rs @@ -0,0 +1,12 @@ +mod config; +mod state; +mod theme; +mod ui; + +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/state.rs b/bos-settings/src/state.rs new file mode 100644 index 0000000..e0e760e --- /dev/null +++ b/bos-settings/src/state.rs @@ -0,0 +1,11 @@ +pub struct AppState { + pub current_view: String, +} + +impl AppState { + pub fn new() -> Self { + Self { + current_view: "snapshots".to_string(), + } + } +} diff --git a/bos-settings/src/theme.rs b/bos-settings/src/theme.rs new file mode 100644 index 0000000..ae850d0 --- /dev/null +++ b/bos-settings/src/theme.rs @@ -0,0 +1,88 @@ +use gtk4::prelude::*; +use gtk4::CssProvider; + +const CSS: &str = r#" +window { + background-color: #2e3440; + color: #eceff4; +} + +.sidebar { + background-color: #3b4252; + border-right: 1px solid #434c5e; +} + +.sidebar row { + padding: 8px 12px; + color: #d8dee9; +} + +.sidebar row:selected { + background-color: #5e81ac; + color: #eceff4; +} + +.sidebar .section-header { + padding: 12px 12px 4px 12px; + font-size: 0.75em; + font-weight: bold; + color: #616e88; + text-transform: uppercase; + letter-spacing: 1px; +} + +.view-content { + padding: 24px; +} + +.view-content label.title { + font-size: 1.4em; + font-weight: bold; + color: #eceff4; + margin-bottom: 16px; +} + +button { + background-color: #5e81ac; + color: #eceff4; + border: none; + border-radius: 4px; + padding: 6px 16px; +} + +button:hover { + background-color: #81a1c1; +} + +button.destructive-action { + background-color: #bf616a; +} + +button.destructive-action:hover { + background-color: #d08770; +} + +entry { + background-color: #434c5e; + color: #eceff4; + border: 1px solid #4c566a; + border-radius: 4px; +} + +textview { + background-color: #272c36; + color: #a3be8c; + font-family: monospace; + padding: 8px; +} +"#; + +pub fn load(display: >k4::gdk::Display) { + let provider = CssProvider::new(); + provider.load_from_string(CSS); + gtk4::style_context_add_provider_for_display( + display, + &provider, + gtk4::STYLE_PROVIDER_PRIORITY_APPLICATION, + ); +} diff --git a/bos-settings/src/ui/mod.rs b/bos-settings/src/ui/mod.rs new file mode 100644 index 0000000..1a2b383 --- /dev/null +++ b/bos-settings/src/ui/mod.rs @@ -0,0 +1,3 @@ +pub mod sidebar; +pub mod views; +pub mod window; diff --git a/bos-settings/src/ui/sidebar.rs b/bos-settings/src/ui/sidebar.rs new file mode 100644 index 0000000..daafb0b --- /dev/null +++ b/bos-settings/src/ui/sidebar.rs @@ -0,0 +1,66 @@ +use gtk4::prelude::*; +use gtk4::{Box as GBox, Label, ListBox, ListBoxRow, Orientation, Separator}; + +pub struct SidebarItem { + pub id: &'static str, + pub label: &'static str, +} + +pub const APPS_ITEMS: &[SidebarItem] = &[ + SidebarItem { id: "bread", label: "bread" }, + SidebarItem { id: "breadbar", label: "breadbar" }, + SidebarItem { id: "breadbox", label: "breadbox" }, + SidebarItem { id: "breadcrumbs", label: "breadcrumbs" }, + SidebarItem { id: "breadpad", label: "breadpad" }, +]; + +pub const SYSTEM_ITEMS: &[SidebarItem] = &[ + SidebarItem { id: "snapshots", label: "Snapshots" }, + SidebarItem { id: "packages", label: "Packages" }, + SidebarItem { id: "hyprland", label: "Hyprland" }, +]; + +pub fn build() -> (GBox, ListBox) { + let vbox = GBox::new(Orientation::Vertical, 0); + vbox.add_css_class("sidebar"); + vbox.set_width_request(190); + + let list = ListBox::new(); + list.set_selection_mode(gtk4::SelectionMode::Single); + list.add_css_class("sidebar"); + + append_section(&list, "Apps", APPS_ITEMS); + append_section(&list, "System", SYSTEM_ITEMS); + + // Select first item by default + if let Some(first) = list.row_at_index(1) { + list.select_row(Some(&first)); + } + + vbox.append(&list); + (vbox, list) +} + +fn append_section(list: &ListBox, title: &str, items: &[SidebarItem]) { + // Section header (non-selectable) + 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 item in items { + let row = ListBoxRow::new(); + row.set_widget_name(item.id); + + let lbl = Label::new(Some(item.label)); + lbl.set_xalign(0.0); + lbl.set_margin_top(2); + lbl.set_margin_bottom(2); + row.set_child(Some(&lbl)); + list.append(&row); + } +} diff --git a/bos-settings/src/ui/views/bread.rs b/bos-settings/src/ui/views/bread.rs new file mode 100644 index 0000000..ca4b080 --- /dev/null +++ b/bos-settings/src/ui/views/bread.rs @@ -0,0 +1,149 @@ +use gtk4::prelude::*; +use gtk4::{Box as GBox, Button, DropDown, Label, Orientation, StringList, Switch}; +use serde::{Deserialize, Serialize}; +use std::cell::RefCell; +use std::rc::Rc; + +use crate::config; + +#[derive(Deserialize, Serialize, Clone)] +pub struct BreadConfig { + #[serde(default = "default_log_level")] + pub log_level: String, + #[serde(default)] + pub adapters: AdaptersConfig, +} + +fn default_log_level() -> String { + "info".to_string() +} + +#[derive(Deserialize, Serialize, Clone, Default)] +pub struct AdaptersConfig { + #[serde(default = "default_true")] + pub keyboard: bool, + #[serde(default = "default_true")] + pub mouse: bool, + #[serde(default = "default_true")] + pub touchpad: bool, + #[serde(default = "default_true")] + pub bluetooth: bool, + #[serde(default = "default_true")] + pub gamepad: bool, +} + +fn default_true() -> bool { + true +} + +impl Default for BreadConfig { + fn default() -> Self { + Self { + log_level: default_log_level(), + adapters: AdaptersConfig::default(), + } + } +} + +fn config_path() -> std::path::PathBuf { + config::config_dir().join("bread/breadd.toml") +} + +fn adapter_row(label: &str, active: bool, cfg: Rc>, field: &'static str) -> GBox { + let row = GBox::new(Orientation::Horizontal, 16); + let lbl = Label::new(Some(label)); + lbl.set_hexpand(true); + lbl.set_xalign(0.0); + let sw = Switch::new(); + sw.set_active(active); + sw.connect_active_notify(move |s| { + let val = s.is_active(); + let mut c = cfg.borrow_mut(); + match field { + "keyboard" => c.adapters.keyboard = val, + "mouse" => c.adapters.mouse = val, + "touchpad" => c.adapters.touchpad = val, + "bluetooth" => c.adapters.bluetooth = val, + "gamepad" => c.adapters.gamepad = val, + _ => {} + } + }); + row.append(&lbl); + row.append(&sw); + row +} + +pub fn build() -> GBox { + let path = config_path(); + let cfg: BreadConfig = config::load(&path).unwrap_or_default(); + let cfg = Rc::new(RefCell::new(cfg)); + + let vbox = GBox::new(Orientation::Vertical, 12); + vbox.add_css_class("view-content"); + + let title = Label::new(Some("bread")); + title.add_css_class("title"); + title.set_xalign(0.0); + vbox.append(&title); + + // Log level + let row = GBox::new(Orientation::Horizontal, 16); + row.set_margin_bottom(8); + let lbl = Label::new(Some("Log level")); + lbl.set_hexpand(true); + lbl.set_xalign(0.0); + let levels = StringList::new(&["error", "warn", "info", "debug", "trace"]); + let dropdown = DropDown::new(Some(levels), gtk4::Expression::NONE); + let current_pos = match cfg.borrow().log_level.as_str() { + "error" => 0u32, + "warn" => 1, + "info" => 2, + "debug" => 3, + "trace" => 4, + _ => 2, + }; + dropdown.set_selected(current_pos); + { + let cfg = cfg.clone(); + dropdown.connect_selected_notify(move |dd| { + let levels = ["error", "warn", "info", "debug", "trace"]; + if let Some(&level) = levels.get(dd.selected() as usize) { + cfg.borrow_mut().log_level = level.to_string(); + } + }); + } + row.append(&lbl); + row.append(&dropdown); + vbox.append(&row); + + // Adapter toggles + let adapter_label = Label::new(Some("Adapters")); + adapter_label.set_xalign(0.0); + adapter_label.set_margin_top(8); + adapter_label.set_margin_bottom(4); + vbox.append(&adapter_label); + + let (kbd, mouse, touchpad, bluetooth, gamepad) = { + let c = cfg.borrow(); + (c.adapters.keyboard, c.adapters.mouse, c.adapters.touchpad, c.adapters.bluetooth, c.adapters.gamepad) + }; + + vbox.append(&adapter_row("Keyboard", kbd, cfg.clone(), "keyboard")); + vbox.append(&adapter_row("Mouse", mouse, cfg.clone(), "mouse")); + vbox.append(&adapter_row("Touchpad", touchpad, cfg.clone(), "touchpad")); + vbox.append(&adapter_row("Bluetooth", bluetooth, cfg.clone(), "bluetooth")); + vbox.append(&adapter_row("Gamepad", gamepad, cfg.clone(), "gamepad")); + + let save_btn = Button::with_label("Save"); + save_btn.set_margin_top(16); + save_btn.set_halign(gtk4::Align::Start); + { + let cfg = cfg.clone(); + save_btn.connect_clicked(move |_| { + let _ = config::save(&path, &*cfg.borrow()); + }); + } + vbox.append(&save_btn); + + vbox +} diff --git a/bos-settings/src/ui/views/breadbar.rs b/bos-settings/src/ui/views/breadbar.rs new file mode 100644 index 0000000..2314111 --- /dev/null +++ b/bos-settings/src/ui/views/breadbar.rs @@ -0,0 +1,58 @@ +use gtk4::prelude::*; +use gtk4::{Box as GBox, Button, Label, Orientation, ScrolledWindow, TextView}; +use std::path::PathBuf; + +fn css_path() -> PathBuf { + let home = std::env::var("HOME").unwrap_or_else(|_| "/root".to_string()); + PathBuf::from(home).join(".config/breadbar/style.css") +} + +pub fn build() -> GBox { + let path = css_path(); + let existing_css = std::fs::read_to_string(&path).unwrap_or_default(); + + let vbox = GBox::new(Orientation::Vertical, 12); + vbox.add_css_class("view-content"); + + let title = Label::new(Some("breadbar")); + title.add_css_class("title"); + title.set_xalign(0.0); + vbox.append(&title); + + let subtitle = Label::new(Some( + "CSS overrides for breadbar. Leave empty to use the default bread theme.", + )); + subtitle.set_xalign(0.0); + subtitle.set_margin_bottom(8); + subtitle.set_wrap(true); + vbox.append(&subtitle); + + let buf = gtk4::TextBuffer::new(None); + buf.set_text(&existing_css); + + let text_view = TextView::with_buffer(&buf); + text_view.set_monospace(true); + + let scroll = ScrolledWindow::new(); + scroll.set_vexpand(true); + scroll.set_child(Some(&text_view)); + vbox.append(&scroll); + + let save_btn = Button::with_label("Save"); + save_btn.set_margin_top(12); + save_btn.set_halign(gtk4::Align::Start); + { + let path = path.clone(); + save_btn.connect_clicked(move |_| { + let (start, end) = buf.bounds(); + let text = buf.text(&start, &end, false); + if let Some(parent) = path.parent() { + let _ = std::fs::create_dir_all(parent); + } + let _ = std::fs::write(&path, text.as_str()); + }); + } + vbox.append(&save_btn); + + vbox +} diff --git a/bos-settings/src/ui/views/breadbox.rs b/bos-settings/src/ui/views/breadbox.rs new file mode 100644 index 0000000..715ce71 --- /dev/null +++ b/bos-settings/src/ui/views/breadbox.rs @@ -0,0 +1,134 @@ +use gtk4::prelude::*; +use gtk4::{Box as GBox, Button, Entry, Label, ListBox, ListBoxRow, Orientation, ScrolledWindow}; +use serde::{Deserialize, Serialize}; +use std::cell::RefCell; +use std::rc::Rc; + +use crate::config; + +#[derive(Deserialize, Serialize, Clone, Default)] +pub struct BreadboxConfig { + #[serde(default)] + pub context: Vec, +} + +#[derive(Deserialize, Serialize, Clone)] +pub struct Context { + pub name: String, + #[serde(default)] + pub apps: Vec, +} + +fn config_path() -> std::path::PathBuf { + config::config_dir().join("breadbox/config.toml") +} + +fn rebuild_list(list: &ListBox, cfg: &Rc>) { + while let Some(child) = list.first_child() { + list.remove(&child); + } + for (i, ctx) in cfg.borrow().context.iter().enumerate() { + let row = ListBoxRow::new(); + let hbox = GBox::new(Orientation::Horizontal, 8); + hbox.set_margin_top(6); + hbox.set_margin_bottom(6); + hbox.set_margin_start(8); + hbox.set_margin_end(8); + + let name_entry = Entry::new(); + name_entry.set_text(&ctx.name); + name_entry.set_width_chars(16); + + let apps_entry = Entry::new(); + apps_entry.set_text(&ctx.apps.join(", ")); + apps_entry.set_hexpand(true); + apps_entry.set_placeholder_text(Some("app1, app2, ...")); + + { + let cfg = cfg.clone(); + name_entry.connect_changed(move |e| { + if let Some(c) = cfg.borrow_mut().context.get_mut(i) { + c.name = e.text().to_string(); + } + }); + } + { + let cfg = cfg.clone(); + apps_entry.connect_changed(move |e| { + if let Some(c) = cfg.borrow_mut().context.get_mut(i) { + c.apps = e + .text() + .split(',') + .map(|s| s.trim().to_string()) + .filter(|s| !s.is_empty()) + .collect(); + } + }); + } + + hbox.append(&name_entry); + hbox.append(&apps_entry); + row.set_child(Some(&hbox)); + list.append(&row); + } +} + +pub fn build() -> GBox { + let path = config_path(); + let cfg: BreadboxConfig = config::load(&path).unwrap_or_default(); + let cfg = Rc::new(RefCell::new(cfg)); + + let vbox = GBox::new(Orientation::Vertical, 12); + vbox.add_css_class("view-content"); + + let title = Label::new(Some("breadbox")); + title.add_css_class("title"); + title.set_xalign(0.0); + vbox.append(&title); + + let subtitle = Label::new(Some("Context priority lists — apps shown in each context.")); + subtitle.set_xalign(0.0); + subtitle.set_margin_bottom(8); + vbox.append(&subtitle); + + let list = ListBox::new(); + list.set_selection_mode(gtk4::SelectionMode::None); + + rebuild_list(&list, &cfg); + + let scroll = ScrolledWindow::new(); + scroll.set_vexpand(true); + scroll.set_child(Some(&list)); + vbox.append(&scroll); + + let btn_row = GBox::new(Orientation::Horizontal, 8); + btn_row.set_margin_top(8); + + let add_btn = Button::with_label("Add context"); + { + let cfg = cfg.clone(); + let list = list.clone(); + add_btn.connect_clicked(move |_| { + cfg.borrow_mut().context.push(Context { + name: "new".to_string(), + apps: Vec::new(), + }); + rebuild_list(&list, &cfg); + }); + } + + let save_btn = Button::with_label("Save"); + { + let cfg = cfg.clone(); + let path = path.clone(); + save_btn.connect_clicked(move |_| { + let _ = config::save(&path, &*cfg.borrow()); + }); + } + + btn_row.append(&add_btn); + btn_row.append(&save_btn); + vbox.append(&btn_row); + + vbox +} diff --git a/bos-settings/src/ui/views/breadcrumbs.rs b/bos-settings/src/ui/views/breadcrumbs.rs new file mode 100644 index 0000000..4062645 --- /dev/null +++ b/bos-settings/src/ui/views/breadcrumbs.rs @@ -0,0 +1,134 @@ +use gtk4::prelude::*; +use gtk4::{Box as GBox, Button, Entry, Label, ListBox, ListBoxRow, Orientation, ScrolledWindow}; +use serde::{Deserialize, Serialize}; +use std::cell::RefCell; +use std::rc::Rc; + +use crate::config; + +#[derive(Deserialize, Serialize, Clone, Default)] +pub struct BreadcrumbsConfig { + #[serde(default)] + pub profile: Vec, +} + +#[derive(Deserialize, Serialize, Clone)] +pub struct Profile { + pub name: String, + #[serde(default)] + pub ssids: Vec, +} + +fn config_path() -> std::path::PathBuf { + config::config_dir().join("breadcrumbs/breadcrumbs.toml") +} + +fn rebuild_list(list: &ListBox, cfg: &Rc>) { + while let Some(child) = list.first_child() { + list.remove(&child); + } + for (i, profile) in cfg.borrow().profile.iter().enumerate() { + let row = ListBoxRow::new(); + let hbox = GBox::new(Orientation::Horizontal, 8); + hbox.set_margin_top(6); + hbox.set_margin_bottom(6); + hbox.set_margin_start(8); + hbox.set_margin_end(8); + + let name_entry = Entry::new(); + name_entry.set_text(&profile.name); + name_entry.set_width_chars(16); + + let ssids_entry = Entry::new(); + ssids_entry.set_text(&profile.ssids.join(", ")); + ssids_entry.set_hexpand(true); + ssids_entry.set_placeholder_text(Some("SSID1, SSID2, ...")); + + { + let cfg = cfg.clone(); + name_entry.connect_changed(move |e| { + if let Some(p) = cfg.borrow_mut().profile.get_mut(i) { + p.name = e.text().to_string(); + } + }); + } + { + let cfg = cfg.clone(); + ssids_entry.connect_changed(move |e| { + if let Some(p) = cfg.borrow_mut().profile.get_mut(i) { + p.ssids = e + .text() + .split(',') + .map(|s| s.trim().to_string()) + .filter(|s| !s.is_empty()) + .collect(); + } + }); + } + + hbox.append(&name_entry); + hbox.append(&ssids_entry); + row.set_child(Some(&hbox)); + list.append(&row); + } +} + +pub fn build() -> GBox { + let path = config_path(); + let cfg: BreadcrumbsConfig = config::load(&path).unwrap_or_default(); + let cfg = Rc::new(RefCell::new(cfg)); + + let vbox = GBox::new(Orientation::Vertical, 12); + vbox.add_css_class("view-content"); + + let title = Label::new(Some("breadcrumbs")); + title.add_css_class("title"); + title.set_xalign(0.0); + vbox.append(&title); + + let subtitle = Label::new(Some("Network profiles — SSIDs associated with each location.")); + subtitle.set_xalign(0.0); + subtitle.set_margin_bottom(8); + vbox.append(&subtitle); + + let list = ListBox::new(); + list.set_selection_mode(gtk4::SelectionMode::None); + + rebuild_list(&list, &cfg); + + let scroll = ScrolledWindow::new(); + scroll.set_vexpand(true); + scroll.set_child(Some(&list)); + vbox.append(&scroll); + + let btn_row = GBox::new(Orientation::Horizontal, 8); + btn_row.set_margin_top(8); + + let add_btn = Button::with_label("Add profile"); + { + let cfg = cfg.clone(); + let list = list.clone(); + add_btn.connect_clicked(move |_| { + cfg.borrow_mut().profile.push(Profile { + name: "new".to_string(), + ssids: Vec::new(), + }); + rebuild_list(&list, &cfg); + }); + } + + let save_btn = Button::with_label("Save"); + { + let cfg = cfg.clone(); + let path = path.clone(); + save_btn.connect_clicked(move |_| { + let _ = config::save(&path, &*cfg.borrow()); + }); + } + + btn_row.append(&add_btn); + btn_row.append(&save_btn); + vbox.append(&btn_row); + + vbox +} diff --git a/bos-settings/src/ui/views/breadpad.rs b/bos-settings/src/ui/views/breadpad.rs new file mode 100644 index 0000000..9c553f4 --- /dev/null +++ b/bos-settings/src/ui/views/breadpad.rs @@ -0,0 +1,118 @@ +use gtk4::prelude::*; +use gtk4::{Box as GBox, Button, Entry, Label, Orientation, Switch}; +use serde::{Deserialize, Serialize}; +use std::cell::RefCell; +use std::rc::Rc; + +use crate::config; + +#[derive(Deserialize, Serialize, Clone)] +pub struct BreadpadConfig { + #[serde(default = "default_model")] + pub model: String, + #[serde(default = "default_true")] + pub reminders: bool, + #[serde(default = "default_true")] + pub calendar: bool, +} + +fn default_model() -> String { + "claude-sonnet-4-6".to_string() +} + +fn default_true() -> bool { + true +} + +impl Default for BreadpadConfig { + fn default() -> Self { + Self { + model: default_model(), + reminders: true, + calendar: true, + } + } +} + +fn config_path() -> std::path::PathBuf { + config::config_dir().join("breadpad/breadpad.toml") +} + +pub fn build() -> GBox { + let path = config_path(); + let cfg: BreadpadConfig = config::load(&path).unwrap_or_default(); + let cfg = Rc::new(RefCell::new(cfg)); + + let vbox = GBox::new(Orientation::Vertical, 12); + vbox.add_css_class("view-content"); + + let title = Label::new(Some("breadpad")); + title.add_css_class("title"); + title.set_xalign(0.0); + vbox.append(&title); + + // Model entry + let row = GBox::new(Orientation::Horizontal, 16); + let lbl = Label::new(Some("Model")); + lbl.set_hexpand(true); + lbl.set_xalign(0.0); + let model_entry = Entry::new(); + model_entry.set_text(&cfg.borrow().model); + { + let cfg = cfg.clone(); + model_entry.connect_changed(move |e| { + cfg.borrow_mut().model = e.text().to_string(); + }); + } + row.append(&lbl); + row.append(&model_entry); + vbox.append(&row); + + // Reminders toggle + let row = GBox::new(Orientation::Horizontal, 16); + let lbl = Label::new(Some("Reminders")); + lbl.set_hexpand(true); + lbl.set_xalign(0.0); + let sw = Switch::new(); + sw.set_active(cfg.borrow().reminders); + { + let cfg = cfg.clone(); + sw.connect_active_notify(move |s| { + cfg.borrow_mut().reminders = s.is_active(); + }); + } + row.append(&lbl); + row.append(&sw); + vbox.append(&row); + + // Calendar toggle + let row = GBox::new(Orientation::Horizontal, 16); + let lbl = Label::new(Some("Calendar integration")); + lbl.set_hexpand(true); + lbl.set_xalign(0.0); + let sw = Switch::new(); + sw.set_active(cfg.borrow().calendar); + { + let cfg = cfg.clone(); + sw.connect_active_notify(move |s| { + cfg.borrow_mut().calendar = s.is_active(); + }); + } + row.append(&lbl); + row.append(&sw); + vbox.append(&row); + + let save_btn = Button::with_label("Save"); + save_btn.set_margin_top(16); + save_btn.set_halign(gtk4::Align::Start); + { + let cfg = cfg.clone(); + let path = path.clone(); + save_btn.connect_clicked(move |_| { + let _ = config::save(&path, &*cfg.borrow()); + }); + } + vbox.append(&save_btn); + + vbox +} diff --git a/bos-settings/src/ui/views/hyprland.rs b/bos-settings/src/ui/views/hyprland.rs new file mode 100644 index 0000000..cb43b30 --- /dev/null +++ b/bos-settings/src/ui/views/hyprland.rs @@ -0,0 +1,92 @@ +use gtk4::prelude::*; +use gtk4::{Box as GBox, Button, Label, Orientation}; +use std::process::Command; + +fn get_monitors() -> Vec { + let Ok(output) = Command::new("hyprctl").args(["monitors", "-j"]).output() else { + return Vec::new(); + }; + let text = String::from_utf8_lossy(&output.stdout); + let Ok(monitors) = serde_json::from_str::>(&text) else { + return Vec::new(); + }; + monitors + .iter() + .filter_map(|m| { + let name = m.get("name")?.as_str()?; + let w = m.get("width")?.as_u64()?; + let h = m.get("height")?.as_u64()?; + let refresh = m.get("refreshRate")?.as_f64()?; + Some(format!("{name} {w}×{h} @ {refresh:.0}Hz")) + }) + .collect() +} + +fn config_path() -> std::path::PathBuf { + let home = std::env::var("HOME").unwrap_or_else(|_| "/root".to_string()); + std::path::PathBuf::from(home).join(".config/hypr/hyprland.conf") +} + +pub fn build() -> GBox { + let vbox = GBox::new(Orientation::Vertical, 12); + vbox.add_css_class("view-content"); + + let title = Label::new(Some("Hyprland")); + title.add_css_class("title"); + title.set_xalign(0.0); + vbox.append(&title); + + // Monitors section + let monitors_lbl = Label::new(Some("Connected monitors")); + monitors_lbl.set_xalign(0.0); + monitors_lbl.set_margin_top(8); + monitors_lbl.set_margin_bottom(4); + vbox.append(&monitors_lbl); + + let monitors = get_monitors(); + if monitors.is_empty() { + let lbl = Label::new(Some("No monitors detected (is Hyprland running?)")); + lbl.set_xalign(0.0); + vbox.append(&lbl); + } else { + for mon in &monitors { + let lbl = Label::new(Some(mon)); + lbl.set_xalign(0.0); + lbl.add_css_class("monospace"); + vbox.append(&lbl); + } + } + + // Open config button + let open_btn = Button::with_label("Open hyprland.conf in editor"); + open_btn.set_margin_top(16); + open_btn.set_halign(gtk4::Align::Start); + { + let path = config_path(); + open_btn.connect_clicked(move |_| { + let editor = std::env::var("EDITOR").unwrap_or_else(|_| "foot".to_string()); + let _ = Command::new(&editor) + .arg(path.to_str().unwrap_or("")) + .spawn(); + }); + } + vbox.append(&open_btn); + + // Open keybinds button + let keybinds_btn = Button::with_label("Open keybinds.conf in editor"); + keybinds_btn.set_margin_top(8); + keybinds_btn.set_halign(gtk4::Align::Start); + { + let home = std::env::var("HOME").unwrap_or_else(|_| "/root".to_string()); + let kb_path = std::path::PathBuf::from(home).join(".config/hypr/keybinds.conf"); + keybinds_btn.connect_clicked(move |_| { + let editor = std::env::var("EDITOR").unwrap_or_else(|_| "foot".to_string()); + let _ = Command::new(&editor) + .arg(kb_path.to_str().unwrap_or("")) + .spawn(); + }); + } + vbox.append(&keybinds_btn); + + vbox +} diff --git a/bos-settings/src/ui/views/mod.rs b/bos-settings/src/ui/views/mod.rs new file mode 100644 index 0000000..67763f0 --- /dev/null +++ b/bos-settings/src/ui/views/mod.rs @@ -0,0 +1,8 @@ +pub mod bread; +pub mod breadbar; +pub mod breadbox; +pub mod breadcrumbs; +pub mod breadpad; +pub mod hyprland; +pub mod packages; +pub mod snapshots; diff --git a/bos-settings/src/ui/views/packages.rs b/bos-settings/src/ui/views/packages.rs new file mode 100644 index 0000000..448ec53 --- /dev/null +++ b/bos-settings/src/ui/views/packages.rs @@ -0,0 +1,171 @@ +use gtk4::prelude::*; +use gtk4::{ + Box as GBox, Button, Label, ListBox, ListBoxRow, Orientation, ScrolledWindow, TextView, +}; +use serde::Deserialize; +use std::collections::HashMap; +use std::process::Command; + +#[derive(Deserialize, Default)] +struct InstalledPackages { + #[serde(flatten)] + packages: HashMap, +} + +#[derive(Deserialize)] +struct PackageInfo { + version: String, +} + +fn read_installed() -> HashMap { + let home = std::env::var("HOME").unwrap_or_else(|_| "/root".to_string()); + let path = std::path::Path::new(&home) + .join(".local/state/bakery/installed.json"); + + let Ok(text) = std::fs::read_to_string(&path) else { + return HashMap::new(); + }; + + let Ok(parsed) = serde_json::from_str::>(&text) else { + return HashMap::new(); + }; + + parsed + .into_iter() + .filter_map(|(name, val)| { + let version = val + .get("version") + .or_else(|| val.as_str().map(|_| &val)) + .and_then(|v| v.as_str()) + .map(|s| s.to_string()) + .unwrap_or_else(|| "unknown".to_string()); + Some((name, version)) + }) + .collect() +} + +pub fn build() -> GBox { + let vbox = GBox::new(Orientation::Vertical, 0); + vbox.add_css_class("view-content"); + + let title = Label::new(Some("Packages")); + title.add_css_class("title"); + title.set_xalign(0.0); + vbox.append(&title); + + let subtitle = Label::new(Some("Bread ecosystem packages installed via bakery.")); + subtitle.set_xalign(0.0); + subtitle.set_margin_bottom(16); + vbox.append(&subtitle); + + let list = ListBox::new(); + list.set_selection_mode(gtk4::SelectionMode::None); + + let packages = read_installed(); + if packages.is_empty() { + let row = ListBoxRow::new(); + let lbl = Label::new(Some("No bakery packages found (~/.local/state/bakery/installed.json)")); + lbl.set_margin_top(8); + lbl.set_margin_bottom(8); + lbl.set_margin_start(8); + row.set_child(Some(&lbl)); + list.append(&row); + } else { + let mut names: Vec<_> = packages.iter().collect(); + names.sort_by_key(|(k, _)| k.as_str()); + for (name, version) in names { + let row = ListBoxRow::new(); + let hbox = GBox::new(Orientation::Horizontal, 16); + hbox.set_margin_top(6); + hbox.set_margin_bottom(6); + hbox.set_margin_start(8); + hbox.set_margin_end(8); + + let name_lbl = Label::new(Some(name)); + name_lbl.set_hexpand(true); + name_lbl.set_xalign(0.0); + + let ver_lbl = Label::new(Some(version)); + ver_lbl.set_xalign(1.0); + + let update_btn = Button::with_label("Update"); + let pkg_name = name.clone(); + update_btn.connect_clicked(move |_| { + let _ = Command::new("bakery") + .args(["update", &pkg_name]) + .spawn(); + }); + + hbox.append(&name_lbl); + hbox.append(&ver_lbl); + hbox.append(&update_btn); + row.set_child(Some(&hbox)); + list.append(&row); + } + } + + let scroll = ScrolledWindow::new(); + scroll.set_vexpand(true); + scroll.set_child(Some(&list)); + vbox.append(&scroll); + + let btn_row = GBox::new(Orientation::Horizontal, 8); + btn_row.set_margin_top(12); + + let check_btn = Button::with_label("Check for updates"); + let update_all_btn = Button::with_label("Update all"); + + let log_buf = gtk4::TextBuffer::new(None); + let log_view = TextView::with_buffer(&log_buf); + log_view.set_editable(false); + log_view.set_height_request(120); + log_view.set_margin_top(8); + + { + let log_buf = log_buf.clone(); + check_btn.connect_clicked(move |_| { + log_buf.set_text("Checking for updates...\n"); + match Command::new("bakery").args(["list"]).output() { + Ok(out) => { + let text = String::from_utf8_lossy(&out.stdout); + log_buf.set_text(&format!("{text}\n")); + } + Err(e) => { + log_buf.set_text(&format!("Error: {e}\n")); + } + } + }); + } + + { + let log_buf = log_buf.clone(); + update_all_btn.connect_clicked(move |_| { + log_buf.set_text("Running bakery update --all...\n"); + let (sender, receiver) = glib::MainContext::channel(glib::Priority::DEFAULT); + std::thread::spawn(move || { + let result = Command::new("bakery") + .args(["update", "--all"]) + .output(); + match result { + Ok(out) => { + let _ = sender.send(String::from_utf8_lossy(&out.stdout).to_string()); + } + Err(e) => { + let _ = sender.send(format!("Error: {e}\n")); + } + } + }); + receiver.attach(None, move |msg| { + log_buf.set_text(&msg); + glib::ControlFlow::Break + }); + }); + } + + btn_row.append(&check_btn); + btn_row.append(&update_all_btn); + vbox.append(&btn_row); + vbox.append(&log_view); + + vbox +} diff --git a/bos-settings/src/ui/views/snapshots.rs b/bos-settings/src/ui/views/snapshots.rs new file mode 100644 index 0000000..da34141 --- /dev/null +++ b/bos-settings/src/ui/views/snapshots.rs @@ -0,0 +1,167 @@ +use gtk4::prelude::*; +use gtk4::{ + Box as GBox, Button, Label, ListBox, ListBoxRow, MessageDialog, Orientation, ScrolledWindow, +}; +use std::process::Command; + +struct SnapshotRow { + number: String, + date: String, + description: String, +} + +fn list_snapshots() -> Vec { + let Ok(output) = Command::new("snapper") + .args(["list", "--output-cols", "number,date,description"]) + .output() + else { + return Vec::new(); + }; + + let text = String::from_utf8_lossy(&output.stdout); + text.lines() + .skip(2) // header + separator + .filter_map(|line| { + let cols: Vec<&str> = line.splitn(3, '|').collect(); + if cols.len() == 3 { + Some(SnapshotRow { + number: cols[0].trim().to_string(), + date: cols[1].trim().to_string(), + description: cols[2].trim().to_string(), + }) + } else { + None + } + }) + .collect() +} + +fn confirm_rollback(number: &str) { + let number = number.to_string(); + let dialog = MessageDialog::new( + None::<>k4::Window>, + gtk4::DialogFlags::MODAL, + gtk4::MessageType::Question, + gtk4::ButtonsType::OkCancel, + &format!("Roll back to snapshot #{number}?\n\nReboot required to apply."), + ); + dialog.connect_response(move |d, resp| { + if resp == gtk4::ResponseType::Ok { + let _ = Command::new("snapper") + .args(["rollback", &number]) + .status(); + let info = MessageDialog::new( + None::<>k4::Window>, + gtk4::DialogFlags::MODAL, + gtk4::MessageType::Info, + gtk4::ButtonsType::Ok, + "Rollback queued. Please reboot to apply.", + ); + info.connect_response(|d, _| d.destroy()); + info.present(); + } + d.destroy(); + }); + dialog.present(); +} + +pub fn build() -> GBox { + let vbox = GBox::new(Orientation::Vertical, 0); + vbox.add_css_class("view-content"); + + let title = Label::new(Some("Snapshots")); + title.add_css_class("title"); + title.set_xalign(0.0); + vbox.append(&title); + + let subtitle = + Label::new(Some("System snapshots created by snap-pac on each pacman transaction.")); + subtitle.set_xalign(0.0); + subtitle.set_margin_bottom(16); + vbox.append(&subtitle); + + let list = ListBox::new(); + list.set_selection_mode(gtk4::SelectionMode::Single); + + let snapshots = list_snapshots(); + if snapshots.is_empty() { + let row = ListBoxRow::new(); + let lbl = Label::new(Some( + "No snapshots found (snapper may not be configured yet)", + )); + lbl.set_margin_top(8); + lbl.set_margin_bottom(8); + lbl.set_margin_start(8); + row.set_child(Some(&lbl)); + list.append(&row); + } else { + for snap in &snapshots { + let row = ListBoxRow::new(); + row.set_widget_name(&snap.number); + + let hbox = GBox::new(Orientation::Horizontal, 16); + hbox.set_margin_top(6); + hbox.set_margin_bottom(6); + hbox.set_margin_start(8); + hbox.set_margin_end(8); + + let num_lbl = Label::new(Some(&snap.number)); + num_lbl.set_width_chars(4); + num_lbl.set_xalign(0.0); + + let date_lbl = Label::new(Some(&snap.date)); + date_lbl.set_width_chars(22); + date_lbl.set_xalign(0.0); + + let desc_lbl = Label::new(Some(&snap.description)); + desc_lbl.set_hexpand(true); + desc_lbl.set_xalign(0.0); + + hbox.append(&num_lbl); + hbox.append(&date_lbl); + hbox.append(&desc_lbl); + row.set_child(Some(&hbox)); + list.append(&row); + } + } + + let scroll = ScrolledWindow::new(); + scroll.set_vexpand(true); + scroll.set_child(Some(&list)); + vbox.append(&scroll); + + let btn_row = GBox::new(Orientation::Horizontal, 8); + btn_row.set_margin_top(12); + + let rollback_btn = Button::with_label("Rollback to selected"); + let delete_btn = Button::with_label("Delete selected"); + delete_btn.add_css_class("destructive-action"); + + { + let list = list.clone(); + rollback_btn.connect_clicked(move |_| { + let Some(row) = list.selected_row() else { + return; + }; + let number = row.widget_name().to_string(); + confirm_rollback(&number); + }); + } + + { + let list = list.clone(); + delete_btn.connect_clicked(move |_| { + let Some(row) = list.selected_row() else { + return; + }; + let number = row.widget_name().to_string(); + let _ = Command::new("snapper").args(["delete", &number]).status(); + }); + } + + btn_row.append(&rollback_btn); + btn_row.append(&delete_btn); + vbox.append(&btn_row); + + vbox +} diff --git a/bos-settings/src/ui/window.rs b/bos-settings/src/ui/window.rs new file mode 100644 index 0000000..0088675 --- /dev/null +++ b/bos-settings/src/ui/window.rs @@ -0,0 +1,57 @@ +use gtk4::prelude::*; +use gtk4::{Application, ApplicationWindow, Box as GBox, Orientation, Paned, Stack}; + +use super::sidebar; +use super::views; + +pub fn build_ui(app: &Application) { + let window = ApplicationWindow::builder() + .application(app) + .title("BOS Settings") + .default_width(960) + .default_height(640) + .build(); + + crate::theme::load(&window.display()); + + let hpaned = Paned::new(Orientation::Horizontal); + hpaned.set_position(190); + hpaned.set_shrink_start_child(false); + hpaned.set_resize_start_child(false); + + let (sidebar_box, list) = sidebar::build(); + + let stack = Stack::new(); + stack.set_hexpand(true); + stack.set_vexpand(true); + + stack.add_named(&views::snapshots::build(), Some("snapshots")); + stack.add_named(&views::packages::build(), Some("packages")); + stack.add_named(&views::bread::build(), Some("bread")); + stack.add_named(&views::breadbar::build(), Some("breadbar")); + stack.add_named(&views::breadbox::build(), Some("breadbox")); + stack.add_named(&views::breadcrumbs::build(), Some("breadcrumbs")); + stack.add_named(&views::breadpad::build(), Some("breadpad")); + stack.add_named(&views::hyprland::build(), Some("hyprland")); + + // Default to snapshots view + stack.set_visible_child_name("snapshots"); + + { + let stack = stack.clone(); + list.connect_row_selected(move |_, row| { + if let Some(row) = row { + let name = row.widget_name(); + if !name.is_empty() { + stack.set_visible_child_name(&name); + } + } + }); + } + + hpaned.set_start_child(Some(&sidebar_box)); + hpaned.set_end_child(Some(&stack)); + + window.set_child(Some(&hpaned)); + window.present(); +} diff --git a/dotfiles/bread/breadd.toml b/dotfiles/bread/breadd.toml new file mode 100644 index 0000000..8473fe3 --- /dev/null +++ b/dotfiles/bread/breadd.toml @@ -0,0 +1,8 @@ +log_level = "info" + +[adapters] +keyboard = true +mouse = true +touchpad = true +bluetooth = true +gamepad = true diff --git a/dotfiles/bread/init.lua b/dotfiles/bread/init.lua new file mode 100644 index 0000000..270ee7e --- /dev/null +++ b/dotfiles/bread/init.lua @@ -0,0 +1 @@ +bread.activate_profile("default") diff --git a/dotfiles/breadbox/config.toml b/dotfiles/breadbox/config.toml new file mode 100644 index 0000000..797b3cc --- /dev/null +++ b/dotfiles/breadbox/config.toml @@ -0,0 +1,3 @@ +[[context]] +name = "default" +apps = ["firefox", "foot", "nautilus", "code"] diff --git a/dotfiles/breadcrumbs/breadcrumbs.toml b/dotfiles/breadcrumbs/breadcrumbs.toml new file mode 100644 index 0000000..46e26f3 --- /dev/null +++ b/dotfiles/breadcrumbs/breadcrumbs.toml @@ -0,0 +1,3 @@ +[[profile]] +name = "home" +ssids = [] diff --git a/dotfiles/hyprland/hyprland.conf b/dotfiles/hyprland/hyprland.conf new file mode 100644 index 0000000..5e56982 --- /dev/null +++ b/dotfiles/hyprland/hyprland.conf @@ -0,0 +1,54 @@ +monitor=,preferred,auto,1 + +exec-once = breadd +exec-once = breadbar +exec-once = breadbox-sync + +source = ~/.config/hypr/keybinds.conf + +general { + gaps_in = 5 + gaps_out = 10 + border_size = 2 + col.active_border = rgba(88c0d0ff) + col.inactive_border = rgba(4c566aff) + layout = dwindle +} + +decoration { + rounding = 8 + blur { + enabled = true + size = 6 + passes = 2 + } + drop_shadow = true + shadow_range = 12 + shadow_render_power = 3 +} + +animations { + enabled = true + bezier = ease, 0.25, 0.1, 0.25, 1.0 + animation = windows, 1, 4, ease + animation = fade, 1, 4, ease + animation = workspaces, 1, 5, ease +} + +input { + kb_layout = us + follow_mouse = 1 + touchpad { + natural_scroll = true + } +} + +dwindle { + pseudotile = true + preserve_split = true +} + +misc { + disable_hyprland_logo = true + disable_splash_rendering = true +} diff --git a/dotfiles/hyprland/keybinds.conf b/dotfiles/hyprland/keybinds.conf new file mode 100644 index 0000000..7cf8cdd --- /dev/null +++ b/dotfiles/hyprland/keybinds.conf @@ -0,0 +1,58 @@ +$mod = SUPER + +# App launchers +bind = $mod, Space, exec, breadbox +bind = $mod, N, exec, breadpad +bind = $mod, M, exec, breadman +bind = $mod, S, exec, bos-settings + +# Core +bind = $mod, Return, exec, foot +bind = $mod, Q, killactive +bind = $mod SHIFT, E, exit +bind = $mod, F, fullscreen + +# Focus +bind = $mod, H, movefocus, l +bind = $mod, L, movefocus, r +bind = $mod, K, movefocus, u +bind = $mod, J, movefocus, d + +# Move windows +bind = $mod SHIFT, H, movewindow, l +bind = $mod SHIFT, L, movewindow, r +bind = $mod SHIFT, K, movewindow, u +bind = $mod SHIFT, J, movewindow, d + +# Workspaces +bind = $mod, 1, workspace, 1 +bind = $mod, 2, workspace, 2 +bind = $mod, 3, workspace, 3 +bind = $mod, 4, workspace, 4 +bind = $mod, 5, workspace, 5 + +bind = $mod SHIFT, 1, movetoworkspace, 1 +bind = $mod SHIFT, 2, movetoworkspace, 2 +bind = $mod SHIFT, 3, movetoworkspace, 3 +bind = $mod SHIFT, 4, movetoworkspace, 4 +bind = $mod SHIFT, 5, movetoworkspace, 5 + +# Scroll through workspaces +bind = $mod, mouse_down, workspace, e+1 +bind = $mod, mouse_up, workspace, e-1 + +# Mouse binds +bindm = $mod, mouse:272, movewindow +bindm = $mod, mouse:273, resizewindow + +# Volume +bind = , XF86AudioRaiseVolume, exec, wpctl set-volume @DEFAULT_AUDIO_SINK@ 5%+ +bind = , XF86AudioLowerVolume, exec, wpctl set-volume @DEFAULT_AUDIO_SINK@ 5%- +bind = , XF86AudioMute, exec, wpctl set-mute @DEFAULT_AUDIO_SINK@ toggle + +# Brightness +bind = , XF86MonBrightnessUp, exec, brightnessctl set 5%+ +bind = , XF86MonBrightnessDown, exec, brightnessctl set 5%- + +# Screenshot +bind = , Print, exec, grimblast copy area diff --git a/iso/airootfs/etc/calamares/modules/bootloader.conf b/iso/airootfs/etc/calamares/modules/bootloader.conf new file mode 100644 index 0000000..d392b23 --- /dev/null +++ b/iso/airootfs/etc/calamares/modules/bootloader.conf @@ -0,0 +1,9 @@ +--- +efiBootloaderId: "BOS" +installEFIFallback: true +grubInstall: "grub-install" +grubMkconfig: "grub-mkconfig" +grubCfg: "/boot/grub/grub.cfg" +grubProbe: "grub-probe" +efiDirectory: "/boot/efi" +kernel: "" diff --git a/iso/airootfs/etc/calamares/modules/finished.conf b/iso/airootfs/etc/calamares/modules/finished.conf new file mode 100644 index 0000000..08cbc62 --- /dev/null +++ b/iso/airootfs/etc/calamares/modules/finished.conf @@ -0,0 +1,5 @@ +--- +restartNowEnabled: true +restartNowChecked: true +restartNowCommand: "systemctl reboot" +notifyOnFinished: false diff --git a/iso/airootfs/etc/calamares/modules/keyboard.conf b/iso/airootfs/etc/calamares/modules/keyboard.conf new file mode 100644 index 0000000..cc51f14 --- /dev/null +++ b/iso/airootfs/etc/calamares/modules/keyboard.conf @@ -0,0 +1,2 @@ +--- +xorgConfDir: "/etc/X11/xorg.conf.d" diff --git a/iso/airootfs/etc/calamares/modules/locale.conf b/iso/airootfs/etc/calamares/modules/locale.conf new file mode 100644 index 0000000..d7d3ba3 --- /dev/null +++ b/iso/airootfs/etc/calamares/modules/locale.conf @@ -0,0 +1,5 @@ +--- +region: "America" +zone: "New_York" +localeGenPath: "/etc/locale.gen" +geoipUrl: "https://geoip.kde.org/v1/calamares" diff --git a/iso/airootfs/etc/calamares/modules/packages.conf b/iso/airootfs/etc/calamares/modules/packages.conf new file mode 100644 index 0000000..c327cb6 --- /dev/null +++ b/iso/airootfs/etc/calamares/modules/packages.conf @@ -0,0 +1,10 @@ +--- +backend: pacman + +options: + - update_db: true + +operations: + - try_install: + - pipewire-pulse + - pipewire-alsa diff --git a/iso/airootfs/etc/calamares/modules/partition.conf b/iso/airootfs/etc/calamares/modules/partition.conf new file mode 100644 index 0000000..a33f199 --- /dev/null +++ b/iso/airootfs/etc/calamares/modules/partition.conf @@ -0,0 +1,29 @@ +--- +efiSystemPartition: "/boot/efi" +efiSystemPartitionSize: "512M" +efiSystemPartitionName: "EFI" + +defaultFileSystemType: "btrfs" + +btrfsSubvolumes: + - mountPoint: / + subvolume: "@" + mountOptions: "noatime,compress=zstd,space_cache=v2" + - mountPoint: /home + subvolume: "@home" + mountOptions: "noatime,compress=zstd,space_cache=v2" + - mountPoint: /.snapshots + subvolume: "@snapshots" + mountOptions: "noatime,compress=zstd,space_cache=v2" + - mountPoint: /var/log + subvolume: "@log" + mountOptions: "noatime,compress=zstd,space_cache=v2" + - mountPoint: /var/cache + subvolume: "@cache" + mountOptions: "noatime,compress=zstd,space_cache=v2" + +userSwapChoices: + - none + - small + - suspend + - file diff --git a/iso/airootfs/etc/calamares/modules/shellprocess.conf b/iso/airootfs/etc/calamares/modules/shellprocess.conf new file mode 100644 index 0000000..3aef052 --- /dev/null +++ b/iso/airootfs/etc/calamares/modules/shellprocess.conf @@ -0,0 +1,3 @@ +--- +script: + - "-/usr/bin/bash /etc/calamares/post-install.sh" diff --git a/iso/airootfs/etc/calamares/modules/users.conf b/iso/airootfs/etc/calamares/modules/users.conf new file mode 100644 index 0000000..8f2d422 --- /dev/null +++ b/iso/airootfs/etc/calamares/modules/users.conf @@ -0,0 +1,40 @@ +--- +defaultGroups: + - name: users + must_exist: true + system: false + - name: lp + must_exist: false + system: true + - name: video + must_exist: false + system: true + - name: network + must_exist: false + system: true + - name: storage + must_exist: false + system: true + - name: wheel + must_exist: false + system: true + - name: audio + must_exist: false + system: true + - name: input + must_exist: false + system: true + +autologinGroup: autologin +doAutologin: false +sudoersGroup: wheel +setRootPassword: false +doReusePassword: true + +passwordRequirements: + minLength: 6 + maxLength: -1 + libpwquality: + - minlen=6 + +allowWeakPasswords: false diff --git a/iso/airootfs/etc/calamares/modules/welcome.conf b/iso/airootfs/etc/calamares/modules/welcome.conf new file mode 100644 index 0000000..33bf7ad --- /dev/null +++ b/iso/airootfs/etc/calamares/modules/welcome.conf @@ -0,0 +1,11 @@ +--- +showSupportUrl: false +showKnownIssuesUrl: false +showReleaseNotesUrl: false + +requirements: + requiredStorage: 20 + requiredRam: 2.0 + checkInternet: true + checkPower: true + internetCheckUrl: "https://archlinux.org" diff --git a/iso/airootfs/etc/calamares/settings.conf b/iso/airootfs/etc/calamares/settings.conf new file mode 100644 index 0000000..499e1cb --- /dev/null +++ b/iso/airootfs/etc/calamares/settings.conf @@ -0,0 +1,36 @@ +--- +modules-search: [local, /usr/lib/calamares/modules] + +sequence: + - show: + - welcome + - locale + - keyboard + - partition + - users + - summary + - exec: + - partition + - mount + - unpackfs + - machineid + - fstab + - locale + - keyboard + - localecfg + - users + - networkcfg + - hwclock + - packages + - bootloader + - shellprocess + - umount + - show: + - finished + +branding: bos +prompt-install: true +dont-chroot: false +oem-setup: false +disable-cancel: false +disable-cancel-during-exec: true diff --git a/iso/packages.x86_64 b/iso/packages.x86_64 new file mode 100644 index 0000000..b7fbf35 --- /dev/null +++ b/iso/packages.x86_64 @@ -0,0 +1,84 @@ +# Base system +base +base-devel +linux +linux-firmware +linux-headers + +# Bootloader + filesystem +grub +efibootmgr +btrfs-progs +dosfstools +mtools + +# Snapshot infrastructure +snapper +snap-pac +grub-btrfs +inotify-tools + +# Wayland / Hyprland +hyprland +xdg-desktop-portal-hyprland +xdg-utils +xdg-user-dirs +polkit +polkit-gnome + +# Audio +pipewire +wireplumber +pipewire-pulse +pipewire-alsa +pipewire-jack + +# Network +networkmanager +network-manager-applet +iw +iwd +bluez +bluez-utils + +# GTK4 runtime +gtk4 +gtk4-layer-shell +librsvg +libpulse + +# Display +wayland +wayland-protocols +wlroots + +# Fonts +noto-fonts +noto-fonts-emoji +ttf-jetbrains-mono + +# Terminal +foot + +# File manager +nautilus + +# Installer +calamares +calamares-qt6 + +# Utilities +sudo +git +curl +wget +unzip +tar +gzip +which +man-db +man-pages +less + +# Dev tools (for bos-settings standalone install) +rustup diff --git a/iso/post-install.sh b/iso/post-install.sh new file mode 100644 index 0000000..e3f3182 --- /dev/null +++ b/iso/post-install.sh @@ -0,0 +1,40 @@ +#!/bin/bash +set -euo pipefail + +# --- Snapper root config --- +snapper -c root create-config / +sed -i 's/TIMELINE_CREATE="yes"/TIMELINE_CREATE="no"/' /etc/snapper/configs/root +sed -i 's/NUMBER_CLEANUP="no"/NUMBER_CLEANUP="yes"/' /etc/snapper/configs/root +sed -i 's/NUMBER_MIN_AGE="[^"]*"/NUMBER_MIN_AGE="1800"/' /etc/snapper/configs/root +sed -i 's/NUMBER_LIMIT="[^"]*"/NUMBER_LIMIT="10"/' /etc/snapper/configs/root +sed -i 's/NUMBER_LIMIT_IMPORTANT="[^"]*"/NUMBER_LIMIT_IMPORTANT="5"/' /etc/snapper/configs/root + +# Allow main user to use snapper without sudo +MAIN_USER=$(getent passwd 1000 | cut -d: -f1) +sed -i "s/ALLOW_USERS=\"\"/ALLOW_USERS=\"$MAIN_USER\"/" /etc/snapper/configs/root + +# --- System services --- +systemctl enable NetworkManager +systemctl enable bluetooth +systemctl enable snapper-cleanup.timer +systemctl enable grub-btrfs.path + +# --- Bakery install --- +if command -v bakery &>/dev/null; then + sudo -u "$MAIN_USER" bakery install bread breadbar breadbox breadcrumbs breadpad bos-settings +fi + +# --- Deploy dotfiles (skip existing files) --- +DOTFILES_SRC="/etc/skel/.config" +DOTFILES_DEST="/home/$MAIN_USER/.config" + +if [[ -d "$DOTFILES_SRC" ]]; then + mkdir -p "$DOTFILES_DEST" + cp -rn "$DOTFILES_SRC/." "$DOTFILES_DEST/" + chown -R "$MAIN_USER:$MAIN_USER" "$DOTFILES_DEST" +fi + +# --- XDG user dirs --- +sudo -u "$MAIN_USER" xdg-user-dirs-update + +echo "BOS post-install complete. Reboot to start your system." diff --git a/iso/profiledef.sh b/iso/profiledef.sh new file mode 100644 index 0000000..8f5cb15 --- /dev/null +++ b/iso/profiledef.sh @@ -0,0 +1,24 @@ +#!/usr/bin/env bash +# shellcheck disable=SC2034 + +iso_name="bos" +iso_label="BOS_$(date +%Y%m)" +iso_publisher="Breadway" +iso_application="Bread Operating System" +iso_version="$(date +%Y.%m.%d)" +install_dir="arch" +buildmodes=('iso') +bootmodes=( + 'bios.syslinux.mbr' + 'bios.syslinux.eltorito' + 'uefi-x64.systemd-boot.esp' + 'uefi-x64.systemd-boot.eltorito' +) +arch="x86_64" +pacman_conf="pacman.conf" +airootfs_image_type="squashfs" +airootfs_image_tool_options=('-comp' 'xz' '-Xbcj' 'x86' '-b' '1M' '-Xdict-size' '1M') +file_permissions=( + ["/etc/shadow"]="0:0:400" + ["/etc/calamares/post-install.sh"]="0:0:755" +) From d5913da277982eaaaee0a6cc4f5e7ee18f91c0a6 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 12 Jun 2026 13:45:00 +0000 Subject: [PATCH 3/7] Fix all issues from code/UX review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ISO structural: - Move post-install.sh → airootfs/etc/calamares/ (it was never in the squashfs) - Create airootfs/etc/skel/.config/ with all dotfiles (deploy path now works) - Add iso/pacman.conf with [breadway] custom repo stub for calamares + bakery - Add Calamares branding component (bos/branding.desc + show.qml) - Add missing unpackfs.conf and mount.conf modules - Add live-session autostart: getty autologin → bash_profile → Hyprland → calamares - Add polkit rule for wheel-group snapper rollback (pkexec path) - Remove wlroots from packages (bundled with Hyprland); add bakery to package list - Fix modules-search path in settings.conf Dotfiles: - Rename dotfiles/hyprland/ → dotfiles/hypr/ (Hyprland reads ~/.config/hypr/) - Fix deprecated shadow options: drop_shadow/shadow_range → shadow { } block bos-settings Rust: - Replace glib::MainContext::channel (removed in glib 0.19) with async_channel - Stream bakery update output line-by-line instead of buffering all at once - Fix zombie processes: per-package update buttons now wait() in a thread - Fix sidebar/stack mismatch at startup: select snapshots row to match default view - Replace deprecated MessageDialog with AlertDialog (GTK 4.10+) throughout - Use pkexec for snapper rollback so polkit handles privilege escalation - Add confirmation dialog before delete snapshot (was missing, rollback had one) - Add refresh button + repopulate after delete in snapshots view - Add "Saved" / "Error: …" status label to every config view save button - Add "Remove" buttons to breadbox contexts and breadcrumbs profiles - Remove hardcoded model string from breadpad defaults - Drop unused state mod; fix config_dir HOME fallback; fix zombie in editor launches https://claude.ai/code/session_01WszGHvCmxgcyTwNSkfLF9P --- bos-settings/Cargo.toml | 1 + bos-settings/src/config/mod.rs | 16 +- bos-settings/src/main.rs | 1 - bos-settings/src/ui/sidebar.rs | 30 ++- bos-settings/src/ui/views/bread.rs | 96 ++++---- bos-settings/src/ui/views/breadbar.rs | 28 ++- bos-settings/src/ui/views/breadbox.rs | 38 +++- bos-settings/src/ui/views/breadcrumbs.rs | 38 +++- bos-settings/src/ui/views/breadpad.rs | 56 ++--- bos-settings/src/ui/views/hyprland.rs | 30 ++- bos-settings/src/ui/views/packages.rs | 132 ++++++----- bos-settings/src/ui/views/snapshots.rs | 209 ++++++++++-------- dotfiles/{hyprland => hypr}/hyprland.conf | 8 +- dotfiles/{hyprland => hypr}/keybinds.conf | 0 .../etc/calamares/branding/bos/branding.desc | 29 +++ .../etc/calamares/branding/bos/show.qml | 43 ++++ iso/airootfs/etc/calamares/modules/mount.conf | 10 + .../etc/calamares/modules/unpackfs.conf | 7 + .../etc/calamares}/post-install.sh | 13 +- iso/airootfs/etc/calamares/settings.conf | 2 +- .../etc/polkit-1/rules.d/10-snapper.rules | 11 + .../etc/skel/.config/bread/breadd.toml | 8 + iso/airootfs/etc/skel/.config/bread/init.lua | 1 + .../etc/skel/.config/breadbox/config.toml | 3 + .../skel/.config/breadcrumbs/breadcrumbs.toml | 3 + .../etc/skel/.config/hypr/hyprland.conf | 56 +++++ .../etc/skel/.config/hypr/keybinds.conf | 58 +++++ .../getty@tty1.service.d/autologin.conf | 3 + iso/airootfs/root/.bash_profile | 4 + iso/airootfs/root/.config/hypr/hyprland.conf | 28 +++ iso/packages.x86_64 | 8 +- iso/pacman.conf | 38 ++++ 32 files changed, 720 insertions(+), 288 deletions(-) rename dotfiles/{hyprland => hypr}/hyprland.conf (90%) rename dotfiles/{hyprland => hypr}/keybinds.conf (100%) create mode 100644 iso/airootfs/etc/calamares/branding/bos/branding.desc create mode 100644 iso/airootfs/etc/calamares/branding/bos/show.qml create mode 100644 iso/airootfs/etc/calamares/modules/mount.conf create mode 100644 iso/airootfs/etc/calamares/modules/unpackfs.conf rename iso/{ => airootfs/etc/calamares}/post-install.sh (77%) create mode 100644 iso/airootfs/etc/polkit-1/rules.d/10-snapper.rules create mode 100644 iso/airootfs/etc/skel/.config/bread/breadd.toml create mode 100644 iso/airootfs/etc/skel/.config/bread/init.lua create mode 100644 iso/airootfs/etc/skel/.config/breadbox/config.toml create mode 100644 iso/airootfs/etc/skel/.config/breadcrumbs/breadcrumbs.toml create mode 100644 iso/airootfs/etc/skel/.config/hypr/hyprland.conf create mode 100644 iso/airootfs/etc/skel/.config/hypr/keybinds.conf create mode 100644 iso/airootfs/etc/systemd/system/getty@tty1.service.d/autologin.conf create mode 100644 iso/airootfs/root/.bash_profile create mode 100644 iso/airootfs/root/.config/hypr/hyprland.conf create mode 100644 iso/pacman.conf diff --git a/bos-settings/Cargo.toml b/bos-settings/Cargo.toml index 24dabe0..d906354 100644 --- a/bos-settings/Cargo.toml +++ b/bos-settings/Cargo.toml @@ -9,3 +9,4 @@ glib = "0.20" serde = { version = "1", features = ["derive"] } serde_json = "1" toml = "0.8" +async-channel = "2" diff --git a/bos-settings/src/config/mod.rs b/bos-settings/src/config/mod.rs index 08b005b..da3b8eb 100644 --- a/bos-settings/src/config/mod.rs +++ b/bos-settings/src/config/mod.rs @@ -1,5 +1,5 @@ use std::error::Error; -use std::path::Path; +use std::path::{Path, PathBuf}; pub fn load serde::Deserialize<'de>>(path: &Path) -> Result> { let text = std::fs::read_to_string(path)?; @@ -14,11 +14,11 @@ pub fn save(path: &Path, val: &T) -> Result<(), Box std::path::PathBuf { - dirs_path() -} - -fn dirs_path() -> std::path::PathBuf { - let home = std::env::var("HOME").unwrap_or_else(|_| "/root".to_string()); - std::path::PathBuf::from(home).join(".config") +pub fn config_dir() -> PathBuf { + let home = std::env::var("HOME").unwrap_or_else(|_| { + std::env::var("XDG_CONFIG_HOME") + .map(|p| PathBuf::from(p).parent().unwrap_or(Path::new("/")).to_string_lossy().to_string()) + .unwrap_or_else(|_| "/home/user".to_string()) + }); + PathBuf::from(home).join(".config") } diff --git a/bos-settings/src/main.rs b/bos-settings/src/main.rs index c93a8d2..fc13dc2 100644 --- a/bos-settings/src/main.rs +++ b/bos-settings/src/main.rs @@ -1,5 +1,4 @@ mod config; -mod state; mod theme; mod ui; diff --git a/bos-settings/src/ui/sidebar.rs b/bos-settings/src/ui/sidebar.rs index daafb0b..4395591 100644 --- a/bos-settings/src/ui/sidebar.rs +++ b/bos-settings/src/ui/sidebar.rs @@ -1,5 +1,5 @@ use gtk4::prelude::*; -use gtk4::{Box as GBox, Label, ListBox, ListBoxRow, Orientation, Separator}; +use gtk4::{Box as GBox, Label, ListBox, ListBoxRow, Orientation}; pub struct SidebarItem { pub id: &'static str, @@ -7,17 +7,17 @@ pub struct SidebarItem { } pub const APPS_ITEMS: &[SidebarItem] = &[ - SidebarItem { id: "bread", label: "bread" }, - SidebarItem { id: "breadbar", label: "breadbar" }, - SidebarItem { id: "breadbox", label: "breadbox" }, + SidebarItem { id: "bread", label: "bread" }, + SidebarItem { id: "breadbar", label: "breadbar" }, + SidebarItem { id: "breadbox", label: "breadbox" }, SidebarItem { id: "breadcrumbs", label: "breadcrumbs" }, - SidebarItem { id: "breadpad", label: "breadpad" }, + SidebarItem { id: "breadpad", label: "breadpad" }, ]; pub const SYSTEM_ITEMS: &[SidebarItem] = &[ SidebarItem { id: "snapshots", label: "Snapshots" }, - SidebarItem { id: "packages", label: "Packages" }, - SidebarItem { id: "hyprland", label: "Hyprland" }, + SidebarItem { id: "packages", label: "Packages" }, + SidebarItem { id: "hyprland", label: "Hyprland" }, ]; pub fn build() -> (GBox, ListBox) { @@ -32,9 +32,17 @@ pub fn build() -> (GBox, ListBox) { append_section(&list, "Apps", APPS_ITEMS); append_section(&list, "System", SYSTEM_ITEMS); - // Select first item by default - if let Some(first) = list.row_at_index(1) { - list.select_row(Some(&first)); + // Select the snapshots row so it matches the default stack page + let mut i = 0; + loop { + match list.row_at_index(i) { + None => break, + Some(row) if row.widget_name() == "snapshots" => { + list.select_row(Some(&row)); + break; + } + _ => i += 1, + } } vbox.append(&list); @@ -42,7 +50,6 @@ pub fn build() -> (GBox, ListBox) { } fn append_section(list: &ListBox, title: &str, items: &[SidebarItem]) { - // Section header (non-selectable) let header_row = ListBoxRow::new(); header_row.set_selectable(false); header_row.set_activatable(false); @@ -55,7 +62,6 @@ fn append_section(list: &ListBox, title: &str, items: &[SidebarItem]) { for item in items { let row = ListBoxRow::new(); row.set_widget_name(item.id); - let lbl = Label::new(Some(item.label)); lbl.set_xalign(0.0); lbl.set_margin_top(2); diff --git a/bos-settings/src/ui/views/bread.rs b/bos-settings/src/ui/views/bread.rs index ca4b080..27d0596 100644 --- a/bos-settings/src/ui/views/bread.rs +++ b/bos-settings/src/ui/views/bread.rs @@ -14,34 +14,22 @@ pub struct BreadConfig { pub adapters: AdaptersConfig, } -fn default_log_level() -> String { - "info".to_string() -} +fn default_log_level() -> String { "info".to_string() } #[derive(Deserialize, Serialize, Clone, Default)] pub struct AdaptersConfig { - #[serde(default = "default_true")] - pub keyboard: bool, - #[serde(default = "default_true")] - pub mouse: bool, - #[serde(default = "default_true")] - pub touchpad: bool, - #[serde(default = "default_true")] - pub bluetooth: bool, - #[serde(default = "default_true")] - pub gamepad: bool, + #[serde(default = "default_true")] pub keyboard: bool, + #[serde(default = "default_true")] pub mouse: bool, + #[serde(default = "default_true")] pub touchpad: bool, + #[serde(default = "default_true")] pub bluetooth: bool, + #[serde(default = "default_true")] pub gamepad: bool, } -fn default_true() -> bool { - true -} +fn default_true() -> bool { true } impl Default for BreadConfig { fn default() -> Self { - Self { - log_level: default_log_level(), - adapters: AdaptersConfig::default(), - } + Self { log_level: default_log_level(), adapters: AdaptersConfig::default() } } } @@ -49,7 +37,12 @@ fn config_path() -> std::path::PathBuf { config::config_dir().join("bread/breadd.toml") } -fn adapter_row(label: &str, active: bool, cfg: Rc>, field: &'static str) -> GBox { +fn adapter_row( + label: &str, + active: bool, + cfg: Rc>, + field: &'static str, +) -> GBox { let row = GBox::new(Orientation::Horizontal, 16); let lbl = Label::new(Some(label)); lbl.set_hexpand(true); @@ -60,11 +53,11 @@ fn adapter_row(label: &str, active: bool, cfg: Rc>, field: let val = s.is_active(); let mut c = cfg.borrow_mut(); match field { - "keyboard" => c.adapters.keyboard = val, - "mouse" => c.adapters.mouse = val, - "touchpad" => c.adapters.touchpad = val, - "bluetooth" => c.adapters.bluetooth = val, - "gamepad" => c.adapters.gamepad = val, + "keyboard" => c.adapters.keyboard = val, + "mouse" => c.adapters.mouse = val, + "touchpad" => c.adapters.touchpad = val, + "bluetooth" => c.adapters.bluetooth = val, + "gamepad" => c.adapters.gamepad = val, _ => {} } }); @@ -94,15 +87,10 @@ pub fn build() -> GBox { lbl.set_xalign(0.0); let levels = StringList::new(&["error", "warn", "info", "debug", "trace"]); let dropdown = DropDown::new(Some(levels), gtk4::Expression::NONE); - let current_pos = match cfg.borrow().log_level.as_str() { - "error" => 0u32, - "warn" => 1, - "info" => 2, - "debug" => 3, - "trace" => 4, - _ => 2, + let pos = match cfg.borrow().log_level.as_str() { + "error" => 0u32, "warn" => 1, "info" => 2, "debug" => 3, "trace" => 4, _ => 2, }; - dropdown.set_selected(current_pos); + dropdown.set_selected(pos); { let cfg = cfg.clone(); dropdown.connect_selected_notify(move |dd| { @@ -116,7 +104,6 @@ pub fn build() -> GBox { row.append(&dropdown); vbox.append(&row); - // Adapter toggles let adapter_label = Label::new(Some("Adapters")); adapter_label.set_xalign(0.0); adapter_label.set_margin_top(8); @@ -125,25 +112,44 @@ pub fn build() -> GBox { let (kbd, mouse, touchpad, bluetooth, gamepad) = { let c = cfg.borrow(); - (c.adapters.keyboard, c.adapters.mouse, c.adapters.touchpad, c.adapters.bluetooth, c.adapters.gamepad) + (c.adapters.keyboard, c.adapters.mouse, c.adapters.touchpad, + c.adapters.bluetooth, c.adapters.gamepad) }; - - vbox.append(&adapter_row("Keyboard", kbd, cfg.clone(), "keyboard")); - vbox.append(&adapter_row("Mouse", mouse, cfg.clone(), "mouse")); - vbox.append(&adapter_row("Touchpad", touchpad, cfg.clone(), "touchpad")); + vbox.append(&adapter_row("Keyboard", kbd, cfg.clone(), "keyboard")); + vbox.append(&adapter_row("Mouse", mouse, cfg.clone(), "mouse")); + vbox.append(&adapter_row("Touchpad", touchpad, cfg.clone(), "touchpad")); vbox.append(&adapter_row("Bluetooth", bluetooth, cfg.clone(), "bluetooth")); - vbox.append(&adapter_row("Gamepad", gamepad, cfg.clone(), "gamepad")); + vbox.append(&adapter_row("Gamepad", gamepad, cfg.clone(), "gamepad")); + + let btn_row = GBox::new(Orientation::Horizontal, 12); + btn_row.set_margin_top(16); let save_btn = Button::with_label("Save"); - save_btn.set_margin_top(16); - save_btn.set_halign(gtk4::Align::Start); + let status_lbl = Label::new(None); + status_lbl.add_css_class("dim-label"); + { let cfg = cfg.clone(); + let path = path.clone(); + let status_lbl = status_lbl.clone(); save_btn.connect_clicked(move |_| { - let _ = config::save(&path, &*cfg.borrow()); + match config::save(&path, &*cfg.borrow()) { + Ok(()) => { + status_lbl.set_text("Saved"); + let lbl = status_lbl.clone(); + glib::timeout_add_seconds_local(3, move || { + lbl.set_text(""); + glib::ControlFlow::Break + }); + } + Err(e) => status_lbl.set_text(&format!("Error: {e}")), + } }); } - vbox.append(&save_btn); + + btn_row.append(&save_btn); + btn_row.append(&status_lbl); + vbox.append(&btn_row); vbox } diff --git a/bos-settings/src/ui/views/breadbar.rs b/bos-settings/src/ui/views/breadbar.rs index 2314111..9f1ceb7 100644 --- a/bos-settings/src/ui/views/breadbar.rs +++ b/bos-settings/src/ui/views/breadbar.rs @@ -3,7 +3,7 @@ use gtk4::{Box as GBox, Button, Label, Orientation, ScrolledWindow, TextView}; use std::path::PathBuf; fn css_path() -> PathBuf { - let home = std::env::var("HOME").unwrap_or_else(|_| "/root".to_string()); + let home = std::env::var("HOME").unwrap_or_else(|_| "/home/user".to_string()); PathBuf::from(home).join(".config/breadbar/style.css") } @@ -38,21 +38,39 @@ pub fn build() -> GBox { scroll.set_child(Some(&text_view)); vbox.append(&scroll); + let btn_row = GBox::new(Orientation::Horizontal, 12); + btn_row.set_margin_top(12); + let save_btn = Button::with_label("Save"); - save_btn.set_margin_top(12); - save_btn.set_halign(gtk4::Align::Start); + let status_lbl = Label::new(None); + status_lbl.add_css_class("dim-label"); + { let path = path.clone(); + let status_lbl = status_lbl.clone(); save_btn.connect_clicked(move |_| { let (start, end) = buf.bounds(); let text = buf.text(&start, &end, false); if let Some(parent) = path.parent() { let _ = std::fs::create_dir_all(parent); } - let _ = std::fs::write(&path, text.as_str()); + match std::fs::write(&path, text.as_str()) { + Ok(()) => { + status_lbl.set_text("Saved"); + let lbl = status_lbl.clone(); + glib::timeout_add_seconds_local(3, move || { + lbl.set_text(""); + glib::ControlFlow::Break + }); + } + Err(e) => status_lbl.set_text(&format!("Error: {e}")), + } }); } - vbox.append(&save_btn); + + btn_row.append(&save_btn); + btn_row.append(&status_lbl); + vbox.append(&btn_row); vbox } diff --git a/bos-settings/src/ui/views/breadbox.rs b/bos-settings/src/ui/views/breadbox.rs index 715ce71..36f88ac 100644 --- a/bos-settings/src/ui/views/breadbox.rs +++ b/bos-settings/src/ui/views/breadbox.rs @@ -29,6 +29,8 @@ fn rebuild_list(list: &ListBox, cfg: &Rc>) { } for (i, ctx) in cfg.borrow().context.iter().enumerate() { let row = ListBoxRow::new(); + row.set_selectable(false); + let hbox = GBox::new(Orientation::Horizontal, 8); hbox.set_margin_top(6); hbox.set_margin_bottom(6); @@ -37,13 +39,17 @@ fn rebuild_list(list: &ListBox, cfg: &Rc>) { let name_entry = Entry::new(); name_entry.set_text(&ctx.name); - name_entry.set_width_chars(16); + name_entry.set_width_chars(14); + name_entry.set_placeholder_text(Some("name")); let apps_entry = Entry::new(); apps_entry.set_text(&ctx.apps.join(", ")); apps_entry.set_hexpand(true); apps_entry.set_placeholder_text(Some("app1, app2, ...")); + let remove_btn = Button::with_label("Remove"); + remove_btn.add_css_class("destructive-action"); + { let cfg = cfg.clone(); name_entry.connect_changed(move |e| { @@ -56,8 +62,7 @@ fn rebuild_list(list: &ListBox, cfg: &Rc>) { let cfg = cfg.clone(); apps_entry.connect_changed(move |e| { if let Some(c) = cfg.borrow_mut().context.get_mut(i) { - c.apps = e - .text() + c.apps = e.text() .split(',') .map(|s| s.trim().to_string()) .filter(|s| !s.is_empty()) @@ -65,9 +70,18 @@ fn rebuild_list(list: &ListBox, cfg: &Rc>) { } }); } + { + let cfg = cfg.clone(); + let list = list.clone(); + remove_btn.connect_clicked(move |_| { + cfg.borrow_mut().context.remove(i); + rebuild_list(&list, &cfg); + }); + } hbox.append(&name_entry); hbox.append(&apps_entry); + hbox.append(&remove_btn); row.set_child(Some(&hbox)); list.append(&row); } @@ -93,7 +107,6 @@ pub fn build() -> GBox { let list = ListBox::new(); list.set_selection_mode(gtk4::SelectionMode::None); - rebuild_list(&list, &cfg); let scroll = ScrolledWindow::new(); @@ -118,16 +131,31 @@ pub fn build() -> GBox { } let save_btn = Button::with_label("Save"); + let status_lbl = Label::new(None); + status_lbl.add_css_class("dim-label"); + { let cfg = cfg.clone(); let path = path.clone(); + let status_lbl = status_lbl.clone(); save_btn.connect_clicked(move |_| { - let _ = config::save(&path, &*cfg.borrow()); + match config::save(&path, &*cfg.borrow()) { + Ok(()) => { + status_lbl.set_text("Saved"); + let lbl = status_lbl.clone(); + glib::timeout_add_seconds_local(3, move || { + lbl.set_text(""); + glib::ControlFlow::Break + }); + } + Err(e) => status_lbl.set_text(&format!("Error: {e}")), + } }); } btn_row.append(&add_btn); btn_row.append(&save_btn); + btn_row.append(&status_lbl); vbox.append(&btn_row); vbox diff --git a/bos-settings/src/ui/views/breadcrumbs.rs b/bos-settings/src/ui/views/breadcrumbs.rs index 4062645..f165f43 100644 --- a/bos-settings/src/ui/views/breadcrumbs.rs +++ b/bos-settings/src/ui/views/breadcrumbs.rs @@ -29,6 +29,8 @@ fn rebuild_list(list: &ListBox, cfg: &Rc>) { } for (i, profile) in cfg.borrow().profile.iter().enumerate() { let row = ListBoxRow::new(); + row.set_selectable(false); + let hbox = GBox::new(Orientation::Horizontal, 8); hbox.set_margin_top(6); hbox.set_margin_bottom(6); @@ -37,13 +39,17 @@ fn rebuild_list(list: &ListBox, cfg: &Rc>) { let name_entry = Entry::new(); name_entry.set_text(&profile.name); - name_entry.set_width_chars(16); + name_entry.set_width_chars(14); + name_entry.set_placeholder_text(Some("name")); let ssids_entry = Entry::new(); ssids_entry.set_text(&profile.ssids.join(", ")); ssids_entry.set_hexpand(true); ssids_entry.set_placeholder_text(Some("SSID1, SSID2, ...")); + let remove_btn = Button::with_label("Remove"); + remove_btn.add_css_class("destructive-action"); + { let cfg = cfg.clone(); name_entry.connect_changed(move |e| { @@ -56,8 +62,7 @@ fn rebuild_list(list: &ListBox, cfg: &Rc>) { let cfg = cfg.clone(); ssids_entry.connect_changed(move |e| { if let Some(p) = cfg.borrow_mut().profile.get_mut(i) { - p.ssids = e - .text() + p.ssids = e.text() .split(',') .map(|s| s.trim().to_string()) .filter(|s| !s.is_empty()) @@ -65,9 +70,18 @@ fn rebuild_list(list: &ListBox, cfg: &Rc>) { } }); } + { + let cfg = cfg.clone(); + let list = list.clone(); + remove_btn.connect_clicked(move |_| { + cfg.borrow_mut().profile.remove(i); + rebuild_list(&list, &cfg); + }); + } hbox.append(&name_entry); hbox.append(&ssids_entry); + hbox.append(&remove_btn); row.set_child(Some(&hbox)); list.append(&row); } @@ -93,7 +107,6 @@ pub fn build() -> GBox { let list = ListBox::new(); list.set_selection_mode(gtk4::SelectionMode::None); - rebuild_list(&list, &cfg); let scroll = ScrolledWindow::new(); @@ -118,16 +131,31 @@ pub fn build() -> GBox { } let save_btn = Button::with_label("Save"); + let status_lbl = Label::new(None); + status_lbl.add_css_class("dim-label"); + { let cfg = cfg.clone(); let path = path.clone(); + let status_lbl = status_lbl.clone(); save_btn.connect_clicked(move |_| { - let _ = config::save(&path, &*cfg.borrow()); + match config::save(&path, &*cfg.borrow()) { + Ok(()) => { + status_lbl.set_text("Saved"); + let lbl = status_lbl.clone(); + glib::timeout_add_seconds_local(3, move || { + lbl.set_text(""); + glib::ControlFlow::Break + }); + } + Err(e) => status_lbl.set_text(&format!("Error: {e}")), + } }); } btn_row.append(&add_btn); btn_row.append(&save_btn); + btn_row.append(&status_lbl); vbox.append(&btn_row); vbox diff --git a/bos-settings/src/ui/views/breadpad.rs b/bos-settings/src/ui/views/breadpad.rs index 9c553f4..6e24346 100644 --- a/bos-settings/src/ui/views/breadpad.rs +++ b/bos-settings/src/ui/views/breadpad.rs @@ -8,7 +8,7 @@ use crate::config; #[derive(Deserialize, Serialize, Clone)] pub struct BreadpadConfig { - #[serde(default = "default_model")] + #[serde(default)] pub model: String, #[serde(default = "default_true")] pub reminders: bool, @@ -16,21 +16,11 @@ pub struct BreadpadConfig { pub calendar: bool, } -fn default_model() -> String { - "claude-sonnet-4-6".to_string() -} - -fn default_true() -> bool { - true -} +fn default_true() -> bool { true } impl Default for BreadpadConfig { fn default() -> Self { - Self { - model: default_model(), - reminders: true, - calendar: true, - } + Self { model: String::new(), reminders: true, calendar: true } } } @@ -58,6 +48,7 @@ pub fn build() -> GBox { lbl.set_xalign(0.0); let model_entry = Entry::new(); model_entry.set_text(&cfg.borrow().model); + model_entry.set_placeholder_text(Some("e.g. claude-sonnet-4-6")); { let cfg = cfg.clone(); model_entry.connect_changed(move |e| { @@ -68,7 +59,7 @@ pub fn build() -> GBox { row.append(&model_entry); vbox.append(&row); - // Reminders toggle + // Reminders let row = GBox::new(Orientation::Horizontal, 16); let lbl = Label::new(Some("Reminders")); lbl.set_hexpand(true); @@ -77,15 +68,13 @@ pub fn build() -> GBox { sw.set_active(cfg.borrow().reminders); { let cfg = cfg.clone(); - sw.connect_active_notify(move |s| { - cfg.borrow_mut().reminders = s.is_active(); - }); + sw.connect_active_notify(move |s| { cfg.borrow_mut().reminders = s.is_active(); }); } row.append(&lbl); row.append(&sw); vbox.append(&row); - // Calendar toggle + // Calendar let row = GBox::new(Orientation::Horizontal, 16); let lbl = Label::new(Some("Calendar integration")); lbl.set_hexpand(true); @@ -94,25 +83,40 @@ pub fn build() -> GBox { sw.set_active(cfg.borrow().calendar); { let cfg = cfg.clone(); - sw.connect_active_notify(move |s| { - cfg.borrow_mut().calendar = s.is_active(); - }); + sw.connect_active_notify(move |s| { cfg.borrow_mut().calendar = s.is_active(); }); } row.append(&lbl); row.append(&sw); vbox.append(&row); + let btn_row = GBox::new(Orientation::Horizontal, 12); + btn_row.set_margin_top(16); + let save_btn = Button::with_label("Save"); - save_btn.set_margin_top(16); - save_btn.set_halign(gtk4::Align::Start); + let status_lbl = Label::new(None); + status_lbl.add_css_class("dim-label"); + { let cfg = cfg.clone(); - let path = path.clone(); + let status_lbl = status_lbl.clone(); save_btn.connect_clicked(move |_| { - let _ = config::save(&path, &*cfg.borrow()); + match config::save(&path, &*cfg.borrow()) { + Ok(()) => { + status_lbl.set_text("Saved"); + let lbl = status_lbl.clone(); + glib::timeout_add_seconds_local(3, move || { + lbl.set_text(""); + glib::ControlFlow::Break + }); + } + Err(e) => status_lbl.set_text(&format!("Error: {e}")), + } }); } - vbox.append(&save_btn); + + btn_row.append(&save_btn); + btn_row.append(&status_lbl); + vbox.append(&btn_row); vbox } diff --git a/bos-settings/src/ui/views/hyprland.rs b/bos-settings/src/ui/views/hyprland.rs index cb43b30..0ed704d 100644 --- a/bos-settings/src/ui/views/hyprland.rs +++ b/bos-settings/src/ui/views/hyprland.rs @@ -17,14 +17,14 @@ fn get_monitors() -> Vec { let w = m.get("width")?.as_u64()?; let h = m.get("height")?.as_u64()?; let refresh = m.get("refreshRate")?.as_f64()?; - Some(format!("{name} {w}×{h} @ {refresh:.0}Hz")) + Some(format!("{name} {w}x{h} @ {refresh:.0}Hz")) }) .collect() } -fn config_path() -> std::path::PathBuf { - let home = std::env::var("HOME").unwrap_or_else(|_| "/root".to_string()); - std::path::PathBuf::from(home).join(".config/hypr/hyprland.conf") +fn hypr_path(name: &str) -> std::path::PathBuf { + let home = std::env::var("HOME").unwrap_or_else(|_| "/home/user".to_string()); + std::path::PathBuf::from(home).join(".config/hypr").join(name) } pub fn build() -> GBox { @@ -36,7 +36,6 @@ pub fn build() -> GBox { title.set_xalign(0.0); vbox.append(&title); - // Monitors section let monitors_lbl = Label::new(Some("Connected monitors")); monitors_lbl.set_xalign(0.0); monitors_lbl.set_margin_top(8); @@ -52,38 +51,35 @@ pub fn build() -> GBox { for mon in &monitors { let lbl = Label::new(Some(mon)); lbl.set_xalign(0.0); - lbl.add_css_class("monospace"); + lbl.set_monospace(true); vbox.append(&lbl); } } - // Open config button let open_btn = Button::with_label("Open hyprland.conf in editor"); open_btn.set_margin_top(16); open_btn.set_halign(gtk4::Align::Start); { - let path = config_path(); + let conf_path = hypr_path("hyprland.conf"); open_btn.connect_clicked(move |_| { let editor = std::env::var("EDITOR").unwrap_or_else(|_| "foot".to_string()); - let _ = Command::new(&editor) - .arg(path.to_str().unwrap_or("")) - .spawn(); + if let Ok(mut child) = Command::new(&editor).arg(&conf_path).spawn() { + std::thread::spawn(move || { let _ = child.wait(); }); + } }); } vbox.append(&open_btn); - // Open keybinds button let keybinds_btn = Button::with_label("Open keybinds.conf in editor"); keybinds_btn.set_margin_top(8); keybinds_btn.set_halign(gtk4::Align::Start); { - let home = std::env::var("HOME").unwrap_or_else(|_| "/root".to_string()); - let kb_path = std::path::PathBuf::from(home).join(".config/hypr/keybinds.conf"); + let kb_path = hypr_path("keybinds.conf"); keybinds_btn.connect_clicked(move |_| { let editor = std::env::var("EDITOR").unwrap_or_else(|_| "foot".to_string()); - let _ = Command::new(&editor) - .arg(kb_path.to_str().unwrap_or("")) - .spawn(); + if let Ok(mut child) = Command::new(&editor).arg(&kb_path).spawn() { + std::thread::spawn(move || { let _ = child.wait(); }); + } }); } vbox.append(&keybinds_btn); diff --git a/bos-settings/src/ui/views/packages.rs b/bos-settings/src/ui/views/packages.rs index 448ec53..6b3aedc 100644 --- a/bos-settings/src/ui/views/packages.rs +++ b/bos-settings/src/ui/views/packages.rs @@ -1,31 +1,20 @@ +use async_channel; use gtk4::prelude::*; use gtk4::{ Box as GBox, Button, Label, ListBox, ListBoxRow, Orientation, ScrolledWindow, TextView, }; -use serde::Deserialize; use std::collections::HashMap; -use std::process::Command; - -#[derive(Deserialize, Default)] -struct InstalledPackages { - #[serde(flatten)] - packages: HashMap, -} - -#[derive(Deserialize)] -struct PackageInfo { - version: String, -} +use std::io::{BufRead, BufReader}; +use std::process::{Command, Stdio}; fn read_installed() -> HashMap { - let home = std::env::var("HOME").unwrap_or_else(|_| "/root".to_string()); + let home = std::env::var("HOME").unwrap_or_else(|_| "/home/user".to_string()); let path = std::path::Path::new(&home) .join(".local/state/bakery/installed.json"); let Ok(text) = std::fs::read_to_string(&path) else { return HashMap::new(); }; - let Ok(parsed) = serde_json::from_str::>(&text) else { return HashMap::new(); }; @@ -35,15 +24,57 @@ fn read_installed() -> HashMap { .filter_map(|(name, val)| { let version = val .get("version") - .or_else(|| val.as_str().map(|_| &val)) .and_then(|v| v.as_str()) - .map(|s| s.to_string()) - .unwrap_or_else(|| "unknown".to_string()); + .unwrap_or("unknown") + .to_string(); Some((name, version)) }) .collect() } +fn stream_command(args: &[&str], log_buf: gtk4::TextBuffer) { + let (sender, receiver) = async_channel::bounded::(256); + let args: Vec = args.iter().map(|s| s.to_string()).collect(); + + std::thread::spawn(move || { + let mut child = match Command::new(&args[0]) + .args(&args[1..]) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + { + Ok(c) => c, + Err(e) => { + let _ = sender.send_blocking(format!("Error: {e}")); + return; + } + }; + + // Merge stderr into the channel too + let stdout = child.stdout.take().unwrap(); + let stderr = child.stderr.take().unwrap(); + + let tx2 = sender.clone(); + std::thread::spawn(move || { + for line in BufReader::new(stderr).lines().flatten() { + let _ = tx2.send_blocking(line); + } + }); + + for line in BufReader::new(stdout).lines().flatten() { + let _ = sender.send_blocking(line); + } + let _ = child.wait(); + }); + + glib::spawn_future_local(async move { + while let Ok(line) = receiver.recv().await { + let mut end = log_buf.end_iter(); + log_buf.insert(&mut end, &format!("{line}\n")); + } + }); +} + pub fn build() -> GBox { let vbox = GBox::new(Orientation::Vertical, 0); vbox.add_css_class("view-content"); @@ -64,7 +95,10 @@ pub fn build() -> GBox { let packages = read_installed(); if packages.is_empty() { let row = ListBoxRow::new(); - let lbl = Label::new(Some("No bakery packages found (~/.local/state/bakery/installed.json)")); + row.set_selectable(false); + let lbl = Label::new(Some( + "No bakery packages found (~/.local/state/bakery/installed.json)", + )); lbl.set_margin_top(8); lbl.set_margin_bottom(8); lbl.set_margin_start(8); @@ -73,8 +107,10 @@ pub fn build() -> GBox { } else { let mut names: Vec<_> = packages.iter().collect(); names.sort_by_key(|(k, _)| k.as_str()); + for (name, version) in names { let row = ListBoxRow::new(); + row.set_selectable(false); let hbox = GBox::new(Orientation::Horizontal, 16); hbox.set_margin_top(6); hbox.set_margin_bottom(6); @@ -88,12 +124,16 @@ pub fn build() -> GBox { let ver_lbl = Label::new(Some(version)); ver_lbl.set_xalign(1.0); - let update_btn = Button::with_label("Update"); + // Spawn a thread to reap the child process — no zombies let pkg_name = name.clone(); + let update_btn = Button::with_label("Update"); update_btn.connect_clicked(move |_| { - let _ = Command::new("bakery") - .args(["update", &pkg_name]) - .spawn(); + match Command::new("bakery").args(["update", &pkg_name]).spawn() { + Ok(mut child) => { + std::thread::spawn(move || { let _ = child.wait(); }); + } + Err(e) => eprintln!("bakery update failed: {e}"), + } }); hbox.append(&name_lbl); @@ -109,56 +149,32 @@ pub fn build() -> GBox { scroll.set_child(Some(&list)); vbox.append(&scroll); + let log_buf = gtk4::TextBuffer::new(None); + let log_view = TextView::with_buffer(&log_buf); + log_view.set_editable(false); + log_view.set_monospace(true); + log_view.set_height_request(140); + log_view.set_margin_top(8); + let btn_row = GBox::new(Orientation::Horizontal, 8); btn_row.set_margin_top(12); let check_btn = Button::with_label("Check for updates"); let update_all_btn = Button::with_label("Update all"); - let log_buf = gtk4::TextBuffer::new(None); - let log_view = TextView::with_buffer(&log_buf); - log_view.set_editable(false); - log_view.set_height_request(120); - log_view.set_margin_top(8); - { let log_buf = log_buf.clone(); check_btn.connect_clicked(move |_| { - log_buf.set_text("Checking for updates...\n"); - match Command::new("bakery").args(["list"]).output() { - Ok(out) => { - let text = String::from_utf8_lossy(&out.stdout); - log_buf.set_text(&format!("{text}\n")); - } - Err(e) => { - log_buf.set_text(&format!("Error: {e}\n")); - } - } + log_buf.set_text(""); + stream_command(&["bakery", "list"], log_buf.clone()); }); } { let log_buf = log_buf.clone(); update_all_btn.connect_clicked(move |_| { - log_buf.set_text("Running bakery update --all...\n"); - let (sender, receiver) = glib::MainContext::channel(glib::Priority::DEFAULT); - std::thread::spawn(move || { - let result = Command::new("bakery") - .args(["update", "--all"]) - .output(); - match result { - Ok(out) => { - let _ = sender.send(String::from_utf8_lossy(&out.stdout).to_string()); - } - Err(e) => { - let _ = sender.send(format!("Error: {e}\n")); - } - } - }); - receiver.attach(None, move |msg| { - log_buf.set_text(&msg); - glib::ControlFlow::Break - }); + log_buf.set_text(""); + stream_command(&["bakery", "update", "--all"], log_buf.clone()); }); } diff --git a/bos-settings/src/ui/views/snapshots.rs b/bos-settings/src/ui/views/snapshots.rs index da34141..d3ac8b7 100644 --- a/bos-settings/src/ui/views/snapshots.rs +++ b/bos-settings/src/ui/views/snapshots.rs @@ -1,9 +1,10 @@ use gtk4::prelude::*; use gtk4::{ - Box as GBox, Button, Label, ListBox, ListBoxRow, MessageDialog, Orientation, ScrolledWindow, + AlertDialog, Box as GBox, Button, Label, ListBox, ListBoxRow, Orientation, ScrolledWindow, }; use std::process::Command; +#[derive(Clone)] struct SnapshotRow { number: String, date: String, @@ -22,47 +23,60 @@ fn list_snapshots() -> Vec { text.lines() .skip(2) // header + separator .filter_map(|line| { - let cols: Vec<&str> = line.splitn(3, '|').collect(); - if cols.len() == 3 { - Some(SnapshotRow { - number: cols[0].trim().to_string(), - date: cols[1].trim().to_string(), - description: cols[2].trim().to_string(), - }) - } else { - None - } + let mut cols = line.splitn(3, '|'); + Some(SnapshotRow { + number: cols.next()?.trim().to_string(), + date: cols.next()?.trim().to_string(), + description: cols.next()?.trim().to_string(), + }) }) .collect() } -fn confirm_rollback(number: &str) { - let number = number.to_string(); - let dialog = MessageDialog::new( - None::<>k4::Window>, - gtk4::DialogFlags::MODAL, - gtk4::MessageType::Question, - gtk4::ButtonsType::OkCancel, - &format!("Roll back to snapshot #{number}?\n\nReboot required to apply."), - ); - dialog.connect_response(move |d, resp| { - if resp == gtk4::ResponseType::Ok { - let _ = Command::new("snapper") - .args(["rollback", &number]) - .status(); - let info = MessageDialog::new( - None::<>k4::Window>, - gtk4::DialogFlags::MODAL, - gtk4::MessageType::Info, - gtk4::ButtonsType::Ok, - "Rollback queued. Please reboot to apply.", - ); - info.connect_response(|d, _| d.destroy()); - info.present(); - } - d.destroy(); - }); - dialog.present(); +fn populate_list(list: &ListBox) { + while let Some(child) = list.first_child() { + list.remove(&child); + } + let snapshots = list_snapshots(); + if snapshots.is_empty() { + let row = ListBoxRow::new(); + row.set_selectable(false); + let lbl = Label::new(Some("No snapshots found (snapper may not be configured yet)")); + lbl.set_margin_top(8); + lbl.set_margin_bottom(8); + lbl.set_margin_start(8); + row.set_child(Some(&lbl)); + list.append(&row); + return; + } + for snap in &snapshots { + let row = ListBoxRow::new(); + row.set_widget_name(&snap.number); + + let hbox = GBox::new(Orientation::Horizontal, 16); + hbox.set_margin_top(6); + hbox.set_margin_bottom(6); + hbox.set_margin_start(8); + hbox.set_margin_end(8); + + let num_lbl = Label::new(Some(&snap.number)); + num_lbl.set_width_chars(4); + num_lbl.set_xalign(0.0); + + let date_lbl = Label::new(Some(&snap.date)); + date_lbl.set_width_chars(22); + date_lbl.set_xalign(0.0); + + let desc_lbl = Label::new(Some(&snap.description)); + desc_lbl.set_hexpand(true); + desc_lbl.set_xalign(0.0); + + hbox.append(&num_lbl); + hbox.append(&date_lbl); + hbox.append(&desc_lbl); + row.set_child(Some(&hbox)); + list.append(&row); + } } pub fn build() -> GBox { @@ -74,56 +88,16 @@ pub fn build() -> GBox { title.set_xalign(0.0); vbox.append(&title); - let subtitle = - Label::new(Some("System snapshots created by snap-pac on each pacman transaction.")); + let subtitle = Label::new(Some( + "System snapshots created by snap-pac on each pacman transaction.", + )); subtitle.set_xalign(0.0); subtitle.set_margin_bottom(16); vbox.append(&subtitle); let list = ListBox::new(); list.set_selection_mode(gtk4::SelectionMode::Single); - - let snapshots = list_snapshots(); - if snapshots.is_empty() { - let row = ListBoxRow::new(); - let lbl = Label::new(Some( - "No snapshots found (snapper may not be configured yet)", - )); - lbl.set_margin_top(8); - lbl.set_margin_bottom(8); - lbl.set_margin_start(8); - row.set_child(Some(&lbl)); - list.append(&row); - } else { - for snap in &snapshots { - let row = ListBoxRow::new(); - row.set_widget_name(&snap.number); - - let hbox = GBox::new(Orientation::Horizontal, 16); - hbox.set_margin_top(6); - hbox.set_margin_bottom(6); - hbox.set_margin_start(8); - hbox.set_margin_end(8); - - let num_lbl = Label::new(Some(&snap.number)); - num_lbl.set_width_chars(4); - num_lbl.set_xalign(0.0); - - let date_lbl = Label::new(Some(&snap.date)); - date_lbl.set_width_chars(22); - date_lbl.set_xalign(0.0); - - let desc_lbl = Label::new(Some(&snap.description)); - desc_lbl.set_hexpand(true); - desc_lbl.set_xalign(0.0); - - hbox.append(&num_lbl); - hbox.append(&date_lbl); - hbox.append(&desc_lbl); - row.set_child(Some(&hbox)); - list.append(&row); - } - } + populate_list(&list); let scroll = ScrolledWindow::new(); scroll.set_vexpand(true); @@ -133,32 +107,81 @@ pub fn build() -> GBox { let btn_row = GBox::new(Orientation::Horizontal, 8); btn_row.set_margin_top(12); + let refresh_btn = Button::with_label("Refresh"); let rollback_btn = Button::with_label("Rollback to selected"); let delete_btn = Button::with_label("Delete selected"); delete_btn.add_css_class("destructive-action"); { let list = list.clone(); - rollback_btn.connect_clicked(move |_| { - let Some(row) = list.selected_row() else { - return; - }; - let number = row.widget_name().to_string(); - confirm_rollback(&number); + refresh_btn.connect_clicked(move |_| { + populate_list(&list); }); } { let list = list.clone(); - delete_btn.connect_clicked(move |_| { - let Some(row) = list.selected_row() else { - return; - }; + rollback_btn.connect_clicked(move |btn| { + let Some(row) = list.selected_row() else { return }; let number = row.widget_name().to_string(); - let _ = Command::new("snapper").args(["delete", &number]).status(); + if number.is_empty() { return } + + let window = btn + .root() + .and_then(|r| r.downcast::().ok()); + + let dialog = AlertDialog::builder() + .message(&format!("Roll back to snapshot #{number}?")) + .detail("The current system state will be replaced on next boot. \ + A polkit prompt will ask for your password.") + .buttons(["Cancel", "Roll back"]) + .cancel_button(0) + .default_button(0) + .build(); + + dialog.choose(window.as_ref(), gtk4::gio::Cancellable::NONE, move |result| { + if result == Ok(1) { + // pkexec so polkit handles the privilege escalation + std::thread::spawn(move || { + let _ = Command::new("pkexec") + .args(["snapper", "rollback", &number]) + .status(); + }); + } + }); }); } + { + let list = list.clone(); + delete_btn.connect_clicked(move |btn| { + let Some(row) = list.selected_row() else { return }; + let number = row.widget_name().to_string(); + if number.is_empty() { return } + + let window = btn + .root() + .and_then(|r| r.downcast::().ok()); + + let list = list.clone(); + let dialog = AlertDialog::builder() + .message(&format!("Delete snapshot #{number}?")) + .detail("This cannot be undone.") + .buttons(["Cancel", "Delete"]) + .cancel_button(0) + .default_button(0) + .build(); + + dialog.choose(window.as_ref(), gtk4::gio::Cancellable::NONE, move |result| { + if result == Ok(1) { + let _ = Command::new("snapper").args(["delete", &number]).status(); + populate_list(&list); + } + }); + }); + } + + btn_row.append(&refresh_btn); btn_row.append(&rollback_btn); btn_row.append(&delete_btn); vbox.append(&btn_row); diff --git a/dotfiles/hyprland/hyprland.conf b/dotfiles/hypr/hyprland.conf similarity index 90% rename from dotfiles/hyprland/hyprland.conf rename to dotfiles/hypr/hyprland.conf index 5e56982..e509b63 100644 --- a/dotfiles/hyprland/hyprland.conf +++ b/dotfiles/hypr/hyprland.conf @@ -22,9 +22,11 @@ decoration { size = 6 passes = 2 } - drop_shadow = true - shadow_range = 12 - shadow_render_power = 3 + shadow { + enabled = true + range = 12 + render_power = 3 + } } animations { diff --git a/dotfiles/hyprland/keybinds.conf b/dotfiles/hypr/keybinds.conf similarity index 100% rename from dotfiles/hyprland/keybinds.conf rename to dotfiles/hypr/keybinds.conf diff --git a/iso/airootfs/etc/calamares/branding/bos/branding.desc b/iso/airootfs/etc/calamares/branding/bos/branding.desc new file mode 100644 index 0000000..d3034ab --- /dev/null +++ b/iso/airootfs/etc/calamares/branding/bos/branding.desc @@ -0,0 +1,29 @@ +--- +componentName: bos + +strings: + productName: "Bread Operating System" + shortProductName: "BOS" + version: "rolling" + shortVersion: "rolling" + versionedName: "BOS (rolling)" + shortVersionedName: "BOS" + bootloaderEntryName: "BOS" + productUrl: "https://github.com/Breadway/bos" + supportUrl: "https://github.com/Breadway/bos/issues" + knownIssuesUrl: "https://github.com/Breadway/bos/issues" + releaseNotesUrl: "https://github.com/Breadway/bos/releases" + +images: + productLogo: "logo.png" + productIcon: "logo.png" + productWelcome: "languages.png" + +slideshow: "show.qml" +slideshowAPI: 2 + +style: + sidebarBackground: "#3b4252" + sidebarText: "#eceff4" + sidebarTextSelect: "#5e81ac" + sidebarTextHighlight:"#eceff4" diff --git a/iso/airootfs/etc/calamares/branding/bos/show.qml b/iso/airootfs/etc/calamares/branding/bos/show.qml new file mode 100644 index 0000000..446c624 --- /dev/null +++ b/iso/airootfs/etc/calamares/branding/bos/show.qml @@ -0,0 +1,43 @@ +/* BOS installer slideshow */ +import QtQuick 2.15 +import io.calamares.ui 1.0 + +Presentation { + id: presentation + + Slide { + anchors.fill: parent + + Rectangle { + anchors.fill: parent + color: "#2e3440" + + Column { + anchors.centerIn: parent + spacing: 20 + + Text { + text: "Bread Operating System" + color: "#eceff4" + font.pointSize: 28 + font.bold: true + anchors.horizontalCenter: parent.horizontalCenter + } + + Text { + text: "Installing your system…" + color: "#88c0d0" + font.pointSize: 16 + anchors.horizontalCenter: parent.horizontalCenter + } + + Text { + text: "Hyprland · bread · bakery · snapshots" + color: "#616e88" + font.pointSize: 12 + anchors.horizontalCenter: parent.horizontalCenter + } + } + } + } +} diff --git a/iso/airootfs/etc/calamares/modules/mount.conf b/iso/airootfs/etc/calamares/modules/mount.conf new file mode 100644 index 0000000..767bf57 --- /dev/null +++ b/iso/airootfs/etc/calamares/modules/mount.conf @@ -0,0 +1,10 @@ +--- +# Extra mount options applied by filesystem type. +# Btrfs subvolume mounts are already configured in partition.conf. +mountOptions: + - filesystem: default + options: [noatime] + - filesystem: btrfs + options: [noatime, "compress=zstd", "space_cache=v2"] + - filesystem: vfat + options: [umask=0077] diff --git a/iso/airootfs/etc/calamares/modules/unpackfs.conf b/iso/airootfs/etc/calamares/modules/unpackfs.conf new file mode 100644 index 0000000..7d9589e --- /dev/null +++ b/iso/airootfs/etc/calamares/modules/unpackfs.conf @@ -0,0 +1,7 @@ +--- +# Unpack the live squashfs onto the target partition. +# "arch" matches profiledef.sh install_dir; adjust if that changes. +unpack: + - source: "/run/archiso/bootmnt/arch/x86_64/airootfs.sfs" + sourcefs: "squashfs" + destination: "" diff --git a/iso/post-install.sh b/iso/airootfs/etc/calamares/post-install.sh similarity index 77% rename from iso/post-install.sh rename to iso/airootfs/etc/calamares/post-install.sh index e3f3182..967292b 100644 --- a/iso/post-install.sh +++ b/iso/airootfs/etc/calamares/post-install.sh @@ -9,7 +9,7 @@ sed -i 's/NUMBER_MIN_AGE="[^"]*"/NUMBER_MIN_AGE="1800"/' /etc/snapper/configs/ro sed -i 's/NUMBER_LIMIT="[^"]*"/NUMBER_LIMIT="10"/' /etc/snapper/configs/root sed -i 's/NUMBER_LIMIT_IMPORTANT="[^"]*"/NUMBER_LIMIT_IMPORTANT="5"/' /etc/snapper/configs/root -# Allow main user to use snapper without sudo +# Allow main user to list/create/delete snapshots without sudo MAIN_USER=$(getent passwd 1000 | cut -d: -f1) sed -i "s/ALLOW_USERS=\"\"/ALLOW_USERS=\"$MAIN_USER\"/" /etc/snapper/configs/root @@ -19,18 +19,19 @@ systemctl enable bluetooth systemctl enable snapper-cleanup.timer systemctl enable grub-btrfs.path -# --- Bakery install --- +# --- Bakery: install bread ecosystem --- +# Requires [breadway] repo in /etc/pacman.conf — see iso/pacman.conf if command -v bakery &>/dev/null; then sudo -u "$MAIN_USER" bakery install bread breadbar breadbox breadcrumbs breadpad bos-settings fi -# --- Deploy dotfiles (skip existing files) --- -DOTFILES_SRC="/etc/skel/.config" +# --- Deploy dotfiles into user home (skip any file that already exists) --- +SKEL_SRC="/etc/skel/.config" DOTFILES_DEST="/home/$MAIN_USER/.config" -if [[ -d "$DOTFILES_SRC" ]]; then +if [[ -d "$SKEL_SRC" ]]; then mkdir -p "$DOTFILES_DEST" - cp -rn "$DOTFILES_SRC/." "$DOTFILES_DEST/" + cp -rn "$SKEL_SRC/." "$DOTFILES_DEST/" chown -R "$MAIN_USER:$MAIN_USER" "$DOTFILES_DEST" fi diff --git a/iso/airootfs/etc/calamares/settings.conf b/iso/airootfs/etc/calamares/settings.conf index 499e1cb..ea17565 100644 --- a/iso/airootfs/etc/calamares/settings.conf +++ b/iso/airootfs/etc/calamares/settings.conf @@ -1,5 +1,5 @@ --- -modules-search: [local, /usr/lib/calamares/modules] +modules-search: [/etc/calamares/modules, /usr/lib/calamares/modules] sequence: - show: diff --git a/iso/airootfs/etc/polkit-1/rules.d/10-snapper.rules b/iso/airootfs/etc/polkit-1/rules.d/10-snapper.rules new file mode 100644 index 0000000..dc2b538 --- /dev/null +++ b/iso/airootfs/etc/polkit-1/rules.d/10-snapper.rules @@ -0,0 +1,11 @@ +// Allow members of the wheel group to perform snapper rollback via pkexec +// without a password prompt. Other snapper operations (list/create/delete) +// are controlled by ALLOW_USERS in /etc/snapper/configs/root. +polkit.addRule(function(action, subject) { + if (action.id == "io.opensuse.Snapper.Rollback" && + subject.local && + subject.active && + subject.isInGroup("wheel")) { + return polkit.Result.YES; + } +}); diff --git a/iso/airootfs/etc/skel/.config/bread/breadd.toml b/iso/airootfs/etc/skel/.config/bread/breadd.toml new file mode 100644 index 0000000..8473fe3 --- /dev/null +++ b/iso/airootfs/etc/skel/.config/bread/breadd.toml @@ -0,0 +1,8 @@ +log_level = "info" + +[adapters] +keyboard = true +mouse = true +touchpad = true +bluetooth = true +gamepad = true diff --git a/iso/airootfs/etc/skel/.config/bread/init.lua b/iso/airootfs/etc/skel/.config/bread/init.lua new file mode 100644 index 0000000..270ee7e --- /dev/null +++ b/iso/airootfs/etc/skel/.config/bread/init.lua @@ -0,0 +1 @@ +bread.activate_profile("default") diff --git a/iso/airootfs/etc/skel/.config/breadbox/config.toml b/iso/airootfs/etc/skel/.config/breadbox/config.toml new file mode 100644 index 0000000..797b3cc --- /dev/null +++ b/iso/airootfs/etc/skel/.config/breadbox/config.toml @@ -0,0 +1,3 @@ +[[context]] +name = "default" +apps = ["firefox", "foot", "nautilus", "code"] diff --git a/iso/airootfs/etc/skel/.config/breadcrumbs/breadcrumbs.toml b/iso/airootfs/etc/skel/.config/breadcrumbs/breadcrumbs.toml new file mode 100644 index 0000000..46e26f3 --- /dev/null +++ b/iso/airootfs/etc/skel/.config/breadcrumbs/breadcrumbs.toml @@ -0,0 +1,3 @@ +[[profile]] +name = "home" +ssids = [] diff --git a/iso/airootfs/etc/skel/.config/hypr/hyprland.conf b/iso/airootfs/etc/skel/.config/hypr/hyprland.conf new file mode 100644 index 0000000..e509b63 --- /dev/null +++ b/iso/airootfs/etc/skel/.config/hypr/hyprland.conf @@ -0,0 +1,56 @@ +monitor=,preferred,auto,1 + +exec-once = breadd +exec-once = breadbar +exec-once = breadbox-sync + +source = ~/.config/hypr/keybinds.conf + +general { + gaps_in = 5 + gaps_out = 10 + border_size = 2 + col.active_border = rgba(88c0d0ff) + col.inactive_border = rgba(4c566aff) + layout = dwindle +} + +decoration { + rounding = 8 + blur { + enabled = true + size = 6 + passes = 2 + } + shadow { + enabled = true + range = 12 + render_power = 3 + } +} + +animations { + enabled = true + bezier = ease, 0.25, 0.1, 0.25, 1.0 + animation = windows, 1, 4, ease + animation = fade, 1, 4, ease + animation = workspaces, 1, 5, ease +} + +input { + kb_layout = us + follow_mouse = 1 + touchpad { + natural_scroll = true + } +} + +dwindle { + pseudotile = true + preserve_split = true +} + +misc { + disable_hyprland_logo = true + disable_splash_rendering = true +} diff --git a/iso/airootfs/etc/skel/.config/hypr/keybinds.conf b/iso/airootfs/etc/skel/.config/hypr/keybinds.conf new file mode 100644 index 0000000..7cf8cdd --- /dev/null +++ b/iso/airootfs/etc/skel/.config/hypr/keybinds.conf @@ -0,0 +1,58 @@ +$mod = SUPER + +# App launchers +bind = $mod, Space, exec, breadbox +bind = $mod, N, exec, breadpad +bind = $mod, M, exec, breadman +bind = $mod, S, exec, bos-settings + +# Core +bind = $mod, Return, exec, foot +bind = $mod, Q, killactive +bind = $mod SHIFT, E, exit +bind = $mod, F, fullscreen + +# Focus +bind = $mod, H, movefocus, l +bind = $mod, L, movefocus, r +bind = $mod, K, movefocus, u +bind = $mod, J, movefocus, d + +# Move windows +bind = $mod SHIFT, H, movewindow, l +bind = $mod SHIFT, L, movewindow, r +bind = $mod SHIFT, K, movewindow, u +bind = $mod SHIFT, J, movewindow, d + +# Workspaces +bind = $mod, 1, workspace, 1 +bind = $mod, 2, workspace, 2 +bind = $mod, 3, workspace, 3 +bind = $mod, 4, workspace, 4 +bind = $mod, 5, workspace, 5 + +bind = $mod SHIFT, 1, movetoworkspace, 1 +bind = $mod SHIFT, 2, movetoworkspace, 2 +bind = $mod SHIFT, 3, movetoworkspace, 3 +bind = $mod SHIFT, 4, movetoworkspace, 4 +bind = $mod SHIFT, 5, movetoworkspace, 5 + +# Scroll through workspaces +bind = $mod, mouse_down, workspace, e+1 +bind = $mod, mouse_up, workspace, e-1 + +# Mouse binds +bindm = $mod, mouse:272, movewindow +bindm = $mod, mouse:273, resizewindow + +# Volume +bind = , XF86AudioRaiseVolume, exec, wpctl set-volume @DEFAULT_AUDIO_SINK@ 5%+ +bind = , XF86AudioLowerVolume, exec, wpctl set-volume @DEFAULT_AUDIO_SINK@ 5%- +bind = , XF86AudioMute, exec, wpctl set-mute @DEFAULT_AUDIO_SINK@ toggle + +# Brightness +bind = , XF86MonBrightnessUp, exec, brightnessctl set 5%+ +bind = , XF86MonBrightnessDown, exec, brightnessctl set 5%- + +# Screenshot +bind = , Print, exec, grimblast copy area diff --git a/iso/airootfs/etc/systemd/system/getty@tty1.service.d/autologin.conf b/iso/airootfs/etc/systemd/system/getty@tty1.service.d/autologin.conf new file mode 100644 index 0000000..b9d22eb --- /dev/null +++ b/iso/airootfs/etc/systemd/system/getty@tty1.service.d/autologin.conf @@ -0,0 +1,3 @@ +[Service] +ExecStart= +ExecStart=-/sbin/agetty -o '-p -f -- \\u' --noclear --autologin root - $TERM diff --git a/iso/airootfs/root/.bash_profile b/iso/airootfs/root/.bash_profile new file mode 100644 index 0000000..fd13f6a --- /dev/null +++ b/iso/airootfs/root/.bash_profile @@ -0,0 +1,4 @@ +# Auto-start Hyprland on tty1 in the live session +if [[ "$(tty)" == "/dev/tty1" ]] && [[ -z "$WAYLAND_DISPLAY" ]]; then + exec Hyprland +fi diff --git a/iso/airootfs/root/.config/hypr/hyprland.conf b/iso/airootfs/root/.config/hypr/hyprland.conf new file mode 100644 index 0000000..8adb80a --- /dev/null +++ b/iso/airootfs/root/.config/hypr/hyprland.conf @@ -0,0 +1,28 @@ +# Live-session Hyprland config — launches Calamares on start. +# This is NOT the installed system config; that lives in dotfiles/hypr/. + +monitor=,preferred,auto,1 + +exec-once = calamares + +general { + border_size = 2 + col.active_border = rgba(88c0d0ff) + col.inactive_border = rgba(4c566aff) +} + +decoration { + rounding = 4 +} + +input { + kb_layout = us + follow_mouse = 1 +} + +misc { + disable_hyprland_logo = true + disable_splash_rendering = true + # Keep compositor running if calamares exits (user can relaunch) + exit_window_request_force = false +} diff --git a/iso/packages.x86_64 b/iso/packages.x86_64 index b7fbf35..32c97d4 100644 --- a/iso/packages.x86_64 +++ b/iso/packages.x86_64 @@ -47,10 +47,9 @@ gtk4-layer-shell librsvg libpulse -# Display +# Display (wlroots is bundled with Hyprland; don't list separately) wayland wayland-protocols -wlroots # Fonts noto-fonts @@ -63,10 +62,13 @@ foot # File manager nautilus -# Installer +# Installer — sourced from [breadway] repo (see pacman.conf) calamares calamares-qt6 +# Bread ecosystem — sourced from [breadway] repo +bakery + # Utilities sudo git diff --git a/iso/pacman.conf b/iso/pacman.conf new file mode 100644 index 0000000..c071a87 --- /dev/null +++ b/iso/pacman.conf @@ -0,0 +1,38 @@ +# +# BOS pacman.conf — used during ISO build and installed to the target system. +# Based on the standard Arch Linux pacman.conf. +# + +[options] +HoldPkg = pacman glibc +Architecture = auto +CheckSpace +ParallelDownloads = 5 + +Color +VerbosePkgLists +ILoveCandy + +SigLevel = Required DatabaseOptional +LocalFileSigLevel = Optional + +[core] +Include = /etc/pacman.d/mirrorlist + +[extra] +Include = /etc/pacman.d/mirrorlist + +[multilib] +Include = /etc/pacman.d/mirrorlist + +# ----------------------------------------------------------------------- +# Breadway custom repo — provides: bakery, calamares (pre-built), and the +# bread ecosystem packages (bread, breadbar, breadbox, breadcrumbs, breadpad, +# bos-settings). +# +# TODO: Replace this URL with the actual hosted repo before building. +# See: https://github.com/Breadway/repo for setup instructions. +# ----------------------------------------------------------------------- +[breadway] +SigLevel = Optional TrustAll +Server = https://repo.breadway.dev/$arch From 86826984028671c68a613d1c478b94abaa8717d3 Mon Sep 17 00:00:00 2001 From: Breadway Date: Sat, 13 Jun 2026 11:29:53 +0800 Subject: [PATCH 4/7] Fix prod-readiness issues flagged in audit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix XDG config dir logic in config/mod.rs (was double-nesting and had /home/user hardcode) - Replace /home/user hardcodes in breadbar.rs and hyprland.rs with config::config_dir() - Fix /home/user hardcode in packages.rs (uses /root fallback for .local/state path) - Remove eprintln! from GTK callback in packages.rs (no stderr at runtime) - Fix YAML parse error in branding.desc (missing space after sidebarTextHighlight key) - Add .gitignore (Rust target/, ISO artifacts, editor/OS junk, secrets) - Delete state.rs (dead code — never mod'd in main.rs) - Add brightnessctl, grim, slurp to packages.x86_64 (used by keybinds) - Rename can-you-begin-a-composed-beacon.md → DESIGN.md Co-Authored-By: Claude Sonnet 4.6 --- .gitignore | 37 +++++++++++++++++++ ...ou-begin-a-composed-beacon.md => DESIGN.md | 0 bos-settings/src/config/mod.rs | 13 ++++--- bos-settings/src/state.rs | 11 ------ bos-settings/src/ui/views/breadbar.rs | 4 +- bos-settings/src/ui/views/hyprland.rs | 3 +- bos-settings/src/ui/views/packages.rs | 4 +- .../etc/calamares/branding/bos/branding.desc | 2 +- iso/packages.x86_64 | 5 +++ 9 files changed, 56 insertions(+), 23 deletions(-) create mode 100644 .gitignore rename can-you-begin-a-composed-beacon.md => DESIGN.md (100%) delete mode 100644 bos-settings/src/state.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d5294b7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,37 @@ +# Rust build artifacts +/target/ +**/*.pdb + +# Editor / IDE +.vscode/ +.idea/ +*.swp +*.swo +*~ +.direnv/ + +# OS artifacts +.DS_Store +Thumbs.db +desktop.ini + +# Environment / secrets +.env +.env.local +*.env.* +secrets/ +*.pem +*.key +*.p12 + +# archiso build artifacts (these are large and reproducible) +/iso-build/ +/iso-out/ +*.iso +*.img + +# Runtime / logs +*.log +logs/ +*.pid +*.sock diff --git a/can-you-begin-a-composed-beacon.md b/DESIGN.md similarity index 100% rename from can-you-begin-a-composed-beacon.md rename to DESIGN.md diff --git a/bos-settings/src/config/mod.rs b/bos-settings/src/config/mod.rs index da3b8eb..bd0c4af 100644 --- a/bos-settings/src/config/mod.rs +++ b/bos-settings/src/config/mod.rs @@ -15,10 +15,13 @@ pub fn save(path: &Path, val: &T) -> Result<(), Box PathBuf { - let home = std::env::var("HOME").unwrap_or_else(|_| { - std::env::var("XDG_CONFIG_HOME") - .map(|p| PathBuf::from(p).parent().unwrap_or(Path::new("/")).to_string_lossy().to_string()) - .unwrap_or_else(|_| "/home/user".to_string()) - }); + // 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") } diff --git a/bos-settings/src/state.rs b/bos-settings/src/state.rs deleted file mode 100644 index e0e760e..0000000 --- a/bos-settings/src/state.rs +++ /dev/null @@ -1,11 +0,0 @@ -pub struct AppState { - pub current_view: String, -} - -impl AppState { - pub fn new() -> Self { - Self { - current_view: "snapshots".to_string(), - } - } -} diff --git a/bos-settings/src/ui/views/breadbar.rs b/bos-settings/src/ui/views/breadbar.rs index 9f1ceb7..dd49a52 100644 --- a/bos-settings/src/ui/views/breadbar.rs +++ b/bos-settings/src/ui/views/breadbar.rs @@ -3,10 +3,10 @@ use gtk4::{Box as GBox, Button, Label, Orientation, ScrolledWindow, TextView}; use std::path::PathBuf; fn css_path() -> PathBuf { - let home = std::env::var("HOME").unwrap_or_else(|_| "/home/user".to_string()); - PathBuf::from(home).join(".config/breadbar/style.css") + crate::config::config_dir().join("breadbar/style.css") } + pub fn build() -> GBox { let path = css_path(); let existing_css = std::fs::read_to_string(&path).unwrap_or_default(); diff --git a/bos-settings/src/ui/views/hyprland.rs b/bos-settings/src/ui/views/hyprland.rs index 0ed704d..49ac07e 100644 --- a/bos-settings/src/ui/views/hyprland.rs +++ b/bos-settings/src/ui/views/hyprland.rs @@ -23,8 +23,7 @@ fn get_monitors() -> Vec { } fn hypr_path(name: &str) -> std::path::PathBuf { - let home = std::env::var("HOME").unwrap_or_else(|_| "/home/user".to_string()); - std::path::PathBuf::from(home).join(".config/hypr").join(name) + crate::config::config_dir().join("hypr").join(name) } pub fn build() -> GBox { diff --git a/bos-settings/src/ui/views/packages.rs b/bos-settings/src/ui/views/packages.rs index 6b3aedc..1281c44 100644 --- a/bos-settings/src/ui/views/packages.rs +++ b/bos-settings/src/ui/views/packages.rs @@ -8,7 +8,7 @@ use std::io::{BufRead, BufReader}; use std::process::{Command, Stdio}; fn read_installed() -> HashMap { - let home = std::env::var("HOME").unwrap_or_else(|_| "/home/user".to_string()); + let home = std::env::var("HOME").unwrap_or_else(|_| "/root".to_string()); let path = std::path::Path::new(&home) .join(".local/state/bakery/installed.json"); @@ -132,7 +132,7 @@ pub fn build() -> GBox { Ok(mut child) => { std::thread::spawn(move || { let _ = child.wait(); }); } - Err(e) => eprintln!("bakery update failed: {e}"), + Err(_) => {} // bakery not found; button is a no-op } }); diff --git a/iso/airootfs/etc/calamares/branding/bos/branding.desc b/iso/airootfs/etc/calamares/branding/bos/branding.desc index d3034ab..ff72dd7 100644 --- a/iso/airootfs/etc/calamares/branding/bos/branding.desc +++ b/iso/airootfs/etc/calamares/branding/bos/branding.desc @@ -26,4 +26,4 @@ style: sidebarBackground: "#3b4252" sidebarText: "#eceff4" sidebarTextSelect: "#5e81ac" - sidebarTextHighlight:"#eceff4" + sidebarTextHighlight: "#eceff4" diff --git a/iso/packages.x86_64 b/iso/packages.x86_64 index 32c97d4..6abc179 100644 --- a/iso/packages.x86_64 +++ b/iso/packages.x86_64 @@ -69,6 +69,11 @@ calamares-qt6 # Bread ecosystem — sourced from [breadway] repo bakery +# Input / screen utilities +brightnessctl +grim +slurp + # Utilities sudo git From 6f148e9a06b946c9f66f5bed837763d850766dc2 Mon Sep 17 00:00:00 2001 From: Breadway Date: Sat, 13 Jun 2026 11:32:40 +0800 Subject: [PATCH 5/7] Add bakery.toml and packaging/arch to match bread ecosystem Mirrors the build/distribution pattern used by the bread project: - bakery.toml describes bos-settings as a bakery-managed package - packaging/arch/PKGBUILD builds and installs the binary via cargo - packaging/arch/bos-settings.desktop for app launchers - LICENSE (MIT) required by PKGBUILD Co-Authored-By: Claude Sonnet 4.6 --- LICENSE | 21 ++++++++++++++++++ bakery.toml | 12 ++++++++++ packaging/arch/PKGBUILD | 34 +++++++++++++++++++++++++++++ packaging/arch/README.md | 25 +++++++++++++++++++++ packaging/arch/bos-settings.desktop | 9 ++++++++ 5 files changed, 101 insertions(+) create mode 100644 LICENSE create mode 100644 bakery.toml create mode 100644 packaging/arch/PKGBUILD create mode 100644 packaging/arch/README.md create mode 100644 packaging/arch/bos-settings.desktop diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..e837178 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Breadway + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/bakery.toml b/bakery.toml new file mode 100644 index 0000000..632de76 --- /dev/null +++ b/bakery.toml @@ -0,0 +1,12 @@ +name = "bos-settings" +description = "System settings app for Bread OS" +binaries = ["bos-settings"] +system_deps = ["gtk4", "glib2"] +optional_system_deps = ["snapper"] +bread_deps = [] + +[config] +dir = "~/.config" + +[install] +post_install = [] diff --git a/packaging/arch/PKGBUILD b/packaging/arch/PKGBUILD new file mode 100644 index 0000000..60a7521 --- /dev/null +++ b/packaging/arch/PKGBUILD @@ -0,0 +1,34 @@ +# Maintainer: Breadway + +pkgname=bos-settings +pkgver=0.1.0 +pkgrel=1 +pkgdesc="System settings app for Bread OS" +arch=('x86_64') +url="https://github.com/Breadway/bos" +license=('MIT') +depends=('gtk4' 'glib2' 'hicolor-icon-theme') +optdepends=( + 'snapper: snapshot management view' +) +makedepends=('rust' 'cargo') +source=("${pkgname}-${pkgver}.tar.gz") +sha256sums=('SKIP') + +build() { + cd "${srcdir}/${pkgname}-${pkgver}" + cargo build --release --locked -p bos-settings +} + +check() { + cd "${srcdir}/${pkgname}-${pkgver}" + cargo test --release --locked -p bos-settings +} + +package() { + cd "${srcdir}/${pkgname}-${pkgver}" + install -Dm755 target/release/bos-settings "${pkgdir}/usr/bin/bos-settings" + install -Dm644 packaging/arch/bos-settings.desktop \ + "${pkgdir}/usr/share/applications/bos-settings.desktop" + install -Dm644 LICENSE "${pkgdir}/usr/share/licenses/${pkgname}/LICENSE" +} diff --git a/packaging/arch/README.md b/packaging/arch/README.md new file mode 100644 index 0000000..af5acc0 --- /dev/null +++ b/packaging/arch/README.md @@ -0,0 +1,25 @@ +Arch packaging +============== + +`PKGBUILD` builds and installs `bos-settings` from source. + +## Local build + +```bash +makepkg -si +``` + +## Before publishing to [breadway] repo + +1. Tag a release on GitHub. +2. Update `pkgver` to match the tag. +3. Update `source` to the release tarball URL. +4. Run `updpkgsums` (or manually set `sha256sums`). + +## Runtime dependencies + +| Package | Required | Notes | +|---------|----------|-------| +| `gtk4` | yes | UI toolkit | +| `glib2` | yes | always | +| `snapper` | optional | snapshot management view | diff --git a/packaging/arch/bos-settings.desktop b/packaging/arch/bos-settings.desktop new file mode 100644 index 0000000..5385adb --- /dev/null +++ b/packaging/arch/bos-settings.desktop @@ -0,0 +1,9 @@ +[Desktop Entry] +Name=BOS Settings +Comment=System settings for Bread OS +Exec=bos-settings +Icon=preferences-system +Terminal=false +Type=Application +Categories=Settings;System; +StartupWMClass=bos-settings From 2c6feb4ea0dd2b6be0293aa825731efccb693748 Mon Sep 17 00:00:00 2001 From: Breadway Date: Sat, 13 Jun 2026 11:42:00 +0800 Subject: [PATCH 6/7] Add Forgejo Actions workflows and fix [breadway] repo URL MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - .forgejo/workflows/mirror.yml: mirrors every push/tag to GitHub - .forgejo/workflows/package.yml: builds PKGBUILD on tag and publishes bos-settings to the Forgejo Arch package registry (distrib=breadway) - iso/pacman.conf: replace placeholder repo.breadway.dev with the actual Forgejo package registry URL Requires two Forgejo secrets: GITHUB_MIRROR_TOKEN — GitHub PAT with repo push scope FORGEJO_TOKEN — Forgejo token with package:write scope Co-Authored-By: Claude Sonnet 4.6 --- .forgejo/workflows/mirror.yml | 20 +++++++++++++++ .forgejo/workflows/package.yml | 46 ++++++++++++++++++++++++++++++++++ iso/pacman.conf | 7 +++--- 3 files changed, 70 insertions(+), 3 deletions(-) create mode 100644 .forgejo/workflows/mirror.yml create mode 100644 .forgejo/workflows/package.yml diff --git a/.forgejo/workflows/mirror.yml b/.forgejo/workflows/mirror.yml new file mode 100644 index 0000000..9e7f427 --- /dev/null +++ b/.forgejo/workflows/mirror.yml @@ -0,0 +1,20 @@ +name: Mirror to GitHub + +on: + push: + branches: ['**'] + tags: ['**'] + +jobs: + mirror: + runs-on: [self-hosted, hestia] + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Push to GitHub + run: | + git remote add github \ + "https://x-access-token:${{ secrets.GITHUB_MIRROR_TOKEN }}@github.com/Breadway/bos.git" + git push github --mirror diff --git a/.forgejo/workflows/package.yml b/.forgejo/workflows/package.yml new file mode 100644 index 0000000..2a339b4 --- /dev/null +++ b/.forgejo/workflows/package.yml @@ -0,0 +1,46 @@ +name: Build and publish package + +on: + push: + tags: ['v*'] + +jobs: + package: + runs-on: [self-hosted, hestia] + container: + image: archlinux:latest + options: --privileged + + steps: + - uses: actions/checkout@v4 + + - name: Set version + run: echo "VERSION=${GITHUB_REF_NAME#v}" >> $GITHUB_ENV + + - name: Install build dependencies + run: pacman -Syu --noconfirm base-devel git rust cargo gtk4 glib2 + + - name: Create builder user + run: useradd -m builder + + - name: Prepare source + run: | + 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 + cp -r . /home/builder/src + chown -R builder:builder /home/builder/src + + - name: Build package + run: su builder -c "cd /home/builder/src/packaging/arch && makepkg -sf --noconfirm" + + - name: Publish to Forgejo registry + run: | + PKG=$(find /home/builder/src/packaging/arch -name '*.pkg.tar.zst' | head -1) + curl -fsS -X PUT \ + -H "Authorization: token ${{ secrets.FORGEJO_TOKEN }}" \ + --upload-file "${PKG}" \ + "https://git.breadway.dev/api/packages/breadway/arch/push?distrib=breadway" diff --git a/iso/pacman.conf b/iso/pacman.conf index c071a87..701e900 100644 --- a/iso/pacman.conf +++ b/iso/pacman.conf @@ -30,9 +30,10 @@ Include = /etc/pacman.d/mirrorlist # bread ecosystem packages (bread, breadbar, breadbox, breadcrumbs, breadpad, # bos-settings). # -# TODO: Replace this URL with the actual hosted repo before building. -# See: https://github.com/Breadway/repo for setup instructions. +# Packages are published here by the Forgejo Actions package.yml workflow +# in each repo. See git.breadway.dev/api/packages/breadway/arch for the +# package registry. # ----------------------------------------------------------------------- [breadway] SigLevel = Optional TrustAll -Server = https://repo.breadway.dev/$arch +Server = https://git.breadway.dev/api/packages/breadway/arch/breadway/$arch From 034e948abb1817b7d07ef9d2a68855294b3bba24 Mon Sep 17 00:00:00 2001 From: Breadway Date: Sat, 13 Jun 2026 03:49:36 +0000 Subject: [PATCH 7/7] Initial commit --- .gitignore | 16 ++++++++++++++++ LICENSE | 9 +++++++++ README.md | 2 ++ 3 files changed, 27 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3ca43ae --- /dev/null +++ b/.gitignore @@ -0,0 +1,16 @@ +# ---> Rust +# Generated by Cargo +# will have compiled files and executables +debug/ +target/ + +# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries +# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html +Cargo.lock + +# These are backup files generated by rustfmt +**/*.rs.bk + +# MSVC Windows builds of rustc generate these, which store debugging information +*.pdb + diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..6fa0fc0 --- /dev/null +++ b/LICENSE @@ -0,0 +1,9 @@ +MIT License + +Copyright (c) 2026 Breadway + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..71a1edf --- /dev/null +++ b/README.md @@ -0,0 +1,2 @@ +# bos +