From 99cc0a4717b0255147fbf6cdbf6af888d4e61e26 Mon Sep 17 00:00:00 2001 From: Breadway Date: Wed, 15 Jul 2026 19:20:23 +0800 Subject: [PATCH] Extract breadhelp to its own repo (git.breadway.dev/Breadway/breadhelp) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mirrors the bos-settings extraction (664298b): a breadhelp release no longer requires a v* tag on the whole bos monorepo, which was indistinguishable from an actual BOS-version release tag. Source lives at ~/Projects/breadhelp now, full history preserved via git-filter-repo. Root Cargo.toml/Cargo.lock and .forgejo/workflows/package.yml removed too — breadhelp was the sole workspace member and the sole thing that workflow built. --- .forgejo/workflows/package.yml | 40 - Cargo.lock | 1108 ----------------- Cargo.toml | 3 - breadhelp/Cargo.toml | 19 - breadhelp/content/README.md | 28 - .../01-snapshots-and-rollback/content.md | 13 - .../01-snapshots-and-rollback/meta.toml | 5 - .../apps/01-notes-and-tasks/content.md | 10 - .../content/apps/01-notes-and-tasks/meta.toml | 5 - .../01-bar-and-launcher/content.md | 7 - .../01-bar-and-launcher/meta.toml | 5 - .../01-searching-your-files/content.md | 9 - .../01-searching-your-files/meta.toml | 5 - .../daily-use/02-clipboard-history/content.md | 7 - .../daily-use/02-clipboard-history/meta.toml | 5 - .../01-what-is-bos/content.beginner.md | 10 - .../getting-started/01-what-is-bos/content.md | 21 - .../getting-started/01-what-is-bos/meta.toml | 5 - .../02-essential-keybinds/content.md | 23 - .../02-essential-keybinds/meta.toml | 5 - .../03-personalizing-bos/content.md | 18 - .../03-personalizing-bos/meta.toml | 5 - breadhelp/content/tours/onboarding.toml | 24 - .../troubleshooting/01-fixing-wifi/content.md | 13 - .../troubleshooting/01-fixing-wifi/meta.toml | 5 - .../troubleshooting/_symptoms/no-sound.toml | 35 - .../_symptoms/wifi-wont-connect.toml | 25 - breadhelp/src/cli.rs | 45 - breadhelp/src/config.rs | 161 --- breadhelp/src/content/keybinds.rs | 126 -- breadhelp/src/content/markdown.rs | 237 ---- breadhelp/src/content/meta.rs | 28 - breadhelp/src/content/mod.rs | 146 --- breadhelp/src/content/tour.rs | 101 -- breadhelp/src/content/troubleshoot.rs | 66 - breadhelp/src/main.rs | 42 - breadhelp/src/services/breadd.rs | 16 - breadhelp/src/services/exec.rs | 28 - breadhelp/src/services/hyprland.rs | 96 -- breadhelp/src/services/mod.rs | 3 - breadhelp/src/theme.rs | 65 - breadhelp/src/ui/ask.rs | 173 --- breadhelp/src/ui/guide_view.rs | 20 - breadhelp/src/ui/home.rs | 180 --- breadhelp/src/ui/keybind_viewer.rs | 115 -- breadhelp/src/ui/learn.rs | 100 -- breadhelp/src/ui/mod.rs | 10 - breadhelp/src/ui/modes.rs | 25 - breadhelp/src/ui/tabs.rs | 14 - breadhelp/src/ui/tour/callout.rs | 135 -- breadhelp/src/ui/tour/mask.rs | 92 -- breadhelp/src/ui/tour/mod.rs | 456 ------- breadhelp/src/ui/tour/target.rs | 67 - breadhelp/src/ui/troubleshoot_wizard.rs | 144 --- breadhelp/src/ui/window.rs | 135 -- packaging/arch/breadhelp/PKGBUILD | 40 - packaging/arch/breadhelp/breadhelp.desktop | 9 - 57 files changed, 4333 deletions(-) delete mode 100644 .forgejo/workflows/package.yml delete mode 100644 Cargo.lock delete mode 100644 Cargo.toml delete mode 100644 breadhelp/Cargo.toml delete mode 100644 breadhelp/content/README.md delete mode 100644 breadhelp/content/advanced/01-snapshots-and-rollback/content.md delete mode 100644 breadhelp/content/advanced/01-snapshots-and-rollback/meta.toml delete mode 100644 breadhelp/content/apps/01-notes-and-tasks/content.md delete mode 100644 breadhelp/content/apps/01-notes-and-tasks/meta.toml delete mode 100644 breadhelp/content/customization/01-bar-and-launcher/content.md delete mode 100644 breadhelp/content/customization/01-bar-and-launcher/meta.toml delete mode 100644 breadhelp/content/daily-use/01-searching-your-files/content.md delete mode 100644 breadhelp/content/daily-use/01-searching-your-files/meta.toml delete mode 100644 breadhelp/content/daily-use/02-clipboard-history/content.md delete mode 100644 breadhelp/content/daily-use/02-clipboard-history/meta.toml delete mode 100644 breadhelp/content/getting-started/01-what-is-bos/content.beginner.md delete mode 100644 breadhelp/content/getting-started/01-what-is-bos/content.md delete mode 100644 breadhelp/content/getting-started/01-what-is-bos/meta.toml delete mode 100644 breadhelp/content/getting-started/02-essential-keybinds/content.md delete mode 100644 breadhelp/content/getting-started/02-essential-keybinds/meta.toml delete mode 100644 breadhelp/content/getting-started/03-personalizing-bos/content.md delete mode 100644 breadhelp/content/getting-started/03-personalizing-bos/meta.toml delete mode 100644 breadhelp/content/tours/onboarding.toml delete mode 100644 breadhelp/content/troubleshooting/01-fixing-wifi/content.md delete mode 100644 breadhelp/content/troubleshooting/01-fixing-wifi/meta.toml delete mode 100644 breadhelp/content/troubleshooting/_symptoms/no-sound.toml delete mode 100644 breadhelp/content/troubleshooting/_symptoms/wifi-wont-connect.toml delete mode 100644 breadhelp/src/cli.rs delete mode 100644 breadhelp/src/config.rs delete mode 100644 breadhelp/src/content/keybinds.rs delete mode 100644 breadhelp/src/content/markdown.rs delete mode 100644 breadhelp/src/content/meta.rs delete mode 100644 breadhelp/src/content/mod.rs delete mode 100644 breadhelp/src/content/tour.rs delete mode 100644 breadhelp/src/content/troubleshoot.rs delete mode 100644 breadhelp/src/main.rs delete mode 100644 breadhelp/src/services/breadd.rs delete mode 100644 breadhelp/src/services/exec.rs delete mode 100644 breadhelp/src/services/hyprland.rs delete mode 100644 breadhelp/src/services/mod.rs delete mode 100644 breadhelp/src/theme.rs delete mode 100644 breadhelp/src/ui/ask.rs delete mode 100644 breadhelp/src/ui/guide_view.rs delete mode 100644 breadhelp/src/ui/home.rs delete mode 100644 breadhelp/src/ui/keybind_viewer.rs delete mode 100644 breadhelp/src/ui/learn.rs delete mode 100644 breadhelp/src/ui/mod.rs delete mode 100644 breadhelp/src/ui/modes.rs delete mode 100644 breadhelp/src/ui/tabs.rs delete mode 100644 breadhelp/src/ui/tour/callout.rs delete mode 100644 breadhelp/src/ui/tour/mask.rs delete mode 100644 breadhelp/src/ui/tour/mod.rs delete mode 100644 breadhelp/src/ui/tour/target.rs delete mode 100644 breadhelp/src/ui/troubleshoot_wizard.rs delete mode 100644 breadhelp/src/ui/window.rs delete mode 100644 packaging/arch/breadhelp/PKGBUILD delete mode 100644 packaging/arch/breadhelp/breadhelp.desktop diff --git a/.forgejo/workflows/package.yml b/.forgejo/workflows/package.yml deleted file mode 100644 index 8f763e5..0000000 --- a/.forgejo/workflows/package.yml +++ /dev/null @@ -1,40 +0,0 @@ -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="breadhelp-${VERSION}/" HEAD \ - > packaging/arch/breadhelp/breadhelp-${VERSION}.tar.gz - SHA=$(sha256sum packaging/arch/breadhelp/breadhelp-${VERSION}.tar.gz | awk '{print $1}') - sed -i "s/^pkgver=.*/pkgver=${VERSION}/" packaging/arch/breadhelp/PKGBUILD - sed -i "s/^sha256sums=.*/sha256sums=('${SHA}')/" packaging/arch/breadhelp/PKGBUILD - chown -R builder:builder /home/builder/src - # --nocheck: packaging builds the artifact; tests belong in a CI job. - su builder -c "cd /home/builder/src/packaging/arch/breadhelp && makepkg -f --noconfirm --nocheck" - PKG=$(find /home/builder/src/packaging/arch/breadhelp -name '*.pkg.tar.zst' | head -1) - curl -fsS -X PUT \ - -H "Authorization: token ${PUBLISH_TOKEN}" \ - -H "Content-Type: application/octet-stream" \ - --data-binary "@${PKG}" \ - "https://git.breadway.dev/api/packages/Breadway/arch/os" diff --git a/Cargo.lock b/Cargo.lock deleted file mode 100644 index 3744843..0000000 --- a/Cargo.lock +++ /dev/null @@ -1,1108 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -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]] -name = "autocfg" -version = "1.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" - -[[package]] -name = "bitflags" -version = "2.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" - -[[package]] -name = "bread-theme" -version = "0.2.3" -source = "git+https://github.com/Breadway/bread-ecosystem?tag=v0.2.10#17d1bb85801b9a8c195b64c02d288cd662c9c780" -dependencies = [ - "dirs", - "gtk4", - "serde", - "serde_json", -] - -[[package]] -name = "breadhelp" -version = "0.2.0" -dependencies = [ - "async-channel", - "bread-theme", - "gdk4", - "glib", - "gtk4", - "gtk4-layer-shell", - "serde", - "serde_json", - "toml 0.8.23", - "toml_edit 0.22.27", -] - -[[package]] -name = "cairo-rs" -version = "0.22.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cc8d9aa793480744cd9a0524fef1a2e197d9eaa0f739cde19d16aba530dcb95" -dependencies = [ - "bitflags", - "cairo-sys-rs", - "glib", - "libc", -] - -[[package]] -name = "cairo-sys-rs" -version = "0.22.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8b4985713047f5faee02b8db6a6ef32bbb50269ff53c1aee716d1d195b76d54" -dependencies = [ - "glib-sys", - "libc", - "system-deps", -] - -[[package]] -name = "cfg-expr" -version = "0.20.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb693542bcafa528e198be0ebd9d3632ca5b7c93dbe7237460e199910835997c" -dependencies = [ - "smallvec", - "target-lexicon", -] - -[[package]] -name = "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]] -name = "equivalent" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -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]] -name = "field-offset" -version = "0.3.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38e2275cc4e4fc009b0669731a1e5ab7ebf11f469eaede2bab9309a5b4d6057f" -dependencies = [ - "memoffset", - "rustc_version", -] - -[[package]] -name = "futures-channel" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" -dependencies = [ - "futures-core", -] - -[[package]] -name = "futures-core" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" - -[[package]] -name = "futures-executor" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" -dependencies = [ - "futures-core", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-io" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" - -[[package]] -name = "futures-macro" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "futures-task" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" - -[[package]] -name = "futures-util" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" -dependencies = [ - "futures-core", - "futures-macro", - "futures-task", - "pin-project-lite", - "slab", -] - -[[package]] -name = "gdk-pixbuf" -version = "0.22.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25f420376dbee041b2db374ce4573892a36222bb3f6c0c43e24f0d67eae9b646" -dependencies = [ - "gdk-pixbuf-sys", - "gio", - "glib", - "libc", -] - -[[package]] -name = "gdk-pixbuf-sys" -version = "0.22.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48f31b37b1fc4b48b54f6b91b7ef04c18e00b4585d98359dd7b998774bbd91fb" -dependencies = [ - "gio-sys", - "glib-sys", - "gobject-sys", - "libc", - "system-deps", -] - -[[package]] -name = "gdk4" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd42fdbbf48612c6e8f47c65fb92d2e8f39c25aecd6af047e83897c1a22d2a4e" -dependencies = [ - "cairo-rs", - "gdk-pixbuf", - "gdk4-sys", - "gio", - "gl", - "glib", - "libc", - "pango", -] - -[[package]] -name = "gdk4-sys" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d974ac4f15e67472c3a9728daf612590b4a5762a4b33f0edd298df0b80d043c" -dependencies = [ - "cairo-sys-rs", - "gdk-pixbuf-sys", - "gio-sys", - "glib-sys", - "gobject-sys", - "libc", - "pango-sys", - "pkg-config", - "system-deps", -] - -[[package]] -name = "getrandom" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -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 = [ - "futures-channel", - "futures-core", - "futures-io", - "futures-util", - "gio-sys", - "glib", - "libc", - "pin-project-lite", - "smallvec", -] - -[[package]] -name = "gio-sys" -version = "0.22.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64729ba2772c080448f9f966dba8f4456beeb100d8c28a865ef8a0f2ef4987e1" -dependencies = [ - "glib-sys", - "gobject-sys", - "libc", - "system-deps", - "windows-sys 0.59.0", -] - -[[package]] -name = "gl" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a94edab108827d67608095e269cf862e60d920f144a5026d3dbcfd8b877fb404" -dependencies = [ - "gl_generator", -] - -[[package]] -name = "gl_generator" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a95dfc23a2b4a9a2f5ab41d194f8bfda3cabec42af4e39f08c339eb2a0c124d" -dependencies = [ - "khronos_api", - "log", - "xml-rs", -] - -[[package]] -name = "glib" -version = "0.22.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c207e04e51605dcf7b2924c41591b3a10e1438eaac5bcf448fb91f325381104a" -dependencies = [ - "bitflags", - "futures-channel", - "futures-core", - "futures-executor", - "futures-task", - "futures-util", - "gio-sys", - "glib-macros", - "glib-sys", - "gobject-sys", - "libc", - "memchr", - "smallvec", -] - -[[package]] -name = "glib-macros" -version = "0.22.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "506d23499707c7142898429757e8d9a3871d965239a2cb66dfa05052be6d6f19" -dependencies = [ - "heck", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "glib-sys" -version = "0.22.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f7fbac234ed5bc2a28359b7bde8e1b9cdf1441cc2d7f068e4824672d7db9445" -dependencies = [ - "libc", - "system-deps", -] - -[[package]] -name = "gobject-sys" -version = "0.22.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22a861859b887a79cf461359c192c97a57d8fb0229dd291232e57aa11f6fa72c" -dependencies = [ - "glib-sys", - "libc", - "system-deps", -] - -[[package]] -name = "graphene-rs" -version = "0.22.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7d1b7881f96869f49808b6adfe906a93a57a34204952253444d68c3208d71f1" -dependencies = [ - "glib", - "graphene-sys", - "libc", -] - -[[package]] -name = "graphene-sys" -version = "0.22.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "517f062f3fd6b7fd3e57a3f038a74b3c23ca32f51199ff028aa704609943f79c" -dependencies = [ - "glib-sys", - "libc", - "pkg-config", - "system-deps", -] - -[[package]] -name = "gsk4" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53c912dfcbd28acace5fc99c40bb9f25e1dcb73efb1f2608327f66a99acdcb62" -dependencies = [ - "cairo-rs", - "gdk4", - "glib", - "graphene-rs", - "gsk4-sys", - "libc", - "pango", -] - -[[package]] -name = "gsk4-sys" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7d54bbc7a9d8b6ffe4f0c95eede15ccfb365c8bf521275abe6bcfb57b18fb8a" -dependencies = [ - "cairo-sys-rs", - "gdk4-sys", - "glib-sys", - "gobject-sys", - "graphene-sys", - "libc", - "pango-sys", - "system-deps", -] - -[[package]] -name = "gtk4" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7181b837f04cbe93f79441475f7a00560a92cba7a72e38cc1a68b6f8b78eaae2" -dependencies = [ - "cairo-rs", - "field-offset", - "futures-channel", - "gdk-pixbuf", - "gdk4", - "gio", - "glib", - "graphene-rs", - "gsk4", - "gtk4-macros", - "gtk4-sys", - "libc", - "pango", -] - -[[package]] -name = "gtk4-layer-shell" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4069987ff4793699511a251028cc336b438e46565b463f111250148d574752a" -dependencies = [ - "bitflags", - "gdk4", - "glib", - "glib-sys", - "gtk4", - "gtk4-layer-shell-sys", - "libc", -] - -[[package]] -name = "gtk4-layer-shell-sys" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f566a5ec5bcc454e7fcf2ab76930887ced5365afce12c1e5201bb296b95f1b9" -dependencies = [ - "gdk4-sys", - "glib-sys", - "gtk4-sys", - "libc", - "system-deps", -] - -[[package]] -name = "gtk4-macros" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3581b242ba62fdff122ebb626ea641582ec326031622bd19d60f85029c804a87" -dependencies = [ - "proc-macro-crate", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "gtk4-sys" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20ba8e695e2640455561274e65e45f0a151619e450746007667f4b23ceae4e1b" -dependencies = [ - "cairo-sys-rs", - "gdk-pixbuf-sys", - "gdk4-sys", - "gio-sys", - "glib-sys", - "gobject-sys", - "graphene-sys", - "gsk4-sys", - "libc", - "pango-sys", - "system-deps", -] - -[[package]] -name = "hashbrown" -version = "0.17.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" - -[[package]] -name = "heck" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" - -[[package]] -name = "indexmap" -version = "2.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" -dependencies = [ - "equivalent", - "hashbrown", -] - -[[package]] -name = "itoa" -version = "1.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" - -[[package]] -name = "khronos_api" -version = "3.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc" - -[[package]] -name = "libc" -version = "0.2.186" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" - -[[package]] -name = "libredox" -version = "0.1.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f02ab6bace2054fb888a3c16f990117b579d14a3088e472d63c6011fa185c9d3" -dependencies = [ - "libc", -] - -[[package]] -name = "log" -version = "0.4.33" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" - -[[package]] -name = "memchr" -version = "2.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4" - -[[package]] -name = "memoffset" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" -dependencies = [ - "autocfg", -] - -[[package]] -name = "option-ext" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" - -[[package]] -name = "pango" -version = "0.22.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "251bdc6e6487b811be0e406a21e301e07e45c0aa8fa39e00c0c8e12a91752438" -dependencies = [ - "gio", - "glib", - "libc", - "pango-sys", -] - -[[package]] -name = "pango-sys" -version = "0.22.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbd111a20ca90fedf03e09c59783c679c00900f1d8491cca5399f5e33609d5d6" -dependencies = [ - "glib-sys", - "gobject-sys", - "libc", - "system-deps", -] - -[[package]] -name = "parking" -version = "2.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" - -[[package]] -name = "pin-project-lite" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" - -[[package]] -name = "pkg-config" -version = "0.3.33" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" - -[[package]] -name = "proc-macro-crate" -version = "3.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" -dependencies = [ - "toml_edit 0.25.12+spec-1.1.0", -] - -[[package]] -name = "proc-macro2" -version = "1.0.106" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "quote" -version = "1.0.45" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "redox_users" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" -dependencies = [ - "getrandom", - "libredox", - "thiserror", -] - -[[package]] -name = "rustc_version" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" -dependencies = [ - "semver", -] - -[[package]] -name = "semver" -version = "1.0.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" - -[[package]] -name = "serde" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" -dependencies = [ - "serde_core", - "serde_derive", -] - -[[package]] -name = "serde_core" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_derive" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "serde_json" -version = "1.0.150" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" -dependencies = [ - "itoa", - "memchr", - "serde", - "serde_core", - "zmij", -] - -[[package]] -name = "serde_spanned" -version = "0.6.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" -dependencies = [ - "serde", -] - -[[package]] -name = "serde_spanned" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" -dependencies = [ - "serde_core", -] - -[[package]] -name = "slab" -version = "0.4.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" - -[[package]] -name = "smallvec" -version = "1.15.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" - -[[package]] -name = "syn" -version = "2.0.117" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "system-deps" -version = "7.0.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "396a35feb67335377e0251fcbc1092fc85c484bd4e3a7a54319399da127796e7" -dependencies = [ - "cfg-expr", - "heck", - "pkg-config", - "toml 1.1.2+spec-1.1.0", - "version-compare", -] - -[[package]] -name = "target-lexicon" -version = "0.13.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca" - -[[package]] -name = "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]] -name = "toml" -version = "0.8.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" -dependencies = [ - "serde", - "serde_spanned 0.6.9", - "toml_datetime 0.6.11", - "toml_edit 0.22.27", -] - -[[package]] -name = "toml" -version = "1.1.2+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81f3d15e84cbcd896376e6730314d59fb5a87f31e4b038454184435cd57defee" -dependencies = [ - "indexmap", - "serde_core", - "serde_spanned 1.1.1", - "toml_datetime 1.1.1+spec-1.1.0", - "toml_parser", - "toml_writer", - "winnow 1.0.3", -] - -[[package]] -name = "toml_datetime" -version = "0.6.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" -dependencies = [ - "serde", -] - -[[package]] -name = "toml_datetime" -version = "1.1.1+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" -dependencies = [ - "serde_core", -] - -[[package]] -name = "toml_edit" -version = "0.22.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" -dependencies = [ - "indexmap", - "serde", - "serde_spanned 0.6.9", - "toml_datetime 0.6.11", - "toml_write", - "winnow 0.7.15", -] - -[[package]] -name = "toml_edit" -version = "0.25.12+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2153edc6955a6c354fad8f5efd38b6a8769bdccf9fe50f8e1329f81b0baa5d7" -dependencies = [ - "indexmap", - "toml_datetime 1.1.1+spec-1.1.0", - "toml_parser", - "winnow 1.0.3", -] - -[[package]] -name = "toml_parser" -version = "1.1.2+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" -dependencies = [ - "winnow 1.0.3", -] - -[[package]] -name = "toml_write" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" - -[[package]] -name = "toml_writer" -version = "1.1.1+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db" - -[[package]] -name = "unicode-ident" -version = "1.0.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" - -[[package]] -name = "version-compare" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03c2856837ef78f57382f06b2b8563a2f512f7185d732608fd9176cb3b8edf0e" - -[[package]] -name = "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]] -name = "windows-sys" -version = "0.59.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" -dependencies = [ - "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]] -name = "windows-targets" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" -dependencies = [ - "windows_aarch64_gnullvm 0.52.6", - "windows_aarch64_msvc 0.52.6", - "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm", - "windows_i686_msvc 0.52.6", - "windows_x86_64_gnu 0.52.6", - "windows_x86_64_gnullvm 0.52.6", - "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]] -name = "windows_aarch64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" - -[[package]] -name = "windows_i686_gnu" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" - -[[package]] -name = "windows_i686_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" - -[[package]] -name = "windows_i686_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" - -[[package]] -name = "windows_i686_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" - -[[package]] -name = "windows_i686_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" - -[[package]] -name = "winnow" -version = "0.7.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" -dependencies = [ - "memchr", -] - -[[package]] -name = "winnow" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1" -dependencies = [ - "memchr", -] - -[[package]] -name = "xml-rs" -version = "0.8.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ae8337f8a065cfc972643663ea4279e04e7256de865aa66fe25cec5fb912d3f" - -[[package]] -name = "zmij" -version = "1.0.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/Cargo.toml b/Cargo.toml deleted file mode 100644 index 9279d56..0000000 --- a/Cargo.toml +++ /dev/null @@ -1,3 +0,0 @@ -[workspace] -members = ["breadhelp"] -resolver = "2" diff --git a/breadhelp/Cargo.toml b/breadhelp/Cargo.toml deleted file mode 100644 index d63710a..0000000 --- a/breadhelp/Cargo.toml +++ /dev/null @@ -1,19 +0,0 @@ -[package] -name = "breadhelp" -version = "0.2.0" -edition = "2021" - -[dependencies] -gtk4 = { version = "0.11", features = ["v4_12"] } -gdk4 = "0.11" -gtk4-layer-shell = "0.8" -glib = "0.22" -# Shared ecosystem theming — same generated stylesheet bos-settings/breadbar/ -# breadbox/breadpad load, so this looks like part of the same desktop. -bread-theme = { git = "https://github.com/Breadway/bread-ecosystem", tag = "v0.2.10", features = ["gtk"] } -serde = { version = "1", features = ["derive"] } -serde_json = "1" -toml = "0.8" -# Non-destructive state editing (mirrors bos-settings/src/config/mod.rs). -toml_edit = "0.22" -async-channel = "2" diff --git a/breadhelp/content/README.md b/breadhelp/content/README.md deleted file mode 100644 index faa7601..0000000 --- a/breadhelp/content/README.md +++ /dev/null @@ -1,28 +0,0 @@ -# breadhelp content - -Guides and troubleshooting data, installed to `/usr/share/breadhelp/content/`. -User overrides/additions live at `~/.local/share/breadhelp/content/` (same -shape; a colliding guide directory name wins wholesale over the system copy). - -Categories are just top-level directories here — the Learn tab lists whatever -exists, so adding a category is a content change, not a code change: - -``` -getting-started/ -daily-use/ -customization/ -apps/ -troubleshooting/ - _symptoms/ # troubleshooting wizard's symptom-tree *.toml files -advanced/ -``` - -Each guide is a directory with: - -- `meta.toml` — title, difficulty, tags, related guides, estimated time -- `content.md` — the guide body (constrained Markdown subset + `[Run]`/ - `[Show Keybind]` tags — see `src/content/markdown.rs`) -- `content.beginner.md` — optional, used in Beginner/Dad mode instead of - `content.md` when present - -Content lands starting at M2. diff --git a/breadhelp/content/advanced/01-snapshots-and-rollback/content.md b/breadhelp/content/advanced/01-snapshots-and-rollback/content.md deleted file mode 100644 index a250d00..0000000 --- a/breadhelp/content/advanced/01-snapshots-and-rollback/content.md +++ /dev/null @@ -1,13 +0,0 @@ -# Snapshots and rollback - -Every package change snapshots your root filesystem automatically (snapper + -snap-pac). Nothing to configure — it just happens. - -## Roll back - -- From BOS Settings' Snapshots page, or -- Right from the GRUB menu at boot (pick an older snapshot to boot into). - -## Take a manual snapshot before something risky - -- [Run]snapper -c root create --description "manual snapshot"|Create a snapshot now[/Run] diff --git a/breadhelp/content/advanced/01-snapshots-and-rollback/meta.toml b/breadhelp/content/advanced/01-snapshots-and-rollback/meta.toml deleted file mode 100644 index 70d051f..0000000 --- a/breadhelp/content/advanced/01-snapshots-and-rollback/meta.toml +++ /dev/null @@ -1,5 +0,0 @@ -title = "Snapshots and rollback" -difficulty = "advanced" -tags = ["snapper", "snapshots", "rollback"] -related = [] -estimated_minutes = 3 diff --git a/breadhelp/content/apps/01-notes-and-tasks/content.md b/breadhelp/content/apps/01-notes-and-tasks/content.md deleted file mode 100644 index 25d8a4e..0000000 --- a/breadhelp/content/apps/01-notes-and-tasks/content.md +++ /dev/null @@ -1,10 +0,0 @@ -# Notes and tasks - -Two dedicated apps, so quick notes and real task tracking don't compete for -the same window. - -- [Show Keybind]super + u[/Show Keybind] opens breadpad — a fast popup for jotting something down right now. -- [Show Keybind]super + m[/Show Keybind] opens breadman — reminders and a task list for things that need to stick around. - -- [Run]breadpad|Open Notes[/Run] -- [Run]breadman|Open Tasks[/Run] diff --git a/breadhelp/content/apps/01-notes-and-tasks/meta.toml b/breadhelp/content/apps/01-notes-and-tasks/meta.toml deleted file mode 100644 index bc9e73e..0000000 --- a/breadhelp/content/apps/01-notes-and-tasks/meta.toml +++ /dev/null @@ -1,5 +0,0 @@ -title = "Notes and tasks" -difficulty = "beginner" -tags = ["breadpad", "breadman", "notes"] -related = [] -estimated_minutes = 2 diff --git a/breadhelp/content/customization/01-bar-and-launcher/content.md b/breadhelp/content/customization/01-bar-and-launcher/content.md deleted file mode 100644 index 5b4b7bd..0000000 --- a/breadhelp/content/customization/01-bar-and-launcher/content.md +++ /dev/null @@ -1,7 +0,0 @@ -# Customizing the bar and launcher - -The bar (breadbar) and launcher (breadbox) both have their own settings -pages — position, modules, appearance. - -- [Run]bos-settings --page breadbar|Open Bar Settings[/Run] -- [Run]bos-settings --page breadbox|Open Launcher Settings[/Run] diff --git a/breadhelp/content/customization/01-bar-and-launcher/meta.toml b/breadhelp/content/customization/01-bar-and-launcher/meta.toml deleted file mode 100644 index 226ef70..0000000 --- a/breadhelp/content/customization/01-bar-and-launcher/meta.toml +++ /dev/null @@ -1,5 +0,0 @@ -title = "Customizing the bar and launcher" -difficulty = "beginner" -tags = ["breadbar", "breadbox", "customization"] -related = ["01-what-is-bos"] -estimated_minutes = 2 diff --git a/breadhelp/content/daily-use/01-searching-your-files/content.md b/breadhelp/content/daily-use/01-searching-your-files/content.md deleted file mode 100644 index b0ee950..0000000 --- a/breadhelp/content/daily-use/01-searching-your-files/content.md +++ /dev/null @@ -1,9 +0,0 @@ -# Searching your files - -breadsearch indexes your files so you can find things instantly, without -digging through folders. - -- Open it from BOS Settings, or run it directly. -- Type a few letters of a filename — results update as you type. - -- [Run]breadsearch|Open File Search[/Run] diff --git a/breadhelp/content/daily-use/01-searching-your-files/meta.toml b/breadhelp/content/daily-use/01-searching-your-files/meta.toml deleted file mode 100644 index 11d0284..0000000 --- a/breadhelp/content/daily-use/01-searching-your-files/meta.toml +++ /dev/null @@ -1,5 +0,0 @@ -title = "Searching your files" -difficulty = "beginner" -tags = ["search", "files", "breadsearch"] -related = [] -estimated_minutes = 2 diff --git a/breadhelp/content/daily-use/02-clipboard-history/content.md b/breadhelp/content/daily-use/02-clipboard-history/content.md deleted file mode 100644 index b65392b..0000000 --- a/breadhelp/content/daily-use/02-clipboard-history/content.md +++ /dev/null @@ -1,7 +0,0 @@ -# Clipboard history - -BOS remembers what you've copied, not just the last thing. - -- [Show Keybind]super + v[/Show Keybind] opens clipboard history — pick anything you've copied recently and paste it again. - -- [Run]breadclip|Open Clipboard History[/Run] diff --git a/breadhelp/content/daily-use/02-clipboard-history/meta.toml b/breadhelp/content/daily-use/02-clipboard-history/meta.toml deleted file mode 100644 index 3abcd7d..0000000 --- a/breadhelp/content/daily-use/02-clipboard-history/meta.toml +++ /dev/null @@ -1,5 +0,0 @@ -title = "Clipboard history" -difficulty = "beginner" -tags = ["clipboard", "breadclip"] -related = [] -estimated_minutes = 1 diff --git a/breadhelp/content/getting-started/01-what-is-bos/content.beginner.md b/breadhelp/content/getting-started/01-what-is-bos/content.beginner.md deleted file mode 100644 index ca01641..0000000 --- a/breadhelp/content/getting-started/01-what-is-bos/content.beginner.md +++ /dev/null @@ -1,10 +0,0 @@ -# Welcome! - -This computer runs BOS — a friendly desktop built for you. - -- Hold down the SUPER key (it looks like a little window logo) and press other keys to do things. -- [Show Keybind]super + /[/Show Keybind] always shows you every shortcut, so you never have to memorize anything. -- [Show Keybind]super + ,[/Show Keybind] opens Settings. -- If anything ever looks wrong, your computer saves a safety copy every time something changes — nothing is ever truly lost. - -- [Run]bos-settings|Open Settings[/Run] diff --git a/breadhelp/content/getting-started/01-what-is-bos/content.md b/breadhelp/content/getting-started/01-what-is-bos/content.md deleted file mode 100644 index 26ccccf..0000000 --- a/breadhelp/content/getting-started/01-what-is-bos/content.md +++ /dev/null @@ -1,21 +0,0 @@ -# Welcome to BOS - -BOS (the Bread Operating System) is a complete Hyprland desktop with the -bread ecosystem preinstalled: a bar, a launcher, notes, Wi-Fi profiles, and a -settings app that needs no config files. - -## The basics - -- SUPER is the Windows/Cmd key — almost everything starts with it. -- [Show Keybind]super + /[/Show Keybind] shows the full keybind cheatsheet any time. -- [Show Keybind]super + ,[/Show Keybind] opens BOS Settings — configure the bar, launcher, Wi-Fi, notes, snapshots, and package updates all in one place. -- [Show Keybind]super + space[/Show Keybind] opens the app launcher (breadbox). -- [Show Keybind]super + return[/Show Keybind] opens a terminal. - -## Safety net - -Your system snapshots itself on every package change. If an update breaks -something, roll it back from BOS Settings or pick a snapshot right from the -GRUB menu at boot. - -- [Run]bos-settings|Open BOS Settings[/Run] diff --git a/breadhelp/content/getting-started/01-what-is-bos/meta.toml b/breadhelp/content/getting-started/01-what-is-bos/meta.toml deleted file mode 100644 index b5d9c19..0000000 --- a/breadhelp/content/getting-started/01-what-is-bos/meta.toml +++ /dev/null @@ -1,5 +0,0 @@ -title = "What is BOS?" -difficulty = "beginner" -tags = ["overview", "welcome"] -related = ["02-essential-keybinds"] -estimated_minutes = 2 diff --git a/breadhelp/content/getting-started/02-essential-keybinds/content.md b/breadhelp/content/getting-started/02-essential-keybinds/content.md deleted file mode 100644 index b5955ba..0000000 --- a/breadhelp/content/getting-started/02-essential-keybinds/content.md +++ /dev/null @@ -1,23 +0,0 @@ -# The essential keybinds - -Hyprland is keyboard-first. You don't need a mouse for any of this — but -nothing here is mandatory either, click around if you'd rather. - -## Windows - -- [Show Keybind]super + return[/Show Keybind] opens a terminal -- [Show Keybind]super + backspace[/Show Keybind] closes the focused window -- [Show Keybind]super + f[/Show Keybind] toggles fullscreen -- [Show Keybind]super + i[/Show Keybind] toggles floating - -## Apps - -- [Show Keybind]super + space[/Show Keybind] opens the launcher (breadbox) -- [Show Keybind]super + e[/Show Keybind] opens files -- [Show Keybind]super + b[/Show Keybind] opens the browser -- [Show Keybind]super + u[/Show Keybind] opens notes - -## The full list - -The Home tab's keybind viewer always has the complete, current list — this -guide only covers the ones you'll use every day. diff --git a/breadhelp/content/getting-started/02-essential-keybinds/meta.toml b/breadhelp/content/getting-started/02-essential-keybinds/meta.toml deleted file mode 100644 index eb3b47d..0000000 --- a/breadhelp/content/getting-started/02-essential-keybinds/meta.toml +++ /dev/null @@ -1,5 +0,0 @@ -title = "The essential keybinds" -difficulty = "beginner" -tags = ["keybinds", "hyprland"] -related = ["01-what-is-bos"] -estimated_minutes = 3 diff --git a/breadhelp/content/getting-started/03-personalizing-bos/content.md b/breadhelp/content/getting-started/03-personalizing-bos/content.md deleted file mode 100644 index b86bccf..0000000 --- a/breadhelp/content/getting-started/03-personalizing-bos/content.md +++ /dev/null @@ -1,18 +0,0 @@ -# Personalizing BOS - -The whole desktop recolors itself from your wallpaper — bar, launcher, -notes, settings, and this help center all pick up the same palette -automatically. - -## Change your wallpaper - -- [Run]breadpaper|Open the wallpaper picker[/Run] - -Picking a new wallpaper regenerates the color palette for every bread app — -no restart needed, everything live-reloads. - -## Change the bar and launcher - -Open BOS Settings for per-app options: - -- [Run]bos-settings|Open BOS Settings[/Run] diff --git a/breadhelp/content/getting-started/03-personalizing-bos/meta.toml b/breadhelp/content/getting-started/03-personalizing-bos/meta.toml deleted file mode 100644 index 1325d32..0000000 --- a/breadhelp/content/getting-started/03-personalizing-bos/meta.toml +++ /dev/null @@ -1,5 +0,0 @@ -title = "Personalizing BOS" -difficulty = "beginner" -tags = ["wallpaper", "theme", "customization"] -related = [] -estimated_minutes = 3 diff --git a/breadhelp/content/tours/onboarding.toml b/breadhelp/content/tours/onboarding.toml deleted file mode 100644 index 950258e..0000000 --- a/breadhelp/content/tours/onboarding.toml +++ /dev/null @@ -1,24 +0,0 @@ -# Live guided tour steps, walked by ui::tour. Phase A validation subset — -# just enough to prove the spotlight/must-do engine end-to-end (breadbar's -# plain observe-only highlight, breadbox's real openlayer detection) before -# authoring the full app-by-app tour. See breadhelp-tour.lua for the -# `--tour-event` ids this matches against. - -[[step]] -id = "welcome" -title = "Welcome to BOS" -body = "This is a quick, hands-on tour of your desktop. Instead of just describing things, we'll point at the real thing on screen as we go. Press Next when you're ready." - -[[step]] -id = "breadbar" -title = "Breadbar" -body = "This bar across the top of your screen always shows your workspace, the time, and quick status for network, volume, and battery. Click any icon on it to see more." -target_namespace = "breadbar" - -[[step]] -id = "breadbox" -title = "Opening apps" -body = "Everything in BOS opens from here. Press [Show Keybind]super + space[/Show Keybind] to open breadbox, then start typing an app's name and press Enter to launch it." -target_namespace = "breadbox" -launch = "breadbox" -success_event = "layer-closed:breadbox" diff --git a/breadhelp/content/troubleshooting/01-fixing-wifi/content.md b/breadhelp/content/troubleshooting/01-fixing-wifi/content.md deleted file mode 100644 index cb14e0e..0000000 --- a/breadhelp/content/troubleshooting/01-fixing-wifi/content.md +++ /dev/null @@ -1,13 +0,0 @@ -# Fixing Wi-Fi problems - -Most Wi-Fi issues clear up by toggling the radio off and back on. - -- [Run]nmcli radio wifi off && sleep 1 && nmcli radio wifi on|Reconnect Wi-Fi[/Run] - -For saved networks and profiles (home, work, away), use breadcrumbs: - -- [Run]breadcrumbs|Manage Wi-Fi Profiles[/Run] - -Or jump straight to the Network settings page: - -- [Run]bos-settings --page network|Open Network Settings[/Run] diff --git a/breadhelp/content/troubleshooting/01-fixing-wifi/meta.toml b/breadhelp/content/troubleshooting/01-fixing-wifi/meta.toml deleted file mode 100644 index 124f122..0000000 --- a/breadhelp/content/troubleshooting/01-fixing-wifi/meta.toml +++ /dev/null @@ -1,5 +0,0 @@ -title = "Fixing Wi-Fi problems" -difficulty = "beginner" -tags = ["wifi", "network", "breadcrumbs"] -related = [] -estimated_minutes = 2 diff --git a/breadhelp/content/troubleshooting/_symptoms/no-sound.toml b/breadhelp/content/troubleshooting/_symptoms/no-sound.toml deleted file mode 100644 index b67d8bb..0000000 --- a/breadhelp/content/troubleshooting/_symptoms/no-sound.toml +++ /dev/null @@ -1,35 +0,0 @@ -# Symptom tree for the troubleshooting wizard — walked by -# src/ui/troubleshoot_wizard.rs via src/content/troubleshoot.rs. - -[[node]] -id = "start" -prompt = "What's wrong with your sound?" -[[node.options]] -label = "No sound at all" -goto = "restart-audio" -[[node.options]] -label = "Wrong output device (e.g. HDMI instead of speakers)" -goto = "wrong-device" - -[[node]] -id = "restart-audio" -prompt = "Restarting the audio service fixes this most of the time." -[node.fix] -description = "Restart PipeWire/WirePlumber" -command = "systemctl --user restart wireplumber pipewire pipewire.socket pipewire-pulse pipewire-pulse.socket" -requires_confirm = false - -[[node]] -id = "wrong-device" -prompt = "Open Sound settings and pick the right output device from the list." -[[node.options]] -label = "Open Sound settings" -goto = "open-sound-settings" - -[[node]] -id = "open-sound-settings" -prompt = "Opening BOS Settings' Sound page." -[node.fix] -description = "Open Sound settings" -command = "bos-settings --page sound" -requires_confirm = false diff --git a/breadhelp/content/troubleshooting/_symptoms/wifi-wont-connect.toml b/breadhelp/content/troubleshooting/_symptoms/wifi-wont-connect.toml deleted file mode 100644 index 2e5c535..0000000 --- a/breadhelp/content/troubleshooting/_symptoms/wifi-wont-connect.toml +++ /dev/null @@ -1,25 +0,0 @@ -[[node]] -id = "start" -prompt = "What's happening with Wi-Fi?" -[[node.options]] -label = "It won't connect / keeps dropping" -goto = "toggle-radio" -[[node.options]] -label = "I need to manage saved networks or profiles" -goto = "breadcrumbs" - -[[node]] -id = "toggle-radio" -prompt = "Toggling the Wi-Fi radio off and back on clears up most connection issues." -[node.fix] -description = "Reconnect Wi-Fi" -command = "nmcli radio wifi off && sleep 1 && nmcli radio wifi on" -requires_confirm = false - -[[node]] -id = "breadcrumbs" -prompt = "breadcrumbs manages your saved networks and profiles (home, work, away)." -[node.fix] -description = "Open breadcrumbs" -command = "breadcrumbs" -requires_confirm = false diff --git a/breadhelp/src/cli.rs b/breadhelp/src/cli.rs deleted file mode 100644 index bb35833..0000000 --- a/breadhelp/src/cli.rs +++ /dev/null @@ -1,45 +0,0 @@ -//! argv handling for a `HANDLES_COMMAND_LINE` GApplication — every -//! invocation (including ones launched while breadhelp is already running, -//! forwarded over D-Bus to the primary instance) reaches -//! `connect_command_line`, so `--onboard` on a second launch re-triggers the -//! tour in the existing window instead of spawning a duplicate. - -#[derive(Clone, Default)] -pub struct Action { - /// Force-restart the onboarding tour from step 0, regardless of whether - /// it was already completed or in progress. - pub force_onboard: bool, - /// Set by hyprland.lua's every-login autostart entry, as opposed to a - /// user explicitly running `breadhelp` or pressing SUPER+/. Lets - /// `present()` build the window (so the app is ready to respond the - /// instant it's needed) without popping it open when onboarding is - /// already done — autostart should only be visible on a genuine first - /// run, never on every subsequent login. - pub autostart: bool, - /// From a breadd Lua module (e.g. `breadhelp-suggest.lua`) reacting to a - /// system event. Resolved to banner text by `services::breadd::resolve`. - pub suggest: Option, - /// From `breadhelp-tour.lua` forwarding a bread/Hyprland event (window - /// opened, layer opened, workspace changed, or a rebind-chained ping) as - /// a candidate "the user just did the thing" signal for the live tour. - /// Only acted on if a tour is currently waiting for this exact id — - /// see the crash-safety note on `ui::tour`. - pub tour_event: Option, -} - -pub fn parse(args: &[std::ffi::OsString]) -> Action { - let mut action = Action::default(); - let mut it = args.iter().skip(1); - while let Some(arg) = it.next() { - if arg == "--onboard" { - action.force_onboard = true; - } else if arg == "--autostart" { - action.autostart = true; - } else if arg == "--suggest" { - action.suggest = it.next().and_then(|s| s.to_str()).map(str::to_string); - } else if arg == "--tour-event" { - action.tour_event = it.next().and_then(|s| s.to_str()).map(str::to_string); - } - } - action -} diff --git a/breadhelp/src/config.rs b/breadhelp/src/config.rs deleted file mode 100644 index acdca4a..0000000 --- a/breadhelp/src/config.rs +++ /dev/null @@ -1,161 +0,0 @@ -//! `~/.config/breadhelp/state.toml` — non-destructive TOML editing, same -//! `load_doc`/`save_doc` discipline as `bos-settings/src/config/mod.rs`: a -//! missing file yields defaults, a file that exists but fails to parse is -//! backed up once before falling back, so a bad edit is always recoverable. - -use std::path::{Path, PathBuf}; -use toml_edit::{value, DocumentMut}; - -pub fn config_dir() -> PathBuf { - if let Ok(xdg) = std::env::var("XDG_CONFIG_HOME") { - let p = PathBuf::from(xdg); - if p.is_absolute() { - return p; - } - } - let home = std::env::var("HOME").unwrap_or_else(|_| "/root".to_string()); - PathBuf::from(home).join(".config") -} - -fn state_path() -> PathBuf { - config_dir().join("breadhelp").join("state.toml") -} - -fn load_doc(path: &Path) -> DocumentMut { - let Ok(text) = std::fs::read_to_string(path) else { - return DocumentMut::default(); - }; - match text.parse::() { - Ok(doc) => doc, - Err(e) => { - let backup = PathBuf::from(format!("{}.bak", path.display())); - eprintln!( - "breadhelp: {} failed to parse ({e}); backed up to {} before falling back to defaults", - path.display(), - backup.display() - ); - let _ = std::fs::write(&backup, &text); - DocumentMut::default() - } - } -} - -fn save_doc(path: &Path, doc: &DocumentMut) { - if let Some(parent) = path.parent() { - if let Err(e) = std::fs::create_dir_all(parent) { - eprintln!("breadhelp: couldn't create {}: {e}", parent.display()); - return; - } - } - if let Err(e) = std::fs::write(path, doc.to_string()) { - eprintln!("breadhelp: couldn't write {}: {e}", path.display()); - } -} - -#[derive(Clone, Copy, PartialEq, Eq, Debug)] -pub enum Mode { - Normal, - Beginner, - Dad, - Compact, -} - -impl Mode { - pub fn as_str(self) -> &'static str { - match self { - Mode::Normal => "normal", - Mode::Beginner => "beginner", - Mode::Dad => "dad", - Mode::Compact => "compact", - } - } - - fn from_str(s: &str) -> Self { - match s { - "beginner" => Mode::Beginner, - "dad" => Mode::Dad, - "compact" => Mode::Compact, - _ => Mode::Normal, - } - } - - /// Beginner/Dad pick `content.beginner.md` over `content.md` when present. - pub fn is_simplified(self) -> bool { - matches!(self, Mode::Beginner | Mode::Dad) - } -} - -pub struct State { - doc: DocumentMut, - path: PathBuf, -} - -impl State { - pub fn load() -> Self { - let path = state_path(); - let doc = load_doc(&path); - Self { doc, path } - } - - pub fn onboarding_completed(&self) -> bool { - self.doc - .get("onboarding") - .and_then(|t| t.get("completed")) - .and_then(|v| v.as_bool()) - .unwrap_or(false) - } - - pub fn set_onboarding_completed(&mut self, completed: bool) { - self.doc["onboarding"]["completed"] = value(completed); - self.save(); - } - - pub fn onboarding_step(&self) -> i64 { - self.doc.get("onboarding").and_then(|t| t.get("step")).and_then(|v| v.as_integer()).unwrap_or(0) - } - - pub fn set_onboarding_step(&mut self, step: i64) { - self.doc["onboarding"]["step"] = value(step); - self.save(); - } - - /// Set the instant before `services::hyprland::rebind_temp` is called for - /// a tour step with no compositor-observable signal (e.g. the screenshot - /// step), cleared the instant after reverting. If breadhelp is killed - /// mid-step, this is how the next launch knows a keybind was left - /// pointing at a chained `--tour-event` ping and self-heals it before - /// the user can be surprised by a stray tour popup — see `ui::tour`. - pub fn pending_rebind(&self) -> Option<(String, String)> { - let key = self.doc.get("tour")?.get("pending_rebind_key")?.as_str()?.to_string(); - let original = self.doc.get("tour")?.get("pending_rebind_original")?.as_str()?.to_string(); - Some((key, original)) - } - - pub fn set_pending_rebind(&mut self, key: &str, original_bind_value: &str) { - self.doc["tour"]["pending_rebind_key"] = value(key); - self.doc["tour"]["pending_rebind_original"] = value(original_bind_value); - self.save(); - } - - pub fn clear_pending_rebind(&mut self) { - if let Some(table) = self.doc.get_mut("tour").and_then(|t| t.as_table_mut()) { - table.remove("pending_rebind_key"); - table.remove("pending_rebind_original"); - } - self.save(); - } - - pub fn mode(&self) -> Mode { - let s = self.doc.get("general").and_then(|t| t.get("mode")).and_then(|v| v.as_str()).unwrap_or("normal"); - Mode::from_str(s) - } - - pub fn set_mode(&mut self, mode: Mode) { - self.doc["general"]["mode"] = value(mode.as_str()); - self.save(); - } - - fn save(&self) { - save_doc(&self.path, &self.doc); - } -} diff --git a/breadhelp/src/content/keybinds.rs b/breadhelp/src/content/keybinds.rs deleted file mode 100644 index 46ded38..0000000 --- a/breadhelp/src/content/keybinds.rs +++ /dev/null @@ -1,126 +0,0 @@ -//! Loads `~/.config/hypr/binds.json` — the SAME structured data -//! `hyprland.lua` itself loads (via `scripts/input/{binds,keybinds}.lua`) to -//! generate the real Hyprland binds. Reading this directly, rather than a -//! separately-maintained cheatsheet, means the keybind viewer can never -//! drift from what's actually bound. - -#[derive(Clone)] -pub struct Keybind { - pub combo: String, - pub action: String, - #[allow(dead_code)] // reserved for Ask-tab filtering/future grouping - pub category: String, - /// What the onboarding tour's "Try it" button runs for this bind. - pub demo_cmd: Option, - /// The `,` half of the real `hyprctl keyword bind` - /// value (e.g. `"exec,breadbox"`) — only synthesized for `action: "exec"` - /// entries, since binds.json's other actions ("close", "fullscreen", ...) - /// are this app's own loader abstraction, not real Hyprland dispatcher - /// names, and there's no safe generic way to reconstruct one. Binds - /// without this simply don't offer a rebind button. - pub raw: Option, -} - -#[derive(serde::Deserialize)] -struct RawEntry { - action: String, - #[serde(default)] - command: Option, - key: String, - #[serde(default)] - mods: Option>, - #[serde(default)] - label: Option, - #[serde(default)] - category: String, - #[serde(default)] - demo_cmd: Option, -} - -#[derive(serde::Deserialize)] -struct BindsFile { - #[serde(default)] - default_mods: Vec, - #[serde(default)] - bindings: Vec, -} - -fn binds_json_path() -> std::path::PathBuf { - let home = std::env::var("HOME").unwrap_or_else(|_| "/root".to_string()); - std::path::PathBuf::from(home).join(".config/hypr/binds.json") -} - -pub fn load() -> Vec { - let Ok(text) = std::fs::read_to_string(binds_json_path()) else { - return Vec::new(); - }; - let file: BindsFile = match serde_json::from_str(&text) { - Ok(f) => f, - Err(e) => { - eprintln!("breadhelp: binds.json failed to parse: {e}"); - return Vec::new(); - } - }; - let default_mods = if file.default_mods.is_empty() { vec!["SUPER".to_string()] } else { file.default_mods }; - - file.bindings - .into_iter() - .map(|entry| { - // An explicit "mods": [] (media keys) must NOT fall back to - // default_mods — only an omitted `mods` field should, matching - // scripts/input/keybinds.lua's own normalize_mods semantics. - let mods = entry.mods.unwrap_or_else(|| default_mods.clone()); - let combo = if mods.is_empty() { - entry.key.to_lowercase() - } else { - format!("{} + {}", mods.join(" + ").to_lowercase(), entry.key.to_lowercase()) - }; - let raw = (entry.action == "exec").then(|| entry.command.clone()).flatten().map(|c| format!("exec,{c}")); - Keybind { - combo, - action: entry.label.unwrap_or(entry.action), - category: entry.category, - demo_cmd: entry.demo_cmd, - raw, - } - }) - .collect() -} - -/// Case-insensitive lookup by combo (e.g. "super + space") for `[Show -/// Keybind]` tags — falls back to `None` (rendered as raw text) rather than -/// panicking when a guide references a combo that doesn't exist. -pub fn find<'a>(binds: &'a [Keybind], combo: &str) -> Option<&'a Keybind> { - let norm = normalize(combo); - binds.iter().find(|b| normalize(&b.combo) == norm) -} - -fn normalize(combo: &str) -> String { - combo.chars().filter(|c| !c.is_whitespace()).collect::().to_lowercase() -} - -/// Best-effort conversion of a human combo ("super + shift + u") into -/// hyprctl's `_,` form ("SUPER_SHIFT,U") for the temporary -/// rebind feature — not a full Hyprland key-name parser, just enough for the -/// common single-letter/named-key binds this viewer lists. -pub fn to_hypr_mods_key(combo: &str) -> String { - let parts: Vec = combo - .split(|c: char| c == '+' || c.is_whitespace()) - .filter(|s| !s.is_empty()) - .map(normalize_token) - .collect(); - match parts.split_last() { - None => String::new(), - Some((key, [])) => key.clone(), - Some((key, mods)) => format!("{},{}", mods.join("_"), key), - } -} - -fn normalize_token(tok: &str) -> String { - match tok.to_lowercase().as_str() { - "space" => "SPACE".to_string(), - "return" | "enter" => "RETURN".to_string(), - "backspace" => "BACKSPACE".to_string(), - other => other.to_uppercase(), - } -} diff --git a/breadhelp/src/content/markdown.rs b/breadhelp/src/content/markdown.rs deleted file mode 100644 index c897c80..0000000 --- a/breadhelp/src/content/markdown.rs +++ /dev/null @@ -1,237 +0,0 @@ -//! Constrained-subset markdown parser + renderer for guide `content.md`. -//! -//! Deliberately not a full CommonMark parser (`pulldown-cmark`/`comrak`) and -//! deliberately not rendered as HTML in a WebKitGTK view: no sibling app in -//! this ecosystem embeds a browser engine, a full parser still wouldn't -//! understand the `[Run]`/`[Show Keybind]` tags so it buys nothing, and this -//! content is authored in-repo by BOS maintainers, not arbitrary user input — -//! a small subset is an acceptable authoring limit, not a security compromise. -//! -//! Supported: `#`/`##` headings, one-block-per-line paragraphs, `- ` bullets, -//! inline `**bold**`/`` `code` `` (rendered via GTK's own Pango markup), and -//! two custom tags: `[Run][/Run]` (optional `|Label` suffix) and -//! `[Show Keybind][/Show Keybind]`. - -use gtk4::prelude::*; -use gtk4::{Box as GBox, Button, Label, Orientation}; - -use super::keybinds::Keybind; - -#[derive(Debug, Clone, PartialEq)] -pub enum Span { - Text(String), - Run { command: String, label: String }, - Keybind(String), -} - -#[derive(Debug, Clone, PartialEq)] -pub enum Block { - Heading(u8, String), - Paragraph(Vec), - Bullet(Vec), -} - -pub fn parse(src: &str) -> Vec { - let mut blocks = Vec::new(); - for raw_line in src.lines() { - let line = raw_line.trim(); - if line.is_empty() { - continue; - } - if let Some(rest) = line.strip_prefix("## ") { - blocks.push(Block::Heading(2, rest.trim().to_string())); - } else if let Some(rest) = line.strip_prefix("# ") { - blocks.push(Block::Heading(1, rest.trim().to_string())); - } else if let Some(rest) = line.strip_prefix("- ") { - blocks.push(Block::Bullet(parse_spans(rest))); - } else { - blocks.push(Block::Paragraph(parse_spans(line))); - } - } - blocks -} - -fn parse_spans(text: &str) -> Vec { - const RUN_OPEN: &str = "[Run]"; - const RUN_CLOSE: &str = "[/Run]"; - const KB_OPEN: &str = "[Show Keybind]"; - const KB_CLOSE: &str = "[/Show Keybind]"; - - let mut spans = Vec::new(); - let mut rest = text; - loop { - let run_idx = rest.find(RUN_OPEN); - let kb_idx = rest.find(KB_OPEN); - let next = match (run_idx, kb_idx) { - (Some(r), Some(k)) if r <= k => Some((r, true)), - (Some(_), Some(k)) => Some((k, false)), - (Some(r), None) => Some((r, true)), - (None, Some(k)) => Some((k, false)), - (None, None) => None, - }; - let Some((idx, is_run)) = next else { - if !rest.is_empty() { - spans.push(Span::Text(rest.to_string())); - } - break; - }; - if idx > 0 { - spans.push(Span::Text(rest[..idx].to_string())); - } - let (open_len, close_tag) = if is_run { (RUN_OPEN.len(), RUN_CLOSE) } else { (KB_OPEN.len(), KB_CLOSE) }; - let after = &rest[idx + open_len..]; - let Some(end) = after.find(close_tag) else { - // Unterminated tag — keep the literal text rather than dropping it. - spans.push(Span::Text(rest[idx..].to_string())); - break; - }; - let inner = &after[..end]; - if is_run { - let (command, label) = match inner.split_once('|') { - Some((c, l)) => (c.trim().to_string(), l.trim().to_string()), - None => (inner.trim().to_string(), inner.trim().to_string()), - }; - spans.push(Span::Run { command, label }); - } else { - spans.push(Span::Keybind(inner.trim().to_string())); - } - rest = &after[end + close_tag.len()..]; - } - spans -} - -pub fn render(blocks: &[Block], binds: &[Keybind]) -> gtk4::Widget { - let vbox = GBox::new(Orientation::Vertical, 10); - for block in blocks { - match block { - Block::Heading(level, text) => { - let lbl = Label::new(None); - let size = if *level == 1 { "large" } else { "medium" }; - lbl.set_markup(&format!("{}", glib::markup_escape_text(text))); - lbl.set_xalign(0.0); - lbl.set_wrap(true); - vbox.append(&lbl); - } - Block::Paragraph(spans) => vbox.append(&render_spans(spans, binds)), - Block::Bullet(spans) => { - let row = GBox::new(Orientation::Horizontal, 6); - row.append(&Label::new(Some("\u{2022}"))); - row.append(&render_spans(spans, binds)); - vbox.append(&row); - } - } - } - vbox.upcast() -} - -fn render_spans(spans: &[Span], binds: &[Keybind]) -> gtk4::Widget { - let row = GBox::new(Orientation::Horizontal, 6); - for span in spans { - match span { - Span::Text(t) => { - if t.trim().is_empty() { - continue; - } - let lbl = Label::new(None); - lbl.set_markup(&inline_markup(t)); - lbl.set_wrap(true); - // `set_wrap` alone only wraps once something else constrains - // the label's allocated width (true inside guide_view's fixed- - // width scrolled pane) — a freestanding top-level window (the - // tour callout) has nothing to allocate against, so without - // this a long paragraph reports its unwrapped natural width - // and the whole window balloons to fit it. - lbl.set_max_width_chars(48); - lbl.set_xalign(0.0); - row.append(&lbl); - } - Span::Run { command, label } => { - let btn = Button::with_label(label); - let cmd = command.clone(); - btn.connect_clicked(move |_| crate::services::exec::run(&cmd)); - row.append(&btn); - } - Span::Keybind(combo) => { - let text = super::keybinds::find(binds, combo).map(|k| k.combo.clone()).unwrap_or_else(|| combo.clone()); - let chip = Label::new(Some(&text)); - chip.add_css_class("keybind-chip"); - row.append(&chip); - } - } - } - row.upcast() -} - -fn inline_markup(text: &str) -> String { - let escaped = glib::markup_escape_text(text).to_string(); - let bolded = replace_delim(&escaped, "**", "", ""); - replace_delim(&bolded, "`", "", "") -} - -/// Alternates `open`/`close` between successive `delim`-separated segments — -/// an odd number of delimiters leaves a dangling tag, which GTK's markup -/// parser reports as a warning rather than crashing (acceptable: this only -/// runs over content authored in-repo, per the module's rationale above). -fn replace_delim(s: &str, delim: &str, open: &str, close: &str) -> String { - let mut parts = s.split(delim); - let mut out = parts.next().unwrap_or_default().to_string(); - for (i, part) in parts.enumerate() { - out.push_str(if i % 2 == 0 { open } else { close }); - out.push_str(part); - } - out -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn parses_heading_bullet_paragraph() { - let blocks = parse("# Title\n\nSome text.\n\n- a bullet\n"); - assert_eq!(blocks.len(), 3); - assert!(matches!(&blocks[0], Block::Heading(1, t) if t == "Title")); - assert!(matches!(&blocks[1], Block::Paragraph(_))); - assert!(matches!(&blocks[2], Block::Bullet(_))); - } - - #[test] - fn parses_run_tag_with_label() { - let spans = parse_spans("Click [Run]bos-update|Update everything[/Run] to update."); - assert_eq!(spans.len(), 3); - assert!(matches!(&spans[0], Span::Text(t) if t == "Click ")); - assert_eq!( - spans[1], - Span::Run { command: "bos-update".into(), label: "Update everything".into() } - ); - assert!(matches!(&spans[2], Span::Text(t) if t == " to update.")); - } - - #[test] - fn parses_run_tag_without_label() { - let spans = parse_spans("[Run]breadpad[/Run]"); - assert_eq!(spans, vec![Span::Run { command: "breadpad".into(), label: "breadpad".into() }]); - } - - #[test] - fn parses_show_keybind_tag() { - let spans = parse_spans("Press [Show Keybind]super + space[/Show Keybind] to launch."); - assert_eq!(spans[1], Span::Keybind("super + space".into())); - } - - #[test] - fn counts_multiple_tags_in_one_line() { - let spans = - parse_spans("[Run]a[/Run] then [Show Keybind]super+/[/Show Keybind] then [Run]b|B[/Run]"); - let run_count = spans.iter().filter(|s| matches!(s, Span::Run { .. })).count(); - let kb_count = spans.iter().filter(|s| matches!(s, Span::Keybind(_))).count(); - assert_eq!(run_count, 2); - assert_eq!(kb_count, 1); - } - - #[test] - fn unterminated_tag_falls_back_to_text() { - let spans = parse_spans("oops [Run]no closing tag"); - assert_eq!(spans, vec![Span::Text("oops ".into()), Span::Text("[Run]no closing tag".into())]); - } -} diff --git a/breadhelp/src/content/meta.rs b/breadhelp/src/content/meta.rs deleted file mode 100644 index 905ce02..0000000 --- a/breadhelp/src/content/meta.rs +++ /dev/null @@ -1,28 +0,0 @@ -//! `meta.toml` schema — one per guide directory. - -#[derive(serde::Deserialize, Clone, Default, PartialEq, Eq)] -#[serde(rename_all = "lowercase")] -pub enum Difficulty { - #[default] - Beginner, - Intermediate, - Advanced, -} - -#[derive(serde::Deserialize, Clone, Default)] -pub struct GuideMeta { - pub title: String, - #[serde(default)] - pub difficulty: Difficulty, - #[serde(default)] - pub tags: Vec, - /// Guide dir-names in the same or another category. Unresolvable entries - /// are silently dropped when rendering "related guides" — never a crash. - #[serde(default)] - pub related: Vec, - #[serde(default)] - pub estimated_minutes: u32, - /// Overrides the parent directory name as the guide's category, if set. - #[serde(default)] - pub category: Option, -} diff --git a/breadhelp/src/content/mod.rs b/breadhelp/src/content/mod.rs deleted file mode 100644 index 814c6c9..0000000 --- a/breadhelp/src/content/mod.rs +++ /dev/null @@ -1,146 +0,0 @@ -//! Guide content: `/usr/share/breadhelp/content/` (system, shipped by the -//! package) merged with `~/.local/share/breadhelp/content/` (user overrides/ -//! additions). A guide directory name colliding between the two roots means -//! the user copy wins wholesale — no field-level merge, same "replace the -//! whole doc" discipline `bos-settings/src/config/mod.rs` uses for TOML, just -//! at the file-tree level. - -pub mod keybinds; -pub mod markdown; -pub mod meta; -pub mod tour; -pub mod troubleshoot; - -use std::collections::BTreeMap; -use std::path::{Path, PathBuf}; - -use meta::GuideMeta; - -const SYSTEM_ROOT: &str = "/usr/share/breadhelp/content"; - -/// Categories are just top-level content directories — this only fixes the -/// *display order* of ones we know about; unrecognized directories still -/// show up (sorted alphabetically after these) instead of being hidden. -const CATEGORY_ORDER: &[&str] = &[ - "getting-started", - "daily-use", - "customization", - "apps", - "troubleshooting", - "advanced", -]; - -pub struct Guide { - pub id: String, - pub category: String, - pub meta: GuideMeta, - pub dir: PathBuf, -} - -pub struct ContentStore { - guides: Vec, -} - -fn user_content_root() -> PathBuf { - if let Ok(xdg) = std::env::var("XDG_DATA_HOME") { - let p = PathBuf::from(xdg); - if p.is_absolute() { - return p.join("breadhelp/content"); - } - } - let home = std::env::var("HOME").unwrap_or_else(|_| "/root".to_string()); - PathBuf::from(home).join(".local/share/breadhelp/content") -} - -fn scan_root(root: &Path, out: &mut BTreeMap<(String, String), Guide>) { - let Ok(categories) = std::fs::read_dir(root) else { - return; - }; - for cat_entry in categories.flatten() { - let cat_path = cat_entry.path(); - if !cat_path.is_dir() { - continue; - } - let category = cat_entry.file_name().to_string_lossy().into_owned(); - let Ok(guide_dirs) = std::fs::read_dir(&cat_path) else { - continue; - }; - for guide_entry in guide_dirs.flatten() { - let guide_dir = guide_entry.path(); - if !guide_dir.is_dir() { - continue; - } - let id = guide_entry.file_name().to_string_lossy().into_owned(); - // troubleshooting/_symptoms holds wizard data, not guides. - if id == "_symptoms" { - continue; - } - let meta_path = guide_dir.join("meta.toml"); - let Ok(meta_text) = std::fs::read_to_string(&meta_path) else { - continue; - }; - let Ok(meta) = toml::from_str::(&meta_text) else { - eprintln!("breadhelp: {} failed to parse, skipping", meta_path.display()); - continue; - }; - let effective_category = meta.category.clone().unwrap_or_else(|| category.clone()); - out.insert( - (effective_category.clone(), id.clone()), - Guide { id, category: effective_category, meta, dir: guide_dir }, - ); - } - } -} - -fn category_rank(category: &str) -> usize { - CATEGORY_ORDER.iter().position(|c| *c == category).unwrap_or(CATEGORY_ORDER.len()) -} - -impl ContentStore { - pub fn load() -> Self { - let mut by_key = BTreeMap::new(); - scan_root(Path::new(SYSTEM_ROOT), &mut by_key); - scan_root(&user_content_root(), &mut by_key); - - let mut guides: Vec = by_key.into_values().collect(); - guides.sort_by(|a, b| { - (category_rank(&a.category), &a.category, &a.id).cmp(&(category_rank(&b.category), &b.category, &b.id)) - }); - Self { guides } - } - - pub fn categories(&self) -> Vec { - let mut seen = Vec::new(); - for g in &self.guides { - if !seen.contains(&g.category) { - seen.push(g.category.clone()); - } - } - seen - } - - pub fn guides_in(&self, category: &str) -> Vec<&Guide> { - self.guides.iter().filter(|g| g.category == category).collect() - } - - pub fn guide(&self, category: &str, id: &str) -> Option<&Guide> { - self.guides.iter().find(|g| g.category == category && g.id == id) - } - - pub fn all(&self) -> &[Guide] { - &self.guides - } - - /// The guide body for the given mode — `content.beginner.md` when in a - /// simplified mode and present, otherwise `content.md`. Never errors: a - /// missing file just renders as an empty guide rather than crashing. - pub fn body(&self, guide: &Guide, simplified: bool) -> String { - if simplified { - let beginner = guide.dir.join("content.beginner.md"); - if let Ok(text) = std::fs::read_to_string(&beginner) { - return text; - } - } - std::fs::read_to_string(guide.dir.join("content.md")).unwrap_or_default() - } -} diff --git a/breadhelp/src/content/tour.rs b/breadhelp/src/content/tour.rs deleted file mode 100644 index d49709f..0000000 --- a/breadhelp/src/content/tour.rs +++ /dev/null @@ -1,101 +0,0 @@ -//! Live tour step loader — same shape of problem as `troubleshoot.rs`'s -//! symptom trees: TOML data, not hardcoded Rust, so the tour can be edited -//! without a rebuild. Fields are flat optionals rather than a tagged enum in -//! the TOML itself (mirroring `SymptomOption::goto`'s plain-string encoding) -//! to keep hand-authored `onboarding.toml` simple; `target()`/`success()` -//! turn that into the richer shape `ui::tour` actually wants to match on. - -use std::path::Path; - -#[derive(Clone)] -pub enum Target { - /// A `gtk4-layer-shell` surface identified by namespace, e.g. "breadbox". - Namespace(String), - /// A plain toplevel window identified by its Wayland app-id/class. - WindowClass(String), - /// Pure-concept step — full-screen dim, centered callout, no spotlight. - None, -} - -#[derive(Clone)] -pub enum Success { - /// Auto-advance when a `--tour-event ` matching this arrives. - Event(String), - /// Auto-advance after this many seconds if no event arrives first. - Timeout(u64), - /// Next button only — no event can signal this step's completion. - Manual, -} - -#[derive(serde::Deserialize, Clone)] -pub struct Step { - pub id: String, - pub title: String, - pub body: String, - #[serde(default)] - pub target_namespace: Option, - #[serde(default)] - pub target_window_class: Option, - /// Shell command run when the step starts, e.g. launching breadbox so - /// there's something to highlight. Skipped if the target is already - /// resolvable — several bread apps toggle-close on a second invocation - /// of the same command, so firing this unconditionally could close an - /// already-open instance instead of opening one. - #[serde(default)] - pub launch: Option, - #[serde(default)] - pub success_event: Option, - #[serde(default)] - pub success_timeout_seconds: Option, - /// A keybind combo (e.g. "super + shift + s") to temporarily rebind so - /// its original action still runs but is chained with a - /// `--tour-event ` ping — for steps whose action has no - /// compositor-observable signal at all (e.g. taking a screenshot). - /// Reverted the moment this step is left, and self-healed on the next - /// launch if breadhelp crashes mid-step — see `ui::tour`. - #[serde(default)] - pub rebind_combo: Option, -} - -impl Step { - pub fn target(&self) -> Target { - if let Some(ns) = &self.target_namespace { - Target::Namespace(ns.clone()) - } else if let Some(class) = &self.target_window_class { - Target::WindowClass(class.clone()) - } else { - Target::None - } - } - - pub fn success(&self) -> Success { - if let Some(id) = &self.success_event { - Success::Event(id.clone()) - } else if let Some(secs) = self.success_timeout_seconds { - Success::Timeout(secs) - } else { - Success::Manual - } - } -} - -#[derive(serde::Deserialize)] -struct TourFile { - #[serde(rename = "step", default)] - steps: Vec, -} - -const SYSTEM_TOUR_PATH: &str = "/usr/share/breadhelp/content/tours/onboarding.toml"; - -pub fn load() -> Vec { - let Ok(text) = std::fs::read_to_string(Path::new(SYSTEM_TOUR_PATH)) else { - return Vec::new(); - }; - match toml::from_str::(&text) { - Ok(f) => f.steps, - Err(e) => { - eprintln!("breadhelp: {SYSTEM_TOUR_PATH} failed to parse: {e}"); - Vec::new() - } - } -} diff --git a/breadhelp/src/content/troubleshoot.rs b/breadhelp/src/content/troubleshoot.rs deleted file mode 100644 index 3613f84..0000000 --- a/breadhelp/src/content/troubleshoot.rs +++ /dev/null @@ -1,66 +0,0 @@ -//! Symptom-tree loader for the troubleshooting wizard (UI lands in M5; -//! this loader is defined now alongside the rest of the content system since -//! it's the same shape of problem as `keybinds.rs`/`meta.rs`). - -use std::path::Path; - -#[derive(serde::Deserialize, Clone)] -pub struct SymptomOption { - pub label: String, - /// Another node id, or ":" to jump to a different file. - pub goto: String, -} - -#[derive(serde::Deserialize, Clone)] -pub struct Fix { - pub description: String, - pub command: String, - #[serde(default)] - pub requires_confirm: bool, -} - -#[derive(serde::Deserialize, Clone)] -pub struct SymptomNode { - pub id: String, - pub prompt: String, - #[serde(default)] - pub options: Vec, - #[serde(default)] - pub fix: Option, -} - -#[derive(serde::Deserialize)] -struct SymptomFile { - #[serde(rename = "node", default)] - nodes: Vec, -} - -const SYSTEM_SYMPTOMS_DIR: &str = "/usr/share/breadhelp/content/troubleshooting/_symptoms"; - -/// Loads every `*.toml` file in the symptoms directory, keyed by file stem so -/// a `SymptomOption::goto` of `"no-sound:check-mute"` can be resolved. -pub fn load_all() -> std::collections::HashMap> { - let mut out = std::collections::HashMap::new(); - let Ok(entries) = std::fs::read_dir(Path::new(SYSTEM_SYMPTOMS_DIR)) else { - return out; - }; - for entry in entries.flatten() { - let path = entry.path(); - if path.extension().and_then(|e| e.to_str()) != Some("toml") { - continue; - } - let Some(stem) = path.file_stem().and_then(|s| s.to_str()) else { - continue; - }; - let Ok(text) = std::fs::read_to_string(&path) else { - continue; - }; - match toml::from_str::(&text) { - Ok(f) => { - out.insert(stem.to_string(), f.nodes); - } - Err(e) => eprintln!("breadhelp: {} failed to parse: {e}", path.display()), - } - } - out -} diff --git a/breadhelp/src/main.rs b/breadhelp/src/main.rs deleted file mode 100644 index 337863e..0000000 --- a/breadhelp/src/main.rs +++ /dev/null @@ -1,42 +0,0 @@ -mod cli; -mod config; -mod content; -mod services; -mod theme; -mod ui; - -use gtk4::gio::ApplicationFlags; -use gtk4::prelude::*; - -fn main() { - // GApplication requires a dotted id (g_application_id_is_valid rejects a - // bare "breadhelp" with a GLib-GIO-CRITICAL and silently skips setting - // it, which would break D-Bus single-instance activation). The Wayland - // app-id GTK reports is this exact string — the Hyprland window rule and - // .desktop's StartupWMClass must match it verbatim. - // - // HANDLES_COMMAND_LINE: `breadhelp --onboard` while already running must - // reach the running (primary) instance's argv, not just re-activate it - // with no arguments — that's what lets a second launch re-trigger the - // onboarding tour instead of only focusing the window. - let app = gtk4::Application::builder() - .application_id("com.breadway.breadhelp") - .flags(ApplicationFlags::HANDLES_COMMAND_LINE) - .build(); - - app.connect_command_line(|app, cmdline| { - let action = cli::parse(&cmdline.arguments()); - ui::window::present(app, action); - glib::ExitCode::SUCCESS - }); - - // Best-effort revert of a tour step's temporarily-rebound keybind (see - // `ui::tour`'s crash-safety notes) on a clean exit — `rebind_temp` is a - // detached fire-and-forget `hyprctl` spawn, so it still completes even - // though this process is exiting right after. - app.connect_shutdown(|_| { - ui::tour::self_heal(); - }); - - app.run(); -} diff --git a/breadhelp/src/services/breadd.rs b/breadhelp/src/services/breadd.rs deleted file mode 100644 index a7c06d0..0000000 --- a/breadhelp/src/services/breadd.rs +++ /dev/null @@ -1,16 +0,0 @@ -//! Maps a `--suggest ` payload (sent by a breadd Lua module, e.g. -//! `breadhelp-suggest.lua` reacting to `bread.monitor.connected`) to Home -//! tab banner text. - -pub struct Suggestion { - pub text: String, -} - -pub fn resolve(id: &str) -> Option { - match id { - "monitor-setup" => Some(Suggestion { - text: "New monitor detected — want to set up your display layout?".to_string(), - }), - _ => None, - } -} diff --git a/breadhelp/src/services/exec.rs b/breadhelp/src/services/exec.rs deleted file mode 100644 index 4bd87f9..0000000 --- a/breadhelp/src/services/exec.rs +++ /dev/null @@ -1,28 +0,0 @@ -use std::process::Command; - -/// Fire-and-forget: runs `command` via a shell, matching the idiom already -/// used throughout bos-settings' views (`let _ = Command::new(...).spawn()`). -pub fn run(command: &str) { - if let Err(e) = Command::new("sh").arg("-c").arg(command).spawn() { - eprintln!("breadhelp: failed to run `{command}`: {e}"); - } -} - -/// Runs `command` on a background thread and delivers success/failure back -/// onto the GTK main loop via `on_done` — plain `spawn()` only confirms the -/// process *launched*, not how it exited, and one-click fixes need to report -/// that back to the user (a toast), so this blocks a throwaway thread on -/// `Command::status()` instead of the UI thread. -pub fn run_reporting(command: &str, on_done: impl Fn(bool) + 'static) { - let command = command.to_string(); - let (tx, rx) = async_channel::bounded(1); - std::thread::spawn(move || { - let status = Command::new("sh").arg("-c").arg(&command).status(); - let _ = tx.send_blocking(status.map(|s| s.success()).unwrap_or(false)); - }); - glib::MainContext::default().spawn_local(async move { - if let Ok(success) = rx.recv().await { - on_done(success); - } - }); -} diff --git a/breadhelp/src/services/hyprland.rs b/breadhelp/src/services/hyprland.rs deleted file mode 100644 index 1060481..0000000 --- a/breadhelp/src/services/hyprland.rs +++ /dev/null @@ -1,96 +0,0 @@ -use serde::Deserialize; -use std::process::Command; - -/// Runtime-only rebind via `hyprctl keyword bind`: takes effect immediately -/// and resets on the next Hyprland reload/login. `hyprland.lua` can't safely -/// be rewritten line-by-line by a program (hand-authored Lua, arbitrary -/// formatting), so this is the only rebind path — callers must label it -/// clearly as temporary in the UI. -/// -/// `bind_value` is the full `,,,` string, e.g. -/// `"SUPER,U,exec,breadpad"`. -pub fn rebind_temp(bind_value: &str) { - if let Err(e) = Command::new("hyprctl").args(["keyword", "bind", bind_value]).spawn() { - eprintln!("breadhelp: hyprctl rebind failed: {e}"); - } -} - -#[derive(Debug, Deserialize)] -pub struct Client { - pub class: String, - pub at: (i32, i32), - pub size: (i32, i32), - /// Numeric monitor id — join against `monitors()` by `Monitor::id` for a name. - pub monitor: i32, -} - -#[derive(Debug, Deserialize)] -pub struct Monitor { - pub id: i32, - pub name: String, - pub x: i32, - pub y: i32, - pub width: i32, - pub height: i32, - pub focused: bool, -} - -#[derive(Debug, Clone)] -pub struct LayerClient { - pub namespace: String, - pub x: i32, - pub y: i32, - pub w: i32, - pub h: i32, - pub monitor: String, -} - -fn query_json(cmd: &str) -> Option { - let output = Command::new("hyprctl").args(["-j", cmd]).output().ok()?; - output.status.success().then(|| serde_json::from_slice(&output.stdout).ok()).flatten() -} - -pub fn clients() -> Vec { - query_json("clients").and_then(|v| serde_json::from_value(v).ok()).unwrap_or_default() -} - -pub fn monitors() -> Vec { - query_json("monitors").and_then(|v| serde_json::from_value(v).ok()).unwrap_or_default() -} - -/// `hyprctl -j layers` nests surfaces as `{monitor_name: {levels: {level: [surface]}}}` -/// with no fixed schema the `hyprland` crate or any sibling repo has a typed struct -/// for, so this walks the `serde_json::Value` by hand instead of deriving `Deserialize`. -pub fn layers() -> Vec { - let Some(serde_json::Value::Object(by_monitor)) = query_json("layers") else { - return Vec::new(); - }; - let mut out = Vec::new(); - for (monitor_name, monitor_val) in by_monitor { - let Some(levels) = monitor_val.get("levels").and_then(|v| v.as_object()) else { - continue; - }; - for entries in levels.values().filter_map(|v| v.as_array()) { - for entry in entries { - let fields = ( - entry.get("namespace").and_then(|v| v.as_str()), - entry.get("x").and_then(|v| v.as_i64()), - entry.get("y").and_then(|v| v.as_i64()), - entry.get("w").and_then(|v| v.as_i64()), - entry.get("h").and_then(|v| v.as_i64()), - ); - if let (Some(namespace), Some(x), Some(y), Some(w), Some(h)) = fields { - out.push(LayerClient { - namespace: namespace.to_string(), - x: x as i32, - y: y as i32, - w: w as i32, - h: h as i32, - monitor: monitor_name.clone(), - }); - } - } - } - } - out -} diff --git a/breadhelp/src/services/mod.rs b/breadhelp/src/services/mod.rs deleted file mode 100644 index 5087959..0000000 --- a/breadhelp/src/services/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub mod breadd; -pub mod exec; -pub mod hyprland; diff --git a/breadhelp/src/theme.rs b/breadhelp/src/theme.rs deleted file mode 100644 index 0f87d6f..0000000 --- a/breadhelp/src/theme.rs +++ /dev/null @@ -1,65 +0,0 @@ -//! Theming for breadhelp. -//! -//! Like bos-settings, breadhelp owns almost no styling: it loads the -//! ecosystem's shared stylesheet (generated by `bread-theme` from the pywal -//! palette) and layers only the layout rules specific to this app's tab -//! switcher + content shell on top. - -use gtk4::CssProvider; -use std::cell::RefCell; - -const APP_CSS: &str = "\ -.view-content { padding: 24px; }\n\ -.view-content > label.title { margin-bottom: 16px; }\n\ -/* [Show Keybind] chip: reads as a keycap inline with guide/keybind-viewer text. */\n\ -label.keybind-chip { \ - font-family: monospace; \ - font-size: 11px; \ - padding: 2px 6px; \ - border-radius: 6px; \ - background-color: alpha(@on-surface, 0.12); \ -}\n\ -/* Special modes (ui/modes.rs) — Beginner/Dad bump text and touch targets; \ - Compact tightens padding. Content simplification (content.beginner.md) \ - is a separate, content-level concern — this is visual only. */\n\ -.beginner-mode label, .dad-mode label { font-size: 15px; }\n\ -.dad-mode label { font-size: 17px; }\n\ -.dad-mode button { min-height: 44px; padding: 10px 18px; }\n\ -.beginner-mode button { min-height: 36px; }\n\ -.compact-mode .view-content { padding: 12px; }\n\ -/* ui::tour — mask strips dim everything except the spotlight cutout; the \ - callout is a small floating card near (or, for no-target steps, centered \ - over) the highlighted thing. The shared stylesheet's `window { \ - background-color: @bg }` rule matches every GtkWindow including these — \ - without this override GTK paints that opaque backdrop before the \ - translucent mask box composites onto it, so the alpha never reaches the \ - compositor and the \"dim\" reads as solid black. A class-qualified \ - selector beats the shared rule's bare type selector on specificity \ - regardless of stylesheet load order. */\n\ -window.tour-window { background-color: transparent; }\n\ -.tour-mask { background-color: rgba(0, 0, 0, 0.65); }\n\ -.tour-callout { \ - background-color: @surface; \ - color: @on-surface; \ - border-radius: 12px; \ - padding: 16px 20px; \ -}\n\ -.tour-callout label.title { font-weight: bold; font-size: 15px; }\n\ -/* The one payoff line in the whole tour — the user did the real thing and \ - the compositor proved it — shouldn't read as the same throwaway meta \ - text as the step counter. */\n\ -.tour-confirmed { color: @green; font-weight: bold; }\n\ -/* Extra top margin (on top of the callout's uniform 12px row gap) marks \ - the \"Show me\" fallback as its own beat, not a continuation of the \ - instructional paragraph above it. */\n\ -.tour-hint-row { margin-top: 8px; }\n\ -"; - -thread_local! { - static APP_PROVIDER: RefCell> = const { RefCell::new(None) }; -} - -pub fn load(_display: >k4::gdk::Display) { - bread_theme::gtk::apply_shared(); - APP_PROVIDER.with(|cell| bread_theme::gtk::apply_css(APP_CSS, cell)); -} diff --git a/breadhelp/src/ui/ask.rs b/breadhelp/src/ui/ask.rs deleted file mode 100644 index 157ea82..0000000 --- a/breadhelp/src/ui/ask.rs +++ /dev/null @@ -1,173 +0,0 @@ -use std::rc::Rc; - -use gtk4::prelude::*; -use gtk4::{Box as GBox, Entry, Label, ListBox, ListBoxRow, Orientation, Paned, ScrolledWindow, SelectionMode, Widget}; - -use crate::config::Mode; -use crate::content::{keybinds::Keybind, ContentStore}; - -use super::{guide_view, keybind_viewer}; - -struct CommandEntry { - name: &'static str, - description: &'static str, -} - -/// A small, curated command gallery — not every `bread*` binary, just the -/// ones worth surfacing to someone searching for "what can I run". -const COMMANDS: &[CommandEntry] = &[ - CommandEntry { name: "bos-update", description: "Update both the system (pacman) and the bread ecosystem (bakery) in one go." }, - CommandEntry { name: "bos-settings", description: "Open BOS Settings." }, - CommandEntry { name: "breadbox", description: "Open the app launcher." }, - CommandEntry { name: "breadpad", description: "Open notes/reminders." }, - CommandEntry { name: "breadman", description: "Open the notes/task manager." }, - CommandEntry { name: "breadpaper", description: "Open the wallpaper picker." }, - CommandEntry { name: "breadclip", description: "Open clipboard history." }, - CommandEntry { name: "breadcrumbs", description: "Manage Wi-Fi profiles." }, -]; - -enum Kind { - Guide { category: String, id: String }, - Keybind(Keybind), - Command { name: &'static str, description: &'static str }, -} - -struct IndexEntry { - label: String, - haystack: String, - kind: Kind, -} - -fn build_index(store: &ContentStore, binds: &[Keybind]) -> Vec { - let mut index = Vec::new(); - for guide in store.all() { - let haystack = format!("{} {}", guide.meta.title, guide.meta.tags.join(" ")).to_lowercase(); - index.push(IndexEntry { - label: guide.meta.title.clone(), - haystack, - kind: Kind::Guide { category: guide.category.clone(), id: guide.id.clone() }, - }); - } - for bind in binds { - let label = format!("{} — {}", bind.combo, bind.action); - let haystack = label.to_lowercase(); - index.push(IndexEntry { label, haystack, kind: Kind::Keybind(bind.clone()) }); - } - for cmd in COMMANDS { - let haystack = format!("{} {}", cmd.name, cmd.description).to_lowercase(); - index.push(IndexEntry { - label: cmd.name.to_string(), - haystack, - kind: Kind::Command { name: cmd.name, description: cmd.description }, - }); - } - index -} - -pub fn build(store: Rc, binds: Rc>, mode: Mode) -> Widget { - let hpaned = Paned::new(Orientation::Horizontal); - hpaned.set_position(320); - hpaned.set_shrink_start_child(false); - hpaned.set_resize_start_child(false); - - let left = GBox::new(Orientation::Vertical, 8); - left.add_css_class("view-content"); - - let search = Entry::new(); - search.set_placeholder_text(Some("Search guides, keybinds, commands\u{2026}")); - left.append(&search); - - let results_list = ListBox::new(); - results_list.set_selection_mode(SelectionMode::None); - results_list.set_activate_on_single_click(true); - results_list.add_css_class("boxed-list"); - let results_scroller = ScrolledWindow::builder().child(&results_list).vexpand(true).build(); - left.append(&results_scroller); - - let detail = GBox::new(Orientation::Vertical, 10); - detail.add_css_class("view-content"); - let detail_scroller = ScrolledWindow::builder().child(&detail).vexpand(true).hexpand(true).build(); - - let index = Rc::new(build_index(&store, &binds)); - - let populate: Rc = { - let results_list = results_list.clone(); - let index = index.clone(); - Rc::new(move |query: &str| { - while let Some(child) = results_list.first_child() { - results_list.remove(&child); - } - let q = query.to_lowercase(); - let matches = index - .iter() - .enumerate() - .filter(|(_, e)| q.is_empty() || e.haystack.contains(&q)) - .take(40); - for (i, entry) in matches { - let row = ListBoxRow::new(); - row.set_widget_name(&i.to_string()); - let lbl = Label::new(Some(&entry.label)); - lbl.set_xalign(0.0); - lbl.set_wrap(true); - lbl.set_margin_top(6); - lbl.set_margin_bottom(6); - lbl.set_margin_start(8); - lbl.set_margin_end(8); - row.set_child(Some(&lbl)); - results_list.append(&row); - } - }) - }; - populate(""); - - search.connect_changed({ - let populate = populate.clone(); - move |entry| populate(&entry.text()) - }); - - results_list.connect_row_activated(move |_, row| { - let Ok(i) = row.widget_name().parse::() else { return }; - let Some(entry) = index.get(i) else { return }; - - while let Some(child) = detail.first_child() { - detail.remove(&child); - } - - match &entry.kind { - Kind::Guide { category, id } => { - if let Some(guide) = store.guide(category, id) { - detail.append(&guide_view::build(&store, guide, mode.is_simplified(), &binds)); - } - } - Kind::Keybind(bind) => { - let list = ListBox::new(); - list.set_selection_mode(SelectionMode::None); - list.add_css_class("boxed-list"); - list.append(&keybind_viewer::row(bind)); - detail.append(&list); - } - Kind::Command { name, description } => { - let title = Label::new(Some(name)); - title.add_css_class("title"); - title.set_xalign(0.0); - detail.append(&title); - - let desc = Label::new(Some(description)); - desc.set_wrap(true); - desc.set_xalign(0.0); - detail.append(&desc); - - let run_btn = gtk4::Button::with_label("Run"); - run_btn.set_halign(gtk4::Align::Start); - let cmd = name.to_string(); - run_btn.connect_clicked(move |_| crate::services::exec::run(&cmd)); - detail.append(&run_btn); - } - } - }); - - hpaned.set_start_child(Some(&left)); - hpaned.set_end_child(Some(&detail_scroller)); - - hpaned.upcast() -} diff --git a/breadhelp/src/ui/guide_view.rs b/breadhelp/src/ui/guide_view.rs deleted file mode 100644 index 42da4c7..0000000 --- a/breadhelp/src/ui/guide_view.rs +++ /dev/null @@ -1,20 +0,0 @@ -use gtk4::prelude::*; -use gtk4::{Box as GBox, Label, Orientation, ScrolledWindow, Widget}; - -use crate::content::{keybinds::Keybind, markdown, ContentStore, Guide}; - -pub fn build(store: &ContentStore, guide: &Guide, simplified: bool, binds: &[Keybind]) -> Widget { - let vbox = GBox::new(Orientation::Vertical, 12); - vbox.add_css_class("view-content"); - - let title = Label::new(Some(&guide.meta.title)); - title.add_css_class("title"); - title.set_xalign(0.0); - vbox.append(&title); - - let body = store.body(guide, simplified); - let blocks = markdown::parse(&body); - vbox.append(&markdown::render(&blocks, binds)); - - ScrolledWindow::builder().child(&vbox).vexpand(true).hexpand(true).build().upcast() -} diff --git a/breadhelp/src/ui/home.rs b/breadhelp/src/ui/home.rs deleted file mode 100644 index f2c1152..0000000 --- a/breadhelp/src/ui/home.rs +++ /dev/null @@ -1,180 +0,0 @@ -use gtk4::prelude::*; -use gtk4::{ - Align, ApplicationWindow, Box as GBox, Button, DropDown, FlowBox, Label, Orientation, Revealer, - RevealerTransitionType, ScrolledWindow, StringList, Widget, -}; - -use crate::config::Mode; -use crate::content::keybinds::Keybind; - -use super::{keybind_viewer, troubleshoot_wizard}; - -const FIX_SOUND: &str = "systemctl --user restart wireplumber pipewire pipewire.socket pipewire-pulse pipewire-pulse.socket"; -const FIX_WIFI: &str = "nmcli radio wifi off && sleep 1 && nmcli radio wifi on"; -const FIX_UPDATE: &str = "bos-update"; -const FIX_BACKUP: &str = "snapper -c root create --description 'breadhelp manual backup'"; - -/// Handle back to the parts of Home that get updated after construction — -/// the suggestion banner (from breadd, see `services::breadd`) and the -/// one-click-fix toast, neither of which exist until the widget tree is -/// built once. -pub struct Home { - pub root: Widget, - banner_revealer: Revealer, - banner_label: Label, -} - -impl Home { - pub fn set_suggestion(&self, text: Option<&str>) { - match text { - Some(t) => { - self.banner_label.set_label(t); - self.banner_revealer.set_reveal_child(true); - } - None => self.banner_revealer.set_reveal_child(false), - } - } -} - -pub fn build( - binds: &[Keybind], - parent_window: &ApplicationWindow, - initial_mode: Mode, - on_mode_change: impl Fn(Mode) + 'static, -) -> Home { - let vbox = GBox::new(Orientation::Vertical, 16); - vbox.add_css_class("view-content"); - - let header = GBox::new(Orientation::Horizontal, 12); - let title = Label::new(Some("Home")); - title.add_css_class("title"); - title.set_xalign(0.0); - title.set_hexpand(true); - header.append(&title); - header.append(&mode_selector(initial_mode, on_mode_change)); - vbox.append(&header); - - let banner_label = Label::new(None); - banner_label.set_wrap(true); - banner_label.set_xalign(0.0); - banner_label.set_hexpand(true); - let banner_dismiss = Button::from_icon_name("window-close-symbolic"); - banner_dismiss.set_tooltip_text(Some("Dismiss")); - banner_dismiss.update_property(&[gtk4::accessible::Property::Label("Dismiss")]); - let banner_row = GBox::new(Orientation::Horizontal, 8); - banner_row.add_css_class("card"); - banner_row.set_margin_top(4); - banner_row.set_margin_bottom(4); - banner_row.append(&banner_label); - banner_row.append(&banner_dismiss); - let banner_revealer = Revealer::new(); - banner_revealer.set_transition_type(RevealerTransitionType::SlideDown); - banner_revealer.set_child(Some(&banner_row)); - banner_revealer.set_reveal_child(false); - { - let banner_revealer = banner_revealer.clone(); - banner_dismiss.connect_clicked(move |_| banner_revealer.set_reveal_child(false)); - } - vbox.append(&banner_revealer); - - vbox.append(&quick_fixes()); - - let troubleshoot_btn = Button::with_label("Something's wrong?"); - troubleshoot_btn.set_halign(Align::Start); - { - let parent = parent_window.clone(); - troubleshoot_btn.connect_clicked(move |_| troubleshoot_wizard::open(&parent)); - } - vbox.append(&troubleshoot_btn); - - // The live guided tour (ui::tour) is the real hands-on onboarding now — - // a static text preview of the same guide titles Learn already lists - // was a fossil of the old in-window wizard and just duplicated Learn's - // sidebar. A single re-entry point is all this tab needs. - let replay_btn = Button::with_label("Replay the tour"); - replay_btn.set_halign(Align::Start); - { - let parent = parent_window.clone(); - replay_btn.connect_clicked(move |_| { - crate::ui::tour::restart(&WidgetExt::display(&parent)); - }); - } - vbox.append(&replay_btn); - - vbox.append(&keybind_viewer::build(binds)); - - let root = ScrolledWindow::builder().child(&vbox).vexpand(true).hexpand(true).build().upcast(); - - Home { root, banner_revealer, banner_label } -} - -fn quick_fixes() -> Widget { - let heading_box = GBox::new(Orientation::Vertical, 8); - - let heading = Label::new(Some("Quick fixes")); - heading.add_css_class("section-header"); - heading.set_xalign(0.0); - heading_box.append(&heading); - - let flow = FlowBox::new(); - flow.set_selection_mode(gtk4::SelectionMode::None); - flow.set_max_children_per_line(4); - flow.set_column_spacing(8); - flow.set_row_spacing(8); - - let status = Label::new(None); - status.set_xalign(0.0); - status.add_css_class("dim-label"); - - for (label, command) in [ - ("Fix sound", FIX_SOUND), - ("Reconnect Wi-Fi", FIX_WIFI), - ("Update everything", FIX_UPDATE), - ("Create backup", FIX_BACKUP), - ] { - let btn = Button::with_label(label); - let command = command.to_string(); - let status = status.clone(); - let label = label.to_string(); - btn.connect_clicked(move |_| { - status.set_label(&format!("{label}\u{2026}")); - let status = status.clone(); - let label = label.clone(); - crate::services::exec::run_reporting(&command, move |success| { - status.set_label(&if success { format!("{label}: done.") } else { format!("{label}: something went wrong.") }); - }); - }); - flow.insert(&btn, -1); - } - - heading_box.append(&flow); - heading_box.append(&status); - heading_box.upcast() -} - -fn mode_selector(initial: Mode, on_change: impl Fn(Mode) + 'static) -> DropDown { - let labels = StringList::new(&["Normal", "Beginner", "Dad", "Compact"]); - let dropdown = DropDown::new(Some(labels), gtk4::Expression::NONE); - dropdown.set_selected(mode_index(initial)); - dropdown.set_tooltip_text(Some("Display mode")); - dropdown.update_property(&[gtk4::accessible::Property::Label("Display mode")]); - dropdown.connect_selected_notify(move |dd| { - let mode = match dd.selected() { - 1 => Mode::Beginner, - 2 => Mode::Dad, - 3 => Mode::Compact, - _ => Mode::Normal, - }; - on_change(mode); - }); - dropdown -} - -fn mode_index(mode: Mode) -> u32 { - match mode { - Mode::Normal => 0, - Mode::Beginner => 1, - Mode::Dad => 2, - Mode::Compact => 3, - } -} diff --git a/breadhelp/src/ui/keybind_viewer.rs b/breadhelp/src/ui/keybind_viewer.rs deleted file mode 100644 index 3cb2f14..0000000 --- a/breadhelp/src/ui/keybind_viewer.rs +++ /dev/null @@ -1,115 +0,0 @@ -use gtk4::prelude::*; -use gtk4::{Box as GBox, Button, Entry, Label, ListBox, ListBoxRow, MenuButton, Orientation, Popover, ScrolledWindow, SelectionMode}; - -use crate::content::keybinds::{self, Keybind}; - -pub fn build(binds: &[Keybind]) -> gtk4::Widget { - let vbox = GBox::new(Orientation::Vertical, 6); - - let header = Label::new(Some("Keybinds")); - header.add_css_class("title"); - header.set_xalign(0.0); - vbox.append(&header); - - let list = ListBox::new(); - list.set_selection_mode(SelectionMode::None); - list.add_css_class("boxed-list"); - - if binds.is_empty() { - let empty = Label::new(Some("No binds.json found at ~/.config/hypr/binds.json")); - empty.add_css_class("dim-label"); - vbox.append(&empty); - return vbox.upcast(); - } - - for bind in binds { - list.append(&row(bind)); - } - - let scroller = ScrolledWindow::builder().min_content_height(320).vexpand(true).child(&list).build(); - vbox.append(&scroller); - vbox.upcast() -} - -pub fn row(bind: &Keybind) -> ListBoxRow { - let row = ListBoxRow::new(); - row.set_selectable(false); - - let hbox = GBox::new(Orientation::Horizontal, 12); - hbox.set_margin_top(6); - hbox.set_margin_bottom(6); - hbox.set_margin_start(8); - hbox.set_margin_end(8); - - let combo = Label::new(Some(&bind.combo)); - combo.add_css_class("keybind-chip"); - combo.set_width_chars(20); - combo.set_xalign(0.0); - hbox.append(&combo); - - let action = Label::new(Some(&bind.action)); - action.set_hexpand(true); - action.set_xalign(0.0); - action.set_wrap(true); - hbox.append(&action); - - if let Some(demo) = &bind.demo_cmd { - let try_btn = Button::with_label("Try it"); - let cmd = demo.clone(); - try_btn.connect_clicked(move |_| crate::services::exec::run(&cmd)); - hbox.append(&try_btn); - } - - if let Some(raw) = bind.raw.clone() { - let rebind_btn = MenuButton::builder().icon_name("document-edit-symbolic").build(); - rebind_btn.set_tooltip_text(Some("Temporary rebind")); - // Icon-only button — screen readers have nothing else to announce. - rebind_btn.update_property(&[gtk4::accessible::Property::Label("Temporary rebind")]); - rebind_btn.set_popover(Some(&rebind_popover(&raw))); - hbox.append(&rebind_btn); - } - - row.set_child(Some(&hbox)); - row -} - -fn rebind_popover(raw: &str) -> Popover { - let popover = Popover::new(); - let vbox = GBox::new(Orientation::Vertical, 8); - vbox.set_margin_top(10); - vbox.set_margin_bottom(10); - vbox.set_margin_start(10); - vbox.set_margin_end(10); - - let hint = Label::new(Some("Temporary — resets on reload/login.\nEdit hyprland.lua for a permanent change.")); - hint.add_css_class("dim-label"); - hint.set_wrap(true); - vbox.append(&hint); - - let entry = Entry::new(); - entry.set_placeholder_text(Some("e.g. super + shift + u")); - vbox.append(&entry); - - let apply = Button::with_label("Apply temporarily"); - apply.add_css_class("suggested-action"); - let raw = raw.to_string(); - let popover_weak = popover.downgrade(); - apply.connect_clicked(move |_| { - let combo = entry.text(); - if combo.trim().is_empty() { - return; - } - let mods_key = keybinds::to_hypr_mods_key(&combo); - if mods_key.is_empty() { - return; - } - crate::services::hyprland::rebind_temp(&format!("{mods_key},{raw}")); - if let Some(p) = popover_weak.upgrade() { - p.popdown(); - } - }); - vbox.append(&apply); - - popover.set_child(Some(&vbox)); - popover -} diff --git a/breadhelp/src/ui/learn.rs b/breadhelp/src/ui/learn.rs deleted file mode 100644 index 84d4623..0000000 --- a/breadhelp/src/ui/learn.rs +++ /dev/null @@ -1,100 +0,0 @@ -use gtk4::prelude::*; -use gtk4::{Label, ListBox, ListBoxRow, Orientation, Paned, ScrolledWindow, SelectionMode, Stack, Widget}; - -use crate::config::Mode; -use crate::content::{keybinds::Keybind, ContentStore}; - -use super::guide_view; - -pub fn build(store: &ContentStore, binds: &[Keybind], mode: Mode) -> Widget { - let hpaned = Paned::new(Orientation::Horizontal); - hpaned.set_position(220); - hpaned.set_shrink_start_child(false); - hpaned.set_resize_start_child(false); - - let list = ListBox::new(); - list.set_selection_mode(SelectionMode::Single); - list.add_css_class("sidebar"); - - let stack = Stack::new(); - stack.set_hexpand(true); - stack.set_vexpand(true); - - let mut first_page: Option = None; - - for category in store.categories() { - let header_row = ListBoxRow::new(); - header_row.set_selectable(false); - header_row.set_activatable(false); - let header_lbl = Label::new(Some(&display_category(&category))); - header_lbl.add_css_class("section-header"); - header_lbl.set_xalign(0.0); - header_row.set_child(Some(&header_lbl)); - list.append(&header_row); - - for guide in store.guides_in(&category) { - let page_name = format!("{}/{}", guide.category, guide.id); - - let row = ListBoxRow::new(); - row.set_widget_name(&page_name); - let lbl = Label::new(Some(&guide.meta.title)); - lbl.set_xalign(0.0); - lbl.set_margin_top(4); - lbl.set_margin_bottom(4); - lbl.set_margin_start(8); - row.set_child(Some(&lbl)); - list.append(&row); - - let view = guide_view::build(store, guide, mode.is_simplified(), binds); - stack.add_named(&view, Some(&page_name)); - if first_page.is_none() { - first_page = Some(page_name); - } - } - } - - if let Some(first) = &first_page { - stack.set_visible_child_name(first); - let mut i = 0; - loop { - match list.row_at_index(i) { - None => break, - Some(row) if row.widget_name() == *first => { - list.select_row(Some(&row)); - break; - } - _ => i += 1, - } - } - } - - { - let stack = stack.clone(); - list.connect_row_selected(move |_, row| { - if let Some(row) = row { - let name = row.widget_name(); - if !name.is_empty() { - stack.set_visible_child_name(&name); - } - } - }); - } - - let sidebar_scroller = ScrolledWindow::builder().child(&list).width_request(220).build(); - hpaned.set_start_child(Some(&sidebar_scroller)); - hpaned.set_end_child(Some(&stack)); - - hpaned.upcast() -} - -fn display_category(id: &str) -> String { - id.split('-').map(capitalize).collect::>().join(" ") -} - -fn capitalize(s: &str) -> String { - let mut chars = s.chars(); - match chars.next() { - None => String::new(), - Some(first) => first.to_uppercase().collect::() + chars.as_str(), - } -} diff --git a/breadhelp/src/ui/mod.rs b/breadhelp/src/ui/mod.rs deleted file mode 100644 index 8374aab..0000000 --- a/breadhelp/src/ui/mod.rs +++ /dev/null @@ -1,10 +0,0 @@ -pub mod ask; -pub mod guide_view; -pub mod home; -pub mod keybind_viewer; -pub mod learn; -pub mod modes; -pub mod tabs; -pub mod tour; -pub mod troubleshoot_wizard; -pub mod window; diff --git a/breadhelp/src/ui/modes.rs b/breadhelp/src/ui/modes.rs deleted file mode 100644 index a18f921..0000000 --- a/breadhelp/src/ui/modes.rs +++ /dev/null @@ -1,25 +0,0 @@ -use gtk4::prelude::*; -use gtk4::Widget; - -use crate::config::Mode; - -const ALL_CLASSES: &[&str] = &["beginner-mode", "dad-mode", "compact-mode"]; - -/// Swaps the mode CSS class on `root` — the actual layout rules (font-size -/// bumps, larger touch targets for Dad, tighter margins for Compact) live in -/// `theme.rs`'s `APP_CSS`, same "app owns only layout" split as -/// `bos-settings/src/theme.rs`. Applied to the window's root widget so it -/// affects every tab, not just Home. -pub fn apply(root: &impl IsA, mode: Mode) { - let root = root.upcast_ref::(); - for class in ALL_CLASSES { - root.remove_css_class(class); - } - let class = match mode { - Mode::Normal => return, - Mode::Beginner => "beginner-mode", - Mode::Dad => "dad-mode", - Mode::Compact => "compact-mode", - }; - root.add_css_class(class); -} diff --git a/breadhelp/src/ui/tabs.rs b/breadhelp/src/ui/tabs.rs deleted file mode 100644 index eb779cd..0000000 --- a/breadhelp/src/ui/tabs.rs +++ /dev/null @@ -1,14 +0,0 @@ -use gtk4::prelude::*; -use gtk4::{Align, Stack, StackSwitcher}; - -/// Home/Learn/Ask tab switcher. Only 3 pages, so GTK's built-in -/// `StackSwitcher` is a better fit here than the grouped-`ListBox` sidebar -/// bos-settings uses for its ~19 settings pages. -pub fn build(stack: &Stack) -> StackSwitcher { - let switcher = StackSwitcher::new(); - switcher.set_stack(Some(stack)); - switcher.set_halign(Align::Center); - switcher.set_margin_top(8); - switcher.set_margin_bottom(8); - switcher -} diff --git a/breadhelp/src/ui/tour/callout.rs b/breadhelp/src/ui/tour/callout.rs deleted file mode 100644 index 2368ca1..0000000 --- a/breadhelp/src/ui/tour/callout.rs +++ /dev/null @@ -1,135 +0,0 @@ -//! The floating explanation bubble: title/body (rendered through the same -//! constrained markdown subset guides use, so `[Show Keybind]`/`[Run]` tags -//! work here too), dot progress, Back/Next. Positioned just below (or, if -//! that would run off the bottom of the screen, above) the spotlighted -//! target; a `Target::None` step gets no anchors at all, which wlr-layer- -//! shell/gtk4-layer-shell centers on the monitor by convention. - -use gtk4::prelude::*; -use gtk4::{Box as GBox, Button, Label, Orientation, Window}; -use gtk4_layer_shell::{Edge, KeyboardMode, Layer, LayerShell}; - -use crate::content::keybinds::Keybind; -use crate::content::markdown; -use crate::content::tour::Step; - -use super::target::Rect; - -pub struct Callout { - pub window: Window, - pub back_btn: Button, - pub next_btn: Button, - pub skip_btn: Button, - pub demo_btn: Option