Compare commits

...

34 commits

Author SHA1 Message Date
Breadway
a32f5bf81a CI: add ISO build + release workflow
Some checks failed
Build and publish package / package (push) Failing after 1m32s
Mirror to GitHub / mirror (push) Successful in 3s
Build and publish bibata-cursor-theme / bibata (push) Failing after 31s
Build and publish powerlevel10k / powerlevel10k (push) Failing after 1m18s
release-iso.yml runs on v* tag pushes (or workflow_dispatch) on the hestia
self-hosted runner. It:
- Boots an archlinux container (--privileged --network=host)
- Downloads all bakery ecosystem binaries from their pinned GitHub releases
- Builds bread-theme from source at the tag in bos-settings/Cargo.toml
- Runs build-local.sh with CI_BUILD=1 + LAPTOP_HOME=/build-home
- Uploads the ISO to a Forgejo pre-release
- Creates a GitHub release pointing to Forgejo (GitHub 2 GB limit workaround)

build-local.sh: add CI_BUILD=1 mode — rewrites the [breadway] pacman repo
URL to localhost:3002 instead of the Tailscale address, since the CI
container runs on hestia with --network=host.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FHr5CeufsAfTrt41XoApir
2026-06-18 20:54:46 +08:00
Breadway
6dc759d318 v0.4.0: branding refresh, breadpaper baked in, bos-settings 0.4.0
All checks were successful
Mirror to GitHub / mirror (push) Successful in 4s
Build and publish package / package (push) Successful in 1m47s
- Move assets to assets/ directory (bread_white.svg, icons 256/512/1024px)
- Update Calamares branding + Plymouth theme logos
- Bake breadpaper (wallpaper manager + pywal) into /etc/skel alongside the
  rest of the bread ecosystem — previously missing from the ISO build
- Bump bos-settings to 0.4.0

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FHr5CeufsAfTrt41XoApir
2026-06-18 14:50:44 +08:00
Breadway
5e043f0834 Minor cleanups: gitignore out/, expect() messages, comment wording
All checks were successful
Mirror to GitHub / mirror (push) Successful in 3s
- .gitignore: ignore the /out/ ISO build dir
- bos-settings: use expect() with messages over unwrap() for piped stdio;
  drop a stray blank line
- pacman.conf: reword the SigLevel=Never TODO as a future-improvement note

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 22:57:58 +08:00
Breadway
b5bfef0435 ISO: microcode + plymouth hooks, PDF/VA-API packages, first-run network
- post-install: ensure the `microcode` initramfs hook (after autodetect) so
  installed systems carry CPU ucode — the live ISO embeds it, so nothing is
  staged onto the target otherwise. Rebuild all presets with `mkinitcpio -P`.
- post-install: drop the nonexistent `sd-plymouth` hook branch; only the udev
  `plymouth` hook exists. Set the theme then rebuild once.
- packages: add zathura + zathura-pdf-mupdf (BOS had no PDF viewer) and
  libva-utils (`vainfo`); the Mesa VA-API backend now ships in `mesa` itself.
- bos-welcome: on first run, if NetworkManager isn't fully online, open nmtui
  so the user connects before the first bos-update/pacman (avoids confusing DNS
  errors on a fresh install). Float the bos-netsetup window like bos-welcome.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 22:57:58 +08:00
Breadway
7d0b08ac1d Initialise the pacman keyring during install
Fresh installs couldn't update — the live medium's /etc/pacman.d/gnupg doesn't
reliably carry to the target, so the first `pacman -Syu` failed with "keyring is
not writable / required key missing from keyring". Run pacman-key --init +
--populate archlinux in post-install so signature verification works out of the
box. ([breadway] is SigLevel=Never, so no extra key needed.)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 18:49:55 +08:00
Breadway
3e61255d43 Default to zsh distro-wide (live user + useradd default)
All checks were successful
Mirror to GitHub / mirror (push) Successful in 5s
BOS shipped zsh + a p10k skel .zshrc and Calamares' userShell was already
/bin/zsh, but two paths still defaulted to bash:
  - /etc/default/useradd had SHELL=/usr/bin/bash, so any plain `useradd` (and
    anything not going through Calamares) created bash users.
  - bos-live-setup created the live ISO user with -s /bin/bash, so the live
    session ran bash instead of the BOS zsh setup.

Ship /etc/default/useradd with SHELL=/usr/bin/zsh and create liveuser with zsh
so the whole distro — live and installed — defaults to zsh.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 17:45:07 +08:00
Breadway
cfe97a7c4f Run breadd as a systemd user service by default
All checks were successful
Mirror to GitHub / mirror (push) Successful in 6s
Best practice for the long-running bread daemon: ship an enabled user unit in
skel (~/.config/systemd/user/breadd.service + default.target.wants symlink)
instead of a bare Hyprland exec-once. Gives crash-restart, journald logging
(journalctl --user -u breadd), and proper lifecycle.

- ExecStart uses %h so it works for any account created from skel (not a
  hardcoded home).
- RuntimeDirectoryPreserve=yes so restarting breadd doesn't wipe the shared
  theme.css that bread-theme writes into /run/user/<uid>/bread.
- hyprland.lua: replace the `breadd` exec-once with a Wayland-env import
  (dbus-update-activation-environment) + `systemctl --user restart breadd`, so
  the service — which autostarts at login before Hyprland exists — picks up
  HYPRLAND_INSTANCE_SIGNATURE and can drive the compositor.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 14:47:58 +08:00
Breadway
95b2277525 bos-settings 0.3.1: bread-theme v0.2.8 (working live reload)
All checks were successful
Mirror to GitHub / mirror (push) Successful in 5s
Build and publish package / package (push) Successful in 1m33s
Pick up the directory-watch fix so bos-settings hot-reloads the shared stylesheet
on `bread-theme reload` like the rest of the desktop (its v0.2.6 build had the
broken file-watch). No code change — only the dependency + version bump.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 13:59:59 +08:00
Breadway
7761dd0ff5 build-local: make WORK dir overridable (avoid /tmp tmpfs exhaustion)
All checks were successful
Mirror to GitHub / mirror (push) Successful in 5s
On hermes /tmp is a 16 GB tmpfs; a full xz build can exhaust it mid-run. WORK now
honours an env override (matching OUT) so it can be pointed at the NVMe.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 09:03:45 +08:00
Breadway
db5728c0b3 Add bos-update + replicate the dev zsh shell
All checks were successful
Mirror to GitHub / mirror (push) Successful in 5s
Build and publish powerlevel10k / powerlevel10k (push) Successful in 1m10s
bos-update: one command that updates both BOS channels — pacman -Syu (snap-pac
snapshotted) and bakery update --all — best-effort so one failing doesn't abort
the other. Baked into the live env and skel.

Shell: match the dev laptop's zsh. Ship Powerlevel10k + zsh-autosuggestions,
zsh-history-substring-search and zsh-syntax-highlighting, sourced from the distro
packages (no oh-my-zsh framework) in the correct order, plus the dev .p10k.zsh.
Powerlevel10k is AUR-only, so it's republished to [breadway] via
packaging/powerlevel10k + a CI workflow (builds libgit2 + gitstatus from source),
same pattern as bibata / zen-browser-bin. skel/.zshrc keeps the BOS QoL aliases
and pywal palette import, with `update` aliased to bos-update.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 08:49:53 +08:00
Breadway
b587f2206e Add a copy-to-RAM boot entry (UEFI + BIOS)
All checks were successful
Mirror to GitHub / mirror (push) Successful in 5s
Loads airootfs.sfs into RAM at boot so the installer reads from memory
instead of a possibly-flaky USB — fixes SquashFS read errors during
unpackfs. Kept as a separate menu entry (not default) since it needs a few
GB of RAM.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 19:40:16 +08:00
Breadway
9bf071b406 bos-settings 0.3.0: shared theme release
All checks were successful
Mirror to GitHub / mirror (push) Successful in 8s
Build and publish package / package (push) Successful in 1m44s
Bump to 0.3.0 and pin bread-theme v0.2.6 in the lockfile so the [breadway]
package build (cargo --locked) picks up the shared-stylesheet migration.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 18:37:55 +08:00
Breadway
d7a8f408b5 Ship a low-battery-warning bread module by default
All checks were successful
Mirror to GitHub / mirror (push) Successful in 5s
A zero-config bread module (auto-discovered) that fires a critical
notification once when the battery runs low and resets on AC. No-op on
desktops. Demonstrates the bread automation layer out of the box.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 17:07:20 +08:00
Breadway
0aeb2c4b6b BOS: bake the bread-theme CLI and generate the shared stylesheet at login
All checks were successful
Mirror to GitHub / mirror (push) Successful in 6s
- Add bread-theme to the binaries baked into /etc/skel from bakery state.
- Run `bread-theme generate` first in the Hyprland autostart so the shared
  GUI stylesheet ($XDG_RUNTIME_DIR/bread/theme.css) exists before breadbar /
  breadbox / bos-settings paint (they also live-reload it on change).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 16:59:03 +08:00
Breadway
29a0070748 bos-settings: use the shared bread-theme stylesheet
Replace the hardcoded Nord palette (which ignored pywal and the rest of the
ecosystem entirely) with bread_theme::gtk::apply_shared() — bos-settings now
loads the same generated stylesheet as breadbar/breadbox/breadpad and keeps
only its own layout rules (.view-content padding). It recolours live with the
desktop. Bump gtk4 0.9 -> 0.11 / glib -> 0.22 to match the ecosystem.

Note: bread-theme dep pins tag v0.2.6 (cut at release); Cargo.lock to be
regenerated then.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 16:47:52 +08:00
Breadway
e471bfe83e docs+test: ecosystem matrix, keybinds, limitations, recovery, smoke test
All checks were successful
Mirror to GitHub / mirror (push) Successful in 5s
README: add a bread-ecosystem feature matrix, keyboard-shortcut reference,
a Known Limitations section (NVIDIA/Mesa, VM GPU accel, Secure Boot, btrfs
assumption), and a Recovery guide (snapshot rollback + GRUB/EFI repair from
the live ISO).

scripts/smoke-test.sh: read-only post-install validator — btrfs subvolumes,
snapper config, enabled services, bread bins on PATH, bos-settings, default
dotfiles, and the GRUB EFI artifacts. Exits non-zero on any failure.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 15:51:47 +08:00
Breadway
7e97b5c04e Add first-run welcome + keybind cheatsheet onboarding
New users get a one-time welcome window on first boot (self-gating marker,
skipped for the live/installer user) and a keybind cheatsheet on SUPER+/.
Also bind BOS Settings to SUPER+, (it had no launcher bind). Both popups
are floated/centred via window rules. Addresses the onboarding/
discoverability gap from external review.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 15:51:47 +08:00
Breadway
20af38f826 Fix dark theme, animation speed, kitty opacity; add README
All checks were successful
Mirror to GitHub / mirror (push) Successful in 6s
- libadwaita apps (nautilus, gnome-text-editor) rendered light because
  gsettings-desktop-schemas + dconf were missing, so the color-scheme
  prefer-dark autostart silently no-op'd. Add both packages.
- Replace Hyprland's slow default animations with the reference laptop's
  bezier curves + per-leaf speeds (hl.curve + hl.animation).
- kitty background_opacity 0.88 -> 0.6 to match the laptop; drop the
  macOS-only background_blur line (Hyprland supplies the blur).
- Add README.md documenting the actual image, build, and test flow.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 15:36:41 +08:00
Breadway
b85e0e32fb Use otf-font-awesome (desktop) instead of ttf-font-awesome
All checks were successful
Mirror to GitHub / mirror (push) Successful in 6s
ttf-font-awesome resolved to woff2-font-awesome, a web-only format that
desktop apps can't render glyphs from. otf-font-awesome is the installable
desktop OTF.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 14:51:50 +08:00
Breadway
7652d92b81 Complete the desktop: default apps, mDNS, firewall, zram, fonts
All checks were successful
Mirror to GitHub / mirror (push) Successful in 6s
Wire up features that were half-shipped and add sensible resilience
defaults:

- mimeapps.list in skel: images->loupe, A/V->vlc, text->gnome-text-editor,
  pdf/html->zen, archives->file-roller, dirs->nautilus (so opening a file
  from nautilus actually does something)
- avahi + nss-mdns: CUPS network-printer discovery + .local resolution
  (enable avahi-daemon; insert mdns_minimal into nsswitch hosts:)
- ufw: deny-incoming firewall, mDNS (5353/udp) allowed so discovery still
  works; enabled in post-install
- zram-generator: compressed RAM swap (half RAM capped 4 GiB, zstd)
- fwupd + reflector.timer: firmware updates and periodic mirror refresh
- fonts: ttf-liberation (Office/web metric compat), ttf-dejavu, font-awesome

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 14:47:06 +08:00
Breadway
1f53377914 bos-settings: full, non-destructive control of every bread* config
All checks were successful
Mirror to GitHub / mirror (push) Successful in 6s
Build and publish package / package (push) Successful in 1m35s
The bread/breadpad/breadcrumbs/breadbox views wrote invented schemas
(e.g. top-level log_level, [[profile]] name/ssids) that did not match the
apps' real TOML, so they showed empty and — worse — clobbered the real
config on Save, since the old config::save serialized only the keys it
modelled.

Rework the config layer onto toml_edit: parse each file into a
DocumentMut, mutate only the specific keys a view exposes, and write it
back preserving comments and any unmodelled keys (calendar password,
saved-network passwords, model paths). Unit-tested.

Add ui/widgets.rs (switch/entry/password/dropdown/spin/float/csv rows +
view scaffold + save button) bound to the shared document, then rewrite
the four views against the real schemas with far more coverage:

- bread: [daemon], [lua], [modules], all five [adapters.*] with their
  sub-options, [events], [notifications]
- breadpad: [settings], [model] + [model.ollama], [reminders], [calendar]
- breadcrumbs: [settings] (7 keys), [[networks]] editor, [profiles.*] editor
- breadbox: fixed to real [[contexts]] name/priority array editor

Goal: configure everything from the GUI rather than hand-editing TOML.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 14:26:49 +08:00
Breadway
c46e348d6a Fill desktop gaps: GUI apps, printing, media, Qt/portal integration
Add the packages a general desktop is expected to ship, chosen to stay
opinionated but average-user friendly:

- Editors: neovim (+ ripgrep, fd for a usable nvim/fzf experience)
- GUI basics: gnome-text-editor, gnome-calculator, file-roller, loupe
- Media: vlc (BOS had codecs but no player)
- Hardware: cups + cups-pk-helper + system-config-printer (enable
  cups.socket in post-install), blueman, seahorse
- Platform: qt5-wayland + qt6-wayland (native Wayland for Qt apps under
  the QT_QPA_PLATFORM=wayland we set), xdg-desktop-portal-gtk (file
  dialogs/screenshare for Flatpak/Electron/Zen), flatpak

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 14:26:49 +08:00
Breadway
31d0875791 Restore Bibata cursor now that it's published to [breadway]
All checks were successful
Mirror to GitHub / mirror (push) Successful in 5s
bibata-cursor-theme-bin 2.0.7-1 is now in the [breadway] repo, so add it
back to the package list and re-enable the Bibata-Modern-Ice cursor in the
Hyprland env, GTK settings, and gsettings autostart.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 13:03:11 +08:00
Breadway
3dd53f3fe6 Republish bibata-cursor-theme to [breadway] (AUR-only upstream)
All checks were successful
Mirror to GitHub / mirror (push) Successful in 6s
Build and publish bibata-cursor-theme / bibata (push) Successful in 34s
Bibata is the chosen BOS default cursor but is AUR-only, so mirror the
prebuilt -bin package into the [breadway] repo the same way calamares and
zen-browser-bin are. The workflow clones the triggering branch (not the
default branch) so it can build from iso-boot-fix, and uses the scoped
REGISTRY_TOKEN for publishing.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 13:01:38 +08:00
Breadway
4a16e18cf3 Drop bibata-cursor-theme (AUR-only, not in repos)
Use Hyprland default cursor instead. All other theming changes from the
previous commit are unaffected.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 10:34:10 +08:00
Breadway
d484c8e933 Polish BOS: dark theme, shell QoL, icons, media, clipboard
All checks were successful
Mirror to GitHub / mirror (push) Successful in 5s
- Global dark theme: gnome-themes-extra (GTK3 Adwaita-dark), qt5ct/qt6ct
  with Fusion dark skel config, QT_QPA_PLATFORMTHEME=qt5ct env
- Icons/cursor: papirus-icon-theme (Papirus-Dark) + bibata-cursor-theme
  (Bibata-Modern-Ice), set via gsettings autostart + GTK settings.ini + env
- gnome-keyring: credential storage for browsers and apps
- gvfs + gvfs-mtp: nautilus trash, phone access, network shares
- zsh: default user shell (users.conf userShell=/bin/zsh) + skel .zshrc
  with eza/bat/fzf/zoxide aliases, git prompt, fzf history search
- Shell QoL packages: eza, bat, fzf, zoxide
- Media: gst-plugins-good/bad/ugly, pavucontrol
- cliphist: clipboard history daemon (autostart) + SUPER+SHIFT+V bind
- Fonts: noto-fonts-cjk, ttf-jetbrains-mono-nerd

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 10:31:18 +08:00
Breadway
a4af3aa938 Make Plymouth splash black to match the black-base theme
All checks were successful
Mirror to GitHub / mirror (push) Successful in 4s
The boot splash still used the old bread-brown background (#230b00)
after the rest of the theme moved to a black base (#0c0c0c). Switch
bos.script's background to black so the boot splash is consistent with
the wallpaper/pywal palette and breadbar.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 09:17:10 +08:00
Breadway
9ea57d87c0 Make BOS a complete, bootable, themed desktop OS
Some checks failed
Mirror to GitHub / mirror (push) Failing after 7s
Install/boot reliability:
- Use native Calamares initcpiocfg/initcpio + explicit grub-install (nvram +
  --removable) in post-install; drop the flaky native bootloader/grubcfg modules.
- mount.conf: bind /proc /sys /dev (devtmpfs) /run + efivars into the chroot.
- bos-copy-kernel: stage kernel + write a stock mkinitcpio preset (replace the
  archiso preset). Per-service systemctl enable (fixes NetworkManager et al.
  silently not enabling due to the all-or-nothing grub-btrfs.path name).

System completeness:
- greetd + tuigreet graphical login; installed pacman.conf + working mirrorlist;
  base CLI tools (nano, micro, vim, htop, …); amd/intel-ucode; tlp + hypridle
  power management; systemd-timesyncd, fstrim.timer; wpa_supplicant wifi; Zen
  browser (republished to the [Breadway] repo).

Desktop + theming:
- Native Lua Hyprland config (hyprland.lua) with curated standard binds; kitty
  (blur) replaces foot; awww wallpaper + pywal palette (tamed to a black base
  with warm accents); GTK dark mode.
- Plymouth boot splash (bos theme: logo + spinner + status) via plymouthcfg.
- Varela Round font; Calamares bread-palette sidebar (logo/black-region polish
  still pending).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 09:09:34 +08:00
Breadway
2a866144f4 Fix breadd skel config schema; remove temp live diagnostic
All checks were successful
Mirror to GitHub / mirror (push) Successful in 6s
- breadd.toml: the shipped skel used a stale [adapters] schema
  (keyboard/mouse/touchpad/gamepad booleans); breadd 0.6.4 expects
  hyprland/udev/power/network/bluetooth structs. `bluetooth = true` collided
  with the real AdapterToggle field and aborted the daemon at startup.
- Drop the temporary bos-live-diag serial diagnostic now that the live-session
  failures are diagnosed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 19:38:06 +08:00
Breadway
122cd39cb1 TEMP: route live diag to serial via sudo (revert after)
All checks were successful
Mirror to GitHub / mirror (push) Successful in 10s
2026-06-14 19:17:46 +08:00
Breadway
356cc08dfe TEMP: live-session diagnostic to serial (revert after)
Some checks failed
Mirror to GitHub / mirror (push) Failing after 9s
2026-06-14 19:08:42 +08:00
Breadway
3a30cd004f Drop removed Hyprland dwindle:pseudotile option
All checks were successful
Mirror to GitHub / mirror (push) Successful in 11s
Current Hyprland no longer accepts dwindle:pseudotile (it's a dispatcher now),
which threw a non-fatal config-error banner on both the live and installed
desktop. preserve_split is still valid and kept.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 18:54:06 +08:00
Breadway
23e60dffe0 Bake bread ecosystem into the ISO + full live desktop; fix installer timeout
All checks were successful
Mirror to GitHub / mirror (push) Successful in 16s
- packages.x86_64: add bread, breadbar, breadbox, breadcrumbs, breadpad,
  bos-settings so they ship in the squashfs and reach the target via unpackfs
  (no network needed; install works fully offline)
- shellprocess.conf: set timeout 1800 — Calamares' 10s default was killing
  post-install.sh partway (the real cause of the empty /boot + ESP); the "-"
  prefix had been masking the kill as success
- bos-live-setup: live user now boots the real BOS desktop from /etc/skel
  (breadd + breadbar + breadbox) with the installer layered on top
  (auto-launch + Super+I), instead of an installer-only kiosk
- post-install.sh: drop the now-redundant networked `bakery install`

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 18:41:59 +08:00
Breadway
b9544d517b Fix unbootable installs: lay the kernel into the target and own GRUB
All checks were successful
Mirror to GitHub / mirror (push) Successful in 9s
archiso keeps vmlinuz/initramfs in the ISO boot dir, not the squashfs, so
unpackfs lays down an empty /boot. The chroot's mkinitcpio/grub-mkconfig had
nothing to work with and the ESP ended up empty (firmware found no bootloader).

- shellprocess@kernel (dontChroot) copies the live kernel into the target
  /boot before the bootloader step
- post-install.sh now runs grub-install itself, including a --removable pass
  so firmware with no NVRAM entry still boots via EFI/BOOT/BOOTX64.EFI

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-14 17:57:50 +08:00
81 changed files with 5373 additions and 699 deletions

View file

@ -0,0 +1,37 @@
name: Build and publish bibata-cursor-theme
# Bibata is AUR-only (not in Arch's official repos), so BOS maintains an
# in-house PKGBUILD and publishes the built package to the [breadway] repo.
# It's the prebuilt -bin variant, so no build deps beyond base-devel.
on:
push:
paths:
- 'packaging/bibata/**'
workflow_dispatch:
jobs:
bibata:
runs-on: [self-hosted, hestia]
container:
image: archlinux:latest
steps:
- name: Build and publish
env:
PUBLISH_TOKEN: ${{ secrets.REGISTRY_TOKEN }}
run: |
set -euo pipefail
pacman -Syu --noconfirm base-devel git
useradd -m builder
git config --global --add safe.directory '*'
# Clone the branch that triggered this run (not the default branch),
# so the package can be built/published from a feature branch.
git clone --depth 1 --branch "${GITHUB_REF_NAME}" \
"https://git.breadway.dev/${GITHUB_REPOSITORY}.git" /home/builder/src
chown -R builder:builder /home/builder/src
su builder -c "cd /home/builder/src/packaging/bibata && makepkg -f --noconfirm --nocheck"
PKG=$(find /home/builder/src/packaging/bibata -name '*.pkg.tar.zst' | head -1)
curl -fsS -X PUT \
-H "Authorization: token ${PUBLISH_TOKEN}" \
-H "Content-Type: application/octet-stream" \
--data-binary "@${PKG}" \
"https://git.breadway.dev/api/packages/Breadway/arch/os"

View file

@ -0,0 +1,35 @@
name: Build and publish powerlevel10k
# Powerlevel10k (the BOS default zsh prompt) is AUR-only, so BOS maintains an
# in-house PKGBUILD and publishes the built package to the [breadway] repo.
# Builds gitstatus + libgit2 from source, so it needs cmake + zsh beyond base-devel.
on:
push:
paths:
- 'packaging/powerlevel10k/**'
workflow_dispatch:
jobs:
powerlevel10k:
runs-on: [self-hosted, hestia]
container:
image: archlinux:latest
steps:
- name: Build and publish
env:
PUBLISH_TOKEN: ${{ secrets.REGISTRY_TOKEN }}
run: |
set -euo pipefail
pacman -Syu --noconfirm base-devel git cmake zsh
useradd -m builder
git config --global --add safe.directory '*'
git clone --depth 1 --branch "${GITHUB_REF_NAME}" \
"https://git.breadway.dev/${GITHUB_REPOSITORY}.git" /home/builder/src
chown -R builder:builder /home/builder/src
su builder -c "cd /home/builder/src/packaging/powerlevel10k && makepkg -f --noconfirm --nocheck"
PKG=$(find /home/builder/src/packaging/powerlevel10k -name '*.pkg.tar.zst' | head -1)
curl -fsS -X PUT \
-H "Authorization: token ${PUBLISH_TOKEN}" \
-H "Content-Type: application/octet-stream" \
--data-binary "@${PKG}" \
"https://git.breadway.dev/api/packages/Breadway/arch/os"

View file

@ -0,0 +1,206 @@
name: Build and release ISO
# Builds the BOS ISO on the hestia self-hosted runner (native Arch container),
# downloads all bakery ecosystem binaries from their GitHub releases, compiles
# bread-theme from source, and uploads the resulting ISO to a Forgejo pre-release.
# A matching GitHub release is created that points to Forgejo for the download
# (GitHub releases cannot host files larger than 2 GB).
#
# Required secrets:
# RELEASE_TOKEN — Forgejo API token with write:repository scope
# MIRROR_TOKEN — GitHub personal access token with repo scope (already used by mirror.yml)
on:
push:
tags: ['v*']
workflow_dispatch:
inputs:
tag:
description: 'Git tag to build (e.g. v0.4.0)'
required: true
jobs:
release-iso:
runs-on: [self-hosted, hestia]
container:
image: archlinux:latest
# --privileged: mkarchiso needs CAP_SYS_ADMIN for loop mounts + mknod
# --network=host: gives localhost:3002 access to Forgejo (avoids the
# public git.breadway.dev → Aegis → Tailscale round-trip for pacman)
options: --privileged --network=host
steps:
- name: Install build dependencies
run: |
pacman -Syu --noconfirm archiso curl python git rust
- name: Determine tag and version
id: vars
run: |
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
TAG="${{ github.event.inputs.tag }}"
else
TAG="${{ github.ref_name }}"
fi
echo "tag=$TAG" >> "$GITHUB_OUTPUT"
echo "version=${TAG#v}" >> "$GITHUB_OUTPUT"
- name: Clone repository at tag
run: |
git clone --branch "${{ steps.vars.outputs.tag }}" --depth 1 \
"https://git.breadway.dev/${GITHUB_REPOSITORY}.git" /bos
- name: Download bakery ecosystem binaries
run: |
set -euo pipefail
mkdir -p /build-home/.local/bin \
/build-home/.local/state/bakery \
/build-home/.cache/bakery
# Fetch the canonical bakery index
curl -fsSL "https://dl.breadway.dev/index.json" \
-o /build-home/.cache/bakery/index.json
# Download each binary from its pinned GitHub release URL and
# generate the installed.json that bakery expects in ~/.local/state.
python3 << 'PYEOF'
import json, urllib.request, os
with open('/build-home/.cache/bakery/index.json') as f:
idx = json.load(f)
BIN_DIR = '/build-home/.local/bin'
installed = {}
for pkg_name, pkg in idx['packages'].items():
bins = []
for b in pkg['binaries']:
dest_name = b['name'].removesuffix('-x86_64')
dest = os.path.join(BIN_DIR, dest_name)
print(f' {dest_name} <- {b["github_url"]}', flush=True)
urllib.request.urlretrieve(b['github_url'], dest)
os.chmod(dest, 0o755)
bins.append(dest_name)
# installed.json services field is a flat list of unit-name strings
services = [
(s['unit'] if isinstance(s, dict) else s)
for s in pkg.get('services', [])
]
installed[pkg_name] = {
'name': pkg_name,
'version': pkg['version'],
'binaries': bins,
'services': services,
'installed_at': '2024-01-01T00:00:00+00:00',
}
with open('/build-home/.local/state/bakery/installed.json', 'w') as f:
json.dump({'packages': installed}, f, indent=2)
print('installed.json written', flush=True)
PYEOF
- name: Build bread-theme from source
run: |
set -euo pipefail
# bread-theme is not in the bakery index; build it at the tag pinned
# in bos-settings/Cargo.toml so the CLI matches the library version.
THEME_TAG=$(grep 'bread-theme.*tag' /bos/bos-settings/Cargo.toml \
| grep -oP '"v[^"]+"' | tr -d '"')
echo "Building bread-theme @ $THEME_TAG"
git clone --branch "$THEME_TAG" --depth 1 \
https://github.com/Breadway/bread-ecosystem /bread-ecosystem
cd /bread-ecosystem
cargo build --release -p bread-theme
install -m 755 target/release/bread-theme /build-home/.local/bin/bread-theme
echo "bread-theme built OK"
- name: Build ISO
run: |
set -euo pipefail
mkdir -p /bos-work /bos-out
cd /bos
LAPTOP_HOME=/build-home \
WORK=/bos-work \
OUT=/bos-out \
CI_BUILD=1 \
bash build-local.sh
ls -lh /bos-out/*.iso
- name: Create Forgejo release and upload ISO
env:
FORGEJO_TOKEN: ${{ secrets.RELEASE_TOKEN }}
run: |
set -euo pipefail
TAG="${{ steps.vars.outputs.tag }}"
VERSION="${{ steps.vars.outputs.version }}"
ISO=$(ls /bos-out/*.iso | head -1)
ISO_NAME="bos-${VERSION}-x86_64.iso"
# Use an existing release for this tag if one exists (e.g. created
# manually or by a prior re-run), otherwise create a fresh one.
EXISTING=$(curl -sf \
-H "Authorization: token ${FORGEJO_TOKEN}" \
"http://localhost:3002/api/v1/repos/${GITHUB_REPOSITORY}/releases/tags/${TAG}" \
2>/dev/null || true)
RELEASE_ID=$(echo "${EXISTING}" | python3 -c \
"import json,sys; d=json.load(sys.stdin); print(d.get('id',''))" 2>/dev/null || true)
if [ -z "${RELEASE_ID}" ]; then
RELEASE=$(curl -fsS -X POST \
-H "Authorization: token ${FORGEJO_TOKEN}" \
-H "Content-Type: application/json" \
"http://localhost:3002/api/v1/repos/${GITHUB_REPOSITORY}/releases" \
-d "{
\"tag_name\": \"${TAG}\",
\"name\": \"BOS ${TAG}\",
\"prerelease\": true,
\"body\": \"ISO image attached below.\\n\\nSee the [README](https://github.com/Breadway/bos#testing-in-a-vm) for VM testing instructions.\"
}")
RELEASE_ID=$(echo "${RELEASE}" | python3 -c "import json,sys; print(json.load(sys.stdin)['id'])")
fi
echo "Using release ID: ${RELEASE_ID}"
# Remove any existing asset with the same name before uploading
ASSET_ID=$(curl -sf \
-H "Authorization: token ${FORGEJO_TOKEN}" \
"http://localhost:3002/api/v1/repos/${GITHUB_REPOSITORY}/releases/${RELEASE_ID}/assets" \
| python3 -c "
import json,sys
assets=json.load(sys.stdin)
match=[a['id'] for a in assets if a['name']=='${ISO_NAME}']
print(match[0] if match else '')
" 2>/dev/null || true)
if [ -n "${ASSET_ID}" ]; then
curl -fsS -X DELETE \
-H "Authorization: token ${FORGEJO_TOKEN}" \
"http://localhost:3002/api/v1/repos/${GITHUB_REPOSITORY}/releases/${RELEASE_ID}/assets/${ASSET_ID}"
echo "Removed existing ${ISO_NAME} asset"
fi
curl -fsS -X POST \
-H "Authorization: token ${FORGEJO_TOKEN}" \
-F "attachment=@${ISO};filename=${ISO_NAME}" \
"http://localhost:3002/api/v1/repos/${GITHUB_REPOSITORY}/releases/${RELEASE_ID}/assets"
echo "Uploaded: ${ISO_NAME}"
- name: Create GitHub release
env:
GH_TOKEN: ${{ secrets.MIRROR_TOKEN }}
run: |
set -euo pipefail
TAG="${{ steps.vars.outputs.tag }}"
VERSION="${{ steps.vars.outputs.version }}"
FORGEJO_URL="https://git.breadway.dev/${GITHUB_REPOSITORY}/releases/tag/${TAG}"
gh release create "${TAG}" \
--repo "Breadway/bos" \
--title "BOS ${TAG}" \
--prerelease \
--notes "**Download ISO:** ${FORGEJO_URL}
GitHub releases cannot host files larger than 2 GB; the \`bos-${VERSION}-x86_64.iso\` (≈2.5 GB) is attached to the Forgejo release linked above.
See the [README](https://github.com/Breadway/bos#testing-in-a-vm) for VM testing instructions and [known limitations](https://github.com/Breadway/bos#known-limitations)." \
2>/dev/null || echo "GitHub release already exists — skipping"

4
.gitignore vendored
View file

@ -27,6 +27,7 @@ secrets/
# archiso build artifacts (these are large and reproducible) # archiso build artifacts (these are large and reproducible)
/iso-build/ /iso-build/
/iso-out/ /iso-out/
/out/
*.iso *.iso
*.img *.img
@ -38,3 +39,6 @@ logs/
# Claude Code local agent state # Claude Code local agent state
.claude/ .claude/
# Wallpaper source drop (baked copy lives in airootfs/usr/share/backgrounds)
/Bread Background.png

278
Cargo.lock generated
View file

@ -28,21 +28,34 @@ checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8"
[[package]] [[package]]
name = "bos-settings" name = "bos-settings"
version = "0.1.0" version = "0.4.0"
dependencies = [ dependencies = [
"async-channel", "async-channel",
"bread-theme",
"glib", "glib",
"gtk4", "gtk4",
"serde", "serde",
"serde_json", "serde_json",
"toml 0.8.23", "toml 0.8.23",
"toml_edit 0.22.27",
]
[[package]]
name = "bread-theme"
version = "0.2.3"
source = "git+https://github.com/Breadway/bread-ecosystem?tag=v0.2.8#77417d552130281ff787e07d52541eb25e9d533b"
dependencies = [
"dirs",
"gtk4",
"serde",
"serde_json",
] ]
[[package]] [[package]]
name = "cairo-rs" name = "cairo-rs"
version = "0.20.12" version = "0.22.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "91e3bd0f4e25afa9cabc157908d14eeef9067d6448c49414d17b3fb55f0eadd0" checksum = "5cc8d9aa793480744cd9a0524fef1a2e197d9eaa0f739cde19d16aba530dcb95"
dependencies = [ dependencies = [
"bitflags", "bitflags",
"cairo-sys-rs", "cairo-sys-rs",
@ -52,9 +65,9 @@ dependencies = [
[[package]] [[package]]
name = "cairo-sys-rs" name = "cairo-sys-rs"
version = "0.20.10" version = "0.22.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "059cc746549898cbfd9a47754288e5a958756650ef4652bbb6c5f71a6bda4f8b" checksum = "f8b4985713047f5faee02b8db6a6ef32bbb50269ff53c1aee716d1d195b76d54"
dependencies = [ dependencies = [
"glib-sys", "glib-sys",
"libc", "libc",
@ -71,6 +84,12 @@ dependencies = [
"target-lexicon", "target-lexicon",
] ]
[[package]]
name = "cfg-if"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]] [[package]]
name = "concurrent-queue" name = "concurrent-queue"
version = "2.5.0" version = "2.5.0"
@ -86,6 +105,27 @@ version = "0.8.21"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
[[package]]
name = "dirs"
version = "5.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225"
dependencies = [
"dirs-sys",
]
[[package]]
name = "dirs-sys"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c"
dependencies = [
"libc",
"option-ext",
"redox_users",
"windows-sys 0.48.0",
]
[[package]] [[package]]
name = "equivalent" name = "equivalent"
version = "1.0.2" version = "1.0.2"
@ -187,9 +227,9 @@ dependencies = [
[[package]] [[package]]
name = "gdk-pixbuf" name = "gdk-pixbuf"
version = "0.20.10" version = "0.22.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2fd242894c084f4beed508a56952750bce3e96e85eb68fdc153637daa163e10c" checksum = "25f420376dbee041b2db374ce4573892a36222bb3f6c0c43e24f0d67eae9b646"
dependencies = [ dependencies = [
"gdk-pixbuf-sys", "gdk-pixbuf-sys",
"gio", "gio",
@ -199,9 +239,9 @@ dependencies = [
[[package]] [[package]]
name = "gdk-pixbuf-sys" name = "gdk-pixbuf-sys"
version = "0.20.10" version = "0.22.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b34f3b580c988bd217e9543a2de59823fafae369d1a055555e5f95a8b130b96" checksum = "48f31b37b1fc4b48b54f6b91b7ef04c18e00b4585d98359dd7b998774bbd91fb"
dependencies = [ dependencies = [
"gio-sys", "gio-sys",
"glib-sys", "glib-sys",
@ -212,9 +252,9 @@ dependencies = [
[[package]] [[package]]
name = "gdk4" name = "gdk4"
version = "0.9.6" version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4850c9d9c1aecd1a3eb14fadc1cdb0ac0a2298037e116264c7473e1740a32d60" checksum = "fd42fdbbf48612c6e8f47c65fb92d2e8f39c25aecd6af047e83897c1a22d2a4e"
dependencies = [ dependencies = [
"cairo-rs", "cairo-rs",
"gdk-pixbuf", "gdk-pixbuf",
@ -227,9 +267,9 @@ dependencies = [
[[package]] [[package]]
name = "gdk4-sys" name = "gdk4-sys"
version = "0.9.6" version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6f6eb95798e2b46f279cf59005daf297d5b69555428f185650d71974a910473a" checksum = "9d974ac4f15e67472c3a9728daf612590b4a5762a4b33f0edd298df0b80d043c"
dependencies = [ dependencies = [
"cairo-sys-rs", "cairo-sys-rs",
"gdk-pixbuf-sys", "gdk-pixbuf-sys",
@ -243,10 +283,21 @@ dependencies = [
] ]
[[package]] [[package]]
name = "gio" name = "getrandom"
version = "0.20.12" version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e27e276e7b6b8d50f6376ee7769a71133e80d093bdc363bd0af71664228b831" checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
dependencies = [
"cfg-if",
"libc",
"wasi",
]
[[package]]
name = "gio"
version = "0.22.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3848bcba3a35cc0a71df8ba8ecfd799d6bfb862342a53a4a915fb62213aa4e6"
dependencies = [ dependencies = [
"futures-channel", "futures-channel",
"futures-core", "futures-core",
@ -261,22 +312,22 @@ dependencies = [
[[package]] [[package]]
name = "gio-sys" name = "gio-sys"
version = "0.20.10" version = "0.22.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "521e93a7e56fc89e84aea9a52cfc9436816a4b363b030260b699950ff1336c83" checksum = "64729ba2772c080448f9f966dba8f4456beeb100d8c28a865ef8a0f2ef4987e1"
dependencies = [ dependencies = [
"glib-sys", "glib-sys",
"gobject-sys", "gobject-sys",
"libc", "libc",
"system-deps", "system-deps",
"windows-sys", "windows-sys 0.59.0",
] ]
[[package]] [[package]]
name = "glib" name = "glib"
version = "0.20.12" version = "0.22.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ffc4b6e352d4716d84d7dde562dd9aee2a7d48beb872dd9ece7f2d1515b2d683" checksum = "c207e04e51605dcf7b2924c41591b3a10e1438eaac5bcf448fb91f325381104a"
dependencies = [ dependencies = [
"bitflags", "bitflags",
"futures-channel", "futures-channel",
@ -295,12 +346,11 @@ dependencies = [
[[package]] [[package]]
name = "glib-macros" name = "glib-macros"
version = "0.20.12" version = "0.22.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8084af62f09475a3f529b1629c10c429d7600ee1398ae12dd3bf175d74e7145" checksum = "506d23499707c7142898429757e8d9a3871d965239a2cb66dfa05052be6d6f19"
dependencies = [ dependencies = [
"heck", "heck",
"proc-macro-crate",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "syn",
@ -308,9 +358,9 @@ dependencies = [
[[package]] [[package]]
name = "glib-sys" name = "glib-sys"
version = "0.20.10" version = "0.22.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ab79e1ed126803a8fb827e3de0e2ff95191912b8db65cee467edb56fc4cc215" checksum = "5f7fbac234ed5bc2a28359b7bde8e1b9cdf1441cc2d7f068e4824672d7db9445"
dependencies = [ dependencies = [
"libc", "libc",
"system-deps", "system-deps",
@ -318,9 +368,9 @@ dependencies = [
[[package]] [[package]]
name = "gobject-sys" name = "gobject-sys"
version = "0.20.10" version = "0.22.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec9aca94bb73989e3cfdbf8f2e0f1f6da04db4d291c431f444838925c4c63eda" checksum = "22a861859b887a79cf461359c192c97a57d8fb0229dd291232e57aa11f6fa72c"
dependencies = [ dependencies = [
"glib-sys", "glib-sys",
"libc", "libc",
@ -329,9 +379,9 @@ dependencies = [
[[package]] [[package]]
name = "graphene-rs" name = "graphene-rs"
version = "0.20.10" version = "0.22.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6b86dfad7d14251c9acaf1de63bc8754b7e3b4e5b16777b6f5a748208fe9519b" checksum = "c7d1b7881f96869f49808b6adfe906a93a57a34204952253444d68c3208d71f1"
dependencies = [ dependencies = [
"glib", "glib",
"graphene-sys", "graphene-sys",
@ -340,9 +390,9 @@ dependencies = [
[[package]] [[package]]
name = "graphene-sys" name = "graphene-sys"
version = "0.20.10" version = "0.22.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df583a85ba2d5e15e1797e40d666057b28bc2f60a67c9c24145e6db2cc3861ea" checksum = "517f062f3fd6b7fd3e57a3f038a74b3c23ca32f51199ff028aa704609943f79c"
dependencies = [ dependencies = [
"glib-sys", "glib-sys",
"libc", "libc",
@ -352,9 +402,9 @@ dependencies = [
[[package]] [[package]]
name = "gsk4" name = "gsk4"
version = "0.9.6" version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "61f5e72f931c8c9f65fbfc89fe0ddc7746f147f822f127a53a9854666ac1f855" checksum = "53c912dfcbd28acace5fc99c40bb9f25e1dcb73efb1f2608327f66a99acdcb62"
dependencies = [ dependencies = [
"cairo-rs", "cairo-rs",
"gdk4", "gdk4",
@ -367,9 +417,9 @@ dependencies = [
[[package]] [[package]]
name = "gsk4-sys" name = "gsk4-sys"
version = "0.9.6" version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "755059de55fa6f85a46bde8caf03e2184c96bfda1f6206163c72fb0ea12436dc" checksum = "d7d54bbc7a9d8b6ffe4f0c95eede15ccfb365c8bf521275abe6bcfb57b18fb8a"
dependencies = [ dependencies = [
"cairo-sys-rs", "cairo-sys-rs",
"gdk4-sys", "gdk4-sys",
@ -383,9 +433,9 @@ dependencies = [
[[package]] [[package]]
name = "gtk4" name = "gtk4"
version = "0.9.7" version = "0.11.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f274dd0102c21c47bbfa8ebcb92d0464fab794a22fad6c3f3d5f165139a326d6" checksum = "7181b837f04cbe93f79441475f7a00560a92cba7a72e38cc1a68b6f8b78eaae2"
dependencies = [ dependencies = [
"cairo-rs", "cairo-rs",
"field-offset", "field-offset",
@ -404,9 +454,9 @@ dependencies = [
[[package]] [[package]]
name = "gtk4-macros" name = "gtk4-macros"
version = "0.9.5" version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ed1786c4703dd196baf7e103525ce0cf579b3a63a0570fe653b7ee6bac33999" checksum = "3581b242ba62fdff122ebb626ea641582ec326031622bd19d60f85029c804a87"
dependencies = [ dependencies = [
"proc-macro-crate", "proc-macro-crate",
"proc-macro2", "proc-macro2",
@ -416,9 +466,9 @@ dependencies = [
[[package]] [[package]]
name = "gtk4-sys" name = "gtk4-sys"
version = "0.9.6" version = "0.11.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41e03b01e54d77c310e1d98647d73f996d04b2f29b9121fe493ea525a7ec03d6" checksum = "20ba8e695e2640455561274e65e45f0a151619e450746007667f4b23ceae4e1b"
dependencies = [ dependencies = [
"cairo-sys-rs", "cairo-sys-rs",
"gdk-pixbuf-sys", "gdk-pixbuf-sys",
@ -467,6 +517,15 @@ version = "0.2.186"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
[[package]]
name = "libredox"
version = "0.1.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f02ab6bace2054fb888a3c16f990117b579d14a3088e472d63c6011fa185c9d3"
dependencies = [
"libc",
]
[[package]] [[package]]
name = "memchr" name = "memchr"
version = "2.8.2" version = "2.8.2"
@ -483,10 +542,16 @@ dependencies = [
] ]
[[package]] [[package]]
name = "pango" name = "option-ext"
version = "0.20.12" version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6576b311f6df659397043a5fa8a021da8f72e34af180b44f7d57348de691ab5c" checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
[[package]]
name = "pango"
version = "0.22.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "251bdc6e6487b811be0e406a21e301e07e45c0aa8fa39e00c0c8e12a91752438"
dependencies = [ dependencies = [
"gio", "gio",
"glib", "glib",
@ -496,9 +561,9 @@ dependencies = [
[[package]] [[package]]
name = "pango-sys" name = "pango-sys"
version = "0.20.10" version = "0.22.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "186909673fc09be354555c302c0b3dcf753cd9fa08dcb8077fa663c80fb243fa" checksum = "bbd111a20ca90fedf03e09c59783c679c00900f1d8491cca5399f5e33609d5d6"
dependencies = [ dependencies = [
"glib-sys", "glib-sys",
"gobject-sys", "gobject-sys",
@ -551,6 +616,17 @@ dependencies = [
"proc-macro2", "proc-macro2",
] ]
[[package]]
name = "redox_users"
version = "0.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43"
dependencies = [
"getrandom",
"libredox",
"thiserror",
]
[[package]] [[package]]
name = "rustc_version" name = "rustc_version"
version = "0.4.1" version = "0.4.1"
@ -669,6 +745,26 @@ version = "0.13.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca" checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca"
[[package]]
name = "thiserror"
version = "1.0.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]] [[package]]
name = "toml" name = "toml"
version = "0.8.23" version = "0.8.23"
@ -773,13 +869,43 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "03c2856837ef78f57382f06b2b8563a2f512f7185d732608fd9176cb3b8edf0e" checksum = "03c2856837ef78f57382f06b2b8563a2f512f7185d732608fd9176cb3b8edf0e"
[[package]]
name = "wasi"
version = "0.11.1+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
[[package]]
name = "windows-sys"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
dependencies = [
"windows-targets 0.48.5",
]
[[package]] [[package]]
name = "windows-sys" name = "windows-sys"
version = "0.59.0" version = "0.59.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
dependencies = [ dependencies = [
"windows-targets", "windows-targets 0.52.6",
]
[[package]]
name = "windows-targets"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
dependencies = [
"windows_aarch64_gnullvm 0.48.5",
"windows_aarch64_msvc 0.48.5",
"windows_i686_gnu 0.48.5",
"windows_i686_msvc 0.48.5",
"windows_x86_64_gnu 0.48.5",
"windows_x86_64_gnullvm 0.48.5",
"windows_x86_64_msvc 0.48.5",
] ]
[[package]] [[package]]
@ -788,28 +914,46 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
dependencies = [ dependencies = [
"windows_aarch64_gnullvm", "windows_aarch64_gnullvm 0.52.6",
"windows_aarch64_msvc", "windows_aarch64_msvc 0.52.6",
"windows_i686_gnu", "windows_i686_gnu 0.52.6",
"windows_i686_gnullvm", "windows_i686_gnullvm",
"windows_i686_msvc", "windows_i686_msvc 0.52.6",
"windows_x86_64_gnu", "windows_x86_64_gnu 0.52.6",
"windows_x86_64_gnullvm", "windows_x86_64_gnullvm 0.52.6",
"windows_x86_64_msvc", "windows_x86_64_msvc 0.52.6",
] ]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
[[package]] [[package]]
name = "windows_aarch64_gnullvm" name = "windows_aarch64_gnullvm"
version = "0.52.6" version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
[[package]]
name = "windows_aarch64_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
[[package]] [[package]]
name = "windows_aarch64_msvc" name = "windows_aarch64_msvc"
version = "0.52.6" version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
[[package]]
name = "windows_i686_gnu"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
[[package]] [[package]]
name = "windows_i686_gnu" name = "windows_i686_gnu"
version = "0.52.6" version = "0.52.6"
@ -822,24 +966,48 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
[[package]]
name = "windows_i686_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
[[package]] [[package]]
name = "windows_i686_msvc" name = "windows_i686_msvc"
version = "0.52.6" version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
[[package]]
name = "windows_x86_64_gnu"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
[[package]] [[package]]
name = "windows_x86_64_gnu" name = "windows_x86_64_gnu"
version = "0.52.6" version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
[[package]] [[package]]
name = "windows_x86_64_gnullvm" name = "windows_x86_64_gnullvm"
version = "0.52.6" version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
[[package]]
name = "windows_x86_64_msvc"
version = "0.48.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
[[package]] [[package]]
name = "windows_x86_64_msvc" name = "windows_x86_64_msvc"
version = "0.52.6" version = "0.52.6"

209
README.md Normal file
View file

@ -0,0 +1,209 @@
# BOS — Bread Operating System
An Arch-based, Hyprland desktop distribution that ships the [bread
ecosystem](https://github.com/Breadway) preconfigured. One Calamares install
produces a themed, bootable Wayland desktop — no manual Arch bootstrap, no
wiring up dotfiles, no per-tool bakery installs.
> Design rationale and the btrfs/A-B roadmap live in [DESIGN.md](DESIGN.md).
> This file is the practical overview: what's in the image, how to build it,
> and how to test it.
## What you get
- **Compositor**: Hyprland with a native-Lua config (`hyprland.lua`), curated
keybinds, snappy animations, blur, and pywal-driven colours on a black base.
- **bread ecosystem**, baked into `/etc/skel` from bakery-managed binaries
(no network needed at install time): `bread`/`breadd`, `breadbar` (status bar
+ notification daemon), `breadbox` (launcher), `breadcrumbs` (Wi-Fi profiles),
`breadpad` (notes/reminders), `breadman`, and the `bakery` package manager.
- **bos-settings**: a GTK4 control panel that configures every bread\* app's
config from a GUI (non-destructively), plus snapshot rollback and bakery
updates. See below.
- **Login**: greetd + tuigreet → Hyprland session.
- **Boot splash**: Plymouth `bos` theme (logo + spinner, black background).
- **Theming**: global dark across GTK3 (Adwaita-dark), GTK4/libadwaita
(`color-scheme: prefer-dark`), and Qt (qt5ct/qt6ct Fusion dark); Papirus-Dark
icons; Bibata cursor.
- **Apps**: kitty, nautilus (+ gvfs), Zen browser, VLC, loupe, gnome-text-editor,
gnome-calculator, file-roller, with file associations wired in `mimeapps.list`.
- **Hardware**: pipewire audio, NetworkManager, BlueZ + blueman, CUPS printing
with avahi mDNS discovery, TLP power management, fwupd firmware updates.
- **Resilience**: btrfs + snapper + snap-pac + grub-btrfs snapshots on every
pacman transaction; zram swap; ufw firewall (deny-incoming, mDNS allowed).
## Repo layout
```
bos/
├── Cargo.toml # workspace (members: bos-settings)
├── bos-settings/ # GTK4 unified settings app (Rust)
│ └── src/
│ ├── config/mod.rs # non-destructive toml_edit config layer
│ └── ui/{widgets,window,sidebar}.rs, ui/views/*.rs
├── iso/ # archiso profile
│ ├── profiledef.sh
│ ├── packages.x86_64 # live + installed package set
│ └── airootfs/ # files overlaid onto the image
│ └── etc/
│ ├── skel/ # default user dotfiles (hypr, kitty, gtk, …)
│ └── calamares/ # installer config + post-install.sh
├── packaging/ # in-house PKGBUILDs for AUR-only deps
│ ├── arch/ # bos-settings
│ ├── calamares/
│ └── bibata/
├── .forgejo/workflows/ # CI: build + publish packages to [breadway]
├── build-local.sh # native ISO build for this machine
└── DESIGN.md
```
## Building the ISO
`build-local.sh` builds the image natively (no container) and bakes this
machine's bakery-installed bread binaries into `/etc/skel`:
```sh
sudo ./build-local.sh # release-quality (xz squashfs)
sudo FAST_BUILD=1 ./build-local.sh # fast dev iteration (zstd squashfs)
```
The ISO lands in `out/bos-<date>-x86_64.iso`. The script pins
`SOURCE_DATE_EPOCH` (reproducible UUIDs) and rewrites the `[breadway]` repo URL
to the Tailscale-reachable Forgejo registry for the build.
### Why some packages are in-house
`calamares`, `zen-browser-bin`, and `bibata-cursor-theme` are AUR-only. BOS
keeps a PKGBUILD for each under `packaging/` and republishes the built package
to the `[breadway]` repo via a Forgejo Actions workflow (built on the hestia
self-hosted runner, published with a scoped registry token). `bos-settings`
itself publishes the same way on a `v*` tag.
## Testing in a VM
A reusable, GPU-accelerated launcher lives at `~/bos-vm/run.sh`:
```sh
~/bos-vm/run.sh install # boot the ISO installer (target disk attached)
~/bos-vm/run.sh # boot the installed system from the disk
```
It uses KVM + `-cpu host`, 8 GiB / 8 vCPU, and `virtio-vga-gl` with
`-display gtk,gl=on` (virgl) — 3D acceleration is essential for a smooth
Hyprland session in QEMU. The disk lives on NVMe (not the tmpfs `/tmp`) to
avoid memory pressure.
## bos-settings
`bos-settings` edits each bread\* app's TOML **non-destructively**: it parses
the file with `toml_edit`, changes only the keys a view exposes, and writes it
back — preserving comments and any keys the UI doesn't model (calendar
passwords, saved-network passwords, model paths). Views:
| View | Config |
|------|--------|
| bread | `bread/breadd.toml` — daemon, lua, modules, all adapters, events, notifications |
| breadbar | `breadbar/style.css` override |
| breadbox | `breadbox/config.toml` — launcher contexts |
| breadcrumbs | `breadcrumbs/breadcrumbs.toml` — settings, saved networks, profiles |
| breadpad | `breadpad/breadpad.toml` — settings, model + ollama, reminders, calendar |
| Snapshots | `snapper` list / rollback / delete |
| Packages | `bakery` installed list + updates |
| Hyprland | open config in editor + monitor list |
Build standalone:
```sh
cargo build --release -p bos-settings
cargo test -p bos-settings # includes config round-trip tests
```
## The bread ecosystem at a glance
| Tool | Role | Launch |
|------|------|--------|
| `bread` / `breadd` | Reactive automation daemon — normalises hardware/compositor signals into events dispatched to Lua modules | runs at login |
| `breadbar` | Top status bar (workspaces, clock, stats, tray) **and** the notification daemon | runs at login |
| `breadbox` | Application launcher | `SUPER+Space` |
| `breadpad` | Notes & reminders (AI-classified, optional CalDAV sync) | `SUPER+U` |
| `breadman` | Package-manager UI | `SUPER+M` |
| `breadcrumbs` | Wi-Fi profile state machine (location-aware) | CLI / BOS Settings |
| `bakery` | CLI package manager for the ecosystem | `bakery` |
| `bos-settings` | Unified GTK4 control panel for all of the above + snapshots + updates | `SUPER+,` |
## Keyboard shortcuts
`SUPER` is the Windows/Cmd key. Press **`SUPER+/`** at any time for this
cheatsheet in-session; first boot shows a short welcome (once).
| Keys | Action |
|------|--------|
| `SUPER+Return` | Terminal (kitty) |
| `SUPER+Space` | App launcher (breadbox) |
| `SUPER+E` / `SUPER+B` | Files (nautilus) / Browser (zen) |
| `SUPER+U` / `SUPER+M` | breadpad / breadman |
| `SUPER+,` / `SUPER+/` | BOS Settings / keybind cheatsheet |
| `SUPER+L` / `SUPER+N` | Lock / log out |
| `SUPER+Backspace` | Close window |
| `SUPER+F` / `SUPER+V` / `SUPER+T` | Fullscreen / float / toggle split |
| `SUPER+Shift+V` | Clipboard history |
| `SUPER+Tab` | Last window |
| `SUPER+Shift+S/C/P` | Screenshot region→file / region→clipboard / screen→file |
| `SUPER+arrows` | Move focus |
| `SUPER+Shift+h/j/k/l` | Move window |
| `SUPER+Shift+arrows` | Resize window |
| `SUPER+1..0` | Switch to workspace 110 |
| `SUPER+Shift+1..0` | Move window to workspace |
| `SUPER+[ / ]` | Previous / next workspace |
| `SUPER+left/right-drag` | Move / resize window with the mouse |
## Known limitations
- **GPUs**: ships the generic Mesa stack — AMD and Intel work out of the box.
The **NVIDIA proprietary driver is not included**; NVIDIA users must install
`nvidia`/`nvidia-utils` and set the usual Hyprland env vars after install.
- **Virtual machines**: Hyprland needs GPU acceleration to be smooth. Use
`virtio-vga-gl` + `-display gtk,gl=on` (virgl); plain software rendering is
noticeably laggy.
- **Wayland-first**: X11-only apps run through XWayland; a few may misbehave.
- **Secure Boot**: not configured. Boot with Secure Boot disabled, or enroll
your own keys. The installer writes both an NVRAM entry and the removable
`EFI/BOOT/BOOTX64.EFI` fallback.
- **Snapshots assume btrfs**: the snapper/grub-btrfs tooling expects the default
btrfs subvolume layout the installer creates.
## Recovery
**An update broke something (system still boots):** open BOS Settings →
Snapshots and roll back, or pick a pre-update snapshot from the **GRUB
“snapshots” submenu** at boot, then run `snapper rollback` from the booted
snapshot.
**The system won't boot (broken GRUB / lost EFI entry):**
1. Boot the BOS ISO and open a terminal (`SUPER+Return`).
2. Mount the installed root and EFI, then chroot:
```sh
mount -o subvol=@ /dev/sdXN /mnt
mount /dev/sdXP /mnt/boot/efi # the EFI partition
arch-chroot /mnt
```
3. Reinstall the bootloader (the same sequence the installer uses):
```sh
grub-install --target=x86_64-efi --efi-directory=/boot/efi --bootloader-id=BOS --recheck
grub-install --target=x86_64-efi --efi-directory=/boot/efi --removable --recheck
grub-mkconfig -o /boot/grub/grub.cfg
```
**Firmware shows “no boot device”:** select `EFI/BOOT/BOOTX64.EFI` from the
firmware boot menu — the installer always writes that removable fallback.
## Boot architecture notes
archiso keeps the kernel and initramfs outside the squashfs, so the installer
stages them explicitly: a `shellprocess@kernel` step copies the kernel + ucode
into the target `/boot` and writes a stock mkinitcpio preset before the native
`initcpio` module builds the initramfs. GRUB is **not** installed by Calamares'
`bootloader`/`grubcfg` modules (they leave the ESP empty in this layout) —
`post-install.sh` runs `grub-install` (NVRAM **and** `--removable`) +
`grub-mkconfig` instead, which is the sequence verified to boot.

BIN
assets/Icon 1024x1024.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

BIN
assets/Icon 256x256.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

BIN
assets/Icon 512x512.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

View file

Before

Width:  |  Height:  |  Size: 4 KiB

After

Width:  |  Height:  |  Size: 4 KiB

Before After
Before After

View file

@ -1,12 +1,19 @@
[package] [package]
name = "bos-settings" name = "bos-settings"
version = "0.1.0" version = "0.4.0"
edition = "2021" edition = "2021"
[dependencies] [dependencies]
gtk4 = { version = "0.9", features = ["v4_12"] } gtk4 = { version = "0.11", features = ["v4_12"] }
glib = "0.20" glib = "0.22"
# Shared ecosystem theming — bos-settings loads the same generated stylesheet as
# breadbar/breadbox/breadpad so the whole desktop looks consistent.
bread-theme = { git = "https://github.com/Breadway/bread-ecosystem", tag = "v0.2.8", features = ["gtk"] }
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }
serde_json = "1" serde_json = "1"
toml = "0.8" toml = "0.8"
# toml_edit drives non-destructive config editing: it preserves comments and
# any keys the UI doesn't model, so saving a single field never rewrites or
# drops the rest of the user's config file.
toml_edit = "0.22"
async-channel = "2" async-channel = "2"

View file

@ -1,16 +1,32 @@
//! Non-destructive config editing.
//!
//! Every bread* app owns a TOML config that may contain keys, sections, and
//! comments this settings app does not model (e.g. breadpad's calendar
//! credentials, breadcrumbs' saved-network passwords). To edit safely we parse
//! the file into a `toml_edit::DocumentMut`, mutate only the specific keys the
//! UI exposes, and write the document back — preserving everything else,
//! formatting and comments included.
use std::error::Error; use std::error::Error;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
pub fn load<T: for<'de> serde::Deserialize<'de>>(path: &Path) -> Result<T, Box<dyn Error>> { use toml_edit::{value, Array, DocumentMut, Item, Table, Value};
let text = std::fs::read_to_string(path)?;
Ok(toml::from_str(&text)?) /// Load a TOML file into an editable document. A missing or unparseable file
/// yields an empty document so the UI still renders (with defaults).
pub fn load_doc(path: &Path) -> DocumentMut {
std::fs::read_to_string(path)
.ok()
.and_then(|s| s.parse::<DocumentMut>().ok())
.unwrap_or_default()
} }
pub fn save<T: serde::Serialize>(path: &Path, val: &T) -> Result<(), Box<dyn Error>> { /// Write the document back to disk, creating parent dirs as needed.
pub fn save_doc(path: &Path, doc: &DocumentMut) -> Result<(), Box<dyn Error>> {
if let Some(parent) = path.parent() { if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)?; std::fs::create_dir_all(parent)?;
} }
std::fs::write(path, toml::to_string_pretty(val)?)?; std::fs::write(path, doc.to_string())?;
Ok(()) Ok(())
} }
@ -25,3 +41,155 @@ pub fn config_dir() -> PathBuf {
let home = std::env::var("HOME").unwrap_or_else(|_| "/root".to_string()); let home = std::env::var("HOME").unwrap_or_else(|_| "/root".to_string());
PathBuf::from(home).join(".config") PathBuf::from(home).join(".config")
} }
// --- typed readers (walk a dotted path, return None if absent/wrong type) ---
fn get<'a>(doc: &'a DocumentMut, path: &[&str]) -> Option<&'a Item> {
let mut tbl = doc.as_table();
let (last, parents) = path.split_last()?;
for key in parents {
tbl = tbl.get(key)?.as_table()?;
}
tbl.get(last)
}
pub fn get_bool(doc: &DocumentMut, path: &[&str]) -> Option<bool> {
get(doc, path)?.as_bool()
}
pub fn get_str(doc: &DocumentMut, path: &[&str]) -> Option<String> {
get(doc, path)?.as_str().map(str::to_string)
}
pub fn get_i64(doc: &DocumentMut, path: &[&str]) -> Option<i64> {
get(doc, path)?.as_integer()
}
pub fn get_f64(doc: &DocumentMut, path: &[&str]) -> Option<f64> {
let item = get(doc, path)?;
item.as_float().or_else(|| item.as_integer().map(|i| i as f64))
}
/// Read an array of strings (e.g. modules.disable, contexts[].priority).
pub fn get_str_list(doc: &DocumentMut, path: &[&str]) -> Vec<String> {
match get(doc, path).and_then(Item::as_array) {
Some(arr) => arr
.iter()
.filter_map(|v| v.as_str().map(str::to_string))
.collect(),
None => Vec::new(),
}
}
// --- setters (auto-create intermediate tables, replace only the leaf) ---
fn table_at_mut<'a>(doc: &'a mut DocumentMut, parents: &[&str]) -> &'a mut Table {
let mut tbl = doc.as_table_mut();
for key in parents {
let entry = tbl.entry(key).or_insert_with(|| Item::Table(Table::new()));
if !entry.is_table() {
*entry = Item::Table(Table::new());
}
tbl = entry.as_table_mut().expect("just ensured table");
}
tbl
}
fn set_item(doc: &mut DocumentMut, path: &[&str], item: Item) {
let Some((last, parents)) = path.split_last() else {
return;
};
table_at_mut(doc, parents).insert(last, item);
}
pub fn set_bool(doc: &mut DocumentMut, path: &[&str], v: bool) {
set_item(doc, path, value(v));
}
pub fn set_str(doc: &mut DocumentMut, path: &[&str], v: &str) {
set_item(doc, path, value(v));
}
pub fn set_i64(doc: &mut DocumentMut, path: &[&str], v: i64) {
set_item(doc, path, value(v));
}
pub fn set_f64(doc: &mut DocumentMut, path: &[&str], v: f64) {
set_item(doc, path, value(v));
}
pub fn set_str_list(doc: &mut DocumentMut, path: &[&str], items: &[String]) {
let mut arr = Array::new();
for s in items {
arr.push(s.as_str());
}
set_item(doc, path, Item::Value(Value::Array(arr)));
}
/// Set a string key, or remove it entirely when the value is empty — keeps
/// optional fields out of the file rather than persisting `key = ""`.
pub fn set_str_or_remove(doc: &mut DocumentMut, path: &[&str], v: &str) {
if v.is_empty() {
remove(doc, path);
} else {
set_str(doc, path, v);
}
}
pub fn remove(doc: &mut DocumentMut, path: &[&str]) {
if let Some((last, parents)) = path.split_last() {
let mut tbl = doc.as_table_mut();
for key in parents {
match tbl.get_mut(key).and_then(Item::as_table_mut) {
Some(t) => tbl = t,
None => return,
}
}
tbl.remove(last);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn edits_preserve_unmodelled_keys_and_comments() {
let src = "\
# a leading comment
[daemon]
log_level = \"info\"
[calendar]
password = \"secret\" # keep me
";
let mut doc: DocumentMut = src.parse().unwrap();
// Modify a single modelled key.
set_str(&mut doc, &["daemon", "log_level"], "debug");
// A key/section the UI never touches must survive untouched.
let out = doc.to_string();
assert!(out.contains("log_level = \"debug\""));
assert!(out.contains("password = \"secret\""));
assert!(out.contains("# keep me"));
assert!(out.contains("# a leading comment"));
}
#[test]
fn setters_create_missing_tables() {
let mut doc = DocumentMut::new();
set_bool(&mut doc, &["adapters", "power", "enabled"], false);
set_i64(&mut doc, &["adapters", "power", "poll_interval_secs"], 45);
assert_eq!(get_bool(&doc, &["adapters", "power", "enabled"]), Some(false));
assert_eq!(
get_i64(&doc, &["adapters", "power", "poll_interval_secs"]),
Some(45)
);
}
#[test]
fn empty_string_removes_key() {
let mut doc: DocumentMut = "[calendar]\nurl = \"x\"\n".parse().unwrap();
set_str_or_remove(&mut doc, &["calendar", "url"], "");
assert_eq!(get_str(&doc, &["calendar", "url"]), None);
}
#[test]
fn str_list_roundtrips() {
let mut doc = DocumentMut::new();
let items = vec!["a".to_string(), "b".to_string()];
set_str_list(&mut doc, &["modules", "disable"], &items);
assert_eq!(get_str_list(&doc, &["modules", "disable"]), items);
}
}

View file

@ -1,87 +1,30 @@
//! Theming for bos-settings.
//!
//! bos-settings deliberately owns almost no styling: it loads the ecosystem's
//! shared stylesheet (the same one breadbar/breadbox/breadpad use, generated by
//! `bread-theme` from the pywal palette) and adds only the few layout rules
//! specific to this app's sidebar + content shell. This keeps it visually
//! identical to the rest of the bread desktop and live-recolouring for free.
use gtk4::CssProvider; use gtk4::CssProvider;
use std::cell::RefCell;
const CSS: &str = r#" // App-specific layout only — everything visual (colours, buttons, entries,
window { // switches, sidebar/row styling, cards, scrollbars) comes from the shared sheet.
background-color: #2e3440; const APP_CSS: &str = "\
color: #eceff4; .view-content { padding: 24px; }\n\
.view-content > label.title { margin-bottom: 16px; }\n\
";
thread_local! {
static APP_PROVIDER: RefCell<Option<CssProvider>> = const { RefCell::new(None) };
} }
.sidebar { pub fn load(_display: &gtk4::gdk::Display) {
background-color: #3b4252; // Shared ecosystem stylesheet (loads the generated file or a rendered
border-right: 1px solid #434c5e; // fallback, and live-reloads when the palette changes).
} bread_theme::gtk::apply_shared();
.sidebar row { // bos-settings layout, layered on top at APPLICATION priority.
padding: 8px 12px; APP_PROVIDER.with(|cell| bread_theme::gtk::apply_css(APP_CSS, cell));
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: &gtk4::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,
);
} }

View file

@ -1,3 +1,4 @@
pub mod sidebar; pub mod sidebar;
pub mod views; pub mod views;
pub mod widgets;
pub mod window; pub mod window;

View file

@ -1,155 +1,158 @@
use gtk4::prelude::*; //! breadd.toml — the bread daemon config.
use gtk4::{Box as GBox, Button, DropDown, Label, Orientation, StringList, Switch}; //! Schema mirrors breadd/src/core/config.rs (daemon, lua, modules, adapters,
use serde::{Deserialize, Serialize}; //! events, notifications). Edited non-destructively via the shared document.
use std::cell::RefCell; use std::cell::RefCell;
use std::rc::Rc; use std::rc::Rc;
use gtk4::prelude::*;
use gtk4::Box as GBox;
use crate::config; use crate::config;
use crate::ui::widgets as w;
#[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 { fn config_path() -> std::path::PathBuf {
config::config_dir().join("bread/breadd.toml") config::config_dir().join("bread/breadd.toml")
} }
fn adapter_row(
label: &str,
active: bool,
cfg: Rc<RefCell<BreadConfig>>,
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 { pub fn build() -> GBox {
let path = config_path(); let path = config_path();
let cfg: BreadConfig = config::load(&path).unwrap_or_default(); let doc = Rc::new(RefCell::new(config::load_doc(&path)));
let cfg = Rc::new(RefCell::new(cfg));
let vbox = GBox::new(Orientation::Vertical, 12); let (outer, c) = w::view_scaffold("bread");
vbox.add_css_class("view-content");
let title = Label::new(Some("bread")); c.append(&w::section("Daemon"));
title.add_css_class("title"); c.append(&w::dropdown_row(
title.set_xalign(0.0); "Log level",
vbox.append(&title); &doc,
&["daemon", "log_level"],
&["error", "warn", "info", "debug", "trace"],
"info",
));
c.append(&w::entry_row(
"Socket path",
&doc,
&["daemon", "socket_path"],
"default (XDG runtime dir)",
"",
));
// Log level c.append(&w::section("Lua"));
let row = GBox::new(Orientation::Horizontal, 16); c.append(&w::entry_row(
row.set_margin_bottom(8); "Entry point",
let lbl = Label::new(Some("Log level")); &doc,
lbl.set_hexpand(true); &["lua", "entry_point"],
lbl.set_xalign(0.0); "~/.config/bread/init.lua",
let levels = StringList::new(&["error", "warn", "info", "debug", "trace"]); "",
let dropdown = DropDown::new(Some(levels), gtk4::Expression::NONE); ));
let pos = match cfg.borrow().log_level.as_str() { c.append(&w::entry_row(
"error" => 0u32, "warn" => 1, "info" => 2, "debug" => 3, "trace" => 4, _ => 2, "Module path",
}; &doc,
dropdown.set_selected(pos); &["lua", "module_path"],
{ "~/.config/bread/modules",
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);
let adapter_label = Label::new(Some("Adapters")); c.append(&w::section("Modules"));
adapter_label.set_xalign(0.0); c.append(&w::switch_row(
adapter_label.set_margin_top(8); "Load built-in modules",
adapter_label.set_margin_bottom(4); &doc,
vbox.append(&adapter_label); &["modules", "builtin"],
true,
));
c.append(&w::csv_row(
"Disabled modules",
&doc,
&["modules", "disable"],
"module-a, module-b",
));
let (kbd, mouse, touchpad, bluetooth, gamepad) = { c.append(&w::section("Adapters"));
let c = cfg.borrow(); c.append(&w::hint(
(c.adapters.keyboard, c.adapters.mouse, c.adapters.touchpad, "Sources breadd normalises into events. Disable any you don't use.",
c.adapters.bluetooth, c.adapters.gamepad) ));
}; c.append(&w::switch_row(
vbox.append(&adapter_row("Keyboard", kbd, cfg.clone(), "keyboard")); "Hyprland",
vbox.append(&adapter_row("Mouse", mouse, cfg.clone(), "mouse")); &doc,
vbox.append(&adapter_row("Touchpad", touchpad, cfg.clone(), "touchpad")); &["adapters", "hyprland", "enabled"],
vbox.append(&adapter_row("Bluetooth", bluetooth, cfg.clone(), "bluetooth")); true,
vbox.append(&adapter_row("Gamepad", gamepad, cfg.clone(), "gamepad")); ));
c.append(&w::switch_row(
"udev (devices)",
&doc,
&["adapters", "udev", "enabled"],
true,
));
c.append(&w::csv_row(
"udev subsystems",
&doc,
&["adapters", "udev", "subsystems"],
"usb, input, power_supply",
));
c.append(&w::switch_row(
"Power",
&doc,
&["adapters", "power", "enabled"],
true,
));
c.append(&w::spin_row(
"Power poll interval (s)",
&doc,
&["adapters", "power", "poll_interval_secs"],
1.0,
3600.0,
1.0,
30,
));
c.append(&w::switch_row(
"Network",
&doc,
&["adapters", "network", "enabled"],
true,
));
c.append(&w::switch_row(
"Bluetooth",
&doc,
&["adapters", "bluetooth", "enabled"],
true,
));
let btn_row = GBox::new(Orientation::Horizontal, 12); c.append(&w::section("Events"));
btn_row.set_margin_top(16); c.append(&w::spin_row(
"Dedup window (ms)",
&doc,
&["events", "dedup_window_ms"],
0.0,
10000.0,
50.0,
250,
));
let save_btn = Button::with_label("Save"); c.append(&w::section("Notifications"));
let status_lbl = Label::new(None); c.append(&w::spin_row(
status_lbl.add_css_class("dim-label"); "Default timeout (ms)",
&doc,
&["notifications", "default_timeout_ms"],
0.0,
60000.0,
500.0,
5000,
));
c.append(&w::dropdown_row(
"Default urgency",
&doc,
&["notifications", "default_urgency"],
&["low", "normal", "critical"],
"normal",
));
c.append(&w::entry_row(
"notify-send path",
&doc,
&["notifications", "notify_send_path"],
"auto-detected",
"",
));
{ outer.append(&w::save_button(&doc, path));
let cfg = cfg.clone(); outer
let path = path.clone();
let status_lbl = status_lbl.clone();
save_btn.connect_clicked(move |_| {
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(&save_btn);
btn_row.append(&status_lbl);
vbox.append(&btn_row);
vbox
} }

View file

@ -6,7 +6,6 @@ fn css_path() -> PathBuf {
crate::config::config_dir().join("breadbar/style.css") crate::config::config_dir().join("breadbar/style.css")
} }
pub fn build() -> GBox { pub fn build() -> GBox {
let path = css_path(); let path = css_path();
let existing_css = std::fs::read_to_string(&path).unwrap_or_default(); let existing_css = std::fs::read_to_string(&path).unwrap_or_default();

View file

@ -1,33 +1,66 @@
use gtk4::prelude::*; //! breadbox config.toml — launcher contexts.
use gtk4::{Box as GBox, Button, Entry, Label, ListBox, ListBoxRow, Orientation, ScrolledWindow}; //! Schema mirrors breadbox-shared (`[[contexts]]` with `name` + `priority`, an
use serde::{Deserialize, Serialize}; //! ordered list of app/category hints). The contexts array is rewritten on
//! save; any other top-level keys/comments in the file are preserved.
use std::cell::RefCell; use std::cell::RefCell;
use std::rc::Rc; use std::rc::Rc;
use gtk4::prelude::*;
use gtk4::{
Box as GBox, Button, Entry, Label, ListBox, ListBoxRow, Orientation, ScrolledWindow,
};
use toml_edit::{value, Array, ArrayOfTables, DocumentMut, Item, Table};
use crate::config; use crate::config;
#[derive(Deserialize, Serialize, Clone, Default)] #[derive(Clone, Default)]
pub struct BreadboxConfig { struct Context {
#[serde(default)] name: String,
pub context: Vec<Context>, priority: Vec<String>,
}
#[derive(Deserialize, Serialize, Clone)]
pub struct Context {
pub name: String,
#[serde(default)]
pub apps: Vec<String>,
} }
fn config_path() -> std::path::PathBuf { fn config_path() -> std::path::PathBuf {
config::config_dir().join("breadbox/config.toml") config::config_dir().join("breadbox/config.toml")
} }
fn rebuild_list(list: &ListBox, cfg: &Rc<RefCell<BreadboxConfig>>) { fn read_contexts(doc: &DocumentMut) -> Vec<Context> {
let Some(aot) = doc.get("contexts").and_then(Item::as_array_of_tables) else {
return Vec::new();
};
aot.iter()
.map(|t| Context {
name: t.get("name").and_then(Item::as_str).unwrap_or("").to_string(),
priority: t
.get("priority")
.and_then(Item::as_array)
.map(|a| a.iter().filter_map(|v| v.as_str().map(String::from)).collect())
.unwrap_or_default(),
})
.collect()
}
/// Rewrite only the `contexts` array-of-tables, leaving the rest of the doc.
fn write_contexts(doc: &mut DocumentMut, ctxs: &[Context]) {
let mut aot = ArrayOfTables::new();
for ctx in ctxs {
let mut t = Table::new();
t.insert("name", value(&ctx.name));
let mut arr = Array::new();
for p in &ctx.priority {
arr.push(p.as_str());
}
t.insert("priority", value(arr));
aot.push(t);
}
doc.as_table_mut().insert("contexts", Item::ArrayOfTables(aot));
}
fn rebuild_list(list: &ListBox, model: &Rc<RefCell<Vec<Context>>>) {
while let Some(child) = list.first_child() { while let Some(child) = list.first_child() {
list.remove(&child); list.remove(&child);
} }
for (i, ctx) in cfg.borrow().context.iter().enumerate() { for (i, ctx) in model.borrow().iter().enumerate() {
let row = ListBoxRow::new(); let row = ListBoxRow::new();
row.set_selectable(false); row.set_selectable(false);
@ -42,27 +75,28 @@ fn rebuild_list(list: &ListBox, cfg: &Rc<RefCell<BreadboxConfig>>) {
name_entry.set_width_chars(14); name_entry.set_width_chars(14);
name_entry.set_placeholder_text(Some("name")); name_entry.set_placeholder_text(Some("name"));
let apps_entry = Entry::new(); let prio_entry = Entry::new();
apps_entry.set_text(&ctx.apps.join(", ")); prio_entry.set_text(&ctx.priority.join(", "));
apps_entry.set_hexpand(true); prio_entry.set_hexpand(true);
apps_entry.set_placeholder_text(Some("app1, app2, ...")); prio_entry.set_placeholder_text(Some("firefox, code, Development, ..."));
let remove_btn = Button::with_label("Remove"); let remove_btn = Button::with_label("Remove");
remove_btn.add_css_class("destructive-action"); remove_btn.add_css_class("destructive-action");
{ {
let cfg = cfg.clone(); let model = model.clone();
name_entry.connect_changed(move |e| { name_entry.connect_changed(move |e| {
if let Some(c) = cfg.borrow_mut().context.get_mut(i) { if let Some(c) = model.borrow_mut().get_mut(i) {
c.name = e.text().to_string(); c.name = e.text().to_string();
} }
}); });
} }
{ {
let cfg = cfg.clone(); let model = model.clone();
apps_entry.connect_changed(move |e| { prio_entry.connect_changed(move |e| {
if let Some(c) = cfg.borrow_mut().context.get_mut(i) { if let Some(c) = model.borrow_mut().get_mut(i) {
c.apps = e.text() c.priority = e
.text()
.split(',') .split(',')
.map(|s| s.trim().to_string()) .map(|s| s.trim().to_string())
.filter(|s| !s.is_empty()) .filter(|s| !s.is_empty())
@ -71,16 +105,16 @@ fn rebuild_list(list: &ListBox, cfg: &Rc<RefCell<BreadboxConfig>>) {
}); });
} }
{ {
let cfg = cfg.clone(); let model = model.clone();
let list = list.clone(); let list = list.clone();
remove_btn.connect_clicked(move |_| { remove_btn.connect_clicked(move |_| {
cfg.borrow_mut().context.remove(i); model.borrow_mut().remove(i);
rebuild_list(&list, &cfg); rebuild_list(&list, &model);
}); });
} }
hbox.append(&name_entry); hbox.append(&name_entry);
hbox.append(&apps_entry); hbox.append(&prio_entry);
hbox.append(&remove_btn); hbox.append(&remove_btn);
row.set_child(Some(&hbox)); row.set_child(Some(&hbox));
list.append(&row); list.append(&row);
@ -89,8 +123,8 @@ fn rebuild_list(list: &ListBox, cfg: &Rc<RefCell<BreadboxConfig>>) {
pub fn build() -> GBox { pub fn build() -> GBox {
let path = config_path(); let path = config_path();
let cfg: BreadboxConfig = config::load(&path).unwrap_or_default(); let doc = Rc::new(RefCell::new(config::load_doc(&path)));
let cfg = Rc::new(RefCell::new(cfg)); let model = Rc::new(RefCell::new(read_contexts(&doc.borrow())));
let vbox = GBox::new(Orientation::Vertical, 12); let vbox = GBox::new(Orientation::Vertical, 12);
vbox.add_css_class("view-content"); vbox.add_css_class("view-content");
@ -100,14 +134,17 @@ pub fn build() -> GBox {
title.set_xalign(0.0); title.set_xalign(0.0);
vbox.append(&title); vbox.append(&title);
let subtitle = Label::new(Some("Context priority lists — apps shown in each context.")); let subtitle = Label::new(Some(
"Launcher contexts — each lists, in priority order, the apps/categories surfaced first.",
));
subtitle.set_xalign(0.0); subtitle.set_xalign(0.0);
subtitle.set_wrap(true);
subtitle.set_margin_bottom(8); subtitle.set_margin_bottom(8);
vbox.append(&subtitle); vbox.append(&subtitle);
let list = ListBox::new(); let list = ListBox::new();
list.set_selection_mode(gtk4::SelectionMode::None); list.set_selection_mode(gtk4::SelectionMode::None);
rebuild_list(&list, &cfg); rebuild_list(&list, &model);
let scroll = ScrolledWindow::new(); let scroll = ScrolledWindow::new();
scroll.set_vexpand(true); scroll.set_vexpand(true);
@ -119,27 +156,30 @@ pub fn build() -> GBox {
let add_btn = Button::with_label("Add context"); let add_btn = Button::with_label("Add context");
{ {
let cfg = cfg.clone(); let model = model.clone();
let list = list.clone(); let list = list.clone();
add_btn.connect_clicked(move |_| { add_btn.connect_clicked(move |_| {
cfg.borrow_mut().context.push(Context { model.borrow_mut().push(Context {
name: "new".to_string(), name: "new".to_string(),
apps: Vec::new(), priority: Vec::new(),
}); });
rebuild_list(&list, &cfg); rebuild_list(&list, &model);
}); });
} }
let save_btn = Button::with_label("Save"); let save_btn = Button::with_label("Save");
save_btn.add_css_class("suggested-action");
let status_lbl = Label::new(None); let status_lbl = Label::new(None);
status_lbl.add_css_class("dim-label"); status_lbl.add_css_class("dim-label");
{ {
let cfg = cfg.clone(); let doc = doc.clone();
let model = model.clone();
let path = path.clone(); let path = path.clone();
let status_lbl = status_lbl.clone(); let status_lbl = status_lbl.clone();
save_btn.connect_clicked(move |_| { save_btn.connect_clicked(move |_| {
match config::save(&path, &*cfg.borrow()) { write_contexts(&mut doc.borrow_mut(), &model.borrow());
match config::save_doc(&path, &doc.borrow()) {
Ok(()) => { Ok(()) => {
status_lbl.set_text("Saved"); status_lbl.set_text("Saved");
let lbl = status_lbl.clone(); let lbl = status_lbl.clone();

View file

@ -1,162 +1,477 @@
use gtk4::prelude::*; //! breadcrumbs.toml — Wi-Fi profile state machine.
use gtk4::{Box as GBox, Button, Entry, Label, ListBox, ListBoxRow, Orientation, ScrolledWindow}; //! Schema mirrors breadcrumbs/src/config.rs:
use serde::{Deserialize, Serialize}; //! [settings] scalar tunables
//! [[networks]] saved networks (ssid / password / hidden)
//! [profiles.<name>] per-location profile (networks, tailscale, …)
//! `[settings]` is edited in place; the `networks` array and `profiles` table
//! are rewritten from their editors on save. Other keys/comments are preserved.
use std::cell::RefCell; use std::cell::RefCell;
use std::rc::Rc; use std::rc::Rc;
use gtk4::prelude::*;
use gtk4::{
Box as GBox, Button, Entry, Label, ListBox, ListBoxRow, Orientation, ScrolledWindow, Switch,
};
use toml_edit::{value, Array, ArrayOfTables, DocumentMut, Item, Table};
use crate::config; use crate::config;
use crate::ui::widgets as w;
#[derive(Deserialize, Serialize, Clone, Default)]
pub struct BreadcrumbsConfig {
#[serde(default)]
pub profile: Vec<Profile>,
}
#[derive(Deserialize, Serialize, Clone)]
pub struct Profile {
pub name: String,
#[serde(default)]
pub ssids: Vec<String>,
}
fn config_path() -> std::path::PathBuf { fn config_path() -> std::path::PathBuf {
config::config_dir().join("breadcrumbs/breadcrumbs.toml") config::config_dir().join("breadcrumbs/breadcrumbs.toml")
} }
fn rebuild_list(list: &ListBox, cfg: &Rc<RefCell<BreadcrumbsConfig>>) { // --- networks ---------------------------------------------------------------
#[derive(Clone, Default)]
struct Network {
ssid: String,
password: String,
hidden: bool,
}
fn read_networks(doc: &DocumentMut) -> Vec<Network> {
let Some(aot) = doc.get("networks").and_then(Item::as_array_of_tables) else {
return Vec::new();
};
aot.iter()
.map(|t| Network {
ssid: t.get("ssid").and_then(Item::as_str).unwrap_or("").to_string(),
password: t
.get("password")
.and_then(Item::as_str)
.unwrap_or("")
.to_string(),
hidden: t.get("hidden").and_then(Item::as_bool).unwrap_or(false),
})
.collect()
}
fn write_networks(doc: &mut DocumentMut, nets: &[Network]) {
let mut aot = ArrayOfTables::new();
for n in nets {
let mut t = Table::new();
t.insert("ssid", value(&n.ssid));
t.insert("password", value(&n.password));
t.insert("hidden", value(n.hidden));
aot.push(t);
}
doc.as_table_mut().insert("networks", Item::ArrayOfTables(aot));
}
fn rebuild_networks(list: &ListBox, model: &Rc<RefCell<Vec<Network>>>) {
while let Some(child) = list.first_child() { while let Some(child) = list.first_child() {
list.remove(&child); list.remove(&child);
} }
for (i, profile) in cfg.borrow().profile.iter().enumerate() { for (i, n) in model.borrow().iter().enumerate() {
let row = ListBoxRow::new(); let row = ListBoxRow::new();
row.set_selectable(false); row.set_selectable(false);
let hbox = GBox::new(Orientation::Horizontal, 8); let hbox = GBox::new(Orientation::Horizontal, 8);
hbox.set_margin_top(6); hbox.set_margin_top(6);
hbox.set_margin_bottom(6); hbox.set_margin_bottom(6);
hbox.set_margin_start(8); hbox.set_margin_start(8);
hbox.set_margin_end(8); hbox.set_margin_end(8);
let name_entry = Entry::new(); let ssid = Entry::new();
name_entry.set_text(&profile.name); ssid.set_text(&n.ssid);
name_entry.set_width_chars(14); ssid.set_width_chars(16);
name_entry.set_placeholder_text(Some("name")); ssid.set_placeholder_text(Some("SSID"));
let ssids_entry = Entry::new(); let pass = Entry::new();
ssids_entry.set_text(&profile.ssids.join(", ")); pass.set_text(&n.password);
ssids_entry.set_hexpand(true); pass.set_hexpand(true);
ssids_entry.set_placeholder_text(Some("SSID1, SSID2, ...")); pass.set_visibility(false);
pass.set_input_purpose(gtk4::InputPurpose::Password);
pass.set_placeholder_text(Some("password"));
let remove_btn = Button::with_label("Remove"); let hidden = Switch::new();
remove_btn.add_css_class("destructive-action"); hidden.set_active(n.hidden);
hidden.set_valign(gtk4::Align::Center);
hidden.set_tooltip_text(Some("Hidden network"));
let remove = Button::with_label("Remove");
remove.add_css_class("destructive-action");
{ {
let cfg = cfg.clone(); let model = model.clone();
name_entry.connect_changed(move |e| { ssid.connect_changed(move |e| {
if let Some(p) = cfg.borrow_mut().profile.get_mut(i) { if let Some(n) = model.borrow_mut().get_mut(i) {
p.name = e.text().to_string(); n.ssid = e.text().to_string();
} }
}); });
} }
{ {
let cfg = cfg.clone(); let model = model.clone();
ssids_entry.connect_changed(move |e| { pass.connect_changed(move |e| {
if let Some(p) = cfg.borrow_mut().profile.get_mut(i) { if let Some(n) = model.borrow_mut().get_mut(i) {
p.ssids = e.text() n.password = e.text().to_string();
.split(',')
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
.collect();
} }
}); });
} }
{ {
let cfg = cfg.clone(); let model = model.clone();
hidden.connect_active_notify(move |s| {
if let Some(n) = model.borrow_mut().get_mut(i) {
n.hidden = s.is_active();
}
});
}
{
let model = model.clone();
let list = list.clone(); let list = list.clone();
remove_btn.connect_clicked(move |_| { remove.connect_clicked(move |_| {
cfg.borrow_mut().profile.remove(i); model.borrow_mut().remove(i);
rebuild_list(&list, &cfg); rebuild_networks(&list, &model);
}); });
} }
hbox.append(&name_entry); hbox.append(&ssid);
hbox.append(&ssids_entry); hbox.append(&pass);
hbox.append(&remove_btn); hbox.append(&Label::new(Some("hidden")));
hbox.append(&hidden);
hbox.append(&remove);
row.set_child(Some(&hbox)); row.set_child(Some(&hbox));
list.append(&row); list.append(&row);
} }
} }
// --- profiles ---------------------------------------------------------------
#[derive(Clone, Default)]
struct Profile {
name: String,
networks: Vec<String>,
detect_ssids: Vec<String>,
bootstrap: String,
exit_node: String,
tailscale: bool,
include_all_known: bool,
}
fn read_profiles(doc: &DocumentMut) -> Vec<Profile> {
let Some(tbl) = doc.get("profiles").and_then(Item::as_table) else {
return Vec::new();
};
let str_list = |item: Option<&Item>| -> Vec<String> {
item.and_then(Item::as_array)
.map(|a| a.iter().filter_map(|v| v.as_str().map(String::from)).collect())
.unwrap_or_default()
};
tbl.iter()
.filter_map(|(name, item)| {
let p = item.as_table()?;
Some(Profile {
name: name.to_string(),
networks: str_list(p.get("networks")),
detect_ssids: str_list(p.get("detect_ssids")),
bootstrap: p.get("bootstrap").and_then(Item::as_str).unwrap_or("").to_string(),
exit_node: p.get("exit_node").and_then(Item::as_str).unwrap_or("").to_string(),
tailscale: p.get("tailscale").and_then(Item::as_bool).unwrap_or(false),
include_all_known: p
.get("include_all_known")
.and_then(Item::as_bool)
.unwrap_or(false),
})
})
.collect()
}
fn write_profiles(doc: &mut DocumentMut, profiles: &[Profile]) {
let mut tbl = Table::new();
let to_arr = |items: &[String]| {
let mut a = Array::new();
for s in items {
a.push(s.as_str());
}
a
};
for p in profiles {
if p.name.is_empty() {
continue;
}
let mut t = Table::new();
t.insert("networks", value(to_arr(&p.networks)));
t.insert("tailscale", value(p.tailscale));
t.insert("include_all_known", value(p.include_all_known));
if !p.detect_ssids.is_empty() {
t.insert("detect_ssids", value(to_arr(&p.detect_ssids)));
}
if !p.bootstrap.is_empty() {
t.insert("bootstrap", value(&p.bootstrap));
}
if !p.exit_node.is_empty() {
t.insert("exit_node", value(&p.exit_node));
}
tbl.insert(&p.name, Item::Table(t));
}
doc.as_table_mut().insert("profiles", Item::Table(tbl));
}
fn field(label: &str, control: &impl IsA<gtk4::Widget>) -> GBox {
let row = GBox::new(Orientation::Horizontal, 12);
let lbl = Label::new(Some(label));
lbl.set_xalign(0.0);
lbl.set_width_chars(16);
row.append(&lbl);
control.set_hexpand(true);
row.append(control);
row
}
fn rebuild_profiles(container: &GBox, model: &Rc<RefCell<Vec<Profile>>>) {
while let Some(child) = container.first_child() {
container.remove(&child);
}
for (i, p) in model.borrow().iter().enumerate() {
let card = GBox::new(Orientation::Vertical, 6);
card.add_css_class("card");
card.set_margin_top(6);
card.set_margin_bottom(6);
let header = GBox::new(Orientation::Horizontal, 8);
let name = Entry::new();
name.set_text(&p.name);
name.set_hexpand(true);
name.set_placeholder_text(Some("profile name (e.g. home)"));
let remove = Button::with_label("Remove");
remove.add_css_class("destructive-action");
header.append(&name);
header.append(&remove);
card.append(&header);
let networks = Entry::new();
networks.set_text(&p.networks.join(", "));
networks.set_placeholder_text(Some("SSID1, SSID2"));
card.append(&field("Networks", &networks));
let detect = Entry::new();
detect.set_text(&p.detect_ssids.join(", "));
detect.set_placeholder_text(Some("SSIDs that auto-select this profile"));
card.append(&field("Detect SSIDs", &detect));
let exit_node = Entry::new();
exit_node.set_text(&p.exit_node);
exit_node.set_placeholder_text(Some("tailscale exit node (optional)"));
card.append(&field("Exit node", &exit_node));
let bootstrap = Entry::new();
bootstrap.set_text(&p.bootstrap);
bootstrap.set_placeholder_text(Some("bootstrap command (optional)"));
card.append(&field("Bootstrap", &bootstrap));
let tailscale = Switch::new();
tailscale.set_active(p.tailscale);
tailscale.set_halign(gtk4::Align::Start);
card.append(&field("Tailscale", &tailscale));
let include_all = Switch::new();
include_all.set_active(p.include_all_known);
include_all.set_halign(gtk4::Align::Start);
card.append(&field("Include all known", &include_all));
// bind each control to the in-memory model entry
macro_rules! bind_csv {
($entry:ident, $f:ident) => {{
let model = model.clone();
$entry.connect_changed(move |e| {
if let Some(p) = model.borrow_mut().get_mut(i) {
p.$f = e
.text()
.split(',')
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
.collect();
}
});
}};
}
macro_rules! bind_str {
($entry:ident, $f:ident) => {{
let model = model.clone();
$entry.connect_changed(move |e| {
if let Some(p) = model.borrow_mut().get_mut(i) {
p.$f = e.text().to_string();
}
});
}};
}
macro_rules! bind_bool {
($sw:ident, $f:ident) => {{
let model = model.clone();
$sw.connect_active_notify(move |s| {
if let Some(p) = model.borrow_mut().get_mut(i) {
p.$f = s.is_active();
}
});
}};
}
bind_str!(name, name);
bind_csv!(networks, networks);
bind_csv!(detect, detect_ssids);
bind_str!(exit_node, exit_node);
bind_str!(bootstrap, bootstrap);
bind_bool!(tailscale, tailscale);
bind_bool!(include_all, include_all_known);
{
let model = model.clone();
let container = container.clone();
remove.connect_clicked(move |_| {
model.borrow_mut().remove(i);
rebuild_profiles(&container, &model);
});
}
container.append(&card);
}
}
// --- view -------------------------------------------------------------------
pub fn build() -> GBox { pub fn build() -> GBox {
let path = config_path(); let path = config_path();
let cfg: BreadcrumbsConfig = config::load(&path).unwrap_or_default(); let doc = Rc::new(RefCell::new(config::load_doc(&path)));
let cfg = Rc::new(RefCell::new(cfg)); let nets = Rc::new(RefCell::new(read_networks(&doc.borrow())));
let profiles = Rc::new(RefCell::new(read_profiles(&doc.borrow())));
let vbox = GBox::new(Orientation::Vertical, 12); let outer = GBox::new(Orientation::Vertical, 8);
vbox.add_css_class("view-content"); outer.add_css_class("view-content");
let title = Label::new(Some("breadcrumbs")); let title = Label::new(Some("breadcrumbs"));
title.add_css_class("title"); title.add_css_class("title");
title.set_xalign(0.0); title.set_xalign(0.0);
vbox.append(&title); outer.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 content = GBox::new(Orientation::Vertical, 8);
let scroll = ScrolledWindow::new(); let scroll = ScrolledWindow::new();
scroll.set_vexpand(true); scroll.set_vexpand(true);
scroll.set_child(Some(&list)); scroll.set_hscrollbar_policy(gtk4::PolicyType::Never);
vbox.append(&scroll); scroll.set_child(Some(&content));
outer.append(&scroll);
let btn_row = GBox::new(Orientation::Horizontal, 8); // [settings] — edited in place on the shared doc
btn_row.set_margin_top(8); content.append(&w::section("Settings"));
content.append(&w::dropdown_row(
"Default profile",
&doc,
&["settings", "default_profile"],
&["home", "away"],
"home",
));
content.append(&w::entry_row("DNS", &doc, &["settings", "dns"], "1.1.1.1", ""));
content.append(&w::entry_row(
"Exit node",
&doc,
&["settings", "exit_node"],
"tailscale exit node",
"",
));
content.append(&w::entry_row(
"Ping host",
&doc,
&["settings", "ping_host"],
"1.1.1.1",
"",
));
content.append(&w::entry_row(
"Connectivity URL",
&doc,
&["settings", "connectivity_url"],
"http://connectivitycheck.gstatic.com/generate_204",
"",
));
content.append(&w::spin_row(
"nmcli wait (s)",
&doc,
&["settings", "nmcli_wait"],
1.0,
120.0,
1.0,
8,
));
content.append(&w::spin_row(
"Watch interval (s)",
&doc,
&["settings", "watch_interval"],
1.0,
600.0,
1.0,
12,
));
let add_btn = Button::with_label("Add profile"); // [[networks]]
content.append(&w::section("Saved networks"));
let net_list = ListBox::new();
net_list.set_selection_mode(gtk4::SelectionMode::None);
rebuild_networks(&net_list, &nets);
content.append(&net_list);
let add_net = Button::with_label("Add network");
add_net.set_halign(gtk4::Align::Start);
{ {
let cfg = cfg.clone(); let nets = nets.clone();
let list = list.clone(); let net_list = net_list.clone();
add_btn.connect_clicked(move |_| { add_net.connect_clicked(move |_| {
cfg.borrow_mut().profile.push(Profile { nets.borrow_mut().push(Network::default());
name: "new".to_string(), rebuild_networks(&net_list, &nets);
ssids: Vec::new(),
});
rebuild_list(&list, &cfg);
}); });
} }
content.append(&add_net);
let save_btn = Button::with_label("Save"); // [profiles.*]
let status_lbl = Label::new(None); content.append(&w::section("Profiles"));
status_lbl.add_css_class("dim-label"); let prof_box = GBox::new(Orientation::Vertical, 4);
rebuild_profiles(&prof_box, &profiles);
content.append(&prof_box);
let add_prof = Button::with_label("Add profile");
add_prof.set_halign(gtk4::Align::Start);
{ {
let cfg = cfg.clone(); let profiles = profiles.clone();
let prof_box = prof_box.clone();
add_prof.connect_clicked(move |_| {
profiles.borrow_mut().push(Profile {
name: "new".to_string(),
..Default::default()
});
rebuild_profiles(&prof_box, &profiles);
});
}
content.append(&add_prof);
// Save — fold the network + profile editors back into the doc, then write.
let btn_row = GBox::new(Orientation::Horizontal, 12);
btn_row.set_margin_top(16);
let save_btn = Button::with_label("Save");
save_btn.add_css_class("suggested-action");
let status = Label::new(None);
status.add_css_class("dim-label");
{
let doc = doc.clone();
let nets = nets.clone();
let profiles = profiles.clone();
let path = path.clone(); let path = path.clone();
let status_lbl = status_lbl.clone(); let status = status.clone();
save_btn.connect_clicked(move |_| { save_btn.connect_clicked(move |_| {
match config::save(&path, &*cfg.borrow()) { {
let mut d = doc.borrow_mut();
write_networks(&mut d, &nets.borrow());
write_profiles(&mut d, &profiles.borrow());
}
match config::save_doc(&path, &doc.borrow()) {
Ok(()) => { Ok(()) => {
status_lbl.set_text("Saved"); status.set_text("Saved");
let lbl = status_lbl.clone(); let lbl = status.clone();
glib::timeout_add_seconds_local(3, move || { glib::timeout_add_seconds_local(3, move || {
lbl.set_text(""); lbl.set_text("");
glib::ControlFlow::Break glib::ControlFlow::Break
}); });
} }
Err(e) => status_lbl.set_text(&format!("Error: {e}")), Err(e) => status.set_text(&format!("Error: {e}")),
} }
}); });
} }
btn_row.append(&add_btn);
btn_row.append(&save_btn); btn_row.append(&save_btn);
btn_row.append(&status_lbl); btn_row.append(&status);
vbox.append(&btn_row); outer.append(&btn_row);
vbox outer
} }

View file

@ -1,28 +1,16 @@
use gtk4::prelude::*; //! breadpad.toml — the breadpad notes/reminders config.
use gtk4::{Box as GBox, Button, Entry, Label, Orientation, Switch}; //! Schema mirrors breadpad-shared/src/config.rs (settings, model + model.ollama,
use serde::{Deserialize, Serialize}; //! reminders, calendar). Edited non-destructively (the calendar password and
//! model paths are preserved across saves).
use std::cell::RefCell; use std::cell::RefCell;
use std::rc::Rc; use std::rc::Rc;
use gtk4::prelude::*;
use gtk4::Box as GBox;
use crate::config; use crate::config;
use crate::ui::widgets as w;
#[derive(Deserialize, Serialize, Clone)]
pub struct BreadpadConfig {
#[serde(default)]
pub model: String,
#[serde(default = "default_true")]
pub reminders: bool,
#[serde(default = "default_true")]
pub calendar: bool,
}
fn default_true() -> bool { true }
impl Default for BreadpadConfig {
fn default() -> Self {
Self { model: String::new(), reminders: true, calendar: true }
}
}
fn config_path() -> std::path::PathBuf { fn config_path() -> std::path::PathBuf {
config::config_dir().join("breadpad/breadpad.toml") config::config_dir().join("breadpad/breadpad.toml")
@ -30,93 +18,129 @@ fn config_path() -> std::path::PathBuf {
pub fn build() -> GBox { pub fn build() -> GBox {
let path = config_path(); let path = config_path();
let cfg: BreadpadConfig = config::load(&path).unwrap_or_default(); let doc = Rc::new(RefCell::new(config::load_doc(&path)));
let cfg = Rc::new(RefCell::new(cfg));
let vbox = GBox::new(Orientation::Vertical, 12); let (outer, c) = w::view_scaffold("breadpad");
vbox.add_css_class("view-content");
let title = Label::new(Some("breadpad")); c.append(&w::section("Capture"));
title.add_css_class("title"); c.append(&w::dropdown_row(
title.set_xalign(0.0); "Default note type",
vbox.append(&title); &doc,
&["settings", "default_type"],
&["note", "reminder", "task"],
"note",
));
c.append(&w::switch_row(
"Tag with active workspace",
&doc,
&["settings", "workspace_tag"],
true,
));
c.append(&w::csv_row(
"Snooze options",
&doc,
&["settings", "snooze_options"],
"15m, 1h, tomorrow_morning",
));
c.append(&w::spin_row(
"Archive after (days)",
&doc,
&["settings", "archive_after_days"],
0.0,
3650.0,
1.0,
30,
));
// Model entry c.append(&w::section("Classifier model"));
let row = GBox::new(Orientation::Horizontal, 16); c.append(&w::entry_row(
let lbl = Label::new(Some("Model")); "ONNX model path",
lbl.set_hexpand(true); &doc,
lbl.set_xalign(0.0); &["model", "path"],
let model_entry = Entry::new(); "~/.local/share/breadpad/model/classifier.onnx",
model_entry.set_text(&cfg.borrow().model); "",
model_entry.set_placeholder_text(Some("e.g. claude-sonnet-4-6")); ));
{ c.append(&w::entry_row(
let cfg = cfg.clone(); "Tokenizer path",
model_entry.connect_changed(move |e| { &doc,
cfg.borrow_mut().model = e.text().to_string(); &["model", "tokenizer"],
}); "~/.local/share/breadpad/model/tokenizer.json",
} "",
row.append(&lbl); ));
row.append(&model_entry);
vbox.append(&row);
// Reminders c.append(&w::section("Ollama (LLM classifier)"));
let row = GBox::new(Orientation::Horizontal, 16); c.append(&w::switch_row(
let lbl = Label::new(Some("Reminders")); "Use Ollama",
lbl.set_hexpand(true); &doc,
lbl.set_xalign(0.0); &["model", "ollama", "enabled"],
let sw = Switch::new(); true,
sw.set_active(cfg.borrow().reminders); ));
{ c.append(&w::entry_row(
let cfg = cfg.clone(); "Endpoint",
sw.connect_active_notify(move |s| { cfg.borrow_mut().reminders = s.is_active(); }); &doc,
} &["model", "ollama", "endpoint"],
row.append(&lbl); "http://localhost:11434",
row.append(&sw); "",
vbox.append(&row); ));
c.append(&w::entry_row(
"Model",
&doc,
&["model", "ollama", "model"],
"e.g. fastflowlm",
"",
));
c.append(&w::spin_f64_row(
"Confidence threshold",
&doc,
&["model", "ollama", "confidence_threshold"],
0.0,
1.0,
0.05,
2,
0.6,
));
// Calendar c.append(&w::section("Reminders"));
let row = GBox::new(Orientation::Horizontal, 16); c.append(&w::entry_row(
let lbl = Label::new(Some("Calendar integration")); "Default morning time",
lbl.set_hexpand(true); &doc,
lbl.set_xalign(0.0); &["reminders", "default_morning"],
let sw = Switch::new(); "7:00",
sw.set_active(cfg.borrow().calendar); "",
{ ));
let cfg = cfg.clone(); c.append(&w::spin_row(
sw.connect_active_notify(move |s| { cfg.borrow_mut().calendar = s.is_active(); }); "Missed grace (minutes)",
} &doc,
row.append(&lbl); &["reminders", "missed_grace_minutes"],
row.append(&sw); 0.0,
vbox.append(&row); 1440.0,
5.0,
60,
));
let btn_row = GBox::new(Orientation::Horizontal, 12); c.append(&w::section("Calendar (CalDAV)"));
btn_row.set_margin_top(16); c.append(&w::switch_row(
"Sync to calendar",
&doc,
&["calendar", "enabled"],
false,
));
c.append(&w::entry_row(
"CalDAV URL",
&doc,
&["calendar", "url"],
"https://host/remote.php/dav/calendars/...",
"",
));
c.append(&w::entry_row(
"Username",
&doc,
&["calendar", "username"],
"",
"",
));
c.append(&w::password_row("Password", &doc, &["calendar", "password"]));
let save_btn = Button::with_label("Save"); outer.append(&w::save_button(&doc, path));
let status_lbl = Label::new(None); outer
status_lbl.add_css_class("dim-label");
{
let cfg = cfg.clone();
let status_lbl = status_lbl.clone();
save_btn.connect_clicked(move |_| {
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(&save_btn);
btn_row.append(&status_lbl);
vbox.append(&btn_row);
vbox
} }

View file

@ -50,9 +50,10 @@ fn stream_command(args: &[&str], log_buf: gtk4::TextBuffer) {
} }
}; };
// Merge stderr into the channel too // Merge stderr into the channel too.
let stdout = child.stdout.take().unwrap(); // Both are Some because we spawned with Stdio::piped() above.
let stderr = child.stderr.take().unwrap(); let stdout = child.stdout.take().expect("stdout piped");
let stderr = child.stderr.take().expect("stderr piped");
let tx2 = sender.clone(); let tx2 = sender.clone();
std::thread::spawn(move || { std::thread::spawn(move || {

View file

@ -0,0 +1,235 @@
//! Reusable settings rows bound to a shared `toml_edit` document.
//!
//! Every row reads its current value from the document on build and writes the
//! single key it owns back into the document on change. A view collects rows,
//! then a [`save_button`] persists the whole document to disk in one shot — so
//! unmodelled keys and comments are always preserved (see `crate::config`).
use std::cell::RefCell;
use std::path::PathBuf;
use std::rc::Rc;
use gtk4::prelude::*;
use gtk4::{
Adjustment, Box as GBox, Button, DropDown, Entry, Expression, Label, Orientation,
SpinButton, StringList, Switch,
};
use toml_edit::DocumentMut;
use crate::config;
/// Shared, mutable config document handed to every row in a view.
pub type Doc = Rc<RefCell<DocumentMut>>;
/// A fixed key path into the document, e.g. `&["adapters", "power", "enabled"]`.
type Path = &'static [&'static str];
fn field_label(text: &str) -> Label {
let lbl = Label::new(Some(text));
lbl.set_hexpand(true);
lbl.set_xalign(0.0);
lbl
}
fn row(label: &str, control: &impl IsA<gtk4::Widget>) -> GBox {
let row = GBox::new(Orientation::Horizontal, 16);
row.append(&field_label(label));
control.set_halign(gtk4::Align::End);
control.set_valign(gtk4::Align::Center);
row.append(control);
row
}
/// A bold section heading with spacing above it.
pub fn section(text: &str) -> Label {
let lbl = Label::new(Some(text));
lbl.add_css_class("heading");
lbl.set_xalign(0.0);
lbl.set_margin_top(12);
lbl.set_margin_bottom(2);
lbl
}
/// Small dimmed helper text under a section or row.
pub fn hint(text: &str) -> Label {
let lbl = Label::new(Some(text));
lbl.add_css_class("dim-label");
lbl.set_xalign(0.0);
lbl.set_wrap(true);
lbl.set_margin_bottom(4);
lbl
}
/// Standard view scaffold: an outer vertical box with a title and a scrollable
/// content area. Append setting rows to the returned `content`, then append a
/// [`save_button`] to `outer`. Returns `(outer, content)`.
pub fn view_scaffold(title: &str) -> (GBox, GBox) {
let outer = GBox::new(Orientation::Vertical, 8);
outer.add_css_class("view-content");
let title_lbl = Label::new(Some(title));
title_lbl.add_css_class("title");
title_lbl.set_xalign(0.0);
outer.append(&title_lbl);
let content = GBox::new(Orientation::Vertical, 8);
let scroll = gtk4::ScrolledWindow::new();
scroll.set_vexpand(true);
scroll.set_hscrollbar_policy(gtk4::PolicyType::Never);
scroll.set_child(Some(&content));
outer.append(&scroll);
(outer, content)
}
pub fn switch_row(label: &str, doc: &Doc, path: Path, default: bool) -> GBox {
let cur = config::get_bool(&doc.borrow(), path).unwrap_or(default);
let sw = Switch::new();
sw.set_active(cur);
let doc = doc.clone();
sw.connect_active_notify(move |s| {
config::set_bool(&mut doc.borrow_mut(), path, s.is_active());
});
row(label, &sw)
}
pub fn entry_row(label: &str, doc: &Doc, path: Path, placeholder: &str, default: &str) -> GBox {
let cur = config::get_str(&doc.borrow(), path).unwrap_or_else(|| default.to_string());
let entry = Entry::new();
entry.set_text(&cur);
entry.set_hexpand(true);
entry.set_width_chars(28);
if !placeholder.is_empty() {
entry.set_placeholder_text(Some(placeholder));
}
let doc = doc.clone();
entry.connect_changed(move |e| {
config::set_str_or_remove(&mut doc.borrow_mut(), path, e.text().as_str());
});
row(label, &entry)
}
pub fn password_row(label: &str, doc: &Doc, path: Path) -> GBox {
let cur = config::get_str(&doc.borrow(), path).unwrap_or_default();
let entry = Entry::new();
entry.set_text(&cur);
entry.set_visibility(false);
entry.set_hexpand(true);
entry.set_width_chars(28);
entry.set_input_purpose(gtk4::InputPurpose::Password);
let doc = doc.clone();
entry.connect_changed(move |e| {
config::set_str_or_remove(&mut doc.borrow_mut(), path, e.text().as_str());
});
row(label, &entry)
}
/// A dropdown that stores the selected option string at `path`.
pub fn dropdown_row(label: &str, doc: &Doc, path: Path, options: &[&str], default: &str) -> GBox {
let cur = config::get_str(&doc.borrow(), path).unwrap_or_else(|| default.to_string());
let model = StringList::new(options);
let dd = DropDown::new(Some(model), Expression::NONE);
let sel = options.iter().position(|o| *o == cur).unwrap_or(0) as u32;
dd.set_selected(sel);
let owned: Vec<String> = options.iter().map(|s| s.to_string()).collect();
let doc = doc.clone();
dd.connect_selected_notify(move |dd| {
if let Some(opt) = owned.get(dd.selected() as usize) {
config::set_str(&mut doc.borrow_mut(), path, opt);
}
});
row(label, &dd)
}
/// An integer spin button storing its value at `path`.
pub fn spin_row(
label: &str,
doc: &Doc,
path: Path,
min: f64,
max: f64,
step: f64,
default: i64,
) -> GBox {
let cur = config::get_i64(&doc.borrow(), path).unwrap_or(default);
let adj = Adjustment::new(cur as f64, min, max, step, step, 0.0);
let spin = SpinButton::new(Some(&adj), step, 0);
let doc = doc.clone();
spin.connect_value_changed(move |s| {
config::set_i64(&mut doc.borrow_mut(), path, s.value() as i64);
});
row(label, &spin)
}
/// A fractional spin button (e.g. 0.01.0 confidence) storing a float.
pub fn spin_f64_row(
label: &str,
doc: &Doc,
path: Path,
min: f64,
max: f64,
step: f64,
digits: u32,
default: f64,
) -> GBox {
let cur = config::get_f64(&doc.borrow(), path).unwrap_or(default);
let adj = Adjustment::new(cur, min, max, step, step, 0.0);
let spin = SpinButton::new(Some(&adj), step, digits);
let doc = doc.clone();
spin.connect_value_changed(move |s| {
config::set_f64(&mut doc.borrow_mut(), path, s.value());
});
row(label, &spin)
}
/// A comma-separated list editor storing an array of strings at `path`.
pub fn csv_row(label: &str, doc: &Doc, path: Path, placeholder: &str) -> GBox {
let cur = config::get_str_list(&doc.borrow(), path).join(", ");
let entry = Entry::new();
entry.set_text(&cur);
entry.set_hexpand(true);
entry.set_width_chars(28);
if !placeholder.is_empty() {
entry.set_placeholder_text(Some(placeholder));
}
let doc = doc.clone();
entry.connect_changed(move |e| {
let items: Vec<String> = e
.text()
.split(',')
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
.collect();
config::set_str_list(&mut doc.borrow_mut(), path, &items);
});
row(label, &entry)
}
/// A Save button + transient status label that persists the document to `path`.
pub fn save_button(doc: &Doc, path: PathBuf) -> GBox {
let btn_row = GBox::new(Orientation::Horizontal, 12);
btn_row.set_margin_top(16);
let save_btn = Button::with_label("Save");
save_btn.add_css_class("suggested-action");
let status = Label::new(None);
status.add_css_class("dim-label");
let doc = doc.clone();
let status_c = status.clone();
save_btn.connect_clicked(move |_| match config::save_doc(&path, &doc.borrow()) {
Ok(()) => {
status_c.set_text("Saved");
let lbl = status_c.clone();
glib::timeout_add_seconds_local(3, move || {
lbl.set_text("");
glib::ControlFlow::Break
});
}
Err(e) => status_c.set_text(&format!("Error: {e}")),
});
btn_row.append(&save_btn);
btn_row.append(&status);
btn_row
}

98
build-local.sh Executable file
View file

@ -0,0 +1,98 @@
#!/usr/bin/env bash
# Local BOS ISO build for hermes (native Arch — no container needed).
#
# Builds straight from the working tree in ./iso. Two speedups vs the hestia
# container build:
# * runs natively (hermes is Arch; hestia needed a dockerised Arch)
# * no 2 GB scp afterwards — the ISO lands here, where we test it
#
# FAST_BUILD=1 zstd squashfs instead of xz -9e: compresses many times
# faster at the cost of a slightly larger image. Dev only.
#
# Usage: sudo ./build-local.sh # release-quality xz
# sudo FAST_BUILD=1 ./build-local.sh # fast dev iteration
set -euo pipefail
REPO="$(cd "$(dirname "$0")" && pwd)"
# WORK defaults to /tmp, but on hermes /tmp is a 16 GB tmpfs — a full xz build
# (uncompressed rootfs + squashfs + work copies) can exhaust it mid-build. Allow
# pointing it at the NVMe instead: WORK=/home/.../bos-work sudo ./build-local.sh
WORK="${WORK:-/tmp/bos-work}"
OUT="${OUT:-$REPO/out}"
# Build against a throwaway copy of the profile so the working tree stays clean
# when FAST_BUILD / the registry rewrite mutate profile files.
STAGE=/tmp/bos-iso-stage
rm -rf "$STAGE" && cp -a "$REPO/iso" "$STAGE"
# Rewrite the [breadway] pacman repo URL to the fastest reachable address.
# CI_BUILD=1 — container runs on hestia with --network=host; localhost:3002 is direct
# default — building on hermes; git.breadway.dev is flaky from there, use Tailscale
# Only ever rewrites the staged copy, never the committed pacman.conf.
if [ "${CI_BUILD:-0}" = "1" ]; then
sed -i 's#https://git.breadway.dev/api/packages/Breadway/arch/os#http://localhost:3002/api/packages/Breadway/arch/os#' "$STAGE/pacman.conf"
else
sed -i 's#https://git.breadway.dev/api/packages/Breadway/arch/os#http://100.66.238.26:3002/api/packages/Breadway/arch/os#' "$STAGE/pacman.conf"
fi
if [ "${FAST_BUILD:-0}" = "1" ]; then
echo "=== FAST_BUILD: squashfs -> zstd level 6 ==="
sed -i "s#^airootfs_image_tool_options=.*#airootfs_image_tool_options=('-comp' 'zstd' '-Xcompression-level' '6' '-b' '1M')#" "$STAGE/profiledef.sh"
fi
grep airootfs_image_tool_options "$STAGE/profiledef.sh"
# --- Bake this laptop's bakery-installed bread ecosystem into /etc/skel -------
# The bread apps are managed by bakery (which fetches release binaries from
# GitHub), not pacman. bakery needs DNS at install time, which the live/installed
# image doesn't have — so instead of running bakery on the target, we copy the
# exact binaries + bakery manifest this laptop already has into skel. Every user
# created from skel (the live user and the installed user) then gets the same
# versions `bakery list` reports here, fully offline. Copied at build time so the
# binaries never bloat the git repo and always track the current bakery state.
BREAD_BINS=(bakery bread breadd breadman breadbar breadbox breadbox-sync breadcrumbs breadpad breadpaper bread-theme)
LAPTOP_HOME="${LAPTOP_HOME:-$(getent passwd "${SUDO_USER:-$USER}" | cut -d: -f6)}"
BAKERY_BIN="$LAPTOP_HOME/.local/bin"
BAKERY_STATE="$LAPTOP_HOME/.local/state/bakery"
BAKERY_CACHE="$LAPTOP_HOME/.cache/bakery"
SKEL="$STAGE/airootfs/etc/skel"
echo "=== baking bakery bread ecosystem from $LAPTOP_HOME ==="
install -d -m 0755 "$SKEL/.local/bin" "$SKEL/.local/state/bakery" "$SKEL/.cache/bakery"
for b in "${BREAD_BINS[@]}"; do
install -m 0755 "$BAKERY_BIN/$b" "$SKEL/.local/bin/$b"
done
install -m 0644 "$BAKERY_STATE/installed.json" "$SKEL/.local/state/bakery/installed.json"
# bakery fetches its package index from dl.breadway.dev (then a GitHub fallback),
# but falls back to a cached index when both are unreachable. With no network/DNS
# in the live/installed image, even `bakery list` errors unless that cache exists,
# so bake it in too — then bakery works fully offline (list/info from cache;
# install/update still need network, as expected).
install -m 0644 "$BAKERY_CACHE/index.json" "$SKEL/.cache/bakery/index.json"
echo "baked: $(ls "$SKEL/.local/bin")"
# mkarchiso resets every airootfs file to 0644, so executables must be declared
# in profiledef.sh's file_permissions array or they ship non-executable and the
# exec-once launches fail with "permission denied". Inject a 0755 entry for each
# baked binary right after the array opener (keeps the binary list in one place).
perm_file="$(mktemp)"
for b in "${BREAD_BINS[@]}"; do
printf ' ["/etc/skel/.local/bin/%s"]="0:0:755"\n' "$b" >>"$perm_file"
done
sed -i "/^file_permissions=(/r $perm_file" "$STAGE/profiledef.sh"
rm -f "$perm_file"
echo "=== file_permissions after injection ==="; grep -A14 '^file_permissions=(' "$STAGE/profiledef.sh"
# Pin one timestamp for the whole build. Without this, mkarchiso derives the
# boot-config UUID (%ARCHISO_UUID%) when it starts and the iso9660 volume UUID
# when xorriso writes the image at the end — on a slow build these diverge by
# the build duration, so the initramfs searches /dev/disk/by-uuid/<wrong-uuid>,
# never finds the medium, and drops to a recovery shell. Fixing the epoch makes
# both derive from the same instant (and makes builds reproducible).
export SOURCE_DATE_EPOCH="${SOURCE_DATE_EPOCH:-$(date +%s)}"
echo "SOURCE_DATE_EPOCH=$SOURCE_DATE_EPOCH ($(date -u -d "@$SOURCE_DATE_EPOCH" +%Y-%m-%d-%H-%M-%S-00))"
echo "=== running mkarchiso ==="
rm -rf "$WORK" && mkdir -p "$OUT"
mkarchiso -v -w "$WORK" -o "$OUT" "$STAGE"
echo "=== RESULT ==="
if ls -lh "$OUT"/*.iso 2>/dev/null; then echo "ISO BUILT OK -> $OUT"; else echo "ISO BUILD FAILED"; exit 1; fi

View file

@ -46,7 +46,6 @@ input {
} }
dwindle { dwindle {
pseudotile = true
preserve_split = true preserve_split = true
} }

View file

@ -23,7 +23,7 @@ slideshow: "show.qml"
slideshowAPI: 2 slideshowAPI: 2
style: style:
sidebarBackground: "#3b4252" sidebarBackground: "#230b00"
sidebarText: "#eceff4" sidebarText: "#f1dcbd"
sidebarTextSelect: "#5e81ac" sidebarTextSelect: "#EAB672"
sidebarTextHighlight: "#eceff4" sidebarTextHighlight: "#ffffff"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9 KiB

After

Width:  |  Height:  |  Size: 42 KiB

Before After
Before After

View file

@ -0,0 +1,23 @@
/* BOS Calamares styling.
*
* The branding `style:` sidebar keys were being overridden, leaving the
* step tabs invisible. This forces them: bread-palette sidebar with clearly
* legible step labels. (A full installer retheme is tracked separately.)
*/
/* Left sidebar / progress steps */
#sidebarApp {
background-color: #230b00;
}
#sidebarApp QLabel {
color: #f1dcbd;
font-size: 11pt;
padding: 3px 0;
}
/* Logo at the top of the sidebar — keep aspect ratio, don't stretch */
#logoApp {
qproperty-alignment: AlignCenter;
margin: 14px 8px;
}

View file

@ -8,3 +8,32 @@ mountOptions:
options: [noatime, "compress=zstd", "space_cache=v2"] options: [noatime, "compress=zstd", "space_cache=v2"]
- filesystem: vfat - filesystem: vfat
options: [umask=0077] options: [umask=0077]
# API filesystems mounted into the target so chroot steps work. Without these
# the chroot has no /proc or /dev and `mkinitcpio` aborts ("/proc must be
# mounted!" / "/dev must be mounted!") and grub-install can't probe properly.
# All are real-fs mounts (not bind) — Calamares 3.4.2 here applies fs-type mounts
# reliably but not bind-type ones, so /dev uses a fresh devtmpfs (which still
# exposes all device nodes). extraMountsEfi adds efivars on UEFI so grub-install
# can write an NVRAM boot entry.
extraMounts:
- device: proc
fs: proc
mountPoint: /proc
- device: sys
fs: sysfs
mountPoint: /sys
- device: udev
fs: devtmpfs
mountPoint: /dev
- device: devpts
fs: devpts
mountPoint: /dev/pts
- device: tmpfs
fs: tmpfs
mountPoint: /run
extraMountsEfi:
- device: efivarfs
fs: efivarfs
mountPoint: /sys/firmware/efi/efivars

View file

@ -0,0 +1,6 @@
---
# Boot-splash theme for the installed system. Calamares sets this as the default
# plymouth theme and signals initcpiocfg to add the plymouth hook to the
# initramfs. The post-install script also enforces the hook + a quiet/splash
# cmdline as a belt-and-suspenders.
plymouth_theme: bos

View file

@ -0,0 +1,8 @@
---
# Lay the kernel into the target /boot before the bootloader/initramfs steps.
# Runs in the live environment (not the chroot) so it can read the ISO boot dir.
dontChroot: true
timeout: 60
script:
- "/usr/bin/bash /usr/local/bin/bos-copy-kernel ${ROOT}"

View file

@ -1,3 +1,10 @@
--- ---
# BOS finalization, run after the native initcpio + bootloader modules. It does
# only fast, non-boot-critical work (live-medium cleanup, snapper, services,
# dotfiles), so the shellprocess timeout can no longer leave the system
# unbootable — the boot-critical steps are owned by dedicated Calamares modules.
# A generous timeout is kept as a safety margin, and the leading "-" keeps a
# non-zero exit non-fatal to the install.
timeout: 600
script: script:
- "-/usr/bin/bash /etc/calamares/post-install.sh" - "-/usr/bin/bash /etc/calamares/post-install.sh"

View file

@ -38,3 +38,4 @@ passwordRequirements:
- minlen=6 - minlen=6
allowWeakPasswords: false allowWeakPasswords: false
userShell: /bin/zsh

View file

@ -1,7 +1,11 @@
#!/bin/bash #!/bin/bash
# Runs inside the installed-system chroot (Calamares shellprocess, after the # BOS-specific finalization, run inside the installed-system chroot (Calamares
# bootloader step). Best-effort: a single failure must not abort the rest, so # shellprocess), AFTER the native initcpio module has built the initramfs. The
# we deliberately do NOT use `set -e`. # kernel (shellprocess@kernel) and initramfs (initcpio) are in place by now, so
# this script installs GRUB and does the rest of setup. Calamares' own
# `bootloader`/`grubcfg` modules are NOT used — in this archiso layout they leave
# the ESP empty and abort; the explicit grub-install below is verified to boot.
# Best-effort: do NOT use `set -e`; a single failure here must not abort the rest.
set -uo pipefail set -uo pipefail
MAIN_USER="$(getent passwd 1000 | cut -d: -f1 || true)" MAIN_USER="$(getent passwd 1000 | cut -d: -f1 || true)"
@ -20,22 +24,77 @@ userdel -r liveuser 2>/dev/null || true
passwd -l root || true passwd -l root || true
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Rebuild the initramfs for a real install — the live image ships the archiso # Pacman keyring. The live medium's /etc/pacman.d/gnupg doesn't reliably carry
# hooks, which would send the installed system into the live-boot path. # over to the target (unpackfs may skip it / perms differ), leaving the installed
# system unable to verify package signatures — the first `pacman -Syu` then dies
# with "keyring is not writable / required key missing". Initialise it here so a
# fresh install can update out of the box. archlinux-keyring is already present;
# [breadway] is SigLevel=Never so it needs no key.
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
rm -f /etc/mkinitcpio.conf.d/archiso.conf if command -v pacman-key &>/dev/null; then
cat >/etc/mkinitcpio.d/linux.preset <<'PRESET' pacman-key --init || echo "WARN: pacman-key --init failed"
# mkinitcpio preset file for the 'linux' package pacman-key --populate archlinux || echo "WARN: pacman-key --populate failed"
ALL_config="/etc/mkinitcpio.conf" fi
ALL_kver="/boot/vmlinuz-linux"
PRESETS=('default' 'fallback')
default_image="/boot/initramfs-linux.img"
fallback_image="/boot/initramfs-linux-fallback.img"
fallback_options="-S autodetect"
PRESET
mkinitcpio -P || echo "WARN: mkinitcpio regeneration failed"
# Refresh GRUB so it references the rebuilt initramfs (and the new fallback). # ---------------------------------------------------------------------------
# Initramfs HOOKS: microcode + plymouth. Edit HOOKS first, rebuild once below.
# microcode — embeds the (autodetect-pruned) CPU microcode into the initramfs
# so it loads at early boot. The live ISO embeds ucode the same way, so the
# ISO /boot carries no separate ucode image and bos-copy-kernel stages none
# onto the target — the installed initramfs must therefore carry it itself.
# Must sit AFTER `autodetect` so it's pruned to the running CPU's microcode.
# plymouth — the BOS boot splash. Only the udev `plymouth` hook exists (there
# is NO `sd-plymouth`), so always insert it after `udev`.
# All best-effort: a failure here still leaves a bootable initramfs.
# ---------------------------------------------------------------------------
if [[ -f /etc/mkinitcpio.conf ]]; then
if ! grep -qE '^HOOKS=.*\bmicrocode\b' /etc/mkinitcpio.conf; then
sed -i 's/^\(HOOKS=.*\bautodetect\b\)/\1 microcode/' /etc/mkinitcpio.conf \
|| echo "WARN: adding microcode hook failed"
fi
if command -v plymouth-set-default-theme &>/dev/null \
&& ! grep -qE '^HOOKS=.*\bplymouth\b' /etc/mkinitcpio.conf; then
sed -i 's/^\(HOOKS=.*\budev\b\)/\1 plymouth/' /etc/mkinitcpio.conf \
|| echo "WARN: adding plymouth hook failed"
fi
fi
# ---------------------------------------------------------------------------
# Boot splash (Plymouth) — BOS logo + spinner instead of kernel text. Set the
# theme + cmdline BEFORE grub so grub.cfg picks up the new cmdline.
# ---------------------------------------------------------------------------
if command -v plymouth-set-default-theme &>/dev/null; then
# Clean boot: splash activates plymouth; hiding systemd status removes the
# "[ OK ] Started ..." text (what looked like kernel output) even if the
# splash itself doesn't grab the display (e.g. in some VMs).
if ! grep -q 'splash' /etc/default/grub 2>/dev/null; then
sed -i 's/^\(GRUB_CMDLINE_LINUX_DEFAULT="\)/\1splash quiet vt.global_cursor_default=0 systemd.show_status=false rd.systemd.show_status=false rd.udev.log_level=3 /' \
/etc/default/grub || echo "WARN: adding splash cmdline failed"
fi
plymouth-set-default-theme bos || echo "WARN: plymouth-set-default-theme failed"
fi
# Rebuild every preset (default + fallback that bos-copy-kernel wrote) so the
# microcode + plymouth HOOKS above are actually baked into the initramfs.
mkinitcpio -P || echo "WARN: mkinitcpio -P failed"
# ---------------------------------------------------------------------------
# Install GRUB (UEFI). /boot now has the kernel + initramfs, and the mount
# module has bind-mounted /proc /sys /dev /run + efivars into this chroot, so
# both grub-install passes and grub-mkconfig succeed.
# 1. NVRAM entry (EFI/BOS/grubx64.efi + a firmware boot entry)
# 2. --removable copy to EFI/BOOT/BOOTX64.EFI, so firmware that ignores/loses
# the NVRAM entry (the "no boot device / PXE fallback" failure) still finds
# a bootloader.
# ---------------------------------------------------------------------------
if command -v grub-install &>/dev/null; then
grub-install --target=x86_64-efi --efi-directory=/boot/efi \
--bootloader-id=BOS --recheck \
|| echo "WARN: grub-install (nvram) failed"
grub-install --target=x86_64-efi --efi-directory=/boot/efi \
--removable --recheck \
|| echo "WARN: grub-install (removable) failed"
fi
if command -v grub-mkconfig &>/dev/null; then if command -v grub-mkconfig &>/dev/null; then
grub-mkconfig -o /boot/grub/grub.cfg || echo "WARN: grub-mkconfig failed" grub-mkconfig -o /boot/grub/grub.cfg || echo "WARN: grub-mkconfig failed"
fi fi
@ -57,27 +116,61 @@ if command -v snapper &>/dev/null; then
fi fi
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# System services. # System services. Enable each one INDEPENDENTLY: `systemctl enable a b c`
# resolves every unit first and enables NONE if any one can't be loaded, so a
# single wrong/absent unit name would silently leave NetworkManager (etc.)
# disabled. The loop isolates failures to the offending unit.
# greetd — graphical login (shipped disabled; live uses tty autologin)
# grub-btrfsd — regenerates GRUB snapshot entries (the unit is grub-btrfsd.service,
# NOT grub-btrfs.path, which no longer exists)
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
systemctl enable NetworkManager bluetooth snapper-cleanup.timer grub-btrfs.path \ for unit in NetworkManager.service bluetooth.service systemd-timesyncd.service \
|| echo "WARN: enabling some services failed" tlp.service greetd.service snapper-cleanup.timer grub-btrfsd.service \
fstrim.timer cups.socket avahi-daemon.service ufw.service \
fwupd-refresh.timer reflector.timer; do
systemctl enable "$unit" || echo "WARN: failed to enable $unit"
done
systemctl set-default graphical.target || echo "WARN: set-default graphical failed"
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Bread ecosystem via bakery (needs network — non-fatal so an offline install # mDNS resolution (nss-mdns): insert mdns_minimal into the hosts: line so the
# still completes; the user can run it after first boot). # resolver answers *.local (network printers, other hosts) via avahi. Idempotent.
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
if [[ -n "$MAIN_USER" ]] && command -v bakery &>/dev/null; then if [[ -f /etc/nsswitch.conf ]] && ! grep -q 'mdns_minimal' /etc/nsswitch.conf; then
sudo -u "$MAIN_USER" bakery install bread breadbar breadbox breadcrumbs breadpad bos-settings \ sed -i 's/^\(hosts:[[:space:]]*\)/\1mdns_minimal [NOTFOUND=return] /' \
|| echo "WARN: bakery install failed (no network during install?); run it after first boot" /etc/nsswitch.conf || echo "WARN: wiring nss-mdns failed"
fi fi
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Deploy dotfiles into the user's home (don't clobber existing files). # Firewall: deny inbound by default, allow outbound, and permit inbound mDNS so
# avahi printer/service discovery keeps working. Best-effort — rule application
# happens at boot; here we only persist the policy + enable the unit.
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
if [[ -n "$MAIN_USER" && -d /etc/skel/.config ]]; then if command -v ufw &>/dev/null; then
mkdir -p "/home/$MAIN_USER/.config" ufw default deny incoming || echo "WARN: ufw default deny incoming failed"
cp -rn /etc/skel/.config/. "/home/$MAIN_USER/.config/" || true ufw default allow outgoing || echo "WARN: ufw default allow outgoing failed"
chown -R "$MAIN_USER:$MAIN_USER" "/home/$MAIN_USER/.config" || true ufw allow 5353/udp || echo "WARN: ufw allow mDNS failed"
ufw --force enable || echo "WARN: ufw enable failed"
fi
# The bread ecosystem (bakery + bread, breadbar, breadbox, breadcrumbs, breadpad)
# is bakery-managed, not pacman: the binaries and bakery manifest live in
# /etc/skel/.local (baked in at ISO build time) and are copied into the user's
# home below, so the install works fully offline with no DNS for bakery/GitHub.
# bos-settings is the only pacman bread package and was installed by unpackfs.
# ---------------------------------------------------------------------------
# Deploy dotfiles + the bakery bread ecosystem into the user's home (Calamares
# already seeds from /etc/skel, but copy explicitly too so a fresh install is
# self-contained even if the users module skips skel). Don't clobber existing.
# ---------------------------------------------------------------------------
if [[ -n "$MAIN_USER" && -d /etc/skel ]]; then
for d in .config .local .cache; do
[[ -d "/etc/skel/$d" ]] || continue
mkdir -p "/home/$MAIN_USER/$d"
cp -rn "/etc/skel/$d/." "/home/$MAIN_USER/$d/" || true
chown -R "$MAIN_USER:$MAIN_USER" "/home/$MAIN_USER/$d" || true
done
sudo -u "$MAIN_USER" xdg-user-dirs-update || true sudo -u "$MAIN_USER" xdg-user-dirs-update || true
fi fi

View file

@ -1,6 +1,13 @@
--- ---
modules-search: [/etc/calamares/modules, /usr/lib/calamares/modules] modules-search: [/etc/calamares/modules, /usr/lib/calamares/modules]
# Second shellprocess instance: copies the live kernel into the target /boot
# (archiso keeps it out of the squashfs) before the bootloader step runs.
instances:
- id: kernel
module: shellprocess
config: shellprocess-kernel.conf
sequence: sequence:
- show: - show:
- welcome - welcome
@ -22,7 +29,21 @@ sequence:
- networkcfg - networkcfg
- hwclock - hwclock
- packages - packages
- bootloader # archiso strips the kernel from the squashfs; stage it, drop the archiso
# initramfs config, and write a stock mkinitcpio preset before initcpio runs.
- shellprocess@kernel
# plymouthcfg sets the boot-splash theme and flags plymouth in use, so
# initcpiocfg adds the plymouth hook to the initramfs that initcpio builds.
- plymouthcfg
# Native initramfs generation (works reliably here). The native `bootloader`
# and `grubcfg` modules do NOT — in this archiso layout they leave the ESP
# empty and abort the install, so GRUB is installed explicitly in
# post-install.sh instead (grub-install --removable + NVRAM + grub-mkconfig,
# the sequence verified to produce a bootable system).
- initcpiocfg
- initcpio
# BOS finalization: GRUB install + cleanup + snapper + services + dotfiles.
# All fast, and runs after initcpio so /boot has the kernel + initramfs.
- shellprocess - shellprocess
- umount - umount
- show: - show:

View file

@ -0,0 +1,7 @@
SHELL=/usr/bin/zsh
GROUP=users
HOME=/home
INACTIVE=-1
EXPIRE=
SKEL=/etc/skel
CREATE_MAIL_SPOOL=no

View file

@ -0,0 +1,12 @@
# greetd drives login on the INSTALLED system. It is shipped DISABLED in the
# squashfs and enabled by post-install.sh; the live ISO instead autologins
# liveuser on tty1 (see getty@tty1 drop-in) so the installer comes straight up.
#
# tuigreet shows a minimal greeter, then launches the BOS Hyprland session via
# bos-session (which fixes up PATH for the bakery bread apps).
[terminal]
vt = 1
[default_session]
command = "tuigreet --remember --time --cmd /usr/local/bin/bos-session"
user = "greeter"

View file

@ -0,0 +1,45 @@
#
# 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 and the bread ecosystem packages
# (bread, breadbar, breadbox, breadcrumbs, breadpad, bos-settings).
# (calamares comes from the official extra repo, not here.)
#
# Packages are published to the Forgejo Arch registry (group "os") by the
# .forgejo/workflows/package.yml workflow in each repo, on tag push.
#
# Forgejo signs the repo db with a key pacman can't look up, so TrustAll
# fails. SigLevel = Never skips verification (acceptable for this private
# repo over TLS). Future improvement: import Forgejo's signing key and
# switch to SigLevel = Required for full package verification.
# -----------------------------------------------------------------------
# The section name must match Forgejo's served db filename
# ({owner}.{group}.{domain}.db) — pacman fetches "<section>.db" from Server.
[Breadway.os.git.breadway.dev]
SigLevel = Never
Server = https://git.breadway.dev/api/packages/Breadway/arch/os/$arch

View file

@ -0,0 +1,11 @@
# BOS default mirrorlist.
#
# geo.mirror.pkgbuild.com is the official Arch geo-IP redirect — it routes to a
# nearby mirror anywhere in the world, so pacman works out of the box without
# region-specific configuration. The others are reliable global fallbacks.
#
# To optimise for your location/speed later: sudo reflector --save \
# /etc/pacman.d/mirrorlist --protocol https --latest 20 --sort rate
Server = https://geo.mirror.pkgbuild.com/$repo/os/$arch
Server = https://mirror.rackspace.com/archlinux/$repo/os/$arch
Server = https://mirror.leaseweb.net/archlinux/$repo/os/$arch

View file

@ -0,0 +1,9 @@
# Put the per-user bakery bin dir on PATH. The bread ecosystem (breadd, breadbar,
# breadbox, …) is installed there by bakery, and the Hyprland session launches
# them via `exec-once`, which resolves against the PATH it inherits from the
# login shell. Arch's stock /etc/profile does not add ~/.local/bin, so do it here
# for every login shell (live user and installed user alike).
case ":$PATH:" in
*":$HOME/.local/bin:"*) ;;
*) export PATH="$HOME/.local/bin:$PATH" ;;
esac

View file

@ -0,0 +1,28 @@
{
"wallpaper": "/usr/share/backgrounds/bos/bread-background.png",
"alpha": "100",
"special": {
"background": "#0c0c0c",
"foreground": "#e8e8e8",
"cursor": "#eab672"
},
"colors": {
"color0": "#1a1a1a",
"color1": "#b98749",
"color2": "#cd9450",
"color3": "#e3a85c",
"color4": "#eab672",
"color5": "#f6c477",
"color6": "#eabe82",
"color7": "#d8d8d8",
"color8": "#3a3a3a",
"color9": "#b98749",
"color10": "#cd9450",
"color11": "#e3a85c",
"color12": "#eab672",
"color13": "#f6c477",
"color14": "#eabe82",
"color15": "#f5f5f5"
}
}

View file

@ -1,8 +1,21 @@
# breadd daemon configuration.
# Every section is optional and every adapter defaults to enabled; this file
# just makes the defaults explicit. See the bread docs for the full schema.
[daemon]
log_level = "info" log_level = "info"
[adapters] [adapters.hyprland]
keyboard = true enabled = true
mouse = true
touchpad = true [adapters.udev]
bluetooth = true enabled = true
gamepad = true
[adapters.power]
enabled = true
[adapters.network]
enabled = true
[adapters.bluetooth]
enabled = true

View file

@ -0,0 +1,26 @@
-- low-battery-warning — notify once when the battery runs low (zero-config).
-- Shipped active in BOS; auto-discovered by breadd. Safe on desktops too
-- (simply never fires without a battery).
local M = bread.module({ name = "low-battery-warning", version = "1.0.0" })
local warned = false
function M.on_load()
bread.on("bread.power.battery.low", function(event)
if warned then return end
warned = true
local pct = event.data.battery_percent or "?"
bread.notify("Battery low (" .. pct .. "%). Plug in soon.", {
urgency = "critical",
title = "Battery",
timeout = 10000,
})
end)
bread.on("bread.power.ac.connected", function()
warned = false
end)
end
return M

View file

@ -0,0 +1,7 @@
[Settings]
gtk-application-prefer-dark-theme=1
gtk-theme-name=Adwaita-dark
gtk-icon-theme-name=Papirus-Dark
gtk-cursor-theme-name=Bibata-Modern-Ice
gtk-cursor-theme-size=24
gtk-font-name=Noto Sans 11

View file

@ -0,0 +1,7 @@
[Settings]
gtk-application-prefer-dark-theme=1
gtk-theme-name=Adwaita-dark
gtk-icon-theme-name=Papirus-Dark
gtk-cursor-theme-name=Bibata-Modern-Ice
gtk-cursor-theme-size=24
gtk-font-name=Noto Sans 11

View file

@ -0,0 +1,37 @@
# Idle management (hypridle). Conservative, mainstream-OS-like defaults:
# dim -> screen off -> lock -> suspend, all respecting idle inhibitors (so media
# playback, etc. won't dim or suspend the machine). Vendor-neutral: nothing here
# is Intel/AMD specific.
general {
lock_cmd = pidof hyprlock || hyprlock
before_sleep_cmd = loginctl lock-session
after_sleep_cmd = hyprctl dispatch dpms on
ignore_dbus_inhibit = false
}
# Dim the backlight after 5 minutes (restored on activity). No-op on hardware
# without a backlight (desktops / VMs).
listener {
timeout = 300
on-timeout = brightnessctl -s set 10%
on-resume = brightnessctl -r
}
# Turn the display off after 8 minutes.
listener {
timeout = 480
on-timeout = hyprctl dispatch dpms off
on-resume = hyprctl dispatch dpms on
}
# Lock shortly after the screen turns off.
listener {
timeout = 510
on-timeout = loginctl lock-session
}
# Suspend after 20 minutes of inactivity (skipped while something inhibits idle).
listener {
timeout = 1200
on-timeout = systemctl suspend
}

View file

@ -1,56 +0,0 @@
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
}

View file

@ -0,0 +1,222 @@
-- BOS Hyprland configuration — native Lua config (Hyprland 0.55+).
-- hyprlang (.conf) is deprecated; this uses the built-in `hl` API.
-- Single-file and non-modular by design. Reference: https://wiki.hypr.land/
local mod = "SUPER"
-- ---------------------------------------------------------------------------
-- Monitors — generic default that works on any hardware.
-- ---------------------------------------------------------------------------
hl.monitor({ output = "", mode = "preferred", position = "auto", scale = "auto" })
-- ---------------------------------------------------------------------------
-- Core settings.
-- ---------------------------------------------------------------------------
hl.config({
general = {
gaps_in = 5,
gaps_out = 10,
border_size = 2,
col = {
active_border = "rgba(88c0d0ff)",
inactive_border = "rgba(4c566aff)",
},
layout = "dwindle",
resize_on_border = true,
},
decoration = {
rounding = 8,
active_opacity = 1.0,
inactive_opacity = 1.0,
blur = {
enabled = true,
size = 6,
passes = 2,
new_optimizations = true,
},
shadow = {
enabled = true,
range = 12,
render_power = 3,
},
},
input = {
kb_layout = "us",
follow_mouse = 1,
touchpad = { natural_scroll = true },
},
dwindle = {
preserve_split = true,
},
animations = {
enabled = true,
},
misc = {
disable_hyprland_logo = true,
disable_splash_rendering = true,
},
})
-- ---------------------------------------------------------------------------
-- Animations — snappy curves + per-leaf speeds (matches the reference laptop;
-- the hl.config default above is much slower).
-- ---------------------------------------------------------------------------
local curves = {
easeOutQuint = { type = "bezier", points = { { 0.23, 1 }, { 0.32, 1 } } },
easeInOutCubic = { type = "bezier", points = { { 0.65, 0.05 }, { 0.36, 1 } } },
almostLinear = { type = "bezier", points = { { 0.5, 0.5 }, { 0.75, 1 } } },
quick = { type = "bezier", points = { { 0.15, 0 }, { 0.1, 1 } } },
}
for name, curve in pairs(curves) do
hl.curve(name, curve)
end
local animations = {
{ leaf = "global", enabled = true, speed = 10, bezier = "default" },
{ leaf = "border", enabled = true, speed = 5.39, bezier = "easeOutQuint" },
{ leaf = "windows", enabled = true, speed = 4.79, bezier = "easeOutQuint" },
{ leaf = "windowsIn", enabled = true, speed = 4.1, bezier = "easeOutQuint", style = "popin 87%" },
{ leaf = "windowsOut", enabled = true, speed = 1.49, bezier = "linear", style = "popin 87%" },
{ leaf = "fade", enabled = true, speed = 3.03, bezier = "quick" },
{ leaf = "layers", enabled = true, speed = 3.81, bezier = "easeOutQuint" },
{ leaf = "workspaces", enabled = true, speed = 1.94, bezier = "almostLinear", style = "fade" },
}
for _, animation in ipairs(animations) do
hl.animation(animation)
end
-- ---------------------------------------------------------------------------
-- Window rules — float + centre the onboarding popups (kitty --class …).
-- ---------------------------------------------------------------------------
hl.window_rule({ name = "bos-keybinds", match = { class = "^(bos-keybinds)$" }, float = true, size = { 760, 720 } })
hl.window_rule({ name = "bos-welcome", match = { class = "^(bos-welcome)$" }, float = true, size = { 700, 560 } })
hl.window_rule({ name = "bos-netsetup", match = { class = "^(bos-netsetup)$" }, float = true, size = { 700, 560 } })
-- ---------------------------------------------------------------------------
-- Environment (vendor-neutral; no GPU-specific vars so it works on Intel/AMD).
-- ---------------------------------------------------------------------------
hl.env("XCURSOR_SIZE", "24")
hl.env("HYPRCURSOR_SIZE", "24")
hl.env("XCURSOR_THEME", "Bibata-Modern-Ice")
hl.env("MOZ_ENABLE_WAYLAND", "1")
hl.env("QT_QPA_PLATFORM", "wayland;xcb")
hl.env("QT_QPA_PLATFORMTHEME", "qt5ct")
hl.env("QT_WAYLAND_DISABLE_WINDOWDECORATION", "1")
hl.env("SDL_VIDEODRIVER", "wayland")
hl.env("ELECTRON_OZONE_PLATFORM_HINT", "auto")
hl.env("_JAVA_AWT_WM_NONREPARENTING", "1")
-- kitty sets its own background_opacity (see kitty.conf), so the global blur
-- above blurs behind the terminal while keeping text fully opaque.
-- ---------------------------------------------------------------------------
-- Standard BOS keybinds (SUPER = mod).
-- ---------------------------------------------------------------------------
-- Apps / window management
hl.bind(mod .. " + RETURN", hl.dsp.exec_cmd("kitty"))
hl.bind(mod .. " + BACKSPACE", hl.dsp.window.close())
hl.bind(mod .. " + SPACE", hl.dsp.exec_cmd("breadbox"))
hl.bind(mod .. " + E", hl.dsp.exec_cmd("nautilus"))
hl.bind(mod .. " + B", hl.dsp.exec_cmd("zen-browser"))
hl.bind(mod .. " + U", hl.dsp.exec_cmd("breadpad"))
hl.bind(mod .. " + M", hl.dsp.exec_cmd("breadman"))
hl.bind(mod .. " + comma", hl.dsp.exec_cmd("bos-settings"))
hl.bind(mod .. " + slash", hl.dsp.exec_cmd("bos-keybinds"))
hl.bind(mod .. " + L", hl.dsp.exec_cmd("loginctl lock-session"))
hl.bind(mod .. " + F", hl.dsp.window.fullscreen({ action = "toggle" }))
hl.bind(mod .. " + V", hl.dsp.window.float({ action = "toggle" }))
hl.bind(mod .. " + SHIFT + V", hl.dsp.exec_cmd([[bash -c 'cliphist list | fzf --reverse --prompt="Clipboard > " | cliphist decode | wl-copy']]))
hl.bind(mod .. " + T", hl.dsp.layout("togglesplit"))
hl.bind(mod .. " + Tab", hl.dsp.focus({ urgent_or_last = true }))
hl.bind(mod .. " + N", hl.dsp.exit())
-- Screenshots (grim + slurp + wl-clipboard)
hl.bind(mod .. " + SHIFT + S", hl.dsp.exec_cmd([[bash -c 'mkdir -p ~/Pictures/Screenshots && grim -g "$(slurp)" ~/Pictures/Screenshots/$(date +%Y%m%d-%H%M%S).png']]))
hl.bind(mod .. " + SHIFT + C", hl.dsp.exec_cmd([[bash -c 'grim -g "$(slurp)" - | wl-copy']]))
hl.bind(mod .. " + SHIFT + P", hl.dsp.exec_cmd([[bash -c 'mkdir -p ~/Pictures/Screenshots && grim ~/Pictures/Screenshots/$(date +%Y%m%d-%H%M%S).png']]))
-- Focus (directional)
hl.bind(mod .. " + left", hl.dsp.focus({ direction = "left" }))
hl.bind(mod .. " + right", hl.dsp.focus({ direction = "right" }))
hl.bind(mod .. " + up", hl.dsp.focus({ direction = "up" }))
hl.bind(mod .. " + down", hl.dsp.focus({ direction = "down" }))
-- Move window (directional, vim keys)
hl.bind(mod .. " + SHIFT + h", hl.dsp.window.move({ direction = "left" }))
hl.bind(mod .. " + SHIFT + j", hl.dsp.window.move({ direction = "down" }))
hl.bind(mod .. " + SHIFT + k", hl.dsp.window.move({ direction = "up" }))
hl.bind(mod .. " + SHIFT + l", hl.dsp.window.move({ direction = "right" }))
-- Resize active window (arrows)
hl.bind(mod .. " + SHIFT + right", hl.dsp.window.resize({ x = 30, y = 0, relative = true }), { repeating = true })
hl.bind(mod .. " + SHIFT + left", hl.dsp.window.resize({ x = -30, y = 0, relative = true }), { repeating = true })
hl.bind(mod .. " + SHIFT + up", hl.dsp.window.resize({ x = 0, y = -30, relative = true }), { repeating = true })
hl.bind(mod .. " + SHIFT + down", hl.dsp.window.resize({ x = 0, y = 30, relative = true }), { repeating = true })
-- Workspaces 110 (0 = workspace 10)
for i = 1, 10 do
local key = tostring(i % 10)
hl.bind(mod .. " + " .. key, hl.dsp.focus({ workspace = i }))
hl.bind(mod .. " + SHIFT + " .. key, hl.dsp.window.move({ workspace = i }))
end
-- Workspace cycling
hl.bind(mod .. " + bracketright", hl.dsp.focus({ workspace = "e+1" }))
hl.bind(mod .. " + bracketleft", hl.dsp.focus({ workspace = "e-1" }))
hl.bind(mod .. " + SHIFT + bracketright", hl.dsp.window.move({ workspace = "e+1" }))
hl.bind(mod .. " + SHIFT + bracketleft", hl.dsp.window.move({ workspace = "e-1" }))
-- Mouse
hl.bind(mod .. " + mouse_down", hl.dsp.focus({ workspace = "e+1" }))
hl.bind(mod .. " + mouse_up", hl.dsp.focus({ workspace = "e-1" }))
hl.bind(mod .. " + mouse:272", hl.dsp.window.drag(), { mouse = true })
hl.bind(mod .. " + mouse:273", hl.dsp.window.resize(), { mouse = true })
-- Media / hardware keys (work locked, i.e. on the lock screen too)
hl.bind("XF86AudioRaiseVolume", hl.dsp.exec_cmd("wpctl set-volume -l 1 @DEFAULT_AUDIO_SINK@ 5%+"), { locked = true, repeating = true })
hl.bind("XF86AudioLowerVolume", hl.dsp.exec_cmd("wpctl set-volume @DEFAULT_AUDIO_SINK@ 5%-"), { locked = true, repeating = true })
hl.bind("XF86AudioMute", hl.dsp.exec_cmd("wpctl set-mute @DEFAULT_AUDIO_SINK@ toggle"), { locked = true })
hl.bind("XF86AudioMicMute", hl.dsp.exec_cmd("wpctl set-mute @DEFAULT_AUDIO_SOURCE@ toggle"), { locked = true })
hl.bind("XF86MonBrightnessUp", hl.dsp.exec_cmd("brightnessctl -e4 -n2 set 5%+"), { locked = true, repeating = true })
hl.bind("XF86MonBrightnessDown", hl.dsp.exec_cmd("brightnessctl -e4 -n2 set 5%-"), { locked = true, repeating = true })
hl.bind("XF86AudioNext", hl.dsp.exec_cmd("playerctl next"), { locked = true })
hl.bind("XF86AudioPrev", hl.dsp.exec_cmd("playerctl previous"), { locked = true })
hl.bind("XF86AudioPlay", hl.dsp.exec_cmd("playerctl play-pause"), { locked = true })
-- ---------------------------------------------------------------------------
-- Autostart. polkit agent + the bread ecosystem + idle daemon + wallpaper.
-- (bos-live-setup appends the live-installer launch below this on the ISO.)
-- ---------------------------------------------------------------------------
hl.on("hyprland.start", function()
local startup = {
-- Generate the shared bread GUI stylesheet first, so breadbar/breadbox/
-- bos-settings load it on start (they also live-reload if it changes).
"bread-theme generate",
-- Global dark theme: GTK4/libadwaita + GTK3 theme + icon + cursor.
"gsettings set org.gnome.desktop.interface color-scheme prefer-dark",
"gsettings set org.gnome.desktop.interface gtk-theme Adwaita-dark",
"gsettings set org.gnome.desktop.interface icon-theme Papirus-Dark",
"gsettings set org.gnome.desktop.interface cursor-theme Bibata-Modern-Ice",
"gsettings set org.gnome.desktop.interface cursor-size 24",
-- Clipboard history daemon (feeds SUPER+V history picker via wl-paste).
"wl-paste --type text --watch cliphist store",
"/usr/lib/polkit-gnome/polkit-gnome-authentication-agent-1",
"awww-daemon",
-- set the default wallpaper once the daemon is up (retry until ready)
[[bash -c 'until awww img /usr/share/backgrounds/bos/bread-background.png 2>/dev/null; do sleep 0.3; done']],
-- breadd runs as a systemd user service (~/.config/systemd/user/breadd.service,
-- enabled in skel). It autostarts at login but before Hyprland exists, so
-- push the compositor's Wayland env into the user manager and restart breadd
-- to pick it up — that's how it gets HYPRLAND_INSTANCE_SIGNATURE to talk to Hyprland.
"dbus-update-activation-environment --systemd WAYLAND_DISPLAY XDG_CURRENT_DESKTOP HYPRLAND_INSTANCE_SIGNATURE",
"systemctl --user restart breadd",
"breadbar",
"breadbox-sync",
"hypridle",
-- first-boot onboarding (self-gates after the first run)
"bos-welcome",
}
for _, cmd in ipairs(startup) do
hl.dispatch(hl.dsp.exec_cmd(cmd))
end
end)

View file

@ -0,0 +1,28 @@
# Lock screen (hyprlock). Solid dark background (no runtime-generated wallpaper
# dependency), accent matched to the Hyprland border colour.
general {
hide_cursor = true
}
background {
monitor =
color = rgba(46, 52, 64, 1.0)
blur_passes = 0
}
input-field {
monitor =
size = 20%, 5%
outline_thickness = 3
inner_color = rgba(0, 0, 0, 0.2)
outer_color = rgba(136, 192, 208, 0.8)
check_color = rgba(120, 220, 140, 0.95)
fail_color = rgba(255, 90, 90, 0.95)
font_color = rgba(255, 255, 255, 0.95)
fade_on_empty = false
rounding = 12
placeholder_text = <i>Password…</i>
position = 0, -20
halign = center
valign = center
}

View file

@ -1,58 +0,0 @@
$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

View file

@ -0,0 +1,14 @@
# BOS kitty config.
# Translucent background so Hyprland's blur shows through behind the terminal,
# while text stays fully opaque. Colours are left to kitty's default / pywal.
# 0.6 matches the reference laptop; the actual blur is supplied by Hyprland's
# decoration:blur (kitty's own background_blur is macOS-only).
background_opacity 0.6
font_family JetBrains Mono
font_size 11.0
cursor_shape beam
scrollback_lines 10000
enable_audio_bell no
window_padding_width 8
confirm_os_window_close 0

View file

@ -0,0 +1,51 @@
# Default applications for common file types. Without this, freshly installed
# BOS has no handler registered for images/video/text/etc., so opening a file
# from nautilus does nothing. Maps to the apps shipped in packages.x86_64.
[Default Applications]
# Images -> Loupe
image/png=org.gnome.Loupe.desktop
image/jpeg=org.gnome.Loupe.desktop
image/gif=org.gnome.Loupe.desktop
image/webp=org.gnome.Loupe.desktop
image/bmp=org.gnome.Loupe.desktop
image/tiff=org.gnome.Loupe.desktop
image/svg+xml=org.gnome.Loupe.desktop
# Audio/Video -> VLC
audio/mpeg=vlc.desktop
audio/flac=vlc.desktop
audio/ogg=vlc.desktop
audio/x-wav=vlc.desktop
audio/aac=vlc.desktop
video/mp4=vlc.desktop
video/x-matroska=vlc.desktop
video/webm=vlc.desktop
video/quicktime=vlc.desktop
video/x-msvideo=vlc.desktop
# Plain text / source -> GNOME Text Editor
text/plain=org.gnome.TextEditor.desktop
text/markdown=org.gnome.TextEditor.desktop
application/x-shellscript=org.gnome.TextEditor.desktop
application/json=org.gnome.TextEditor.desktop
application/toml=org.gnome.TextEditor.desktop
text/x-readme=org.gnome.TextEditor.desktop
# Documents / web -> Zen (PDF + HTML)
application/pdf=zen.desktop
text/html=zen.desktop
x-scheme-handler/http=zen.desktop
x-scheme-handler/https=zen.desktop
# Archives -> File Roller
application/zip=org.gnome.FileRoller.desktop
application/x-tar=org.gnome.FileRoller.desktop
application/gzip=org.gnome.FileRoller.desktop
application/x-7z-compressed=org.gnome.FileRoller.desktop
application/x-rar=org.gnome.FileRoller.desktop
application/vnd.rar=org.gnome.FileRoller.desktop
application/x-xz=org.gnome.FileRoller.desktop
application/x-bzip2=org.gnome.FileRoller.desktop
# Directories -> Nautilus
inode/directory=org.gnome.Nautilus.desktop

View file

@ -0,0 +1,25 @@
[Appearance]
style=Fusion
color_scheme_path=/usr/share/qt5ct/colors/darker.conf
custom_palette=true
standard_dialogs=default
icon_theme=Papirus-Dark
[Fonts]
fixed=@Variant(\0\0\0@\0\0\0\x12JetBrains Mono\0\0\0\0\0\0\0\0\0\0\0\0\0\xa0\0\x64\xff\xff\xff\xff)
general=@Variant(\0\0\0@\0\0\0\x16Noto Sans\0\0\0\0\0\0\0\0\0\0\0\0\x11\0\x64\xff\xff\xff\xff)
[Interface]
activate_item_on_single_click=1
buttonbox_layout=0
cursor_flash_time=1000
dialog_buttons_have_icons=1
double_click_interval=400
gui_effects=@Invalid()
keyboard_scheme=2
menus_have_icons=true
show_shortcuts_in_context_menus=true
stylesheets=@Invalid()
toolbutton_style=4
underline_shortcut=1
wheel_scroll_lines=3

View file

@ -0,0 +1,25 @@
[Appearance]
style=Fusion
color_scheme_path=/usr/share/qt6ct/colors/darker.conf
custom_palette=true
standard_dialogs=default
icon_theme=Papirus-Dark
[Fonts]
fixed=@Variant(\0\0\0@\0\0\0\x12JetBrains Mono\0\0\0\0\0\0\0\0\0\0\0\0\0\xa0\0\x64\xff\xff\xff\xff)
general=@Variant(\0\0\0@\0\0\0\x16Noto Sans\0\0\0\0\0\0\0\0\0\0\0\0\x11\0\x64\xff\xff\xff\xff)
[Interface]
activate_item_on_single_click=1
buttonbox_layout=0
cursor_flash_time=1000
dialog_buttons_have_icons=1
double_click_interval=400
gui_effects=@Invalid()
keyboard_scheme=2
menus_have_icons=true
show_shortcuts_in_context_menus=true
stylesheets=@Invalid()
toolbutton_style=4
underline_shortcut=1
wheel_scroll_lines=3

View file

@ -0,0 +1,21 @@
[Unit]
Description=Bread Runtime Daemon
[Service]
Type=simple
# %h = the user's home — works for any account created from this skel.
ExecStart=%h/.local/bin/breadd
Restart=on-failure
RestartSec=2
UMask=0077
RuntimeDirectory=bread
RuntimeDirectoryMode=0700
# Keep /run/user/<uid>/bread across restarts so the shared theme.css that
# bread-theme writes there (and the daemon socket) survive a `restart breadd`.
RuntimeDirectoryPreserve=yes
KillSignal=SIGTERM
TimeoutStopSec=5
Environment=RUST_LOG=info
[Install]
WantedBy=default.target

View file

@ -0,0 +1 @@
../breadd.service

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,93 @@
# BOS default zsh config — Powerlevel10k prompt + plugins + pywal palette.
#
# Mirrors the BOS dev shell, but sources plugins from the distro packages
# (/usr/share/zsh/...) instead of oh-my-zsh, so there's no framework to manage.
# Customise the prompt with `p10k configure` (rewrites ~/.p10k.zsh).
# Enable Powerlevel10k instant prompt. Should stay close to the top of ~/.zshrc.
# Initialization code that may require console input (password prompts, [y/n]
# confirmations, etc.) must go above this block; everything else may go below.
if [[ -r "${XDG_CACHE_HOME:-$HOME/.cache}/p10k-instant-prompt-${(%):-%n}.zsh" ]]; then
source "${XDG_CACHE_HOME:-$HOME/.cache}/p10k-instant-prompt-${(%):-%n}.zsh"
fi
# History
HISTFILE=~/.zsh_history
HISTSIZE=10000
SAVEHIST=10000
setopt HIST_IGNORE_DUPS HIST_IGNORE_SPACE SHARE_HISTORY
# Completion
autoload -Uz compinit && compinit
zstyle ':completion:*' menu select
zstyle ':completion:*' matcher-list 'm:{a-z}={A-Z}'
# Emacs-style key bindings
bindkey -e
# Prompt — Powerlevel10k (republished to [breadway] as zsh-theme-powerlevel10k).
source /usr/share/zsh-theme-powerlevel10k/powerlevel10k.zsh-theme
# Plugins (order matters: syntax-highlighting must be sourced LAST).
ZSH_AUTOSUGGEST_HIGHLIGHT_STYLE='fg=60'
source /usr/share/zsh/plugins/zsh-autosuggestions/zsh-autosuggestions.zsh 2>/dev/null
source /usr/share/zsh/plugins/zsh-history-substring-search/zsh-history-substring-search.zsh 2>/dev/null
source /usr/share/zsh/plugins/zsh-syntax-highlighting/zsh-syntax-highlighting.zsh 2>/dev/null
# history-substring-search: ↑/↓ search history by the typed prefix.
bindkey '^[[A' history-substring-search-up
bindkey '^[[B' history-substring-search-down
# fzf — fuzzy history search on Ctrl+R, fuzzy file find on Ctrl+T
if command -v fzf &>/dev/null; then
source /usr/share/fzf/key-bindings.zsh 2>/dev/null || true
source /usr/share/fzf/completion.zsh 2>/dev/null || true
export FZF_DEFAULT_OPTS='--height 40% --layout=reverse --border'
fi
# zoxide — smarter cd (type z instead of cd)
if command -v zoxide &>/dev/null; then
eval "$(zoxide init zsh)"
fi
# Modern replacements with fallbacks
if command -v eza &>/dev/null; then
alias ls='eza --icons --group-directories-first'
alias ll='eza -la --icons --group-directories-first --git'
alias lt='eza --tree --icons --level=2'
else
alias ls='ls --color=auto'
alias ll='ls -la'
fi
if command -v bat &>/dev/null; then
alias cat='bat --style=plain --paging=never'
fi
# General aliases
alias clr='clear'
alias ..='cd ..'
alias ...='cd ../..'
alias mkdir='mkdir -p'
alias cp='cp -i'
alias mv='mv -i'
alias df='df -h'
alias free='free -h'
alias grep='grep --color=auto'
alias ip='ip --color=auto'
# Updates — bos-update runs both channels (pacman + bakery). pacman aliased to
# sudo so `pacman -Syu` etc. just work.
alias update='bos-update'
alias pacman='sudo pacman'
# ~/.local/bin holds the bread* binaries baked in at build time.
export PATH="$HOME/.local/bin:$PATH"
# Powerlevel10k prompt configuration.
[[ ! -f ~/.p10k.zsh ]] || source ~/.p10k.zsh
# Import pywal colour palette (drives the terminal colours from the wallpaper).
if [ -f "$HOME/.cache/wal/sequences" ]; then
cat "$HOME/.cache/wal/sequences"
fi

View file

@ -0,0 +1,7 @@
# Lid behaviour: suspend on close (on battery or AC), but ignore the lid when
# docked / an external display is connected so closing the laptop with a monitor
# attached keeps the session running. Mainstream-desktop default.
[Login]
HandleLidSwitch=suspend
HandleLidSwitchExternalPower=suspend
HandleLidSwitchDocked=ignore

View file

@ -0,0 +1 @@
/usr/lib/systemd/system/NetworkManager.service

View file

@ -0,0 +1,6 @@
# Compressed RAM swap. systemd-zram-generator reads this and creates a zram
# device + swap at boot — no on-disk swap partition needed. Sized at half RAM
# capped to 4 GiB, zstd-compressed (typically ~3:1, so cheap headroom).
[zram0]
zram-size = min(ram / 2, 4096)
compression-algorithm = zstd

View file

@ -0,0 +1,46 @@
#!/bin/bash
# Copy the live kernel into the freshly-unpacked target /boot.
#
# archiso keeps vmlinuz/initramfs in the ISO boot dir (arch/boot/x86_64/), NOT
# in the squashfs, so the rootfs that unpackfs lays down has an empty /boot.
# The kernel must be present before Calamares' `initcpio` module runs mkinitcpio
# (the stock linux.preset points ALL_kver at /boot/vmlinuz-linux) and before the
# `bootloader` module runs grub — otherwise the installed system is unbootable.
#
# Runs in the LIVE environment (Calamares shellprocess, dontChroot) so it can
# read /run/archiso/bootmnt; the target root mount point is passed as $1.
set -uo pipefail
ROOT="${1:?target root required}"
SRC="/run/archiso/bootmnt/arch/boot/x86_64"
install -d -m 0755 "$ROOT/boot"
cp -f "$SRC/vmlinuz-linux" "$ROOT/boot/vmlinuz-linux"
# Microcode, if the live medium carries it (grub-mkconfig picks it up).
for u in amd-ucode.img intel-ucode.img; do
[ -f "$SRC/$u" ] && cp -f "$SRC/$u" "$ROOT/boot/$u"
done
# Replace the archiso initramfs setup that unpackfs copied from the live medium.
# On archiso the linux preset is PRESETS=('archiso') using archiso.conf (the live
# HOOKS). Calamares' `initcpio` runs `mkinitcpio -P`, which would build that
# archiso preset and either bake the live-boot hooks into the install or fail
# once archiso.conf is gone. Drop the drop-in and write a stock default+fallback
# preset so `initcpio` produces a normal, bootable initramfs from the config that
# the `initcpiocfg` module generates at /etc/mkinitcpio.conf.
rm -f "$ROOT/etc/mkinitcpio.conf.d/archiso.conf"
install -d -m 0755 "$ROOT/etc/mkinitcpio.d"
cat >"$ROOT/etc/mkinitcpio.d/linux.preset" <<'PRESET'
# mkinitcpio preset file for the 'linux' package
ALL_config="/etc/mkinitcpio.conf"
ALL_kver="/boot/vmlinuz-linux"
PRESETS=('default' 'fallback')
default_image="/boot/initramfs-linux.img"
fallback_image="/boot/initramfs-linux-fallback.img"
fallback_options="-S autodetect"
PRESET
echo "Copied live kernel into $ROOT/boot; reset mkinitcpio to a stock preset"

View file

@ -0,0 +1,4 @@
#!/bin/bash
# Show the BOS keybind cheatsheet in a floating terminal (bound to SUPER+/).
# The bos-keybinds window class is floated/centred by a Hyprland window rule.
exec kitty --class bos-keybinds --title "BOS Keybinds" -- less -R /usr/share/bos/keybinds.txt

View file

@ -7,37 +7,30 @@
# bos-launch-calamares). Runs once at boot, before the tty1 autologin getty. # bos-launch-calamares). Runs once at boot, before the tty1 autologin getty.
set -e set -e
# useradd -m copies /etc/skel, so the live user gets the real BOS desktop
# (breadd + breadbar + breadbox + keybinds) — proper live-media functionality,
# not an installer kiosk.
if ! id liveuser &>/dev/null; then if ! id liveuser &>/dev/null; then
useradd -m -s /bin/bash liveuser useradd -m -s /usr/bin/zsh liveuser
for g in wheel video input audio storage power; do for g in wheel video input audio storage power; do
getent group "$g" >/dev/null 2>&1 && gpasswd -a liveuser "$g" >/dev/null || true getent group "$g" >/dev/null 2>&1 && gpasswd -a liveuser "$g" >/dev/null || true
done done
passwd -d liveuser >/dev/null passwd -d liveuser >/dev/null
fi fi
install -d -m 0700 -o liveuser -g liveuser /home/liveuser/.config/hypr # Layer the installer onto the live desktop: auto-launch it, and bind Super+I to
# relaunch it after it's been closed. Appended (in Lua) to the skel hyprland.lua
# native config so the full desktop stays intact.
HYPR=/home/liveuser/.config/hypr/hyprland.lua
install -d -m 0755 -o liveuser -g liveuser /home/liveuser/.config/hypr
if ! grep -q bos-launch-calamares "$HYPR" 2>/dev/null; then
cat >>"$HYPR" <<'EOF'
# Minimal live compositor config: auto-launch the installer. -- --- live-media installer (added by bos-live-setup; absent on installed system) ---
cat >/home/liveuser/.config/hypr/hyprland.conf <<'EOF' hl.bind("SUPER + I", hl.dsp.exec_cmd("bos-launch-calamares"))
monitor=,preferred,auto,1 hl.on("hyprland.start", function() hl.dispatch(hl.dsp.exec_cmd("bos-launch-calamares")) end)
exec-once = bos-launch-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
}
EOF EOF
fi
# Start Hyprland on tty1 login; capture output and fall back to a shell so a # Start Hyprland on tty1 login; capture output and fall back to a shell so a
# failed compositor start is visible rather than a blank looping cursor. # failed compositor start is visible rather than a blank looping cursor.

View file

@ -0,0 +1,15 @@
#!/bin/bash
# BOS graphical session launcher, run by greetd on the INSTALLED system after
# the user authenticates (see /etc/greetd/config.toml).
#
# greetd does not start a login shell, so /etc/profile.d is never sourced — which
# means ~/.local/bin (where bakery installs the bread ecosystem: breadd, breadbar,
# breadbox-sync, …) would be missing from PATH and the Hyprland `exec-once`
# launches would fail. Source the login profile here so PATH is correct, set the
# Wayland session hints, then hand off to Hyprland.
source /etc/profile 2>/dev/null
export XDG_SESSION_TYPE=wayland
export XDG_CURRENT_DESKTOP=Hyprland
exec Hyprland

View file

@ -0,0 +1,32 @@
#!/bin/bash
# bos-update — update all of BOS in one go.
#
# BOS packages come from two channels, so a full update touches both:
# 1. pacman — Arch base/desktop + the [breadway] repo (bos-settings, etc.).
# Every transaction is snapshotted by snap-pac, so you can roll
# back from the GRUB "snapshots" submenu or BOS Settings.
# 2. bakery — the bread ecosystem apps in ~/.local/bin (bread, breadbar,
# breadbox, breadcrumbs, breadpad, breadman, bread-theme).
#
# Best-effort: a failure in one channel doesn't abort the other.
set -uo pipefail
bold() { printf '\033[1m%s\033[0m\n' "$1"; }
bold "==> System packages (pacman -Syu)"
if command -v pacman >/dev/null; then
sudo pacman -Syu || echo "WARN: pacman update failed"
else
echo "pacman not found; skipping"
fi
echo
bold "==> Bread ecosystem (bakery update --all)"
if command -v bakery >/dev/null; then
bakery update --all || echo "WARN: bakery update failed"
else
echo "bakery not found; skipping"
fi
echo
bold "==> BOS is up to date."

View file

@ -0,0 +1,34 @@
#!/bin/bash
# First-run welcome. Shows a short getting-started message once, then drops a
# marker so it never shows again. Launched from the Hyprland autostart; the
# bos-welcome window class is floated/centred by a Hyprland window rule.
set -u
# Never run in the live/installer session — only on an installed system.
[[ "$(id -un)" == "liveuser" ]] && exit 0
marker="${XDG_CONFIG_HOME:-$HOME/.config}/bos/.welcomed"
[[ -f "$marker" ]] && exit 0
mkdir -p "$(dirname "$marker")"
# First-run network check. A fresh install usually boots with no connection
# (Wi-Fi isn't configured during install), and the first `bos-update`/pacman run
# then fails with confusing DNS/"could not resolve host" errors. If
# NetworkManager reports we're not fully online, open nmtui so the user can join
# a network before anything else. Best-effort: missing nmcli/nmtui/kitty, or the
# user quitting nmtui, must never block the welcome below.
if command -v nmcli &>/dev/null; then
conn="$(nmcli networking connectivity check 2>/dev/null)"
if [[ "$conn" != "full" ]]; then
notify-send -u normal "BOS" "No internet yet — opening network setup so updates work." 2>/dev/null || true
if command -v nmtui &>/dev/null; then
kitty --class bos-netsetup --title "Connect to a network" -- nmtui connect 2>/dev/null || true
fi
fi
fi
# Mark welcomed only now, so an interrupted/aborted network step still re-prompts
# next login rather than being suppressed forever.
touch "$marker"
exec kitty --class bos-welcome --title "Welcome to BOS" -- less -R /usr/share/bos/welcome.txt

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 MiB

View file

@ -0,0 +1,50 @@
██████ ██████ ███████ keyboard shortcuts
██ ██ ██ ██ ██ SUPER is the Windows/Cmd key
██████ ██ ██ ███████
══════════════════════════════════════════════════════════
APPS & WINDOWS
SUPER + Return terminal (kitty)
SUPER + Space app launcher (breadbox)
SUPER + E files (nautilus)
SUPER + B browser (zen)
SUPER + U notes / reminders (breadpad)
SUPER + M package manager (breadman)
SUPER + , BOS Settings
SUPER + / this keybind cheatsheet
SUPER + L lock screen
SUPER + Backspace close window
SUPER + F fullscreen
SUPER + V toggle floating
SUPER + Shift + V clipboard history
SUPER + T toggle split direction
SUPER + Tab last window
SUPER + N exit Hyprland (log out)
SCREENSHOTS
SUPER + Shift + S select region -> file
SUPER + Shift + C select region -> clipboard
SUPER + Shift + P whole screen -> file
FOCUS & MOVE
SUPER + arrows move focus
SUPER + Shift + h/j/k/l move window
SUPER + Shift + arrows resize window
WORKSPACES
SUPER + 1..0 switch to workspace 1..10
SUPER + Shift + 1..0 move window to workspace
SUPER + [ / ] previous / next workspace
SUPER + Shift + [ / ] move window prev / next workspace
SUPER + scroll cycle workspaces
MOUSE
SUPER + left-drag move window
SUPER + right-drag resize window
MEDIA & HARDWARE KEYS
volume / brightness / play-pause / next / prev (work on lock screen)
──────────────────────────────────────────────────────────
Press q to close. Configure everything in BOS Settings (SUPER + ,).

View file

@ -0,0 +1,24 @@
Welcome to BOS — the Bread Operating System
══════════════════════════════════════════════════════════
You're running a complete Hyprland desktop with the bread
ecosystem preinstalled. A few things to get you started:
• SUPER + / show the keybind cheatsheet (any time)
• SUPER + , open BOS Settings — configure bread, the
bar, launcher, Wi-Fi profiles, notes,
snapshots and package updates, all in one
place (no config files needed)
• SUPER + Space the app launcher (breadbox)
• SUPER + Return a terminal
The bar at the top (breadbar) shows workspaces, the clock,
system stats, and your tray. Notifications appear top-right.
Your system is snapshotted on every package change — if an
update breaks something, roll back from BOS Settings or pick
a snapshot from the GRUB menu at boot.
──────────────────────────────────────────────────────────
Press q to close. This message won't show again.

View file

@ -0,0 +1,8 @@
[Plymouth Theme]
Name=BOS
Description=Bread Operating System boot splash — logo, spinner, status
ModuleName=script
[script]
ImageDir=/usr/share/plymouth/themes/bos
ScriptFile=/usr/share/plymouth/themes/bos/bos.script

View file

@ -0,0 +1,49 @@
# BOS Plymouth boot splash (script module).
# Black background, centred white BOS logo, a spinning accent ring, and a
# status line at the bottom. Colours match the bread palette (black base + warm accent).
# --- background (#0c0c0c) ---
Window.SetBackgroundTopColor(0.047, 0.047, 0.047);
Window.SetBackgroundBottomColor(0.047, 0.047, 0.047);
screen_w = Window.GetWidth();
screen_h = Window.GetHeight();
# --- logo (centred, slightly above the middle) ---
logo.image = Image("logo.png");
logo.sprite = Sprite(logo.image);
logo.x = screen_w / 2 - logo.image.GetWidth() / 2;
logo.y = screen_h / 2 - logo.image.GetHeight() / 2 - 40;
logo.sprite.SetX(logo.x);
logo.sprite.SetY(logo.y);
logo.sprite.SetZ(1);
# --- spinner (rotating accent ring, below the logo) ---
spinner.image = Image("spinner.png");
spinner.sprite = Sprite();
spinner.cx = screen_w / 2;
spinner.cy = logo.y + logo.image.GetHeight() + 60;
spinner.angle = 0;
fun refresh_callback() {
spinner.angle += 0.10;
if (spinner.angle > 6.28318) spinner.angle -= 6.28318;
rotated = spinner.image.Rotate(spinner.angle);
spinner.sprite.SetImage(rotated);
spinner.sprite.SetX(spinner.cx - rotated.GetWidth() / 2);
spinner.sprite.SetY(spinner.cy - rotated.GetHeight() / 2);
spinner.sprite.SetZ(2);
}
Plymouth.SetRefreshFunction(refresh_callback);
# --- status line (cream text, near the bottom) ---
status.sprite = Sprite();
fun show_status(text) {
status.image = Image.Text(text, 0.945, 0.863, 0.741);
status.sprite.SetImage(status.image);
status.sprite.SetX(screen_w / 2 - status.image.GetWidth() / 2);
status.sprite.SetY(screen_h * 0.84);
status.sprite.SetZ(2);
}
Plymouth.SetMessageFunction(show_status);
Plymouth.SetUpdateStatusFunction(show_status);

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1 KiB

View file

@ -0,0 +1,5 @@
title Bread OS install medium (copy to RAM, UEFI)
sort-key 015
linux /%INSTALL_DIR%/boot/%ARCH%/vmlinuz-linux
initrd /%INSTALL_DIR%/boot/%ARCH%/initramfs-linux.img
options archisobasedir=%INSTALL_DIR% archisosearchuuid=%ARCHISO_UUID% copytoram=y

View file

@ -4,6 +4,21 @@ base-devel
linux linux
linux-firmware linux-firmware
linux-headers linux-headers
# CPU microcode — applied early by GRUB on the installed system (picked up by
# the bootloader module). amd-ucode for the dev laptop's Ryzen; intel-ucode for
# Intel targets. bos-copy-kernel also stages these into the live target /boot.
amd-ucode
intel-ucode
# Power management (vendor-neutral; works on Intel and AMD). tlp auto-tunes power
# by AC/battery and CPU driver; hypridle/hyprlock handle idle dim/off/lock/suspend
# and the lock screen; upower exposes battery state. NOT power-profiles-daemon —
# it conflicts with tlp.
tlp
tlp-rdw
upower
hypridle
hyprlock
# Bootloader + filesystem # Bootloader + filesystem
grub grub
@ -37,6 +52,13 @@ inotify-tools
# Wayland / Hyprland # Wayland / Hyprland
hyprland hyprland
xdg-desktop-portal-hyprland xdg-desktop-portal-hyprland
# GTK portal backend — file-chooser/screenshot portals for Flatpak, Electron,
# and Firefox-based apps (Zen). Without it those apps get no file dialog.
xdg-desktop-portal-gtk
# Login manager for the installed system (Wayland-native; enabled by
# post-install.sh, launches the Hyprland session via tuigreet → bos-session).
greetd
greetd-tuigreet
xdg-utils xdg-utils
xdg-user-dirs xdg-user-dirs
polkit polkit
@ -53,15 +75,37 @@ pipewire-jack
networkmanager networkmanager
network-manager-applet network-manager-applet
iw iw
iwd # mDNS service/name resolution — lets CUPS auto-discover network printers and
# resolves .local hostnames (avahi-daemon enabled + nss-mdns wired in
# post-install.sh).
avahi
nss-mdns
# Wi-Fi backend for NetworkManager (its default; no extra config needed).
wpa_supplicant
bluez bluez
bluez-utils bluez-utils
# blueman: GUI Bluetooth manager (pair/connect devices; breadbar shows status only).
blueman
# GTK4 runtime # GTK4 runtime
gtk4 gtk4
gtk4-layer-shell gtk4-layer-shell
librsvg librsvg
libpulse libpulse
# GTK3 dark theme (Adwaita-dark); without this package the gtk-theme-name in
# skel settings.ini silently falls back to the light theme for GTK3 apps.
gnome-themes-extra
# Schema + backend behind `gsettings set org.gnome.desktop.interface
# color-scheme prefer-dark` (set in hyprland.lua autostart). Without these the
# gsettings call fails silently and libadwaita apps (nautilus, gnome-text-editor)
# render in LIGHT mode regardless of the GTK theme.
gsettings-desktop-schemas
dconf
# Credential/keyring storage — browsers, SSH agents, and most apps persist
# passwords here; without it every session loses saved logins. seahorse is the
# GUI to view/manage the stored secrets and keys.
gnome-keyring
seahorse
# Display (wlroots is bundled with Hyprland; don't list separately) # Display (wlroots is bundled with Hyprland; don't list separately)
wayland wayland
@ -69,26 +113,94 @@ wayland-protocols
# Fonts # Fonts
noto-fonts noto-fonts
noto-fonts-cjk
noto-fonts-emoji noto-fonts-emoji
ttf-jetbrains-mono ttf-jetbrains-mono
# Nerd font variant — icons in terminal tools (eza --icons, fastfetch, yazi)
ttf-jetbrains-mono-nerd
# Metric-compatible (Arial/Times/Courier) so Office/web docs lay out correctly,
# broad Unicode fallback, and the Font Awesome icon glyph set (otf-, the desktop
# variant — ttf-font-awesome resolves to the web-only woff2 build).
ttf-liberation
ttf-dejavu
otf-font-awesome
# Terminal # Terminal
foot kitty
# File manager # File manager
nautilus nautilus
# gvfs: virtual filesystem layer for nautilus (trash, network places, removable
# media). gvfs-mtp adds Android/MTP device support (phones, tablets via USB).
gvfs
gvfs-mtp
# file-roller: archive manager — gives nautilus right-click Extract/Compress.
file-roller
# GUI applications a general desktop is expected to have out of the box.
# gnome-text-editor: graphical editor (terminal editors aside); gnome-calculator:
# calculator; loupe: Wayland-native image viewer (default for image files);
# zathura(+pdf-mupdf): lightweight Wayland PDF viewer (BOS had no PDF reader).
gnome-text-editor
gnome-calculator
loupe
zathura
zathura-pdf-mupdf
# Media player — BOS ships gstreamer codecs but otherwise has no player app.
vlc
# Web browser (served from the [Breadway] repo; AUR zen-browser-bin republished
# there so the ISO build can pull it via pacman). mailcap satisfies zen's
# mime-types dependency explicitly.
zen-browser-bin
mailcap
# Installer — Calamares is AUR-only; built in-house and served from [breadway] # Installer — Calamares is AUR-only; built in-house and served from [breadway]
# (calamares 3.4.x is already Qt6; there is no separate calamares-qt6 package) # (calamares 3.4.x is already Qt6; there is no separate calamares-qt6 package)
calamares calamares
# Bread ecosystem — sourced from [breadway] repo # Bread ecosystem.
bakery #
# The bread apps themselves (bakery, bread, breadbar, breadbox, breadcrumbs,
# breadpad) are NOT pacman packages here — they are bakery-managed binaries
# baked into /etc/skel/.local/bin at build time (see build-local.sh), so every
# user gets the exact versions from this laptop's bakery install with no
# network/DNS needed at install or runtime. Their runtime system deps are pulled
# in elsewhere in this list (gtk4, gtk4-layer-shell, iw, libpulse, librsvg,
# networkmanager, openssl, zlib, systemd-libs) — keep those even though no bread
# package depends on them.
#
# bos-settings is a BOS-specific pacman package (not part of the bakery index),
# so it stays here, served from the [breadway] repo.
bos-settings
# Input / screen utilities # Input / screen utilities
brightnessctl brightnessctl
grim grim
slurp slurp
# Clipboard (Wayland copy/paste; also clipboard screenshots) and media keys.
wl-clipboard
playerctl
# Clipboard history daemon (stores wl-clipboard events; breadbox bind replays them).
cliphist
# Wallpaper daemon + pywal (drives the bread* colour palette from the wallpaper).
awww
python-pywal
# Boot splash (BOS logo + spinner instead of kernel text).
plymouth
# Media codecs — GStreamer plugins for video thumbnails in nautilus, browser
# media, and general playback. bad/ugly add patent-encumbered formats (H.264 etc).
gst-plugins-good
gst-plugins-bad
gst-plugins-ugly
# Hardware video acceleration (VA-API) — lets the AMD/Intel GPU decode H.264/HEVC/
# VP9 in mpv, VLC, and browsers instead of the CPU (cooler, longer battery on
# video). The open Mesa VA-API backend (radeonsi_drv_video.so etc.) now ships in
# the `mesa` package itself (pulled in already), so only libva (deps) + the
# `vainfo` verification tool need listing here.
libva-utils
# GUI audio mixer — useful when output device needs manual switching.
pavucontrol
# Utilities # Utilities
sudo sudo
@ -103,5 +215,86 @@ man-db
man-pages man-pages
less less
# Base CLI tools every install should have.
# Shell — zsh with the same prompt + plugins as the dev laptop. Powerlevel10k is
# AUR-only, so it's republished to [breadway] (see packaging/powerlevel10k). The
# three plugins come from the official repos; skel/.zshrc sources them in order
# (autosuggestions → history-substring-search → syntax-highlighting LAST).
zsh
zsh-theme-powerlevel10k
zsh-autosuggestions
zsh-history-substring-search
zsh-syntax-highlighting
# Editors
nano
micro
vim
neovim
# Shell QoL — modern replacements shipped with skel aliases set up
eza
bat
fzf
zoxide
# Fast search — pairs with fzf/zsh and underpins a good neovim experience
ripgrep
fd
# System / hardware inspection
htop
usbutils
pciutils
dmidecode
lsof
tree
fastfetch
# Removable-media filesystems (USB sticks, external drives)
ntfs-3g
exfatprogs
# Archives
7zip
zip
unrar
# Remote access (ssh client; sshd ships disabled)
openssh
# Mirror management (refresh /etc/pacman.d/mirrorlist for the user's location)
reflector
# Printing — CUPS daemon + GUI printer setup. cups-pk-helper lets the GUI add
# printers via polkit without a root shell. cups.socket is enabled in
# post-install.sh so printing works on the installed system.
cups
cups-pk-helper
system-config-printer
# Flatpak — sandboxed third-party app distribution (Flathub). The user adds a
# remote post-install (needs network); the runtime is shipped ready.
flatpak
# Firewall — ufw, enabled deny-incoming in post-install.sh (mDNS allowed so
# printer discovery still works).
ufw
# Firmware updates via LVFS (works with gnome-software / fwupdmgr).
fwupd
# Compressed RAM swap — see /etc/systemd/zram-generator.conf.
zram-generator
# Icon and cursor themes
# Papirus-Dark: cohesive icon set used as the BOS default (set via gsettings in
# hyprland.lua autostart and in skel gtk-3.0/settings.ini).
papirus-icon-theme
# Bibata-Modern-Ice: BOS default cursor. AUR-only upstream, republished to the
# [breadway] repo (see packaging/bibata + .forgejo/workflows/bibata.yml). Set via
# XCURSOR_THEME env in hyprland.lua and gtk settings.ini / gsettings.
bibata-cursor-theme-bin
# Qt dark theme — makes Qt5/Qt6 apps (VLC, pavucontrol, etc.) respect the dark
# palette. qt5ct/qt6ct are configured via skel to use Fusion style in dark mode;
# QT_QPA_PLATFORMTHEME=qt5ct is set in hyprland.lua env.
qt5ct
qt6ct
# Native Wayland platform plugins for Qt — QT_QPA_PLATFORM=wayland (set in
# hyprland.lua) needs these or Qt apps fall back to (blurry) XWayland.
qt5-wayland
qt6-wayland
# Dev tools (for bos-settings standalone install) # Dev tools (for bos-settings standalone install)
rustup rustup

View file

@ -35,7 +35,8 @@ Include = /etc/pacman.d/mirrorlist
# #
# Forgejo signs the repo db with a key pacman can't look up, so TrustAll # Forgejo signs the repo db with a key pacman can't look up, so TrustAll
# fails. SigLevel = Never skips verification (acceptable for this private # fails. SigLevel = Never skips verification (acceptable for this private
# repo over TLS). TODO: import Forgejo's signing key + SigLevel = Required. # repo over TLS). Future improvement: import Forgejo's signing key and
# switch to SigLevel = Required for full package verification.
# ----------------------------------------------------------------------- # -----------------------------------------------------------------------
# The section name must match Forgejo's served db filename # The section name must match Forgejo's served db filename
# ({owner}.{group}.{domain}.db) — pacman fetches "<section>.db" from Server. # ({owner}.{group}.{domain}.db) — pacman fetches "<section>.db" from Server.

View file

@ -19,4 +19,9 @@ file_permissions=(
["/etc/calamares/post-install.sh"]="0:0:755" ["/etc/calamares/post-install.sh"]="0:0:755"
["/usr/local/bin/bos-live-setup"]="0:0:755" ["/usr/local/bin/bos-live-setup"]="0:0:755"
["/usr/local/bin/bos-launch-calamares"]="0:0:755" ["/usr/local/bin/bos-launch-calamares"]="0:0:755"
["/usr/local/bin/bos-copy-kernel"]="0:0:755"
["/usr/local/bin/bos-session"]="0:0:755"
["/usr/local/bin/bos-keybinds"]="0:0:755"
["/usr/local/bin/bos-welcome"]="0:0:755"
["/usr/local/bin/bos-update"]="0:0:755"
) )

View file

@ -8,6 +8,19 @@ LINUX /%INSTALL_DIR%/boot/%ARCH%/vmlinuz-linux
INITRD /%INSTALL_DIR%/boot/%ARCH%/initramfs-linux.img INITRD /%INSTALL_DIR%/boot/%ARCH%/initramfs-linux.img
APPEND archisobasedir=%INSTALL_DIR% archisosearchuuid=%ARCHISO_UUID% APPEND archisobasedir=%INSTALL_DIR% archisosearchuuid=%ARCHISO_UUID%
# Copy-to-RAM boot option — loads airootfs.sfs entirely into RAM, so the
# installer reads from memory rather than a possibly-flaky USB (avoids SquashFS
# read errors during unpackfs). Needs enough RAM for the image (~3 GB).
LABEL archtoram
TEXT HELP
Boot Bread OS, copying the image into RAM first.
More reliable installs from USB; needs a few GB of RAM.
ENDTEXT
MENU LABEL Bread OS install medium (%ARCH%, BIOS) ^copy to RAM
LINUX /%INSTALL_DIR%/boot/%ARCH%/vmlinuz-linux
INITRD /%INSTALL_DIR%/boot/%ARCH%/initramfs-linux.img
APPEND archisobasedir=%INSTALL_DIR% archisosearchuuid=%ARCHISO_UUID% copytoram=y
# Accessibility boot option # Accessibility boot option
LABEL archspeech LABEL archspeech
TEXT HELP TEXT HELP

22
packaging/bibata/PKGBUILD Normal file
View file

@ -0,0 +1,22 @@
# BOS in-house rebuild of bibata-cursor-theme-bin (AUR-only upstream).
# Bibata is the BOS default cursor theme; the AUR package is republished to the
# [breadway] repo so the ISO build can pull it via pacman (same pattern as
# zen-browser-bin and calamares). Prebuilt release tarball — no build step.
# Upstream maintainer: Mark Wagie <mark dot wagie at proton dot me>
pkgname=bibata-cursor-theme-bin
pkgver=2.0.7
pkgrel=1
pkgdesc="Material Based Cursor Theme"
arch=('any')
url="https://github.com/ful1e5/Bibata_Cursor"
license=('GPL-3.0-or-later')
provides=("${pkgname%-bin}")
conflicts=("${pkgname%-bin}")
options=('!strip')
source=("${pkgname%-bin}-$pkgver.tar.xz::$url/releases/download/v$pkgver/Bibata.tar.xz")
sha256sums=('172e33c4ae415278384dcecc7d1a9b7a024266bc944bc751fd86532be1cc6251')
package() {
install -d "$pkgdir/usr/share/icons"
cp -r Bibata* "$pkgdir/usr/share/icons"
}

View file

@ -0,0 +1,105 @@
# BOS in-house rebuild of zsh-theme-powerlevel10k (AUR-only upstream).
# Republished to [breadway] so the ISO can pull the BOS default prompt via pacman
# (same pattern as bibata / zen-browser-bin). Upstream maintainer header kept below.
# Maintainer: Mark Wagie <mark dot wagie at proton dot me>
# Contributor: Christian Rebischke <chris.rebischke@archlinux.org>
# Contributor: Jeff Henson <jeff@henson.io>
# Contributor: Ron Asimi <ron dot asimi at gmail dot com>
# Contributor: Roman Perepelitsa <roman.perepelitsa@gmail.com>
pkgname=zsh-theme-powerlevel10k
# Whenever pkgver is updated, _libgit2ver below must also be updated.
pkgver=1.20.17 ## see P9K_VERSION in internal/p10k.zsh
_libgit2ver="tag-2ecf33948a4df9ef45a66c68b8ef24a5e60eaac6"
pkgrel=1
epoch=1
pkgdesc="Powerlevel10k is a theme for Zsh. It emphasizes speed, flexibility and out-of-the-box experience."
arch=('x86_64' 'aarch64')
url='https://github.com/romkatv/powerlevel10k'
license=('MIT')
depends=(
'glibc'
'zsh'
)
makedepends=(
'git'
'cmake'
)
optdepends=(
# It works well with Nerd Fonts, Source Code Pro, Font Awesome, Powerline,
# and even the default system fonts. The full choice of style options is
# available only when using Nerd Fonts.
'ttf-meslo-nerd-font-powerlevel10k: recommended font'
'powerline-fonts: patched fonts for powerline'
'ttf-font-nerd: full choice of style options'
)
replaces=('zsh-theme-powerlevel9k')
_commit=9253fb1c5034410c43a0c681ff8294181c54016c
# _libgit2ver depends on pkgver. They must be updated together. See libgit2_version in:
# https://raw.githubusercontent.com/romkatv/powerlevel10k/v${pkgver}/gitstatus/build.info
source=(
"git+https://github.com/romkatv/powerlevel10k.git#commit=${_commit}"
# "powerlevel10k-${pkgver}.tar.gz::https://github.com/romkatv/powerlevel10k/archive/v${pkgver}.tar.gz"
# "https://github.com/romkatv/powerlevel10k/releases/download/v$pkgver/powerlevel10k-$pkgver.tar.gz.asc"
"libgit2-${_libgit2ver}.tar.gz::https://github.com/romkatv/libgit2/archive/${_libgit2ver}.tar.gz")
sha256sums=('f0edc2cc5bfcdfcf3b94f10597c252873567a990e651d04059c887046fba6701'
'4ce11d71ee576dbbc410b9fa33a9642809cc1fa687b315f7c23eeb825b251e93')
#validpgpkeys=('8B060F8B9EB395614A669F2A90ACE942EB90C3DD') # Roman Perepelitsa <roman.perepelitsa@gmail.com>
build() {
cd "libgit2-${_libgit2ver}"
local cmake_options=(
-W no-dev
-D CMAKE_BUILD_TYPE='None'
-D ZERO_NSEC='ON'
-D THREADSAFE='ON'
-D USE_BUNDLED_ZLIB='ON'
-D REGEX_BACKEND='builtin'
-D USE_HTTP_PARSER='builtin'
-D USE_SSH='OFF'
-D USE_HTTPS='OFF'
-D BUILD_CLAR='OFF'
-D USE_GSSAPI='OFF'
-D USE_NTLMCLIENT='OFF'
-D BUILD_SHARED_LIBS='OFF'
-D ENABLE_REPRODUCIBLE_BUILDS='ON'
)
cmake "${cmake_options[@]}" .
make
# build gitstatus
cd "$srcdir/powerlevel10k/gitstatus"
export CXXFLAGS+=" -I${srcdir}/libgit2-${_libgit2ver}/include -DGITSTATUS_ZERO_NSEC -D_GNU_SOURCE"
export LDFLAGS+=" -L${srcdir}/libgit2-${_libgit2ver}"
make
}
package() {
cd powerlevel10k
find . -type f -exec install -D '{}' "$pkgdir/usr/share/${pkgname}/{}" ';'
install -d "${pkgdir}/usr/share/licenses/${pkgname}"
ln -s "/usr/share/${pkgname}/LICENSE" "${pkgdir}/usr/share/licenses/${pkgname}"
# delete unnecessary files. See also: https://bugs.archlinux.org/task/66737
rm -r "${pkgdir}/usr/share/${pkgname}/.git"
rm -r "${pkgdir}/usr/share/${pkgname}/gitstatus/deps/"
rm -r "${pkgdir}/usr/share/${pkgname}/gitstatus/obj"
rm -r "${pkgdir}/usr/share/${pkgname}/gitstatus/src/"
rm -r "${pkgdir}/usr/share/${pkgname}/gitstatus/.vscode/"
rm "${pkgdir}/usr/share/${pkgname}/.gitattributes"
rm "${pkgdir}/usr/share/${pkgname}/.gitignore"
rm "${pkgdir}/usr/share/${pkgname}/Makefile"
rm "${pkgdir}/usr/share/${pkgname}/gitstatus/build"
rm "${pkgdir}/usr/share/${pkgname}/gitstatus/Makefile"
rm "${pkgdir}/usr/share/${pkgname}/gitstatus/mbuild"
rm "${pkgdir}/usr/share/${pkgname}/gitstatus/.clang-format"
rm "${pkgdir}/usr/share/${pkgname}/gitstatus/.gitignore"
rm "${pkgdir}/usr/share/${pkgname}/gitstatus/.gitattributes"
rm "${pkgdir}/usr/share/${pkgname}/gitstatus/usrbin/.gitkeep"
cd "${pkgdir}/usr/share/${pkgname}"
for file in *.zsh-theme internal/*.zsh gitstatus/*.zsh gitstatus/install; do
zsh -fc "emulate zsh -o no_aliases && zcompile -R -- $file.zwc $file"
done
}

68
scripts/smoke-test.sh Executable file
View file

@ -0,0 +1,68 @@
#!/usr/bin/env bash
# BOS post-install smoke test.
#
# Run this INSIDE a freshly installed BOS (as the main user) to assert the
# install's core invariants. It is read-only and safe to run any time.
#
# ./smoke-test.sh
#
# Exit status is non-zero if any check fails, so it can gate CI / manual QA.
set -uo pipefail
pass=0 fail=0
ok() { printf ' \033[32mPASS\033[0m %s\n' "$1"; pass=$((pass+1)); }
bad() { printf ' \033[31mFAIL\033[0m %s\n' "$1"; fail=$((fail+1)); }
note() { printf ' \033[33m----\033[0m %s\n' "$1"; }
check() { if eval "$2" >/dev/null 2>&1; then ok "$1"; else bad "$1"; fi; }
echo "== btrfs subvolume layout =="
if command -v btrfs >/dev/null; then
# `btrfs subvolume list` needs root; try unprivileged, fall back to non-
# interactive sudo (no hang if creds aren't cached).
paths="$(btrfs subvolume list / 2>/dev/null || sudo -n btrfs subvolume list / 2>/dev/null)"
paths="$(awk '{print $NF}' <<<"$paths")"
if [ -z "$paths" ]; then
note "couldn't list subvolumes (need root) — skipping"
else
for sv in @ @home @snapshots @log @cache; do
if grep -qx "$sv" <<<"$paths"; then ok "subvolume $sv present"; else bad "subvolume $sv missing"; fi
done
fi
else
note "btrfs not installed (not a btrfs root?) — skipping subvolume checks"
fi
echo "== snapshot tooling =="
check "snapper root config exists" "[ -f /etc/snapper/configs/root ]"
check "snap-pac hook present" "pacman -Qq snap-pac"
check "grub-btrfs present" "pacman -Qq grub-btrfs"
echo "== enabled system services =="
for unit in NetworkManager.service greetd.service bluetooth.service tlp.service \
cups.socket avahi-daemon.service ufw.service systemd-timesyncd.service; do
check "$unit enabled" "systemctl is-enabled $unit"
done
check "graphical.target is default" "[ \"\$(systemctl get-default)\" = graphical.target ]"
echo "== bread ecosystem on PATH =="
for bin in bakery bread breadd breadbar breadbox breadbox-sync breadcrumbs breadpad breadman; do
check "$bin found" "command -v $bin"
done
echo "== bos-settings =="
check "bos-settings installed" "command -v bos-settings"
echo "== default dotfiles =="
check "hyprland.lua present" "[ -f \"\$HOME/.config/hypr/hyprland.lua\" ]"
check "mimeapps.list present" "[ -f \"\$HOME/.config/mimeapps.list\" ]"
check "kitty config present" "[ -f \"\$HOME/.config/kitty/kitty.conf\" ]"
echo "== bootloader (EFI) =="
check "GRUB EFI binary present" \
"[ -f /boot/efi/EFI/BOS/grubx64.efi ] || [ -f /boot/efi/EFI/BOOT/BOOTX64.EFI ]"
check "grub.cfg present" "[ -f /boot/grub/grub.cfg ]"
echo
printf 'Result: \033[32m%d passed\033[0m, \033[31m%d failed\033[0m\n' "$pass" "$fail"
[ "$fail" -eq 0 ]