Compare commits

...

64 commits
v0.3.1 ... main

Author SHA1 Message Date
Breadway
4edd356151 CI: set prerelease=false for release tags
Some checks failed
Mirror to GitHub / mirror (push) Successful in 6s
Build and publish package / package (push) Failing after 2m47s
Build and release ISO / release-iso (push) Successful in 16m24s
2026-06-19 07:11:22 +08:00
Breadway
4250b4be0e fix: use dl_url not github_url for bakery binary downloads
github_url points to GitHub releases that aren't always published for
dev/patch versions (bread v0.6.4 exists on dl.breadway.dev but not on
GitHub). dl.breadway.dev is publicly accessible and is the canonical
source — use dl_url from the bakery index instead.
2026-06-18 21:03:33 +08:00
Breadway
09a2c098ef fix: workflow YAML broken by blank lines inside --notes string
Blank lines inside a shell double-quoted string within a YAML literal
block cause the YAML parser to end the block early. Write the release
notes to a temp file with printf and pass --notes-file instead.
2026-06-18 21:00:24 +08:00
Breadway
6e82bf25ff CI: add ISO build + release workflow
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.
2026-06-18 20:54:46 +08:00
Breadway
86d41f231e v0.4.0: branding refresh, breadpaper baked in, bos-settings 0.4.0
- 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
2026-06-18 14:50:44 +08:00
Breadway
9a5af3ea8f Minor cleanups: gitignore out/, expect() messages, comment wording
- .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
2026-06-17 22:57:58 +08:00
Breadway
6e85f812e4 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.
2026-06-17 22:57:58 +08:00
Breadway
514d0b900c 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.)
2026-06-17 18:49:55 +08:00
Breadway
1ab6c7b188 Default to zsh distro-wide (live user + useradd default)
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.
2026-06-17 17:45:07 +08:00
Breadway
adc316fac6 Run breadd as a systemd user service by default
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.
2026-06-17 14:47:58 +08:00
Breadway
0a6e220974 bos-settings 0.3.1: bread-theme v0.2.8 (working live reload)
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.
2026-06-17 13:59:59 +08:00
Breadway
82fb48cffa build-local: make WORK dir overridable (avoid /tmp tmpfs exhaustion)
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.
2026-06-17 09:03:45 +08:00
Breadway
aadda08797 Add bos-update + replicate the dev zsh shell
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.
2026-06-17 08:49:53 +08:00
Breadway
fbe9c9693e Add a copy-to-RAM boot entry (UEFI + BIOS)
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.
2026-06-16 19:40:16 +08:00
Breadway
aee05b814b bos-settings 0.3.0: shared theme release
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.
2026-06-16 18:37:55 +08:00
Breadway
a3e14ba3a8 Ship a low-battery-warning bread module by default
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.
2026-06-16 17:07:20 +08:00
Breadway
a1e3291a0c BOS: bake the bread-theme CLI and generate the shared stylesheet at login
- 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).
2026-06-16 16:59:03 +08:00
Breadway
7d422d78f3 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.
2026-06-16 16:47:52 +08:00
Breadway
f4043130ad docs+test: ecosystem matrix, keybinds, limitations, recovery, smoke test
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.
2026-06-16 15:51:47 +08:00
Breadway
1d7193773a 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.
2026-06-16 15:51:47 +08:00
Breadway
569ba01550 Fix dark theme, animation speed, kitty opacity; add README
- 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.
2026-06-16 15:36:41 +08:00
Breadway
17e3e13e80 Use otf-font-awesome (desktop) instead of ttf-font-awesome
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.
2026-06-16 14:51:50 +08:00
Breadway
0457bac59a Complete the desktop: default apps, mDNS, firewall, zram, fonts
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
2026-06-16 14:47:06 +08:00
Breadway
04f31c409d bos-settings: full, non-destructive control of every bread* config
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.
2026-06-16 14:26:49 +08:00
Breadway
e193bf26cf 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
2026-06-16 14:26:49 +08:00
Breadway
82549286d2 Restore Bibata cursor now that it's published to [breadway]
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.
2026-06-16 13:03:11 +08:00
Breadway
556c24a50a Republish bibata-cursor-theme to [breadway] (AUR-only upstream)
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.
2026-06-16 13:01:38 +08:00
Breadway
e885488bd6 Drop bibata-cursor-theme (AUR-only, not in repos)
Use Hyprland default cursor instead. All other theming changes from the
previous commit are unaffected.
2026-06-16 10:34:10 +08:00
Breadway
d7acd251b4 Polish BOS: dark theme, shell QoL, icons, media, clipboard
- 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
2026-06-16 10:31:18 +08:00
Breadway
f0a050fdc5 Make Plymouth splash black to match the black-base theme
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.
2026-06-16 09:17:10 +08:00
Breadway
f8ae8fe125 Make BOS a complete, bootable, themed desktop OS
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).
2026-06-16 09:09:34 +08:00
Breadway
787cc0e4c5 Fix breadd skel config schema; remove temp live diagnostic
- 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.
2026-06-14 19:38:06 +08:00
Breadway
76252f20b8 TEMP: route live diag to serial via sudo (revert after) 2026-06-14 19:17:46 +08:00
Breadway
ecd0fcda7a TEMP: live-session diagnostic to serial (revert after) 2026-06-14 19:08:42 +08:00
Breadway
a16245e7c5 Drop removed Hyprland dwindle:pseudotile option
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.
2026-06-14 18:54:06 +08:00
Breadway
105b67bb4d Bake bread ecosystem into the ISO + full live desktop; fix installer timeout
- 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`
2026-06-14 18:41:59 +08:00
Breadway
078c5f4f94 Fix unbootable installs: lay the kernel into the target and own GRUB
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
2026-06-14 17:57:50 +08:00
Breadway
2116b7cd7b Add rsync and make the installed system bootable/clean
unpackfs runs unsquashfs then rsync to copy the rootfs onto the target;
rsync was missing (error code 127), so add it alongside squashfs-tools.

unpackfs also copies the live filesystem verbatim, so the installed
system would inherit the archiso initramfs hooks (booting into the live
path) plus the live autologin/user/sudoers. Rework post-install.sh to run
in the target chroot as a resilient best-effort script that:
- removes the live autologin drop-in, bos-live-setup service/scripts and
  the liveuser sudoers file, and locks root (sudo model; the live medium
  left root passwordless),
- drops the archiso mkinitcpio config, installs the stock linux.preset and
  regenerates the initramfs, then refreshes grub.cfg,
- keeps the snapper/services/dotfiles setup, with the network-dependent
  bakery install made non-fatal so offline installs still complete.
2026-06-14 13:29:49 +08:00
Breadway
8aebfc26c4 Add squashfs-tools so Calamares can unpack the rootfs
Calamares' unpackfs module shells out to unsquashfs to extract
airootfs.sfs onto the target. squashfs-tools wasn't in the live package
list, so installs failed at the Finish step with "Failed to find
unsquashfs ... Bad unpackfs configuration". Add it.
2026-06-14 13:15:27 +08:00
Breadway
08855ecd86 Log the live Hyprland session to a user-writable path
liveuser can't write /var/log, so the .bash_profile redirect
(Hyprland &>/var/log/hyprland-live.log) failed and bash aborted the line
without ever launching the compositor. Log to /tmp/hyprland-live.log,
which the live user can write.
2026-06-14 04:24:52 +08:00
Breadway
937a31732b Run the live session as an unprivileged user (Hyprland won't run as root)
The live medium autologged root on tty1 and exec'd Hyprland, but Hyprland
refuses to start with superuser privileges ("launched with superuser
privileges, but the privileges check is not omitted") and exited before
even creating a log — leaving tty1 at a blank blinking cursor. (Boot,
switch-root, firstboot suppression and the bos login on other ttys were
all already working.)

Adopt the standard live-ISO pattern:
- bos-live-setup.service (oneshot, gated on the archisobasedir cmdline so
  it only runs on the live medium) creates an unprivileged `liveuser`,
  adds it to the usual hardware groups, clears its password, and drops in
  a minimal live Hyprland config that auto-launches the installer.
- tty1 autologin now targets liveuser instead of root.
- Calamares needs root, so bos-launch-calamares runs it via passwordless
  sudo (/etc/sudoers.d/99-bos-live) with the Wayland env preserved, so the
  root installer renders on the live user's compositor.
2026-06-14 04:13:10 +08:00
Breadway
80e8efc84e Capture live-session Hyprland output and fall back to a shell
Redirect the live autologin compositor's stdout/stderr to
/var/log/hyprland-live.log, and on exit drop to an interactive shell
showing the return code instead of letting the getty autologin
respawn-loop hide any startup failure behind a blank blinking cursor.
Makes a failed live boot diagnosable and leaves the medium usable.
2026-06-14 03:57:27 +08:00
Breadway
f967422d61 Let the live Hyprland session fall back to software rendering
On GPU-less targets (VMs, headless, exotic hardware) wlroots refuses to
initialise without a hardware renderer, so the autologin session exec'd
Hyprland on tty1 and it died immediately — leaving a blinking cursor and
no desktop, while tty2 still showed the (correct) `bos` login.

Export WLR_RENDERER_ALLOW_SOFTWARE=1 before exec Hyprland in root's
.bash_profile so wlroots may use the llvmpipe software renderer when no
GPU renderer exists. On real hardware the hardware renderer is still
chosen; this is purely a fallback. Also set WLR_NO_HARDWARE_CURSORS=1 so
the pointer isn't invisible in VMs. Both must be real env vars (read at
wlroots init), not Hyprland `env=` lines, which apply too late.
2026-06-14 03:35:23 +08:00
Breadway
10f9449272 Add live-environment config so the ISO boots straight to the session
The fixed initramfs boots into userspace, but systemd-firstboot
(ConditionFirstBoot=yes, --prompt-locale --prompt-keymap-auto
--prompt-timezone --prompt-root-password) then blocked the console
waiting for interactive input, and root was locked (no /etc/shadow),
so the live medium never reached the autologin getty + Hyprland.

Ship the same base files releng uses to satisfy firstboot and unlock
root for autologin:
- etc/locale.conf  (LANG=C.UTF-8)        -> no locale prompt
- etc/localtime    (-> UTC)              -> no timezone prompt
- etc/vconsole.conf (KEYMAP=us)          -> no keymap prompt
- etc/hostname     (bos)
- etc/shadow       (root unlocked, empty pw, perms 0400 via profiledef)
- etc/passwd       (root shell = bash; system users are appended by the
                    systemd-sysusers pacman hook during pacstrap)

The overlay is applied before pacstrap (mkarchiso _make_custom_airootfs
precedes _make_packages) and these are pacman backup files, so the
static passwd/shadow act as the base and package scriptlets add the
rest — no clobbering of polkitd/pipewire/etc. users.
2026-06-14 03:13:54 +08:00
Breadway
6b20163c92 Add archiso initramfs hooks so the live ISO can switch root
The profile shipped boot configs and the package list but lacked the
mkinitcpio archiso configuration, so mkarchiso built a stock initramfs
with no archiso hook. At boot the kernel honoured archisosearchuuid/
archisobasedir but nothing knew how to find and mount airootfs.sfs, so
switch-root failed and the live medium dropped to emergency mode.

Add the canonical releng pieces:
- airootfs/etc/mkinitcpio.conf.d/archiso.conf (HOOKS incl. archiso)
- airootfs/etc/mkinitcpio.d/linux.preset (builds initramfs-linux.img)
- mkinitcpio{,-archiso,-nfs-utils} in packages.x86_64
2026-06-14 02:55:53 +08:00
Breadway
159d14774e Add in-house Calamares package (AUR-only upstream)
Calamares isn't in Arch's official repos, so BOS vendors the PKGBUILD and
publishes a built package to the [breadway] repo. All its deps are official
(kpmcore, qt6-*, yaml-cpp). Also drop the nonexistent calamares-qt6 from the
package list (calamares 3.4.x is already Qt6).
2026-06-13 23:39:39 +08:00
Breadway
9f9a5db5cc Set [breadway] SigLevel=Never (Forgejo db key unavailable to pacman) 2026-06-13 23:34:51 +08:00
Breadway
47ec044cd6 Fix archiso bootmodes and add syslinux to package list
mkarchiso validation: bios.syslinux.mbr/eltorito and uefi-x64.* bootmodes
are deprecated -> use bios.syslinux + uefi.systemd-boot. syslinux must be
in the package list for the BIOS bootmode; add memtest86+/edk2-shell too.
2026-06-13 23:32:25 +08:00
Breadway
a11a063c12 Add bootloader configs to archiso profile (syslinux/efiboot/grub)
The profile declared syslinux + systemd-boot bootmodes but lacked the
required config directories, so mkarchiso would fail. Added from the
official releng profile, rebranded to Bread OS; %PLACEHOLDER% tokens are
substituted by mkarchiso at build time.
2026-06-13 23:03:54 +08:00
Breadway
0486f4c7c6 Disable debug package so the main package publishes correctly
makepkg's debug split produced a -debug pkg; the upload's head -1 could
grab it instead of the main package. !debug yields a single package.
2026-06-13 23:00:48 +08:00
Breadway
617aeb3d99 Remove accidentally-committed .claude agent state; gitignore it 2026-06-13 22:54:47 +08:00
Breadway
a71ecdcd0b Fix bos-settings compile errors and use REGISTRY_TOKEN for publishing
bos-settings was scaffolded but never compiled. Fixes:
- main.rs: import gtk4::prelude (connect_activate/run)
- window.rs: disambiguate WidgetExt::display(); drop unused GBox import
- hyprland.rs: Label has no set_monospace -> use the monospace CSS class
- theme.rs: drop unused prelude import

Also switch package.yml to secrets.REGISTRY_TOKEN (scoped write:package),
since the auto Actions token is not authorized for the owner registry.
2026-06-13 22:54:27 +08:00
Breadway
b34217d869 Disable LTO in PKGBUILD (vendored ring/mlua static libs vs makepkg -flto) 2026-06-13 17:06:53 +08:00
Breadway
be81e03c45 Regenerate Cargo.lock for bos-settings
The scaffolded lockfile was stale, so packaging builds with --locked failed.
Regenerated against current Cargo.toml (88 packages).
2026-06-13 16:59:30 +08:00
Breadway
ac84b6bb36 Add Calamares branding images from bread logo
- logo.png (productLogo/productIcon): rasterised from the bread logo, transparent
- languages.png (productWelcome): logo centred on a light Nord canvas
- logo.svg / bread_white.svg: source vector

Resolves the missing-branding-asset blocker so Calamares can render.
Colour scheme can be refined when final SVGs land.
2026-06-13 16:53:25 +08:00
Breadway
e8e33e35c4 Source calamares from official extra, not [breadway]
calamares and calamares-qt6 are in Arch's extra repo; no custom PKGBUILD
needed. Update packages.x86_64 and the pacman.conf comment accordingly.
2026-06-13 16:40:59 +08:00
Breadway
7d7737c3b0 Clone from public URL, not GITHUB_SERVER_URL (resolves to localhost in runner)
The Forgejo runner injects GITHUB_SERVER_URL as http://localhost:3002, which
is unreachable from inside the job container. Use the public URL instead.
2026-06-13 16:14:12 +08:00
Breadway
8838cc35f2 Rename mirror secret to MIRROR_TOKEN (GITHUB_ prefix is reserved)
Forgejo/gitea rejects user secret names starting with GITHUB_.
2026-06-13 16:10:39 +08:00
Breadway
11e27a0723 Use Forgejo-prescribed pacman section name for the Arch registry
Forgejo serves the repo db as {owner}.{group}.{domain}.db, and pacman
fetches "<section>.db" from Server — so the section name must match.
2026-06-13 16:03:55 +08:00
Breadway
267f6df523 Fix Forgejo workflows for the actual server capabilities
- package.yml: use correct Arch registry upload (octet-stream + binary body
  + PUT /api/packages/Breadway/arch/os), drop --privileged, remove
  actions/checkout (archlinux image has no Node) in favour of a manual
  shell clone, use the built-in Actions token instead of a stored secret,
  and --nocheck (tests belong in CI, not packaging)
- mirror.yml: clone --mirror + explicit refs/heads + refs/tags push with
  --prune, instead of pushing refs/remotes pollution from a checkout
- pacman.conf: correct Server URL to the Forgejo Arch registry format

Requires only the GITHUB_MIRROR_TOKEN secret (GitHub PAT, repo scope) for
the mirror job; package publishing uses the automatic per-run token.
2026-06-13 16:01:50 +08:00
Breadway
baff024016 Add Forgejo Actions workflows and fix [breadway] repo URL
- .forgejo/workflows/mirror.yml: mirrors every push/tag to GitHub
- .forgejo/workflows/package.yml: builds PKGBUILD on tag and publishes
  bos-settings to the Forgejo Arch package registry (distrib=breadway)
- iso/pacman.conf: replace placeholder repo.breadway.dev with the actual
  Forgejo package registry URL

Requires two Forgejo secrets:
  GITHUB_MIRROR_TOKEN — GitHub PAT with repo push scope
  FORGEJO_TOKEN       — Forgejo token with package:write scope
2026-06-13 11:42:00 +08:00
Breadway
a028e7462a Add bakery.toml and packaging/arch to match bread ecosystem
Mirrors the build/distribution pattern used by the bread project:
- bakery.toml describes bos-settings as a bakery-managed package
- packaging/arch/PKGBUILD builds and installs the binary via cargo
- packaging/arch/bos-settings.desktop for app launchers
- LICENSE (MIT) required by PKGBUILD
2026-06-13 11:32:40 +08:00
Breadway
e67e2a2f66 Fix prod-readiness issues flagged in audit
- Fix XDG config dir logic in config/mod.rs (was double-nesting and had /home/user hardcode)
- Replace /home/user hardcodes in breadbar.rs and hyprland.rs with config::config_dir()
- Fix /home/user hardcode in packages.rs (uses /root fallback for .local/state path)
- Remove eprintln! from GTK callback in packages.rs (no stderr at runtime)
- Fix YAML parse error in branding.desc (missing space after sidebarTextHighlight key)
- Add .gitignore (Rust target/, ISO artifacts, editor/OS junk, secrets)
- Delete state.rs (dead code — never mod'd in main.rs)
- Add brightnessctl, grim, slurp to packages.x86_64 (used by keybinds)
- Rename can-you-begin-a-composed-beacon.md → DESIGN.md
2026-06-13 11:29:53 +08:00
Breadway
8a1157dfce Merge pull request #1 from Breadway/scaffold/bos-initial
Scaffold/bos initial
2026-06-13 11:15:38 +08:00
124 changed files with 6317 additions and 720 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 calamares
# Calamares 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.
on:
push:
paths:
- 'packaging/calamares/**'
workflow_dispatch:
jobs:
calamares:
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 ninja \
extra-cmake-modules qt6-tools qt6-translations libglvnd \
kcoreaddons kpmcore libpwquality qt6-declarative qt6-svg yaml-cpp
useradd -m builder
git config --global --add safe.directory '*'
git clone --depth 1 "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/calamares && makepkg -f --noconfirm --nocheck"
PKG=$(find /home/builder/src/packaging/calamares -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,21 @@
name: Mirror to GitHub
on:
push:
branches: ['**']
tags: ['**']
jobs:
mirror:
runs-on: [self-hosted, hestia]
steps:
- name: Mirror to GitHub
run: |
set -euo pipefail
git clone --mirror "https://git.breadway.dev/${GITHUB_REPOSITORY}.git" repo.git
cd repo.git
# Mirror only branches and tags (not refs/pull/*, which GitHub rejects);
# --prune deletes GitHub refs that no longer exist on Forgejo.
git push --prune \
"https://x-access-token:${{ secrets.MIRROR_TOKEN }}@github.com/Breadway/bos.git" \
'+refs/heads/*:refs/heads/*' '+refs/tags/*:refs/tags/*'

View file

@ -0,0 +1,40 @@
name: Build and publish package
on:
push:
tags: ['v*']
jobs:
package:
runs-on: [self-hosted, hestia]
container:
image: archlinux:latest
steps:
# Note: no actions/checkout — the archlinux image has no Node, which JS
# actions require. Everything runs as shell steps and clones manually.
- name: Build and publish
env:
PUBLISH_TOKEN: ${{ secrets.REGISTRY_TOKEN }}
run: |
set -euo pipefail
VERSION="${GITHUB_REF_NAME#v}"
pacman -Syu --noconfirm base-devel git rust cargo gtk4 glib2
useradd -m builder
git config --global --add safe.directory '*'
git clone --branch "${GITHUB_REF_NAME}" --depth 1 \
"https://git.breadway.dev/${GITHUB_REPOSITORY}.git" /home/builder/src
cd /home/builder/src
git archive --format=tar.gz --prefix="bos-settings-${VERSION}/" HEAD \
> packaging/arch/bos-settings-${VERSION}.tar.gz
SHA=$(sha256sum packaging/arch/bos-settings-${VERSION}.tar.gz | awk '{print $1}')
sed -i "s/^pkgver=.*/pkgver=${VERSION}/" packaging/arch/PKGBUILD
sed -i "s/^sha256sums=.*/sha256sums=('${SHA}')/" packaging/arch/PKGBUILD
chown -R builder:builder /home/builder/src
# --nocheck: packaging builds the artifact; tests belong in a CI job.
su builder -c "cd /home/builder/src/packaging/arch && makepkg -f --noconfirm --nocheck"
PKG=$(find /home/builder/src/packaging/arch -name '*.pkg.tar.zst' | head -1)
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,207 @@
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 dl.breadway.dev (canonical source; github_url
# is not always published for dev/patch releases) 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)
url = b['dl_url']
print(f' {dest_name} <- {url}', flush=True)
urllib.request.urlretrieve(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\": false,
\"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}"
printf '**Download ISO:** %s\n\nGitHub releases cannot host files >2 GB; the `bos-%s-x86_64.iso` (~2.5 GB) is on Forgejo.\n\nSee the [README](https://github.com/Breadway/bos#testing-in-a-vm) for VM testing instructions.' \
"${FORGEJO_URL}" "${VERSION}" > /tmp/gh-release-notes.md
gh release create "${TAG}" \
--repo "Breadway/bos" \
--title "BOS ${TAG}" \
\
--notes-file /tmp/gh-release-notes.md \
2>/dev/null || echo "GitHub release already exists — skipping"

44
.gitignore vendored Normal file
View file

@ -0,0 +1,44 @@
# Rust build artifacts
/target/
**/*.pdb
# Editor / IDE
.vscode/
.idea/
*.swp
*.swo
*~
.direnv/
# OS artifacts
.DS_Store
Thumbs.db
desktop.ini
# Environment / secrets
.env
.env.local
*.env.*
secrets/
*.pem
*.key
*.p12
# archiso build artifacts (these are large and reproducible)
/iso-build/
/iso-out/
/out/
*.iso
*.img
# Runtime / logs
*.log
logs/
*.pid
*.sock
# Claude Code local agent state
.claude/
# Wallpaper source drop (baked copy lives in airootfs/usr/share/backgrounds)
/Bread Background.png

333
Cargo.lock generated
View file

@ -2,6 +2,18 @@
# It is not intended for manual editing. # It is not intended for manual editing.
version = 4 version = 4
[[package]]
name = "async-channel"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2"
dependencies = [
"concurrent-queue",
"event-listener-strategy",
"futures-core",
"pin-project-lite",
]
[[package]] [[package]]
name = "autocfg" name = "autocfg"
version = "1.5.1" version = "1.5.1"
@ -16,20 +28,34 @@ checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8"
[[package]] [[package]]
name = "bos-settings" name = "bos-settings"
version = "0.1.0" version = "0.4.0"
dependencies = [ dependencies = [
"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",
@ -39,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",
@ -58,12 +84,75 @@ 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]]
name = "concurrent-queue"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973"
dependencies = [
"crossbeam-utils",
]
[[package]]
name = "crossbeam-utils"
version = "0.8.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
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"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
[[package]]
name = "event-listener"
version = "5.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab"
dependencies = [
"concurrent-queue",
"parking",
"pin-project-lite",
]
[[package]]
name = "event-listener-strategy"
version = "0.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93"
dependencies = [
"event-listener",
"pin-project-lite",
]
[[package]] [[package]]
name = "field-offset" name = "field-offset"
version = "0.3.6" version = "0.3.6"
@ -138,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",
@ -150,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",
@ -163,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",
@ -178,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",
@ -194,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",
@ -212,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",
@ -246,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",
@ -259,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",
@ -269,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",
@ -280,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",
@ -291,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",
@ -303,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",
@ -318,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",
@ -334,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",
@ -355,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",
@ -367,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",
@ -418,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"
@ -434,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",
@ -447,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",
@ -457,6 +571,12 @@ dependencies = [
"system-deps", "system-deps",
] ]
[[package]]
name = "parking"
version = "2.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba"
[[package]] [[package]]
name = "pin-project-lite" name = "pin-project-lite"
version = "0.2.17" version = "0.2.17"
@ -496,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"
@ -614,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"
@ -718,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]]
@ -733,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"
@ -767,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"

21
LICENSE Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 Breadway
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

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

4
assets/bread_white.svg Normal file
View file

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 677 369" width="677" height="369" role="img" aria-label="Bread terminal logo">
<path fill="#FFFFFF" d="M 233.00 96.00 C 228.18 100.52 224.36 105.12 222.00 111.00 C 219.37 117.57 218.50 127.32 219.00 134.00 C 219.39 139.29 220.67 143.48 223.00 148.00 C 225.68 153.19 232.13 154.83 235.00 163.00 C 242.25 183.68 230.25 267.37 235.00 286.00 C 236.52 291.95 238.13 294.33 241.00 297.00 C 243.82 299.62 245.92 300.68 252.00 302.00 C 274.95 306.97 399.57 308.16 424.00 302.00 C 431.16 300.20 434.14 297.95 437.00 295.00 C 439.23 292.71 439.95 291.45 441.00 287.00 C 444.91 270.42 433.78 182.71 441.00 163.00 C 443.68 155.70 449.32 154.77 452.00 150.00 C 454.58 145.40 456.36 140.55 457.00 135.00 C 457.76 128.44 457.34 119.48 455.00 113.00 C 452.81 106.92 448.70 101.69 444.00 97.00 C 438.89 91.90 431.52 87.55 425.00 84.00 C 418.83 80.64 414.26 78.40 406.00 76.00 C 391.93 71.90 363.58 67.13 347.00 66.00 C 335.07 65.19 326.39 66.02 316.00 67.00 C 305.39 68.00 294.29 69.44 284.00 72.00 C 273.98 74.49 263.78 77.77 255.00 82.00 C 246.91 85.90 238.71 90.65 233.00 96.00 Z"/>
<path fill-rule="evenodd" clip-rule="evenodd" fill="#22272B" d="M 233.00 96.00 C 228.18 100.52 224.36 105.12 222.00 111.00 C 219.37 117.57 218.50 127.32 219.00 134.00 C 219.39 139.29 220.67 143.48 223.00 148.00 C 225.68 153.19 232.13 154.83 235.00 163.00 C 242.25 183.68 230.25 267.37 235.00 286.00 C 236.52 291.95 238.13 294.33 241.00 297.00 C 243.82 299.62 245.92 300.68 252.00 302.00 C 274.95 306.97 399.57 308.16 424.00 302.00 C 431.16 300.20 434.14 297.95 437.00 295.00 C 439.23 292.71 439.95 291.45 441.00 287.00 C 444.91 270.42 433.78 182.71 441.00 163.00 C 443.68 155.70 449.32 154.77 452.00 150.00 C 454.58 145.40 456.36 140.55 457.00 135.00 C 457.76 128.44 457.34 119.48 455.00 113.00 C 452.81 106.92 448.70 101.69 444.00 97.00 C 438.89 91.90 431.52 87.55 425.00 84.00 C 418.83 80.64 414.26 78.40 406.00 76.00 C 391.93 71.90 363.58 67.13 347.00 66.00 C 335.07 65.19 326.39 66.02 316.00 67.00 C 305.39 68.00 294.29 69.44 284.00 72.00 C 273.98 74.49 263.78 77.77 255.00 82.00 C 246.91 85.90 238.71 90.65 233.00 96.00 Z M 236.00 111.00 C 240.70 104.95 252.16 97.27 260.00 93.00 C 266.44 89.50 271.52 88.07 279.00 86.00 C 289.20 83.18 303.07 80.21 316.00 79.00 C 329.97 77.69 345.66 77.61 360.00 79.00 C 373.97 80.35 388.66 82.80 401.00 87.00 C 412.06 90.77 423.64 96.26 431.00 102.00 C 436.45 106.26 440.67 110.64 443.00 116.00 C 445.31 121.32 445.92 128.39 445.00 134.00 C 444.12 139.36 440.97 145.01 438.00 149.00 C 435.46 152.40 431.30 151.88 429.00 157.00 C 421.48 173.75 434.77 272.07 429.00 286.00 C 427.65 289.26 427.35 289.61 424.00 291.00 C 407.50 297.83 268.52 298.74 252.00 291.00 C 248.48 289.35 248.34 288.69 247.00 285.00 C 241.59 270.06 254.49 173.67 247.00 157.00 C 244.70 151.89 240.54 152.40 238.00 149.00 C 235.03 145.01 232.05 138.72 231.00 134.00 C 230.15 130.15 230.27 126.67 231.00 123.00 C 231.80 119.01 232.90 114.98 236.00 111.00 Z M 250.00 114.00 C 247.10 116.53 245.19 118.87 244.00 122.00 C 242.69 125.44 242.28 130.28 243.00 134.00 C 243.69 137.59 245.68 140.95 248.00 144.00 C 250.55 147.34 255.50 147.38 258.00 153.00 C 265.57 170.01 255.06 263.56 258.00 276.00 C 258.56 278.37 258.16 278.85 260.00 280.00 C 270.82 286.73 401.52 284.73 414.00 281.00 C 415.91 280.43 416.14 280.60 417.00 279.00 C 422.10 269.55 409.97 170.96 418.00 153.00 C 420.76 146.83 426.43 146.39 429.00 143.00 C 430.99 140.37 432.40 138.08 433.00 135.00 C 433.73 131.28 433.45 125.75 432.00 122.00 C 430.64 118.49 428.73 115.87 425.00 113.00 C 418.39 107.92 402.93 102.18 392.00 99.00 C 381.94 96.07 371.95 95.00 362.00 94.00 C 352.28 93.03 343.91 92.52 333.00 93.00 C 318.78 93.62 295.80 96.46 284.00 99.00 C 277.13 100.48 273.37 101.70 268.00 104.00 C 262.05 106.55 254.16 110.38 250.00 114.00 Z M 292.00 152.00 L 294.00 152.00 L 334.00 192.00 L 334.00 194.00 L 294.00 234.00 L 291.00 234.00 L 283.00 223.00 L 312.00 194.00 L 312.00 192.00 L 283.00 163.00 Z M 337.00 222.00 L 339.00 220.00 L 393.00 220.00 L 394.00 234.00 L 339.00 235.00 L 337.00 233.00 Z"/>
</svg>

After

Width:  |  Height:  |  Size: 4 KiB

12
bakery.toml Normal file
View file

@ -0,0 +1,12 @@
name = "bos-settings"
description = "System settings app for Bread OS"
binaries = ["bos-settings"]
system_deps = ["gtk4", "glib2"]
optional_system_deps = ["snapper"]
bread_deps = []
[config]
dir = "~/.config"
[install]
post_install = []

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,24 +1,195 @@
//! 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(())
} }
pub fn config_dir() -> PathBuf { pub fn config_dir() -> PathBuf {
let home = std::env::var("HOME").unwrap_or_else(|_| { // Honour XDG_CONFIG_HOME if set; otherwise fall back to $HOME/.config.
std::env::var("XDG_CONFIG_HOME") if let Ok(xdg) = std::env::var("XDG_CONFIG_HOME") {
.map(|p| PathBuf::from(p).parent().unwrap_or(Path::new("/")).to_string_lossy().to_string()) let p = PathBuf::from(xdg);
.unwrap_or_else(|_| "/home/user".to_string()) if p.is_absolute() {
}); return p;
}
}
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

@ -2,6 +2,8 @@ mod config;
mod theme; mod theme;
mod ui; mod ui;
use gtk4::prelude::*;
fn main() { fn main() {
let app = gtk4::Application::builder() let app = gtk4::Application::builder()
.application_id("com.breadway.bos-settings") .application_id("com.breadway.bos-settings")

View file

@ -1,11 +0,0 @@
pub struct AppState {
pub current_view: String,
}
impl AppState {
pub fn new() -> Self {
Self {
current_view: "snapshots".to_string(),
}
}
}

View file

@ -1,88 +1,30 @@
use gtk4::prelude::*; //! 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

@ -3,8 +3,7 @@ use gtk4::{Box as GBox, Button, Label, Orientation, ScrolledWindow, TextView};
use std::path::PathBuf; use std::path::PathBuf;
fn css_path() -> PathBuf { fn css_path() -> PathBuf {
let home = std::env::var("HOME").unwrap_or_else(|_| "/home/user".to_string()); crate::config::config_dir().join("breadbar/style.css")
PathBuf::from(home).join(".config/breadbar/style.css")
} }
pub fn build() -> GBox { pub fn build() -> GBox {

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();
}
});
}
{
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();
remove.connect_clicked(move |_| {
model.borrow_mut().remove(i);
rebuild_networks(&list, &model);
});
}
hbox.append(&ssid);
hbox.append(&pass);
hbox.append(&Label::new(Some("hidden")));
hbox.append(&hidden);
hbox.append(&remove);
row.set_child(Some(&hbox));
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(',') .split(',')
.map(|s| s.trim().to_string()) .map(|s| s.trim().to_string())
.filter(|s| !s.is_empty()) .filter(|s| !s.is_empty())
.collect(); .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 cfg = cfg.clone(); let model = model.clone();
let list = list.clone(); let container = container.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_profiles(&container, &model);
}); });
} }
hbox.append(&name_entry); container.append(&card);
hbox.append(&ssids_entry);
hbox.append(&remove_btn);
row.set_child(Some(&hbox));
list.append(&row);
} }
} }
// --- 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

@ -23,8 +23,7 @@ fn get_monitors() -> Vec<String> {
} }
fn hypr_path(name: &str) -> std::path::PathBuf { fn hypr_path(name: &str) -> std::path::PathBuf {
let home = std::env::var("HOME").unwrap_or_else(|_| "/home/user".to_string()); crate::config::config_dir().join("hypr").join(name)
std::path::PathBuf::from(home).join(".config/hypr").join(name)
} }
pub fn build() -> GBox { pub fn build() -> GBox {
@ -51,7 +50,7 @@ pub fn build() -> GBox {
for mon in &monitors { for mon in &monitors {
let lbl = Label::new(Some(mon)); let lbl = Label::new(Some(mon));
lbl.set_xalign(0.0); lbl.set_xalign(0.0);
lbl.set_monospace(true); lbl.add_css_class("monospace");
vbox.append(&lbl); vbox.append(&lbl);
} }
} }

View file

@ -8,7 +8,7 @@ use std::io::{BufRead, BufReader};
use std::process::{Command, Stdio}; use std::process::{Command, Stdio};
fn read_installed() -> HashMap<String, String> { fn read_installed() -> HashMap<String, String> {
let home = std::env::var("HOME").unwrap_or_else(|_| "/home/user".to_string()); let home = std::env::var("HOME").unwrap_or_else(|_| "/root".to_string());
let path = std::path::Path::new(&home) let path = std::path::Path::new(&home)
.join(".local/state/bakery/installed.json"); .join(".local/state/bakery/installed.json");
@ -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 || {
@ -132,7 +133,7 @@ pub fn build() -> GBox {
Ok(mut child) => { Ok(mut child) => {
std::thread::spawn(move || { let _ = child.wait(); }); std::thread::spawn(move || { let _ = child.wait(); });
} }
Err(e) => eprintln!("bakery update failed: {e}"), Err(_) => {} // bakery not found; button is a no-op
} }
}); });

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
}

View file

@ -1,5 +1,5 @@
use gtk4::prelude::*; use gtk4::prelude::*;
use gtk4::{Application, ApplicationWindow, Box as GBox, Orientation, Paned, Stack}; use gtk4::{Application, ApplicationWindow, Orientation, Paned, Stack};
use super::sidebar; use super::sidebar;
use super::views; use super::views;
@ -12,7 +12,7 @@ pub fn build_ui(app: &Application) {
.default_height(640) .default_height(640)
.build(); .build();
crate::theme::load(&window.display()); crate::theme::load(&WidgetExt::display(&window));
let hpaned = Paned::new(Orientation::Horizontal); let hpaned = Paned::new(Orientation::Horizontal);
hpaned.set_position(190); hpaned.set_position(190);

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.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

View file

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 677 369" width="677" height="369" role="img" aria-label="Bread terminal logo">
<path fill="#FFFFFF" d="M 233.00 96.00 C 228.18 100.52 224.36 105.12 222.00 111.00 C 219.37 117.57 218.50 127.32 219.00 134.00 C 219.39 139.29 220.67 143.48 223.00 148.00 C 225.68 153.19 232.13 154.83 235.00 163.00 C 242.25 183.68 230.25 267.37 235.00 286.00 C 236.52 291.95 238.13 294.33 241.00 297.00 C 243.82 299.62 245.92 300.68 252.00 302.00 C 274.95 306.97 399.57 308.16 424.00 302.00 C 431.16 300.20 434.14 297.95 437.00 295.00 C 439.23 292.71 439.95 291.45 441.00 287.00 C 444.91 270.42 433.78 182.71 441.00 163.00 C 443.68 155.70 449.32 154.77 452.00 150.00 C 454.58 145.40 456.36 140.55 457.00 135.00 C 457.76 128.44 457.34 119.48 455.00 113.00 C 452.81 106.92 448.70 101.69 444.00 97.00 C 438.89 91.90 431.52 87.55 425.00 84.00 C 418.83 80.64 414.26 78.40 406.00 76.00 C 391.93 71.90 363.58 67.13 347.00 66.00 C 335.07 65.19 326.39 66.02 316.00 67.00 C 305.39 68.00 294.29 69.44 284.00 72.00 C 273.98 74.49 263.78 77.77 255.00 82.00 C 246.91 85.90 238.71 90.65 233.00 96.00 Z"/>
<path fill-rule="evenodd" clip-rule="evenodd" fill="#22272B" d="M 233.00 96.00 C 228.18 100.52 224.36 105.12 222.00 111.00 C 219.37 117.57 218.50 127.32 219.00 134.00 C 219.39 139.29 220.67 143.48 223.00 148.00 C 225.68 153.19 232.13 154.83 235.00 163.00 C 242.25 183.68 230.25 267.37 235.00 286.00 C 236.52 291.95 238.13 294.33 241.00 297.00 C 243.82 299.62 245.92 300.68 252.00 302.00 C 274.95 306.97 399.57 308.16 424.00 302.00 C 431.16 300.20 434.14 297.95 437.00 295.00 C 439.23 292.71 439.95 291.45 441.00 287.00 C 444.91 270.42 433.78 182.71 441.00 163.00 C 443.68 155.70 449.32 154.77 452.00 150.00 C 454.58 145.40 456.36 140.55 457.00 135.00 C 457.76 128.44 457.34 119.48 455.00 113.00 C 452.81 106.92 448.70 101.69 444.00 97.00 C 438.89 91.90 431.52 87.55 425.00 84.00 C 418.83 80.64 414.26 78.40 406.00 76.00 C 391.93 71.90 363.58 67.13 347.00 66.00 C 335.07 65.19 326.39 66.02 316.00 67.00 C 305.39 68.00 294.29 69.44 284.00 72.00 C 273.98 74.49 263.78 77.77 255.00 82.00 C 246.91 85.90 238.71 90.65 233.00 96.00 Z M 236.00 111.00 C 240.70 104.95 252.16 97.27 260.00 93.00 C 266.44 89.50 271.52 88.07 279.00 86.00 C 289.20 83.18 303.07 80.21 316.00 79.00 C 329.97 77.69 345.66 77.61 360.00 79.00 C 373.97 80.35 388.66 82.80 401.00 87.00 C 412.06 90.77 423.64 96.26 431.00 102.00 C 436.45 106.26 440.67 110.64 443.00 116.00 C 445.31 121.32 445.92 128.39 445.00 134.00 C 444.12 139.36 440.97 145.01 438.00 149.00 C 435.46 152.40 431.30 151.88 429.00 157.00 C 421.48 173.75 434.77 272.07 429.00 286.00 C 427.65 289.26 427.35 289.61 424.00 291.00 C 407.50 297.83 268.52 298.74 252.00 291.00 C 248.48 289.35 248.34 288.69 247.00 285.00 C 241.59 270.06 254.49 173.67 247.00 157.00 C 244.70 151.89 240.54 152.40 238.00 149.00 C 235.03 145.01 232.05 138.72 231.00 134.00 C 230.15 130.15 230.27 126.67 231.00 123.00 C 231.80 119.01 232.90 114.98 236.00 111.00 Z M 250.00 114.00 C 247.10 116.53 245.19 118.87 244.00 122.00 C 242.69 125.44 242.28 130.28 243.00 134.00 C 243.69 137.59 245.68 140.95 248.00 144.00 C 250.55 147.34 255.50 147.38 258.00 153.00 C 265.57 170.01 255.06 263.56 258.00 276.00 C 258.56 278.37 258.16 278.85 260.00 280.00 C 270.82 286.73 401.52 284.73 414.00 281.00 C 415.91 280.43 416.14 280.60 417.00 279.00 C 422.10 269.55 409.97 170.96 418.00 153.00 C 420.76 146.83 426.43 146.39 429.00 143.00 C 430.99 140.37 432.40 138.08 433.00 135.00 C 433.73 131.28 433.45 125.75 432.00 122.00 C 430.64 118.49 428.73 115.87 425.00 113.00 C 418.39 107.92 402.93 102.18 392.00 99.00 C 381.94 96.07 371.95 95.00 362.00 94.00 C 352.28 93.03 343.91 92.52 333.00 93.00 C 318.78 93.62 295.80 96.46 284.00 99.00 C 277.13 100.48 273.37 101.70 268.00 104.00 C 262.05 106.55 254.16 110.38 250.00 114.00 Z M 292.00 152.00 L 294.00 152.00 L 334.00 192.00 L 334.00 194.00 L 294.00 234.00 L 291.00 234.00 L 283.00 223.00 L 312.00 194.00 L 312.00 192.00 L 283.00 163.00 Z M 337.00 222.00 L 339.00 220.00 L 393.00 220.00 L 394.00 234.00 L 339.00 235.00 L 337.00 233.00 Z"/>
</svg>

After

Width:  |  Height:  |  Size: 4 KiB

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,41 +1,177 @@
#!/bin/bash #!/bin/bash
set -euo pipefail # BOS-specific finalization, run inside the installed-system chroot (Calamares
# shellprocess), AFTER the native initcpio module has built the initramfs. The
# 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
# --- Snapper root config --- MAIN_USER="$(getent passwd 1000 | cut -d: -f1 || true)"
snapper -c root create-config /
sed -i 's/TIMELINE_CREATE="yes"/TIMELINE_CREATE="no"/' /etc/snapper/configs/root
sed -i 's/NUMBER_CLEANUP="no"/NUMBER_CLEANUP="yes"/' /etc/snapper/configs/root
sed -i 's/NUMBER_MIN_AGE="[^"]*"/NUMBER_MIN_AGE="1800"/' /etc/snapper/configs/root
sed -i 's/NUMBER_LIMIT="[^"]*"/NUMBER_LIMIT="10"/' /etc/snapper/configs/root
sed -i 's/NUMBER_LIMIT_IMPORTANT="[^"]*"/NUMBER_LIMIT_IMPORTANT="5"/' /etc/snapper/configs/root
# Allow main user to list/create/delete snapshots without sudo # ---------------------------------------------------------------------------
MAIN_USER=$(getent passwd 1000 | cut -d: -f1) # Strip live-only bits that unpackfs copied verbatim from the live medium.
sed -i "s/ALLOW_USERS=\"\"/ALLOW_USERS=\"$MAIN_USER\"/" /etc/snapper/configs/root # ---------------------------------------------------------------------------
rm -f /etc/systemd/system/getty@tty1.service.d/autologin.conf
rm -f /etc/systemd/system/bos-live-setup.service \
/etc/systemd/system/multi-user.target.wants/bos-live-setup.service
rm -f /usr/local/bin/bos-live-setup /usr/local/bin/bos-launch-calamares
rm -f /etc/sudoers.d/99-bos-live
userdel -r liveuser 2>/dev/null || true
# --- System services --- # Root used a passwordless entry on the live medium; lock it (sudo model).
systemctl enable NetworkManager passwd -l root || true
systemctl enable bluetooth
systemctl enable snapper-cleanup.timer
systemctl enable grub-btrfs.path
# --- Bakery: install bread ecosystem --- # ---------------------------------------------------------------------------
# Requires [breadway] repo in /etc/pacman.conf — see iso/pacman.conf # Pacman keyring. The live medium's /etc/pacman.d/gnupg doesn't reliably carry
if command -v bakery &>/dev/null; then # over to the target (unpackfs may skip it / perms differ), leaving the installed
sudo -u "$MAIN_USER" bakery install bread breadbar breadbox breadcrumbs breadpad bos-settings # 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.
# ---------------------------------------------------------------------------
if command -v pacman-key &>/dev/null; then
pacman-key --init || echo "WARN: pacman-key --init failed"
pacman-key --populate archlinux || echo "WARN: pacman-key --populate failed"
fi fi
# --- Deploy dotfiles into user home (skip any file that already exists) --- # ---------------------------------------------------------------------------
SKEL_SRC="/etc/skel/.config" # Initramfs HOOKS: microcode + plymouth. Edit HOOKS first, rebuild once below.
DOTFILES_DEST="/home/$MAIN_USER/.config" # 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
if [[ -d "$SKEL_SRC" ]]; then # ISO /boot carries no separate ucode image and bos-copy-kernel stages none
mkdir -p "$DOTFILES_DEST" # onto the target — the installed initramfs must therefore carry it itself.
cp -rn "$SKEL_SRC/." "$DOTFILES_DEST/" # Must sit AFTER `autodetect` so it's pruned to the running CPU's microcode.
chown -R "$MAIN_USER:$MAIN_USER" "$DOTFILES_DEST" # 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 fi
# --- XDG user dirs --- # ---------------------------------------------------------------------------
sudo -u "$MAIN_USER" xdg-user-dirs-update # 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
echo "BOS post-install complete. Reboot to start your system." # 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
grub-mkconfig -o /boot/grub/grub.cfg || echo "WARN: grub-mkconfig failed"
fi
# ---------------------------------------------------------------------------
# Snapper root config (root is btrfs).
# ---------------------------------------------------------------------------
if command -v snapper &>/dev/null; then
snapper -c root create-config / || echo "WARN: snapper create-config failed"
if [[ -f /etc/snapper/configs/root ]]; then
sed -i 's/TIMELINE_CREATE="yes"/TIMELINE_CREATE="no"/' /etc/snapper/configs/root
sed -i 's/NUMBER_CLEANUP="no"/NUMBER_CLEANUP="yes"/' /etc/snapper/configs/root
sed -i 's/NUMBER_MIN_AGE="[^"]*"/NUMBER_MIN_AGE="1800"/' /etc/snapper/configs/root
sed -i 's/NUMBER_LIMIT="[^"]*"/NUMBER_LIMIT="10"/' /etc/snapper/configs/root
sed -i 's/NUMBER_LIMIT_IMPORTANT="[^"]*"/NUMBER_LIMIT_IMPORTANT="5"/' /etc/snapper/configs/root
[[ -n "$MAIN_USER" ]] && \
sed -i "s/ALLOW_USERS=\"\"/ALLOW_USERS=\"$MAIN_USER\"/" /etc/snapper/configs/root
fi
fi
# ---------------------------------------------------------------------------
# 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)
# ---------------------------------------------------------------------------
for unit in NetworkManager.service bluetooth.service systemd-timesyncd.service \
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"
# ---------------------------------------------------------------------------
# mDNS resolution (nss-mdns): insert mdns_minimal into the hosts: line so the
# resolver answers *.local (network printers, other hosts) via avahi. Idempotent.
# ---------------------------------------------------------------------------
if [[ -f /etc/nsswitch.conf ]] && ! grep -q 'mdns_minimal' /etc/nsswitch.conf; then
sed -i 's/^\(hosts:[[:space:]]*\)/\1mdns_minimal [NOTFOUND=return] /' \
/etc/nsswitch.conf || echo "WARN: wiring nss-mdns failed"
fi
# ---------------------------------------------------------------------------
# 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 command -v ufw &>/dev/null; then
ufw default deny incoming || echo "WARN: ufw default deny incoming failed"
ufw default allow outgoing || echo "WARN: ufw default allow outgoing failed"
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
fi
echo "BOS post-install complete."

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 @@
bos

View file

@ -0,0 +1 @@
LANG=C.UTF-8

1
iso/airootfs/etc/localtime Symbolic link
View file

@ -0,0 +1 @@
/usr/share/zoneinfo/UTC

View file

@ -0,0 +1,3 @@
HOOKS=(base udev microcode modconf kms memdisk archiso archiso_loop_mnt archiso_pxe_common archiso_pxe_nbd archiso_pxe_http archiso_pxe_nfs block filesystems keyboard)
COMPRESSION="xz"
COMPRESSION_OPTIONS=(-9e)

View file

@ -0,0 +1,8 @@
# mkinitcpio preset file for the 'linux' package on archiso
PRESETS=('archiso')
ALL_kver='/boot/vmlinuz-linux'
archiso_config='/etc/mkinitcpio.conf.d/archiso.conf'
archiso_image="/boot/initramfs-linux.img"

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

1
iso/airootfs/etc/passwd Normal file
View file

@ -0,0 +1 @@
root:x:0:0:root:/root:/usr/bin/bash

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

1
iso/airootfs/etc/shadow Normal file
View file

@ -0,0 +1 @@
root::14871::::::

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,3 @@
# Live medium only: the unprivileged live user may escalate without a password
# so the installer (Calamares) can run as root from the Wayland session.
liveuser ALL=(ALL) NOPASSWD: ALL

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,14 @@
[Unit]
Description=Set up the BOS live user and session
# Only on the live medium — the installed system has no archisobasedir cmdline.
ConditionKernelCommandLine=archisobasedir
Before=getty@tty1.service
After=systemd-tmpfiles-setup.service
[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=/usr/local/bin/bos-live-setup
[Install]
WantedBy=multi-user.target

View file

@ -1,3 +1,3 @@
[Service] [Service]
ExecStart= ExecStart=
ExecStart=-/sbin/agetty -o '-p -f -- \\u' --noclear --autologin root - $TERM ExecStart=-/sbin/agetty -o '-p -f -- \\u' --noclear --autologin liveuser - $TERM

View file

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

View file

@ -0,0 +1 @@
../bos-live-setup.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 @@
KEYMAP=us

View file

@ -1,4 +1,19 @@
# Auto-start Hyprland on tty1 in the live session # Auto-start Hyprland on tty1 in the live session
if [[ "$(tty)" == "/dev/tty1" ]] && [[ -z "$WAYLAND_DISPLAY" ]]; then if [[ "$(tty)" == "/dev/tty1" ]] && [[ -z "$WAYLAND_DISPLAY" ]]; then
exec Hyprland # Allow a software-rendering fallback so the live session comes up even
# without a GPU (VMs, headless, exotic hardware). On real hardware wlroots
# still selects the hardware renderer; this only permits llvmpipe when no
# GPU renderer is available. Must be exported before Hyprland starts —
# wlroots reads it at renderer init, earlier than any Hyprland `env=` line.
export WLR_RENDERER_ALLOW_SOFTWARE=1
# Software cursors: hardware-cursor planes are often unusable in VMs and
# show as invisible/garbled; this is the reliable choice for a live medium.
export WLR_NO_HARDWARE_CURSORS=1
# Run the compositor, capturing its output so a failed live boot is
# diagnosable (Hyprland also keeps its own log under $XDG_RUNTIME_DIR/hypr/).
# On exit, drop to an interactive shell with the error in view instead of
# letting the getty autologin respawn-loop hide it behind a blank cursor.
Hyprland &>/var/log/hyprland-live.log
echo "Hyprland exited (rc=$?). Log: /var/log/hyprland-live.log"
exec bash -i
fi fi

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

@ -0,0 +1,7 @@
#!/bin/sh
# Launch Calamares as root on the live user's Wayland session.
# Calamares performs partitioning/bootloader work and needs root; the live user
# has passwordless sudo (see /etc/sudoers.d/99-bos-live). We preserve the Wayland
# environment so the root process renders on the user's compositor.
export QT_QPA_PLATFORM=wayland
exec sudo --preserve-env=WAYLAND_DISPLAY,XDG_RUNTIME_DIR,QT_QPA_PLATFORM calamares

View file

@ -0,0 +1,49 @@
#!/bin/bash
# Create the unprivileged BOS live user and its Hyprland session.
#
# Hyprland refuses to run as root (superuser-privileges check), so the live
# session must run as a normal user. Calamares — which does need root — is
# launched onto the user's Wayland socket via passwordless sudo (see
# bos-launch-calamares). Runs once at boot, before the tty1 autologin getty.
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
useradd -m -s /usr/bin/zsh liveuser
for g in wheel video input audio storage power; do
getent group "$g" >/dev/null 2>&1 && gpasswd -a liveuser "$g" >/dev/null || true
done
passwd -d liveuser >/dev/null
fi
# 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'
-- --- live-media installer (added by bos-live-setup; absent on installed system) ---
hl.bind("SUPER + I", hl.dsp.exec_cmd("bos-launch-calamares"))
hl.on("hyprland.start", function() hl.dispatch(hl.dsp.exec_cmd("bos-launch-calamares")) end)
EOF
fi
# 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.
cat >/home/liveuser/.bash_profile <<'EOF'
if [[ "$(tty)" == /dev/tty1 ]] && [[ -z "$WAYLAND_DISPLAY" ]]; then
export WLR_RENDERER_ALLOW_SOFTWARE=1
export WLR_NO_HARDWARE_CURSORS=1
# Log to a user-writable path (/var/log is root-only; redirecting there
# would fail and silently keep the compositor from ever launching).
Hyprland &>/tmp/hyprland-live.log
echo "Hyprland exited (rc=$?). Log: /tmp/hyprland-live.log"
exec bash -i
fi
EOF
chown -R liveuser:liveuser /home/liveuser

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

Some files were not shown because too many files have changed in this diff Show more