diff --git a/.forgejo/workflows/bibata.yml b/.forgejo/workflows/bibata.yml new file mode 100644 index 0000000..fe87f51 --- /dev/null +++ b/.forgejo/workflows/bibata.yml @@ -0,0 +1,37 @@ +name: Build and publish bibata-cursor-theme + +# Bibata is AUR-only (not in Arch's official repos), so BOS maintains an +# in-house PKGBUILD and publishes the built package to the [breadway] repo. +# It's the prebuilt -bin variant, so no build deps beyond base-devel. +on: + push: + paths: + - 'packaging/bibata/**' + workflow_dispatch: + +jobs: + bibata: + runs-on: [self-hosted, hestia] + container: + image: archlinux:latest + steps: + - name: Build and publish + env: + PUBLISH_TOKEN: ${{ secrets.REGISTRY_TOKEN }} + run: | + set -euo pipefail + pacman -Syu --noconfirm base-devel git + useradd -m builder + git config --global --add safe.directory '*' + # Clone the branch that triggered this run (not the default branch), + # so the package can be built/published from a feature branch. + git clone --depth 1 --branch "${GITHUB_REF_NAME}" \ + "https://git.breadway.dev/${GITHUB_REPOSITORY}.git" /home/builder/src + chown -R builder:builder /home/builder/src + su builder -c "cd /home/builder/src/packaging/bibata && makepkg -f --noconfirm --nocheck" + PKG=$(find /home/builder/src/packaging/bibata -name '*.pkg.tar.zst' | head -1) + curl -fsS -X PUT \ + -H "Authorization: token ${PUBLISH_TOKEN}" \ + -H "Content-Type: application/octet-stream" \ + --data-binary "@${PKG}" \ + "https://git.breadway.dev/api/packages/Breadway/arch/os" diff --git a/.forgejo/workflows/calamares.yml b/.forgejo/workflows/calamares.yml new file mode 100644 index 0000000..80637f9 --- /dev/null +++ b/.forgejo/workflows/calamares.yml @@ -0,0 +1,35 @@ +name: Build and publish calamares + +# Calamares is AUR-only (not in Arch's official repos), so BOS maintains an +# in-house PKGBUILD and publishes the built package to the [breadway] repo. +on: + push: + paths: + - 'packaging/calamares/**' + workflow_dispatch: + +jobs: + calamares: + runs-on: [self-hosted, hestia] + container: + image: archlinux:latest + steps: + - name: Build and publish + env: + PUBLISH_TOKEN: ${{ secrets.REGISTRY_TOKEN }} + run: | + set -euo pipefail + pacman -Syu --noconfirm base-devel git cmake ninja \ + extra-cmake-modules qt6-tools qt6-translations libglvnd \ + kcoreaddons kpmcore libpwquality qt6-declarative qt6-svg yaml-cpp + useradd -m builder + git config --global --add safe.directory '*' + git clone --depth 1 "https://git.breadway.dev/${GITHUB_REPOSITORY}.git" /home/builder/src + chown -R builder:builder /home/builder/src + su builder -c "cd /home/builder/src/packaging/calamares && makepkg -f --noconfirm --nocheck" + PKG=$(find /home/builder/src/packaging/calamares -name '*.pkg.tar.zst' | head -1) + curl -fsS -X PUT \ + -H "Authorization: token ${PUBLISH_TOKEN}" \ + -H "Content-Type: application/octet-stream" \ + --data-binary "@${PKG}" \ + "https://git.breadway.dev/api/packages/Breadway/arch/os" diff --git a/.forgejo/workflows/mirror.yml b/.forgejo/workflows/mirror.yml new file mode 100644 index 0000000..6a2f9ab --- /dev/null +++ b/.forgejo/workflows/mirror.yml @@ -0,0 +1,21 @@ +name: Mirror to GitHub + +on: + push: + branches: ['**'] + tags: ['**'] + +jobs: + mirror: + runs-on: [self-hosted, hestia] + steps: + - name: Mirror to GitHub + run: | + set -euo pipefail + git clone --mirror "https://git.breadway.dev/${GITHUB_REPOSITORY}.git" repo.git + cd repo.git + # Mirror only branches and tags (not refs/pull/*, which GitHub rejects); + # --prune deletes GitHub refs that no longer exist on Forgejo. + git push --prune \ + "https://x-access-token:${{ secrets.MIRROR_TOKEN }}@github.com/Breadway/bos.git" \ + '+refs/heads/*:refs/heads/*' '+refs/tags/*:refs/tags/*' diff --git a/.forgejo/workflows/package.yml b/.forgejo/workflows/package.yml new file mode 100644 index 0000000..aaf7eb7 --- /dev/null +++ b/.forgejo/workflows/package.yml @@ -0,0 +1,40 @@ +name: Build and publish package + +on: + push: + tags: ['v*'] + +jobs: + package: + runs-on: [self-hosted, hestia] + container: + image: archlinux:latest + steps: + # Note: no actions/checkout — the archlinux image has no Node, which JS + # actions require. Everything runs as shell steps and clones manually. + - name: Build and publish + env: + PUBLISH_TOKEN: ${{ secrets.REGISTRY_TOKEN }} + run: | + set -euo pipefail + VERSION="${GITHUB_REF_NAME#v}" + pacman -Syu --noconfirm base-devel git rust cargo gtk4 glib2 + useradd -m builder + git config --global --add safe.directory '*' + git clone --branch "${GITHUB_REF_NAME}" --depth 1 \ + "https://git.breadway.dev/${GITHUB_REPOSITORY}.git" /home/builder/src + cd /home/builder/src + git archive --format=tar.gz --prefix="bos-settings-${VERSION}/" HEAD \ + > packaging/arch/bos-settings-${VERSION}.tar.gz + SHA=$(sha256sum packaging/arch/bos-settings-${VERSION}.tar.gz | awk '{print $1}') + sed -i "s/^pkgver=.*/pkgver=${VERSION}/" packaging/arch/PKGBUILD + sed -i "s/^sha256sums=.*/sha256sums=('${SHA}')/" packaging/arch/PKGBUILD + chown -R builder:builder /home/builder/src + # --nocheck: packaging builds the artifact; tests belong in a CI job. + su builder -c "cd /home/builder/src/packaging/arch && makepkg -f --noconfirm --nocheck" + PKG=$(find /home/builder/src/packaging/arch -name '*.pkg.tar.zst' | head -1) + curl -fsS -X PUT \ + -H "Authorization: token ${PUBLISH_TOKEN}" \ + -H "Content-Type: application/octet-stream" \ + --data-binary "@${PKG}" \ + "https://git.breadway.dev/api/packages/Breadway/arch/os" diff --git a/.forgejo/workflows/powerlevel10k.yml b/.forgejo/workflows/powerlevel10k.yml new file mode 100644 index 0000000..b804105 --- /dev/null +++ b/.forgejo/workflows/powerlevel10k.yml @@ -0,0 +1,35 @@ +name: Build and publish powerlevel10k + +# Powerlevel10k (the BOS default zsh prompt) is AUR-only, so BOS maintains an +# in-house PKGBUILD and publishes the built package to the [breadway] repo. +# Builds gitstatus + libgit2 from source, so it needs cmake + zsh beyond base-devel. +on: + push: + paths: + - 'packaging/powerlevel10k/**' + workflow_dispatch: + +jobs: + powerlevel10k: + runs-on: [self-hosted, hestia] + container: + image: archlinux:latest + steps: + - name: Build and publish + env: + PUBLISH_TOKEN: ${{ secrets.REGISTRY_TOKEN }} + run: | + set -euo pipefail + pacman -Syu --noconfirm base-devel git cmake zsh + useradd -m builder + git config --global --add safe.directory '*' + git clone --depth 1 --branch "${GITHUB_REF_NAME}" \ + "https://git.breadway.dev/${GITHUB_REPOSITORY}.git" /home/builder/src + chown -R builder:builder /home/builder/src + su builder -c "cd /home/builder/src/packaging/powerlevel10k && makepkg -f --noconfirm --nocheck" + PKG=$(find /home/builder/src/packaging/powerlevel10k -name '*.pkg.tar.zst' | head -1) + curl -fsS -X PUT \ + -H "Authorization: token ${PUBLISH_TOKEN}" \ + -H "Content-Type: application/octet-stream" \ + --data-binary "@${PKG}" \ + "https://git.breadway.dev/api/packages/Breadway/arch/os" diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..693a6d1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,43 @@ +# Rust build artifacts +/target/ +**/*.pdb + +# Editor / IDE +.vscode/ +.idea/ +*.swp +*.swo +*~ +.direnv/ + +# OS artifacts +.DS_Store +Thumbs.db +desktop.ini + +# Environment / secrets +.env +.env.local +*.env.* +secrets/ +*.pem +*.key +*.p12 + +# archiso build artifacts (these are large and reproducible) +/iso-build/ +/iso-out/ +*.iso +*.img + +# Runtime / logs +*.log +logs/ +*.pid +*.sock + +# Claude Code local agent state +.claude/ + +# Wallpaper source drop (baked copy lives in airootfs/usr/share/backgrounds) +/Bread Background.png diff --git a/Cargo.lock b/Cargo.lock index a8c150b..9199452 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,18 @@ # 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" @@ -16,20 +28,34 @@ checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" [[package]] name = "bos-settings" -version = "0.1.0" +version = "0.3.1" dependencies = [ + "async-channel", + "bread-theme", "glib", "gtk4", "serde", "serde_json", "toml 0.8.23", + "toml_edit 0.22.27", +] + +[[package]] +name = "bread-theme" +version = "0.2.3" +source = "git+https://github.com/Breadway/bread-ecosystem?tag=v0.2.8#77417d552130281ff787e07d52541eb25e9d533b" +dependencies = [ + "dirs", + "gtk4", + "serde", + "serde_json", ] [[package]] name = "cairo-rs" -version = "0.20.12" +version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e3bd0f4e25afa9cabc157908d14eeef9067d6448c49414d17b3fb55f0eadd0" +checksum = "5cc8d9aa793480744cd9a0524fef1a2e197d9eaa0f739cde19d16aba530dcb95" dependencies = [ "bitflags", "cairo-sys-rs", @@ -39,9 +65,9 @@ dependencies = [ [[package]] name = "cairo-sys-rs" -version = "0.20.10" +version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "059cc746549898cbfd9a47754288e5a958756650ef4652bbb6c5f71a6bda4f8b" +checksum = "f8b4985713047f5faee02b8db6a6ef32bbb50269ff53c1aee716d1d195b76d54" dependencies = [ "glib-sys", "libc", @@ -58,12 +84,75 @@ dependencies = [ "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" @@ -138,9 +227,9 @@ dependencies = [ [[package]] name = "gdk-pixbuf" -version = "0.20.10" +version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fd242894c084f4beed508a56952750bce3e96e85eb68fdc153637daa163e10c" +checksum = "25f420376dbee041b2db374ce4573892a36222bb3f6c0c43e24f0d67eae9b646" dependencies = [ "gdk-pixbuf-sys", "gio", @@ -150,9 +239,9 @@ dependencies = [ [[package]] name = "gdk-pixbuf-sys" -version = "0.20.10" +version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b34f3b580c988bd217e9543a2de59823fafae369d1a055555e5f95a8b130b96" +checksum = "48f31b37b1fc4b48b54f6b91b7ef04c18e00b4585d98359dd7b998774bbd91fb" dependencies = [ "gio-sys", "glib-sys", @@ -163,9 +252,9 @@ dependencies = [ [[package]] name = "gdk4" -version = "0.9.6" +version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4850c9d9c1aecd1a3eb14fadc1cdb0ac0a2298037e116264c7473e1740a32d60" +checksum = "fd42fdbbf48612c6e8f47c65fb92d2e8f39c25aecd6af047e83897c1a22d2a4e" dependencies = [ "cairo-rs", "gdk-pixbuf", @@ -178,9 +267,9 @@ dependencies = [ [[package]] name = "gdk4-sys" -version = "0.9.6" +version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f6eb95798e2b46f279cf59005daf297d5b69555428f185650d71974a910473a" +checksum = "9d974ac4f15e67472c3a9728daf612590b4a5762a4b33f0edd298df0b80d043c" dependencies = [ "cairo-sys-rs", "gdk-pixbuf-sys", @@ -194,10 +283,21 @@ dependencies = [ ] [[package]] -name = "gio" -version = "0.20.12" +name = "getrandom" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e27e276e7b6b8d50f6376ee7769a71133e80d093bdc363bd0af71664228b831" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "gio" +version = "0.22.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3848bcba3a35cc0a71df8ba8ecfd799d6bfb862342a53a4a915fb62213aa4e6" dependencies = [ "futures-channel", "futures-core", @@ -212,22 +312,22 @@ dependencies = [ [[package]] name = "gio-sys" -version = "0.20.10" +version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "521e93a7e56fc89e84aea9a52cfc9436816a4b363b030260b699950ff1336c83" +checksum = "64729ba2772c080448f9f966dba8f4456beeb100d8c28a865ef8a0f2ef4987e1" dependencies = [ "glib-sys", "gobject-sys", "libc", "system-deps", - "windows-sys", + "windows-sys 0.59.0", ] [[package]] name = "glib" -version = "0.20.12" +version = "0.22.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffc4b6e352d4716d84d7dde562dd9aee2a7d48beb872dd9ece7f2d1515b2d683" +checksum = "c207e04e51605dcf7b2924c41591b3a10e1438eaac5bcf448fb91f325381104a" dependencies = [ "bitflags", "futures-channel", @@ -246,12 +346,11 @@ dependencies = [ [[package]] name = "glib-macros" -version = "0.20.12" +version = "0.22.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8084af62f09475a3f529b1629c10c429d7600ee1398ae12dd3bf175d74e7145" +checksum = "506d23499707c7142898429757e8d9a3871d965239a2cb66dfa05052be6d6f19" dependencies = [ "heck", - "proc-macro-crate", "proc-macro2", "quote", "syn", @@ -259,9 +358,9 @@ dependencies = [ [[package]] name = "glib-sys" -version = "0.20.10" +version = "0.22.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ab79e1ed126803a8fb827e3de0e2ff95191912b8db65cee467edb56fc4cc215" +checksum = "5f7fbac234ed5bc2a28359b7bde8e1b9cdf1441cc2d7f068e4824672d7db9445" dependencies = [ "libc", "system-deps", @@ -269,9 +368,9 @@ dependencies = [ [[package]] name = "gobject-sys" -version = "0.20.10" +version = "0.22.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec9aca94bb73989e3cfdbf8f2e0f1f6da04db4d291c431f444838925c4c63eda" +checksum = "22a861859b887a79cf461359c192c97a57d8fb0229dd291232e57aa11f6fa72c" dependencies = [ "glib-sys", "libc", @@ -280,9 +379,9 @@ dependencies = [ [[package]] name = "graphene-rs" -version = "0.20.10" +version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b86dfad7d14251c9acaf1de63bc8754b7e3b4e5b16777b6f5a748208fe9519b" +checksum = "c7d1b7881f96869f49808b6adfe906a93a57a34204952253444d68c3208d71f1" dependencies = [ "glib", "graphene-sys", @@ -291,9 +390,9 @@ dependencies = [ [[package]] name = "graphene-sys" -version = "0.20.10" +version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df583a85ba2d5e15e1797e40d666057b28bc2f60a67c9c24145e6db2cc3861ea" +checksum = "517f062f3fd6b7fd3e57a3f038a74b3c23ca32f51199ff028aa704609943f79c" dependencies = [ "glib-sys", "libc", @@ -303,9 +402,9 @@ dependencies = [ [[package]] name = "gsk4" -version = "0.9.6" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61f5e72f931c8c9f65fbfc89fe0ddc7746f147f822f127a53a9854666ac1f855" +checksum = "53c912dfcbd28acace5fc99c40bb9f25e1dcb73efb1f2608327f66a99acdcb62" dependencies = [ "cairo-rs", "gdk4", @@ -318,9 +417,9 @@ dependencies = [ [[package]] name = "gsk4-sys" -version = "0.9.6" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "755059de55fa6f85a46bde8caf03e2184c96bfda1f6206163c72fb0ea12436dc" +checksum = "d7d54bbc7a9d8b6ffe4f0c95eede15ccfb365c8bf521275abe6bcfb57b18fb8a" dependencies = [ "cairo-sys-rs", "gdk4-sys", @@ -334,9 +433,9 @@ dependencies = [ [[package]] name = "gtk4" -version = "0.9.7" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f274dd0102c21c47bbfa8ebcb92d0464fab794a22fad6c3f3d5f165139a326d6" +checksum = "7181b837f04cbe93f79441475f7a00560a92cba7a72e38cc1a68b6f8b78eaae2" dependencies = [ "cairo-rs", "field-offset", @@ -355,9 +454,9 @@ dependencies = [ [[package]] name = "gtk4-macros" -version = "0.9.5" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ed1786c4703dd196baf7e103525ce0cf579b3a63a0570fe653b7ee6bac33999" +checksum = "3581b242ba62fdff122ebb626ea641582ec326031622bd19d60f85029c804a87" dependencies = [ "proc-macro-crate", "proc-macro2", @@ -367,9 +466,9 @@ dependencies = [ [[package]] name = "gtk4-sys" -version = "0.9.6" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41e03b01e54d77c310e1d98647d73f996d04b2f29b9121fe493ea525a7ec03d6" +checksum = "20ba8e695e2640455561274e65e45f0a151619e450746007667f4b23ceae4e1b" dependencies = [ "cairo-sys-rs", "gdk-pixbuf-sys", @@ -418,6 +517,15 @@ 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 = "memchr" version = "2.8.2" @@ -434,10 +542,16 @@ dependencies = [ ] [[package]] -name = "pango" -version = "0.20.12" +name = "option-ext" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6576b311f6df659397043a5fa8a021da8f72e34af180b44f7d57348de691ab5c" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] +name = "pango" +version = "0.22.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "251bdc6e6487b811be0e406a21e301e07e45c0aa8fa39e00c0c8e12a91752438" dependencies = [ "gio", "glib", @@ -447,9 +561,9 @@ dependencies = [ [[package]] name = "pango-sys" -version = "0.20.10" +version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "186909673fc09be354555c302c0b3dcf753cd9fa08dcb8077fa663c80fb243fa" +checksum = "bbd111a20ca90fedf03e09c59783c679c00900f1d8491cca5399f5e33609d5d6" dependencies = [ "glib-sys", "gobject-sys", @@ -457,6 +571,12 @@ dependencies = [ "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" @@ -496,6 +616,17 @@ 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" @@ -614,6 +745,26 @@ 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" @@ -718,13 +869,43 @@ 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", + "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]] @@ -733,28 +914,46 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", "windows_i686_gnullvm", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "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" @@ -767,24 +966,48 @@ 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" diff --git a/can-you-begin-a-composed-beacon.md b/DESIGN.md similarity index 100% rename from can-you-begin-a-composed-beacon.md rename to DESIGN.md diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..e837178 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Breadway + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..930804c --- /dev/null +++ b/README.md @@ -0,0 +1,209 @@ +# BOS — Bread Operating System + +An Arch-based, Hyprland desktop distribution that ships the [bread +ecosystem](https://github.com/Breadway) preconfigured. One Calamares install +produces a themed, bootable Wayland desktop — no manual Arch bootstrap, no +wiring up dotfiles, no per-tool bakery installs. + +> Design rationale and the btrfs/A-B roadmap live in [DESIGN.md](DESIGN.md). +> This file is the practical overview: what's in the image, how to build it, +> and how to test it. + +## What you get + +- **Compositor**: Hyprland with a native-Lua config (`hyprland.lua`), curated + keybinds, snappy animations, blur, and pywal-driven colours on a black base. +- **bread ecosystem**, baked into `/etc/skel` from bakery-managed binaries + (no network needed at install time): `bread`/`breadd`, `breadbar` (status bar + + notification daemon), `breadbox` (launcher), `breadcrumbs` (Wi-Fi profiles), + `breadpad` (notes/reminders), `breadman`, and the `bakery` package manager. +- **bos-settings**: a GTK4 control panel that configures every bread\* app's + config from a GUI (non-destructively), plus snapshot rollback and bakery + updates. See below. +- **Login**: greetd + tuigreet → Hyprland session. +- **Boot splash**: Plymouth `bos` theme (logo + spinner, black background). +- **Theming**: global dark across GTK3 (Adwaita-dark), GTK4/libadwaita + (`color-scheme: prefer-dark`), and Qt (qt5ct/qt6ct Fusion dark); Papirus-Dark + icons; Bibata cursor. +- **Apps**: kitty, nautilus (+ gvfs), Zen browser, VLC, loupe, gnome-text-editor, + gnome-calculator, file-roller, with file associations wired in `mimeapps.list`. +- **Hardware**: pipewire audio, NetworkManager, BlueZ + blueman, CUPS printing + with avahi mDNS discovery, TLP power management, fwupd firmware updates. +- **Resilience**: btrfs + snapper + snap-pac + grub-btrfs snapshots on every + pacman transaction; zram swap; ufw firewall (deny-incoming, mDNS allowed). + +## Repo layout + +``` +bos/ +├── Cargo.toml # workspace (members: bos-settings) +├── bos-settings/ # GTK4 unified settings app (Rust) +│ └── src/ +│ ├── config/mod.rs # non-destructive toml_edit config layer +│ └── ui/{widgets,window,sidebar}.rs, ui/views/*.rs +├── iso/ # archiso profile +│ ├── profiledef.sh +│ ├── packages.x86_64 # live + installed package set +│ └── airootfs/ # files overlaid onto the image +│ └── etc/ +│ ├── skel/ # default user dotfiles (hypr, kitty, gtk, …) +│ └── calamares/ # installer config + post-install.sh +├── packaging/ # in-house PKGBUILDs for AUR-only deps +│ ├── arch/ # bos-settings +│ ├── calamares/ +│ └── bibata/ +├── .forgejo/workflows/ # CI: build + publish packages to [breadway] +├── build-local.sh # native ISO build for this machine +└── DESIGN.md +``` + +## Building the ISO + +`build-local.sh` builds the image natively (no container) and bakes this +machine's bakery-installed bread binaries into `/etc/skel`: + +```sh +sudo ./build-local.sh # release-quality (xz squashfs) +sudo FAST_BUILD=1 ./build-local.sh # fast dev iteration (zstd squashfs) +``` + +The ISO lands in `out/bos--x86_64.iso`. The script pins +`SOURCE_DATE_EPOCH` (reproducible UUIDs) and rewrites the `[breadway]` repo URL +to the Tailscale-reachable Forgejo registry for the build. + +### Why some packages are in-house + +`calamares`, `zen-browser-bin`, and `bibata-cursor-theme` are AUR-only. BOS +keeps a PKGBUILD for each under `packaging/` and republishes the built package +to the `[breadway]` repo via a Forgejo Actions workflow (built on the hestia +self-hosted runner, published with a scoped registry token). `bos-settings` +itself publishes the same way on a `v*` tag. + +## Testing in a VM + +A reusable, GPU-accelerated launcher lives at `~/bos-vm/run.sh`: + +```sh +~/bos-vm/run.sh install # boot the ISO installer (target disk attached) +~/bos-vm/run.sh # boot the installed system from the disk +``` + +It uses KVM + `-cpu host`, 8 GiB / 8 vCPU, and `virtio-vga-gl` with +`-display gtk,gl=on` (virgl) — 3D acceleration is essential for a smooth +Hyprland session in QEMU. The disk lives on NVMe (not the tmpfs `/tmp`) to +avoid memory pressure. + +## bos-settings + +`bos-settings` edits each bread\* app's TOML **non-destructively**: it parses +the file with `toml_edit`, changes only the keys a view exposes, and writes it +back — preserving comments and any keys the UI doesn't model (calendar +passwords, saved-network passwords, model paths). Views: + +| View | Config | +|------|--------| +| bread | `bread/breadd.toml` — daemon, lua, modules, all adapters, events, notifications | +| breadbar | `breadbar/style.css` override | +| breadbox | `breadbox/config.toml` — launcher contexts | +| breadcrumbs | `breadcrumbs/breadcrumbs.toml` — settings, saved networks, profiles | +| breadpad | `breadpad/breadpad.toml` — settings, model + ollama, reminders, calendar | +| Snapshots | `snapper` list / rollback / delete | +| Packages | `bakery` installed list + updates | +| Hyprland | open config in editor + monitor list | + +Build standalone: + +```sh +cargo build --release -p bos-settings +cargo test -p bos-settings # includes config round-trip tests +``` + +## The bread ecosystem at a glance + +| Tool | Role | Launch | +|------|------|--------| +| `bread` / `breadd` | Reactive automation daemon — normalises hardware/compositor signals into events dispatched to Lua modules | runs at login | +| `breadbar` | Top status bar (workspaces, clock, stats, tray) **and** the notification daemon | runs at login | +| `breadbox` | Application launcher | `SUPER+Space` | +| `breadpad` | Notes & reminders (AI-classified, optional CalDAV sync) | `SUPER+U` | +| `breadman` | Package-manager UI | `SUPER+M` | +| `breadcrumbs` | Wi-Fi profile state machine (location-aware) | CLI / BOS Settings | +| `bakery` | CLI package manager for the ecosystem | `bakery` | +| `bos-settings` | Unified GTK4 control panel for all of the above + snapshots + updates | `SUPER+,` | + +## Keyboard shortcuts + +`SUPER` is the Windows/Cmd key. Press **`SUPER+/`** at any time for this +cheatsheet in-session; first boot shows a short welcome (once). + +| Keys | Action | +|------|--------| +| `SUPER+Return` | Terminal (kitty) | +| `SUPER+Space` | App launcher (breadbox) | +| `SUPER+E` / `SUPER+B` | Files (nautilus) / Browser (zen) | +| `SUPER+U` / `SUPER+M` | breadpad / breadman | +| `SUPER+,` / `SUPER+/` | BOS Settings / keybind cheatsheet | +| `SUPER+L` / `SUPER+N` | Lock / log out | +| `SUPER+Backspace` | Close window | +| `SUPER+F` / `SUPER+V` / `SUPER+T` | Fullscreen / float / toggle split | +| `SUPER+Shift+V` | Clipboard history | +| `SUPER+Tab` | Last window | +| `SUPER+Shift+S/C/P` | Screenshot region→file / region→clipboard / screen→file | +| `SUPER+arrows` | Move focus | +| `SUPER+Shift+h/j/k/l` | Move window | +| `SUPER+Shift+arrows` | Resize window | +| `SUPER+1..0` | Switch to workspace 1–10 | +| `SUPER+Shift+1..0` | Move window to workspace | +| `SUPER+[ / ]` | Previous / next workspace | +| `SUPER+left/right-drag` | Move / resize window with the mouse | + +## Known limitations + +- **GPUs**: ships the generic Mesa stack — AMD and Intel work out of the box. + The **NVIDIA proprietary driver is not included**; NVIDIA users must install + `nvidia`/`nvidia-utils` and set the usual Hyprland env vars after install. +- **Virtual machines**: Hyprland needs GPU acceleration to be smooth. Use + `virtio-vga-gl` + `-display gtk,gl=on` (virgl); plain software rendering is + noticeably laggy. +- **Wayland-first**: X11-only apps run through XWayland; a few may misbehave. +- **Secure Boot**: not configured. Boot with Secure Boot disabled, or enroll + your own keys. The installer writes both an NVRAM entry and the removable + `EFI/BOOT/BOOTX64.EFI` fallback. +- **Snapshots assume btrfs**: the snapper/grub-btrfs tooling expects the default + btrfs subvolume layout the installer creates. + +## Recovery + +**An update broke something (system still boots):** open BOS Settings → +Snapshots and roll back, or pick a pre-update snapshot from the **GRUB +“snapshots” submenu** at boot, then run `snapper rollback` from the booted +snapshot. + +**The system won't boot (broken GRUB / lost EFI entry):** + +1. Boot the BOS ISO and open a terminal (`SUPER+Return`). +2. Mount the installed root and EFI, then chroot: + ```sh + mount -o subvol=@ /dev/sdXN /mnt + mount /dev/sdXP /mnt/boot/efi # the EFI partition + arch-chroot /mnt + ``` +3. Reinstall the bootloader (the same sequence the installer uses): + ```sh + grub-install --target=x86_64-efi --efi-directory=/boot/efi --bootloader-id=BOS --recheck + grub-install --target=x86_64-efi --efi-directory=/boot/efi --removable --recheck + grub-mkconfig -o /boot/grub/grub.cfg + ``` + +**Firmware shows “no boot device”:** select `EFI/BOOT/BOOTX64.EFI` from the +firmware boot menu — the installer always writes that removable fallback. + +## Boot architecture notes + +archiso keeps the kernel and initramfs outside the squashfs, so the installer +stages them explicitly: a `shellprocess@kernel` step copies the kernel + ucode +into the target `/boot` and writes a stock mkinitcpio preset before the native +`initcpio` module builds the initramfs. GRUB is **not** installed by Calamares' +`bootloader`/`grubcfg` modules (they leave the ESP empty in this layout) — +`post-install.sh` runs `grub-install` (NVRAM **and** `--removable`) + +`grub-mkconfig` instead, which is the sequence verified to boot. diff --git a/bakery.toml b/bakery.toml new file mode 100644 index 0000000..632de76 --- /dev/null +++ b/bakery.toml @@ -0,0 +1,12 @@ +name = "bos-settings" +description = "System settings app for Bread OS" +binaries = ["bos-settings"] +system_deps = ["gtk4", "glib2"] +optional_system_deps = ["snapper"] +bread_deps = [] + +[config] +dir = "~/.config" + +[install] +post_install = [] diff --git a/bos-settings/Cargo.toml b/bos-settings/Cargo.toml index d906354..cb30ac3 100644 --- a/bos-settings/Cargo.toml +++ b/bos-settings/Cargo.toml @@ -1,12 +1,19 @@ [package] name = "bos-settings" -version = "0.1.0" +version = "0.3.1" edition = "2021" [dependencies] -gtk4 = { version = "0.9", features = ["v4_12"] } -glib = "0.20" +gtk4 = { version = "0.11", features = ["v4_12"] } +glib = "0.22" +# Shared ecosystem theming — bos-settings loads the same generated stylesheet as +# breadbar/breadbox/breadpad so the whole desktop looks consistent. +bread-theme = { git = "https://github.com/Breadway/bread-ecosystem", tag = "v0.2.8", features = ["gtk"] } serde = { version = "1", features = ["derive"] } serde_json = "1" toml = "0.8" +# toml_edit drives non-destructive config editing: it preserves comments and +# any keys the UI doesn't model, so saving a single field never rewrites or +# drops the rest of the user's config file. +toml_edit = "0.22" async-channel = "2" diff --git a/bos-settings/src/config/mod.rs b/bos-settings/src/config/mod.rs index da3b8eb..4b56f1c 100644 --- a/bos-settings/src/config/mod.rs +++ b/bos-settings/src/config/mod.rs @@ -1,24 +1,195 @@ +//! Non-destructive config editing. +//! +//! Every bread* app owns a TOML config that may contain keys, sections, and +//! comments this settings app does not model (e.g. breadpad's calendar +//! credentials, breadcrumbs' saved-network passwords). To edit safely we parse +//! the file into a `toml_edit::DocumentMut`, mutate only the specific keys the +//! UI exposes, and write the document back — preserving everything else, +//! formatting and comments included. + use std::error::Error; use std::path::{Path, PathBuf}; -pub fn load serde::Deserialize<'de>>(path: &Path) -> Result> { - let text = std::fs::read_to_string(path)?; - Ok(toml::from_str(&text)?) +use toml_edit::{value, Array, DocumentMut, Item, Table, Value}; + +/// Load a TOML file into an editable document. A missing or unparseable file +/// yields an empty document so the UI still renders (with defaults). +pub fn load_doc(path: &Path) -> DocumentMut { + std::fs::read_to_string(path) + .ok() + .and_then(|s| s.parse::().ok()) + .unwrap_or_default() } -pub fn save(path: &Path, val: &T) -> Result<(), Box> { +/// Write the document back to disk, creating parent dirs as needed. +pub fn save_doc(path: &Path, doc: &DocumentMut) -> Result<(), Box> { if let Some(parent) = path.parent() { std::fs::create_dir_all(parent)?; } - std::fs::write(path, toml::to_string_pretty(val)?)?; + std::fs::write(path, doc.to_string())?; Ok(()) } pub fn config_dir() -> PathBuf { - let home = std::env::var("HOME").unwrap_or_else(|_| { - std::env::var("XDG_CONFIG_HOME") - .map(|p| PathBuf::from(p).parent().unwrap_or(Path::new("/")).to_string_lossy().to_string()) - .unwrap_or_else(|_| "/home/user".to_string()) - }); + // Honour XDG_CONFIG_HOME if set; otherwise fall back to $HOME/.config. + if let Ok(xdg) = std::env::var("XDG_CONFIG_HOME") { + let p = PathBuf::from(xdg); + if p.is_absolute() { + return p; + } + } + let home = std::env::var("HOME").unwrap_or_else(|_| "/root".to_string()); PathBuf::from(home).join(".config") } + +// --- typed readers (walk a dotted path, return None if absent/wrong type) --- + +fn get<'a>(doc: &'a DocumentMut, path: &[&str]) -> Option<&'a Item> { + let mut tbl = doc.as_table(); + let (last, parents) = path.split_last()?; + for key in parents { + tbl = tbl.get(key)?.as_table()?; + } + tbl.get(last) +} + +pub fn get_bool(doc: &DocumentMut, path: &[&str]) -> Option { + get(doc, path)?.as_bool() +} +pub fn get_str(doc: &DocumentMut, path: &[&str]) -> Option { + get(doc, path)?.as_str().map(str::to_string) +} +pub fn get_i64(doc: &DocumentMut, path: &[&str]) -> Option { + get(doc, path)?.as_integer() +} +pub fn get_f64(doc: &DocumentMut, path: &[&str]) -> Option { + let item = get(doc, path)?; + item.as_float().or_else(|| item.as_integer().map(|i| i as f64)) +} +/// Read an array of strings (e.g. modules.disable, contexts[].priority). +pub fn get_str_list(doc: &DocumentMut, path: &[&str]) -> Vec { + match get(doc, path).and_then(Item::as_array) { + Some(arr) => arr + .iter() + .filter_map(|v| v.as_str().map(str::to_string)) + .collect(), + None => Vec::new(), + } +} + +// --- setters (auto-create intermediate tables, replace only the leaf) --- + +fn table_at_mut<'a>(doc: &'a mut DocumentMut, parents: &[&str]) -> &'a mut Table { + let mut tbl = doc.as_table_mut(); + for key in parents { + let entry = tbl.entry(key).or_insert_with(|| Item::Table(Table::new())); + if !entry.is_table() { + *entry = Item::Table(Table::new()); + } + tbl = entry.as_table_mut().expect("just ensured table"); + } + tbl +} + +fn set_item(doc: &mut DocumentMut, path: &[&str], item: Item) { + let Some((last, parents)) = path.split_last() else { + return; + }; + table_at_mut(doc, parents).insert(last, item); +} + +pub fn set_bool(doc: &mut DocumentMut, path: &[&str], v: bool) { + set_item(doc, path, value(v)); +} +pub fn set_str(doc: &mut DocumentMut, path: &[&str], v: &str) { + set_item(doc, path, value(v)); +} +pub fn set_i64(doc: &mut DocumentMut, path: &[&str], v: i64) { + set_item(doc, path, value(v)); +} +pub fn set_f64(doc: &mut DocumentMut, path: &[&str], v: f64) { + set_item(doc, path, value(v)); +} +pub fn set_str_list(doc: &mut DocumentMut, path: &[&str], items: &[String]) { + let mut arr = Array::new(); + for s in items { + arr.push(s.as_str()); + } + set_item(doc, path, Item::Value(Value::Array(arr))); +} + +/// Set a string key, or remove it entirely when the value is empty — keeps +/// optional fields out of the file rather than persisting `key = ""`. +pub fn set_str_or_remove(doc: &mut DocumentMut, path: &[&str], v: &str) { + if v.is_empty() { + remove(doc, path); + } else { + set_str(doc, path, v); + } +} + +pub fn remove(doc: &mut DocumentMut, path: &[&str]) { + if let Some((last, parents)) = path.split_last() { + let mut tbl = doc.as_table_mut(); + for key in parents { + match tbl.get_mut(key).and_then(Item::as_table_mut) { + Some(t) => tbl = t, + None => return, + } + } + tbl.remove(last); + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn edits_preserve_unmodelled_keys_and_comments() { + let src = "\ +# a leading comment +[daemon] +log_level = \"info\" + +[calendar] +password = \"secret\" # keep me +"; + let mut doc: DocumentMut = src.parse().unwrap(); + // Modify a single modelled key. + set_str(&mut doc, &["daemon", "log_level"], "debug"); + // A key/section the UI never touches must survive untouched. + let out = doc.to_string(); + assert!(out.contains("log_level = \"debug\"")); + assert!(out.contains("password = \"secret\"")); + assert!(out.contains("# keep me")); + assert!(out.contains("# a leading comment")); + } + + #[test] + fn setters_create_missing_tables() { + let mut doc = DocumentMut::new(); + set_bool(&mut doc, &["adapters", "power", "enabled"], false); + set_i64(&mut doc, &["adapters", "power", "poll_interval_secs"], 45); + assert_eq!(get_bool(&doc, &["adapters", "power", "enabled"]), Some(false)); + assert_eq!( + get_i64(&doc, &["adapters", "power", "poll_interval_secs"]), + Some(45) + ); + } + + #[test] + fn empty_string_removes_key() { + let mut doc: DocumentMut = "[calendar]\nurl = \"x\"\n".parse().unwrap(); + set_str_or_remove(&mut doc, &["calendar", "url"], ""); + assert_eq!(get_str(&doc, &["calendar", "url"]), None); + } + + #[test] + fn str_list_roundtrips() { + let mut doc = DocumentMut::new(); + let items = vec!["a".to_string(), "b".to_string()]; + set_str_list(&mut doc, &["modules", "disable"], &items); + assert_eq!(get_str_list(&doc, &["modules", "disable"]), items); + } +} diff --git a/bos-settings/src/main.rs b/bos-settings/src/main.rs index fc13dc2..a5e73fd 100644 --- a/bos-settings/src/main.rs +++ b/bos-settings/src/main.rs @@ -2,6 +2,8 @@ mod config; mod theme; mod ui; +use gtk4::prelude::*; + fn main() { let app = gtk4::Application::builder() .application_id("com.breadway.bos-settings") diff --git a/bos-settings/src/state.rs b/bos-settings/src/state.rs deleted file mode 100644 index e0e760e..0000000 --- a/bos-settings/src/state.rs +++ /dev/null @@ -1,11 +0,0 @@ -pub struct AppState { - pub current_view: String, -} - -impl AppState { - pub fn new() -> Self { - Self { - current_view: "snapshots".to_string(), - } - } -} diff --git a/bos-settings/src/theme.rs b/bos-settings/src/theme.rs index ae850d0..4bd254e 100644 --- a/bos-settings/src/theme.rs +++ b/bos-settings/src/theme.rs @@ -1,88 +1,30 @@ -use gtk4::prelude::*; +//! Theming for bos-settings. +//! +//! bos-settings deliberately owns almost no styling: it loads the ecosystem's +//! shared stylesheet (the same one breadbar/breadbox/breadpad use, generated by +//! `bread-theme` from the pywal palette) and adds only the few layout rules +//! specific to this app's sidebar + content shell. This keeps it visually +//! identical to the rest of the bread desktop and live-recolouring for free. + use gtk4::CssProvider; +use std::cell::RefCell; -const CSS: &str = r#" -window { - background-color: #2e3440; - color: #eceff4; +// App-specific layout only — everything visual (colours, buttons, entries, +// switches, sidebar/row styling, cards, scrollbars) comes from the shared sheet. +const APP_CSS: &str = "\ +.view-content { padding: 24px; }\n\ +.view-content > label.title { margin-bottom: 16px; }\n\ +"; + +thread_local! { + static APP_PROVIDER: RefCell> = const { RefCell::new(None) }; } -.sidebar { - background-color: #3b4252; - border-right: 1px solid #434c5e; -} +pub fn load(_display: >k4::gdk::Display) { + // Shared ecosystem stylesheet (loads the generated file or a rendered + // fallback, and live-reloads when the palette changes). + bread_theme::gtk::apply_shared(); -.sidebar row { - padding: 8px 12px; - color: #d8dee9; -} - -.sidebar row:selected { - background-color: #5e81ac; - color: #eceff4; -} - -.sidebar .section-header { - padding: 12px 12px 4px 12px; - font-size: 0.75em; - font-weight: bold; - color: #616e88; - text-transform: uppercase; - letter-spacing: 1px; -} - -.view-content { - padding: 24px; -} - -.view-content label.title { - font-size: 1.4em; - font-weight: bold; - color: #eceff4; - margin-bottom: 16px; -} - -button { - background-color: #5e81ac; - color: #eceff4; - border: none; - border-radius: 4px; - padding: 6px 16px; -} - -button:hover { - background-color: #81a1c1; -} - -button.destructive-action { - background-color: #bf616a; -} - -button.destructive-action:hover { - background-color: #d08770; -} - -entry { - background-color: #434c5e; - color: #eceff4; - border: 1px solid #4c566a; - border-radius: 4px; -} - -textview { - background-color: #272c36; - color: #a3be8c; - font-family: monospace; - padding: 8px; -} -"#; - -pub fn load(display: >k4::gdk::Display) { - let provider = CssProvider::new(); - provider.load_from_string(CSS); - gtk4::style_context_add_provider_for_display( - display, - &provider, - gtk4::STYLE_PROVIDER_PRIORITY_APPLICATION, - ); + // bos-settings layout, layered on top at APPLICATION priority. + APP_PROVIDER.with(|cell| bread_theme::gtk::apply_css(APP_CSS, cell)); } diff --git a/bos-settings/src/ui/mod.rs b/bos-settings/src/ui/mod.rs index 1a2b383..3867fcd 100644 --- a/bos-settings/src/ui/mod.rs +++ b/bos-settings/src/ui/mod.rs @@ -1,3 +1,4 @@ pub mod sidebar; pub mod views; +pub mod widgets; pub mod window; diff --git a/bos-settings/src/ui/views/bread.rs b/bos-settings/src/ui/views/bread.rs index 27d0596..7560e73 100644 --- a/bos-settings/src/ui/views/bread.rs +++ b/bos-settings/src/ui/views/bread.rs @@ -1,155 +1,158 @@ -use gtk4::prelude::*; -use gtk4::{Box as GBox, Button, DropDown, Label, Orientation, StringList, Switch}; -use serde::{Deserialize, Serialize}; +//! breadd.toml — the bread daemon config. +//! Schema mirrors breadd/src/core/config.rs (daemon, lua, modules, adapters, +//! events, notifications). Edited non-destructively via the shared document. + use std::cell::RefCell; use std::rc::Rc; +use gtk4::prelude::*; +use gtk4::Box as GBox; + use crate::config; - -#[derive(Deserialize, Serialize, Clone)] -pub struct BreadConfig { - #[serde(default = "default_log_level")] - pub log_level: String, - #[serde(default)] - pub adapters: AdaptersConfig, -} - -fn default_log_level() -> String { "info".to_string() } - -#[derive(Deserialize, Serialize, Clone, Default)] -pub struct AdaptersConfig { - #[serde(default = "default_true")] pub keyboard: bool, - #[serde(default = "default_true")] pub mouse: bool, - #[serde(default = "default_true")] pub touchpad: bool, - #[serde(default = "default_true")] pub bluetooth: bool, - #[serde(default = "default_true")] pub gamepad: bool, -} - -fn default_true() -> bool { true } - -impl Default for BreadConfig { - fn default() -> Self { - Self { log_level: default_log_level(), adapters: AdaptersConfig::default() } - } -} +use crate::ui::widgets as w; fn config_path() -> std::path::PathBuf { config::config_dir().join("bread/breadd.toml") } -fn adapter_row( - label: &str, - active: bool, - cfg: Rc>, - field: &'static str, -) -> GBox { - let row = GBox::new(Orientation::Horizontal, 16); - let lbl = Label::new(Some(label)); - lbl.set_hexpand(true); - lbl.set_xalign(0.0); - let sw = Switch::new(); - sw.set_active(active); - sw.connect_active_notify(move |s| { - let val = s.is_active(); - let mut c = cfg.borrow_mut(); - match field { - "keyboard" => c.adapters.keyboard = val, - "mouse" => c.adapters.mouse = val, - "touchpad" => c.adapters.touchpad = val, - "bluetooth" => c.adapters.bluetooth = val, - "gamepad" => c.adapters.gamepad = val, - _ => {} - } - }); - row.append(&lbl); - row.append(&sw); - row -} - pub fn build() -> GBox { let path = config_path(); - let cfg: BreadConfig = config::load(&path).unwrap_or_default(); - let cfg = Rc::new(RefCell::new(cfg)); + let doc = Rc::new(RefCell::new(config::load_doc(&path))); - let vbox = GBox::new(Orientation::Vertical, 12); - vbox.add_css_class("view-content"); + let (outer, c) = w::view_scaffold("bread"); - let title = Label::new(Some("bread")); - title.add_css_class("title"); - title.set_xalign(0.0); - vbox.append(&title); + c.append(&w::section("Daemon")); + c.append(&w::dropdown_row( + "Log level", + &doc, + &["daemon", "log_level"], + &["error", "warn", "info", "debug", "trace"], + "info", + )); + c.append(&w::entry_row( + "Socket path", + &doc, + &["daemon", "socket_path"], + "default (XDG runtime dir)", + "", + )); - // Log level - let row = GBox::new(Orientation::Horizontal, 16); - row.set_margin_bottom(8); - let lbl = Label::new(Some("Log level")); - lbl.set_hexpand(true); - lbl.set_xalign(0.0); - let levels = StringList::new(&["error", "warn", "info", "debug", "trace"]); - let dropdown = DropDown::new(Some(levels), gtk4::Expression::NONE); - let pos = match cfg.borrow().log_level.as_str() { - "error" => 0u32, "warn" => 1, "info" => 2, "debug" => 3, "trace" => 4, _ => 2, - }; - dropdown.set_selected(pos); - { - let cfg = cfg.clone(); - dropdown.connect_selected_notify(move |dd| { - let levels = ["error", "warn", "info", "debug", "trace"]; - if let Some(&level) = levels.get(dd.selected() as usize) { - cfg.borrow_mut().log_level = level.to_string(); - } - }); - } - row.append(&lbl); - row.append(&dropdown); - vbox.append(&row); + c.append(&w::section("Lua")); + c.append(&w::entry_row( + "Entry point", + &doc, + &["lua", "entry_point"], + "~/.config/bread/init.lua", + "", + )); + c.append(&w::entry_row( + "Module path", + &doc, + &["lua", "module_path"], + "~/.config/bread/modules", + "", + )); - let adapter_label = Label::new(Some("Adapters")); - adapter_label.set_xalign(0.0); - adapter_label.set_margin_top(8); - adapter_label.set_margin_bottom(4); - vbox.append(&adapter_label); + c.append(&w::section("Modules")); + c.append(&w::switch_row( + "Load built-in modules", + &doc, + &["modules", "builtin"], + true, + )); + c.append(&w::csv_row( + "Disabled modules", + &doc, + &["modules", "disable"], + "module-a, module-b", + )); - let (kbd, mouse, touchpad, bluetooth, gamepad) = { - let c = cfg.borrow(); - (c.adapters.keyboard, c.adapters.mouse, c.adapters.touchpad, - c.adapters.bluetooth, c.adapters.gamepad) - }; - vbox.append(&adapter_row("Keyboard", kbd, cfg.clone(), "keyboard")); - vbox.append(&adapter_row("Mouse", mouse, cfg.clone(), "mouse")); - vbox.append(&adapter_row("Touchpad", touchpad, cfg.clone(), "touchpad")); - vbox.append(&adapter_row("Bluetooth", bluetooth, cfg.clone(), "bluetooth")); - vbox.append(&adapter_row("Gamepad", gamepad, cfg.clone(), "gamepad")); + c.append(&w::section("Adapters")); + c.append(&w::hint( + "Sources breadd normalises into events. Disable any you don't use.", + )); + c.append(&w::switch_row( + "Hyprland", + &doc, + &["adapters", "hyprland", "enabled"], + true, + )); + c.append(&w::switch_row( + "udev (devices)", + &doc, + &["adapters", "udev", "enabled"], + true, + )); + c.append(&w::csv_row( + "udev subsystems", + &doc, + &["adapters", "udev", "subsystems"], + "usb, input, power_supply", + )); + c.append(&w::switch_row( + "Power", + &doc, + &["adapters", "power", "enabled"], + true, + )); + c.append(&w::spin_row( + "Power poll interval (s)", + &doc, + &["adapters", "power", "poll_interval_secs"], + 1.0, + 3600.0, + 1.0, + 30, + )); + c.append(&w::switch_row( + "Network", + &doc, + &["adapters", "network", "enabled"], + true, + )); + c.append(&w::switch_row( + "Bluetooth", + &doc, + &["adapters", "bluetooth", "enabled"], + true, + )); - let btn_row = GBox::new(Orientation::Horizontal, 12); - btn_row.set_margin_top(16); + c.append(&w::section("Events")); + c.append(&w::spin_row( + "Dedup window (ms)", + &doc, + &["events", "dedup_window_ms"], + 0.0, + 10000.0, + 50.0, + 250, + )); - let save_btn = Button::with_label("Save"); - let status_lbl = Label::new(None); - status_lbl.add_css_class("dim-label"); + c.append(&w::section("Notifications")); + c.append(&w::spin_row( + "Default timeout (ms)", + &doc, + &["notifications", "default_timeout_ms"], + 0.0, + 60000.0, + 500.0, + 5000, + )); + c.append(&w::dropdown_row( + "Default urgency", + &doc, + &["notifications", "default_urgency"], + &["low", "normal", "critical"], + "normal", + )); + c.append(&w::entry_row( + "notify-send path", + &doc, + &["notifications", "notify_send_path"], + "auto-detected", + "", + )); - { - let cfg = cfg.clone(); - let path = path.clone(); - let status_lbl = status_lbl.clone(); - save_btn.connect_clicked(move |_| { - match config::save(&path, &*cfg.borrow()) { - Ok(()) => { - status_lbl.set_text("Saved"); - let lbl = status_lbl.clone(); - glib::timeout_add_seconds_local(3, move || { - lbl.set_text(""); - glib::ControlFlow::Break - }); - } - Err(e) => status_lbl.set_text(&format!("Error: {e}")), - } - }); - } - - btn_row.append(&save_btn); - btn_row.append(&status_lbl); - vbox.append(&btn_row); - - vbox + outer.append(&w::save_button(&doc, path)); + outer } diff --git a/bos-settings/src/ui/views/breadbar.rs b/bos-settings/src/ui/views/breadbar.rs index 9f1ceb7..dd49a52 100644 --- a/bos-settings/src/ui/views/breadbar.rs +++ b/bos-settings/src/ui/views/breadbar.rs @@ -3,10 +3,10 @@ use gtk4::{Box as GBox, Button, Label, Orientation, ScrolledWindow, TextView}; use std::path::PathBuf; fn css_path() -> PathBuf { - let home = std::env::var("HOME").unwrap_or_else(|_| "/home/user".to_string()); - PathBuf::from(home).join(".config/breadbar/style.css") + crate::config::config_dir().join("breadbar/style.css") } + pub fn build() -> GBox { let path = css_path(); let existing_css = std::fs::read_to_string(&path).unwrap_or_default(); diff --git a/bos-settings/src/ui/views/breadbox.rs b/bos-settings/src/ui/views/breadbox.rs index 36f88ac..e34a31b 100644 --- a/bos-settings/src/ui/views/breadbox.rs +++ b/bos-settings/src/ui/views/breadbox.rs @@ -1,33 +1,66 @@ -use gtk4::prelude::*; -use gtk4::{Box as GBox, Button, Entry, Label, ListBox, ListBoxRow, Orientation, ScrolledWindow}; -use serde::{Deserialize, Serialize}; +//! breadbox config.toml — launcher contexts. +//! Schema mirrors breadbox-shared (`[[contexts]]` with `name` + `priority`, an +//! ordered list of app/category hints). The contexts array is rewritten on +//! save; any other top-level keys/comments in the file are preserved. + use std::cell::RefCell; use std::rc::Rc; +use gtk4::prelude::*; +use gtk4::{ + Box as GBox, Button, Entry, Label, ListBox, ListBoxRow, Orientation, ScrolledWindow, +}; +use toml_edit::{value, Array, ArrayOfTables, DocumentMut, Item, Table}; + use crate::config; -#[derive(Deserialize, Serialize, Clone, Default)] -pub struct BreadboxConfig { - #[serde(default)] - pub context: Vec, -} - -#[derive(Deserialize, Serialize, Clone)] -pub struct Context { - pub name: String, - #[serde(default)] - pub apps: Vec, +#[derive(Clone, Default)] +struct Context { + name: String, + priority: Vec, } fn config_path() -> std::path::PathBuf { config::config_dir().join("breadbox/config.toml") } -fn rebuild_list(list: &ListBox, cfg: &Rc>) { +fn read_contexts(doc: &DocumentMut) -> Vec { + let Some(aot) = doc.get("contexts").and_then(Item::as_array_of_tables) else { + return Vec::new(); + }; + aot.iter() + .map(|t| Context { + name: t.get("name").and_then(Item::as_str).unwrap_or("").to_string(), + priority: t + .get("priority") + .and_then(Item::as_array) + .map(|a| a.iter().filter_map(|v| v.as_str().map(String::from)).collect()) + .unwrap_or_default(), + }) + .collect() +} + +/// Rewrite only the `contexts` array-of-tables, leaving the rest of the doc. +fn write_contexts(doc: &mut DocumentMut, ctxs: &[Context]) { + let mut aot = ArrayOfTables::new(); + for ctx in ctxs { + let mut t = Table::new(); + t.insert("name", value(&ctx.name)); + let mut arr = Array::new(); + for p in &ctx.priority { + arr.push(p.as_str()); + } + t.insert("priority", value(arr)); + aot.push(t); + } + doc.as_table_mut().insert("contexts", Item::ArrayOfTables(aot)); +} + +fn rebuild_list(list: &ListBox, model: &Rc>>) { while let Some(child) = list.first_child() { list.remove(&child); } - for (i, ctx) in cfg.borrow().context.iter().enumerate() { + for (i, ctx) in model.borrow().iter().enumerate() { let row = ListBoxRow::new(); row.set_selectable(false); @@ -42,27 +75,28 @@ fn rebuild_list(list: &ListBox, cfg: &Rc>) { name_entry.set_width_chars(14); name_entry.set_placeholder_text(Some("name")); - let apps_entry = Entry::new(); - apps_entry.set_text(&ctx.apps.join(", ")); - apps_entry.set_hexpand(true); - apps_entry.set_placeholder_text(Some("app1, app2, ...")); + let prio_entry = Entry::new(); + prio_entry.set_text(&ctx.priority.join(", ")); + prio_entry.set_hexpand(true); + prio_entry.set_placeholder_text(Some("firefox, code, Development, ...")); let remove_btn = Button::with_label("Remove"); remove_btn.add_css_class("destructive-action"); { - let cfg = cfg.clone(); + let model = model.clone(); name_entry.connect_changed(move |e| { - if let Some(c) = cfg.borrow_mut().context.get_mut(i) { + if let Some(c) = model.borrow_mut().get_mut(i) { c.name = e.text().to_string(); } }); } { - let cfg = cfg.clone(); - apps_entry.connect_changed(move |e| { - if let Some(c) = cfg.borrow_mut().context.get_mut(i) { - c.apps = e.text() + let model = model.clone(); + prio_entry.connect_changed(move |e| { + if let Some(c) = model.borrow_mut().get_mut(i) { + c.priority = e + .text() .split(',') .map(|s| s.trim().to_string()) .filter(|s| !s.is_empty()) @@ -71,16 +105,16 @@ fn rebuild_list(list: &ListBox, cfg: &Rc>) { }); } { - let cfg = cfg.clone(); + let model = model.clone(); let list = list.clone(); remove_btn.connect_clicked(move |_| { - cfg.borrow_mut().context.remove(i); - rebuild_list(&list, &cfg); + model.borrow_mut().remove(i); + rebuild_list(&list, &model); }); } hbox.append(&name_entry); - hbox.append(&apps_entry); + hbox.append(&prio_entry); hbox.append(&remove_btn); row.set_child(Some(&hbox)); list.append(&row); @@ -89,8 +123,8 @@ fn rebuild_list(list: &ListBox, cfg: &Rc>) { pub fn build() -> GBox { let path = config_path(); - let cfg: BreadboxConfig = config::load(&path).unwrap_or_default(); - let cfg = Rc::new(RefCell::new(cfg)); + let doc = Rc::new(RefCell::new(config::load_doc(&path))); + let model = Rc::new(RefCell::new(read_contexts(&doc.borrow()))); let vbox = GBox::new(Orientation::Vertical, 12); vbox.add_css_class("view-content"); @@ -100,14 +134,17 @@ pub fn build() -> GBox { title.set_xalign(0.0); vbox.append(&title); - let subtitle = Label::new(Some("Context priority lists — apps shown in each context.")); + let subtitle = Label::new(Some( + "Launcher contexts — each lists, in priority order, the apps/categories surfaced first.", + )); subtitle.set_xalign(0.0); + subtitle.set_wrap(true); subtitle.set_margin_bottom(8); vbox.append(&subtitle); let list = ListBox::new(); list.set_selection_mode(gtk4::SelectionMode::None); - rebuild_list(&list, &cfg); + rebuild_list(&list, &model); let scroll = ScrolledWindow::new(); scroll.set_vexpand(true); @@ -119,27 +156,30 @@ pub fn build() -> GBox { let add_btn = Button::with_label("Add context"); { - let cfg = cfg.clone(); + let model = model.clone(); let list = list.clone(); add_btn.connect_clicked(move |_| { - cfg.borrow_mut().context.push(Context { + model.borrow_mut().push(Context { name: "new".to_string(), - apps: Vec::new(), + priority: Vec::new(), }); - rebuild_list(&list, &cfg); + rebuild_list(&list, &model); }); } let save_btn = Button::with_label("Save"); + save_btn.add_css_class("suggested-action"); let status_lbl = Label::new(None); status_lbl.add_css_class("dim-label"); { - let cfg = cfg.clone(); + let doc = doc.clone(); + let model = model.clone(); let path = path.clone(); let status_lbl = status_lbl.clone(); save_btn.connect_clicked(move |_| { - match config::save(&path, &*cfg.borrow()) { + write_contexts(&mut doc.borrow_mut(), &model.borrow()); + match config::save_doc(&path, &doc.borrow()) { Ok(()) => { status_lbl.set_text("Saved"); let lbl = status_lbl.clone(); diff --git a/bos-settings/src/ui/views/breadcrumbs.rs b/bos-settings/src/ui/views/breadcrumbs.rs index f165f43..f479a13 100644 --- a/bos-settings/src/ui/views/breadcrumbs.rs +++ b/bos-settings/src/ui/views/breadcrumbs.rs @@ -1,162 +1,477 @@ -use gtk4::prelude::*; -use gtk4::{Box as GBox, Button, Entry, Label, ListBox, ListBoxRow, Orientation, ScrolledWindow}; -use serde::{Deserialize, Serialize}; +//! breadcrumbs.toml — Wi-Fi profile state machine. +//! Schema mirrors breadcrumbs/src/config.rs: +//! [settings] scalar tunables +//! [[networks]] saved networks (ssid / password / hidden) +//! [profiles.] per-location profile (networks, tailscale, …) +//! `[settings]` is edited in place; the `networks` array and `profiles` table +//! are rewritten from their editors on save. Other keys/comments are preserved. + use std::cell::RefCell; use std::rc::Rc; +use gtk4::prelude::*; +use gtk4::{ + Box as GBox, Button, Entry, Label, ListBox, ListBoxRow, Orientation, ScrolledWindow, Switch, +}; +use toml_edit::{value, Array, ArrayOfTables, DocumentMut, Item, Table}; + use crate::config; - -#[derive(Deserialize, Serialize, Clone, Default)] -pub struct BreadcrumbsConfig { - #[serde(default)] - pub profile: Vec, -} - -#[derive(Deserialize, Serialize, Clone)] -pub struct Profile { - pub name: String, - #[serde(default)] - pub ssids: Vec, -} +use crate::ui::widgets as w; fn config_path() -> std::path::PathBuf { config::config_dir().join("breadcrumbs/breadcrumbs.toml") } -fn rebuild_list(list: &ListBox, cfg: &Rc>) { +// --- networks --------------------------------------------------------------- + +#[derive(Clone, Default)] +struct Network { + ssid: String, + password: String, + hidden: bool, +} + +fn read_networks(doc: &DocumentMut) -> Vec { + let Some(aot) = doc.get("networks").and_then(Item::as_array_of_tables) else { + return Vec::new(); + }; + aot.iter() + .map(|t| Network { + ssid: t.get("ssid").and_then(Item::as_str).unwrap_or("").to_string(), + password: t + .get("password") + .and_then(Item::as_str) + .unwrap_or("") + .to_string(), + hidden: t.get("hidden").and_then(Item::as_bool).unwrap_or(false), + }) + .collect() +} + +fn write_networks(doc: &mut DocumentMut, nets: &[Network]) { + let mut aot = ArrayOfTables::new(); + for n in nets { + let mut t = Table::new(); + t.insert("ssid", value(&n.ssid)); + t.insert("password", value(&n.password)); + t.insert("hidden", value(n.hidden)); + aot.push(t); + } + doc.as_table_mut().insert("networks", Item::ArrayOfTables(aot)); +} + +fn rebuild_networks(list: &ListBox, model: &Rc>>) { while let Some(child) = list.first_child() { list.remove(&child); } - for (i, profile) in cfg.borrow().profile.iter().enumerate() { + for (i, n) in model.borrow().iter().enumerate() { let row = ListBoxRow::new(); row.set_selectable(false); - let hbox = GBox::new(Orientation::Horizontal, 8); hbox.set_margin_top(6); hbox.set_margin_bottom(6); hbox.set_margin_start(8); hbox.set_margin_end(8); - let name_entry = Entry::new(); - name_entry.set_text(&profile.name); - name_entry.set_width_chars(14); - name_entry.set_placeholder_text(Some("name")); + let ssid = Entry::new(); + ssid.set_text(&n.ssid); + ssid.set_width_chars(16); + ssid.set_placeholder_text(Some("SSID")); - let ssids_entry = Entry::new(); - ssids_entry.set_text(&profile.ssids.join(", ")); - ssids_entry.set_hexpand(true); - ssids_entry.set_placeholder_text(Some("SSID1, SSID2, ...")); + let pass = Entry::new(); + pass.set_text(&n.password); + pass.set_hexpand(true); + pass.set_visibility(false); + pass.set_input_purpose(gtk4::InputPurpose::Password); + pass.set_placeholder_text(Some("password")); - let remove_btn = Button::with_label("Remove"); - remove_btn.add_css_class("destructive-action"); + let hidden = Switch::new(); + hidden.set_active(n.hidden); + hidden.set_valign(gtk4::Align::Center); + hidden.set_tooltip_text(Some("Hidden network")); + + let remove = Button::with_label("Remove"); + remove.add_css_class("destructive-action"); { - let cfg = cfg.clone(); - name_entry.connect_changed(move |e| { - if let Some(p) = cfg.borrow_mut().profile.get_mut(i) { - p.name = e.text().to_string(); + let model = model.clone(); + ssid.connect_changed(move |e| { + if let Some(n) = model.borrow_mut().get_mut(i) { + n.ssid = e.text().to_string(); } }); } { - let cfg = cfg.clone(); - ssids_entry.connect_changed(move |e| { - if let Some(p) = cfg.borrow_mut().profile.get_mut(i) { - p.ssids = e.text() - .split(',') - .map(|s| s.trim().to_string()) - .filter(|s| !s.is_empty()) - .collect(); + let model = model.clone(); + pass.connect_changed(move |e| { + if let Some(n) = model.borrow_mut().get_mut(i) { + n.password = e.text().to_string(); } }); } { - let cfg = cfg.clone(); + let model = model.clone(); + hidden.connect_active_notify(move |s| { + if let Some(n) = model.borrow_mut().get_mut(i) { + n.hidden = s.is_active(); + } + }); + } + { + let model = model.clone(); let list = list.clone(); - remove_btn.connect_clicked(move |_| { - cfg.borrow_mut().profile.remove(i); - rebuild_list(&list, &cfg); + remove.connect_clicked(move |_| { + model.borrow_mut().remove(i); + rebuild_networks(&list, &model); }); } - hbox.append(&name_entry); - hbox.append(&ssids_entry); - hbox.append(&remove_btn); + hbox.append(&ssid); + hbox.append(&pass); + hbox.append(&Label::new(Some("hidden"))); + hbox.append(&hidden); + hbox.append(&remove); row.set_child(Some(&hbox)); list.append(&row); } } +// --- profiles --------------------------------------------------------------- + +#[derive(Clone, Default)] +struct Profile { + name: String, + networks: Vec, + detect_ssids: Vec, + bootstrap: String, + exit_node: String, + tailscale: bool, + include_all_known: bool, +} + +fn read_profiles(doc: &DocumentMut) -> Vec { + let Some(tbl) = doc.get("profiles").and_then(Item::as_table) else { + return Vec::new(); + }; + let str_list = |item: Option<&Item>| -> Vec { + item.and_then(Item::as_array) + .map(|a| a.iter().filter_map(|v| v.as_str().map(String::from)).collect()) + .unwrap_or_default() + }; + tbl.iter() + .filter_map(|(name, item)| { + let p = item.as_table()?; + Some(Profile { + name: name.to_string(), + networks: str_list(p.get("networks")), + detect_ssids: str_list(p.get("detect_ssids")), + bootstrap: p.get("bootstrap").and_then(Item::as_str).unwrap_or("").to_string(), + exit_node: p.get("exit_node").and_then(Item::as_str).unwrap_or("").to_string(), + tailscale: p.get("tailscale").and_then(Item::as_bool).unwrap_or(false), + include_all_known: p + .get("include_all_known") + .and_then(Item::as_bool) + .unwrap_or(false), + }) + }) + .collect() +} + +fn write_profiles(doc: &mut DocumentMut, profiles: &[Profile]) { + let mut tbl = Table::new(); + let to_arr = |items: &[String]| { + let mut a = Array::new(); + for s in items { + a.push(s.as_str()); + } + a + }; + for p in profiles { + if p.name.is_empty() { + continue; + } + let mut t = Table::new(); + t.insert("networks", value(to_arr(&p.networks))); + t.insert("tailscale", value(p.tailscale)); + t.insert("include_all_known", value(p.include_all_known)); + if !p.detect_ssids.is_empty() { + t.insert("detect_ssids", value(to_arr(&p.detect_ssids))); + } + if !p.bootstrap.is_empty() { + t.insert("bootstrap", value(&p.bootstrap)); + } + if !p.exit_node.is_empty() { + t.insert("exit_node", value(&p.exit_node)); + } + tbl.insert(&p.name, Item::Table(t)); + } + doc.as_table_mut().insert("profiles", Item::Table(tbl)); +} + +fn field(label: &str, control: &impl IsA) -> GBox { + let row = GBox::new(Orientation::Horizontal, 12); + let lbl = Label::new(Some(label)); + lbl.set_xalign(0.0); + lbl.set_width_chars(16); + row.append(&lbl); + control.set_hexpand(true); + row.append(control); + row +} + +fn rebuild_profiles(container: &GBox, model: &Rc>>) { + while let Some(child) = container.first_child() { + container.remove(&child); + } + for (i, p) in model.borrow().iter().enumerate() { + let card = GBox::new(Orientation::Vertical, 6); + card.add_css_class("card"); + card.set_margin_top(6); + card.set_margin_bottom(6); + + let header = GBox::new(Orientation::Horizontal, 8); + let name = Entry::new(); + name.set_text(&p.name); + name.set_hexpand(true); + name.set_placeholder_text(Some("profile name (e.g. home)")); + let remove = Button::with_label("Remove"); + remove.add_css_class("destructive-action"); + header.append(&name); + header.append(&remove); + card.append(&header); + + let networks = Entry::new(); + networks.set_text(&p.networks.join(", ")); + networks.set_placeholder_text(Some("SSID1, SSID2")); + card.append(&field("Networks", &networks)); + + let detect = Entry::new(); + detect.set_text(&p.detect_ssids.join(", ")); + detect.set_placeholder_text(Some("SSIDs that auto-select this profile")); + card.append(&field("Detect SSIDs", &detect)); + + let exit_node = Entry::new(); + exit_node.set_text(&p.exit_node); + exit_node.set_placeholder_text(Some("tailscale exit node (optional)")); + card.append(&field("Exit node", &exit_node)); + + let bootstrap = Entry::new(); + bootstrap.set_text(&p.bootstrap); + bootstrap.set_placeholder_text(Some("bootstrap command (optional)")); + card.append(&field("Bootstrap", &bootstrap)); + + let tailscale = Switch::new(); + tailscale.set_active(p.tailscale); + tailscale.set_halign(gtk4::Align::Start); + card.append(&field("Tailscale", &tailscale)); + + let include_all = Switch::new(); + include_all.set_active(p.include_all_known); + include_all.set_halign(gtk4::Align::Start); + card.append(&field("Include all known", &include_all)); + + // bind each control to the in-memory model entry + macro_rules! bind_csv { + ($entry:ident, $f:ident) => {{ + let model = model.clone(); + $entry.connect_changed(move |e| { + if let Some(p) = model.borrow_mut().get_mut(i) { + p.$f = e + .text() + .split(',') + .map(|s| s.trim().to_string()) + .filter(|s| !s.is_empty()) + .collect(); + } + }); + }}; + } + macro_rules! bind_str { + ($entry:ident, $f:ident) => {{ + let model = model.clone(); + $entry.connect_changed(move |e| { + if let Some(p) = model.borrow_mut().get_mut(i) { + p.$f = e.text().to_string(); + } + }); + }}; + } + macro_rules! bind_bool { + ($sw:ident, $f:ident) => {{ + let model = model.clone(); + $sw.connect_active_notify(move |s| { + if let Some(p) = model.borrow_mut().get_mut(i) { + p.$f = s.is_active(); + } + }); + }}; + } + bind_str!(name, name); + bind_csv!(networks, networks); + bind_csv!(detect, detect_ssids); + bind_str!(exit_node, exit_node); + bind_str!(bootstrap, bootstrap); + bind_bool!(tailscale, tailscale); + bind_bool!(include_all, include_all_known); + { + let model = model.clone(); + let container = container.clone(); + remove.connect_clicked(move |_| { + model.borrow_mut().remove(i); + rebuild_profiles(&container, &model); + }); + } + + container.append(&card); + } +} + +// --- view ------------------------------------------------------------------- + pub fn build() -> GBox { let path = config_path(); - let cfg: BreadcrumbsConfig = config::load(&path).unwrap_or_default(); - let cfg = Rc::new(RefCell::new(cfg)); + let doc = Rc::new(RefCell::new(config::load_doc(&path))); + let nets = Rc::new(RefCell::new(read_networks(&doc.borrow()))); + let profiles = Rc::new(RefCell::new(read_profiles(&doc.borrow()))); - let vbox = GBox::new(Orientation::Vertical, 12); - vbox.add_css_class("view-content"); + let outer = GBox::new(Orientation::Vertical, 8); + outer.add_css_class("view-content"); let title = Label::new(Some("breadcrumbs")); title.add_css_class("title"); title.set_xalign(0.0); - vbox.append(&title); - - let subtitle = Label::new(Some("Network profiles — SSIDs associated with each location.")); - subtitle.set_xalign(0.0); - subtitle.set_margin_bottom(8); - vbox.append(&subtitle); - - let list = ListBox::new(); - list.set_selection_mode(gtk4::SelectionMode::None); - rebuild_list(&list, &cfg); + outer.append(&title); + let content = GBox::new(Orientation::Vertical, 8); let scroll = ScrolledWindow::new(); scroll.set_vexpand(true); - scroll.set_child(Some(&list)); - vbox.append(&scroll); + scroll.set_hscrollbar_policy(gtk4::PolicyType::Never); + scroll.set_child(Some(&content)); + outer.append(&scroll); - let btn_row = GBox::new(Orientation::Horizontal, 8); - btn_row.set_margin_top(8); + // [settings] — edited in place on the shared doc + content.append(&w::section("Settings")); + content.append(&w::dropdown_row( + "Default profile", + &doc, + &["settings", "default_profile"], + &["home", "away"], + "home", + )); + content.append(&w::entry_row("DNS", &doc, &["settings", "dns"], "1.1.1.1", "")); + content.append(&w::entry_row( + "Exit node", + &doc, + &["settings", "exit_node"], + "tailscale exit node", + "", + )); + content.append(&w::entry_row( + "Ping host", + &doc, + &["settings", "ping_host"], + "1.1.1.1", + "", + )); + content.append(&w::entry_row( + "Connectivity URL", + &doc, + &["settings", "connectivity_url"], + "http://connectivitycheck.gstatic.com/generate_204", + "", + )); + content.append(&w::spin_row( + "nmcli wait (s)", + &doc, + &["settings", "nmcli_wait"], + 1.0, + 120.0, + 1.0, + 8, + )); + content.append(&w::spin_row( + "Watch interval (s)", + &doc, + &["settings", "watch_interval"], + 1.0, + 600.0, + 1.0, + 12, + )); - let add_btn = Button::with_label("Add profile"); + // [[networks]] + content.append(&w::section("Saved networks")); + let net_list = ListBox::new(); + net_list.set_selection_mode(gtk4::SelectionMode::None); + rebuild_networks(&net_list, &nets); + content.append(&net_list); + let add_net = Button::with_label("Add network"); + add_net.set_halign(gtk4::Align::Start); { - let cfg = cfg.clone(); - let list = list.clone(); - add_btn.connect_clicked(move |_| { - cfg.borrow_mut().profile.push(Profile { - name: "new".to_string(), - ssids: Vec::new(), - }); - rebuild_list(&list, &cfg); + let nets = nets.clone(); + let net_list = net_list.clone(); + add_net.connect_clicked(move |_| { + nets.borrow_mut().push(Network::default()); + rebuild_networks(&net_list, &nets); }); } + content.append(&add_net); - let save_btn = Button::with_label("Save"); - let status_lbl = Label::new(None); - status_lbl.add_css_class("dim-label"); - + // [profiles.*] + content.append(&w::section("Profiles")); + let prof_box = GBox::new(Orientation::Vertical, 4); + rebuild_profiles(&prof_box, &profiles); + content.append(&prof_box); + let add_prof = Button::with_label("Add profile"); + add_prof.set_halign(gtk4::Align::Start); { - let cfg = cfg.clone(); + let profiles = profiles.clone(); + let prof_box = prof_box.clone(); + add_prof.connect_clicked(move |_| { + profiles.borrow_mut().push(Profile { + name: "new".to_string(), + ..Default::default() + }); + rebuild_profiles(&prof_box, &profiles); + }); + } + content.append(&add_prof); + + // Save — fold the network + profile editors back into the doc, then write. + let btn_row = GBox::new(Orientation::Horizontal, 12); + btn_row.set_margin_top(16); + let save_btn = Button::with_label("Save"); + save_btn.add_css_class("suggested-action"); + let status = Label::new(None); + status.add_css_class("dim-label"); + { + let doc = doc.clone(); + let nets = nets.clone(); + let profiles = profiles.clone(); let path = path.clone(); - let status_lbl = status_lbl.clone(); + let status = status.clone(); save_btn.connect_clicked(move |_| { - match config::save(&path, &*cfg.borrow()) { + { + let mut d = doc.borrow_mut(); + write_networks(&mut d, &nets.borrow()); + write_profiles(&mut d, &profiles.borrow()); + } + match config::save_doc(&path, &doc.borrow()) { Ok(()) => { - status_lbl.set_text("Saved"); - let lbl = status_lbl.clone(); + status.set_text("Saved"); + let lbl = status.clone(); glib::timeout_add_seconds_local(3, move || { lbl.set_text(""); glib::ControlFlow::Break }); } - Err(e) => status_lbl.set_text(&format!("Error: {e}")), + Err(e) => status.set_text(&format!("Error: {e}")), } }); } - - btn_row.append(&add_btn); btn_row.append(&save_btn); - btn_row.append(&status_lbl); - vbox.append(&btn_row); + btn_row.append(&status); + outer.append(&btn_row); - vbox + outer } diff --git a/bos-settings/src/ui/views/breadpad.rs b/bos-settings/src/ui/views/breadpad.rs index 6e24346..6fe5268 100644 --- a/bos-settings/src/ui/views/breadpad.rs +++ b/bos-settings/src/ui/views/breadpad.rs @@ -1,28 +1,16 @@ -use gtk4::prelude::*; -use gtk4::{Box as GBox, Button, Entry, Label, Orientation, Switch}; -use serde::{Deserialize, Serialize}; +//! breadpad.toml — the breadpad notes/reminders config. +//! Schema mirrors breadpad-shared/src/config.rs (settings, model + model.ollama, +//! reminders, calendar). Edited non-destructively (the calendar password and +//! model paths are preserved across saves). + use std::cell::RefCell; use std::rc::Rc; +use gtk4::prelude::*; +use gtk4::Box as GBox; + use crate::config; - -#[derive(Deserialize, Serialize, Clone)] -pub struct BreadpadConfig { - #[serde(default)] - pub model: String, - #[serde(default = "default_true")] - pub reminders: bool, - #[serde(default = "default_true")] - pub calendar: bool, -} - -fn default_true() -> bool { true } - -impl Default for BreadpadConfig { - fn default() -> Self { - Self { model: String::new(), reminders: true, calendar: true } - } -} +use crate::ui::widgets as w; fn config_path() -> std::path::PathBuf { config::config_dir().join("breadpad/breadpad.toml") @@ -30,93 +18,129 @@ fn config_path() -> std::path::PathBuf { pub fn build() -> GBox { let path = config_path(); - let cfg: BreadpadConfig = config::load(&path).unwrap_or_default(); - let cfg = Rc::new(RefCell::new(cfg)); + let doc = Rc::new(RefCell::new(config::load_doc(&path))); - let vbox = GBox::new(Orientation::Vertical, 12); - vbox.add_css_class("view-content"); + let (outer, c) = w::view_scaffold("breadpad"); - let title = Label::new(Some("breadpad")); - title.add_css_class("title"); - title.set_xalign(0.0); - vbox.append(&title); + c.append(&w::section("Capture")); + c.append(&w::dropdown_row( + "Default note type", + &doc, + &["settings", "default_type"], + &["note", "reminder", "task"], + "note", + )); + c.append(&w::switch_row( + "Tag with active workspace", + &doc, + &["settings", "workspace_tag"], + true, + )); + c.append(&w::csv_row( + "Snooze options", + &doc, + &["settings", "snooze_options"], + "15m, 1h, tomorrow_morning", + )); + c.append(&w::spin_row( + "Archive after (days)", + &doc, + &["settings", "archive_after_days"], + 0.0, + 3650.0, + 1.0, + 30, + )); - // Model entry - let row = GBox::new(Orientation::Horizontal, 16); - let lbl = Label::new(Some("Model")); - lbl.set_hexpand(true); - lbl.set_xalign(0.0); - let model_entry = Entry::new(); - model_entry.set_text(&cfg.borrow().model); - model_entry.set_placeholder_text(Some("e.g. claude-sonnet-4-6")); - { - let cfg = cfg.clone(); - model_entry.connect_changed(move |e| { - cfg.borrow_mut().model = e.text().to_string(); - }); - } - row.append(&lbl); - row.append(&model_entry); - vbox.append(&row); + c.append(&w::section("Classifier model")); + c.append(&w::entry_row( + "ONNX model path", + &doc, + &["model", "path"], + "~/.local/share/breadpad/model/classifier.onnx", + "", + )); + c.append(&w::entry_row( + "Tokenizer path", + &doc, + &["model", "tokenizer"], + "~/.local/share/breadpad/model/tokenizer.json", + "", + )); - // Reminders - let row = GBox::new(Orientation::Horizontal, 16); - let lbl = Label::new(Some("Reminders")); - lbl.set_hexpand(true); - lbl.set_xalign(0.0); - let sw = Switch::new(); - sw.set_active(cfg.borrow().reminders); - { - let cfg = cfg.clone(); - sw.connect_active_notify(move |s| { cfg.borrow_mut().reminders = s.is_active(); }); - } - row.append(&lbl); - row.append(&sw); - vbox.append(&row); + c.append(&w::section("Ollama (LLM classifier)")); + c.append(&w::switch_row( + "Use Ollama", + &doc, + &["model", "ollama", "enabled"], + true, + )); + c.append(&w::entry_row( + "Endpoint", + &doc, + &["model", "ollama", "endpoint"], + "http://localhost:11434", + "", + )); + c.append(&w::entry_row( + "Model", + &doc, + &["model", "ollama", "model"], + "e.g. fastflowlm", + "", + )); + c.append(&w::spin_f64_row( + "Confidence threshold", + &doc, + &["model", "ollama", "confidence_threshold"], + 0.0, + 1.0, + 0.05, + 2, + 0.6, + )); - // Calendar - let row = GBox::new(Orientation::Horizontal, 16); - let lbl = Label::new(Some("Calendar integration")); - lbl.set_hexpand(true); - lbl.set_xalign(0.0); - let sw = Switch::new(); - sw.set_active(cfg.borrow().calendar); - { - let cfg = cfg.clone(); - sw.connect_active_notify(move |s| { cfg.borrow_mut().calendar = s.is_active(); }); - } - row.append(&lbl); - row.append(&sw); - vbox.append(&row); + c.append(&w::section("Reminders")); + c.append(&w::entry_row( + "Default morning time", + &doc, + &["reminders", "default_morning"], + "7:00", + "", + )); + c.append(&w::spin_row( + "Missed grace (minutes)", + &doc, + &["reminders", "missed_grace_minutes"], + 0.0, + 1440.0, + 5.0, + 60, + )); - let btn_row = GBox::new(Orientation::Horizontal, 12); - btn_row.set_margin_top(16); + c.append(&w::section("Calendar (CalDAV)")); + c.append(&w::switch_row( + "Sync to calendar", + &doc, + &["calendar", "enabled"], + false, + )); + c.append(&w::entry_row( + "CalDAV URL", + &doc, + &["calendar", "url"], + "https://host/remote.php/dav/calendars/...", + "", + )); + c.append(&w::entry_row( + "Username", + &doc, + &["calendar", "username"], + "", + "", + )); + c.append(&w::password_row("Password", &doc, &["calendar", "password"])); - let save_btn = Button::with_label("Save"); - let status_lbl = Label::new(None); - status_lbl.add_css_class("dim-label"); - - { - let cfg = cfg.clone(); - let status_lbl = status_lbl.clone(); - save_btn.connect_clicked(move |_| { - match config::save(&path, &*cfg.borrow()) { - Ok(()) => { - status_lbl.set_text("Saved"); - let lbl = status_lbl.clone(); - glib::timeout_add_seconds_local(3, move || { - lbl.set_text(""); - glib::ControlFlow::Break - }); - } - Err(e) => status_lbl.set_text(&format!("Error: {e}")), - } - }); - } - - btn_row.append(&save_btn); - btn_row.append(&status_lbl); - vbox.append(&btn_row); - - vbox + outer.append(&w::save_button(&doc, path)); + outer } diff --git a/bos-settings/src/ui/views/hyprland.rs b/bos-settings/src/ui/views/hyprland.rs index 0ed704d..fba4537 100644 --- a/bos-settings/src/ui/views/hyprland.rs +++ b/bos-settings/src/ui/views/hyprland.rs @@ -23,8 +23,7 @@ fn get_monitors() -> Vec { } fn hypr_path(name: &str) -> std::path::PathBuf { - let home = std::env::var("HOME").unwrap_or_else(|_| "/home/user".to_string()); - std::path::PathBuf::from(home).join(".config/hypr").join(name) + crate::config::config_dir().join("hypr").join(name) } pub fn build() -> GBox { @@ -51,7 +50,7 @@ pub fn build() -> GBox { for mon in &monitors { let lbl = Label::new(Some(mon)); lbl.set_xalign(0.0); - lbl.set_monospace(true); + lbl.add_css_class("monospace"); vbox.append(&lbl); } } diff --git a/bos-settings/src/ui/views/packages.rs b/bos-settings/src/ui/views/packages.rs index 6b3aedc..1281c44 100644 --- a/bos-settings/src/ui/views/packages.rs +++ b/bos-settings/src/ui/views/packages.rs @@ -8,7 +8,7 @@ use std::io::{BufRead, BufReader}; use std::process::{Command, Stdio}; fn read_installed() -> HashMap { - let home = std::env::var("HOME").unwrap_or_else(|_| "/home/user".to_string()); + let home = std::env::var("HOME").unwrap_or_else(|_| "/root".to_string()); let path = std::path::Path::new(&home) .join(".local/state/bakery/installed.json"); @@ -132,7 +132,7 @@ pub fn build() -> GBox { Ok(mut child) => { std::thread::spawn(move || { let _ = child.wait(); }); } - Err(e) => eprintln!("bakery update failed: {e}"), + Err(_) => {} // bakery not found; button is a no-op } }); diff --git a/bos-settings/src/ui/widgets.rs b/bos-settings/src/ui/widgets.rs new file mode 100644 index 0000000..ca207da --- /dev/null +++ b/bos-settings/src/ui/widgets.rs @@ -0,0 +1,235 @@ +//! Reusable settings rows bound to a shared `toml_edit` document. +//! +//! Every row reads its current value from the document on build and writes the +//! single key it owns back into the document on change. A view collects rows, +//! then a [`save_button`] persists the whole document to disk in one shot — so +//! unmodelled keys and comments are always preserved (see `crate::config`). + +use std::cell::RefCell; +use std::path::PathBuf; +use std::rc::Rc; + +use gtk4::prelude::*; +use gtk4::{ + Adjustment, Box as GBox, Button, DropDown, Entry, Expression, Label, Orientation, + SpinButton, StringList, Switch, +}; +use toml_edit::DocumentMut; + +use crate::config; + +/// Shared, mutable config document handed to every row in a view. +pub type Doc = Rc>; + +/// A fixed key path into the document, e.g. `&["adapters", "power", "enabled"]`. +type Path = &'static [&'static str]; + +fn field_label(text: &str) -> Label { + let lbl = Label::new(Some(text)); + lbl.set_hexpand(true); + lbl.set_xalign(0.0); + lbl +} + +fn row(label: &str, control: &impl IsA) -> GBox { + let row = GBox::new(Orientation::Horizontal, 16); + row.append(&field_label(label)); + control.set_halign(gtk4::Align::End); + control.set_valign(gtk4::Align::Center); + row.append(control); + row +} + +/// A bold section heading with spacing above it. +pub fn section(text: &str) -> Label { + let lbl = Label::new(Some(text)); + lbl.add_css_class("heading"); + lbl.set_xalign(0.0); + lbl.set_margin_top(12); + lbl.set_margin_bottom(2); + lbl +} + +/// Small dimmed helper text under a section or row. +pub fn hint(text: &str) -> Label { + let lbl = Label::new(Some(text)); + lbl.add_css_class("dim-label"); + lbl.set_xalign(0.0); + lbl.set_wrap(true); + lbl.set_margin_bottom(4); + lbl +} + +/// Standard view scaffold: an outer vertical box with a title and a scrollable +/// content area. Append setting rows to the returned `content`, then append a +/// [`save_button`] to `outer`. Returns `(outer, content)`. +pub fn view_scaffold(title: &str) -> (GBox, GBox) { + let outer = GBox::new(Orientation::Vertical, 8); + outer.add_css_class("view-content"); + + let title_lbl = Label::new(Some(title)); + title_lbl.add_css_class("title"); + title_lbl.set_xalign(0.0); + outer.append(&title_lbl); + + let content = GBox::new(Orientation::Vertical, 8); + let scroll = gtk4::ScrolledWindow::new(); + scroll.set_vexpand(true); + scroll.set_hscrollbar_policy(gtk4::PolicyType::Never); + scroll.set_child(Some(&content)); + outer.append(&scroll); + + (outer, content) +} + +pub fn switch_row(label: &str, doc: &Doc, path: Path, default: bool) -> GBox { + let cur = config::get_bool(&doc.borrow(), path).unwrap_or(default); + let sw = Switch::new(); + sw.set_active(cur); + let doc = doc.clone(); + sw.connect_active_notify(move |s| { + config::set_bool(&mut doc.borrow_mut(), path, s.is_active()); + }); + row(label, &sw) +} + +pub fn entry_row(label: &str, doc: &Doc, path: Path, placeholder: &str, default: &str) -> GBox { + let cur = config::get_str(&doc.borrow(), path).unwrap_or_else(|| default.to_string()); + let entry = Entry::new(); + entry.set_text(&cur); + entry.set_hexpand(true); + entry.set_width_chars(28); + if !placeholder.is_empty() { + entry.set_placeholder_text(Some(placeholder)); + } + let doc = doc.clone(); + entry.connect_changed(move |e| { + config::set_str_or_remove(&mut doc.borrow_mut(), path, e.text().as_str()); + }); + row(label, &entry) +} + +pub fn password_row(label: &str, doc: &Doc, path: Path) -> GBox { + let cur = config::get_str(&doc.borrow(), path).unwrap_or_default(); + let entry = Entry::new(); + entry.set_text(&cur); + entry.set_visibility(false); + entry.set_hexpand(true); + entry.set_width_chars(28); + entry.set_input_purpose(gtk4::InputPurpose::Password); + let doc = doc.clone(); + entry.connect_changed(move |e| { + config::set_str_or_remove(&mut doc.borrow_mut(), path, e.text().as_str()); + }); + row(label, &entry) +} + +/// A dropdown that stores the selected option string at `path`. +pub fn dropdown_row(label: &str, doc: &Doc, path: Path, options: &[&str], default: &str) -> GBox { + let cur = config::get_str(&doc.borrow(), path).unwrap_or_else(|| default.to_string()); + let model = StringList::new(options); + let dd = DropDown::new(Some(model), Expression::NONE); + let sel = options.iter().position(|o| *o == cur).unwrap_or(0) as u32; + dd.set_selected(sel); + let owned: Vec = options.iter().map(|s| s.to_string()).collect(); + let doc = doc.clone(); + dd.connect_selected_notify(move |dd| { + if let Some(opt) = owned.get(dd.selected() as usize) { + config::set_str(&mut doc.borrow_mut(), path, opt); + } + }); + row(label, &dd) +} + +/// An integer spin button storing its value at `path`. +pub fn spin_row( + label: &str, + doc: &Doc, + path: Path, + min: f64, + max: f64, + step: f64, + default: i64, +) -> GBox { + let cur = config::get_i64(&doc.borrow(), path).unwrap_or(default); + let adj = Adjustment::new(cur as f64, min, max, step, step, 0.0); + let spin = SpinButton::new(Some(&adj), step, 0); + let doc = doc.clone(); + spin.connect_value_changed(move |s| { + config::set_i64(&mut doc.borrow_mut(), path, s.value() as i64); + }); + row(label, &spin) +} + +/// A fractional spin button (e.g. 0.0–1.0 confidence) storing a float. +pub fn spin_f64_row( + label: &str, + doc: &Doc, + path: Path, + min: f64, + max: f64, + step: f64, + digits: u32, + default: f64, +) -> GBox { + let cur = config::get_f64(&doc.borrow(), path).unwrap_or(default); + let adj = Adjustment::new(cur, min, max, step, step, 0.0); + let spin = SpinButton::new(Some(&adj), step, digits); + let doc = doc.clone(); + spin.connect_value_changed(move |s| { + config::set_f64(&mut doc.borrow_mut(), path, s.value()); + }); + row(label, &spin) +} + +/// A comma-separated list editor storing an array of strings at `path`. +pub fn csv_row(label: &str, doc: &Doc, path: Path, placeholder: &str) -> GBox { + let cur = config::get_str_list(&doc.borrow(), path).join(", "); + let entry = Entry::new(); + entry.set_text(&cur); + entry.set_hexpand(true); + entry.set_width_chars(28); + if !placeholder.is_empty() { + entry.set_placeholder_text(Some(placeholder)); + } + let doc = doc.clone(); + entry.connect_changed(move |e| { + let items: Vec = e + .text() + .split(',') + .map(|s| s.trim().to_string()) + .filter(|s| !s.is_empty()) + .collect(); + config::set_str_list(&mut doc.borrow_mut(), path, &items); + }); + row(label, &entry) +} + +/// A Save button + transient status label that persists the document to `path`. +pub fn save_button(doc: &Doc, path: PathBuf) -> GBox { + let btn_row = GBox::new(Orientation::Horizontal, 12); + btn_row.set_margin_top(16); + + let save_btn = Button::with_label("Save"); + save_btn.add_css_class("suggested-action"); + let status = Label::new(None); + status.add_css_class("dim-label"); + + let doc = doc.clone(); + let status_c = status.clone(); + save_btn.connect_clicked(move |_| match config::save_doc(&path, &doc.borrow()) { + Ok(()) => { + status_c.set_text("Saved"); + let lbl = status_c.clone(); + glib::timeout_add_seconds_local(3, move || { + lbl.set_text(""); + glib::ControlFlow::Break + }); + } + Err(e) => status_c.set_text(&format!("Error: {e}")), + }); + + btn_row.append(&save_btn); + btn_row.append(&status); + btn_row +} diff --git a/bos-settings/src/ui/window.rs b/bos-settings/src/ui/window.rs index 0088675..c07a231 100644 --- a/bos-settings/src/ui/window.rs +++ b/bos-settings/src/ui/window.rs @@ -1,5 +1,5 @@ use gtk4::prelude::*; -use gtk4::{Application, ApplicationWindow, Box as GBox, Orientation, Paned, Stack}; +use gtk4::{Application, ApplicationWindow, Orientation, Paned, Stack}; use super::sidebar; use super::views; @@ -12,7 +12,7 @@ pub fn build_ui(app: &Application) { .default_height(640) .build(); - crate::theme::load(&window.display()); + crate::theme::load(&WidgetExt::display(&window)); let hpaned = Paned::new(Orientation::Horizontal); hpaned.set_position(190); diff --git a/bread_white.svg b/bread_white.svg new file mode 100644 index 0000000..b705951 --- /dev/null +++ b/bread_white.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/build-local.sh b/build-local.sh new file mode 100755 index 0000000..3e39da8 --- /dev/null +++ b/build-local.sh @@ -0,0 +1,93 @@ +#!/usr/bin/env bash +# Local BOS ISO build for hermes (native Arch — no container needed). +# +# Builds straight from the working tree in ./iso. Two speedups vs the hestia +# container build: +# * runs natively (hermes is Arch; hestia needed a dockerised Arch) +# * no 2 GB scp afterwards — the ISO lands here, where we test it +# +# FAST_BUILD=1 zstd squashfs instead of xz -9e: compresses many times +# faster at the cost of a slightly larger image. Dev only. +# +# Usage: sudo ./build-local.sh # release-quality xz +# sudo FAST_BUILD=1 ./build-local.sh # fast dev iteration +set -euo pipefail + +REPO="$(cd "$(dirname "$0")" && pwd)" +# WORK defaults to /tmp, but on hermes /tmp is a 16 GB tmpfs — a full xz build +# (uncompressed rootfs + squashfs + work copies) can exhaust it mid-build. Allow +# pointing it at the NVMe instead: WORK=/home/.../bos-work sudo ./build-local.sh +WORK="${WORK:-/tmp/bos-work}" +OUT="${OUT:-$REPO/out}" + +# Build against a throwaway copy of the profile so the working tree stays clean +# when FAST_BUILD / the registry rewrite mutate profile files. +STAGE=/tmp/bos-iso-stage +rm -rf "$STAGE" && cp -a "$REPO/iso" "$STAGE" + +# The public git.breadway.dev URL is flaky/unreachable from hermes; Forgejo is +# directly reachable over Tailscale (hestia 100.66.238.26:3002). Only rewrites +# the staged copy, never the committed pacman.conf. +sed -i 's#https://git.breadway.dev/api/packages/Breadway/arch/os#http://100.66.238.26:3002/api/packages/Breadway/arch/os#' "$STAGE/pacman.conf" + +if [ "${FAST_BUILD:-0}" = "1" ]; then + echo "=== FAST_BUILD: squashfs -> zstd level 6 ===" + sed -i "s#^airootfs_image_tool_options=.*#airootfs_image_tool_options=('-comp' 'zstd' '-Xcompression-level' '6' '-b' '1M')#" "$STAGE/profiledef.sh" +fi +grep airootfs_image_tool_options "$STAGE/profiledef.sh" + +# --- Bake this laptop's bakery-installed bread ecosystem into /etc/skel ------- +# The bread apps are managed by bakery (which fetches release binaries from +# GitHub), not pacman. bakery needs DNS at install time, which the live/installed +# image doesn't have — so instead of running bakery on the target, we copy the +# exact binaries + bakery manifest this laptop already has into skel. Every user +# created from skel (the live user and the installed user) then gets the same +# versions `bakery list` reports here, fully offline. Copied at build time so the +# binaries never bloat the git repo and always track the current bakery state. +BREAD_BINS=(bakery bread breadd breadman breadbar breadbox breadbox-sync breadcrumbs breadpad bread-theme) +LAPTOP_HOME="${LAPTOP_HOME:-$(getent passwd "${SUDO_USER:-$USER}" | cut -d: -f6)}" +BAKERY_BIN="$LAPTOP_HOME/.local/bin" +BAKERY_STATE="$LAPTOP_HOME/.local/state/bakery" +BAKERY_CACHE="$LAPTOP_HOME/.cache/bakery" +SKEL="$STAGE/airootfs/etc/skel" +echo "=== baking bakery bread ecosystem from $LAPTOP_HOME ===" +install -d -m 0755 "$SKEL/.local/bin" "$SKEL/.local/state/bakery" "$SKEL/.cache/bakery" +for b in "${BREAD_BINS[@]}"; do + install -m 0755 "$BAKERY_BIN/$b" "$SKEL/.local/bin/$b" +done +install -m 0644 "$BAKERY_STATE/installed.json" "$SKEL/.local/state/bakery/installed.json" +# bakery fetches its package index from dl.breadway.dev (then a GitHub fallback), +# but falls back to a cached index when both are unreachable. With no network/DNS +# in the live/installed image, even `bakery list` errors unless that cache exists, +# so bake it in too — then bakery works fully offline (list/info from cache; +# install/update still need network, as expected). +install -m 0644 "$BAKERY_CACHE/index.json" "$SKEL/.cache/bakery/index.json" +echo "baked: $(ls "$SKEL/.local/bin")" + +# mkarchiso resets every airootfs file to 0644, so executables must be declared +# in profiledef.sh's file_permissions array or they ship non-executable and the +# exec-once launches fail with "permission denied". Inject a 0755 entry for each +# baked binary right after the array opener (keeps the binary list in one place). +perm_file="$(mktemp)" +for b in "${BREAD_BINS[@]}"; do + printf ' ["/etc/skel/.local/bin/%s"]="0:0:755"\n' "$b" >>"$perm_file" +done +sed -i "/^file_permissions=(/r $perm_file" "$STAGE/profiledef.sh" +rm -f "$perm_file" +echo "=== file_permissions after injection ==="; grep -A14 '^file_permissions=(' "$STAGE/profiledef.sh" + +# Pin one timestamp for the whole build. Without this, mkarchiso derives the +# boot-config UUID (%ARCHISO_UUID%) when it starts and the iso9660 volume UUID +# when xorriso writes the image at the end — on a slow build these diverge by +# the build duration, so the initramfs searches /dev/disk/by-uuid/, +# never finds the medium, and drops to a recovery shell. Fixing the epoch makes +# both derive from the same instant (and makes builds reproducible). +export SOURCE_DATE_EPOCH="${SOURCE_DATE_EPOCH:-$(date +%s)}" +echo "SOURCE_DATE_EPOCH=$SOURCE_DATE_EPOCH ($(date -u -d "@$SOURCE_DATE_EPOCH" +%Y-%m-%d-%H-%M-%S-00))" + +echo "=== running mkarchiso ===" +rm -rf "$WORK" && mkdir -p "$OUT" +mkarchiso -v -w "$WORK" -o "$OUT" "$STAGE" + +echo "=== RESULT ===" +if ls -lh "$OUT"/*.iso 2>/dev/null; then echo "ISO BUILT OK -> $OUT"; else echo "ISO BUILD FAILED"; exit 1; fi diff --git a/dotfiles/hypr/hyprland.conf b/dotfiles/hypr/hyprland.conf index e509b63..e0a4889 100644 --- a/dotfiles/hypr/hyprland.conf +++ b/dotfiles/hypr/hyprland.conf @@ -46,7 +46,6 @@ input { } dwindle { - pseudotile = true preserve_split = true } diff --git a/iso/airootfs/etc/calamares/branding/bos/branding.desc b/iso/airootfs/etc/calamares/branding/bos/branding.desc index d3034ab..91c929f 100644 --- a/iso/airootfs/etc/calamares/branding/bos/branding.desc +++ b/iso/airootfs/etc/calamares/branding/bos/branding.desc @@ -23,7 +23,7 @@ slideshow: "show.qml" slideshowAPI: 2 style: - sidebarBackground: "#3b4252" - sidebarText: "#eceff4" - sidebarTextSelect: "#5e81ac" - sidebarTextHighlight:"#eceff4" + sidebarBackground: "#230b00" + sidebarText: "#f1dcbd" + sidebarTextSelect: "#EAB672" + sidebarTextHighlight: "#ffffff" diff --git a/iso/airootfs/etc/calamares/branding/bos/languages.png b/iso/airootfs/etc/calamares/branding/bos/languages.png new file mode 100644 index 0000000..ee04ad0 Binary files /dev/null and b/iso/airootfs/etc/calamares/branding/bos/languages.png differ diff --git a/iso/airootfs/etc/calamares/branding/bos/logo.png b/iso/airootfs/etc/calamares/branding/bos/logo.png new file mode 100644 index 0000000..30bee39 Binary files /dev/null and b/iso/airootfs/etc/calamares/branding/bos/logo.png differ diff --git a/iso/airootfs/etc/calamares/branding/bos/logo.svg b/iso/airootfs/etc/calamares/branding/bos/logo.svg new file mode 100644 index 0000000..b705951 --- /dev/null +++ b/iso/airootfs/etc/calamares/branding/bos/logo.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/iso/airootfs/etc/calamares/branding/bos/stylesheet.qss b/iso/airootfs/etc/calamares/branding/bos/stylesheet.qss new file mode 100644 index 0000000..f83b887 --- /dev/null +++ b/iso/airootfs/etc/calamares/branding/bos/stylesheet.qss @@ -0,0 +1,23 @@ +/* BOS Calamares styling. + * + * The branding `style:` sidebar keys were being overridden, leaving the + * step tabs invisible. This forces them: bread-palette sidebar with clearly + * legible step labels. (A full installer retheme is tracked separately.) + */ + +/* Left sidebar / progress steps */ +#sidebarApp { + background-color: #230b00; +} + +#sidebarApp QLabel { + color: #f1dcbd; + font-size: 11pt; + padding: 3px 0; +} + +/* Logo at the top of the sidebar — keep aspect ratio, don't stretch */ +#logoApp { + qproperty-alignment: AlignCenter; + margin: 14px 8px; +} diff --git a/iso/airootfs/etc/calamares/modules/mount.conf b/iso/airootfs/etc/calamares/modules/mount.conf index 767bf57..4218ca7 100644 --- a/iso/airootfs/etc/calamares/modules/mount.conf +++ b/iso/airootfs/etc/calamares/modules/mount.conf @@ -8,3 +8,32 @@ mountOptions: options: [noatime, "compress=zstd", "space_cache=v2"] - filesystem: vfat options: [umask=0077] + +# API filesystems mounted into the target so chroot steps work. Without these +# the chroot has no /proc or /dev and `mkinitcpio` aborts ("/proc must be +# mounted!" / "/dev must be mounted!") and grub-install can't probe properly. +# All are real-fs mounts (not bind) — Calamares 3.4.2 here applies fs-type mounts +# reliably but not bind-type ones, so /dev uses a fresh devtmpfs (which still +# exposes all device nodes). extraMountsEfi adds efivars on UEFI so grub-install +# can write an NVRAM boot entry. +extraMounts: + - device: proc + fs: proc + mountPoint: /proc + - device: sys + fs: sysfs + mountPoint: /sys + - device: udev + fs: devtmpfs + mountPoint: /dev + - device: devpts + fs: devpts + mountPoint: /dev/pts + - device: tmpfs + fs: tmpfs + mountPoint: /run + +extraMountsEfi: + - device: efivarfs + fs: efivarfs + mountPoint: /sys/firmware/efi/efivars diff --git a/iso/airootfs/etc/calamares/modules/plymouthcfg.conf b/iso/airootfs/etc/calamares/modules/plymouthcfg.conf new file mode 100644 index 0000000..d9aeb16 --- /dev/null +++ b/iso/airootfs/etc/calamares/modules/plymouthcfg.conf @@ -0,0 +1,6 @@ +--- +# Boot-splash theme for the installed system. Calamares sets this as the default +# plymouth theme and signals initcpiocfg to add the plymouth hook to the +# initramfs. The post-install script also enforces the hook + a quiet/splash +# cmdline as a belt-and-suspenders. +plymouth_theme: bos diff --git a/iso/airootfs/etc/calamares/modules/shellprocess-kernel.conf b/iso/airootfs/etc/calamares/modules/shellprocess-kernel.conf new file mode 100644 index 0000000..4e5ab5c --- /dev/null +++ b/iso/airootfs/etc/calamares/modules/shellprocess-kernel.conf @@ -0,0 +1,8 @@ +--- +# Lay the kernel into the target /boot before the bootloader/initramfs steps. +# Runs in the live environment (not the chroot) so it can read the ISO boot dir. +dontChroot: true +timeout: 60 + +script: + - "/usr/bin/bash /usr/local/bin/bos-copy-kernel ${ROOT}" diff --git a/iso/airootfs/etc/calamares/modules/shellprocess.conf b/iso/airootfs/etc/calamares/modules/shellprocess.conf index 3aef052..b05cee1 100644 --- a/iso/airootfs/etc/calamares/modules/shellprocess.conf +++ b/iso/airootfs/etc/calamares/modules/shellprocess.conf @@ -1,3 +1,10 @@ --- +# BOS finalization, run after the native initcpio + bootloader modules. It does +# only fast, non-boot-critical work (live-medium cleanup, snapper, services, +# dotfiles), so the shellprocess timeout can no longer leave the system +# unbootable — the boot-critical steps are owned by dedicated Calamares modules. +# A generous timeout is kept as a safety margin, and the leading "-" keeps a +# non-zero exit non-fatal to the install. +timeout: 600 script: - "-/usr/bin/bash /etc/calamares/post-install.sh" diff --git a/iso/airootfs/etc/calamares/modules/users.conf b/iso/airootfs/etc/calamares/modules/users.conf index 8f2d422..566fff2 100644 --- a/iso/airootfs/etc/calamares/modules/users.conf +++ b/iso/airootfs/etc/calamares/modules/users.conf @@ -38,3 +38,4 @@ passwordRequirements: - minlen=6 allowWeakPasswords: false +userShell: /bin/zsh diff --git a/iso/airootfs/etc/calamares/post-install.sh b/iso/airootfs/etc/calamares/post-install.sh index 967292b..e318ed1 100644 --- a/iso/airootfs/etc/calamares/post-install.sh +++ b/iso/airootfs/etc/calamares/post-install.sh @@ -1,41 +1,151 @@ #!/bin/bash -set -euo pipefail +# BOS-specific finalization, run inside the installed-system chroot (Calamares +# shellprocess), AFTER the native initcpio module has built the initramfs. The +# kernel (shellprocess@kernel) and initramfs (initcpio) are in place by now, so +# this script installs GRUB and does the rest of setup. Calamares' own +# `bootloader`/`grubcfg` modules are NOT used — in this archiso layout they leave +# the ESP empty and abort; the explicit grub-install below is verified to boot. +# Best-effort: do NOT use `set -e`; a single failure here must not abort the rest. +set -uo pipefail -# --- Snapper root config --- -snapper -c root create-config / -sed -i 's/TIMELINE_CREATE="yes"/TIMELINE_CREATE="no"/' /etc/snapper/configs/root -sed -i 's/NUMBER_CLEANUP="no"/NUMBER_CLEANUP="yes"/' /etc/snapper/configs/root -sed -i 's/NUMBER_MIN_AGE="[^"]*"/NUMBER_MIN_AGE="1800"/' /etc/snapper/configs/root -sed -i 's/NUMBER_LIMIT="[^"]*"/NUMBER_LIMIT="10"/' /etc/snapper/configs/root -sed -i 's/NUMBER_LIMIT_IMPORTANT="[^"]*"/NUMBER_LIMIT_IMPORTANT="5"/' /etc/snapper/configs/root +MAIN_USER="$(getent passwd 1000 | cut -d: -f1 || true)" -# Allow main user to list/create/delete snapshots without sudo -MAIN_USER=$(getent passwd 1000 | cut -d: -f1) -sed -i "s/ALLOW_USERS=\"\"/ALLOW_USERS=\"$MAIN_USER\"/" /etc/snapper/configs/root +# --------------------------------------------------------------------------- +# Strip live-only bits that unpackfs copied verbatim from the live medium. +# --------------------------------------------------------------------------- +rm -f /etc/systemd/system/getty@tty1.service.d/autologin.conf +rm -f /etc/systemd/system/bos-live-setup.service \ + /etc/systemd/system/multi-user.target.wants/bos-live-setup.service +rm -f /usr/local/bin/bos-live-setup /usr/local/bin/bos-launch-calamares +rm -f /etc/sudoers.d/99-bos-live +userdel -r liveuser 2>/dev/null || true -# --- System services --- -systemctl enable NetworkManager -systemctl enable bluetooth -systemctl enable snapper-cleanup.timer -systemctl enable grub-btrfs.path +# Root used a passwordless entry on the live medium; lock it (sudo model). +passwd -l root || true -# --- Bakery: install bread ecosystem --- -# Requires [breadway] repo in /etc/pacman.conf — see iso/pacman.conf -if command -v bakery &>/dev/null; then - sudo -u "$MAIN_USER" bakery install bread breadbar breadbox breadcrumbs breadpad bos-settings +# --------------------------------------------------------------------------- +# Boot splash (Plymouth) — BOS logo + spinner instead of kernel text. Done +# BEFORE grub so grub.cfg picks up the new cmdline and the rebuilt initramfs. +# All best-effort: if anything here fails the system still boots (just without +# the splash) — the initramfs the initcpio module already built stays valid. +# --------------------------------------------------------------------------- +if command -v plymouth-set-default-theme &>/dev/null; then + # Ensure the plymouth hook is in HOOKS (plymouthcfg/initcpiocfg usually add it; + # this is the belt). Handle both the udev and systemd initramfs styles. + if ! grep -q 'plymouth' /etc/mkinitcpio.conf 2>/dev/null; then + if grep -qE '^HOOKS=.*\bsystemd\b' /etc/mkinitcpio.conf; then + sed -i 's/^\(HOOKS=.*\bsystemd\b\)/\1 sd-plymouth/' /etc/mkinitcpio.conf \ + || echo "WARN: adding sd-plymouth hook failed" + else + sed -i 's/^\(HOOKS=.*\budev\b\)/\1 plymouth/' /etc/mkinitcpio.conf \ + || echo "WARN: adding plymouth hook failed" + fi + fi + # Clean boot: splash activates plymouth; hiding systemd status removes the + # "[ OK ] Started ..." text (what looked like kernel output) even if the + # splash itself doesn't grab the display (e.g. in some VMs). + if ! grep -q 'splash' /etc/default/grub 2>/dev/null; then + sed -i 's/^\(GRUB_CMDLINE_LINUX_DEFAULT="\)/\1splash quiet vt.global_cursor_default=0 systemd.show_status=false rd.systemd.show_status=false rd.udev.log_level=3 /' \ + /etc/default/grub || echo "WARN: adding splash cmdline failed" + fi + # Set the BOS theme and rebuild the initramfs (-R) with the plymouth hook. + plymouth-set-default-theme -R bos || echo "WARN: plymouth-set-default-theme failed" fi -# --- Deploy dotfiles into user home (skip any file that already exists) --- -SKEL_SRC="/etc/skel/.config" -DOTFILES_DEST="/home/$MAIN_USER/.config" - -if [[ -d "$SKEL_SRC" ]]; then - mkdir -p "$DOTFILES_DEST" - cp -rn "$SKEL_SRC/." "$DOTFILES_DEST/" - chown -R "$MAIN_USER:$MAIN_USER" "$DOTFILES_DEST" +# --------------------------------------------------------------------------- +# Install GRUB (UEFI). /boot now has the kernel + initramfs, and the mount +# module has bind-mounted /proc /sys /dev /run + efivars into this chroot, so +# both grub-install passes and grub-mkconfig succeed. +# 1. NVRAM entry (EFI/BOS/grubx64.efi + a firmware boot entry) +# 2. --removable copy to EFI/BOOT/BOOTX64.EFI, so firmware that ignores/loses +# the NVRAM entry (the "no boot device / PXE fallback" failure) still finds +# a bootloader. +# --------------------------------------------------------------------------- +if command -v grub-install &>/dev/null; then + grub-install --target=x86_64-efi --efi-directory=/boot/efi \ + --bootloader-id=BOS --recheck \ + || echo "WARN: grub-install (nvram) failed" + grub-install --target=x86_64-efi --efi-directory=/boot/efi \ + --removable --recheck \ + || echo "WARN: grub-install (removable) failed" +fi +if command -v grub-mkconfig &>/dev/null; then + grub-mkconfig -o /boot/grub/grub.cfg || echo "WARN: grub-mkconfig failed" fi -# --- XDG user dirs --- -sudo -u "$MAIN_USER" xdg-user-dirs-update +# --------------------------------------------------------------------------- +# Snapper root config (root is btrfs). +# --------------------------------------------------------------------------- +if command -v snapper &>/dev/null; then + snapper -c root create-config / || echo "WARN: snapper create-config failed" + if [[ -f /etc/snapper/configs/root ]]; then + sed -i 's/TIMELINE_CREATE="yes"/TIMELINE_CREATE="no"/' /etc/snapper/configs/root + sed -i 's/NUMBER_CLEANUP="no"/NUMBER_CLEANUP="yes"/' /etc/snapper/configs/root + sed -i 's/NUMBER_MIN_AGE="[^"]*"/NUMBER_MIN_AGE="1800"/' /etc/snapper/configs/root + sed -i 's/NUMBER_LIMIT="[^"]*"/NUMBER_LIMIT="10"/' /etc/snapper/configs/root + sed -i 's/NUMBER_LIMIT_IMPORTANT="[^"]*"/NUMBER_LIMIT_IMPORTANT="5"/' /etc/snapper/configs/root + [[ -n "$MAIN_USER" ]] && \ + sed -i "s/ALLOW_USERS=\"\"/ALLOW_USERS=\"$MAIN_USER\"/" /etc/snapper/configs/root + fi +fi -echo "BOS post-install complete. Reboot to start your system." +# --------------------------------------------------------------------------- +# System services. Enable each one INDEPENDENTLY: `systemctl enable a b c` +# resolves every unit first and enables NONE if any one can't be loaded, so a +# single wrong/absent unit name would silently leave NetworkManager (etc.) +# disabled. The loop isolates failures to the offending unit. +# greetd — graphical login (shipped disabled; live uses tty autologin) +# grub-btrfsd — regenerates GRUB snapshot entries (the unit is grub-btrfsd.service, +# NOT grub-btrfs.path, which no longer exists) +# --------------------------------------------------------------------------- +for unit in NetworkManager.service bluetooth.service systemd-timesyncd.service \ + tlp.service greetd.service snapper-cleanup.timer grub-btrfsd.service \ + fstrim.timer cups.socket avahi-daemon.service ufw.service \ + fwupd-refresh.timer reflector.timer; do + systemctl enable "$unit" || echo "WARN: failed to enable $unit" +done +systemctl set-default graphical.target || echo "WARN: set-default graphical failed" + +# --------------------------------------------------------------------------- +# mDNS resolution (nss-mdns): insert mdns_minimal into the hosts: line so the +# resolver answers *.local (network printers, other hosts) via avahi. Idempotent. +# --------------------------------------------------------------------------- +if [[ -f /etc/nsswitch.conf ]] && ! grep -q 'mdns_minimal' /etc/nsswitch.conf; then + sed -i 's/^\(hosts:[[:space:]]*\)/\1mdns_minimal [NOTFOUND=return] /' \ + /etc/nsswitch.conf || echo "WARN: wiring nss-mdns failed" +fi + +# --------------------------------------------------------------------------- +# Firewall: deny inbound by default, allow outbound, and permit inbound mDNS so +# avahi printer/service discovery keeps working. Best-effort — rule application +# happens at boot; here we only persist the policy + enable the unit. +# --------------------------------------------------------------------------- +if command -v ufw &>/dev/null; then + ufw default deny incoming || echo "WARN: ufw default deny incoming failed" + ufw default allow outgoing || echo "WARN: ufw default allow outgoing failed" + ufw allow 5353/udp || echo "WARN: ufw allow mDNS failed" + ufw --force enable || echo "WARN: ufw enable failed" +fi + +# The bread ecosystem (bakery + bread, breadbar, breadbox, breadcrumbs, breadpad) +# is bakery-managed, not pacman: the binaries and bakery manifest live in +# /etc/skel/.local (baked in at ISO build time) and are copied into the user's +# home below, so the install works fully offline with no DNS for bakery/GitHub. +# bos-settings is the only pacman bread package and was installed by unpackfs. + +# --------------------------------------------------------------------------- +# Deploy dotfiles + the bakery bread ecosystem into the user's home (Calamares +# already seeds from /etc/skel, but copy explicitly too so a fresh install is +# self-contained even if the users module skips skel). Don't clobber existing. +# --------------------------------------------------------------------------- +if [[ -n "$MAIN_USER" && -d /etc/skel ]]; then + for d in .config .local .cache; do + [[ -d "/etc/skel/$d" ]] || continue + mkdir -p "/home/$MAIN_USER/$d" + cp -rn "/etc/skel/$d/." "/home/$MAIN_USER/$d/" || true + chown -R "$MAIN_USER:$MAIN_USER" "/home/$MAIN_USER/$d" || true + done + sudo -u "$MAIN_USER" xdg-user-dirs-update || true +fi + +echo "BOS post-install complete." diff --git a/iso/airootfs/etc/calamares/settings.conf b/iso/airootfs/etc/calamares/settings.conf index ea17565..f97ae87 100644 --- a/iso/airootfs/etc/calamares/settings.conf +++ b/iso/airootfs/etc/calamares/settings.conf @@ -1,6 +1,13 @@ --- modules-search: [/etc/calamares/modules, /usr/lib/calamares/modules] +# Second shellprocess instance: copies the live kernel into the target /boot +# (archiso keeps it out of the squashfs) before the bootloader step runs. +instances: +- id: kernel + module: shellprocess + config: shellprocess-kernel.conf + sequence: - show: - welcome @@ -22,7 +29,21 @@ sequence: - networkcfg - hwclock - packages - - bootloader + # archiso strips the kernel from the squashfs; stage it, drop the archiso + # initramfs config, and write a stock mkinitcpio preset before initcpio runs. + - shellprocess@kernel + # plymouthcfg sets the boot-splash theme and flags plymouth in use, so + # initcpiocfg adds the plymouth hook to the initramfs that initcpio builds. + - plymouthcfg + # Native initramfs generation (works reliably here). The native `bootloader` + # and `grubcfg` modules do NOT — in this archiso layout they leave the ESP + # empty and abort the install, so GRUB is installed explicitly in + # post-install.sh instead (grub-install --removable + NVRAM + grub-mkconfig, + # the sequence verified to produce a bootable system). + - initcpiocfg + - initcpio + # BOS finalization: GRUB install + cleanup + snapper + services + dotfiles. + # All fast, and runs after initcpio so /boot has the kernel + initramfs. - shellprocess - umount - show: diff --git a/iso/airootfs/etc/greetd/config.toml b/iso/airootfs/etc/greetd/config.toml new file mode 100644 index 0000000..4488779 --- /dev/null +++ b/iso/airootfs/etc/greetd/config.toml @@ -0,0 +1,12 @@ +# greetd drives login on the INSTALLED system. It is shipped DISABLED in the +# squashfs and enabled by post-install.sh; the live ISO instead autologins +# liveuser on tty1 (see getty@tty1 drop-in) so the installer comes straight up. +# +# tuigreet shows a minimal greeter, then launches the BOS Hyprland session via +# bos-session (which fixes up PATH for the bakery bread apps). +[terminal] +vt = 1 + +[default_session] +command = "tuigreet --remember --time --cmd /usr/local/bin/bos-session" +user = "greeter" diff --git a/iso/airootfs/etc/hostname b/iso/airootfs/etc/hostname new file mode 100644 index 0000000..8d6d854 --- /dev/null +++ b/iso/airootfs/etc/hostname @@ -0,0 +1 @@ +bos diff --git a/iso/airootfs/etc/locale.conf b/iso/airootfs/etc/locale.conf new file mode 100644 index 0000000..f9c983c --- /dev/null +++ b/iso/airootfs/etc/locale.conf @@ -0,0 +1 @@ +LANG=C.UTF-8 diff --git a/iso/airootfs/etc/localtime b/iso/airootfs/etc/localtime new file mode 120000 index 0000000..0e35b57 --- /dev/null +++ b/iso/airootfs/etc/localtime @@ -0,0 +1 @@ +/usr/share/zoneinfo/UTC \ No newline at end of file diff --git a/iso/airootfs/etc/mkinitcpio.conf.d/archiso.conf b/iso/airootfs/etc/mkinitcpio.conf.d/archiso.conf new file mode 100644 index 0000000..5c008e5 --- /dev/null +++ b/iso/airootfs/etc/mkinitcpio.conf.d/archiso.conf @@ -0,0 +1,3 @@ +HOOKS=(base udev microcode modconf kms memdisk archiso archiso_loop_mnt archiso_pxe_common archiso_pxe_nbd archiso_pxe_http archiso_pxe_nfs block filesystems keyboard) +COMPRESSION="xz" +COMPRESSION_OPTIONS=(-9e) diff --git a/iso/airootfs/etc/mkinitcpio.d/linux.preset b/iso/airootfs/etc/mkinitcpio.d/linux.preset new file mode 100644 index 0000000..8e85205 --- /dev/null +++ b/iso/airootfs/etc/mkinitcpio.d/linux.preset @@ -0,0 +1,8 @@ +# mkinitcpio preset file for the 'linux' package on archiso + +PRESETS=('archiso') + +ALL_kver='/boot/vmlinuz-linux' +archiso_config='/etc/mkinitcpio.conf.d/archiso.conf' + +archiso_image="/boot/initramfs-linux.img" diff --git a/iso/airootfs/etc/pacman.conf b/iso/airootfs/etc/pacman.conf new file mode 100644 index 0000000..90e4517 --- /dev/null +++ b/iso/airootfs/etc/pacman.conf @@ -0,0 +1,44 @@ +# +# BOS pacman.conf — used during ISO build and installed to the target system. +# Based on the standard Arch Linux pacman.conf. +# + +[options] +HoldPkg = pacman glibc +Architecture = auto +CheckSpace +ParallelDownloads = 5 + +Color +VerbosePkgLists +ILoveCandy + +SigLevel = Required DatabaseOptional +LocalFileSigLevel = Optional + +[core] +Include = /etc/pacman.d/mirrorlist + +[extra] +Include = /etc/pacman.d/mirrorlist + +[multilib] +Include = /etc/pacman.d/mirrorlist + +# ----------------------------------------------------------------------- +# Breadway custom repo — provides: bakery and the bread ecosystem packages +# (bread, breadbar, breadbox, breadcrumbs, breadpad, bos-settings). +# (calamares comes from the official extra repo, not here.) +# +# Packages are published to the Forgejo Arch registry (group "os") by the +# .forgejo/workflows/package.yml workflow in each repo, on tag push. +# +# Forgejo signs the repo db with a key pacman can't look up, so TrustAll +# fails. SigLevel = Never skips verification (acceptable for this private +# repo over TLS). TODO: import Forgejo's signing key + SigLevel = Required. +# ----------------------------------------------------------------------- +# The section name must match Forgejo's served db filename +# ({owner}.{group}.{domain}.db) — pacman fetches "
.db" from Server. +[Breadway.os.git.breadway.dev] +SigLevel = Never +Server = https://git.breadway.dev/api/packages/Breadway/arch/os/$arch diff --git a/iso/airootfs/etc/pacman.d/mirrorlist b/iso/airootfs/etc/pacman.d/mirrorlist new file mode 100644 index 0000000..1199b26 --- /dev/null +++ b/iso/airootfs/etc/pacman.d/mirrorlist @@ -0,0 +1,11 @@ +# BOS default mirrorlist. +# +# geo.mirror.pkgbuild.com is the official Arch geo-IP redirect — it routes to a +# nearby mirror anywhere in the world, so pacman works out of the box without +# region-specific configuration. The others are reliable global fallbacks. +# +# To optimise for your location/speed later: sudo reflector --save \ +# /etc/pacman.d/mirrorlist --protocol https --latest 20 --sort rate +Server = https://geo.mirror.pkgbuild.com/$repo/os/$arch +Server = https://mirror.rackspace.com/archlinux/$repo/os/$arch +Server = https://mirror.leaseweb.net/archlinux/$repo/os/$arch diff --git a/iso/airootfs/etc/passwd b/iso/airootfs/etc/passwd new file mode 100644 index 0000000..3f1a5e2 --- /dev/null +++ b/iso/airootfs/etc/passwd @@ -0,0 +1 @@ +root:x:0:0:root:/root:/usr/bin/bash diff --git a/iso/airootfs/etc/profile.d/bos-local-bin.sh b/iso/airootfs/etc/profile.d/bos-local-bin.sh new file mode 100644 index 0000000..642af43 --- /dev/null +++ b/iso/airootfs/etc/profile.d/bos-local-bin.sh @@ -0,0 +1,9 @@ +# Put the per-user bakery bin dir on PATH. The bread ecosystem (breadd, breadbar, +# breadbox, …) is installed there by bakery, and the Hyprland session launches +# them via `exec-once`, which resolves against the PATH it inherits from the +# login shell. Arch's stock /etc/profile does not add ~/.local/bin, so do it here +# for every login shell (live user and installed user alike). +case ":$PATH:" in + *":$HOME/.local/bin:"*) ;; + *) export PATH="$HOME/.local/bin:$PATH" ;; +esac diff --git a/iso/airootfs/etc/shadow b/iso/airootfs/etc/shadow new file mode 100644 index 0000000..7edfd69 --- /dev/null +++ b/iso/airootfs/etc/shadow @@ -0,0 +1 @@ +root::14871:::::: diff --git a/iso/airootfs/etc/skel/.cache/wal/colors.json b/iso/airootfs/etc/skel/.cache/wal/colors.json new file mode 100644 index 0000000..2b32390 --- /dev/null +++ b/iso/airootfs/etc/skel/.cache/wal/colors.json @@ -0,0 +1,28 @@ +{ + "wallpaper": "/usr/share/backgrounds/bos/bread-background.png", + "alpha": "100", + + "special": { + "background": "#0c0c0c", + "foreground": "#e8e8e8", + "cursor": "#eab672" + }, + "colors": { + "color0": "#1a1a1a", + "color1": "#b98749", + "color2": "#cd9450", + "color3": "#e3a85c", + "color4": "#eab672", + "color5": "#f6c477", + "color6": "#eabe82", + "color7": "#d8d8d8", + "color8": "#3a3a3a", + "color9": "#b98749", + "color10": "#cd9450", + "color11": "#e3a85c", + "color12": "#eab672", + "color13": "#f6c477", + "color14": "#eabe82", + "color15": "#f5f5f5" + } +} diff --git a/iso/airootfs/etc/skel/.config/bread/breadd.toml b/iso/airootfs/etc/skel/.config/bread/breadd.toml index 8473fe3..9bf2e1e 100644 --- a/iso/airootfs/etc/skel/.config/bread/breadd.toml +++ b/iso/airootfs/etc/skel/.config/bread/breadd.toml @@ -1,8 +1,21 @@ +# breadd daemon configuration. +# Every section is optional and every adapter defaults to enabled; this file +# just makes the defaults explicit. See the bread docs for the full schema. + +[daemon] log_level = "info" -[adapters] -keyboard = true -mouse = true -touchpad = true -bluetooth = true -gamepad = true +[adapters.hyprland] +enabled = true + +[adapters.udev] +enabled = true + +[adapters.power] +enabled = true + +[adapters.network] +enabled = true + +[adapters.bluetooth] +enabled = true diff --git a/iso/airootfs/etc/skel/.config/bread/modules/low-battery-warning.lua b/iso/airootfs/etc/skel/.config/bread/modules/low-battery-warning.lua new file mode 100644 index 0000000..40b515b --- /dev/null +++ b/iso/airootfs/etc/skel/.config/bread/modules/low-battery-warning.lua @@ -0,0 +1,26 @@ +-- low-battery-warning — notify once when the battery runs low (zero-config). +-- Shipped active in BOS; auto-discovered by breadd. Safe on desktops too +-- (simply never fires without a battery). + +local M = bread.module({ name = "low-battery-warning", version = "1.0.0" }) + +local warned = false + +function M.on_load() + bread.on("bread.power.battery.low", function(event) + if warned then return end + warned = true + local pct = event.data.battery_percent or "?" + bread.notify("Battery low (" .. pct .. "%). Plug in soon.", { + urgency = "critical", + title = "Battery", + timeout = 10000, + }) + end) + + bread.on("bread.power.ac.connected", function() + warned = false + end) +end + +return M diff --git a/iso/airootfs/etc/skel/.config/gtk-3.0/settings.ini b/iso/airootfs/etc/skel/.config/gtk-3.0/settings.ini new file mode 100644 index 0000000..b0bff85 --- /dev/null +++ b/iso/airootfs/etc/skel/.config/gtk-3.0/settings.ini @@ -0,0 +1,7 @@ +[Settings] +gtk-application-prefer-dark-theme=1 +gtk-theme-name=Adwaita-dark +gtk-icon-theme-name=Papirus-Dark +gtk-cursor-theme-name=Bibata-Modern-Ice +gtk-cursor-theme-size=24 +gtk-font-name=Noto Sans 11 diff --git a/iso/airootfs/etc/skel/.config/gtk-4.0/settings.ini b/iso/airootfs/etc/skel/.config/gtk-4.0/settings.ini new file mode 100644 index 0000000..b0bff85 --- /dev/null +++ b/iso/airootfs/etc/skel/.config/gtk-4.0/settings.ini @@ -0,0 +1,7 @@ +[Settings] +gtk-application-prefer-dark-theme=1 +gtk-theme-name=Adwaita-dark +gtk-icon-theme-name=Papirus-Dark +gtk-cursor-theme-name=Bibata-Modern-Ice +gtk-cursor-theme-size=24 +gtk-font-name=Noto Sans 11 diff --git a/iso/airootfs/etc/skel/.config/hypr/hypridle.conf b/iso/airootfs/etc/skel/.config/hypr/hypridle.conf new file mode 100644 index 0000000..aacd512 --- /dev/null +++ b/iso/airootfs/etc/skel/.config/hypr/hypridle.conf @@ -0,0 +1,37 @@ +# Idle management (hypridle). Conservative, mainstream-OS-like defaults: +# dim -> screen off -> lock -> suspend, all respecting idle inhibitors (so media +# playback, etc. won't dim or suspend the machine). Vendor-neutral: nothing here +# is Intel/AMD specific. +general { + lock_cmd = pidof hyprlock || hyprlock + before_sleep_cmd = loginctl lock-session + after_sleep_cmd = hyprctl dispatch dpms on + ignore_dbus_inhibit = false +} + +# Dim the backlight after 5 minutes (restored on activity). No-op on hardware +# without a backlight (desktops / VMs). +listener { + timeout = 300 + on-timeout = brightnessctl -s set 10% + on-resume = brightnessctl -r +} + +# Turn the display off after 8 minutes. +listener { + timeout = 480 + on-timeout = hyprctl dispatch dpms off + on-resume = hyprctl dispatch dpms on +} + +# Lock shortly after the screen turns off. +listener { + timeout = 510 + on-timeout = loginctl lock-session +} + +# Suspend after 20 minutes of inactivity (skipped while something inhibits idle). +listener { + timeout = 1200 + on-timeout = systemctl suspend +} diff --git a/iso/airootfs/etc/skel/.config/hypr/hyprland.conf b/iso/airootfs/etc/skel/.config/hypr/hyprland.conf deleted file mode 100644 index e509b63..0000000 --- a/iso/airootfs/etc/skel/.config/hypr/hyprland.conf +++ /dev/null @@ -1,56 +0,0 @@ -monitor=,preferred,auto,1 - -exec-once = breadd -exec-once = breadbar -exec-once = breadbox-sync - -source = ~/.config/hypr/keybinds.conf - -general { - gaps_in = 5 - gaps_out = 10 - border_size = 2 - col.active_border = rgba(88c0d0ff) - col.inactive_border = rgba(4c566aff) - layout = dwindle -} - -decoration { - rounding = 8 - blur { - enabled = true - size = 6 - passes = 2 - } - shadow { - enabled = true - range = 12 - render_power = 3 - } -} - -animations { - enabled = true - bezier = ease, 0.25, 0.1, 0.25, 1.0 - animation = windows, 1, 4, ease - animation = fade, 1, 4, ease - animation = workspaces, 1, 5, ease -} - -input { - kb_layout = us - follow_mouse = 1 - touchpad { - natural_scroll = true - } -} - -dwindle { - pseudotile = true - preserve_split = true -} - -misc { - disable_hyprland_logo = true - disable_splash_rendering = true -} diff --git a/iso/airootfs/etc/skel/.config/hypr/hyprland.lua b/iso/airootfs/etc/skel/.config/hypr/hyprland.lua new file mode 100644 index 0000000..c7ce981 --- /dev/null +++ b/iso/airootfs/etc/skel/.config/hypr/hyprland.lua @@ -0,0 +1,216 @@ +-- BOS Hyprland configuration — native Lua config (Hyprland 0.55+). +-- hyprlang (.conf) is deprecated; this uses the built-in `hl` API. +-- Single-file and non-modular by design. Reference: https://wiki.hypr.land/ + +local mod = "SUPER" + +-- --------------------------------------------------------------------------- +-- Monitors — generic default that works on any hardware. +-- --------------------------------------------------------------------------- +hl.monitor({ output = "", mode = "preferred", position = "auto", scale = "auto" }) + +-- --------------------------------------------------------------------------- +-- Core settings. +-- --------------------------------------------------------------------------- +hl.config({ + general = { + gaps_in = 5, + gaps_out = 10, + border_size = 2, + col = { + active_border = "rgba(88c0d0ff)", + inactive_border = "rgba(4c566aff)", + }, + layout = "dwindle", + resize_on_border = true, + }, + decoration = { + rounding = 8, + active_opacity = 1.0, + inactive_opacity = 1.0, + blur = { + enabled = true, + size = 6, + passes = 2, + new_optimizations = true, + }, + shadow = { + enabled = true, + range = 12, + render_power = 3, + }, + }, + input = { + kb_layout = "us", + follow_mouse = 1, + touchpad = { natural_scroll = true }, + }, + dwindle = { + preserve_split = true, + }, + animations = { + enabled = true, + }, + misc = { + disable_hyprland_logo = true, + disable_splash_rendering = true, + }, +}) + +-- --------------------------------------------------------------------------- +-- Animations — snappy curves + per-leaf speeds (matches the reference laptop; +-- the hl.config default above is much slower). +-- --------------------------------------------------------------------------- +local curves = { + easeOutQuint = { type = "bezier", points = { { 0.23, 1 }, { 0.32, 1 } } }, + easeInOutCubic = { type = "bezier", points = { { 0.65, 0.05 }, { 0.36, 1 } } }, + almostLinear = { type = "bezier", points = { { 0.5, 0.5 }, { 0.75, 1 } } }, + quick = { type = "bezier", points = { { 0.15, 0 }, { 0.1, 1 } } }, +} +for name, curve in pairs(curves) do + hl.curve(name, curve) +end + +local animations = { + { leaf = "global", enabled = true, speed = 10, bezier = "default" }, + { leaf = "border", enabled = true, speed = 5.39, bezier = "easeOutQuint" }, + { leaf = "windows", enabled = true, speed = 4.79, bezier = "easeOutQuint" }, + { leaf = "windowsIn", enabled = true, speed = 4.1, bezier = "easeOutQuint", style = "popin 87%" }, + { leaf = "windowsOut", enabled = true, speed = 1.49, bezier = "linear", style = "popin 87%" }, + { leaf = "fade", enabled = true, speed = 3.03, bezier = "quick" }, + { leaf = "layers", enabled = true, speed = 3.81, bezier = "easeOutQuint" }, + { leaf = "workspaces", enabled = true, speed = 1.94, bezier = "almostLinear", style = "fade" }, +} +for _, animation in ipairs(animations) do + hl.animation(animation) +end + +-- --------------------------------------------------------------------------- +-- Window rules — float + centre the onboarding popups (kitty --class …). +-- --------------------------------------------------------------------------- +hl.window_rule({ name = "bos-keybinds", match = { class = "^(bos-keybinds)$" }, float = true, size = { 760, 720 } }) +hl.window_rule({ name = "bos-welcome", match = { class = "^(bos-welcome)$" }, float = true, size = { 700, 560 } }) + +-- --------------------------------------------------------------------------- +-- Environment (vendor-neutral; no GPU-specific vars so it works on Intel/AMD). +-- --------------------------------------------------------------------------- +hl.env("XCURSOR_SIZE", "24") +hl.env("HYPRCURSOR_SIZE", "24") +hl.env("XCURSOR_THEME", "Bibata-Modern-Ice") +hl.env("MOZ_ENABLE_WAYLAND", "1") +hl.env("QT_QPA_PLATFORM", "wayland;xcb") +hl.env("QT_QPA_PLATFORMTHEME", "qt5ct") +hl.env("QT_WAYLAND_DISABLE_WINDOWDECORATION", "1") +hl.env("SDL_VIDEODRIVER", "wayland") +hl.env("ELECTRON_OZONE_PLATFORM_HINT", "auto") +hl.env("_JAVA_AWT_WM_NONREPARENTING", "1") + +-- kitty sets its own background_opacity (see kitty.conf), so the global blur +-- above blurs behind the terminal while keeping text fully opaque. + +-- --------------------------------------------------------------------------- +-- Standard BOS keybinds (SUPER = mod). +-- --------------------------------------------------------------------------- +-- Apps / window management +hl.bind(mod .. " + RETURN", hl.dsp.exec_cmd("kitty")) +hl.bind(mod .. " + BACKSPACE", hl.dsp.window.close()) +hl.bind(mod .. " + SPACE", hl.dsp.exec_cmd("breadbox")) +hl.bind(mod .. " + E", hl.dsp.exec_cmd("nautilus")) +hl.bind(mod .. " + B", hl.dsp.exec_cmd("zen-browser")) +hl.bind(mod .. " + U", hl.dsp.exec_cmd("breadpad")) +hl.bind(mod .. " + M", hl.dsp.exec_cmd("breadman")) +hl.bind(mod .. " + comma", hl.dsp.exec_cmd("bos-settings")) +hl.bind(mod .. " + slash", hl.dsp.exec_cmd("bos-keybinds")) +hl.bind(mod .. " + L", hl.dsp.exec_cmd("loginctl lock-session")) +hl.bind(mod .. " + F", hl.dsp.window.fullscreen({ action = "toggle" })) +hl.bind(mod .. " + V", hl.dsp.window.float({ action = "toggle" })) +hl.bind(mod .. " + SHIFT + V", hl.dsp.exec_cmd([[bash -c 'cliphist list | fzf --reverse --prompt="Clipboard > " | cliphist decode | wl-copy']])) +hl.bind(mod .. " + T", hl.dsp.layout("togglesplit")) +hl.bind(mod .. " + Tab", hl.dsp.focus({ urgent_or_last = true })) +hl.bind(mod .. " + N", hl.dsp.exit()) + +-- Screenshots (grim + slurp + wl-clipboard) +hl.bind(mod .. " + SHIFT + S", hl.dsp.exec_cmd([[bash -c 'mkdir -p ~/Pictures/Screenshots && grim -g "$(slurp)" ~/Pictures/Screenshots/$(date +%Y%m%d-%H%M%S).png']])) +hl.bind(mod .. " + SHIFT + C", hl.dsp.exec_cmd([[bash -c 'grim -g "$(slurp)" - | wl-copy']])) +hl.bind(mod .. " + SHIFT + P", hl.dsp.exec_cmd([[bash -c 'mkdir -p ~/Pictures/Screenshots && grim ~/Pictures/Screenshots/$(date +%Y%m%d-%H%M%S).png']])) + +-- Focus (directional) +hl.bind(mod .. " + left", hl.dsp.focus({ direction = "left" })) +hl.bind(mod .. " + right", hl.dsp.focus({ direction = "right" })) +hl.bind(mod .. " + up", hl.dsp.focus({ direction = "up" })) +hl.bind(mod .. " + down", hl.dsp.focus({ direction = "down" })) + +-- Move window (directional, vim keys) +hl.bind(mod .. " + SHIFT + h", hl.dsp.window.move({ direction = "left" })) +hl.bind(mod .. " + SHIFT + j", hl.dsp.window.move({ direction = "down" })) +hl.bind(mod .. " + SHIFT + k", hl.dsp.window.move({ direction = "up" })) +hl.bind(mod .. " + SHIFT + l", hl.dsp.window.move({ direction = "right" })) + +-- Resize active window (arrows) +hl.bind(mod .. " + SHIFT + right", hl.dsp.window.resize({ x = 30, y = 0, relative = true }), { repeating = true }) +hl.bind(mod .. " + SHIFT + left", hl.dsp.window.resize({ x = -30, y = 0, relative = true }), { repeating = true }) +hl.bind(mod .. " + SHIFT + up", hl.dsp.window.resize({ x = 0, y = -30, relative = true }), { repeating = true }) +hl.bind(mod .. " + SHIFT + down", hl.dsp.window.resize({ x = 0, y = 30, relative = true }), { repeating = true }) + +-- Workspaces 1–10 (0 = workspace 10) +for i = 1, 10 do + local key = tostring(i % 10) + hl.bind(mod .. " + " .. key, hl.dsp.focus({ workspace = i })) + hl.bind(mod .. " + SHIFT + " .. key, hl.dsp.window.move({ workspace = i })) +end + +-- Workspace cycling +hl.bind(mod .. " + bracketright", hl.dsp.focus({ workspace = "e+1" })) +hl.bind(mod .. " + bracketleft", hl.dsp.focus({ workspace = "e-1" })) +hl.bind(mod .. " + SHIFT + bracketright", hl.dsp.window.move({ workspace = "e+1" })) +hl.bind(mod .. " + SHIFT + bracketleft", hl.dsp.window.move({ workspace = "e-1" })) + +-- Mouse +hl.bind(mod .. " + mouse_down", hl.dsp.focus({ workspace = "e+1" })) +hl.bind(mod .. " + mouse_up", hl.dsp.focus({ workspace = "e-1" })) +hl.bind(mod .. " + mouse:272", hl.dsp.window.drag(), { mouse = true }) +hl.bind(mod .. " + mouse:273", hl.dsp.window.resize(), { mouse = true }) + +-- Media / hardware keys (work locked, i.e. on the lock screen too) +hl.bind("XF86AudioRaiseVolume", hl.dsp.exec_cmd("wpctl set-volume -l 1 @DEFAULT_AUDIO_SINK@ 5%+"), { locked = true, repeating = true }) +hl.bind("XF86AudioLowerVolume", hl.dsp.exec_cmd("wpctl set-volume @DEFAULT_AUDIO_SINK@ 5%-"), { locked = true, repeating = true }) +hl.bind("XF86AudioMute", hl.dsp.exec_cmd("wpctl set-mute @DEFAULT_AUDIO_SINK@ toggle"), { locked = true }) +hl.bind("XF86AudioMicMute", hl.dsp.exec_cmd("wpctl set-mute @DEFAULT_AUDIO_SOURCE@ toggle"), { locked = true }) +hl.bind("XF86MonBrightnessUp", hl.dsp.exec_cmd("brightnessctl -e4 -n2 set 5%+"), { locked = true, repeating = true }) +hl.bind("XF86MonBrightnessDown", hl.dsp.exec_cmd("brightnessctl -e4 -n2 set 5%-"), { locked = true, repeating = true }) +hl.bind("XF86AudioNext", hl.dsp.exec_cmd("playerctl next"), { locked = true }) +hl.bind("XF86AudioPrev", hl.dsp.exec_cmd("playerctl previous"), { locked = true }) +hl.bind("XF86AudioPlay", hl.dsp.exec_cmd("playerctl play-pause"), { locked = true }) + +-- --------------------------------------------------------------------------- +-- Autostart. polkit agent + the bread ecosystem + idle daemon + wallpaper. +-- (bos-live-setup appends the live-installer launch below this on the ISO.) +-- --------------------------------------------------------------------------- +hl.on("hyprland.start", function() + local startup = { + -- Generate the shared bread GUI stylesheet first, so breadbar/breadbox/ + -- bos-settings load it on start (they also live-reload if it changes). + "bread-theme generate", + -- Global dark theme: GTK4/libadwaita + GTK3 theme + icon + cursor. + "gsettings set org.gnome.desktop.interface color-scheme prefer-dark", + "gsettings set org.gnome.desktop.interface gtk-theme Adwaita-dark", + "gsettings set org.gnome.desktop.interface icon-theme Papirus-Dark", + "gsettings set org.gnome.desktop.interface cursor-theme Bibata-Modern-Ice", + "gsettings set org.gnome.desktop.interface cursor-size 24", + -- Clipboard history daemon (feeds SUPER+V history picker via wl-paste). + "wl-paste --type text --watch cliphist store", + "/usr/lib/polkit-gnome/polkit-gnome-authentication-agent-1", + "awww-daemon", + -- set the default wallpaper once the daemon is up (retry until ready) + [[bash -c 'until awww img /usr/share/backgrounds/bos/bread-background.png 2>/dev/null; do sleep 0.3; done']], + "breadd", + "breadbar", + "breadbox-sync", + "hypridle", + -- first-boot onboarding (self-gates after the first run) + "bos-welcome", + } + for _, cmd in ipairs(startup) do + hl.dispatch(hl.dsp.exec_cmd(cmd)) + end +end) diff --git a/iso/airootfs/etc/skel/.config/hypr/hyprlock.conf b/iso/airootfs/etc/skel/.config/hypr/hyprlock.conf new file mode 100644 index 0000000..30903df --- /dev/null +++ b/iso/airootfs/etc/skel/.config/hypr/hyprlock.conf @@ -0,0 +1,28 @@ +# Lock screen (hyprlock). Solid dark background (no runtime-generated wallpaper +# dependency), accent matched to the Hyprland border colour. +general { + hide_cursor = true +} + +background { + monitor = + color = rgba(46, 52, 64, 1.0) + blur_passes = 0 +} + +input-field { + monitor = + size = 20%, 5% + outline_thickness = 3 + inner_color = rgba(0, 0, 0, 0.2) + outer_color = rgba(136, 192, 208, 0.8) + check_color = rgba(120, 220, 140, 0.95) + fail_color = rgba(255, 90, 90, 0.95) + font_color = rgba(255, 255, 255, 0.95) + fade_on_empty = false + rounding = 12 + placeholder_text = Password… + position = 0, -20 + halign = center + valign = center +} diff --git a/iso/airootfs/etc/skel/.config/hypr/keybinds.conf b/iso/airootfs/etc/skel/.config/hypr/keybinds.conf deleted file mode 100644 index 7cf8cdd..0000000 --- a/iso/airootfs/etc/skel/.config/hypr/keybinds.conf +++ /dev/null @@ -1,58 +0,0 @@ -$mod = SUPER - -# App launchers -bind = $mod, Space, exec, breadbox -bind = $mod, N, exec, breadpad -bind = $mod, M, exec, breadman -bind = $mod, S, exec, bos-settings - -# Core -bind = $mod, Return, exec, foot -bind = $mod, Q, killactive -bind = $mod SHIFT, E, exit -bind = $mod, F, fullscreen - -# Focus -bind = $mod, H, movefocus, l -bind = $mod, L, movefocus, r -bind = $mod, K, movefocus, u -bind = $mod, J, movefocus, d - -# Move windows -bind = $mod SHIFT, H, movewindow, l -bind = $mod SHIFT, L, movewindow, r -bind = $mod SHIFT, K, movewindow, u -bind = $mod SHIFT, J, movewindow, d - -# Workspaces -bind = $mod, 1, workspace, 1 -bind = $mod, 2, workspace, 2 -bind = $mod, 3, workspace, 3 -bind = $mod, 4, workspace, 4 -bind = $mod, 5, workspace, 5 - -bind = $mod SHIFT, 1, movetoworkspace, 1 -bind = $mod SHIFT, 2, movetoworkspace, 2 -bind = $mod SHIFT, 3, movetoworkspace, 3 -bind = $mod SHIFT, 4, movetoworkspace, 4 -bind = $mod SHIFT, 5, movetoworkspace, 5 - -# Scroll through workspaces -bind = $mod, mouse_down, workspace, e+1 -bind = $mod, mouse_up, workspace, e-1 - -# Mouse binds -bindm = $mod, mouse:272, movewindow -bindm = $mod, mouse:273, resizewindow - -# Volume -bind = , XF86AudioRaiseVolume, exec, wpctl set-volume @DEFAULT_AUDIO_SINK@ 5%+ -bind = , XF86AudioLowerVolume, exec, wpctl set-volume @DEFAULT_AUDIO_SINK@ 5%- -bind = , XF86AudioMute, exec, wpctl set-mute @DEFAULT_AUDIO_SINK@ toggle - -# Brightness -bind = , XF86MonBrightnessUp, exec, brightnessctl set 5%+ -bind = , XF86MonBrightnessDown, exec, brightnessctl set 5%- - -# Screenshot -bind = , Print, exec, grimblast copy area diff --git a/iso/airootfs/etc/skel/.config/kitty/kitty.conf b/iso/airootfs/etc/skel/.config/kitty/kitty.conf new file mode 100644 index 0000000..ddc734b --- /dev/null +++ b/iso/airootfs/etc/skel/.config/kitty/kitty.conf @@ -0,0 +1,14 @@ +# BOS kitty config. +# Translucent background so Hyprland's blur shows through behind the terminal, +# while text stays fully opaque. Colours are left to kitty's default / pywal. +# 0.6 matches the reference laptop; the actual blur is supplied by Hyprland's +# decoration:blur (kitty's own background_blur is macOS-only). +background_opacity 0.6 + +font_family JetBrains Mono +font_size 11.0 +cursor_shape beam +scrollback_lines 10000 +enable_audio_bell no +window_padding_width 8 +confirm_os_window_close 0 diff --git a/iso/airootfs/etc/skel/.config/mimeapps.list b/iso/airootfs/etc/skel/.config/mimeapps.list new file mode 100644 index 0000000..58c48fe --- /dev/null +++ b/iso/airootfs/etc/skel/.config/mimeapps.list @@ -0,0 +1,51 @@ +# Default applications for common file types. Without this, freshly installed +# BOS has no handler registered for images/video/text/etc., so opening a file +# from nautilus does nothing. Maps to the apps shipped in packages.x86_64. +[Default Applications] +# Images -> Loupe +image/png=org.gnome.Loupe.desktop +image/jpeg=org.gnome.Loupe.desktop +image/gif=org.gnome.Loupe.desktop +image/webp=org.gnome.Loupe.desktop +image/bmp=org.gnome.Loupe.desktop +image/tiff=org.gnome.Loupe.desktop +image/svg+xml=org.gnome.Loupe.desktop + +# Audio/Video -> VLC +audio/mpeg=vlc.desktop +audio/flac=vlc.desktop +audio/ogg=vlc.desktop +audio/x-wav=vlc.desktop +audio/aac=vlc.desktop +video/mp4=vlc.desktop +video/x-matroska=vlc.desktop +video/webm=vlc.desktop +video/quicktime=vlc.desktop +video/x-msvideo=vlc.desktop + +# Plain text / source -> GNOME Text Editor +text/plain=org.gnome.TextEditor.desktop +text/markdown=org.gnome.TextEditor.desktop +application/x-shellscript=org.gnome.TextEditor.desktop +application/json=org.gnome.TextEditor.desktop +application/toml=org.gnome.TextEditor.desktop +text/x-readme=org.gnome.TextEditor.desktop + +# Documents / web -> Zen (PDF + HTML) +application/pdf=zen.desktop +text/html=zen.desktop +x-scheme-handler/http=zen.desktop +x-scheme-handler/https=zen.desktop + +# Archives -> File Roller +application/zip=org.gnome.FileRoller.desktop +application/x-tar=org.gnome.FileRoller.desktop +application/gzip=org.gnome.FileRoller.desktop +application/x-7z-compressed=org.gnome.FileRoller.desktop +application/x-rar=org.gnome.FileRoller.desktop +application/vnd.rar=org.gnome.FileRoller.desktop +application/x-xz=org.gnome.FileRoller.desktop +application/x-bzip2=org.gnome.FileRoller.desktop + +# Directories -> Nautilus +inode/directory=org.gnome.Nautilus.desktop diff --git a/iso/airootfs/etc/skel/.config/qt5ct/qt5ct.conf b/iso/airootfs/etc/skel/.config/qt5ct/qt5ct.conf new file mode 100644 index 0000000..003be3a --- /dev/null +++ b/iso/airootfs/etc/skel/.config/qt5ct/qt5ct.conf @@ -0,0 +1,25 @@ +[Appearance] +style=Fusion +color_scheme_path=/usr/share/qt5ct/colors/darker.conf +custom_palette=true +standard_dialogs=default +icon_theme=Papirus-Dark + +[Fonts] +fixed=@Variant(\0\0\0@\0\0\0\x12JetBrains Mono\0\0\0\0\0\0\0\0\0\0\0\0\0\xa0\0\x64\xff\xff\xff\xff) +general=@Variant(\0\0\0@\0\0\0\x16Noto Sans\0\0\0\0\0\0\0\0\0\0\0\0\x11\0\x64\xff\xff\xff\xff) + +[Interface] +activate_item_on_single_click=1 +buttonbox_layout=0 +cursor_flash_time=1000 +dialog_buttons_have_icons=1 +double_click_interval=400 +gui_effects=@Invalid() +keyboard_scheme=2 +menus_have_icons=true +show_shortcuts_in_context_menus=true +stylesheets=@Invalid() +toolbutton_style=4 +underline_shortcut=1 +wheel_scroll_lines=3 diff --git a/iso/airootfs/etc/skel/.config/qt6ct/qt6ct.conf b/iso/airootfs/etc/skel/.config/qt6ct/qt6ct.conf new file mode 100644 index 0000000..650258d --- /dev/null +++ b/iso/airootfs/etc/skel/.config/qt6ct/qt6ct.conf @@ -0,0 +1,25 @@ +[Appearance] +style=Fusion +color_scheme_path=/usr/share/qt6ct/colors/darker.conf +custom_palette=true +standard_dialogs=default +icon_theme=Papirus-Dark + +[Fonts] +fixed=@Variant(\0\0\0@\0\0\0\x12JetBrains Mono\0\0\0\0\0\0\0\0\0\0\0\0\0\xa0\0\x64\xff\xff\xff\xff) +general=@Variant(\0\0\0@\0\0\0\x16Noto Sans\0\0\0\0\0\0\0\0\0\0\0\0\x11\0\x64\xff\xff\xff\xff) + +[Interface] +activate_item_on_single_click=1 +buttonbox_layout=0 +cursor_flash_time=1000 +dialog_buttons_have_icons=1 +double_click_interval=400 +gui_effects=@Invalid() +keyboard_scheme=2 +menus_have_icons=true +show_shortcuts_in_context_menus=true +stylesheets=@Invalid() +toolbutton_style=4 +underline_shortcut=1 +wheel_scroll_lines=3 diff --git a/iso/airootfs/etc/skel/.p10k.zsh b/iso/airootfs/etc/skel/.p10k.zsh new file mode 100644 index 0000000..04e3a41 --- /dev/null +++ b/iso/airootfs/etc/skel/.p10k.zsh @@ -0,0 +1,1745 @@ +# Generated by Powerlevel10k configuration wizard on 2026-05-09 at 11:13 AWST. +# Based on romkatv/powerlevel10k/config/p10k-classic.zsh, checksum 57870. +# Wizard options: nerdfont-v3 + powerline, small icons, classic, unicode, light, +# angled separators, sharp heads, sharp tails, 2 lines, disconnected, full frame, +# sparse, many icons, concise, transient_prompt, instant_prompt=verbose. +# Type `p10k configure` to generate another config. +# +# Config for Powerlevel10k with classic powerline prompt style. Type `p10k configure` to generate +# your own config based on it. +# +# Tip: Looking for a nice color? Here's a one-liner to print colormap. +# +# for i in {0..255}; do print -Pn "%K{$i} %k%F{$i}${(l:3::0:)i}%f " ${${(M)$((i%6)):#3}:+$'\n'}; done + +# Temporarily change options. +'builtin' 'local' '-a' 'p10k_config_opts' +[[ ! -o 'aliases' ]] || p10k_config_opts+=('aliases') +[[ ! -o 'sh_glob' ]] || p10k_config_opts+=('sh_glob') +[[ ! -o 'no_brace_expand' ]] || p10k_config_opts+=('no_brace_expand') +'builtin' 'setopt' 'no_aliases' 'no_sh_glob' 'brace_expand' + +() { + emulate -L zsh -o extended_glob + + # Unset all configuration options. This allows you to apply configuration changes without + # restarting zsh. Edit ~/.p10k.zsh and type `source ~/.p10k.zsh`. + unset -m '(POWERLEVEL9K_*|DEFAULT_USER)~POWERLEVEL9K_GITSTATUS_DIR' + + # Zsh >= 5.1 is required. + [[ $ZSH_VERSION == (5.<1->*|<6->.*) ]] || return + + # The list of segments shown on the left. Fill it with the most important segments. + typeset -g POWERLEVEL9K_LEFT_PROMPT_ELEMENTS=( + # =========================[ Line #1 ]========================= + os_icon # os identifier + dir # current directory + vcs # git status + # =========================[ Line #2 ]========================= + newline # \n + # prompt_char # prompt symbol + ) + + # The list of segments shown on the right. Fill it with less important segments. + # Right prompt on the last prompt line (where you are typing your commands) gets + # automatically hidden when the input line reaches it. Right prompt above the + # last prompt line gets hidden if it would overlap with left prompt. + typeset -g POWERLEVEL9K_RIGHT_PROMPT_ELEMENTS=( + # =========================[ Line #1 ]========================= + status # exit code of the last command + command_execution_time # duration of the last command + background_jobs # presence of background jobs + direnv # direnv status (https://direnv.net/) + asdf # asdf version manager (https://github.com/asdf-vm/asdf) + virtualenv # python virtual environment (https://docs.python.org/3/library/venv.html) + anaconda # conda environment (https://conda.io/) + pyenv # python environment (https://github.com/pyenv/pyenv) + goenv # go environment (https://github.com/syndbg/goenv) + nodenv # node.js version from nodenv (https://github.com/nodenv/nodenv) + nvm # node.js version from nvm (https://github.com/nvm-sh/nvm) + nodeenv # node.js environment (https://github.com/ekalinin/nodeenv) + # node_version # node.js version + # go_version # go version (https://golang.org) + # rust_version # rustc version (https://www.rust-lang.org) + # dotnet_version # .NET version (https://dotnet.microsoft.com) + # php_version # php version (https://www.php.net/) + # laravel_version # laravel php framework version (https://laravel.com/) + # java_version # java version (https://www.java.com/) + # package # name@version from package.json (https://docs.npmjs.com/files/package.json) + rbenv # ruby version from rbenv (https://github.com/rbenv/rbenv) + rvm # ruby version from rvm (https://rvm.io) + fvm # flutter version management (https://github.com/leoafarias/fvm) + luaenv # lua version from luaenv (https://github.com/cehoffman/luaenv) + jenv # java version from jenv (https://github.com/jenv/jenv) + plenv # perl version from plenv (https://github.com/tokuhirom/plenv) + perlbrew # perl version from perlbrew (https://github.com/gugod/App-perlbrew) + phpenv # php version from phpenv (https://github.com/phpenv/phpenv) + scalaenv # scala version from scalaenv (https://github.com/scalaenv/scalaenv) + haskell_stack # haskell version from stack (https://haskellstack.org/) + kubecontext # current kubernetes context (https://kubernetes.io/) + terraform # terraform workspace (https://www.terraform.io) + # terraform_version # terraform version (https://www.terraform.io) + aws # aws profile (https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-profiles.html) + aws_eb_env # aws elastic beanstalk environment (https://aws.amazon.com/elasticbeanstalk/) + azure # azure account name (https://docs.microsoft.com/en-us/cli/azure) + gcloud # google cloud cli account and project (https://cloud.google.com/) + google_app_cred # google application credentials (https://cloud.google.com/docs/authentication/production) + toolbox # toolbox name (https://github.com/containers/toolbox) + context # user@hostname + nordvpn # nordvpn connection status, linux only (https://nordvpn.com/) + ranger # ranger shell (https://github.com/ranger/ranger) + yazi # yazi shell (https://github.com/sxyazi/yazi) + nnn # nnn shell (https://github.com/jarun/nnn) + lf # lf shell (https://github.com/gokcehan/lf) + xplr # xplr shell (https://github.com/sayanarijit/xplr) + vim_shell # vim shell indicator (:sh) + midnight_commander # midnight commander shell (https://midnight-commander.org/) + nix_shell # nix shell (https://nixos.org/nixos/nix-pills/developing-with-nix-shell.html) + chezmoi_shell # chezmoi shell (https://www.chezmoi.io/) + vi_mode # vi mode (you don't need this if you've enabled prompt_char) + # vpn_ip # virtual private network indicator + # load # CPU load + # disk_usage # disk usage + # ram # free RAM + # swap # used swap + todo # todo items (https://github.com/todotxt/todo.txt-cli) + timewarrior # timewarrior tracking status (https://timewarrior.net/) + taskwarrior # taskwarrior task count (https://taskwarrior.org/) + per_directory_history # Oh My Zsh per-directory-history local/global indicator + # cpu_arch # CPU architecture + # time # current time + # =========================[ Line #2 ]========================= + newline # \n + # ip # ip address and bandwidth usage for a specified network interface + # public_ip # public IP address + # proxy # system-wide http/https/ftp proxy + # battery # internal battery + # wifi # wifi speed + # example # example user-defined segment (see prompt_example function below) + ) + + # Defines character set used by powerlevel10k. It's best to let `p10k configure` set it for you. + typeset -g POWERLEVEL9K_MODE=nerdfont-v3 + # When set to `moderate`, some icons will have an extra space after them. This is meant to avoid + # icon overlap when using non-monospace fonts. When set to `none`, spaces are not added. + typeset -g POWERLEVEL9K_ICON_PADDING=none + + # When set to true, icons appear before content on both sides of the prompt. When set + # to false, icons go after content. If empty or not set, icons go before content in the left + # prompt and after content in the right prompt. + # + # You can also override it for a specific segment: + # + # POWERLEVEL9K_STATUS_ICON_BEFORE_CONTENT=false + # + # Or for a specific segment in specific state: + # + # POWERLEVEL9K_DIR_NOT_WRITABLE_ICON_BEFORE_CONTENT=false + typeset -g POWERLEVEL9K_ICON_BEFORE_CONTENT= + + # Add an empty line before each prompt. + typeset -g POWERLEVEL9K_PROMPT_ADD_NEWLINE=true + + # Connect left prompt lines with these symbols. You'll probably want to use the same color + # as POWERLEVEL9K_MULTILINE_FIRST_PROMPT_GAP_FOREGROUND below. + typeset -g POWERLEVEL9K_MULTILINE_FIRST_PROMPT_PREFIX='%242F╭─' + typeset -g POWERLEVEL9K_MULTILINE_NEWLINE_PROMPT_PREFIX='%242F├─' + typeset -g POWERLEVEL9K_MULTILINE_LAST_PROMPT_PREFIX='%242F╰─' + # Connect right prompt lines with these symbols. + typeset -g POWERLEVEL9K_MULTILINE_FIRST_PROMPT_SUFFIX='%242F─╮' + typeset -g POWERLEVEL9K_MULTILINE_NEWLINE_PROMPT_SUFFIX='%242F─┤' + typeset -g POWERLEVEL9K_MULTILINE_LAST_PROMPT_SUFFIX='%242F─╯' + + # Filler between left and right prompt on the first prompt line. You can set it to ' ', '·' or + # '─'. The last two make it easier to see the alignment between left and right prompt and to + # separate prompt from command output. You might want to set POWERLEVEL9K_PROMPT_ADD_NEWLINE=false + # for more compact prompt if using this option. + typeset -g POWERLEVEL9K_MULTILINE_FIRST_PROMPT_GAP_CHAR=' ' + typeset -g POWERLEVEL9K_MULTILINE_FIRST_PROMPT_GAP_BACKGROUND= + typeset -g POWERLEVEL9K_MULTILINE_NEWLINE_PROMPT_GAP_BACKGROUND= + if [[ $POWERLEVEL9K_MULTILINE_FIRST_PROMPT_GAP_CHAR != ' ' ]]; then + # The color of the filler. You'll probably want to match the color of POWERLEVEL9K_MULTILINE + # ornaments defined above. + typeset -g POWERLEVEL9K_MULTILINE_FIRST_PROMPT_GAP_FOREGROUND=242 + # Start filler from the edge of the screen if there are no left segments on the first line. + typeset -g POWERLEVEL9K_EMPTY_LINE_LEFT_PROMPT_FIRST_SEGMENT_END_SYMBOL='%{%}' + # End filler on the edge of the screen if there are no right segments on the first line. + typeset -g POWERLEVEL9K_EMPTY_LINE_RIGHT_PROMPT_FIRST_SEGMENT_START_SYMBOL='%{%}' + fi + + # Default background color. + typeset -g POWERLEVEL9K_BACKGROUND=238 + + # Separator between same-color segments on the left. + typeset -g POWERLEVEL9K_LEFT_SUBSEGMENT_SEPARATOR='%246F\uE0B1' + # Separator between same-color segments on the right. + typeset -g POWERLEVEL9K_RIGHT_SUBSEGMENT_SEPARATOR='%246F\uE0B3' + # Separator between different-color segments on the left. + typeset -g POWERLEVEL9K_LEFT_SEGMENT_SEPARATOR='\uE0B0' + # Separator between different-color segments on the right. + typeset -g POWERLEVEL9K_RIGHT_SEGMENT_SEPARATOR='\uE0B2' + # To remove a separator between two segments, add "_joined" to the second segment name. + # For example: POWERLEVEL9K_RIGHT_PROMPT_ELEMENTS=(os_icon context_joined) + + # The right end of left prompt. + typeset -g POWERLEVEL9K_LEFT_PROMPT_LAST_SEGMENT_END_SYMBOL='\uE0B0' + # The left end of right prompt. + typeset -g POWERLEVEL9K_RIGHT_PROMPT_FIRST_SEGMENT_START_SYMBOL='\uE0B2' + # The left end of left prompt. + typeset -g POWERLEVEL9K_LEFT_PROMPT_FIRST_SEGMENT_START_SYMBOL='\uE0B2' + # The right end of right prompt. + typeset -g POWERLEVEL9K_RIGHT_PROMPT_LAST_SEGMENT_END_SYMBOL='\uE0B0' + # Left prompt terminator for lines without any segments. + typeset -g POWERLEVEL9K_EMPTY_LINE_LEFT_PROMPT_LAST_SEGMENT_END_SYMBOL= + + #################################[ os_icon: os identifier ]################################## + # OS identifier color. + typeset -g POWERLEVEL9K_OS_ICON_FOREGROUND=255 + # Custom icon. + # typeset -g POWERLEVEL9K_OS_ICON_CONTENT_EXPANSION='⭐' + + ################################[ prompt_char: prompt symbol ]################################ + # Transparent background. + typeset -g POWERLEVEL9K_PROMPT_CHAR_BACKGROUND= + # Green prompt symbol if the last command succeeded. + typeset -g POWERLEVEL9K_PROMPT_CHAR_OK_{VIINS,VICMD,VIVIS,VIOWR}_FOREGROUND=76 + # Red prompt symbol if the last command failed. + typeset -g POWERLEVEL9K_PROMPT_CHAR_ERROR_{VIINS,VICMD,VIVIS,VIOWR}_FOREGROUND=196 + # Default prompt symbol. + typeset -g POWERLEVEL9K_PROMPT_CHAR_{OK,ERROR}_VIINS_CONTENT_EXPANSION='❯' + # Prompt symbol in command vi mode. + typeset -g POWERLEVEL9K_PROMPT_CHAR_{OK,ERROR}_VICMD_CONTENT_EXPANSION='❮' + # Prompt symbol in visual vi mode. + typeset -g POWERLEVEL9K_PROMPT_CHAR_{OK,ERROR}_VIVIS_CONTENT_EXPANSION='V' + # Prompt symbol in overwrite vi mode. + typeset -g POWERLEVEL9K_PROMPT_CHAR_{OK,ERROR}_VIOWR_CONTENT_EXPANSION='▶' + typeset -g POWERLEVEL9K_PROMPT_CHAR_OVERWRITE_STATE=true + # No line terminator if prompt_char is the last segment. + typeset -g POWERLEVEL9K_PROMPT_CHAR_LEFT_PROMPT_LAST_SEGMENT_END_SYMBOL= + # No line introducer if prompt_char is the first segment. + typeset -g POWERLEVEL9K_PROMPT_CHAR_LEFT_PROMPT_FIRST_SEGMENT_START_SYMBOL= + # No surrounding whitespace. + typeset -g POWERLEVEL9K_PROMPT_CHAR_LEFT_{LEFT,RIGHT}_WHITESPACE= + + ##################################[ dir: current directory ]################################## + # Default current directory color. + typeset -g POWERLEVEL9K_DIR_FOREGROUND=31 + # If directory is too long, shorten some of its segments to the shortest possible unique + # prefix. The shortened directory can be tab-completed to the original. + typeset -g POWERLEVEL9K_SHORTEN_STRATEGY=truncate_to_unique + # Replace removed segment suffixes with this symbol. + typeset -g POWERLEVEL9K_SHORTEN_DELIMITER= + # Color of the shortened directory segments. + typeset -g POWERLEVEL9K_DIR_SHORTENED_FOREGROUND=103 + # Color of the anchor directory segments. Anchor segments are never shortened. The first + # segment is always an anchor. + typeset -g POWERLEVEL9K_DIR_ANCHOR_FOREGROUND=39 + # Display anchor directory segments in bold. + typeset -g POWERLEVEL9K_DIR_ANCHOR_BOLD=true + # Don't shorten directories that contain any of these files. They are anchors. + local anchor_files=( + .bzr + .citc + .git + .hg + .node-version + .python-version + .go-version + .ruby-version + .lua-version + .java-version + .perl-version + .php-version + .tool-versions + .mise.toml + .shorten_folder_marker + .svn + .terraform + CVS + Cargo.toml + composer.json + go.mod + package.json + stack.yaml + ) + typeset -g POWERLEVEL9K_SHORTEN_FOLDER_MARKER="(${(j:|:)anchor_files})" + # If set to "first" ("last"), remove everything before the first (last) subdirectory that contains + # files matching $POWERLEVEL9K_SHORTEN_FOLDER_MARKER. For example, when the current directory is + # /foo/bar/git_repo/nested_git_repo/baz, prompt will display git_repo/nested_git_repo/baz (first) + # or nested_git_repo/baz (last). This assumes that git_repo and nested_git_repo contain markers + # and other directories don't. + # + # Optionally, "first" and "last" can be followed by ":" where is an integer. + # This moves the truncation point to the right (positive offset) or to the left (negative offset) + # relative to the marker. Plain "first" and "last" are equivalent to "first:0" and "last:0" + # respectively. + typeset -g POWERLEVEL9K_DIR_TRUNCATE_BEFORE_MARKER=false + # Don't shorten this many last directory segments. They are anchors. + typeset -g POWERLEVEL9K_SHORTEN_DIR_LENGTH=1 + # Shorten directory if it's longer than this even if there is space for it. The value can + # be either absolute (e.g., '80') or a percentage of terminal width (e.g, '50%'). If empty, + # directory will be shortened only when prompt doesn't fit or when other parameters demand it + # (see POWERLEVEL9K_DIR_MIN_COMMAND_COLUMNS and POWERLEVEL9K_DIR_MIN_COMMAND_COLUMNS_PCT below). + # If set to `0`, directory will always be shortened to its minimum length. + typeset -g POWERLEVEL9K_DIR_MAX_LENGTH=80 + # When `dir` segment is on the last prompt line, try to shorten it enough to leave at least this + # many columns for typing commands. + typeset -g POWERLEVEL9K_DIR_MIN_COMMAND_COLUMNS=40 + # When `dir` segment is on the last prompt line, try to shorten it enough to leave at least + # COLUMNS * POWERLEVEL9K_DIR_MIN_COMMAND_COLUMNS_PCT * 0.01 columns for typing commands. + typeset -g POWERLEVEL9K_DIR_MIN_COMMAND_COLUMNS_PCT=50 + # If set to true, embed a hyperlink into the directory. Useful for quickly + # opening a directory in the file manager simply by clicking the link. + # Can also be handy when the directory is shortened, as it allows you to see + # the full directory that was used in previous commands. + typeset -g POWERLEVEL9K_DIR_HYPERLINK=false + + # Enable special styling for non-writable and non-existent directories. See POWERLEVEL9K_LOCK_ICON + # and POWERLEVEL9K_DIR_CLASSES below. + typeset -g POWERLEVEL9K_DIR_SHOW_WRITABLE=v3 + + # The default icon shown next to non-writable and non-existent directories when + # POWERLEVEL9K_DIR_SHOW_WRITABLE is set to v3. + # typeset -g POWERLEVEL9K_LOCK_ICON='⭐' + + # POWERLEVEL9K_DIR_CLASSES allows you to specify custom icons and colors for different + # directories. It must be an array with 3 * N elements. Each triplet consists of: + # + # 1. A pattern against which the current directory ($PWD) is matched. Matching is done with + # extended_glob option enabled. + # 2. Directory class for the purpose of styling. + # 3. An empty string. + # + # Triplets are tried in order. The first triplet whose pattern matches $PWD wins. + # + # If POWERLEVEL9K_DIR_SHOW_WRITABLE is set to v3, non-writable and non-existent directories + # acquire class suffix _NOT_WRITABLE and NON_EXISTENT respectively. + # + # For example, given these settings: + # + # typeset -g POWERLEVEL9K_DIR_CLASSES=( + # '~/work(|/*)' WORK '' + # '~(|/*)' HOME '' + # '*' DEFAULT '') + # + # Whenever the current directory is ~/work or a subdirectory of ~/work, it gets styled with one + # of the following classes depending on its writability and existence: WORK, WORK_NOT_WRITABLE or + # WORK_NON_EXISTENT. + # + # Simply assigning classes to directories doesn't have any visible effects. It merely gives you an + # option to define custom colors and icons for different directory classes. + # + # # Styling for WORK. + # typeset -g POWERLEVEL9K_DIR_WORK_VISUAL_IDENTIFIER_EXPANSION='⭐' + # typeset -g POWERLEVEL9K_DIR_WORK_FOREGROUND=31 + # typeset -g POWERLEVEL9K_DIR_WORK_SHORTENED_FOREGROUND=103 + # typeset -g POWERLEVEL9K_DIR_WORK_ANCHOR_FOREGROUND=39 + # + # # Styling for WORK_NOT_WRITABLE. + # typeset -g POWERLEVEL9K_DIR_WORK_NOT_WRITABLE_VISUAL_IDENTIFIER_EXPANSION='⭐' + # typeset -g POWERLEVEL9K_DIR_WORK_NOT_WRITABLE_FOREGROUND=31 + # typeset -g POWERLEVEL9K_DIR_WORK_NOT_WRITABLE_SHORTENED_FOREGROUND=103 + # typeset -g POWERLEVEL9K_DIR_WORK_NOT_WRITABLE_ANCHOR_FOREGROUND=39 + # + # # Styling for WORK_NON_EXISTENT. + # typeset -g POWERLEVEL9K_DIR_WORK_NON_EXISTENT_VISUAL_IDENTIFIER_EXPANSION='⭐' + # typeset -g POWERLEVEL9K_DIR_WORK_NON_EXISTENT_FOREGROUND=31 + # typeset -g POWERLEVEL9K_DIR_WORK_NON_EXISTENT_SHORTENED_FOREGROUND=103 + # typeset -g POWERLEVEL9K_DIR_WORK_NON_EXISTENT_ANCHOR_FOREGROUND=39 + # + # If a styling parameter isn't explicitly defined for some class, it falls back to the classless + # parameter. For example, if POWERLEVEL9K_DIR_WORK_NOT_WRITABLE_FOREGROUND is not set, it falls + # back to POWERLEVEL9K_DIR_FOREGROUND. + # + # typeset -g POWERLEVEL9K_DIR_CLASSES=() + + # Custom prefix. + # typeset -g POWERLEVEL9K_DIR_PREFIX='%248Fin ' + + #####################################[ vcs: git status ]###################################### + # Branch icon. Set this parameter to '\UE0A0 ' for the popular Powerline branch icon. + typeset -g POWERLEVEL9K_VCS_BRANCH_ICON='\uF126 ' + + # Untracked files icon. It's really a question mark, your font isn't broken. + # Change the value of this parameter to show a different icon. + typeset -g POWERLEVEL9K_VCS_UNTRACKED_ICON='?' + + # Formatter for Git status. + # + # Example output: master wip ⇣42⇡42 *42 merge ~42 +42 !42 ?42. + # + # You can edit the function to customize how Git status looks. + # + # VCS_STATUS_* parameters are set by gitstatus plugin. See reference: + # https://github.com/romkatv/gitstatus/blob/master/gitstatus.plugin.zsh. + function my_git_formatter() { + emulate -L zsh + + if [[ -n $P9K_CONTENT ]]; then + # If P9K_CONTENT is not empty, use it. It's either "loading" or from vcs_info (not from + # gitstatus plugin). VCS_STATUS_* parameters are not available in this case. + typeset -g my_git_format=$P9K_CONTENT + return + fi + + if (( $1 )); then + # Styling for up-to-date Git status. + local meta='%248F' # grey foreground + local clean='%76F' # green foreground + local modified='%178F' # yellow foreground + local untracked='%39F' # blue foreground + local conflicted='%196F' # red foreground + else + # Styling for incomplete and stale Git status. + local meta='%244F' # grey foreground + local clean='%244F' # grey foreground + local modified='%244F' # grey foreground + local untracked='%244F' # grey foreground + local conflicted='%244F' # grey foreground + fi + + local res + + if [[ -n $VCS_STATUS_LOCAL_BRANCH ]]; then + local branch=${(V)VCS_STATUS_LOCAL_BRANCH} + # If local branch name is at most 32 characters long, show it in full. + # Otherwise show the first 12 … the last 12. + # Tip: To always show local branch name in full without truncation, delete the next line. + (( $#branch > 32 )) && branch[13,-13]="…" # <-- this line + res+="${clean}${(g::)POWERLEVEL9K_VCS_BRANCH_ICON}${branch//\%/%%}" + fi + + if [[ -n $VCS_STATUS_TAG + # Show tag only if not on a branch. + # Tip: To always show tag, delete the next line. + && -z $VCS_STATUS_LOCAL_BRANCH # <-- this line + ]]; then + local tag=${(V)VCS_STATUS_TAG} + # If tag name is at most 32 characters long, show it in full. + # Otherwise show the first 12 … the last 12. + # Tip: To always show tag name in full without truncation, delete the next line. + (( $#tag > 32 )) && tag[13,-13]="…" # <-- this line + res+="${meta}#${clean}${tag//\%/%%}" + fi + + # Display the current Git commit if there is no branch and no tag. + # Tip: To always display the current Git commit, delete the next line. + [[ -z $VCS_STATUS_LOCAL_BRANCH && -z $VCS_STATUS_TAG ]] && # <-- this line + res+="${meta}@${clean}${VCS_STATUS_COMMIT[1,8]}" + + # Show tracking branch name if it differs from local branch. + if [[ -n ${VCS_STATUS_REMOTE_BRANCH:#$VCS_STATUS_LOCAL_BRANCH} ]]; then + res+="${meta}:${clean}${(V)VCS_STATUS_REMOTE_BRANCH//\%/%%}" + fi + + # Display "wip" if the latest commit's summary contains "wip" or "WIP". + if [[ $VCS_STATUS_COMMIT_SUMMARY == (|*[^[:alnum:]])(wip|WIP)(|[^[:alnum:]]*) ]]; then + res+=" ${modified}wip" + fi + + if (( VCS_STATUS_COMMITS_AHEAD || VCS_STATUS_COMMITS_BEHIND )); then + # ⇣42 if behind the remote. + (( VCS_STATUS_COMMITS_BEHIND )) && res+=" ${clean}⇣${VCS_STATUS_COMMITS_BEHIND}" + # ⇡42 if ahead of the remote; no leading space if also behind the remote: ⇣42⇡42. + (( VCS_STATUS_COMMITS_AHEAD && !VCS_STATUS_COMMITS_BEHIND )) && res+=" " + (( VCS_STATUS_COMMITS_AHEAD )) && res+="${clean}⇡${VCS_STATUS_COMMITS_AHEAD}" + elif [[ -n $VCS_STATUS_REMOTE_BRANCH ]]; then + # Tip: Uncomment the next line to display '=' if up to date with the remote. + # res+=" ${clean}=" + fi + + # ⇠42 if behind the push remote. + (( VCS_STATUS_PUSH_COMMITS_BEHIND )) && res+=" ${clean}⇠${VCS_STATUS_PUSH_COMMITS_BEHIND}" + (( VCS_STATUS_PUSH_COMMITS_AHEAD && !VCS_STATUS_PUSH_COMMITS_BEHIND )) && res+=" " + # ⇢42 if ahead of the push remote; no leading space if also behind: ⇠42⇢42. + (( VCS_STATUS_PUSH_COMMITS_AHEAD )) && res+="${clean}⇢${VCS_STATUS_PUSH_COMMITS_AHEAD}" + # *42 if have stashes. + (( VCS_STATUS_STASHES )) && res+=" ${clean}*${VCS_STATUS_STASHES}" + # 'merge' if the repo is in an unusual state. + [[ -n $VCS_STATUS_ACTION ]] && res+=" ${conflicted}${VCS_STATUS_ACTION}" + # ~42 if have merge conflicts. + (( VCS_STATUS_NUM_CONFLICTED )) && res+=" ${conflicted}~${VCS_STATUS_NUM_CONFLICTED}" + # +42 if have staged changes. + (( VCS_STATUS_NUM_STAGED )) && res+=" ${modified}+${VCS_STATUS_NUM_STAGED}" + # !42 if have unstaged changes. + (( VCS_STATUS_NUM_UNSTAGED )) && res+=" ${modified}!${VCS_STATUS_NUM_UNSTAGED}" + # ?42 if have untracked files. It's really a question mark, your font isn't broken. + # See POWERLEVEL9K_VCS_UNTRACKED_ICON above if you want to use a different icon. + # Remove the next line if you don't want to see untracked files at all. + (( VCS_STATUS_NUM_UNTRACKED )) && res+=" ${untracked}${(g::)POWERLEVEL9K_VCS_UNTRACKED_ICON}${VCS_STATUS_NUM_UNTRACKED}" + # "─" if the number of unstaged files is unknown. This can happen due to + # POWERLEVEL9K_VCS_MAX_INDEX_SIZE_DIRTY (see below) being set to a non-negative number lower + # than the number of files in the Git index, or due to bash.showDirtyState being set to false + # in the repository config. The number of staged and untracked files may also be unknown + # in this case. + (( VCS_STATUS_HAS_UNSTAGED == -1 )) && res+=" ${modified}─" + + typeset -g my_git_format=$res + } + functions -M my_git_formatter 2>/dev/null + + # Don't count the number of unstaged, untracked and conflicted files in Git repositories with + # more than this many files in the index. Negative value means infinity. + # + # If you are working in Git repositories with tens of millions of files and seeing performance + # sagging, try setting POWERLEVEL9K_VCS_MAX_INDEX_SIZE_DIRTY to a number lower than the output + # of `git ls-files | wc -l`. Alternatively, add `bash.showDirtyState = false` to the repository's + # config: `git config bash.showDirtyState false`. + typeset -g POWERLEVEL9K_VCS_MAX_INDEX_SIZE_DIRTY=-1 + + # Don't show Git status in prompt for repositories whose workdir matches this pattern. + # For example, if set to '~', the Git repository at $HOME/.git will be ignored. + # Multiple patterns can be combined with '|': '~(|/foo)|/bar/baz/*'. + typeset -g POWERLEVEL9K_VCS_DISABLED_WORKDIR_PATTERN='~' + + # Disable the default Git status formatting. + typeset -g POWERLEVEL9K_VCS_DISABLE_GITSTATUS_FORMATTING=true + # Install our own Git status formatter. + typeset -g POWERLEVEL9K_VCS_CONTENT_EXPANSION='${$((my_git_formatter(1)))+${my_git_format}}' + typeset -g POWERLEVEL9K_VCS_LOADING_CONTENT_EXPANSION='${$((my_git_formatter(0)))+${my_git_format}}' + # Enable counters for staged, unstaged, etc. + typeset -g POWERLEVEL9K_VCS_{STAGED,UNSTAGED,UNTRACKED,CONFLICTED,COMMITS_AHEAD,COMMITS_BEHIND}_MAX_NUM=-1 + + # Icon color. + typeset -g POWERLEVEL9K_VCS_VISUAL_IDENTIFIER_COLOR=76 + typeset -g POWERLEVEL9K_VCS_LOADING_VISUAL_IDENTIFIER_COLOR=244 + # Custom icon. + # typeset -g POWERLEVEL9K_VCS_VISUAL_IDENTIFIER_EXPANSION='⭐' + # Custom prefix. + # typeset -g POWERLEVEL9K_VCS_PREFIX='%248Fon ' + + # Show status of repositories of these types. You can add svn and/or hg if you are + # using them. If you do, your prompt may become slow even when your current directory + # isn't in an svn or hg reposotiry. + typeset -g POWERLEVEL9K_VCS_BACKENDS=(git) + + # These settings are used for repositories other than Git or when gitstatusd fails and + # Powerlevel10k has to fall back to using vcs_info. + typeset -g POWERLEVEL9K_VCS_CLEAN_FOREGROUND=76 + typeset -g POWERLEVEL9K_VCS_UNTRACKED_FOREGROUND=76 + typeset -g POWERLEVEL9K_VCS_MODIFIED_FOREGROUND=178 + + ##########################[ status: exit code of the last command ]########################### + # Enable OK_PIPE, ERROR_PIPE and ERROR_SIGNAL status states to allow us to enable, disable and + # style them independently from the regular OK and ERROR state. + typeset -g POWERLEVEL9K_STATUS_EXTENDED_STATES=true + + # Status on success. No content, just an icon. No need to show it if prompt_char is enabled as + # it will signify success by turning green. + typeset -g POWERLEVEL9K_STATUS_OK=true + typeset -g POWERLEVEL9K_STATUS_OK_FOREGROUND=70 + typeset -g POWERLEVEL9K_STATUS_OK_VISUAL_IDENTIFIER_EXPANSION='✔' + + # Status when some part of a pipe command fails but the overall exit status is zero. It may look + # like this: 1|0. + typeset -g POWERLEVEL9K_STATUS_OK_PIPE=true + typeset -g POWERLEVEL9K_STATUS_OK_PIPE_FOREGROUND=70 + typeset -g POWERLEVEL9K_STATUS_OK_PIPE_VISUAL_IDENTIFIER_EXPANSION='✔' + + # Status when it's just an error code (e.g., '1'). No need to show it if prompt_char is enabled as + # it will signify error by turning red. + typeset -g POWERLEVEL9K_STATUS_ERROR=true + typeset -g POWERLEVEL9K_STATUS_ERROR_FOREGROUND=160 + typeset -g POWERLEVEL9K_STATUS_ERROR_VISUAL_IDENTIFIER_EXPANSION='✘' + + # Status when the last command was terminated by a signal. + typeset -g POWERLEVEL9K_STATUS_ERROR_SIGNAL=true + typeset -g POWERLEVEL9K_STATUS_ERROR_SIGNAL_FOREGROUND=160 + # Use terse signal names: "INT" instead of "SIGINT(2)". + typeset -g POWERLEVEL9K_STATUS_VERBOSE_SIGNAME=false + typeset -g POWERLEVEL9K_STATUS_ERROR_SIGNAL_VISUAL_IDENTIFIER_EXPANSION='✘' + + # Status when some part of a pipe command fails and the overall exit status is also non-zero. + # It may look like this: 1|0. + typeset -g POWERLEVEL9K_STATUS_ERROR_PIPE=true + typeset -g POWERLEVEL9K_STATUS_ERROR_PIPE_FOREGROUND=160 + typeset -g POWERLEVEL9K_STATUS_ERROR_PIPE_VISUAL_IDENTIFIER_EXPANSION='✘' + + ###################[ command_execution_time: duration of the last command ]################### + # Show duration of the last command if takes at least this many seconds. + typeset -g POWERLEVEL9K_COMMAND_EXECUTION_TIME_THRESHOLD=3 + # Show this many fractional digits. Zero means round to seconds. + typeset -g POWERLEVEL9K_COMMAND_EXECUTION_TIME_PRECISION=0 + # Execution time color. + typeset -g POWERLEVEL9K_COMMAND_EXECUTION_TIME_FOREGROUND=248 + # Duration format: 1d 2h 3m 4s. + typeset -g POWERLEVEL9K_COMMAND_EXECUTION_TIME_FORMAT='d h m s' + # Custom icon. + # typeset -g POWERLEVEL9K_COMMAND_EXECUTION_TIME_VISUAL_IDENTIFIER_EXPANSION='⭐' + # Custom prefix. + # typeset -g POWERLEVEL9K_COMMAND_EXECUTION_TIME_PREFIX='%248Ftook ' + + #######################[ background_jobs: presence of background jobs ]####################### + # Don't show the number of background jobs. + typeset -g POWERLEVEL9K_BACKGROUND_JOBS_VERBOSE=false + # Background jobs color. + typeset -g POWERLEVEL9K_BACKGROUND_JOBS_FOREGROUND=37 + # Custom icon. + # typeset -g POWERLEVEL9K_BACKGROUND_JOBS_VISUAL_IDENTIFIER_EXPANSION='⭐' + + #######################[ direnv: direnv status (https://direnv.net/) ]######################## + # Direnv color. + typeset -g POWERLEVEL9K_DIRENV_FOREGROUND=178 + # Custom icon. + # typeset -g POWERLEVEL9K_DIRENV_VISUAL_IDENTIFIER_EXPANSION='⭐' + + ###############[ asdf: asdf version manager (https://github.com/asdf-vm/asdf) ]############### + # Default asdf color. Only used to display tools for which there is no color override (see below). + # Tip: Override this parameter for ${TOOL} with POWERLEVEL9K_ASDF_${TOOL}_FOREGROUND. + typeset -g POWERLEVEL9K_ASDF_FOREGROUND=66 + + # There are four parameters that can be used to hide asdf tools. Each parameter describes + # conditions under which a tool gets hidden. Parameters can hide tools but not unhide them. If at + # least one parameter decides to hide a tool, that tool gets hidden. If no parameter decides to + # hide a tool, it gets shown. + # + # Special note on the difference between POWERLEVEL9K_ASDF_SOURCES and + # POWERLEVEL9K_ASDF_PROMPT_ALWAYS_SHOW. Consider the effect of the following commands: + # + # asdf local python 3.8.1 + # asdf global python 3.8.1 + # + # After running both commands the current python version is 3.8.1 and its source is "local" as + # it takes precedence over "global". If POWERLEVEL9K_ASDF_PROMPT_ALWAYS_SHOW is set to false, + # it'll hide python version in this case because 3.8.1 is the same as the global version. + # POWERLEVEL9K_ASDF_SOURCES will hide python version only if the value of this parameter doesn't + # contain "local". + + # Hide tool versions that don't come from one of these sources. + # + # Available sources: + # + # - shell `asdf current` says "set by ASDF_${TOOL}_VERSION environment variable" + # - local `asdf current` says "set by /some/not/home/directory/file" + # - global `asdf current` says "set by /home/username/file" + # + # Note: If this parameter is set to (shell local global), it won't hide tools. + # Tip: Override this parameter for ${TOOL} with POWERLEVEL9K_ASDF_${TOOL}_SOURCES. + typeset -g POWERLEVEL9K_ASDF_SOURCES=(shell local global) + + # If set to false, hide tool versions that are the same as global. + # + # Note: The name of this parameter doesn't reflect its meaning at all. + # Note: If this parameter is set to true, it won't hide tools. + # Tip: Override this parameter for ${TOOL} with POWERLEVEL9K_ASDF_${TOOL}_PROMPT_ALWAYS_SHOW. + typeset -g POWERLEVEL9K_ASDF_PROMPT_ALWAYS_SHOW=false + + # If set to false, hide tool versions that are equal to "system". + # + # Note: If this parameter is set to true, it won't hide tools. + # Tip: Override this parameter for ${TOOL} with POWERLEVEL9K_ASDF_${TOOL}_SHOW_SYSTEM. + typeset -g POWERLEVEL9K_ASDF_SHOW_SYSTEM=true + + # If set to non-empty value, hide tools unless there is a file matching the specified file pattern + # in the current directory, or its parent directory, or its grandparent directory, and so on. + # + # Note: If this parameter is set to empty value, it won't hide tools. + # Note: SHOW_ON_UPGLOB isn't specific to asdf. It works with all prompt segments. + # Tip: Override this parameter for ${TOOL} with POWERLEVEL9K_ASDF_${TOOL}_SHOW_ON_UPGLOB. + # + # Example: Hide nodejs version when there is no package.json and no *.js files in the current + # directory, in `..`, in `../..` and so on. + # + # typeset -g POWERLEVEL9K_ASDF_NODEJS_SHOW_ON_UPGLOB='*.js|package.json' + typeset -g POWERLEVEL9K_ASDF_SHOW_ON_UPGLOB= + + # Ruby version from asdf. + typeset -g POWERLEVEL9K_ASDF_RUBY_FOREGROUND=168 + # typeset -g POWERLEVEL9K_ASDF_RUBY_VISUAL_IDENTIFIER_EXPANSION='⭐' + # typeset -g POWERLEVEL9K_ASDF_RUBY_SHOW_ON_UPGLOB='*.foo|*.bar' + + # Python version from asdf. + typeset -g POWERLEVEL9K_ASDF_PYTHON_FOREGROUND=37 + # typeset -g POWERLEVEL9K_ASDF_PYTHON_VISUAL_IDENTIFIER_EXPANSION='⭐' + # typeset -g POWERLEVEL9K_ASDF_PYTHON_SHOW_ON_UPGLOB='*.foo|*.bar' + + # Go version from asdf. + typeset -g POWERLEVEL9K_ASDF_GOLANG_FOREGROUND=37 + # typeset -g POWERLEVEL9K_ASDF_GOLANG_VISUAL_IDENTIFIER_EXPANSION='⭐' + # typeset -g POWERLEVEL9K_ASDF_GOLANG_SHOW_ON_UPGLOB='*.foo|*.bar' + + # Node.js version from asdf. + typeset -g POWERLEVEL9K_ASDF_NODEJS_FOREGROUND=70 + # typeset -g POWERLEVEL9K_ASDF_NODEJS_VISUAL_IDENTIFIER_EXPANSION='⭐' + # typeset -g POWERLEVEL9K_ASDF_NODEJS_SHOW_ON_UPGLOB='*.foo|*.bar' + + # Rust version from asdf. + typeset -g POWERLEVEL9K_ASDF_RUST_FOREGROUND=37 + # typeset -g POWERLEVEL9K_ASDF_RUST_VISUAL_IDENTIFIER_EXPANSION='⭐' + # typeset -g POWERLEVEL9K_ASDF_RUST_SHOW_ON_UPGLOB='*.foo|*.bar' + + # .NET Core version from asdf. + typeset -g POWERLEVEL9K_ASDF_DOTNET_CORE_FOREGROUND=134 + # typeset -g POWERLEVEL9K_ASDF_DOTNET_CORE_VISUAL_IDENTIFIER_EXPANSION='⭐' + # typeset -g POWERLEVEL9K_ASDF_DOTNET_CORE_SHOW_ON_UPGLOB='*.foo|*.bar' + + # Flutter version from asdf. + typeset -g POWERLEVEL9K_ASDF_FLUTTER_FOREGROUND=38 + # typeset -g POWERLEVEL9K_ASDF_FLUTTER_VISUAL_IDENTIFIER_EXPANSION='⭐' + # typeset -g POWERLEVEL9K_ASDF_FLUTTER_SHOW_ON_UPGLOB='*.foo|*.bar' + + # Lua version from asdf. + typeset -g POWERLEVEL9K_ASDF_LUA_FOREGROUND=32 + # typeset -g POWERLEVEL9K_ASDF_LUA_VISUAL_IDENTIFIER_EXPANSION='⭐' + # typeset -g POWERLEVEL9K_ASDF_LUA_SHOW_ON_UPGLOB='*.foo|*.bar' + + # Java version from asdf. + typeset -g POWERLEVEL9K_ASDF_JAVA_FOREGROUND=32 + # typeset -g POWERLEVEL9K_ASDF_JAVA_VISUAL_IDENTIFIER_EXPANSION='⭐' + # typeset -g POWERLEVEL9K_ASDF_JAVA_SHOW_ON_UPGLOB='*.foo|*.bar' + + # Perl version from asdf. + typeset -g POWERLEVEL9K_ASDF_PERL_FOREGROUND=67 + # typeset -g POWERLEVEL9K_ASDF_PERL_VISUAL_IDENTIFIER_EXPANSION='⭐' + # typeset -g POWERLEVEL9K_ASDF_PERL_SHOW_ON_UPGLOB='*.foo|*.bar' + + # Erlang version from asdf. + typeset -g POWERLEVEL9K_ASDF_ERLANG_FOREGROUND=125 + # typeset -g POWERLEVEL9K_ASDF_ERLANG_VISUAL_IDENTIFIER_EXPANSION='⭐' + # typeset -g POWERLEVEL9K_ASDF_ERLANG_SHOW_ON_UPGLOB='*.foo|*.bar' + + # Elixir version from asdf. + typeset -g POWERLEVEL9K_ASDF_ELIXIR_FOREGROUND=129 + # typeset -g POWERLEVEL9K_ASDF_ELIXIR_VISUAL_IDENTIFIER_EXPANSION='⭐' + # typeset -g POWERLEVEL9K_ASDF_ELIXIR_SHOW_ON_UPGLOB='*.foo|*.bar' + + # Postgres version from asdf. + typeset -g POWERLEVEL9K_ASDF_POSTGRES_FOREGROUND=31 + # typeset -g POWERLEVEL9K_ASDF_POSTGRES_VISUAL_IDENTIFIER_EXPANSION='⭐' + # typeset -g POWERLEVEL9K_ASDF_POSTGRES_SHOW_ON_UPGLOB='*.foo|*.bar' + + # PHP version from asdf. + typeset -g POWERLEVEL9K_ASDF_PHP_FOREGROUND=99 + # typeset -g POWERLEVEL9K_ASDF_PHP_VISUAL_IDENTIFIER_EXPANSION='⭐' + # typeset -g POWERLEVEL9K_ASDF_PHP_SHOW_ON_UPGLOB='*.foo|*.bar' + + # Haskell version from asdf. + typeset -g POWERLEVEL9K_ASDF_HASKELL_FOREGROUND=172 + # typeset -g POWERLEVEL9K_ASDF_HASKELL_VISUAL_IDENTIFIER_EXPANSION='⭐' + # typeset -g POWERLEVEL9K_ASDF_HASKELL_SHOW_ON_UPGLOB='*.foo|*.bar' + + # Julia version from asdf. + typeset -g POWERLEVEL9K_ASDF_JULIA_FOREGROUND=70 + # typeset -g POWERLEVEL9K_ASDF_JULIA_VISUAL_IDENTIFIER_EXPANSION='⭐' + # typeset -g POWERLEVEL9K_ASDF_JULIA_SHOW_ON_UPGLOB='*.foo|*.bar' + + ##########[ nordvpn: nordvpn connection status, linux only (https://nordvpn.com/) ]########### + # NordVPN connection indicator color. + typeset -g POWERLEVEL9K_NORDVPN_FOREGROUND=39 + # Hide NordVPN connection indicator when not connected. + typeset -g POWERLEVEL9K_NORDVPN_{DISCONNECTED,CONNECTING,DISCONNECTING}_CONTENT_EXPANSION= + typeset -g POWERLEVEL9K_NORDVPN_{DISCONNECTED,CONNECTING,DISCONNECTING}_VISUAL_IDENTIFIER_EXPANSION= + # Custom icon. + # typeset -g POWERLEVEL9K_NORDVPN_VISUAL_IDENTIFIER_EXPANSION='⭐' + + #################[ ranger: ranger shell (https://github.com/ranger/ranger) ]################## + # Ranger shell color. + typeset -g POWERLEVEL9K_RANGER_FOREGROUND=178 + # Custom icon. + # typeset -g POWERLEVEL9K_RANGER_VISUAL_IDENTIFIER_EXPANSION='⭐' + + ####################[ yazi: yazi shell (https://github.com/sxyazi/yazi) ]##################### + # Yazi shell color. + typeset -g POWERLEVEL9K_YAZI_FOREGROUND=178 + # Custom icon. + # typeset -g POWERLEVEL9K_YAZI_VISUAL_IDENTIFIER_EXPANSION='⭐' + + ######################[ nnn: nnn shell (https://github.com/jarun/nnn) ]####################### + # Nnn shell color. + typeset -g POWERLEVEL9K_NNN_FOREGROUND=72 + # Custom icon. + # typeset -g POWERLEVEL9K_NNN_VISUAL_IDENTIFIER_EXPANSION='⭐' + + ######################[ lf: lf shell (https://github.com/gokcehan/lf) ]####################### + # lf shell color. + typeset -g POWERLEVEL9K_LF_FOREGROUND=72 + # Custom icon. + # typeset -g POWERLEVEL9K_LF_VISUAL_IDENTIFIER_EXPANSION='⭐' + + ##################[ xplr: xplr shell (https://github.com/sayanarijit/xplr) ]################## + # xplr shell color. + typeset -g POWERLEVEL9K_XPLR_FOREGROUND=72 + # Custom icon. + # typeset -g POWERLEVEL9K_XPLR_VISUAL_IDENTIFIER_EXPANSION='⭐' + + ###########################[ vim_shell: vim shell indicator (:sh) ]########################### + # Vim shell indicator color. + typeset -g POWERLEVEL9K_VIM_SHELL_FOREGROUND=34 + # Custom icon. + # typeset -g POWERLEVEL9K_VIM_SHELL_VISUAL_IDENTIFIER_EXPANSION='⭐' + + ######[ midnight_commander: midnight commander shell (https://midnight-commander.org/) ]###### + # Midnight Commander shell color. + typeset -g POWERLEVEL9K_MIDNIGHT_COMMANDER_FOREGROUND=178 + # Custom icon. + # typeset -g POWERLEVEL9K_MIDNIGHT_COMMANDER_VISUAL_IDENTIFIER_EXPANSION='⭐' + + #[ nix_shell: nix shell (https://nixos.org/nixos/nix-pills/developing-with-nix-shell.html) ]## + # Nix shell color. + typeset -g POWERLEVEL9K_NIX_SHELL_FOREGROUND=74 + + # Display the icon of nix_shell if PATH contains a subdirectory of /nix/store. + # typeset -g POWERLEVEL9K_NIX_SHELL_INFER_FROM_PATH=false + + # Tip: If you want to see just the icon without "pure" and "impure", uncomment the next line. + # typeset -g POWERLEVEL9K_NIX_SHELL_CONTENT_EXPANSION= + + # Custom icon. + # typeset -g POWERLEVEL9K_NIX_SHELL_VISUAL_IDENTIFIER_EXPANSION='⭐' + + ##################[ chezmoi_shell: chezmoi shell (https://www.chezmoi.io/) ]################## + # chezmoi shell color. + typeset -g POWERLEVEL9K_CHEZMOI_SHELL_FOREGROUND=33 + # Custom icon. + # typeset -g POWERLEVEL9K_CHEZMOI_SHELL_VISUAL_IDENTIFIER_EXPANSION='⭐' + + ##################################[ disk_usage: disk usage ]################################## + # Colors for different levels of disk usage. + typeset -g POWERLEVEL9K_DISK_USAGE_NORMAL_FOREGROUND=35 + typeset -g POWERLEVEL9K_DISK_USAGE_WARNING_FOREGROUND=220 + typeset -g POWERLEVEL9K_DISK_USAGE_CRITICAL_FOREGROUND=160 + # Thresholds for different levels of disk usage (percentage points). + typeset -g POWERLEVEL9K_DISK_USAGE_WARNING_LEVEL=90 + typeset -g POWERLEVEL9K_DISK_USAGE_CRITICAL_LEVEL=95 + # If set to true, hide disk usage when below $POWERLEVEL9K_DISK_USAGE_WARNING_LEVEL percent. + typeset -g POWERLEVEL9K_DISK_USAGE_ONLY_WARNING=false + # Custom icon. + # typeset -g POWERLEVEL9K_DISK_USAGE_VISUAL_IDENTIFIER_EXPANSION='⭐' + + ###########[ vi_mode: vi mode (you don't need this if you've enabled prompt_char) ]########### + # Text and color for normal (a.k.a. command) vi mode. + typeset -g POWERLEVEL9K_VI_COMMAND_MODE_STRING=NORMAL + typeset -g POWERLEVEL9K_VI_MODE_NORMAL_FOREGROUND=106 + # Text and color for visual vi mode. + typeset -g POWERLEVEL9K_VI_VISUAL_MODE_STRING=VISUAL + typeset -g POWERLEVEL9K_VI_MODE_VISUAL_FOREGROUND=68 + # Text and color for overtype (a.k.a. overwrite and replace) vi mode. + typeset -g POWERLEVEL9K_VI_OVERWRITE_MODE_STRING=OVERTYPE + typeset -g POWERLEVEL9K_VI_MODE_OVERWRITE_FOREGROUND=172 + # Text and color for insert vi mode. + typeset -g POWERLEVEL9K_VI_INSERT_MODE_STRING= + typeset -g POWERLEVEL9K_VI_MODE_INSERT_FOREGROUND=66 + # Custom icon. + # typeset -g POWERLEVEL9K_VI_MODE_VISUAL_IDENTIFIER_EXPANSION='⭐' + + ######################################[ ram: free RAM ]####################################### + # RAM color. + typeset -g POWERLEVEL9K_RAM_FOREGROUND=66 + # Custom icon. + # typeset -g POWERLEVEL9K_RAM_VISUAL_IDENTIFIER_EXPANSION='⭐' + + #####################################[ swap: used swap ]###################################### + # Swap color. + typeset -g POWERLEVEL9K_SWAP_FOREGROUND=96 + # Custom icon. + # typeset -g POWERLEVEL9K_SWAP_VISUAL_IDENTIFIER_EXPANSION='⭐' + + ######################################[ load: CPU load ]###################################### + # Show average CPU load over this many last minutes. Valid values are 1, 5 and 15. + typeset -g POWERLEVEL9K_LOAD_WHICH=5 + # Load color when load is under 50%. + typeset -g POWERLEVEL9K_LOAD_NORMAL_FOREGROUND=66 + # Load color when load is between 50% and 70%. + typeset -g POWERLEVEL9K_LOAD_WARNING_FOREGROUND=178 + # Load color when load is over 70%. + typeset -g POWERLEVEL9K_LOAD_CRITICAL_FOREGROUND=166 + # Custom icon. + # typeset -g POWERLEVEL9K_LOAD_VISUAL_IDENTIFIER_EXPANSION='⭐' + + ################[ todo: todo items (https://github.com/todotxt/todo.txt-cli) ]################ + # Todo color. + typeset -g POWERLEVEL9K_TODO_FOREGROUND=110 + # Hide todo when the total number of tasks is zero. + typeset -g POWERLEVEL9K_TODO_HIDE_ZERO_TOTAL=true + # Hide todo when the number of tasks after filtering is zero. + typeset -g POWERLEVEL9K_TODO_HIDE_ZERO_FILTERED=false + + # Todo format. The following parameters are available within the expansion. + # + # - P9K_TODO_TOTAL_TASK_COUNT The total number of tasks. + # - P9K_TODO_FILTERED_TASK_COUNT The number of tasks after filtering. + # + # These variables correspond to the last line of the output of `todo.sh -p ls`: + # + # TODO: 24 of 42 tasks shown + # + # Here 24 is P9K_TODO_FILTERED_TASK_COUNT and 42 is P9K_TODO_TOTAL_TASK_COUNT. + # + # typeset -g POWERLEVEL9K_TODO_CONTENT_EXPANSION='$P9K_TODO_FILTERED_TASK_COUNT' + + # Custom icon. + # typeset -g POWERLEVEL9K_TODO_VISUAL_IDENTIFIER_EXPANSION='⭐' + + ###########[ timewarrior: timewarrior tracking status (https://timewarrior.net/) ]############ + # Timewarrior color. + typeset -g POWERLEVEL9K_TIMEWARRIOR_FOREGROUND=110 + # If the tracked task is longer than 24 characters, truncate and append "…". + # Tip: To always display tasks without truncation, delete the following parameter. + # Tip: To hide task names and display just the icon when time tracking is enabled, set the + # value of the following parameter to "". + typeset -g POWERLEVEL9K_TIMEWARRIOR_CONTENT_EXPANSION='${P9K_CONTENT:0:24}${${P9K_CONTENT:24}:+…}' + + # Custom icon. + # typeset -g POWERLEVEL9K_TIMEWARRIOR_VISUAL_IDENTIFIER_EXPANSION='⭐' + + ##############[ taskwarrior: taskwarrior task count (https://taskwarrior.org/) ]############## + # Taskwarrior color. + typeset -g POWERLEVEL9K_TASKWARRIOR_FOREGROUND=74 + + # Taskwarrior segment format. The following parameters are available within the expansion. + # + # - P9K_TASKWARRIOR_PENDING_COUNT The number of pending tasks: `task +PENDING count`. + # - P9K_TASKWARRIOR_OVERDUE_COUNT The number of overdue tasks: `task +OVERDUE count`. + # + # Zero values are represented as empty parameters. + # + # The default format: + # + # '${P9K_TASKWARRIOR_OVERDUE_COUNT:+"!$P9K_TASKWARRIOR_OVERDUE_COUNT/"}$P9K_TASKWARRIOR_PENDING_COUNT' + # + # typeset -g POWERLEVEL9K_TASKWARRIOR_CONTENT_EXPANSION='$P9K_TASKWARRIOR_PENDING_COUNT' + + # Custom icon. + # typeset -g POWERLEVEL9K_TASKWARRIOR_VISUAL_IDENTIFIER_EXPANSION='⭐' + + ######[ per_directory_history: Oh My Zsh per-directory-history local/global indicator ]####### + # Color when using local/global history. + typeset -g POWERLEVEL9K_PER_DIRECTORY_HISTORY_LOCAL_FOREGROUND=135 + typeset -g POWERLEVEL9K_PER_DIRECTORY_HISTORY_GLOBAL_FOREGROUND=130 + + # Tip: Uncomment the next two lines to hide "local"/"global" text and leave just the icon. + # typeset -g POWERLEVEL9K_PER_DIRECTORY_HISTORY_LOCAL_CONTENT_EXPANSION='' + # typeset -g POWERLEVEL9K_PER_DIRECTORY_HISTORY_GLOBAL_CONTENT_EXPANSION='' + + # Custom icon. + # typeset -g POWERLEVEL9K_PER_DIRECTORY_HISTORY_LOCAL_VISUAL_IDENTIFIER_EXPANSION='⭐' + # typeset -g POWERLEVEL9K_PER_DIRECTORY_HISTORY_GLOBAL_VISUAL_IDENTIFIER_EXPANSION='⭐' + + ################################[ cpu_arch: CPU architecture ]################################ + # CPU architecture color. + typeset -g POWERLEVEL9K_CPU_ARCH_FOREGROUND=172 + + # Hide the segment when on a specific CPU architecture. + # typeset -g POWERLEVEL9K_CPU_ARCH_X86_64_CONTENT_EXPANSION= + # typeset -g POWERLEVEL9K_CPU_ARCH_X86_64_VISUAL_IDENTIFIER_EXPANSION= + + # Custom icon. + # typeset -g POWERLEVEL9K_CPU_ARCH_VISUAL_IDENTIFIER_EXPANSION='⭐' + + ##################################[ context: user@hostname ]################################## + # Context color when running with privileges. + typeset -g POWERLEVEL9K_CONTEXT_ROOT_FOREGROUND=178 + # Context color in SSH without privileges. + typeset -g POWERLEVEL9K_CONTEXT_{REMOTE,REMOTE_SUDO}_FOREGROUND=180 + # Default context color (no privileges, no SSH). + typeset -g POWERLEVEL9K_CONTEXT_FOREGROUND=180 + + # Context format when running with privileges: bold user@hostname. + typeset -g POWERLEVEL9K_CONTEXT_ROOT_TEMPLATE='%B%n@%m' + # Context format when in SSH without privileges: user@hostname. + typeset -g POWERLEVEL9K_CONTEXT_{REMOTE,REMOTE_SUDO}_TEMPLATE='%n@%m' + # Default context format (no privileges, no SSH): user@hostname. + typeset -g POWERLEVEL9K_CONTEXT_TEMPLATE='%n@%m' + + # Don't show context unless running with privileges or in SSH. + # Tip: Remove the next line to always show context. + typeset -g POWERLEVEL9K_CONTEXT_{DEFAULT,SUDO}_{CONTENT,VISUAL_IDENTIFIER}_EXPANSION= + + # Custom icon. + # typeset -g POWERLEVEL9K_CONTEXT_VISUAL_IDENTIFIER_EXPANSION='⭐' + # Custom prefix. + # typeset -g POWERLEVEL9K_CONTEXT_PREFIX='%248Fwith ' + + ###[ virtualenv: python virtual environment (https://docs.python.org/3/library/venv.html) ]### + # Python virtual environment color. + typeset -g POWERLEVEL9K_VIRTUALENV_FOREGROUND=37 + # Don't show Python version next to the virtual environment name. + typeset -g POWERLEVEL9K_VIRTUALENV_SHOW_PYTHON_VERSION=false + # If set to "false", won't show virtualenv if pyenv is already shown. + # If set to "if-different", won't show virtualenv if it's the same as pyenv. + typeset -g POWERLEVEL9K_VIRTUALENV_SHOW_WITH_PYENV=false + # Separate environment name from Python version only with a space. + typeset -g POWERLEVEL9K_VIRTUALENV_{LEFT,RIGHT}_DELIMITER= + # Custom icon. + # typeset -g POWERLEVEL9K_VIRTUALENV_VISUAL_IDENTIFIER_EXPANSION='⭐' + + #####################[ anaconda: conda environment (https://conda.io/) ]###################### + # Anaconda environment color. + typeset -g POWERLEVEL9K_ANACONDA_FOREGROUND=37 + + # Anaconda segment format. The following parameters are available within the expansion. + # + # - CONDA_PREFIX Absolute path to the active Anaconda/Miniconda environment. + # - CONDA_DEFAULT_ENV Name of the active Anaconda/Miniconda environment. + # - CONDA_PROMPT_MODIFIER Configurable prompt modifier (see below). + # - P9K_ANACONDA_PYTHON_VERSION Current python version (python --version). + # + # CONDA_PROMPT_MODIFIER can be configured with the following command: + # + # conda config --set env_prompt '({default_env}) ' + # + # The last argument is a Python format string that can use the following variables: + # + # - prefix The same as CONDA_PREFIX. + # - default_env The same as CONDA_DEFAULT_ENV. + # - name The last segment of CONDA_PREFIX. + # - stacked_env Comma-separated list of names in the environment stack. The first element is + # always the same as default_env. + # + # Note: '({default_env}) ' is the default value of env_prompt. + # + # The default value of POWERLEVEL9K_ANACONDA_CONTENT_EXPANSION expands to $CONDA_PROMPT_MODIFIER + # without the surrounding parentheses, or to the last path component of CONDA_PREFIX if the former + # is empty. + typeset -g POWERLEVEL9K_ANACONDA_CONTENT_EXPANSION='${${${${CONDA_PROMPT_MODIFIER#\(}% }%\)}:-${CONDA_PREFIX:t}}' + + # Custom icon. + # typeset -g POWERLEVEL9K_ANACONDA_VISUAL_IDENTIFIER_EXPANSION='⭐' + + ################[ pyenv: python environment (https://github.com/pyenv/pyenv) ]################ + # Pyenv color. + typeset -g POWERLEVEL9K_PYENV_FOREGROUND=37 + # Hide python version if it doesn't come from one of these sources. + typeset -g POWERLEVEL9K_PYENV_SOURCES=(shell local global) + # If set to false, hide python version if it's the same as global: + # $(pyenv version-name) == $(pyenv global). + typeset -g POWERLEVEL9K_PYENV_PROMPT_ALWAYS_SHOW=false + # If set to false, hide python version if it's equal to "system". + typeset -g POWERLEVEL9K_PYENV_SHOW_SYSTEM=true + + # Pyenv segment format. The following parameters are available within the expansion. + # + # - P9K_CONTENT Current pyenv environment (pyenv version-name). + # - P9K_PYENV_PYTHON_VERSION Current python version (python --version). + # + # The default format has the following logic: + # + # 1. Display just "$P9K_CONTENT" if it's equal to "$P9K_PYENV_PYTHON_VERSION" or + # starts with "$P9K_PYENV_PYTHON_VERSION/". + # 2. Otherwise display "$P9K_CONTENT $P9K_PYENV_PYTHON_VERSION". + typeset -g POWERLEVEL9K_PYENV_CONTENT_EXPANSION='${P9K_CONTENT}${${P9K_CONTENT:#$P9K_PYENV_PYTHON_VERSION(|/*)}:+ $P9K_PYENV_PYTHON_VERSION}' + + # Custom icon. + # typeset -g POWERLEVEL9K_PYENV_VISUAL_IDENTIFIER_EXPANSION='⭐' + + ################[ goenv: go environment (https://github.com/syndbg/goenv) ]################ + # Goenv color. + typeset -g POWERLEVEL9K_GOENV_FOREGROUND=37 + # Hide go version if it doesn't come from one of these sources. + typeset -g POWERLEVEL9K_GOENV_SOURCES=(shell local global) + # If set to false, hide go version if it's the same as global: + # $(goenv version-name) == $(goenv global). + typeset -g POWERLEVEL9K_GOENV_PROMPT_ALWAYS_SHOW=false + # If set to false, hide go version if it's equal to "system". + typeset -g POWERLEVEL9K_GOENV_SHOW_SYSTEM=true + # Custom icon. + # typeset -g POWERLEVEL9K_GOENV_VISUAL_IDENTIFIER_EXPANSION='⭐' + + ##########[ nodenv: node.js version from nodenv (https://github.com/nodenv/nodenv) ]########## + # Nodenv color. + typeset -g POWERLEVEL9K_NODENV_FOREGROUND=70 + # Hide node version if it doesn't come from one of these sources. + typeset -g POWERLEVEL9K_NODENV_SOURCES=(shell local global) + # If set to false, hide node version if it's the same as global: + # $(nodenv version-name) == $(nodenv global). + typeset -g POWERLEVEL9K_NODENV_PROMPT_ALWAYS_SHOW=false + # If set to false, hide node version if it's equal to "system". + typeset -g POWERLEVEL9K_NODENV_SHOW_SYSTEM=true + # Custom icon. + # typeset -g POWERLEVEL9K_NODENV_VISUAL_IDENTIFIER_EXPANSION='⭐' + + ##############[ nvm: node.js version from nvm (https://github.com/nvm-sh/nvm) ]############### + # Nvm color. + typeset -g POWERLEVEL9K_NVM_FOREGROUND=70 + # If set to false, hide node version if it's the same as default: + # $(nvm version current) == $(nvm version default). + typeset -g POWERLEVEL9K_NVM_PROMPT_ALWAYS_SHOW=false + # If set to false, hide node version if it's equal to "system". + typeset -g POWERLEVEL9K_NVM_SHOW_SYSTEM=true + # Custom icon. + # typeset -g POWERLEVEL9K_NVM_VISUAL_IDENTIFIER_EXPANSION='⭐' + + ############[ nodeenv: node.js environment (https://github.com/ekalinin/nodeenv) ]############ + # Nodeenv color. + typeset -g POWERLEVEL9K_NODEENV_FOREGROUND=70 + # Don't show Node version next to the environment name. + typeset -g POWERLEVEL9K_NODEENV_SHOW_NODE_VERSION=false + # Separate environment name from Node version only with a space. + typeset -g POWERLEVEL9K_NODEENV_{LEFT,RIGHT}_DELIMITER= + # Custom icon. + # typeset -g POWERLEVEL9K_NODEENV_VISUAL_IDENTIFIER_EXPANSION='⭐' + + ##############################[ node_version: node.js version ]############################### + # Node version color. + typeset -g POWERLEVEL9K_NODE_VERSION_FOREGROUND=70 + # Show node version only when in a directory tree containing package.json. + typeset -g POWERLEVEL9K_NODE_VERSION_PROJECT_ONLY=true + # Custom icon. + # typeset -g POWERLEVEL9K_NODE_VERSION_VISUAL_IDENTIFIER_EXPANSION='⭐' + + #######################[ go_version: go version (https://golang.org) ]######################## + # Go version color. + typeset -g POWERLEVEL9K_GO_VERSION_FOREGROUND=37 + # Show go version only when in a go project subdirectory. + typeset -g POWERLEVEL9K_GO_VERSION_PROJECT_ONLY=true + # Custom icon. + # typeset -g POWERLEVEL9K_GO_VERSION_VISUAL_IDENTIFIER_EXPANSION='⭐' + + #################[ rust_version: rustc version (https://www.rust-lang.org) ]################## + # Rust version color. + typeset -g POWERLEVEL9K_RUST_VERSION_FOREGROUND=37 + # Show rust version only when in a rust project subdirectory. + typeset -g POWERLEVEL9K_RUST_VERSION_PROJECT_ONLY=true + # Custom icon. + # typeset -g POWERLEVEL9K_RUST_VERSION_VISUAL_IDENTIFIER_EXPANSION='⭐' + + ###############[ dotnet_version: .NET version (https://dotnet.microsoft.com) ]################ + # .NET version color. + typeset -g POWERLEVEL9K_DOTNET_VERSION_FOREGROUND=134 + # Show .NET version only when in a .NET project subdirectory. + typeset -g POWERLEVEL9K_DOTNET_VERSION_PROJECT_ONLY=true + # Custom icon. + # typeset -g POWERLEVEL9K_DOTNET_VERSION_VISUAL_IDENTIFIER_EXPANSION='⭐' + + #####################[ php_version: php version (https://www.php.net/) ]###################### + # PHP version color. + typeset -g POWERLEVEL9K_PHP_VERSION_FOREGROUND=99 + # Show PHP version only when in a PHP project subdirectory. + typeset -g POWERLEVEL9K_PHP_VERSION_PROJECT_ONLY=true + # Custom icon. + # typeset -g POWERLEVEL9K_PHP_VERSION_VISUAL_IDENTIFIER_EXPANSION='⭐' + + ##########[ laravel_version: laravel php framework version (https://laravel.com/) ]########### + # Laravel version color. + typeset -g POWERLEVEL9K_LARAVEL_VERSION_FOREGROUND=161 + # Custom icon. + # typeset -g POWERLEVEL9K_LARAVEL_VERSION_VISUAL_IDENTIFIER_EXPANSION='⭐' + + ####################[ java_version: java version (https://www.java.com/) ]#################### + # Java version color. + typeset -g POWERLEVEL9K_JAVA_VERSION_FOREGROUND=32 + # Show java version only when in a java project subdirectory. + typeset -g POWERLEVEL9K_JAVA_VERSION_PROJECT_ONLY=true + # Show brief version. + typeset -g POWERLEVEL9K_JAVA_VERSION_FULL=false + # Custom icon. + # typeset -g POWERLEVEL9K_JAVA_VERSION_VISUAL_IDENTIFIER_EXPANSION='⭐' + + ###[ package: name@version from package.json (https://docs.npmjs.com/files/package.json) ]#### + # Package color. + typeset -g POWERLEVEL9K_PACKAGE_FOREGROUND=117 + # Package format. The following parameters are available within the expansion. + # + # - P9K_PACKAGE_NAME The value of `name` field in package.json. + # - P9K_PACKAGE_VERSION The value of `version` field in package.json. + # + # typeset -g POWERLEVEL9K_PACKAGE_CONTENT_EXPANSION='${P9K_PACKAGE_NAME//\%/%%}@${P9K_PACKAGE_VERSION//\%/%%}' + # Custom icon. + # typeset -g POWERLEVEL9K_PACKAGE_VISUAL_IDENTIFIER_EXPANSION='⭐' + + #############[ rbenv: ruby version from rbenv (https://github.com/rbenv/rbenv) ]############## + # Rbenv color. + typeset -g POWERLEVEL9K_RBENV_FOREGROUND=168 + # Hide ruby version if it doesn't come from one of these sources. + typeset -g POWERLEVEL9K_RBENV_SOURCES=(shell local global) + # If set to false, hide ruby version if it's the same as global: + # $(rbenv version-name) == $(rbenv global). + typeset -g POWERLEVEL9K_RBENV_PROMPT_ALWAYS_SHOW=false + # If set to false, hide ruby version if it's equal to "system". + typeset -g POWERLEVEL9K_RBENV_SHOW_SYSTEM=true + # Custom icon. + # typeset -g POWERLEVEL9K_RBENV_VISUAL_IDENTIFIER_EXPANSION='⭐' + + #######################[ rvm: ruby version from rvm (https://rvm.io) ]######################## + # Rvm color. + typeset -g POWERLEVEL9K_RVM_FOREGROUND=168 + # Don't show @gemset at the end. + typeset -g POWERLEVEL9K_RVM_SHOW_GEMSET=false + # Don't show ruby- at the front. + typeset -g POWERLEVEL9K_RVM_SHOW_PREFIX=false + # Custom icon. + # typeset -g POWERLEVEL9K_RVM_VISUAL_IDENTIFIER_EXPANSION='⭐' + + ###########[ fvm: flutter version management (https://github.com/leoafarias/fvm) ]############ + # Fvm color. + typeset -g POWERLEVEL9K_FVM_FOREGROUND=38 + # Custom icon. + # typeset -g POWERLEVEL9K_FVM_VISUAL_IDENTIFIER_EXPANSION='⭐' + + ##########[ luaenv: lua version from luaenv (https://github.com/cehoffman/luaenv) ]########### + # Lua color. + typeset -g POWERLEVEL9K_LUAENV_FOREGROUND=32 + # Hide lua version if it doesn't come from one of these sources. + typeset -g POWERLEVEL9K_LUAENV_SOURCES=(shell local global) + # If set to false, hide lua version if it's the same as global: + # $(luaenv version-name) == $(luaenv global). + typeset -g POWERLEVEL9K_LUAENV_PROMPT_ALWAYS_SHOW=false + # If set to false, hide lua version if it's equal to "system". + typeset -g POWERLEVEL9K_LUAENV_SHOW_SYSTEM=true + # Custom icon. + # typeset -g POWERLEVEL9K_LUAENV_VISUAL_IDENTIFIER_EXPANSION='⭐' + + ###############[ jenv: java version from jenv (https://github.com/jenv/jenv) ]################ + # Java color. + typeset -g POWERLEVEL9K_JENV_FOREGROUND=32 + # Hide java version if it doesn't come from one of these sources. + typeset -g POWERLEVEL9K_JENV_SOURCES=(shell local global) + # If set to false, hide java version if it's the same as global: + # $(jenv version-name) == $(jenv global). + typeset -g POWERLEVEL9K_JENV_PROMPT_ALWAYS_SHOW=false + # If set to false, hide java version if it's equal to "system". + typeset -g POWERLEVEL9K_JENV_SHOW_SYSTEM=true + # Custom icon. + # typeset -g POWERLEVEL9K_JENV_VISUAL_IDENTIFIER_EXPANSION='⭐' + + ###########[ plenv: perl version from plenv (https://github.com/tokuhirom/plenv) ]############ + # Perl color. + typeset -g POWERLEVEL9K_PLENV_FOREGROUND=67 + # Hide perl version if it doesn't come from one of these sources. + typeset -g POWERLEVEL9K_PLENV_SOURCES=(shell local global) + # If set to false, hide perl version if it's the same as global: + # $(plenv version-name) == $(plenv global). + typeset -g POWERLEVEL9K_PLENV_PROMPT_ALWAYS_SHOW=false + # If set to false, hide perl version if it's equal to "system". + typeset -g POWERLEVEL9K_PLENV_SHOW_SYSTEM=true + # Custom icon. + # typeset -g POWERLEVEL9K_PLENV_VISUAL_IDENTIFIER_EXPANSION='⭐' + + ###########[ perlbrew: perl version from perlbrew (https://github.com/gugod/App-perlbrew) ]############ + # Perlbrew color. + typeset -g POWERLEVEL9K_PERLBREW_FOREGROUND=67 + # Show perlbrew version only when in a perl project subdirectory. + typeset -g POWERLEVEL9K_PERLBREW_PROJECT_ONLY=true + # Don't show "perl-" at the front. + typeset -g POWERLEVEL9K_PERLBREW_SHOW_PREFIX=false + # Custom icon. + # typeset -g POWERLEVEL9K_PERLBREW_VISUAL_IDENTIFIER_EXPANSION='⭐' + + ############[ phpenv: php version from phpenv (https://github.com/phpenv/phpenv) ]############ + # PHP color. + typeset -g POWERLEVEL9K_PHPENV_FOREGROUND=99 + # Hide php version if it doesn't come from one of these sources. + typeset -g POWERLEVEL9K_PHPENV_SOURCES=(shell local global) + # If set to false, hide php version if it's the same as global: + # $(phpenv version-name) == $(phpenv global). + typeset -g POWERLEVEL9K_PHPENV_PROMPT_ALWAYS_SHOW=false + # If set to false, hide php version if it's equal to "system". + typeset -g POWERLEVEL9K_PHPENV_SHOW_SYSTEM=true + # Custom icon. + # typeset -g POWERLEVEL9K_PHPENV_VISUAL_IDENTIFIER_EXPANSION='⭐' + + #######[ scalaenv: scala version from scalaenv (https://github.com/scalaenv/scalaenv) ]####### + # Scala color. + typeset -g POWERLEVEL9K_SCALAENV_FOREGROUND=160 + # Hide scala version if it doesn't come from one of these sources. + typeset -g POWERLEVEL9K_SCALAENV_SOURCES=(shell local global) + # If set to false, hide scala version if it's the same as global: + # $(scalaenv version-name) == $(scalaenv global). + typeset -g POWERLEVEL9K_SCALAENV_PROMPT_ALWAYS_SHOW=false + # If set to false, hide scala version if it's equal to "system". + typeset -g POWERLEVEL9K_SCALAENV_SHOW_SYSTEM=true + # Custom icon. + # typeset -g POWERLEVEL9K_SCALAENV_VISUAL_IDENTIFIER_EXPANSION='⭐' + + ##########[ haskell_stack: haskell version from stack (https://haskellstack.org/) ]########### + # Haskell color. + typeset -g POWERLEVEL9K_HASKELL_STACK_FOREGROUND=172 + # Hide haskell version if it doesn't come from one of these sources. + # + # shell: version is set by STACK_YAML + # local: version is set by stack.yaml up the directory tree + # global: version is set by the implicit global project (~/.stack/global-project/stack.yaml) + typeset -g POWERLEVEL9K_HASKELL_STACK_SOURCES=(shell local) + # If set to false, hide haskell version if it's the same as in the implicit global project. + typeset -g POWERLEVEL9K_HASKELL_STACK_ALWAYS_SHOW=true + # Custom icon. + # typeset -g POWERLEVEL9K_HASKELL_STACK_VISUAL_IDENTIFIER_EXPANSION='⭐' + + ################[ terraform: terraform workspace (https://www.terraform.io) ]################# + # Don't show terraform workspace if it's literally "default". + typeset -g POWERLEVEL9K_TERRAFORM_SHOW_DEFAULT=false + # POWERLEVEL9K_TERRAFORM_CLASSES is an array with even number of elements. The first element + # in each pair defines a pattern against which the current terraform workspace gets matched. + # More specifically, it's P9K_CONTENT prior to the application of context expansion (see below) + # that gets matched. If you unset all POWERLEVEL9K_TERRAFORM_*CONTENT_EXPANSION parameters, + # you'll see this value in your prompt. The second element of each pair in + # POWERLEVEL9K_TERRAFORM_CLASSES defines the workspace class. Patterns are tried in order. The + # first match wins. + # + # For example, given these settings: + # + # typeset -g POWERLEVEL9K_TERRAFORM_CLASSES=( + # '*prod*' PROD + # '*test*' TEST + # '*' OTHER) + # + # If your current terraform workspace is "project_test", its class is TEST because "project_test" + # doesn't match the pattern '*prod*' but does match '*test*'. + # + # You can define different colors, icons and content expansions for different classes: + # + # typeset -g POWERLEVEL9K_TERRAFORM_TEST_FOREGROUND=28 + # typeset -g POWERLEVEL9K_TERRAFORM_TEST_VISUAL_IDENTIFIER_EXPANSION='⭐' + # typeset -g POWERLEVEL9K_TERRAFORM_TEST_CONTENT_EXPANSION='> ${P9K_CONTENT} <' + typeset -g POWERLEVEL9K_TERRAFORM_CLASSES=( + # '*prod*' PROD # These values are examples that are unlikely + # '*test*' TEST # to match your needs. Customize them as needed. + '*' OTHER) + typeset -g POWERLEVEL9K_TERRAFORM_OTHER_FOREGROUND=38 + # typeset -g POWERLEVEL9K_TERRAFORM_OTHER_VISUAL_IDENTIFIER_EXPANSION='⭐' + + #############[ terraform_version: terraform version (https://www.terraform.io) ]############## + # Terraform version color. + typeset -g POWERLEVEL9K_TERRAFORM_VERSION_FOREGROUND=38 + # Custom icon. + # typeset -g POWERLEVEL9K_TERRAFORM_VERSION_VISUAL_IDENTIFIER_EXPANSION='⭐' + + #############[ kubecontext: current kubernetes context (https://kubernetes.io/) ]############# + # Show kubecontext only when the command you are typing invokes one of these tools. + # Tip: Remove the next line to always show kubecontext. + typeset -g POWERLEVEL9K_KUBECONTEXT_SHOW_ON_COMMAND='kubectl|helm|kubens|kubectx|oc|istioctl|kogito|k9s|helmfile|flux|fluxctl|stern|kubeseal|skaffold|kubent|kubecolor|cmctl|sparkctl' + + # Kubernetes context classes for the purpose of using different colors, icons and expansions with + # different contexts. + # + # POWERLEVEL9K_KUBECONTEXT_CLASSES is an array with even number of elements. The first element + # in each pair defines a pattern against which the current kubernetes context gets matched. + # More specifically, it's P9K_CONTENT prior to the application of context expansion (see below) + # that gets matched. If you unset all POWERLEVEL9K_KUBECONTEXT_*CONTENT_EXPANSION parameters, + # you'll see this value in your prompt. The second element of each pair in + # POWERLEVEL9K_KUBECONTEXT_CLASSES defines the context class. Patterns are tried in order. The + # first match wins. + # + # For example, given these settings: + # + # typeset -g POWERLEVEL9K_KUBECONTEXT_CLASSES=( + # '*prod*' PROD + # '*test*' TEST + # '*' DEFAULT) + # + # If your current kubernetes context is "deathray-testing/default", its class is TEST + # because "deathray-testing/default" doesn't match the pattern '*prod*' but does match '*test*'. + # + # You can define different colors, icons and content expansions for different classes: + # + # typeset -g POWERLEVEL9K_KUBECONTEXT_TEST_FOREGROUND=28 + # typeset -g POWERLEVEL9K_KUBECONTEXT_TEST_VISUAL_IDENTIFIER_EXPANSION='⭐' + # typeset -g POWERLEVEL9K_KUBECONTEXT_TEST_CONTENT_EXPANSION='> ${P9K_CONTENT} <' + typeset -g POWERLEVEL9K_KUBECONTEXT_CLASSES=( + # '*prod*' PROD # These values are examples that are unlikely + # '*test*' TEST # to match your needs. Customize them as needed. + '*' DEFAULT) + typeset -g POWERLEVEL9K_KUBECONTEXT_DEFAULT_FOREGROUND=134 + # typeset -g POWERLEVEL9K_KUBECONTEXT_DEFAULT_VISUAL_IDENTIFIER_EXPANSION='⭐' + + # Use POWERLEVEL9K_KUBECONTEXT_CONTENT_EXPANSION to specify the content displayed by kubecontext + # segment. Parameter expansions are very flexible and fast, too. See reference: + # http://zsh.sourceforge.net/Doc/Release/Expansion.html#Parameter-Expansion. + # + # Within the expansion the following parameters are always available: + # + # - P9K_CONTENT The content that would've been displayed if there was no content + # expansion defined. + # - P9K_KUBECONTEXT_NAME The current context's name. Corresponds to column NAME in the + # output of `kubectl config get-contexts`. + # - P9K_KUBECONTEXT_CLUSTER The current context's cluster. Corresponds to column CLUSTER in the + # output of `kubectl config get-contexts`. + # - P9K_KUBECONTEXT_NAMESPACE The current context's namespace. Corresponds to column NAMESPACE + # in the output of `kubectl config get-contexts`. If there is no + # namespace, the parameter is set to "default". + # - P9K_KUBECONTEXT_USER The current context's user. Corresponds to column AUTHINFO in the + # output of `kubectl config get-contexts`. + # + # If the context points to Google Kubernetes Engine (GKE) or Elastic Kubernetes Service (EKS), + # the following extra parameters are available: + # + # - P9K_KUBECONTEXT_CLOUD_NAME Either "gke" or "eks". + # - P9K_KUBECONTEXT_CLOUD_ACCOUNT Account/project ID. + # - P9K_KUBECONTEXT_CLOUD_ZONE Availability zone. + # - P9K_KUBECONTEXT_CLOUD_CLUSTER Cluster. + # + # P9K_KUBECONTEXT_CLOUD_* parameters are derived from P9K_KUBECONTEXT_CLUSTER. For example, + # if P9K_KUBECONTEXT_CLUSTER is "gke_my-account_us-east1-a_my-cluster-01": + # + # - P9K_KUBECONTEXT_CLOUD_NAME=gke + # - P9K_KUBECONTEXT_CLOUD_ACCOUNT=my-account + # - P9K_KUBECONTEXT_CLOUD_ZONE=us-east1-a + # - P9K_KUBECONTEXT_CLOUD_CLUSTER=my-cluster-01 + # + # If P9K_KUBECONTEXT_CLUSTER is "arn:aws:eks:us-east-1:123456789012:cluster/my-cluster-01": + # + # - P9K_KUBECONTEXT_CLOUD_NAME=eks + # - P9K_KUBECONTEXT_CLOUD_ACCOUNT=123456789012 + # - P9K_KUBECONTEXT_CLOUD_ZONE=us-east-1 + # - P9K_KUBECONTEXT_CLOUD_CLUSTER=my-cluster-01 + typeset -g POWERLEVEL9K_KUBECONTEXT_DEFAULT_CONTENT_EXPANSION= + # Show P9K_KUBECONTEXT_CLOUD_CLUSTER if it's not empty and fall back to P9K_KUBECONTEXT_NAME. + POWERLEVEL9K_KUBECONTEXT_DEFAULT_CONTENT_EXPANSION+='${P9K_KUBECONTEXT_CLOUD_CLUSTER:-${P9K_KUBECONTEXT_NAME}}' + # Append the current context's namespace if it's not "default". + POWERLEVEL9K_KUBECONTEXT_DEFAULT_CONTENT_EXPANSION+='${${:-/$P9K_KUBECONTEXT_NAMESPACE}:#/default}' + + # Custom prefix. + # typeset -g POWERLEVEL9K_KUBECONTEXT_PREFIX='%248Fat ' + + #[ aws: aws profile (https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-profiles.html) ]# + # Show aws only when the command you are typing invokes one of these tools. + # Tip: Remove the next line to always show aws. + typeset -g POWERLEVEL9K_AWS_SHOW_ON_COMMAND='aws|awless|cdk|terraform|tofu|pulumi|terragrunt' + + # POWERLEVEL9K_AWS_CLASSES is an array with even number of elements. The first element + # in each pair defines a pattern against which the current AWS profile gets matched. + # More specifically, it's P9K_CONTENT prior to the application of context expansion (see below) + # that gets matched. If you unset all POWERLEVEL9K_AWS_*CONTENT_EXPANSION parameters, + # you'll see this value in your prompt. The second element of each pair in + # POWERLEVEL9K_AWS_CLASSES defines the profile class. Patterns are tried in order. The + # first match wins. + # + # For example, given these settings: + # + # typeset -g POWERLEVEL9K_AWS_CLASSES=( + # '*prod*' PROD + # '*test*' TEST + # '*' DEFAULT) + # + # If your current AWS profile is "company_test", its class is TEST + # because "company_test" doesn't match the pattern '*prod*' but does match '*test*'. + # + # You can define different colors, icons and content expansions for different classes: + # + # typeset -g POWERLEVEL9K_AWS_TEST_FOREGROUND=28 + # typeset -g POWERLEVEL9K_AWS_TEST_VISUAL_IDENTIFIER_EXPANSION='⭐' + # typeset -g POWERLEVEL9K_AWS_TEST_CONTENT_EXPANSION='> ${P9K_CONTENT} <' + typeset -g POWERLEVEL9K_AWS_CLASSES=( + # '*prod*' PROD # These values are examples that are unlikely + # '*test*' TEST # to match your needs. Customize them as needed. + '*' DEFAULT) + typeset -g POWERLEVEL9K_AWS_DEFAULT_FOREGROUND=208 + # typeset -g POWERLEVEL9K_AWS_DEFAULT_VISUAL_IDENTIFIER_EXPANSION='⭐' + + # AWS segment format. The following parameters are available within the expansion. + # + # - P9K_AWS_PROFILE The name of the current AWS profile. + # - P9K_AWS_REGION The region associated with the current AWS profile. + typeset -g POWERLEVEL9K_AWS_CONTENT_EXPANSION='${P9K_AWS_PROFILE//\%/%%}${P9K_AWS_REGION:+ ${P9K_AWS_REGION//\%/%%}}' + + #[ aws_eb_env: aws elastic beanstalk environment (https://aws.amazon.com/elasticbeanstalk/) ]# + # AWS Elastic Beanstalk environment color. + typeset -g POWERLEVEL9K_AWS_EB_ENV_FOREGROUND=70 + # Custom icon. + # typeset -g POWERLEVEL9K_AWS_EB_ENV_VISUAL_IDENTIFIER_EXPANSION='⭐' + + ##########[ azure: azure account name (https://docs.microsoft.com/en-us/cli/azure) ]########## + # Show azure only when the command you are typing invokes one of these tools. + # Tip: Remove the next line to always show azure. + typeset -g POWERLEVEL9K_AZURE_SHOW_ON_COMMAND='az|terraform|tofu|pulumi|terragrunt' + + # POWERLEVEL9K_AZURE_CLASSES is an array with even number of elements. The first element + # in each pair defines a pattern against which the current azure account name gets matched. + # More specifically, it's P9K_CONTENT prior to the application of context expansion (see below) + # that gets matched. If you unset all POWERLEVEL9K_AZURE_*CONTENT_EXPANSION parameters, + # you'll see this value in your prompt. The second element of each pair in + # POWERLEVEL9K_AZURE_CLASSES defines the account class. Patterns are tried in order. The + # first match wins. + # + # For example, given these settings: + # + # typeset -g POWERLEVEL9K_AZURE_CLASSES=( + # '*prod*' PROD + # '*test*' TEST + # '*' OTHER) + # + # If your current azure account is "company_test", its class is TEST because "company_test" + # doesn't match the pattern '*prod*' but does match '*test*'. + # + # You can define different colors, icons and content expansions for different classes: + # + # typeset -g POWERLEVEL9K_AZURE_TEST_FOREGROUND=28 + # typeset -g POWERLEVEL9K_AZURE_TEST_VISUAL_IDENTIFIER_EXPANSION='⭐' + # typeset -g POWERLEVEL9K_AZURE_TEST_CONTENT_EXPANSION='> ${P9K_CONTENT} <' + typeset -g POWERLEVEL9K_AZURE_CLASSES=( + # '*prod*' PROD # These values are examples that are unlikely + # '*test*' TEST # to match your needs. Customize them as needed. + '*' OTHER) + + # Azure account name color. + typeset -g POWERLEVEL9K_AZURE_OTHER_FOREGROUND=32 + # Custom icon. + # typeset -g POWERLEVEL9K_AZURE_OTHER_VISUAL_IDENTIFIER_EXPANSION='⭐' + + ##########[ gcloud: google cloud account and project (https://cloud.google.com/) ]########### + # Show gcloud only when the command you are typing invokes one of these tools. + # Tip: Remove the next line to always show gcloud. + typeset -g POWERLEVEL9K_GCLOUD_SHOW_ON_COMMAND='gcloud|gcs|gsutil' + # Google cloud color. + typeset -g POWERLEVEL9K_GCLOUD_FOREGROUND=32 + + # Google cloud format. Change the value of POWERLEVEL9K_GCLOUD_PARTIAL_CONTENT_EXPANSION and/or + # POWERLEVEL9K_GCLOUD_COMPLETE_CONTENT_EXPANSION if the default is too verbose or not informative + # enough. You can use the following parameters in the expansions. Each of them corresponds to the + # output of `gcloud` tool. + # + # Parameter | Source + # -------------------------|-------------------------------------------------------------------- + # P9K_GCLOUD_CONFIGURATION | gcloud config configurations list --format='value(name)' + # P9K_GCLOUD_ACCOUNT | gcloud config get-value account + # P9K_GCLOUD_PROJECT_ID | gcloud config get-value project + # P9K_GCLOUD_PROJECT_NAME | gcloud projects describe $P9K_GCLOUD_PROJECT_ID --format='value(name)' + # + # Note: ${VARIABLE//\%/%%} expands to ${VARIABLE} with all occurrences of '%' replaced with '%%'. + # + # Obtaining project name requires sending a request to Google servers. This can take a long time + # and even fail. When project name is unknown, P9K_GCLOUD_PROJECT_NAME is not set and gcloud + # prompt segment is in state PARTIAL. When project name gets known, P9K_GCLOUD_PROJECT_NAME gets + # set and gcloud prompt segment transitions to state COMPLETE. + # + # You can customize the format, icon and colors of gcloud segment separately for states PARTIAL + # and COMPLETE. You can also hide gcloud in state PARTIAL by setting + # POWERLEVEL9K_GCLOUD_PARTIAL_VISUAL_IDENTIFIER_EXPANSION and + # POWERLEVEL9K_GCLOUD_PARTIAL_CONTENT_EXPANSION to empty. + typeset -g POWERLEVEL9K_GCLOUD_PARTIAL_CONTENT_EXPANSION='${P9K_GCLOUD_PROJECT_ID//\%/%%}' + typeset -g POWERLEVEL9K_GCLOUD_COMPLETE_CONTENT_EXPANSION='${P9K_GCLOUD_PROJECT_NAME//\%/%%}' + + # Send a request to Google (by means of `gcloud projects describe ...`) to obtain project name + # this often. Negative value disables periodic polling. In this mode project name is retrieved + # only when the current configuration, account or project id changes. + typeset -g POWERLEVEL9K_GCLOUD_REFRESH_PROJECT_NAME_SECONDS=60 + + # Custom icon. + # typeset -g POWERLEVEL9K_GCLOUD_VISUAL_IDENTIFIER_EXPANSION='⭐' + + #[ google_app_cred: google application credentials (https://cloud.google.com/docs/authentication/production) ]# + # Show google_app_cred only when the command you are typing invokes one of these tools. + # Tip: Remove the next line to always show google_app_cred. + typeset -g POWERLEVEL9K_GOOGLE_APP_CRED_SHOW_ON_COMMAND='terraform|tofu|pulumi|terragrunt' + + # Google application credentials classes for the purpose of using different colors, icons and + # expansions with different credentials. + # + # POWERLEVEL9K_GOOGLE_APP_CRED_CLASSES is an array with even number of elements. The first + # element in each pair defines a pattern against which the current kubernetes context gets + # matched. More specifically, it's P9K_CONTENT prior to the application of context expansion + # (see below) that gets matched. If you unset all POWERLEVEL9K_GOOGLE_APP_CRED_*CONTENT_EXPANSION + # parameters, you'll see this value in your prompt. The second element of each pair in + # POWERLEVEL9K_GOOGLE_APP_CRED_CLASSES defines the context class. Patterns are tried in order. + # The first match wins. + # + # For example, given these settings: + # + # typeset -g POWERLEVEL9K_GOOGLE_APP_CRED_CLASSES=( + # '*:*prod*:*' PROD + # '*:*test*:*' TEST + # '*' DEFAULT) + # + # If your current Google application credentials is "service_account deathray-testing x@y.com", + # its class is TEST because it doesn't match the pattern '* *prod* *' but does match '* *test* *'. + # + # You can define different colors, icons and content expansions for different classes: + # + # typeset -g POWERLEVEL9K_GOOGLE_APP_CRED_TEST_FOREGROUND=28 + # typeset -g POWERLEVEL9K_GOOGLE_APP_CRED_TEST_VISUAL_IDENTIFIER_EXPANSION='⭐' + # typeset -g POWERLEVEL9K_GOOGLE_APP_CRED_TEST_CONTENT_EXPANSION='$P9K_GOOGLE_APP_CRED_PROJECT_ID' + typeset -g POWERLEVEL9K_GOOGLE_APP_CRED_CLASSES=( + # '*:*prod*:*' PROD # These values are examples that are unlikely + # '*:*test*:*' TEST # to match your needs. Customize them as needed. + '*' DEFAULT) + typeset -g POWERLEVEL9K_GOOGLE_APP_CRED_DEFAULT_FOREGROUND=32 + # typeset -g POWERLEVEL9K_GOOGLE_APP_CRED_DEFAULT_VISUAL_IDENTIFIER_EXPANSION='⭐' + + # Use POWERLEVEL9K_GOOGLE_APP_CRED_CONTENT_EXPANSION to specify the content displayed by + # google_app_cred segment. Parameter expansions are very flexible and fast, too. See reference: + # http://zsh.sourceforge.net/Doc/Release/Expansion.html#Parameter-Expansion. + # + # You can use the following parameters in the expansion. Each of them corresponds to one of the + # fields in the JSON file pointed to by GOOGLE_APPLICATION_CREDENTIALS. + # + # Parameter | JSON key file field + # ---------------------------------+--------------- + # P9K_GOOGLE_APP_CRED_TYPE | type + # P9K_GOOGLE_APP_CRED_PROJECT_ID | project_id + # P9K_GOOGLE_APP_CRED_CLIENT_EMAIL | client_email + # + # Note: ${VARIABLE//\%/%%} expands to ${VARIABLE} with all occurrences of '%' replaced by '%%'. + typeset -g POWERLEVEL9K_GOOGLE_APP_CRED_DEFAULT_CONTENT_EXPANSION='${P9K_GOOGLE_APP_CRED_PROJECT_ID//\%/%%}' + + ##############[ toolbox: toolbox name (https://github.com/containers/toolbox) ]############### + # Toolbox color. + typeset -g POWERLEVEL9K_TOOLBOX_FOREGROUND=178 + # Don't display the name of the toolbox if it matches fedora-toolbox-*. + typeset -g POWERLEVEL9K_TOOLBOX_CONTENT_EXPANSION='${P9K_TOOLBOX_NAME:#fedora-toolbox-*}' + # Custom icon. + # typeset -g POWERLEVEL9K_TOOLBOX_VISUAL_IDENTIFIER_EXPANSION='⭐' + # Custom prefix. + # typeset -g POWERLEVEL9K_TOOLBOX_PREFIX='%248Fin ' + + ###############################[ public_ip: public IP address ]############################### + # Public IP color. + typeset -g POWERLEVEL9K_PUBLIC_IP_FOREGROUND=94 + # Custom icon. + # typeset -g POWERLEVEL9K_PUBLIC_IP_VISUAL_IDENTIFIER_EXPANSION='⭐' + + ########################[ vpn_ip: virtual private network indicator ]######################### + # VPN IP color. + typeset -g POWERLEVEL9K_VPN_IP_FOREGROUND=81 + # When on VPN, show just an icon without the IP address. + # Tip: To display the private IP address when on VPN, remove the next line. + typeset -g POWERLEVEL9K_VPN_IP_CONTENT_EXPANSION= + # Regular expression for the VPN network interface. Run `ifconfig` or `ip -4 a show` while on VPN + # to see the name of the interface. + typeset -g POWERLEVEL9K_VPN_IP_INTERFACE='(gpd|wg|(.*tun)|tailscale)[0-9]*|(zt.*)' + # If set to true, show one segment per matching network interface. If set to false, show only + # one segment corresponding to the first matching network interface. + # Tip: If you set it to true, you'll probably want to unset POWERLEVEL9K_VPN_IP_CONTENT_EXPANSION. + typeset -g POWERLEVEL9K_VPN_IP_SHOW_ALL=false + # Custom icon. + # typeset -g POWERLEVEL9K_VPN_IP_VISUAL_IDENTIFIER_EXPANSION='⭐' + + ###########[ ip: ip address and bandwidth usage for a specified network interface ]########### + # IP color. + typeset -g POWERLEVEL9K_IP_FOREGROUND=38 + # The following parameters are accessible within the expansion: + # + # Parameter | Meaning + # ----------------------+------------------------------------------- + # P9K_IP_IP | IP address + # P9K_IP_INTERFACE | network interface + # P9K_IP_RX_BYTES | total number of bytes received + # P9K_IP_TX_BYTES | total number of bytes sent + # P9K_IP_RX_BYTES_DELTA | number of bytes received since last prompt + # P9K_IP_TX_BYTES_DELTA | number of bytes sent since last prompt + # P9K_IP_RX_RATE | receive rate (since last prompt) + # P9K_IP_TX_RATE | send rate (since last prompt) + typeset -g POWERLEVEL9K_IP_CONTENT_EXPANSION='${P9K_IP_RX_RATE:+%70F⇣$P9K_IP_RX_RATE }${P9K_IP_TX_RATE:+%215F⇡$P9K_IP_TX_RATE }%38F$P9K_IP_IP' + # Show information for the first network interface whose name matches this regular expression. + # Run `ifconfig` or `ip -4 a show` to see the names of all network interfaces. + typeset -g POWERLEVEL9K_IP_INTERFACE='[ew].*' + # Custom icon. + # typeset -g POWERLEVEL9K_IP_VISUAL_IDENTIFIER_EXPANSION='⭐' + + #########################[ proxy: system-wide http/https/ftp proxy ]########################## + # Proxy color. + typeset -g POWERLEVEL9K_PROXY_FOREGROUND=68 + # Custom icon. + # typeset -g POWERLEVEL9K_PROXY_VISUAL_IDENTIFIER_EXPANSION='⭐' + + ################################[ battery: internal battery ]################################# + # Show battery in red when it's below this level and not connected to power supply. + typeset -g POWERLEVEL9K_BATTERY_LOW_THRESHOLD=20 + typeset -g POWERLEVEL9K_BATTERY_LOW_FOREGROUND=160 + # Show battery in green when it's charging or fully charged. + typeset -g POWERLEVEL9K_BATTERY_{CHARGING,CHARGED}_FOREGROUND=70 + # Show battery in yellow when it's discharging. + typeset -g POWERLEVEL9K_BATTERY_DISCONNECTED_FOREGROUND=178 + # Battery pictograms going from low to high level of charge. + typeset -g POWERLEVEL9K_BATTERY_STAGES='\UF008E\UF007A\UF007B\UF007C\UF007D\UF007E\UF007F\UF0080\UF0081\UF0082\UF0079' + # Don't show the remaining time to charge/discharge. + typeset -g POWERLEVEL9K_BATTERY_VERBOSE=false + + #####################################[ wifi: wifi speed ]##################################### + # WiFi color. + typeset -g POWERLEVEL9K_WIFI_FOREGROUND=68 + # Custom icon. + # typeset -g POWERLEVEL9K_WIFI_VISUAL_IDENTIFIER_EXPANSION='⭐' + + # Use different colors and icons depending on signal strength ($P9K_WIFI_BARS). + # + # # Wifi colors and icons for different signal strength levels (low to high). + # typeset -g my_wifi_fg=(68 68 68 68 68) # <-- change these values + # typeset -g my_wifi_icon=('WiFi' 'WiFi' 'WiFi' 'WiFi' 'WiFi') # <-- change these values + # + # typeset -g POWERLEVEL9K_WIFI_CONTENT_EXPANSION='%F{${my_wifi_fg[P9K_WIFI_BARS+1]}}$P9K_WIFI_LAST_TX_RATE Mbps' + # typeset -g POWERLEVEL9K_WIFI_VISUAL_IDENTIFIER_EXPANSION='%F{${my_wifi_fg[P9K_WIFI_BARS+1]}}${my_wifi_icon[P9K_WIFI_BARS+1]}' + # + # The following parameters are accessible within the expansions: + # + # Parameter | Meaning + # ----------------------+--------------- + # P9K_WIFI_SSID | service set identifier, a.k.a. network name + # P9K_WIFI_LINK_AUTH | authentication protocol such as "wpa2-psk" or "none"; empty if unknown + # P9K_WIFI_LAST_TX_RATE | wireless transmit rate in megabits per second + # P9K_WIFI_RSSI | signal strength in dBm, from -120 to 0 + # P9K_WIFI_NOISE | noise in dBm, from -120 to 0 + # P9K_WIFI_BARS | signal strength in bars, from 0 to 4 (derived from P9K_WIFI_RSSI and P9K_WIFI_NOISE) + + ####################################[ time: current time ]#################################### + # Current time color. + typeset -g POWERLEVEL9K_TIME_FOREGROUND=66 + # Format for the current time: 09:51:02. See `man 3 strftime`. + typeset -g POWERLEVEL9K_TIME_FORMAT='%D{%H:%M:%S}' + # If set to true, time will update when you hit enter. This way prompts for the past + # commands will contain the start times of their commands as opposed to the default + # behavior where they contain the end times of their preceding commands. + typeset -g POWERLEVEL9K_TIME_UPDATE_ON_COMMAND=false + # Custom icon. + # typeset -g POWERLEVEL9K_TIME_VISUAL_IDENTIFIER_EXPANSION='⭐' + # Custom prefix. + # typeset -g POWERLEVEL9K_TIME_PREFIX='%248Fat ' + + # Example of a user-defined prompt segment. Function prompt_example will be called on every + # prompt if `example` prompt segment is added to POWERLEVEL9K_LEFT_PROMPT_ELEMENTS or + # POWERLEVEL9K_RIGHT_PROMPT_ELEMENTS. It displays an icon and orange text greeting the user. + # + # Type `p10k help segment` for documentation and a more sophisticated example. + function prompt_example() { + p10k segment -f 208 -i '⭐' -t 'hello, %n' + } + + # User-defined prompt segments may optionally provide an instant_prompt_* function. Its job + # is to generate the prompt segment for display in instant prompt. See + # https://github.com/romkatv/powerlevel10k#instant-prompt. + # + # Powerlevel10k will call instant_prompt_* at the same time as the regular prompt_* function + # and will record all `p10k segment` calls it makes. When displaying instant prompt, Powerlevel10k + # will replay these calls without actually calling instant_prompt_*. It is imperative that + # instant_prompt_* always makes the same `p10k segment` calls regardless of environment. If this + # rule is not observed, the content of instant prompt will be incorrect. + # + # Usually, you should either not define instant_prompt_* or simply call prompt_* from it. If + # instant_prompt_* is not defined for a segment, the segment won't be shown in instant prompt. + function instant_prompt_example() { + # Since prompt_example always makes the same `p10k segment` calls, we can call it from + # instant_prompt_example. This will give us the same `example` prompt segment in the instant + # and regular prompts. + prompt_example + } + + # User-defined prompt segments can be customized the same way as built-in segments. + # typeset -g POWERLEVEL9K_EXAMPLE_FOREGROUND=208 + # typeset -g POWERLEVEL9K_EXAMPLE_VISUAL_IDENTIFIER_EXPANSION='⭐' + + # Transient prompt works similarly to the builtin transient_rprompt option. It trims down prompt + # when accepting a command line. Supported values: + # + # - off: Don't change prompt when accepting a command line. + # - always: Trim down prompt when accepting a command line. + # - same-dir: Trim down prompt when accepting a command line unless this is the first command + # typed after changing current working directory. + typeset -g POWERLEVEL9K_TRANSIENT_PROMPT=always + + # Instant prompt mode. + # + # - off: Disable instant prompt. Choose this if you've tried instant prompt and found + # it incompatible with your zsh configuration files. + # - quiet: Enable instant prompt and don't print warnings when detecting console output + # during zsh initialization. Choose this if you've read and understood + # https://github.com/romkatv/powerlevel10k#instant-prompt. + # - verbose: Enable instant prompt and print a warning when detecting console output during + # zsh initialization. Choose this if you've never tried instant prompt, haven't + # seen the warning, or if you are unsure what this all means. + typeset -g POWERLEVEL9K_INSTANT_PROMPT=verbose + + # Hot reload allows you to change POWERLEVEL9K options after Powerlevel10k has been initialized. + # For example, you can type POWERLEVEL9K_BACKGROUND=red and see your prompt turn red. Hot reload + # can slow down prompt by 1-2 milliseconds, so it's better to keep it turned off unless you + # really need it. + typeset -g POWERLEVEL9K_DISABLE_HOT_RELOAD=true + + # If p10k is already loaded, reload configuration. + # This works even with POWERLEVEL9K_DISABLE_HOT_RELOAD=true. + (( ! $+functions[p10k] )) || p10k reload +} + +# Tell `p10k configure` which file it should overwrite. +typeset -g POWERLEVEL9K_CONFIG_FILE=${${(%):-%x}:a} + +(( ${#p10k_config_opts} )) && setopt ${p10k_config_opts[@]} +'builtin' 'unset' 'p10k_config_opts' diff --git a/iso/airootfs/etc/skel/.zshrc b/iso/airootfs/etc/skel/.zshrc new file mode 100644 index 0000000..8a2a31e --- /dev/null +++ b/iso/airootfs/etc/skel/.zshrc @@ -0,0 +1,93 @@ +# BOS default zsh config — Powerlevel10k prompt + plugins + pywal palette. +# +# Mirrors the BOS dev shell, but sources plugins from the distro packages +# (/usr/share/zsh/...) instead of oh-my-zsh, so there's no framework to manage. +# Customise the prompt with `p10k configure` (rewrites ~/.p10k.zsh). + +# Enable Powerlevel10k instant prompt. Should stay close to the top of ~/.zshrc. +# Initialization code that may require console input (password prompts, [y/n] +# confirmations, etc.) must go above this block; everything else may go below. +if [[ -r "${XDG_CACHE_HOME:-$HOME/.cache}/p10k-instant-prompt-${(%):-%n}.zsh" ]]; then + source "${XDG_CACHE_HOME:-$HOME/.cache}/p10k-instant-prompt-${(%):-%n}.zsh" +fi + +# History +HISTFILE=~/.zsh_history +HISTSIZE=10000 +SAVEHIST=10000 +setopt HIST_IGNORE_DUPS HIST_IGNORE_SPACE SHARE_HISTORY + +# Completion +autoload -Uz compinit && compinit +zstyle ':completion:*' menu select +zstyle ':completion:*' matcher-list 'm:{a-z}={A-Z}' + +# Emacs-style key bindings +bindkey -e + +# Prompt — Powerlevel10k (republished to [breadway] as zsh-theme-powerlevel10k). +source /usr/share/zsh-theme-powerlevel10k/powerlevel10k.zsh-theme + +# Plugins (order matters: syntax-highlighting must be sourced LAST). +ZSH_AUTOSUGGEST_HIGHLIGHT_STYLE='fg=60' +source /usr/share/zsh/plugins/zsh-autosuggestions/zsh-autosuggestions.zsh 2>/dev/null +source /usr/share/zsh/plugins/zsh-history-substring-search/zsh-history-substring-search.zsh 2>/dev/null +source /usr/share/zsh/plugins/zsh-syntax-highlighting/zsh-syntax-highlighting.zsh 2>/dev/null + +# history-substring-search: ↑/↓ search history by the typed prefix. +bindkey '^[[A' history-substring-search-up +bindkey '^[[B' history-substring-search-down + +# fzf — fuzzy history search on Ctrl+R, fuzzy file find on Ctrl+T +if command -v fzf &>/dev/null; then + source /usr/share/fzf/key-bindings.zsh 2>/dev/null || true + source /usr/share/fzf/completion.zsh 2>/dev/null || true + export FZF_DEFAULT_OPTS='--height 40% --layout=reverse --border' +fi + +# zoxide — smarter cd (type z instead of cd) +if command -v zoxide &>/dev/null; then + eval "$(zoxide init zsh)" +fi + +# Modern replacements with fallbacks +if command -v eza &>/dev/null; then + alias ls='eza --icons --group-directories-first' + alias ll='eza -la --icons --group-directories-first --git' + alias lt='eza --tree --icons --level=2' +else + alias ls='ls --color=auto' + alias ll='ls -la' +fi + +if command -v bat &>/dev/null; then + alias cat='bat --style=plain --paging=never' +fi + +# General aliases +alias clr='clear' +alias ..='cd ..' +alias ...='cd ../..' +alias mkdir='mkdir -p' +alias cp='cp -i' +alias mv='mv -i' +alias df='df -h' +alias free='free -h' +alias grep='grep --color=auto' +alias ip='ip --color=auto' + +# Updates — bos-update runs both channels (pacman + bakery). pacman aliased to +# sudo so `pacman -Syu` etc. just work. +alias update='bos-update' +alias pacman='sudo pacman' + +# ~/.local/bin holds the bread* binaries baked in at build time. +export PATH="$HOME/.local/bin:$PATH" + +# Powerlevel10k prompt configuration. +[[ ! -f ~/.p10k.zsh ]] || source ~/.p10k.zsh + +# Import pywal colour palette (drives the terminal colours from the wallpaper). +if [ -f "$HOME/.cache/wal/sequences" ]; then + cat "$HOME/.cache/wal/sequences" +fi diff --git a/iso/airootfs/etc/sudoers.d/99-bos-live b/iso/airootfs/etc/sudoers.d/99-bos-live new file mode 100644 index 0000000..591d9b9 --- /dev/null +++ b/iso/airootfs/etc/sudoers.d/99-bos-live @@ -0,0 +1,3 @@ +# Live medium only: the unprivileged live user may escalate without a password +# so the installer (Calamares) can run as root from the Wayland session. +liveuser ALL=(ALL) NOPASSWD: ALL diff --git a/iso/airootfs/etc/systemd/logind.conf.d/90-bos-power.conf b/iso/airootfs/etc/systemd/logind.conf.d/90-bos-power.conf new file mode 100644 index 0000000..f3bd2bb --- /dev/null +++ b/iso/airootfs/etc/systemd/logind.conf.d/90-bos-power.conf @@ -0,0 +1,7 @@ +# Lid behaviour: suspend on close (on battery or AC), but ignore the lid when +# docked / an external display is connected so closing the laptop with a monitor +# attached keeps the session running. Mainstream-desktop default. +[Login] +HandleLidSwitch=suspend +HandleLidSwitchExternalPower=suspend +HandleLidSwitchDocked=ignore diff --git a/iso/airootfs/etc/systemd/system/bos-live-setup.service b/iso/airootfs/etc/systemd/system/bos-live-setup.service new file mode 100644 index 0000000..22e91f5 --- /dev/null +++ b/iso/airootfs/etc/systemd/system/bos-live-setup.service @@ -0,0 +1,14 @@ +[Unit] +Description=Set up the BOS live user and session +# Only on the live medium — the installed system has no archisobasedir cmdline. +ConditionKernelCommandLine=archisobasedir +Before=getty@tty1.service +After=systemd-tmpfiles-setup.service + +[Service] +Type=oneshot +RemainAfterExit=yes +ExecStart=/usr/local/bin/bos-live-setup + +[Install] +WantedBy=multi-user.target diff --git a/iso/airootfs/etc/systemd/system/getty@tty1.service.d/autologin.conf b/iso/airootfs/etc/systemd/system/getty@tty1.service.d/autologin.conf index b9d22eb..b10ceb2 100644 --- a/iso/airootfs/etc/systemd/system/getty@tty1.service.d/autologin.conf +++ b/iso/airootfs/etc/systemd/system/getty@tty1.service.d/autologin.conf @@ -1,3 +1,3 @@ [Service] ExecStart= -ExecStart=-/sbin/agetty -o '-p -f -- \\u' --noclear --autologin root - $TERM +ExecStart=-/sbin/agetty -o '-p -f -- \\u' --noclear --autologin liveuser - $TERM diff --git a/iso/airootfs/etc/systemd/system/multi-user.target.wants/NetworkManager.service b/iso/airootfs/etc/systemd/system/multi-user.target.wants/NetworkManager.service new file mode 120000 index 0000000..e874a9b --- /dev/null +++ b/iso/airootfs/etc/systemd/system/multi-user.target.wants/NetworkManager.service @@ -0,0 +1 @@ +/usr/lib/systemd/system/NetworkManager.service \ No newline at end of file diff --git a/iso/airootfs/etc/systemd/system/multi-user.target.wants/bos-live-setup.service b/iso/airootfs/etc/systemd/system/multi-user.target.wants/bos-live-setup.service new file mode 120000 index 0000000..a8d5e7d --- /dev/null +++ b/iso/airootfs/etc/systemd/system/multi-user.target.wants/bos-live-setup.service @@ -0,0 +1 @@ +../bos-live-setup.service \ No newline at end of file diff --git a/iso/airootfs/etc/systemd/zram-generator.conf b/iso/airootfs/etc/systemd/zram-generator.conf new file mode 100644 index 0000000..b1d46e5 --- /dev/null +++ b/iso/airootfs/etc/systemd/zram-generator.conf @@ -0,0 +1,6 @@ +# Compressed RAM swap. systemd-zram-generator reads this and creates a zram +# device + swap at boot — no on-disk swap partition needed. Sized at half RAM +# capped to 4 GiB, zstd-compressed (typically ~3:1, so cheap headroom). +[zram0] +zram-size = min(ram / 2, 4096) +compression-algorithm = zstd diff --git a/iso/airootfs/etc/vconsole.conf b/iso/airootfs/etc/vconsole.conf new file mode 100644 index 0000000..12b81a1 --- /dev/null +++ b/iso/airootfs/etc/vconsole.conf @@ -0,0 +1 @@ +KEYMAP=us diff --git a/iso/airootfs/root/.bash_profile b/iso/airootfs/root/.bash_profile index fd13f6a..390a22d 100644 --- a/iso/airootfs/root/.bash_profile +++ b/iso/airootfs/root/.bash_profile @@ -1,4 +1,19 @@ # Auto-start Hyprland on tty1 in the live session if [[ "$(tty)" == "/dev/tty1" ]] && [[ -z "$WAYLAND_DISPLAY" ]]; then - exec Hyprland + # Allow a software-rendering fallback so the live session comes up even + # without a GPU (VMs, headless, exotic hardware). On real hardware wlroots + # still selects the hardware renderer; this only permits llvmpipe when no + # GPU renderer is available. Must be exported before Hyprland starts — + # wlroots reads it at renderer init, earlier than any Hyprland `env=` line. + export WLR_RENDERER_ALLOW_SOFTWARE=1 + # Software cursors: hardware-cursor planes are often unusable in VMs and + # show as invisible/garbled; this is the reliable choice for a live medium. + export WLR_NO_HARDWARE_CURSORS=1 + # Run the compositor, capturing its output so a failed live boot is + # diagnosable (Hyprland also keeps its own log under $XDG_RUNTIME_DIR/hypr/). + # On exit, drop to an interactive shell with the error in view instead of + # letting the getty autologin respawn-loop hide it behind a blank cursor. + Hyprland &>/var/log/hyprland-live.log + echo "Hyprland exited (rc=$?). Log: /var/log/hyprland-live.log" + exec bash -i fi diff --git a/iso/airootfs/usr/local/bin/bos-copy-kernel b/iso/airootfs/usr/local/bin/bos-copy-kernel new file mode 100755 index 0000000..ee23c30 --- /dev/null +++ b/iso/airootfs/usr/local/bin/bos-copy-kernel @@ -0,0 +1,46 @@ +#!/bin/bash +# Copy the live kernel into the freshly-unpacked target /boot. +# +# archiso keeps vmlinuz/initramfs in the ISO boot dir (arch/boot/x86_64/), NOT +# in the squashfs, so the rootfs that unpackfs lays down has an empty /boot. +# The kernel must be present before Calamares' `initcpio` module runs mkinitcpio +# (the stock linux.preset points ALL_kver at /boot/vmlinuz-linux) and before the +# `bootloader` module runs grub — otherwise the installed system is unbootable. +# +# Runs in the LIVE environment (Calamares shellprocess, dontChroot) so it can +# read /run/archiso/bootmnt; the target root mount point is passed as $1. +set -uo pipefail + +ROOT="${1:?target root required}" +SRC="/run/archiso/bootmnt/arch/boot/x86_64" + +install -d -m 0755 "$ROOT/boot" +cp -f "$SRC/vmlinuz-linux" "$ROOT/boot/vmlinuz-linux" + +# Microcode, if the live medium carries it (grub-mkconfig picks it up). +for u in amd-ucode.img intel-ucode.img; do + [ -f "$SRC/$u" ] && cp -f "$SRC/$u" "$ROOT/boot/$u" +done + +# Replace the archiso initramfs setup that unpackfs copied from the live medium. +# On archiso the linux preset is PRESETS=('archiso') using archiso.conf (the live +# HOOKS). Calamares' `initcpio` runs `mkinitcpio -P`, which would build that +# archiso preset and either bake the live-boot hooks into the install or fail +# once archiso.conf is gone. Drop the drop-in and write a stock default+fallback +# preset so `initcpio` produces a normal, bootable initramfs from the config that +# the `initcpiocfg` module generates at /etc/mkinitcpio.conf. +rm -f "$ROOT/etc/mkinitcpio.conf.d/archiso.conf" +install -d -m 0755 "$ROOT/etc/mkinitcpio.d" +cat >"$ROOT/etc/mkinitcpio.d/linux.preset" <<'PRESET' +# mkinitcpio preset file for the 'linux' package +ALL_config="/etc/mkinitcpio.conf" +ALL_kver="/boot/vmlinuz-linux" + +PRESETS=('default' 'fallback') + +default_image="/boot/initramfs-linux.img" +fallback_image="/boot/initramfs-linux-fallback.img" +fallback_options="-S autodetect" +PRESET + +echo "Copied live kernel into $ROOT/boot; reset mkinitcpio to a stock preset" diff --git a/iso/airootfs/usr/local/bin/bos-keybinds b/iso/airootfs/usr/local/bin/bos-keybinds new file mode 100644 index 0000000..4d036cb --- /dev/null +++ b/iso/airootfs/usr/local/bin/bos-keybinds @@ -0,0 +1,4 @@ +#!/bin/bash +# Show the BOS keybind cheatsheet in a floating terminal (bound to SUPER+/). +# The bos-keybinds window class is floated/centred by a Hyprland window rule. +exec kitty --class bos-keybinds --title "BOS Keybinds" -- less -R /usr/share/bos/keybinds.txt diff --git a/iso/airootfs/usr/local/bin/bos-launch-calamares b/iso/airootfs/usr/local/bin/bos-launch-calamares new file mode 100644 index 0000000..d7f3fe9 --- /dev/null +++ b/iso/airootfs/usr/local/bin/bos-launch-calamares @@ -0,0 +1,7 @@ +#!/bin/sh +# Launch Calamares as root on the live user's Wayland session. +# Calamares performs partitioning/bootloader work and needs root; the live user +# has passwordless sudo (see /etc/sudoers.d/99-bos-live). We preserve the Wayland +# environment so the root process renders on the user's compositor. +export QT_QPA_PLATFORM=wayland +exec sudo --preserve-env=WAYLAND_DISPLAY,XDG_RUNTIME_DIR,QT_QPA_PLATFORM calamares diff --git a/iso/airootfs/usr/local/bin/bos-live-setup b/iso/airootfs/usr/local/bin/bos-live-setup new file mode 100644 index 0000000..0fbe5bd --- /dev/null +++ b/iso/airootfs/usr/local/bin/bos-live-setup @@ -0,0 +1,49 @@ +#!/bin/bash +# Create the unprivileged BOS live user and its Hyprland session. +# +# Hyprland refuses to run as root (superuser-privileges check), so the live +# session must run as a normal user. Calamares — which does need root — is +# launched onto the user's Wayland socket via passwordless sudo (see +# bos-launch-calamares). Runs once at boot, before the tty1 autologin getty. +set -e + +# useradd -m copies /etc/skel, so the live user gets the real BOS desktop +# (breadd + breadbar + breadbox + keybinds) — proper live-media functionality, +# not an installer kiosk. +if ! id liveuser &>/dev/null; then + useradd -m -s /bin/bash liveuser + for g in wheel video input audio storage power; do + getent group "$g" >/dev/null 2>&1 && gpasswd -a liveuser "$g" >/dev/null || true + done + passwd -d liveuser >/dev/null +fi + +# Layer the installer onto the live desktop: auto-launch it, and bind Super+I to +# relaunch it after it's been closed. Appended (in Lua) to the skel hyprland.lua +# native config so the full desktop stays intact. +HYPR=/home/liveuser/.config/hypr/hyprland.lua +install -d -m 0755 -o liveuser -g liveuser /home/liveuser/.config/hypr +if ! grep -q bos-launch-calamares "$HYPR" 2>/dev/null; then + cat >>"$HYPR" <<'EOF' + +-- --- live-media installer (added by bos-live-setup; absent on installed system) --- +hl.bind("SUPER + I", hl.dsp.exec_cmd("bos-launch-calamares")) +hl.on("hyprland.start", function() hl.dispatch(hl.dsp.exec_cmd("bos-launch-calamares")) end) +EOF +fi + +# Start Hyprland on tty1 login; capture output and fall back to a shell so a +# failed compositor start is visible rather than a blank looping cursor. +cat >/home/liveuser/.bash_profile <<'EOF' +if [[ "$(tty)" == /dev/tty1 ]] && [[ -z "$WAYLAND_DISPLAY" ]]; then + export WLR_RENDERER_ALLOW_SOFTWARE=1 + export WLR_NO_HARDWARE_CURSORS=1 + # Log to a user-writable path (/var/log is root-only; redirecting there + # would fail and silently keep the compositor from ever launching). + Hyprland &>/tmp/hyprland-live.log + echo "Hyprland exited (rc=$?). Log: /tmp/hyprland-live.log" + exec bash -i +fi +EOF + +chown -R liveuser:liveuser /home/liveuser diff --git a/iso/airootfs/usr/local/bin/bos-session b/iso/airootfs/usr/local/bin/bos-session new file mode 100644 index 0000000..8fecd2c --- /dev/null +++ b/iso/airootfs/usr/local/bin/bos-session @@ -0,0 +1,15 @@ +#!/bin/bash +# BOS graphical session launcher, run by greetd on the INSTALLED system after +# the user authenticates (see /etc/greetd/config.toml). +# +# greetd does not start a login shell, so /etc/profile.d is never sourced — which +# means ~/.local/bin (where bakery installs the bread ecosystem: breadd, breadbar, +# breadbox-sync, …) would be missing from PATH and the Hyprland `exec-once` +# launches would fail. Source the login profile here so PATH is correct, set the +# Wayland session hints, then hand off to Hyprland. +source /etc/profile 2>/dev/null + +export XDG_SESSION_TYPE=wayland +export XDG_CURRENT_DESKTOP=Hyprland + +exec Hyprland diff --git a/iso/airootfs/usr/local/bin/bos-update b/iso/airootfs/usr/local/bin/bos-update new file mode 100644 index 0000000..42231ea --- /dev/null +++ b/iso/airootfs/usr/local/bin/bos-update @@ -0,0 +1,32 @@ +#!/bin/bash +# bos-update — update all of BOS in one go. +# +# BOS packages come from two channels, so a full update touches both: +# 1. pacman — Arch base/desktop + the [breadway] repo (bos-settings, etc.). +# Every transaction is snapshotted by snap-pac, so you can roll +# back from the GRUB "snapshots" submenu or BOS Settings. +# 2. bakery — the bread ecosystem apps in ~/.local/bin (bread, breadbar, +# breadbox, breadcrumbs, breadpad, breadman, bread-theme). +# +# Best-effort: a failure in one channel doesn't abort the other. +set -uo pipefail + +bold() { printf '\033[1m%s\033[0m\n' "$1"; } + +bold "==> System packages (pacman -Syu)" +if command -v pacman >/dev/null; then + sudo pacman -Syu || echo "WARN: pacman update failed" +else + echo "pacman not found; skipping" +fi + +echo +bold "==> Bread ecosystem (bakery update --all)" +if command -v bakery >/dev/null; then + bakery update --all || echo "WARN: bakery update failed" +else + echo "bakery not found; skipping" +fi + +echo +bold "==> BOS is up to date." diff --git a/iso/airootfs/usr/local/bin/bos-welcome b/iso/airootfs/usr/local/bin/bos-welcome new file mode 100644 index 0000000..413256b --- /dev/null +++ b/iso/airootfs/usr/local/bin/bos-welcome @@ -0,0 +1,15 @@ +#!/bin/bash +# First-run welcome. Shows a short getting-started message once, then drops a +# marker so it never shows again. Launched from the Hyprland autostart; the +# bos-welcome window class is floated/centred by a Hyprland window rule. +set -u + +# Never run in the live/installer session — only on an installed system. +[[ "$(id -un)" == "liveuser" ]] && exit 0 + +marker="${XDG_CONFIG_HOME:-$HOME/.config}/bos/.welcomed" +[[ -f "$marker" ]] && exit 0 +mkdir -p "$(dirname "$marker")" +touch "$marker" + +exec kitty --class bos-welcome --title "Welcome to BOS" -- less -R /usr/share/bos/welcome.txt diff --git a/iso/airootfs/usr/share/backgrounds/bos/bread-background.png b/iso/airootfs/usr/share/backgrounds/bos/bread-background.png new file mode 100644 index 0000000..12dee74 Binary files /dev/null and b/iso/airootfs/usr/share/backgrounds/bos/bread-background.png differ diff --git a/iso/airootfs/usr/share/bos/keybinds.txt b/iso/airootfs/usr/share/bos/keybinds.txt new file mode 100644 index 0000000..4ecf0b7 --- /dev/null +++ b/iso/airootfs/usr/share/bos/keybinds.txt @@ -0,0 +1,50 @@ + + ██████ ██████ ███████ keyboard shortcuts + ██ ██ ██ ██ ██ SUPER is the Windows/Cmd key + ██████ ██ ██ ███████ + ══════════════════════════════════════════════════════════ + + APPS & WINDOWS + SUPER + Return terminal (kitty) + SUPER + Space app launcher (breadbox) + SUPER + E files (nautilus) + SUPER + B browser (zen) + SUPER + U notes / reminders (breadpad) + SUPER + M package manager (breadman) + SUPER + , BOS Settings + SUPER + / this keybind cheatsheet + SUPER + L lock screen + SUPER + Backspace close window + SUPER + F fullscreen + SUPER + V toggle floating + SUPER + Shift + V clipboard history + SUPER + T toggle split direction + SUPER + Tab last window + SUPER + N exit Hyprland (log out) + + SCREENSHOTS + SUPER + Shift + S select region -> file + SUPER + Shift + C select region -> clipboard + SUPER + Shift + P whole screen -> file + + FOCUS & MOVE + SUPER + arrows move focus + SUPER + Shift + h/j/k/l move window + SUPER + Shift + arrows resize window + + WORKSPACES + SUPER + 1..0 switch to workspace 1..10 + SUPER + Shift + 1..0 move window to workspace + SUPER + [ / ] previous / next workspace + SUPER + Shift + [ / ] move window prev / next workspace + SUPER + scroll cycle workspaces + + MOUSE + SUPER + left-drag move window + SUPER + right-drag resize window + + MEDIA & HARDWARE KEYS + volume / brightness / play-pause / next / prev (work on lock screen) + + ────────────────────────────────────────────────────────── + Press q to close. Configure everything in BOS Settings (SUPER + ,). diff --git a/iso/airootfs/usr/share/bos/welcome.txt b/iso/airootfs/usr/share/bos/welcome.txt new file mode 100644 index 0000000..a412f4a --- /dev/null +++ b/iso/airootfs/usr/share/bos/welcome.txt @@ -0,0 +1,24 @@ + + Welcome to BOS — the Bread Operating System + ══════════════════════════════════════════════════════════ + + You're running a complete Hyprland desktop with the bread + ecosystem preinstalled. A few things to get you started: + + • SUPER + / show the keybind cheatsheet (any time) + • SUPER + , open BOS Settings — configure bread, the + bar, launcher, Wi-Fi profiles, notes, + snapshots and package updates, all in one + place (no config files needed) + • SUPER + Space the app launcher (breadbox) + • SUPER + Return a terminal + + The bar at the top (breadbar) shows workspaces, the clock, + system stats, and your tray. Notifications appear top-right. + + Your system is snapshotted on every package change — if an + update breaks something, roll back from BOS Settings or pick + a snapshot from the GRUB menu at boot. + + ────────────────────────────────────────────────────────── + Press q to close. This message won't show again. diff --git a/iso/airootfs/usr/share/fonts/TTF/VarelaRound-Regular.ttf b/iso/airootfs/usr/share/fonts/TTF/VarelaRound-Regular.ttf new file mode 100644 index 0000000..3a54b78 Binary files /dev/null and b/iso/airootfs/usr/share/fonts/TTF/VarelaRound-Regular.ttf differ diff --git a/iso/airootfs/usr/share/plymouth/themes/bos/bos.plymouth b/iso/airootfs/usr/share/plymouth/themes/bos/bos.plymouth new file mode 100644 index 0000000..76ec2ea --- /dev/null +++ b/iso/airootfs/usr/share/plymouth/themes/bos/bos.plymouth @@ -0,0 +1,8 @@ +[Plymouth Theme] +Name=BOS +Description=Bread Operating System boot splash — logo, spinner, status +ModuleName=script + +[script] +ImageDir=/usr/share/plymouth/themes/bos +ScriptFile=/usr/share/plymouth/themes/bos/bos.script diff --git a/iso/airootfs/usr/share/plymouth/themes/bos/bos.script b/iso/airootfs/usr/share/plymouth/themes/bos/bos.script new file mode 100644 index 0000000..1fe0c09 --- /dev/null +++ b/iso/airootfs/usr/share/plymouth/themes/bos/bos.script @@ -0,0 +1,49 @@ +# BOS Plymouth boot splash (script module). +# Black background, centred white BOS logo, a spinning accent ring, and a +# status line at the bottom. Colours match the bread palette (black base + warm accent). + +# --- background (#0c0c0c) --- +Window.SetBackgroundTopColor(0.047, 0.047, 0.047); +Window.SetBackgroundBottomColor(0.047, 0.047, 0.047); + +screen_w = Window.GetWidth(); +screen_h = Window.GetHeight(); + +# --- logo (centred, slightly above the middle) --- +logo.image = Image("logo.png"); +logo.sprite = Sprite(logo.image); +logo.x = screen_w / 2 - logo.image.GetWidth() / 2; +logo.y = screen_h / 2 - logo.image.GetHeight() / 2 - 40; +logo.sprite.SetX(logo.x); +logo.sprite.SetY(logo.y); +logo.sprite.SetZ(1); + +# --- spinner (rotating accent ring, below the logo) --- +spinner.image = Image("spinner.png"); +spinner.sprite = Sprite(); +spinner.cx = screen_w / 2; +spinner.cy = logo.y + logo.image.GetHeight() + 60; +spinner.angle = 0; + +fun refresh_callback() { + spinner.angle += 0.10; + if (spinner.angle > 6.28318) spinner.angle -= 6.28318; + rotated = spinner.image.Rotate(spinner.angle); + spinner.sprite.SetImage(rotated); + spinner.sprite.SetX(spinner.cx - rotated.GetWidth() / 2); + spinner.sprite.SetY(spinner.cy - rotated.GetHeight() / 2); + spinner.sprite.SetZ(2); +} +Plymouth.SetRefreshFunction(refresh_callback); + +# --- status line (cream text, near the bottom) --- +status.sprite = Sprite(); +fun show_status(text) { + status.image = Image.Text(text, 0.945, 0.863, 0.741); + status.sprite.SetImage(status.image); + status.sprite.SetX(screen_w / 2 - status.image.GetWidth() / 2); + status.sprite.SetY(screen_h * 0.84); + status.sprite.SetZ(2); +} +Plymouth.SetMessageFunction(show_status); +Plymouth.SetUpdateStatusFunction(show_status); diff --git a/iso/airootfs/usr/share/plymouth/themes/bos/logo.png b/iso/airootfs/usr/share/plymouth/themes/bos/logo.png new file mode 100644 index 0000000..fb96675 Binary files /dev/null and b/iso/airootfs/usr/share/plymouth/themes/bos/logo.png differ diff --git a/iso/airootfs/usr/share/plymouth/themes/bos/spinner.png b/iso/airootfs/usr/share/plymouth/themes/bos/spinner.png new file mode 100644 index 0000000..d82452c Binary files /dev/null and b/iso/airootfs/usr/share/plymouth/themes/bos/spinner.png differ diff --git a/iso/efiboot/loader/entries/01-archiso-linux-copytoram.conf b/iso/efiboot/loader/entries/01-archiso-linux-copytoram.conf new file mode 100644 index 0000000..289bd88 --- /dev/null +++ b/iso/efiboot/loader/entries/01-archiso-linux-copytoram.conf @@ -0,0 +1,5 @@ +title Bread OS install medium (copy to RAM, UEFI) +sort-key 015 +linux /%INSTALL_DIR%/boot/%ARCH%/vmlinuz-linux +initrd /%INSTALL_DIR%/boot/%ARCH%/initramfs-linux.img +options archisobasedir=%INSTALL_DIR% archisosearchuuid=%ARCHISO_UUID% copytoram=y diff --git a/iso/efiboot/loader/entries/01-archiso-linux.conf b/iso/efiboot/loader/entries/01-archiso-linux.conf new file mode 100644 index 0000000..d872e48 --- /dev/null +++ b/iso/efiboot/loader/entries/01-archiso-linux.conf @@ -0,0 +1,5 @@ +title Bread OS install medium (%ARCH%, UEFI) +sort-key 01 +linux /%INSTALL_DIR%/boot/%ARCH%/vmlinuz-linux +initrd /%INSTALL_DIR%/boot/%ARCH%/initramfs-linux.img +options archisobasedir=%INSTALL_DIR% archisosearchuuid=%ARCHISO_UUID% diff --git a/iso/efiboot/loader/entries/02-archiso-speech-linux.conf b/iso/efiboot/loader/entries/02-archiso-speech-linux.conf new file mode 100644 index 0000000..4551279 --- /dev/null +++ b/iso/efiboot/loader/entries/02-archiso-speech-linux.conf @@ -0,0 +1,5 @@ +title Bread OS install medium (%ARCH%, UEFI) with speech +sort-key 02 +linux /%INSTALL_DIR%/boot/%ARCH%/vmlinuz-linux +initrd /%INSTALL_DIR%/boot/%ARCH%/initramfs-linux.img +options archisobasedir=%INSTALL_DIR% archisosearchuuid=%ARCHISO_UUID% accessibility=on diff --git a/iso/efiboot/loader/entries/03-archiso-memtest86+x64.conf b/iso/efiboot/loader/entries/03-archiso-memtest86+x64.conf new file mode 100644 index 0000000..7a0ef5e --- /dev/null +++ b/iso/efiboot/loader/entries/03-archiso-memtest86+x64.conf @@ -0,0 +1,4 @@ +title Memtest86+ +sort-key 03 +efi /boot/memtest86+/memtest.efi +architecture x64 diff --git a/iso/efiboot/loader/loader.conf b/iso/efiboot/loader/loader.conf new file mode 100644 index 0000000..06d4ac4 --- /dev/null +++ b/iso/efiboot/loader/loader.conf @@ -0,0 +1,3 @@ +timeout 15 +default 01-archiso-linux.conf +beep on diff --git a/iso/grub/grub.cfg b/iso/grub/grub.cfg new file mode 100644 index 0000000..bf55211 --- /dev/null +++ b/iso/grub/grub.cfg @@ -0,0 +1,112 @@ +# Load partition table and file system modules +insmod part_gpt +insmod part_msdos +insmod fat +insmod iso9660 +insmod ntfs +insmod ntfscomp +insmod exfat +insmod udf + +# Use graphics-mode output +if loadfont "${prefix}/fonts/unicode.pf2" ; then + insmod all_video + set gfxmode="auto" + terminal_input console + terminal_output console +fi + +# Enable serial console +insmod serial +insmod usbserial_common +insmod usbserial_ftdi +insmod usbserial_pl2303 +insmod usbserial_usbdebug +if serial --unit=0 --speed=115200; then + terminal_input --append serial + terminal_output --append serial +fi + +# Get a human readable platform identifier +if [ "${grub_platform}" == 'efi' ]; then + archiso_platform='UEFI' +elif [ "${grub_platform}" == 'pc' ]; then + archiso_platform='BIOS' +else + archiso_platform="${grub_cpu}-${grub_platform}" +fi + +# Set default menu entry +default=archlinux +timeout=15 +timeout_style=menu + + +# Menu entries + +menuentry "Bread OS install medium (%ARCH%, ${archiso_platform})" --class arch --class gnu-linux --class gnu --class os --id 'archlinux' { + set gfxpayload=keep + linux /%INSTALL_DIR%/boot/%ARCH%/vmlinuz-linux archisobasedir=%INSTALL_DIR% archisosearchuuid=%ARCHISO_UUID% + initrd /%INSTALL_DIR%/boot/%ARCH%/initramfs-linux.img +} + +menuentry "Bread OS install medium with speakup screen reader (%ARCH%, ${archiso_platform})" --hotkey s --class arch --class gnu-linux --class gnu --class os --id 'archlinux-accessibility' { + set gfxpayload=keep + linux /%INSTALL_DIR%/boot/%ARCH%/vmlinuz-linux archisobasedir=%INSTALL_DIR% archisosearchuuid=%ARCHISO_UUID% accessibility=on + initrd /%INSTALL_DIR%/boot/%ARCH%/initramfs-linux.img +} + + +if [ "${grub_platform}" == 'efi' -a "${grub_cpu}" == 'x86_64' -a -f '/boot/memtest86+/memtest.efi' ]; then + menuentry 'Run Memtest86+ (RAM test)' --class memtest86 --class memtest --class gnu --class tool { + set gfxpayload=800x600,1024x768 + linux /boot/memtest86+/memtest.efi + } +fi +if [ "${grub_platform}" == 'pc' -a -f '/boot/memtest86+/memtest' ]; then + menuentry 'Run Memtest86+ (RAM test)' --class memtest86 --class memtest --class gnu --class tool { + set gfxpayload=800x600,1024x768 + linux /boot/memtest86+/memtest + } +fi +if [ "${grub_platform}" == 'efi' ]; then + if [ "${grub_cpu}" == 'x86_64' -a -f '/shellx64.efi' ]; then + menuentry 'UEFI Shell' --class efi { + chainloader /shellx64.efi + } + elif [ "${grub_cpu}" == 'i386' -a -f '/shellia32.efi' ]; then + menuentry 'UEFI Shell' --class efi { + chainloader /shellia32.efi + } + elif [ "${grub_cpu}" == 'arm64' -a -f '/shellaa64.efi' ]; then + menuentry 'UEFI Shell' --class efi { + chainloader /shellaa64.efi + } + elif [ "${grub_cpu}" == 'riscv64' -a -f '/shellriscv64.efi' ]; then + menuentry 'UEFI Shell' --class efi { + chainloader /shellriscv64.efi + } + elif [ "${grub_cpu}" == 'loongarch64' -a -f '/shellloongarch64.efi' ]; then + menuentry 'UEFI Shell' --class efi { + chainloader /shellloongarch64.efi + } + fi + + menuentry 'UEFI Firmware Settings' --id 'uefi-firmware' { + fwsetup + } +fi + +menuentry 'System shutdown' --class shutdown --class poweroff { + echo 'System shutting down...' + halt +} + +menuentry 'System restart' --class reboot --class restart { + echo 'System rebooting...' + reboot +} + + +# GRUB init tune for accessibility +play 600 988 1 1319 4 diff --git a/iso/grub/loopback.cfg b/iso/grub/loopback.cfg new file mode 100644 index 0000000..4bb311a --- /dev/null +++ b/iso/grub/loopback.cfg @@ -0,0 +1,85 @@ +# https://www.supergrubdisk.org/wiki/Loopback.cfg + +# Search for the ISO volume +search --no-floppy --set=archiso_img_dev --file "${iso_path}" +probe --set archiso_img_dev_uuid --fs-uuid "${archiso_img_dev}" + +# Get a human readable platform identifier +if [ "${grub_platform}" == 'efi' ]; then + archiso_platform='UEFI' +elif [ "${grub_platform}" == 'pc' ]; then + archiso_platform='BIOS' +else + archiso_platform="${grub_cpu}-${grub_platform}" +fi + +# Set default menu entry +default=archlinux +timeout=15 +timeout_style=menu + + +# Menu entries + +menuentry "Bread OS install medium (%ARCH%, ${archiso_platform})" --class arch --class gnu-linux --class gnu --class os --id 'archlinux' { + set gfxpayload=keep + linux /%INSTALL_DIR%/boot/%ARCH%/vmlinuz-linux archisobasedir=%INSTALL_DIR% img_dev=UUID=${archiso_img_dev_uuid} img_loop="${iso_path}" + initrd /%INSTALL_DIR%/boot/%ARCH%/initramfs-linux.img +} + +menuentry "Bread OS install medium with speakup screen reader (%ARCH%, ${archiso_platform})" --hotkey s --class arch --class gnu-linux --class gnu --class os --id 'archlinux-accessibility' { + set gfxpayload=keep + linux /%INSTALL_DIR%/boot/%ARCH%/vmlinuz-linux archisobasedir=%INSTALL_DIR% img_dev=UUID=${archiso_img_dev_uuid} img_loop="${iso_path}" accessibility=on + initrd /%INSTALL_DIR%/boot/%ARCH%/initramfs-linux.img +} + + +if [ "${grub_platform}" == 'efi' -a "${grub_cpu}" == 'x86_64' -a -f '/boot/memtest86+/memtest.efi' ]; then + menuentry 'Run Memtest86+ (RAM test)' --class memtest86 --class memtest --class gnu --class tool { + set gfxpayload=800x600,1024x768 + linux /boot/memtest86+/memtest.efi + } +fi +if [ "${grub_platform}" == 'pc' -a -f '/boot/memtest86+/memtest' ]; then + menuentry 'Run Memtest86+ (RAM test)' --class memtest86 --class memtest --class gnu --class tool { + set gfxpayload=800x600,1024x768 + linux /boot/memtest86+/memtest + } +fi +if [ "${grub_platform}" == 'efi' ]; then + if [ "${grub_cpu}" == 'x86_64' -a -f '/shellx64.efi' ]; then + menuentry 'UEFI Shell' --class efi { + chainloader /shellx64.efi + } + elif [ "${grub_cpu}" == 'i386' -a -f '/shellia32.efi' ]; then + menuentry 'UEFI Shell' --class efi { + chainloader /shellia32.efi + } + elif [ "${grub_cpu}" == 'arm64' -a -f '/shellaa64.efi' ]; then + menuentry 'UEFI Shell' --class efi { + chainloader /shellaa64.efi + } + elif [ "${grub_cpu}" == 'riscv64' -a -f '/shellriscv64.efi' ]; then + menuentry 'UEFI Shell' --class efi { + chainloader /shellriscv64.efi + } + elif [ "${grub_cpu}" == 'loongarch64' -a -f '/shellloongarch64.efi' ]; then + menuentry 'UEFI Shell' --class efi { + chainloader /shellloongarch64.efi + } + fi + + menuentry 'UEFI Firmware Settings' --id 'uefi-firmware' { + fwsetup + } +fi + +menuentry 'System shutdown' --class shutdown --class poweroff { + echo 'System shutting down...' + halt +} + +menuentry 'System restart' --class reboot --class restart { + echo 'System rebooting...' + reboot +} diff --git a/iso/packages.x86_64 b/iso/packages.x86_64 index 32c97d4..aa033d2 100644 --- a/iso/packages.x86_64 +++ b/iso/packages.x86_64 @@ -4,6 +4,21 @@ base-devel linux linux-firmware linux-headers +# CPU microcode — applied early by GRUB on the installed system (picked up by +# the bootloader module). amd-ucode for the dev laptop's Ryzen; intel-ucode for +# Intel targets. bos-copy-kernel also stages these into the live target /boot. +amd-ucode +intel-ucode + +# Power management (vendor-neutral; works on Intel and AMD). tlp auto-tunes power +# by AC/battery and CPU driver; hypridle/hyprlock handle idle dim/off/lock/suspend +# and the lock screen; upower exposes battery state. NOT power-profiles-daemon — +# it conflicts with tlp. +tlp +tlp-rdw +upower +hypridle +hyprlock # Bootloader + filesystem grub @@ -11,6 +26,22 @@ efibootmgr btrfs-progs dosfstools mtools +# squashfs-tools: provides unsquashfs, which Calamares' unpackfs module uses +# to extract airootfs.sfs onto the target during install. +squashfs-tools +# rsync: unpackfs copies the unpacked rootfs onto the target with rsync. +rsync +# Live-ISO boot (archiso bootmodes: bios.syslinux + uefi.systemd-boot) +# mkinitcpio-archiso provides the initramfs hooks that find and mount +# airootfs.sfs and switch root into it — without it the live ISO drops +# to emergency mode on boot. +mkinitcpio +mkinitcpio-archiso +mkinitcpio-nfs-utils +syslinux +memtest86+ +memtest86+-efi +edk2-shell # Snapshot infrastructure snapper @@ -21,6 +52,13 @@ inotify-tools # Wayland / Hyprland hyprland xdg-desktop-portal-hyprland +# GTK portal backend — file-chooser/screenshot portals for Flatpak, Electron, +# and Firefox-based apps (Zen). Without it those apps get no file dialog. +xdg-desktop-portal-gtk +# Login manager for the installed system (Wayland-native; enabled by +# post-install.sh, launches the Hyprland session via tuigreet → bos-session). +greetd +greetd-tuigreet xdg-utils xdg-user-dirs polkit @@ -37,15 +75,37 @@ pipewire-jack networkmanager network-manager-applet iw -iwd +# mDNS service/name resolution — lets CUPS auto-discover network printers and +# resolves .local hostnames (avahi-daemon enabled + nss-mdns wired in +# post-install.sh). +avahi +nss-mdns +# Wi-Fi backend for NetworkManager (its default; no extra config needed). +wpa_supplicant bluez bluez-utils +# blueman: GUI Bluetooth manager (pair/connect devices; breadbar shows status only). +blueman # GTK4 runtime gtk4 gtk4-layer-shell librsvg libpulse +# GTK3 dark theme (Adwaita-dark); without this package the gtk-theme-name in +# skel settings.ini silently falls back to the light theme for GTK3 apps. +gnome-themes-extra +# Schema + backend behind `gsettings set org.gnome.desktop.interface +# color-scheme prefer-dark` (set in hyprland.lua autostart). Without these the +# gsettings call fails silently and libadwaita apps (nautilus, gnome-text-editor) +# render in LIGHT mode regardless of the GTK theme. +gsettings-desktop-schemas +dconf +# Credential/keyring storage — browsers, SSH agents, and most apps persist +# passwords here; without it every session loses saved logins. seahorse is the +# GUI to view/manage the stored secrets and keys. +gnome-keyring +seahorse # Display (wlroots is bundled with Hyprland; don't list separately) wayland @@ -53,21 +113,85 @@ wayland-protocols # Fonts noto-fonts +noto-fonts-cjk noto-fonts-emoji ttf-jetbrains-mono +# Nerd font variant — icons in terminal tools (eza --icons, fastfetch, yazi) +ttf-jetbrains-mono-nerd +# Metric-compatible (Arial/Times/Courier) so Office/web docs lay out correctly, +# broad Unicode fallback, and the Font Awesome icon glyph set (otf-, the desktop +# variant — ttf-font-awesome resolves to the web-only woff2 build). +ttf-liberation +ttf-dejavu +otf-font-awesome # Terminal -foot +kitty # File manager nautilus +# gvfs: virtual filesystem layer for nautilus (trash, network places, removable +# media). gvfs-mtp adds Android/MTP device support (phones, tablets via USB). +gvfs +gvfs-mtp +# file-roller: archive manager — gives nautilus right-click Extract/Compress. +file-roller -# Installer — sourced from [breadway] repo (see pacman.conf) +# GUI applications a general desktop is expected to have out of the box. +# gnome-text-editor: graphical editor (terminal editors aside); gnome-calculator: +# calculator; loupe: Wayland-native image viewer (default for image files). +gnome-text-editor +gnome-calculator +loupe +# Media player — BOS ships gstreamer codecs but otherwise has no player app. +vlc +# Web browser (served from the [Breadway] repo; AUR zen-browser-bin republished +# there so the ISO build can pull it via pacman). mailcap satisfies zen's +# mime-types dependency explicitly. +zen-browser-bin +mailcap + +# Installer — Calamares is AUR-only; built in-house and served from [breadway] +# (calamares 3.4.x is already Qt6; there is no separate calamares-qt6 package) calamares -calamares-qt6 -# Bread ecosystem — sourced from [breadway] repo -bakery +# Bread ecosystem. +# +# The bread apps themselves (bakery, bread, breadbar, breadbox, breadcrumbs, +# breadpad) are NOT pacman packages here — they are bakery-managed binaries +# baked into /etc/skel/.local/bin at build time (see build-local.sh), so every +# user gets the exact versions from this laptop's bakery install with no +# network/DNS needed at install or runtime. Their runtime system deps are pulled +# in elsewhere in this list (gtk4, gtk4-layer-shell, iw, libpulse, librsvg, +# networkmanager, openssl, zlib, systemd-libs) — keep those even though no bread +# package depends on them. +# +# bos-settings is a BOS-specific pacman package (not part of the bakery index), +# so it stays here, served from the [breadway] repo. +bos-settings + +# Input / screen utilities +brightnessctl +grim +slurp +# Clipboard (Wayland copy/paste; also clipboard screenshots) and media keys. +wl-clipboard +playerctl +# Clipboard history daemon (stores wl-clipboard events; breadbox bind replays them). +cliphist +# Wallpaper daemon + pywal (drives the bread* colour palette from the wallpaper). +awww +python-pywal +# Boot splash (BOS logo + spinner instead of kernel text). +plymouth + +# Media codecs — GStreamer plugins for video thumbnails in nautilus, browser +# media, and general playback. bad/ugly add patent-encumbered formats (H.264 etc). +gst-plugins-good +gst-plugins-bad +gst-plugins-ugly +# GUI audio mixer — useful when output device needs manual switching. +pavucontrol # Utilities sudo @@ -82,5 +206,86 @@ man-db man-pages less +# Base CLI tools every install should have. +# Shell — zsh with the same prompt + plugins as the dev laptop. Powerlevel10k is +# AUR-only, so it's republished to [breadway] (see packaging/powerlevel10k). The +# three plugins come from the official repos; skel/.zshrc sources them in order +# (autosuggestions → history-substring-search → syntax-highlighting LAST). +zsh +zsh-theme-powerlevel10k +zsh-autosuggestions +zsh-history-substring-search +zsh-syntax-highlighting +# Editors +nano +micro +vim +neovim +# Shell QoL — modern replacements shipped with skel aliases set up +eza +bat +fzf +zoxide +# Fast search — pairs with fzf/zsh and underpins a good neovim experience +ripgrep +fd +# System / hardware inspection +htop +usbutils +pciutils +dmidecode +lsof +tree +fastfetch +# Removable-media filesystems (USB sticks, external drives) +ntfs-3g +exfatprogs +# Archives +7zip +zip +unrar +# Remote access (ssh client; sshd ships disabled) +openssh +# Mirror management (refresh /etc/pacman.d/mirrorlist for the user's location) +reflector + +# Printing — CUPS daemon + GUI printer setup. cups-pk-helper lets the GUI add +# printers via polkit without a root shell. cups.socket is enabled in +# post-install.sh so printing works on the installed system. +cups +cups-pk-helper +system-config-printer + +# Flatpak — sandboxed third-party app distribution (Flathub). The user adds a +# remote post-install (needs network); the runtime is shipped ready. +flatpak + +# Firewall — ufw, enabled deny-incoming in post-install.sh (mDNS allowed so +# printer discovery still works). +ufw +# Firmware updates via LVFS (works with gnome-software / fwupdmgr). +fwupd +# Compressed RAM swap — see /etc/systemd/zram-generator.conf. +zram-generator + +# Icon and cursor themes +# Papirus-Dark: cohesive icon set used as the BOS default (set via gsettings in +# hyprland.lua autostart and in skel gtk-3.0/settings.ini). +papirus-icon-theme +# Bibata-Modern-Ice: BOS default cursor. AUR-only upstream, republished to the +# [breadway] repo (see packaging/bibata + .forgejo/workflows/bibata.yml). Set via +# XCURSOR_THEME env in hyprland.lua and gtk settings.ini / gsettings. +bibata-cursor-theme-bin + +# Qt dark theme — makes Qt5/Qt6 apps (VLC, pavucontrol, etc.) respect the dark +# palette. qt5ct/qt6ct are configured via skel to use Fusion style in dark mode; +# QT_QPA_PLATFORMTHEME=qt5ct is set in hyprland.lua env. +qt5ct +qt6ct +# Native Wayland platform plugins for Qt — QT_QPA_PLATFORM=wayland (set in +# hyprland.lua) needs these or Qt apps fall back to (blurry) XWayland. +qt5-wayland +qt6-wayland + # Dev tools (for bos-settings standalone install) rustup diff --git a/iso/pacman.conf b/iso/pacman.conf index c071a87..90e4517 100644 --- a/iso/pacman.conf +++ b/iso/pacman.conf @@ -26,13 +26,19 @@ Include = /etc/pacman.d/mirrorlist Include = /etc/pacman.d/mirrorlist # ----------------------------------------------------------------------- -# Breadway custom repo — provides: bakery, calamares (pre-built), and the -# bread ecosystem packages (bread, breadbar, breadbox, breadcrumbs, breadpad, -# bos-settings). +# Breadway custom repo — provides: bakery and the bread ecosystem packages +# (bread, breadbar, breadbox, breadcrumbs, breadpad, bos-settings). +# (calamares comes from the official extra repo, not here.) # -# TODO: Replace this URL with the actual hosted repo before building. -# See: https://github.com/Breadway/repo for setup instructions. +# Packages are published to the Forgejo Arch registry (group "os") by the +# .forgejo/workflows/package.yml workflow in each repo, on tag push. +# +# Forgejo signs the repo db with a key pacman can't look up, so TrustAll +# fails. SigLevel = Never skips verification (acceptable for this private +# repo over TLS). TODO: import Forgejo's signing key + SigLevel = Required. # ----------------------------------------------------------------------- -[breadway] -SigLevel = Optional TrustAll -Server = https://repo.breadway.dev/$arch +# The section name must match Forgejo's served db filename +# ({owner}.{group}.{domain}.db) — pacman fetches "
.db" from Server. +[Breadway.os.git.breadway.dev] +SigLevel = Never +Server = https://git.breadway.dev/api/packages/Breadway/arch/os/$arch diff --git a/iso/profiledef.sh b/iso/profiledef.sh index 8f5cb15..709071a 100644 --- a/iso/profiledef.sh +++ b/iso/profiledef.sh @@ -8,17 +8,20 @@ iso_application="Bread Operating System" iso_version="$(date +%Y.%m.%d)" install_dir="arch" buildmodes=('iso') -bootmodes=( - 'bios.syslinux.mbr' - 'bios.syslinux.eltorito' - 'uefi-x64.systemd-boot.esp' - 'uefi-x64.systemd-boot.eltorito' -) +bootmodes=('bios.syslinux' 'uefi.systemd-boot') arch="x86_64" pacman_conf="pacman.conf" airootfs_image_type="squashfs" airootfs_image_tool_options=('-comp' 'xz' '-Xbcj' 'x86' '-b' '1M' '-Xdict-size' '1M') file_permissions=( ["/etc/shadow"]="0:0:400" + ["/etc/sudoers.d/99-bos-live"]="0:0:440" ["/etc/calamares/post-install.sh"]="0:0:755" + ["/usr/local/bin/bos-live-setup"]="0:0:755" + ["/usr/local/bin/bos-launch-calamares"]="0:0:755" + ["/usr/local/bin/bos-copy-kernel"]="0:0:755" + ["/usr/local/bin/bos-session"]="0:0:755" + ["/usr/local/bin/bos-keybinds"]="0:0:755" + ["/usr/local/bin/bos-welcome"]="0:0:755" + ["/usr/local/bin/bos-update"]="0:0:755" ) diff --git a/iso/syslinux/archiso_head.cfg b/iso/syslinux/archiso_head.cfg new file mode 100644 index 0000000..9acd03a --- /dev/null +++ b/iso/syslinux/archiso_head.cfg @@ -0,0 +1,28 @@ +SERIAL 0 115200 +UI vesamenu.c32 +MENU TITLE Bread OS +MENU BACKGROUND splash.png + +MENU WIDTH 78 +MENU MARGIN 4 +MENU ROWS 7 +MENU VSHIFT 10 +MENU TABMSGROW 14 +MENU CMDLINEROW 14 +MENU HELPMSGROW 16 +MENU HELPMSGENDROW 29 + +# Refer to https://wiki.syslinux.org/wiki/index.php/Comboot/menu.c32 + +MENU COLOR border 30;44 #40ffffff #a0000000 std +MENU COLOR title 1;36;44 #9033ccff #a0000000 std +MENU COLOR sel 7;37;40 #e0ffffff #20ffffff all +MENU COLOR unsel 37;44 #50ffffff #a0000000 std +MENU COLOR help 37;40 #c0ffffff #a0000000 std +MENU COLOR timeout_msg 37;40 #80ffffff #00000000 std +MENU COLOR timeout 1;37;40 #c0ffffff #00000000 std +MENU COLOR msg07 37;40 #90ffffff #a0000000 std +MENU COLOR tabmsg 31;40 #30ffffff #00000000 std + +MENU CLEAR +MENU IMMEDIATE diff --git a/iso/syslinux/archiso_pxe-linux.cfg b/iso/syslinux/archiso_pxe-linux.cfg new file mode 100644 index 0000000..2e93f12 --- /dev/null +++ b/iso/syslinux/archiso_pxe-linux.cfg @@ -0,0 +1,32 @@ +LABEL arch_nbd +TEXT HELP +Boot the Bread OS install medium using NBD. +It allows you to install Bread OS or perform system maintenance. +ENDTEXT +MENU LABEL Bread OS install medium (%ARCH%, NBD) +LINUX ::/%INSTALL_DIR%/boot/%ARCH%/vmlinuz-linux +INITRD ::/%INSTALL_DIR%/boot/%ARCH%/initramfs-linux.img +APPEND archisobasedir=%INSTALL_DIR% archisosearchuuid=%ARCHISO_UUID% archiso_nbd_srv=${pxeserver} cms_verify=y +SYSAPPEND 3 + +LABEL arch_nfs +TEXT HELP +Boot the Bread OS live medium using NFS. +It allows you to install Bread OS or perform system maintenance. +ENDTEXT +MENU LABEL Bread OS install medium (%ARCH%, NFS) +LINUX ::/%INSTALL_DIR%/boot/%ARCH%/vmlinuz-linux +INITRD ::/%INSTALL_DIR%/boot/%ARCH%/initramfs-linux.img +APPEND archisobasedir=%INSTALL_DIR% archiso_nfs_srv=${pxeserver}:/run/archiso/bootmnt cms_verify=y +SYSAPPEND 3 + +LABEL arch_http +TEXT HELP +Boot the Bread OS live medium using HTTP. +It allows you to install Bread OS or perform system maintenance. +ENDTEXT +MENU LABEL Bread OS install medium (%ARCH%, HTTP) +LINUX ::/%INSTALL_DIR%/boot/%ARCH%/vmlinuz-linux +INITRD ::/%INSTALL_DIR%/boot/%ARCH%/initramfs-linux.img +APPEND archisobasedir=%INSTALL_DIR% archiso_http_srv=http://${pxeserver}/ cms_verify=y +SYSAPPEND 3 diff --git a/iso/syslinux/archiso_pxe.cfg b/iso/syslinux/archiso_pxe.cfg new file mode 100644 index 0000000..b4c9a80 --- /dev/null +++ b/iso/syslinux/archiso_pxe.cfg @@ -0,0 +1,5 @@ +INCLUDE archiso_head.cfg + +INCLUDE archiso_pxe-linux.cfg + +INCLUDE archiso_tail.cfg diff --git a/iso/syslinux/archiso_sys-linux.cfg b/iso/syslinux/archiso_sys-linux.cfg new file mode 100644 index 0000000..0ec10f1 --- /dev/null +++ b/iso/syslinux/archiso_sys-linux.cfg @@ -0,0 +1,33 @@ +LABEL arch +TEXT HELP +Boot the Bread OS install medium on BIOS. +It allows you to install Bread OS or perform system maintenance. +ENDTEXT +MENU LABEL Bread OS install medium (%ARCH%, BIOS) +LINUX /%INSTALL_DIR%/boot/%ARCH%/vmlinuz-linux +INITRD /%INSTALL_DIR%/boot/%ARCH%/initramfs-linux.img +APPEND archisobasedir=%INSTALL_DIR% archisosearchuuid=%ARCHISO_UUID% + +# Copy-to-RAM boot option — loads airootfs.sfs entirely into RAM, so the +# installer reads from memory rather than a possibly-flaky USB (avoids SquashFS +# read errors during unpackfs). Needs enough RAM for the image (~3 GB). +LABEL archtoram +TEXT HELP +Boot Bread OS, copying the image into RAM first. +More reliable installs from USB; needs a few GB of RAM. +ENDTEXT +MENU LABEL Bread OS install medium (%ARCH%, BIOS) ^copy to RAM +LINUX /%INSTALL_DIR%/boot/%ARCH%/vmlinuz-linux +INITRD /%INSTALL_DIR%/boot/%ARCH%/initramfs-linux.img +APPEND archisobasedir=%INSTALL_DIR% archisosearchuuid=%ARCHISO_UUID% copytoram=y + +# Accessibility boot option +LABEL archspeech +TEXT HELP +Boot the Bread OS install medium on BIOS with speakup screen reader. +It allows you to install Bread OS or perform system maintenance with speech feedback. +ENDTEXT +MENU LABEL Bread OS install medium (%ARCH%, BIOS) with ^speech +LINUX /%INSTALL_DIR%/boot/%ARCH%/vmlinuz-linux +INITRD /%INSTALL_DIR%/boot/%ARCH%/initramfs-linux.img +APPEND archisobasedir=%INSTALL_DIR% archisosearchuuid=%ARCHISO_UUID% accessibility=on diff --git a/iso/syslinux/archiso_sys.cfg b/iso/syslinux/archiso_sys.cfg new file mode 100644 index 0000000..662482c --- /dev/null +++ b/iso/syslinux/archiso_sys.cfg @@ -0,0 +1,8 @@ +INCLUDE archiso_head.cfg + +DEFAULT arch +TIMEOUT 150 + +INCLUDE archiso_sys-linux.cfg + +INCLUDE archiso_tail.cfg diff --git a/iso/syslinux/archiso_tail.cfg b/iso/syslinux/archiso_tail.cfg new file mode 100644 index 0000000..e84897c --- /dev/null +++ b/iso/syslinux/archiso_tail.cfg @@ -0,0 +1,35 @@ +LABEL existing +TEXT HELP +Boot an existing operating system. +Press TAB to edit the disk and partition number to boot. +ENDTEXT +MENU LABEL Boot existing OS +COM32 chain.c32 +APPEND hd0 0 + +# https://www.memtest.org/ +LABEL memtest +MENU LABEL Run Memtest86+ (RAM test) +LINUX /boot/memtest86+/memtest + +# https://wiki.syslinux.org/wiki/index.php/Hdt_(Hardware_Detection_Tool) +LABEL hdt +MENU LABEL Hardware Information (HDT) +COM32 hdt.c32 +APPEND modules_alias=hdt/modalias.gz pciids=hdt/pciids.gz + +LABEL reboot +TEXT HELP +Reboot computer. +The computer's firmware must support APM. +ENDTEXT +MENU LABEL Reboot +COM32 reboot.c32 + +LABEL poweroff +TEXT HELP +Power off computer. +The computer's firmware must support APM. +ENDTEXT +MENU LABEL Power Off +COM32 poweroff.c32 diff --git a/iso/syslinux/splash.png b/iso/syslinux/splash.png new file mode 100644 index 0000000..64b959a Binary files /dev/null and b/iso/syslinux/splash.png differ diff --git a/iso/syslinux/syslinux.cfg b/iso/syslinux/syslinux.cfg new file mode 100644 index 0000000..cbda72f --- /dev/null +++ b/iso/syslinux/syslinux.cfg @@ -0,0 +1,11 @@ +DEFAULT select + +LABEL select +COM32 whichsys.c32 +APPEND -pxe- pxe -sys- sys -iso- sys + +LABEL pxe +CONFIG archiso_pxe.cfg + +LABEL sys +CONFIG archiso_sys.cfg diff --git a/packaging/arch/PKGBUILD b/packaging/arch/PKGBUILD new file mode 100644 index 0000000..6747f88 --- /dev/null +++ b/packaging/arch/PKGBUILD @@ -0,0 +1,38 @@ +# Maintainer: Breadway + +pkgname=bos-settings +pkgver=0.1.0 +pkgrel=1 +pkgdesc="System settings app for Bread OS" +arch=('x86_64') +url="https://github.com/Breadway/bos" +license=('MIT') +# Some Rust deps (ring/mlua) build vendored C/asm into static archives; makepkg's +# default -flto=auto emits GCC LTO bitcode the Rust (lld) link cannot read, +# causing undefined-symbol errors. Disable LTO. +options=(!lto !debug) +depends=('gtk4' 'glib2' 'hicolor-icon-theme') +optdepends=( + 'snapper: snapshot management view' +) +makedepends=('rust' 'cargo') +source=("${pkgname}-${pkgver}.tar.gz") +sha256sums=('SKIP') + +build() { + cd "${srcdir}/${pkgname}-${pkgver}" + cargo build --release --locked -p bos-settings +} + +check() { + cd "${srcdir}/${pkgname}-${pkgver}" + cargo test --release --locked -p bos-settings +} + +package() { + cd "${srcdir}/${pkgname}-${pkgver}" + install -Dm755 target/release/bos-settings "${pkgdir}/usr/bin/bos-settings" + install -Dm644 packaging/arch/bos-settings.desktop \ + "${pkgdir}/usr/share/applications/bos-settings.desktop" + install -Dm644 LICENSE "${pkgdir}/usr/share/licenses/${pkgname}/LICENSE" +} diff --git a/packaging/arch/README.md b/packaging/arch/README.md new file mode 100644 index 0000000..af5acc0 --- /dev/null +++ b/packaging/arch/README.md @@ -0,0 +1,25 @@ +Arch packaging +============== + +`PKGBUILD` builds and installs `bos-settings` from source. + +## Local build + +```bash +makepkg -si +``` + +## Before publishing to [breadway] repo + +1. Tag a release on GitHub. +2. Update `pkgver` to match the tag. +3. Update `source` to the release tarball URL. +4. Run `updpkgsums` (or manually set `sha256sums`). + +## Runtime dependencies + +| Package | Required | Notes | +|---------|----------|-------| +| `gtk4` | yes | UI toolkit | +| `glib2` | yes | always | +| `snapper` | optional | snapshot management view | diff --git a/packaging/arch/bos-settings.desktop b/packaging/arch/bos-settings.desktop new file mode 100644 index 0000000..5385adb --- /dev/null +++ b/packaging/arch/bos-settings.desktop @@ -0,0 +1,9 @@ +[Desktop Entry] +Name=BOS Settings +Comment=System settings for Bread OS +Exec=bos-settings +Icon=preferences-system +Terminal=false +Type=Application +Categories=Settings;System; +StartupWMClass=bos-settings diff --git a/packaging/bibata/PKGBUILD b/packaging/bibata/PKGBUILD new file mode 100644 index 0000000..b49e382 --- /dev/null +++ b/packaging/bibata/PKGBUILD @@ -0,0 +1,22 @@ +# BOS in-house rebuild of bibata-cursor-theme-bin (AUR-only upstream). +# Bibata is the BOS default cursor theme; the AUR package is republished to the +# [breadway] repo so the ISO build can pull it via pacman (same pattern as +# zen-browser-bin and calamares). Prebuilt release tarball — no build step. +# Upstream maintainer: Mark Wagie +pkgname=bibata-cursor-theme-bin +pkgver=2.0.7 +pkgrel=1 +pkgdesc="Material Based Cursor Theme" +arch=('any') +url="https://github.com/ful1e5/Bibata_Cursor" +license=('GPL-3.0-or-later') +provides=("${pkgname%-bin}") +conflicts=("${pkgname%-bin}") +options=('!strip') +source=("${pkgname%-bin}-$pkgver.tar.xz::$url/releases/download/v$pkgver/Bibata.tar.xz") +sha256sums=('172e33c4ae415278384dcecc7d1a9b7a024266bc944bc751fd86532be1cc6251') + +package() { + install -d "$pkgdir/usr/share/icons" + cp -r Bibata* "$pkgdir/usr/share/icons" +} diff --git a/packaging/calamares/PKGBUILD b/packaging/calamares/PKGBUILD new file mode 100644 index 0000000..494ae31 --- /dev/null +++ b/packaging/calamares/PKGBUILD @@ -0,0 +1,92 @@ +# Maintainer: Breadway +# In-house copy of the AUR calamares PKGBUILD (Calamares is AUR-only; not in +# Arch's official repos). Built by CI and published to the [breadway] repo. +# Source of truth: https://aur.archlinux.org/packages/calamares +# Contributor: Rustmilian Rustmilian@proton.me + +_pkgname="calamares" +pkgname="$_pkgname" +pkgver=3.4.2 +pkgrel=2 +pkgdesc="Distribution-independent installer framework" +url="https://codeberg.org/Calamares/calamares" +license=("GPL-3.0-or-later") +arch=('i686' 'x86_64') +options=(!debug) + +depends=( + 'kcoreaddons' + 'kpmcore' + 'libpwquality' + 'qt6-declarative' + 'qt6-svg' + 'yaml-cpp' +) +makedepends=( + 'extra-cmake-modules' + 'libglvnd' + 'ninja' + 'qt6-tools' + 'qt6-translations' +) + +_pkgext="tar.gz" +source=("$_pkgname-$pkgver.$_pkgext"::"$url/releases/download/v$pkgver/$_pkgname-$pkgver.$_pkgext") +sha256sums=('733bbbb00dc9f84874bd5c22960952f317ea2537565431179fa2152b2fbfdccc') + +build() { + local _skip_modules=( + dracut + dracutlukscfg + dummycpp + dummyprocess + dummypython + dummypythonqt + initramfs + initramfscfg + interactiveterminal + packagechooser + packagechooserq + services-openrc + ) + + # Correct source directory (inside src/) + local _pkgsrc_dir="$srcdir/$_pkgname-$pkgver" + + local _cmake_options=( + -B build + -S "$_pkgsrc_dir" + -G Ninja + -DCMAKE_BUILD_TYPE=Release + -DCMAKE_INSTALL_PREFIX='/usr' + -DCMAKE_INSTALL_LIBDIR='lib' + -DWITH_QT6=ON + -DINSTALL_CONFIG=ON + -DSKIP_MODULES="${_skip_modules[*]}" + -DBUILD_TESTING=OFF + + # Explicit KDE install dirs to suppress warnings + -DKDE_INSTALL_BINDIR=/usr/bin + -DKDE_INSTALL_SBINDIR=/usr/sbin + -DKDE_INSTALL_LIBDIR=/usr/lib + -DKDE_INSTALL_LIBEXECDIR=/usr/libexec + -DKDE_INSTALL_INCLUDEDIR=/usr/include + -DKDE_INSTALL_LOCALSTATEDIR=/var + -DKDE_INSTALL_SHAREDSTATEDIR=/usr/share + -DKDE_INSTALL_DATAROOTDIR=/usr/share + -DKDE_INSTALL_DATADIR=/usr/share + -DKDE_INSTALL_LOCALEDIR=/usr/share/locale + -DKDE_INSTALL_MANDIR=/usr/share/man + -DKDE_INSTALL_INFODIR=/usr/share/info + -DKDE_INSTALL_SYSCONFDIR=/etc + + -Wno-dev + ) + + cmake "${_cmake_options[@]}" + cmake --build build +} + +package() { + DESTDIR="$pkgdir" cmake --install build +} diff --git a/packaging/powerlevel10k/PKGBUILD b/packaging/powerlevel10k/PKGBUILD new file mode 100644 index 0000000..472777d --- /dev/null +++ b/packaging/powerlevel10k/PKGBUILD @@ -0,0 +1,105 @@ +# BOS in-house rebuild of zsh-theme-powerlevel10k (AUR-only upstream). +# Republished to [breadway] so the ISO can pull the BOS default prompt via pacman +# (same pattern as bibata / zen-browser-bin). Upstream maintainer header kept below. +# Maintainer: Mark Wagie +# Contributor: Christian Rebischke +# Contributor: Jeff Henson +# Contributor: Ron Asimi +# Contributor: Roman Perepelitsa +pkgname=zsh-theme-powerlevel10k +# Whenever pkgver is updated, _libgit2ver below must also be updated. +pkgver=1.20.17 ## see P9K_VERSION in internal/p10k.zsh +_libgit2ver="tag-2ecf33948a4df9ef45a66c68b8ef24a5e60eaac6" +pkgrel=1 +epoch=1 +pkgdesc="Powerlevel10k is a theme for Zsh. It emphasizes speed, flexibility and out-of-the-box experience." +arch=('x86_64' 'aarch64') +url='https://github.com/romkatv/powerlevel10k' +license=('MIT') +depends=( + 'glibc' + 'zsh' +) +makedepends=( + 'git' + 'cmake' +) +optdepends=( + # It works well with Nerd Fonts, Source Code Pro, Font Awesome, Powerline, + # and even the default system fonts. The full choice of style options is + # available only when using Nerd Fonts. + 'ttf-meslo-nerd-font-powerlevel10k: recommended font' + 'powerline-fonts: patched fonts for powerline' + 'ttf-font-nerd: full choice of style options' +) +replaces=('zsh-theme-powerlevel9k') +_commit=9253fb1c5034410c43a0c681ff8294181c54016c + +# _libgit2ver depends on pkgver. They must be updated together. See libgit2_version in: +# https://raw.githubusercontent.com/romkatv/powerlevel10k/v${pkgver}/gitstatus/build.info +source=( + "git+https://github.com/romkatv/powerlevel10k.git#commit=${_commit}" +# "powerlevel10k-${pkgver}.tar.gz::https://github.com/romkatv/powerlevel10k/archive/v${pkgver}.tar.gz" +# "https://github.com/romkatv/powerlevel10k/releases/download/v$pkgver/powerlevel10k-$pkgver.tar.gz.asc" + "libgit2-${_libgit2ver}.tar.gz::https://github.com/romkatv/libgit2/archive/${_libgit2ver}.tar.gz") +sha256sums=('f0edc2cc5bfcdfcf3b94f10597c252873567a990e651d04059c887046fba6701' + '4ce11d71ee576dbbc410b9fa33a9642809cc1fa687b315f7c23eeb825b251e93') +#validpgpkeys=('8B060F8B9EB395614A669F2A90ACE942EB90C3DD') # Roman Perepelitsa + +build() { + cd "libgit2-${_libgit2ver}" + local cmake_options=( + -W no-dev + -D CMAKE_BUILD_TYPE='None' + -D ZERO_NSEC='ON' + -D THREADSAFE='ON' + -D USE_BUNDLED_ZLIB='ON' + -D REGEX_BACKEND='builtin' + -D USE_HTTP_PARSER='builtin' + -D USE_SSH='OFF' + -D USE_HTTPS='OFF' + -D BUILD_CLAR='OFF' + -D USE_GSSAPI='OFF' + -D USE_NTLMCLIENT='OFF' + -D BUILD_SHARED_LIBS='OFF' + -D ENABLE_REPRODUCIBLE_BUILDS='ON' + ) + cmake "${cmake_options[@]}" . + make + + # build gitstatus + cd "$srcdir/powerlevel10k/gitstatus" + export CXXFLAGS+=" -I${srcdir}/libgit2-${_libgit2ver}/include -DGITSTATUS_ZERO_NSEC -D_GNU_SOURCE" + export LDFLAGS+=" -L${srcdir}/libgit2-${_libgit2ver}" + make +} + +package() { + cd powerlevel10k + find . -type f -exec install -D '{}' "$pkgdir/usr/share/${pkgname}/{}" ';' + + install -d "${pkgdir}/usr/share/licenses/${pkgname}" + ln -s "/usr/share/${pkgname}/LICENSE" "${pkgdir}/usr/share/licenses/${pkgname}" + + # delete unnecessary files. See also: https://bugs.archlinux.org/task/66737 + rm -r "${pkgdir}/usr/share/${pkgname}/.git" + rm -r "${pkgdir}/usr/share/${pkgname}/gitstatus/deps/" + rm -r "${pkgdir}/usr/share/${pkgname}/gitstatus/obj" + rm -r "${pkgdir}/usr/share/${pkgname}/gitstatus/src/" + rm -r "${pkgdir}/usr/share/${pkgname}/gitstatus/.vscode/" + rm "${pkgdir}/usr/share/${pkgname}/.gitattributes" + rm "${pkgdir}/usr/share/${pkgname}/.gitignore" + rm "${pkgdir}/usr/share/${pkgname}/Makefile" + rm "${pkgdir}/usr/share/${pkgname}/gitstatus/build" + rm "${pkgdir}/usr/share/${pkgname}/gitstatus/Makefile" + rm "${pkgdir}/usr/share/${pkgname}/gitstatus/mbuild" + rm "${pkgdir}/usr/share/${pkgname}/gitstatus/.clang-format" + rm "${pkgdir}/usr/share/${pkgname}/gitstatus/.gitignore" + rm "${pkgdir}/usr/share/${pkgname}/gitstatus/.gitattributes" + rm "${pkgdir}/usr/share/${pkgname}/gitstatus/usrbin/.gitkeep" + + cd "${pkgdir}/usr/share/${pkgname}" + for file in *.zsh-theme internal/*.zsh gitstatus/*.zsh gitstatus/install; do + zsh -fc "emulate zsh -o no_aliases && zcompile -R -- $file.zwc $file" + done +} diff --git a/scripts/smoke-test.sh b/scripts/smoke-test.sh new file mode 100755 index 0000000..a551253 --- /dev/null +++ b/scripts/smoke-test.sh @@ -0,0 +1,68 @@ +#!/usr/bin/env bash +# BOS post-install smoke test. +# +# Run this INSIDE a freshly installed BOS (as the main user) to assert the +# install's core invariants. It is read-only and safe to run any time. +# +# ./smoke-test.sh +# +# Exit status is non-zero if any check fails, so it can gate CI / manual QA. +set -uo pipefail + +pass=0 fail=0 +ok() { printf ' \033[32mPASS\033[0m %s\n' "$1"; pass=$((pass+1)); } +bad() { printf ' \033[31mFAIL\033[0m %s\n' "$1"; fail=$((fail+1)); } +note() { printf ' \033[33m----\033[0m %s\n' "$1"; } + +check() { if eval "$2" >/dev/null 2>&1; then ok "$1"; else bad "$1"; fi; } + +echo "== btrfs subvolume layout ==" +if command -v btrfs >/dev/null; then + # `btrfs subvolume list` needs root; try unprivileged, fall back to non- + # interactive sudo (no hang if creds aren't cached). + paths="$(btrfs subvolume list / 2>/dev/null || sudo -n btrfs subvolume list / 2>/dev/null)" + paths="$(awk '{print $NF}' <<<"$paths")" + if [ -z "$paths" ]; then + note "couldn't list subvolumes (need root) — skipping" + else + for sv in @ @home @snapshots @log @cache; do + if grep -qx "$sv" <<<"$paths"; then ok "subvolume $sv present"; else bad "subvolume $sv missing"; fi + done + fi +else + note "btrfs not installed (not a btrfs root?) — skipping subvolume checks" +fi + +echo "== snapshot tooling ==" +check "snapper root config exists" "[ -f /etc/snapper/configs/root ]" +check "snap-pac hook present" "pacman -Qq snap-pac" +check "grub-btrfs present" "pacman -Qq grub-btrfs" + +echo "== enabled system services ==" +for unit in NetworkManager.service greetd.service bluetooth.service tlp.service \ + cups.socket avahi-daemon.service ufw.service systemd-timesyncd.service; do + check "$unit enabled" "systemctl is-enabled $unit" +done +check "graphical.target is default" "[ \"\$(systemctl get-default)\" = graphical.target ]" + +echo "== bread ecosystem on PATH ==" +for bin in bakery bread breadd breadbar breadbox breadbox-sync breadcrumbs breadpad breadman; do + check "$bin found" "command -v $bin" +done + +echo "== bos-settings ==" +check "bos-settings installed" "command -v bos-settings" + +echo "== default dotfiles ==" +check "hyprland.lua present" "[ -f \"\$HOME/.config/hypr/hyprland.lua\" ]" +check "mimeapps.list present" "[ -f \"\$HOME/.config/mimeapps.list\" ]" +check "kitty config present" "[ -f \"\$HOME/.config/kitty/kitty.conf\" ]" + +echo "== bootloader (EFI) ==" +check "GRUB EFI binary present" \ + "[ -f /boot/efi/EFI/BOS/grubx64.efi ] || [ -f /boot/efi/EFI/BOOT/BOOTX64.EFI ]" +check "grub.cfg present" "[ -f /boot/grub/grub.cfg ]" + +echo +printf 'Result: \033[32m%d passed\033[0m, \033[31m%d failed\033[0m\n' "$pass" "$fail" +[ "$fail" -eq 0 ]