Compare commits
No commits in common. "main" and "v0.2.1" have entirely different histories.
23 changed files with 155 additions and 1603 deletions
|
|
@ -1,21 +0,0 @@
|
|||
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/bread-ecosystem.git" \
|
||||
'+refs/heads/*:refs/heads/*' '+refs/tags/*:refs/tags/*'
|
||||
|
|
@ -1,40 +0,0 @@
|
|||
name: Build and publish package
|
||||
|
||||
on:
|
||||
push:
|
||||
tags: ['v*']
|
||||
|
||||
jobs:
|
||||
package:
|
||||
runs-on: [self-hosted, hestia]
|
||||
container:
|
||||
image: archlinux:latest
|
||||
steps:
|
||||
# Note: no actions/checkout — the archlinux image has no Node, which JS
|
||||
# actions require. Everything runs as shell steps and clones manually.
|
||||
- name: Build and publish
|
||||
env:
|
||||
PUBLISH_TOKEN: ${{ secrets.REGISTRY_TOKEN }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
VERSION="${GITHUB_REF_NAME#v}"
|
||||
pacman -Syu --noconfirm base-devel git rust cargo
|
||||
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="bakery-${VERSION}/" HEAD \
|
||||
> packaging/arch/bakery-${VERSION}.tar.gz
|
||||
SHA=$(sha256sum packaging/arch/bakery-${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"
|
||||
|
|
@ -1,118 +0,0 @@
|
|||
# Bread Design System
|
||||
|
||||
Unified visual identity for breadbar, breadbox, breadpad/breadman, and
|
||||
bos-settings.
|
||||
|
||||
## Architecture (single source of truth)
|
||||
|
||||
The tokens below are implemented once in the **`bread-theme`** crate as
|
||||
`stylesheet(&Palette)` — the full component stylesheet (buttons, entries,
|
||||
switches, lists/rows/sidebars, cards, chips, scrollbars, headings) over a
|
||||
canonical `@define-color` palette (`surface`=color0, `overlay`=color7,
|
||||
`accent`=color4).
|
||||
|
||||
- The `bread-theme` **CLI** renders it from the live pywal palette to
|
||||
`$XDG_RUNTIME_DIR/bread/theme.css` (run at login and from a pywal hook).
|
||||
- Every GUI loads that file via `bread_theme::gtk::apply_shared()` and
|
||||
**live-reloads** it, then layers on only its own app-specific rules.
|
||||
|
||||
Result: one definition, no per-app drift, and palette changes recolour the
|
||||
whole desktop with no rebuilds. Apps reference the shared `@define-color`
|
||||
names rather than raw palette slots.
|
||||
|
||||
## Typography
|
||||
|
||||
- **Font Family**: Varela Round, sans-serif
|
||||
- **Base Size**: 14px
|
||||
- **Secondary**: 12px (metadata, helper text, secondary labels)
|
||||
- **Font Weight**: Normal (400) for body, Bold (700) for emphasis
|
||||
|
||||
## Spacing Scale (4px units)
|
||||
|
||||
Use these values consistently across all projects:
|
||||
|
||||
- **xs**: 4px (small gaps, internal padding)
|
||||
- **sm**: 8px (default spacing between elements)
|
||||
- **md**: 12px (medium spacing, main padding)
|
||||
- **lg**: 16px (large padding, major spacing)
|
||||
- **xl**: 20px (extra large spacing, section breaks)
|
||||
|
||||
## Border Radius
|
||||
|
||||
Establish a visual hierarchy with consistent rounding:
|
||||
|
||||
- **Primary** (buttons, cards, main containers): **8px**
|
||||
- **Secondary** (input fields, chips, entries): **6px**
|
||||
- **Tertiary** (small interactive elements): **4px**
|
||||
- **Pill** (fully rounded buttons, badges): **999px**
|
||||
|
||||
## Color System
|
||||
|
||||
All projects use **pywal dynamic theming** with **Catppuccin Mocha** as the fallback palette:
|
||||
|
||||
- **Background**: `#1e1e2e` (Catppuccin)
|
||||
- **Foreground**: `#cdd6f4` (Catppuccin)
|
||||
- **Surface**: `#181825` (Catppuccin)
|
||||
- **Accent**: Dynamic (from pywal)
|
||||
|
||||
Color palette slots (via wal):
|
||||
- color0–color7: ANSI colors
|
||||
- Semantic: red, green, yellow, blue, pink, teal
|
||||
|
||||
## Component Standards
|
||||
|
||||
### Buttons
|
||||
- Border Radius: 8px
|
||||
- Padding: 8px 16px (primary), 4px 8px (secondary)
|
||||
- Font Size: 14px
|
||||
- Background: Theme accent color
|
||||
|
||||
### Input Fields
|
||||
- Border Radius: 6px
|
||||
- Padding: 12px 16px
|
||||
- Font Size: 14px
|
||||
- Border: 1px or 2px solid (blue on focus)
|
||||
|
||||
### Cards
|
||||
- Border Radius: 8px
|
||||
- Padding: 12px
|
||||
- Margin: 8px
|
||||
- Box Shadow: Optional, for depth
|
||||
|
||||
### Stat Labels
|
||||
- Font Size: 14px
|
||||
- Margin Right (between icon/text): 5px
|
||||
- Group Margin Right: 12px
|
||||
|
||||
### Notification Cards
|
||||
- Border Radius: 8px
|
||||
- Padding: 12px
|
||||
- Margin Bottom: 8px
|
||||
- Font Size: 14px (summary), 12px (body)
|
||||
|
||||
## Current Implementation
|
||||
|
||||
All GUI apps load `bread_theme::stylesheet` (via the generated shared file) and
|
||||
add only app-specific rules:
|
||||
|
||||
- **breadbar** — shared base + bar window, workspace buttons, stats, notification
|
||||
and OSD cards.
|
||||
- **breadbox** — shared base + launcher panel, search entry, result rows.
|
||||
- **breadpad / breadman** — shared base + capture popup, type chips, note cards,
|
||||
reminder window, sidebar rows.
|
||||
- **bos-settings** — shared base + content padding only (was previously a
|
||||
hardcoded Nord palette; migrated to the shared stylesheet).
|
||||
- **breadcrumbs** — CLI tool; ANSI colours only, no GUI styling.
|
||||
|
||||
> Palette note: the fallback is Catppuccin Mocha, but installs (e.g. BOS) drive
|
||||
> the real palette from pywal — BOS ships a black-base palette.
|
||||
|
||||
## Future Consistency Checks
|
||||
|
||||
When adding new components or updating existing ones:
|
||||
1. Use Varela Round for all text
|
||||
2. Set base font size to 14px (12px for secondary)
|
||||
3. Use spacing scale (4px units: 4, 8, 12, 16, 20)
|
||||
4. Use border radius from this system (8px default, 6px secondary)
|
||||
5. Leverage pywal colors for dynamic theming
|
||||
6. Keep margins/padding consistent across similar components
|
||||
269
Cargo.lock
generated
269
Cargo.lock
generated
|
|
@ -81,7 +81,7 @@ checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
|
|||
|
||||
[[package]]
|
||||
name = "bakery"
|
||||
version = "0.2.3"
|
||||
version = "0.2.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"chrono",
|
||||
|
|
@ -91,7 +91,6 @@ dependencies = [
|
|||
"serde",
|
||||
"serde_json",
|
||||
"sha2",
|
||||
"tempfile",
|
||||
"toml 0.8.23",
|
||||
"ureq",
|
||||
]
|
||||
|
|
@ -119,7 +118,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "bread-theme"
|
||||
version = "0.2.3"
|
||||
version = "0.2.1"
|
||||
dependencies = [
|
||||
"dirs",
|
||||
"gtk4",
|
||||
|
|
@ -323,22 +322,6 @@ version = "1.0.2"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
|
||||
|
||||
[[package]]
|
||||
name = "errno"
|
||||
version = "0.3.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fastrand"
|
||||
version = "2.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6"
|
||||
|
||||
[[package]]
|
||||
name = "field-offset"
|
||||
version = "0.3.6"
|
||||
|
|
@ -365,12 +348,6 @@ dependencies = [
|
|||
"miniz_oxide",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "foldhash"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
|
||||
|
||||
[[package]]
|
||||
name = "form_urlencoded"
|
||||
version = "1.2.2"
|
||||
|
|
@ -520,19 +497,6 @@ dependencies = [
|
|||
"wasi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"r-efi",
|
||||
"wasip2",
|
||||
"wasip3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gio"
|
||||
version = "0.22.6"
|
||||
|
|
@ -723,15 +687,6 @@ dependencies = [
|
|||
"system-deps",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.15.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
|
||||
dependencies = [
|
||||
"foldhash",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.17.1"
|
||||
|
|
@ -856,12 +811,6 @@ dependencies = [
|
|||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "id-arena"
|
||||
version = "2.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954"
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "1.1.0"
|
||||
|
|
@ -890,9 +839,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9"
|
||||
dependencies = [
|
||||
"equivalent",
|
||||
"hashbrown 0.17.1",
|
||||
"serde",
|
||||
"serde_core",
|
||||
"hashbrown",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -919,12 +866,6 @@ dependencies = [
|
|||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "leb128fmt"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.186"
|
||||
|
|
@ -940,12 +881,6 @@ dependencies = [
|
|||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "linux-raw-sys"
|
||||
version = "0.12.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
|
||||
|
||||
[[package]]
|
||||
name = "litemap"
|
||||
version = "0.8.2"
|
||||
|
|
@ -1061,16 +996,6 @@ dependencies = [
|
|||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "prettyplease"
|
||||
version = "0.2.37"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro-crate"
|
||||
version = "3.5.0"
|
||||
|
|
@ -1098,19 +1023,13 @@ dependencies = [
|
|||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "r-efi"
|
||||
version = "6.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
|
||||
|
||||
[[package]]
|
||||
name = "redox_users"
|
||||
version = "0.4.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43"
|
||||
dependencies = [
|
||||
"getrandom 0.2.17",
|
||||
"getrandom",
|
||||
"libredox",
|
||||
"thiserror",
|
||||
]
|
||||
|
|
@ -1123,7 +1042,7 @@ checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7"
|
|||
dependencies = [
|
||||
"cc",
|
||||
"cfg-if",
|
||||
"getrandom 0.2.17",
|
||||
"getrandom",
|
||||
"libc",
|
||||
"untrusted",
|
||||
"windows-sys 0.52.0",
|
||||
|
|
@ -1138,19 +1057,6 @@ dependencies = [
|
|||
"semver",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustix"
|
||||
version = "1.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"errno",
|
||||
"libc",
|
||||
"linux-raw-sys",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustls"
|
||||
version = "0.23.40"
|
||||
|
|
@ -1353,19 +1259,6 @@ version = "0.13.5"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca"
|
||||
|
||||
[[package]]
|
||||
name = "tempfile"
|
||||
version = "3.27.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
|
||||
dependencies = [
|
||||
"fastrand",
|
||||
"getrandom 0.4.2",
|
||||
"once_cell",
|
||||
"rustix",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "1.0.69"
|
||||
|
|
@ -1500,12 +1393,6 @@ version = "1.0.24"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-xid"
|
||||
version = "0.2.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
|
||||
|
||||
[[package]]
|
||||
name = "untrusted"
|
||||
version = "0.9.0"
|
||||
|
|
@ -1572,24 +1459,6 @@ version = "0.11.1+wasi-snapshot-preview1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
|
||||
|
||||
[[package]]
|
||||
name = "wasip2"
|
||||
version = "1.0.3+wasi-0.2.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6"
|
||||
dependencies = [
|
||||
"wit-bindgen 0.57.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasip3"
|
||||
version = "0.4.0+wasi-0.3.0-rc-2026-01-06"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5"
|
||||
dependencies = [
|
||||
"wit-bindgen 0.51.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen"
|
||||
version = "0.2.122"
|
||||
|
|
@ -1635,40 +1504,6 @@ dependencies = [
|
|||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-encoder"
|
||||
version = "0.244.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319"
|
||||
dependencies = [
|
||||
"leb128fmt",
|
||||
"wasmparser",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-metadata"
|
||||
version = "0.244.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"indexmap",
|
||||
"wasm-encoder",
|
||||
"wasmparser",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasmparser"
|
||||
version = "0.244.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"hashbrown 0.15.5",
|
||||
"indexmap",
|
||||
"semver",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "webpki-roots"
|
||||
version = "0.26.11"
|
||||
|
|
@ -1912,100 +1747,6 @@ dependencies = [
|
|||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-bindgen"
|
||||
version = "0.51.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5"
|
||||
dependencies = [
|
||||
"wit-bindgen-rust-macro",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-bindgen"
|
||||
version = "0.57.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e"
|
||||
|
||||
[[package]]
|
||||
name = "wit-bindgen-core"
|
||||
version = "0.51.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"heck",
|
||||
"wit-parser",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-bindgen-rust"
|
||||
version = "0.51.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"heck",
|
||||
"indexmap",
|
||||
"prettyplease",
|
||||
"syn",
|
||||
"wasm-metadata",
|
||||
"wit-bindgen-core",
|
||||
"wit-component",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-bindgen-rust-macro"
|
||||
version = "0.51.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"prettyplease",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"wit-bindgen-core",
|
||||
"wit-bindgen-rust",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-component"
|
||||
version = "0.244.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bitflags",
|
||||
"indexmap",
|
||||
"log",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"wasm-encoder",
|
||||
"wasm-metadata",
|
||||
"wasmparser",
|
||||
"wit-parser",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-parser"
|
||||
version = "0.244.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"id-arena",
|
||||
"indexmap",
|
||||
"log",
|
||||
"semver",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"unicode-xid",
|
||||
"wasmparser",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "writeable"
|
||||
version = "0.6.3"
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ members = ["bakery", "bread-theme"]
|
|||
resolver = "2"
|
||||
|
||||
[workspace.package]
|
||||
version = "0.2.3"
|
||||
version = "0.2.1"
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
authors = ["Breadway <rileyhorsham@gmail.com>"]
|
||||
|
|
|
|||
65
README.md
65
README.md
|
|
@ -18,36 +18,6 @@ bakery install breadbar
|
|||
| `breadcrumbs` | Profile-aware Wi-Fi state machine with Tailscale exit-node management and a self-healing watch daemon |
|
||||
| `breadpad` | Quick-capture scratchpad popup with AI-powered note classification, reminders, recurrence, and a full note viewer (`breadman`) |
|
||||
|
||||
## Recommended keybinds
|
||||
|
||||
The ecosystem assumes a Hyprland setup with `SUPER` as the modifier. The
|
||||
conventional bindings (used by BOS and recommended for any install):
|
||||
|
||||
| Keys | Action |
|
||||
|------|--------|
|
||||
| `SUPER+Space` | `breadbox` — app launcher |
|
||||
| `SUPER+U` | `breadpad` — quick-capture notes/reminders |
|
||||
| `SUPER+M` | `breadman` — note viewer / manager |
|
||||
| `SUPER+,` | settings (`bos-settings`, where installed) |
|
||||
|
||||
`breadbar` and `breadd` are services started at login (`exec-once`), not bound
|
||||
to keys.
|
||||
|
||||
## Theming
|
||||
|
||||
All GUIs share one look via `bread-theme`. The `bread-theme` CLI renders the
|
||||
component stylesheet from your pywal palette (Catppuccin Mocha fallback) to
|
||||
`$XDG_RUNTIME_DIR/bread/theme.css`; every app loads that file and **live-reloads**
|
||||
it, so changing your wallpaper recolours the whole ecosystem with no rebuilds:
|
||||
|
||||
```sh
|
||||
wal -i ~/Pictures/wall.png # regenerate pywal palette
|
||||
bread-theme generate # render the shared stylesheet (run from a wal hook)
|
||||
```
|
||||
|
||||
See [`BREAD_DESIGN_SYSTEM.md`](BREAD_DESIGN_SYSTEM.md) for the tokens (fonts,
|
||||
spacing, radii, colour roles) the stylesheet is built from.
|
||||
|
||||
## Installing bakery
|
||||
|
||||
`bakery` is the package manager for the ecosystem. Install it with the bootstrap script:
|
||||
|
|
@ -79,18 +49,13 @@ bakery remove <pkg> # remove a package (data files are never deleted)
|
|||
|
||||
## System dependencies by product
|
||||
|
||||
`bakery doctor` checks these automatically before any install. Required deps block installation; optional deps generate a warning but never block.
|
||||
|
||||
| Package | Required | Optional |
|
||||
|---------|----------|---------|
|
||||
| `bakery` | _(statically linked, none)_ | — |
|
||||
| `bread` | `systemd-libs` `openssl` `zlib` | `bluez` `hyprland` |
|
||||
| `breadbar` | `gtk4` `gtk4-layer-shell` `iw` `libpulse` | `hyprland` |
|
||||
| `breadbox` | `gtk4` `gtk4-layer-shell` `librsvg` | `hyprland` |
|
||||
| `breadcrumbs` | `networkmanager` | `tailscale` `sudo` `xdg-utils` |
|
||||
| `breadpad` | `gtk4` `gtk4-layer-shell` | `rocm-hip-runtime` `ollama` `hyprland` |
|
||||
|
||||
Install all required deps with `sudo pacman -S <packages>`. Use `pacman -Q <pkg>` to check whether any are already present.
|
||||
| Package | Arch packages |
|
||||
|---------|--------------|
|
||||
| `bread` | `libudev` `dbus` |
|
||||
| `breadbar` | `gtk4` `gtk4-layer-shell` `dbus` `iw` |
|
||||
| `breadbox` | `gtk4` `gtk4-layer-shell` `librsvg` |
|
||||
| `breadcrumbs` | `networkmanager` |
|
||||
| `breadpad` | `gtk4` `gtk4-layer-shell` `dbus` |
|
||||
|
||||
## Theming
|
||||
|
||||
|
|
@ -125,22 +90,6 @@ and mirrors the binary to GitHub Releases as a fallback.
|
|||
`bakery` always tries `dl.breadway.dev` first and transparently falls back
|
||||
to the GitHub Release URL recorded in the manifest.
|
||||
|
||||
### Release artifact contract
|
||||
|
||||
Each product's `release.yml` **must** upload the following files alongside
|
||||
the binary to `dl.breadway.dev/<name>/<version>/`:
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `bakery.toml` | Metadata (deps, services, config) read by `gen-index.sh` |
|
||||
| `<binary>-x86_64.sha256` | Checksum verified by `bakery install` and `get.sh` |
|
||||
| `*.service` | systemd unit files installed by `bakery install` |
|
||||
| `*.example.toml` / `config.example.toml` | Example configs copied on first install |
|
||||
|
||||
`gen-index.sh` **fails loudly** if `bakery.toml` is missing — this is by
|
||||
design to catch omissions in the release workflow before they silently
|
||||
produce empty metadata in production.
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ name = "bakery"
|
|||
description = "Bread ecosystem package manager"
|
||||
binaries = ["bakery"]
|
||||
system_deps = []
|
||||
optional_system_deps = []
|
||||
bread_deps = []
|
||||
|
||||
[install]
|
||||
|
|
|
|||
|
|
@ -18,6 +18,3 @@ sha2 = { workspace = true }
|
|||
hex = { workspace = true }
|
||||
clap = { workspace = true }
|
||||
chrono = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3"
|
||||
|
|
|
|||
|
|
@ -1,45 +1,37 @@
|
|||
use anyhow::Result;
|
||||
use std::process::Command;
|
||||
|
||||
pub struct DepReport {
|
||||
/// Required deps that are not present — blocks install.
|
||||
pub missing: Vec<String>,
|
||||
/// Optional deps that are not present — advisory only, never blocks.
|
||||
pub warnings: Vec<String>,
|
||||
/// Check whether a list of system dependencies are present.
|
||||
/// Returns (missing, warnings) — missing are hard fails, warnings are advisory.
|
||||
pub fn check_deps(deps: &[String]) -> Result<Vec<String>> {
|
||||
let mut missing = Vec::new();
|
||||
for dep in deps {
|
||||
if !dep_present(dep) {
|
||||
missing.push(dep.clone());
|
||||
}
|
||||
}
|
||||
Ok(missing)
|
||||
}
|
||||
|
||||
pub fn check_deps(required: &[String], optional: &[String]) -> Result<DepReport> {
|
||||
Ok(DepReport {
|
||||
missing: required.iter().filter(|d| !dep_present(d)).cloned().collect(),
|
||||
warnings: optional.iter().filter(|d| !dep_present(d)).cloned().collect(),
|
||||
})
|
||||
}
|
||||
|
||||
fn dep_present(pkg: &str) -> bool {
|
||||
// Primary: `pacman -Q` uses the exact Arch package name — no name mapping needed.
|
||||
if pacman_installed(pkg) {
|
||||
fn dep_present(dep: &str) -> bool {
|
||||
// Try `which` first (covers executables like `iw`, `nmcli`).
|
||||
if which(dep) {
|
||||
return true;
|
||||
}
|
||||
// Fallback for environments without pacman: native PATH search then pkg-config.
|
||||
path_has(pkg) || pkg_config_exists(pkg)
|
||||
// Try `pkg-config --exists` for library packages (gtk4, gtk4-layer-shell, librsvg).
|
||||
pkg_config_exists(dep)
|
||||
}
|
||||
|
||||
fn pacman_installed(pkg: &str) -> bool {
|
||||
Command::new("pacman")
|
||||
.args(["-Q", pkg])
|
||||
fn which(bin: &str) -> bool {
|
||||
Command::new("which")
|
||||
.arg(bin)
|
||||
.output()
|
||||
.map(|o| o.status.success())
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
/// Check PATH without shelling out to `which` (avoids the external dependency).
|
||||
fn path_has(bin: &str) -> bool {
|
||||
std::env::var_os("PATH")
|
||||
.map(|p| std::env::split_paths(&p).any(|dir| dir.join(bin).is_file()))
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
fn pkg_config_exists(lib: &str) -> bool {
|
||||
// Arch package names map directly to pkg-config names for GTK libs.
|
||||
Command::new("pkg-config")
|
||||
.arg("--exists")
|
||||
.arg(lib)
|
||||
|
|
@ -48,90 +40,33 @@ fn pkg_config_exists(lib: &str) -> bool {
|
|||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
/// Print a formatted doctor report for a package's system deps.
|
||||
/// Returns true if all *required* deps are satisfied.
|
||||
pub fn report(package_name: &str, required: &[String], optional: &[String]) -> bool {
|
||||
if required.is_empty() && optional.is_empty() {
|
||||
/// Print a formatted doctor report for a list of system deps.
|
||||
/// Returns true if all deps are satisfied.
|
||||
pub fn report(package_name: &str, deps: &[String]) -> bool {
|
||||
if deps.is_empty() {
|
||||
println!(" {package_name}: no system deps required");
|
||||
return true;
|
||||
}
|
||||
match check_deps(required, optional) {
|
||||
match check_deps(deps) {
|
||||
Err(e) => {
|
||||
eprintln!(" error running doctor for {package_name}: {e}");
|
||||
eprintln!(" error running doctor: {e}");
|
||||
false
|
||||
}
|
||||
Ok(rep) => {
|
||||
for warn in &rep.warnings {
|
||||
eprintln!(
|
||||
" {package_name}: optional dep not found: {warn} \
|
||||
(install for full functionality)"
|
||||
);
|
||||
}
|
||||
if rep.missing.is_empty() {
|
||||
println!(" {package_name}: all required system deps satisfied");
|
||||
Ok(missing) => {
|
||||
if missing.is_empty() {
|
||||
println!(" {package_name}: all system deps satisfied");
|
||||
true
|
||||
} else {
|
||||
eprintln!(
|
||||
" {package_name}: missing system deps: {}",
|
||||
rep.missing.join(", ")
|
||||
missing.join(", ")
|
||||
);
|
||||
eprintln!(
|
||||
" install with: sudo pacman -S {}",
|
||||
missing.join(" ")
|
||||
);
|
||||
eprintln!(" install with: sudo pacman -S {}", rep.missing.join(" "));
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn empty_deps_pass() {
|
||||
let rep = check_deps(&[], &[]).unwrap();
|
||||
assert!(rep.missing.is_empty());
|
||||
assert!(rep.warnings.is_empty());
|
||||
}
|
||||
|
||||
// This test only runs on systems where pacman is available (Arch Linux).
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn pacman_finds_itself() {
|
||||
assert!(pacman_installed("pacman"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn path_has_finds_sh() {
|
||||
assert!(path_has("sh"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_required_dep_detected() {
|
||||
let rep = check_deps(
|
||||
&["this-package-does-not-exist-xyzzy42".to_string()],
|
||||
&[],
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(rep.missing.len(), 1);
|
||||
assert!(rep.warnings.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_optional_dep_becomes_warning_not_error() {
|
||||
let rep = check_deps(
|
||||
&[],
|
||||
&["this-package-does-not-exist-xyzzy42".to_string()],
|
||||
)
|
||||
.unwrap();
|
||||
assert!(rep.missing.is_empty());
|
||||
assert_eq!(rep.warnings.len(), 1);
|
||||
}
|
||||
|
||||
// This test only runs on systems where pacman is available (Arch Linux).
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn installed_dep_not_missing() {
|
||||
let rep = check_deps(&["pacman".to_string()], &[]).unwrap();
|
||||
assert!(rep.missing.is_empty());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -45,34 +45,3 @@ fn verify_sha256(bytes: &[u8], expected_hex: &str) -> Result<()> {
|
|||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use sha2::{Digest, Sha256};
|
||||
|
||||
fn sha256_hex(data: &[u8]) -> String {
|
||||
hex::encode(Sha256::digest(data))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn verify_correct_hash() {
|
||||
let bytes = b"hello bakery";
|
||||
let hash = sha256_hex(bytes);
|
||||
assert!(verify_sha256(bytes, &hash).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn verify_wrong_hash_fails() {
|
||||
let bytes = b"hello bakery";
|
||||
let wrong = "0".repeat(64);
|
||||
assert!(verify_sha256(bytes, &wrong).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn verify_empty_bytes() {
|
||||
let bytes = b"";
|
||||
let hash = sha256_hex(bytes);
|
||||
assert!(verify_sha256(bytes, &hash).is_ok());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ use std::path::{Path, PathBuf};
|
|||
use std::process::Command;
|
||||
|
||||
use crate::download::fetch_and_place;
|
||||
use crate::manifest::{fetch_binary, Package, Service};
|
||||
use crate::manifest::{Package, Service};
|
||||
use crate::state::{InstalledPackage, State};
|
||||
|
||||
pub fn install_package(pkg: &Package, bin_dir: &Path) -> Result<()> {
|
||||
|
|
@ -18,15 +18,15 @@ pub fn install_package(pkg: &Package, bin_dir: &Path) -> Result<()> {
|
|||
binary_names.push(install_name.to_string());
|
||||
}
|
||||
|
||||
// 2. Scaffold config dir + download example file.
|
||||
// 2. Scaffold config dir + example file.
|
||||
if let Some(cfg) = &pkg.config {
|
||||
scaffold_config(cfg, pkg)?;
|
||||
scaffold_config(cfg)?;
|
||||
}
|
||||
|
||||
// 3. Install systemd user units.
|
||||
let mut service_names = Vec::new();
|
||||
for svc in &pkg.services {
|
||||
install_service(svc, bin_dir, pkg)?;
|
||||
install_service(svc, bin_dir)?;
|
||||
service_names.push(svc.unit.clone());
|
||||
}
|
||||
|
||||
|
|
@ -60,8 +60,6 @@ pub fn remove_package(pkg_name: &str, bin_dir: &Path) -> Result<()> {
|
|||
return Ok(());
|
||||
}
|
||||
};
|
||||
// Commit removal immediately — file cleanup below is best-effort.
|
||||
state.save()?;
|
||||
|
||||
// Remove binaries.
|
||||
for bin in &installed.binaries {
|
||||
|
|
@ -106,111 +104,66 @@ pub fn remove_package(pkg_name: &str, bin_dir: &Path) -> Result<()> {
|
|||
println!(" data preserved at {}", data_dir.display());
|
||||
}
|
||||
|
||||
state.save()?;
|
||||
println!(" {pkg_name} removed");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn scaffold_config(cfg: &crate::manifest::ConfigScaffold, pkg: &Package) -> Result<()> {
|
||||
fn scaffold_config(cfg: &crate::manifest::ConfigScaffold) -> Result<()> {
|
||||
let dir = expand_tilde(&cfg.dir);
|
||||
std::fs::create_dir_all(&dir)?;
|
||||
|
||||
if let Some(example) = &cfg.example {
|
||||
let dest = dir.join(example);
|
||||
if !dest.exists() {
|
||||
if let Some((primary, fallback)) = pkg.artifact_urls(example) {
|
||||
match fetch_binary(&primary, &fallback) {
|
||||
Ok(bytes) => {
|
||||
std::fs::write(&dest, &bytes)
|
||||
.with_context(|| format!("writing {}", dest.display()))?;
|
||||
println!(" installed example config at {}", dest.display());
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!(" warning: could not download example config {example}: {e}");
|
||||
println!(" config dir created at {}", dir.display());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
println!(" config dir created at {}", dir.display());
|
||||
}
|
||||
// We don't have the actual example file here at install time —
|
||||
// the product repo's release bundle should include it.
|
||||
// For now just note it; release.yml will bundle example configs.
|
||||
println!(" config dir ready at {}", dir.display());
|
||||
println!(
|
||||
" copy your {example} to {} to configure {}",
|
||||
dest.display(),
|
||||
dir.display()
|
||||
);
|
||||
} else {
|
||||
println!(" config at {} already exists, skipping", dest.display());
|
||||
}
|
||||
} else {
|
||||
println!(" config dir created at {}", dir.display());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn install_service(svc: &Service, bin_dir: &Path, pkg: &Package) -> Result<()> {
|
||||
fn install_service(svc: &Service, bin_dir: &Path) -> Result<()> {
|
||||
let service_dir = systemd_user_dir();
|
||||
std::fs::create_dir_all(&service_dir)?;
|
||||
|
||||
let unit_path = service_dir.join(&svc.unit);
|
||||
|
||||
// Download the unit file if not already present.
|
||||
if !unit_path.exists() {
|
||||
if let Some((primary, fallback)) = pkg.artifact_urls(&svc.unit) {
|
||||
match fetch_binary(&primary, &fallback) {
|
||||
Ok(bytes) => {
|
||||
std::fs::write(&unit_path, &bytes)
|
||||
.with_context(|| format!("writing {}", unit_path.display()))?;
|
||||
println!(" downloaded unit {}", unit_path.display());
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!(" warning: could not download {}: {e}", svc.unit);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
eprintln!(" warning: no artifact URL to download {}", svc.unit);
|
||||
}
|
||||
// The unit file is expected to be bundled alongside the binary in the
|
||||
// release artifact (or embedded). For now, patch ExecStart if the unit
|
||||
// already exists (same pattern as bread/scripts/install.sh).
|
||||
if unit_path.exists() {
|
||||
patch_exec_start(&unit_path, bin_dir)?;
|
||||
}
|
||||
|
||||
if !unit_path.exists() {
|
||||
eprintln!(
|
||||
" warning: unit file {} not found — skipping service setup",
|
||||
svc.unit
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
patch_exec_start(&unit_path, bin_dir)?;
|
||||
|
||||
if !Command::new("systemctl")
|
||||
let _ = Command::new("systemctl")
|
||||
.args(["--user", "daemon-reload"])
|
||||
.status()
|
||||
.map(|s| s.success())
|
||||
.unwrap_or(false)
|
||||
{
|
||||
eprintln!(" warning: systemctl daemon-reload failed");
|
||||
}
|
||||
.status();
|
||||
|
||||
if svc.enable {
|
||||
let already_active = Command::new("systemctl")
|
||||
if Command::new("systemctl")
|
||||
.args(["--user", "is-active", "--quiet", &svc.unit])
|
||||
.status()
|
||||
.map(|s| s.success())
|
||||
.unwrap_or(false);
|
||||
|
||||
if already_active {
|
||||
if Command::new("systemctl")
|
||||
.args(["--user", "restart", &svc.unit])
|
||||
.status()
|
||||
.map(|s| s.success())
|
||||
.unwrap_or(false)
|
||||
{
|
||||
println!(" {} restarted", svc.unit);
|
||||
} else {
|
||||
eprintln!(" warning: failed to restart {}", svc.unit);
|
||||
}
|
||||
} else if Command::new("systemctl")
|
||||
.args(["--user", "enable", "--now", &svc.unit])
|
||||
.status()
|
||||
.map(|s| s.success())
|
||||
.unwrap_or(false)
|
||||
{
|
||||
println!(" {} enabled and started", svc.unit);
|
||||
let _ = Command::new("systemctl")
|
||||
.args(["--user", "restart", &svc.unit])
|
||||
.status();
|
||||
println!(" {} restarted", svc.unit);
|
||||
} else {
|
||||
eprintln!(" warning: failed to enable {}", svc.unit);
|
||||
let _ = Command::new("systemctl")
|
||||
.args(["--user", "enable", "--now", &svc.unit])
|
||||
.status();
|
||||
println!(" {} enabled and started", svc.unit);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -223,6 +176,7 @@ fn patch_exec_start(unit_path: &Path, bin_dir: &Path) -> Result<()> {
|
|||
.lines()
|
||||
.map(|line| {
|
||||
if line.trim_start().starts_with("ExecStart=") {
|
||||
// Replace only the path prefix, keep args.
|
||||
let rest = line.splitn(2, '=').nth(1).unwrap_or("");
|
||||
let argv: Vec<&str> = rest.split_whitespace().collect();
|
||||
if let Some(bin_name) = argv.first().and_then(|p| Path::new(p).file_name()) {
|
||||
|
|
@ -242,13 +196,7 @@ fn patch_exec_start(unit_path: &Path, bin_dir: &Path) -> Result<()> {
|
|||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
// Preserve trailing newline if the original had one.
|
||||
let output = if text.ends_with('\n') {
|
||||
format!("{patched}\n")
|
||||
} else {
|
||||
patched
|
||||
};
|
||||
std::fs::write(unit_path, output)?;
|
||||
std::fs::write(unit_path, patched)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
|
@ -293,7 +241,7 @@ fn expand_tilde(path: &str) -> PathBuf {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn strip_arch_suffix(name: &str) -> &str {
|
||||
fn strip_arch_suffix(name: &str) -> &str {
|
||||
const SUFFIXES: &[&str] = &["-x86_64", "-aarch64", "-arm64", "-armv7"];
|
||||
for s in SUFFIXES {
|
||||
if let Some(base) = name.strip_suffix(s) {
|
||||
|
|
@ -314,53 +262,3 @@ fn warn_path_if_needed(bin_dir: &Path) {
|
|||
println!(" export PATH=\"{}:$PATH\"", bin_str);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::fs;
|
||||
use tempfile::tempdir;
|
||||
|
||||
#[test]
|
||||
fn strip_known_suffixes() {
|
||||
assert_eq!(strip_arch_suffix("breadd-x86_64"), "breadd");
|
||||
assert_eq!(strip_arch_suffix("breadd-aarch64"), "breadd");
|
||||
assert_eq!(strip_arch_suffix("breadd-arm64"), "breadd");
|
||||
assert_eq!(strip_arch_suffix("breadd-armv7"), "breadd");
|
||||
assert_eq!(strip_arch_suffix("bakery-x86_64"), "bakery");
|
||||
assert_eq!(strip_arch_suffix("breadd"), "breadd");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn patch_exec_start_with_args() {
|
||||
let dir = tempdir().unwrap();
|
||||
let path = dir.path().join("test.service");
|
||||
fs::write(&path, "[Service]\nExecStart=/old/path/bin arg1 arg2\n").unwrap();
|
||||
patch_exec_start(&path, Path::new("/new/bin")).unwrap();
|
||||
let out = fs::read_to_string(&path).unwrap();
|
||||
assert!(out.contains("ExecStart=/new/bin/bin arg1 arg2"));
|
||||
assert!(out.ends_with('\n'));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn patch_exec_start_no_args() {
|
||||
let dir = tempdir().unwrap();
|
||||
let path = dir.path().join("test.service");
|
||||
fs::write(&path, "[Service]\nExecStart=/old/path/daemon\n").unwrap();
|
||||
patch_exec_start(&path, Path::new("/usr/local/bin")).unwrap();
|
||||
let out = fs::read_to_string(&path).unwrap();
|
||||
assert!(out.contains("ExecStart=/usr/local/bin/daemon"));
|
||||
assert!(!out.contains("daemon "));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn patch_exec_start_non_exec_lines_unchanged() {
|
||||
let dir = tempdir().unwrap();
|
||||
let path = dir.path().join("test.service");
|
||||
fs::write(&path, "[Unit]\nDescription=foo\nExecStart=/bin/foo\n").unwrap();
|
||||
patch_exec_start(&path, Path::new("/usr/bin")).unwrap();
|
||||
let out = fs::read_to_string(&path).unwrap();
|
||||
assert!(out.contains("Description=foo"));
|
||||
assert!(out.contains("ExecStart=/usr/bin/foo"));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,11 +6,10 @@ mod state;
|
|||
|
||||
use anyhow::{bail, Result};
|
||||
use clap::{Parser, Subcommand};
|
||||
use std::collections::HashSet;
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(name = "bakery", about = "Package manager for the bread ecosystem", version)]
|
||||
#[command(name = "bakery", about = "Package manager for the bread ecosystem")]
|
||||
struct Cli {
|
||||
#[command(subcommand)]
|
||||
command: Cmd,
|
||||
|
|
@ -32,12 +31,8 @@ enum Cmd {
|
|||
},
|
||||
/// Update one or all installed packages
|
||||
Update {
|
||||
/// Package to update (omit or use --all to update everything installed)
|
||||
#[arg(conflicts_with = "all")]
|
||||
/// Package to update; omit to update all installed packages
|
||||
package: Option<String>,
|
||||
/// Update all installed packages
|
||||
#[arg(long, conflicts_with = "package")]
|
||||
all: bool,
|
||||
},
|
||||
/// List packages
|
||||
List {
|
||||
|
|
@ -68,58 +63,31 @@ fn main() -> Result<()> {
|
|||
|
||||
match cli.command {
|
||||
Cmd::Install { packages } => {
|
||||
let index = manifest::load(true)?;
|
||||
for pkg in &packages {
|
||||
cmd_install(&index, pkg, &bin_dir)?;
|
||||
cmd_install(pkg, &bin_dir)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
Cmd::Remove { package } => cmd_remove(&package, &bin_dir),
|
||||
Cmd::Update { package, all } => cmd_update(package.as_deref(), all, &bin_dir),
|
||||
Cmd::Update { package } => cmd_update(package.as_deref(), &bin_dir),
|
||||
Cmd::List { installed } => cmd_list(installed),
|
||||
Cmd::Info { package } => cmd_info(&package),
|
||||
Cmd::Doctor { package } => cmd_doctor(package.as_deref()),
|
||||
}
|
||||
}
|
||||
|
||||
fn cmd_install(index: &manifest::Index, name: &str, bin_dir: &std::path::Path) -> Result<()> {
|
||||
let mut visited = HashSet::new();
|
||||
install_with_deps(index, name, bin_dir, &mut visited)
|
||||
}
|
||||
|
||||
/// Recursively installs `name` and any bread_deps, skipping already-installed
|
||||
/// packages. The `visited` set prevents cycles.
|
||||
fn install_with_deps(
|
||||
index: &manifest::Index,
|
||||
name: &str,
|
||||
bin_dir: &std::path::Path,
|
||||
visited: &mut HashSet<String>,
|
||||
) -> Result<()> {
|
||||
if !visited.insert(name.to_string()) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
fn cmd_install(name: &str, bin_dir: &std::path::Path) -> Result<()> {
|
||||
let index = manifest::load(false)?;
|
||||
let pkg = index
|
||||
.get(name)
|
||||
.ok_or_else(|| anyhow::anyhow!("unknown package: {name}"))?;
|
||||
|
||||
// Install bread_deps first (skip those already recorded in state).
|
||||
let state = state::State::load()?;
|
||||
for dep in pkg.bread_deps.clone() {
|
||||
if !state.is_installed(&dep) {
|
||||
println!("installing bread dependency: {dep}");
|
||||
install_with_deps(index, &dep, bin_dir, visited)?;
|
||||
}
|
||||
}
|
||||
|
||||
println!("checking system dependencies for {name}…");
|
||||
let rep = doctor::check_deps(&pkg.system_deps, &pkg.optional_system_deps)?;
|
||||
for warn in &rep.warnings {
|
||||
eprintln!(" note: optional dep not installed: {warn}");
|
||||
}
|
||||
if !rep.missing.is_empty() {
|
||||
eprintln!("missing system deps for {name}: {}", rep.missing.join(", "));
|
||||
eprintln!("install with: sudo pacman -S {}", rep.missing.join(" "));
|
||||
// Doctor runs first — bail if system deps are missing.
|
||||
println!("checking system dependencies…");
|
||||
let missing = doctor::check_deps(&pkg.system_deps)?;
|
||||
if !missing.is_empty() {
|
||||
eprintln!("missing system dependencies for {name}: {}", missing.join(", "));
|
||||
eprintln!("install with: sudo pacman -S {}", missing.join(" "));
|
||||
bail!("system deps not satisfied");
|
||||
}
|
||||
|
||||
|
|
@ -130,22 +98,16 @@ fn cmd_remove(name: &str, bin_dir: &std::path::Path) -> Result<()> {
|
|||
install::remove_package(name, bin_dir)
|
||||
}
|
||||
|
||||
fn cmd_update(name: Option<&str>, all: bool, bin_dir: &std::path::Path) -> Result<()> {
|
||||
let index = manifest::load(true)?;
|
||||
fn cmd_update(name: Option<&str>, bin_dir: &std::path::Path) -> Result<()> {
|
||||
let index = manifest::load(true)?; // force refresh on update
|
||||
let state = state::State::load()?;
|
||||
|
||||
let targets: Vec<String> = if all || name.is_none() {
|
||||
state.packages.keys().cloned().collect()
|
||||
} else {
|
||||
vec![name.unwrap().to_string()]
|
||||
let effective = name.filter(|&n| n != "all");
|
||||
let targets: Vec<String> = match effective {
|
||||
Some(n) => vec![n.to_string()],
|
||||
None => state.packages.keys().cloned().collect(),
|
||||
};
|
||||
|
||||
if targets.is_empty() {
|
||||
println!("no packages installed");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let mut any_failed = false;
|
||||
for pkg_name in &targets {
|
||||
let installed = match state.packages.get(pkg_name.as_str()) {
|
||||
Some(p) => p,
|
||||
|
|
@ -161,45 +123,15 @@ fn cmd_update(name: Option<&str>, all: bool, bin_dir: &std::path::Path) -> Resul
|
|||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
if installed.version == latest.version {
|
||||
println!("{pkg_name} is already at {}", installed.version);
|
||||
continue;
|
||||
}
|
||||
|
||||
println!(
|
||||
"updating {pkg_name} {} → {}",
|
||||
installed.version, latest.version
|
||||
);
|
||||
|
||||
let rep = match doctor::check_deps(&latest.system_deps, &latest.optional_system_deps) {
|
||||
Ok(r) => r,
|
||||
Err(e) => {
|
||||
eprintln!(" doctor check failed for {pkg_name}: {e}");
|
||||
any_failed = true;
|
||||
continue;
|
||||
}
|
||||
};
|
||||
for warn in &rep.warnings {
|
||||
eprintln!(" note: optional dep not installed: {warn}");
|
||||
}
|
||||
if !rep.missing.is_empty() {
|
||||
eprintln!(
|
||||
" missing deps for {pkg_name}: {} — skipping update",
|
||||
rep.missing.join(", ")
|
||||
} else {
|
||||
println!(
|
||||
"updating {pkg_name} {} → {}",
|
||||
installed.version, latest.version
|
||||
);
|
||||
any_failed = true;
|
||||
continue;
|
||||
install::install_package(latest, bin_dir)?;
|
||||
}
|
||||
|
||||
if let Err(e) = install::install_package(latest, bin_dir) {
|
||||
eprintln!(" failed to update {pkg_name}: {e}");
|
||||
any_failed = true;
|
||||
}
|
||||
}
|
||||
|
||||
if any_failed {
|
||||
bail!("one or more packages could not be updated");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -248,32 +180,15 @@ fn cmd_info(name: &str) -> Result<()> {
|
|||
println!("{} {}", pkg.name, pkg.version);
|
||||
println!(" {}", pkg.description);
|
||||
println!(" status: {status}");
|
||||
println!(
|
||||
" binaries: {}",
|
||||
pkg.binaries
|
||||
.iter()
|
||||
.map(|b| b.name.as_str())
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ")
|
||||
);
|
||||
println!(" binaries: {}", pkg.binaries.iter().map(|b| b.name.as_str()).collect::<Vec<_>>().join(", "));
|
||||
if !pkg.system_deps.is_empty() {
|
||||
println!(" system deps: {}", pkg.system_deps.join(", "));
|
||||
}
|
||||
if !pkg.optional_system_deps.is_empty() {
|
||||
println!(" optional deps: {}", pkg.optional_system_deps.join(", "));
|
||||
}
|
||||
if !pkg.bread_deps.is_empty() {
|
||||
println!(" bread deps: {}", pkg.bread_deps.join(", "));
|
||||
}
|
||||
if !pkg.services.is_empty() {
|
||||
println!(
|
||||
" services: {}",
|
||||
pkg.services
|
||||
.iter()
|
||||
.map(|s| s.unit.as_str())
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ")
|
||||
);
|
||||
println!(" services: {}", pkg.services.iter().map(|s| s.unit.as_str()).collect::<Vec<_>>().join(", "));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -283,12 +198,7 @@ fn cmd_doctor(name: Option<&str>) -> Result<()> {
|
|||
let state = state::State::load()?;
|
||||
|
||||
let targets: Vec<String> = match name {
|
||||
Some(n) => {
|
||||
if index.get(n).is_none() {
|
||||
bail!("unknown package: {n}");
|
||||
}
|
||||
vec![n.to_string()]
|
||||
}
|
||||
Some(n) => vec![n.to_string()],
|
||||
None => state.packages.keys().cloned().collect(),
|
||||
};
|
||||
|
||||
|
|
@ -300,12 +210,9 @@ fn cmd_doctor(name: Option<&str>) -> Result<()> {
|
|||
let mut all_ok = true;
|
||||
for pkg_name in &targets {
|
||||
if let Some(pkg) = index.get(pkg_name) {
|
||||
if !doctor::report(pkg_name, &pkg.system_deps, &pkg.optional_system_deps) {
|
||||
if !doctor::report(pkg_name, &pkg.system_deps) {
|
||||
all_ok = false;
|
||||
}
|
||||
} else {
|
||||
eprintln!(" {pkg_name}: not found in index (removed from registry?)");
|
||||
all_ok = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ pub struct Service {
|
|||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct ConfigScaffold {
|
||||
pub dir: String,
|
||||
/// Example config filename, relative to the release artifact directory.
|
||||
/// relative to the product repo root; copied as-is if absent at install time
|
||||
pub example: Option<String>,
|
||||
}
|
||||
|
||||
|
|
@ -36,8 +36,6 @@ pub struct Package {
|
|||
#[serde(default)]
|
||||
pub system_deps: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub optional_system_deps: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub bread_deps: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub services: Vec<Service>,
|
||||
|
|
@ -46,21 +44,6 @@ pub struct Package {
|
|||
pub post_install: Vec<String>,
|
||||
}
|
||||
|
||||
impl Package {
|
||||
/// Returns `(primary_url, github_url)` for any artifact filename in this
|
||||
/// package's release directory. Derived by stripping the filename from the
|
||||
/// first binary's URLs.
|
||||
pub fn artifact_urls(&self, filename: &str) -> Option<(String, String)> {
|
||||
let first = self.binaries.first()?;
|
||||
let dl_base = first.dl_url.rsplit_once('/')?.0;
|
||||
let gh_base = first.github_url.rsplit_once('/')?.0;
|
||||
Some((
|
||||
format!("{dl_base}/{filename}"),
|
||||
format!("{gh_base}/{filename}"),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct Index {
|
||||
pub version: String,
|
||||
|
|
@ -84,7 +67,8 @@ pub fn load(force_refresh: bool) -> Result<Index> {
|
|||
let cache_path = cache_path();
|
||||
|
||||
if !force_refresh && cache_is_fresh(&cache_path) {
|
||||
let text = std::fs::read_to_string(&cache_path).context("reading cached index")?;
|
||||
let text = std::fs::read_to_string(&cache_path)
|
||||
.context("reading cached index")?;
|
||||
return serde_json::from_str(&text).context("parsing cached index");
|
||||
}
|
||||
|
||||
|
|
@ -148,6 +132,6 @@ fn fetch_bytes(url: &str) -> Result<Vec<u8>> {
|
|||
let mut buf = Vec::new();
|
||||
resp.into_reader()
|
||||
.read_to_end(&mut buf)
|
||||
.context("reading response")?;
|
||||
.context("reading binary")?;
|
||||
Ok(buf)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,12 +33,7 @@ impl State {
|
|||
std::fs::create_dir_all(dir)?;
|
||||
}
|
||||
let text = serde_json::to_string_pretty(self)?;
|
||||
// Write to a temp file then rename for atomicity — avoids a torn write
|
||||
// if the process is killed mid-save.
|
||||
let tmp = path.with_extension("tmp");
|
||||
std::fs::write(&tmp, &text).context("writing installed.json.tmp")?;
|
||||
std::fs::rename(&tmp, &path).context("atomically replacing installed.json")?;
|
||||
Ok(())
|
||||
std::fs::write(&path, text).context("writing installed.json")
|
||||
}
|
||||
|
||||
pub fn is_installed(&self, name: &str) -> bool {
|
||||
|
|
@ -63,58 +58,3 @@ fn state_path() -> PathBuf {
|
|||
})
|
||||
.join("bakery/installed.json")
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn pkg(name: &str, version: &str) -> InstalledPackage {
|
||||
InstalledPackage {
|
||||
name: name.to_string(),
|
||||
version: version.to_string(),
|
||||
binaries: vec![],
|
||||
services: vec![],
|
||||
installed_at: "2026-01-01T00:00:00Z".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn record_and_is_installed() {
|
||||
let mut state = State::default();
|
||||
assert!(!state.is_installed("foo"));
|
||||
state.record(pkg("foo", "1.0.0"));
|
||||
assert!(state.is_installed("foo"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remove_installed() {
|
||||
let mut state = State::default();
|
||||
state.record(pkg("foo", "1.0.0"));
|
||||
let removed = state.remove("foo");
|
||||
assert!(removed.is_some());
|
||||
assert!(!state.is_installed("foo"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remove_unknown_returns_none() {
|
||||
let mut state = State::default();
|
||||
assert!(state.remove("nope").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn json_roundtrip() {
|
||||
let mut state = State::default();
|
||||
state.record(InstalledPackage {
|
||||
name: "bar".to_string(),
|
||||
version: "2.0.0".to_string(),
|
||||
binaries: vec!["bar".to_string()],
|
||||
services: vec!["bar.service".to_string()],
|
||||
installed_at: "2026-06-01T00:00:00Z".to_string(),
|
||||
});
|
||||
let json = serde_json::to_string(&state).unwrap();
|
||||
let restored: State = serde_json::from_str(&json).unwrap();
|
||||
assert!(restored.is_installed("bar"));
|
||||
assert_eq!(restored.packages["bar"].version, "2.0.0");
|
||||
assert_eq!(restored.packages["bar"].services, ["bar.service"]);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,9 +18,3 @@ gtk4 = { version = "0.11", features = ["v4_12"], optional = true }
|
|||
# Enable GTK4 CSS provider helpers (breadbar, breadbox, breadpad use this).
|
||||
# bread (daemon) and breadcrumbs (CLI) depend on this crate without the feature.
|
||||
gtk = ["dep:gtk4"]
|
||||
|
||||
# The generator CLI. It only touches the gtk-free lib API (render + write), so
|
||||
# it builds without the gtk feature and stays light.
|
||||
[[bin]]
|
||||
name = "bread-theme"
|
||||
path = "src/bin/bread-theme.rs"
|
||||
|
|
|
|||
|
|
@ -1,62 +0,0 @@
|
|||
//! `bread-theme` — generates the ecosystem's shared GTK stylesheet from the
|
||||
//! current pywal palette and writes it to the canonical path that every bread
|
||||
//! GUI loads. Run it at session start, and again after the wallpaper/palette
|
||||
//! changes (e.g. from a pywal hook); apps watch the file and recolour live.
|
||||
//!
|
||||
//! bread-theme # same as `generate`
|
||||
//! bread-theme generate # render + write the shared stylesheet
|
||||
//! bread-theme reload # re-render from the current pywal palette and
|
||||
//! # signal every running bread GUI to recolour
|
||||
//! bread-theme path # print the stylesheet path
|
||||
//! bread-theme print # render to stdout (no write)
|
||||
|
||||
use std::process::ExitCode;
|
||||
|
||||
fn write_and_report(verb: &str) -> ExitCode {
|
||||
match bread_theme::write_shared_css() {
|
||||
Ok(path) => {
|
||||
eprintln!("bread-theme: {verb} {}", path.display());
|
||||
ExitCode::SUCCESS
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("bread-theme: failed to write stylesheet: {e}");
|
||||
ExitCode::FAILURE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn main() -> ExitCode {
|
||||
let cmd = std::env::args().nth(1).unwrap_or_else(|| "generate".into());
|
||||
match cmd.as_str() {
|
||||
"path" => {
|
||||
println!("{}", bread_theme::shared_css_path().display());
|
||||
ExitCode::SUCCESS
|
||||
}
|
||||
"print" => {
|
||||
print!("{}", bread_theme::render());
|
||||
ExitCode::SUCCESS
|
||||
}
|
||||
"generate" => write_and_report("wrote"),
|
||||
// `reload` is `generate` from the caller's view, but it's the verb to use
|
||||
// after changing pywal colours: rewriting the file (atomic rename) trips
|
||||
// the file monitor in every running bread GUI, so they all re-read the
|
||||
// palette and recolour live — shared widgets *and* each app's own rules.
|
||||
"reload" => write_and_report("reloaded"),
|
||||
"-h" | "--help" | "help" => {
|
||||
eprintln!(
|
||||
"bread-theme — shared stylesheet generator\n\n\
|
||||
USAGE:\n bread-theme [generate|reload|path|print]\n\n\
|
||||
generate render the pywal palette to the shared stylesheet (default)\n\
|
||||
reload re-render and signal running bread GUIs to recolour live\n\
|
||||
path print the stylesheet path ({})\n\
|
||||
print render to stdout without writing",
|
||||
bread_theme::shared_css_path().display()
|
||||
);
|
||||
ExitCode::SUCCESS
|
||||
}
|
||||
other => {
|
||||
eprintln!("bread-theme: unknown command '{other}' (try generate|reload|path|print)");
|
||||
ExitCode::FAILURE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,100 +1,7 @@
|
|||
use gtk4::gio;
|
||||
use gtk4::prelude::*;
|
||||
use gtk4::CssProvider;
|
||||
use std::cell::RefCell;
|
||||
use std::path::Path;
|
||||
|
||||
thread_local! {
|
||||
static SHARED_PROVIDER: RefCell<Option<CssProvider>> = const { RefCell::new(None) };
|
||||
static SHARED_MONITOR: RefCell<Option<gio::FileMonitor>> = const { RefCell::new(None) };
|
||||
static APP_PROVIDER: RefCell<Option<CssProvider>> = const { RefCell::new(None) };
|
||||
static APP_MONITOR: RefCell<Option<gio::FileMonitor>> = const { RefCell::new(None) };
|
||||
#[allow(clippy::type_complexity)]
|
||||
static APP_BUILDER: RefCell<Option<Box<dyn Fn() -> String>>> = const { RefCell::new(None) };
|
||||
}
|
||||
|
||||
fn reload_shared() {
|
||||
let css = std::fs::read_to_string(crate::shared_css_path())
|
||||
.unwrap_or_else(|_| crate::render());
|
||||
SHARED_PROVIDER.with(|cell| apply_css(&css, cell));
|
||||
}
|
||||
|
||||
fn reload_app() {
|
||||
let css = APP_BUILDER.with(|b| b.borrow().as_ref().map(|f| f()));
|
||||
if let Some(css) = css {
|
||||
APP_PROVIDER.with(|cell| apply_css(&css, cell));
|
||||
}
|
||||
}
|
||||
|
||||
/// Watch the shared stylesheet for changes and run `reload` when it's rewritten.
|
||||
///
|
||||
/// `bread-theme` writes the file with write-tmp-then-rename (atomic), which
|
||||
/// *replaces the inode*. A monitor on the file itself dies after the first
|
||||
/// replace (inotify reports DELETE_SELF and never re-arms), so we monitor the
|
||||
/// parent *directory* and filter for the stylesheet's filename — that fires
|
||||
/// reliably on every reload. Returns the monitor (keep it alive to stay armed).
|
||||
fn watch_theme_file(reload: fn()) -> Option<gio::FileMonitor> {
|
||||
let target = crate::shared_css_path();
|
||||
let dir = target.parent()?;
|
||||
// The dir must exist to be monitored; `bread-theme generate` makes it at
|
||||
// login, but create it here too so a GUI started first still arms the watch.
|
||||
let _ = std::fs::create_dir_all(dir);
|
||||
let monitor = gio::File::for_path(dir)
|
||||
.monitor_directory(gio::FileMonitorFlags::WATCH_MOVES, gio::Cancellable::NONE)
|
||||
.ok()?;
|
||||
monitor.connect_changed(move |_, file, other, _event| {
|
||||
// The rename lands as an event whose file (or move destination) is the
|
||||
// stylesheet. Match either to catch both CREATED/CHANGED and MOVED_IN.
|
||||
let is_target = |f: &gio::File| f.path().as_deref() == Some(target.as_path());
|
||||
if is_target(file) || other.is_some_and(is_target) {
|
||||
reload();
|
||||
}
|
||||
});
|
||||
Some(monitor)
|
||||
}
|
||||
|
||||
/// Apply an app's *own* stylesheet and keep it live across palette changes.
|
||||
///
|
||||
/// `build` is called now to produce the app-specific CSS, and again every time
|
||||
/// the shared theme file is rewritten — i.e. whenever `bread-theme reload` (or
|
||||
/// `generate`) runs after pywal changes. The app recolours in place, no restart.
|
||||
///
|
||||
/// This is the counterpart to [`apply_shared`]: that hot-reloads the *shared*
|
||||
/// component sheet; this hot-reloads the app's *own* rules (which are built from
|
||||
/// the palette, so they'd otherwise be frozen at startup). Apps that build their
|
||||
/// CSS from [`crate::stylesheet`] themselves can use this alone; apps that layer
|
||||
/// on top of [`apply_shared`] call both.
|
||||
///
|
||||
/// Call once at startup. The closure should read the current palette
|
||||
/// ([`crate::load_palette`]) each time so it picks up the new colours.
|
||||
pub fn apply_app_css<F: Fn() -> String + 'static>(build: F) {
|
||||
APP_BUILDER.with(|b| *b.borrow_mut() = Some(Box::new(build)));
|
||||
reload_app();
|
||||
APP_MONITOR.with(|cell| {
|
||||
if cell.borrow().is_some() {
|
||||
return;
|
||||
}
|
||||
*cell.borrow_mut() = watch_theme_file(reload_app);
|
||||
});
|
||||
}
|
||||
|
||||
/// Load the ecosystem's shared stylesheet (the file written by
|
||||
/// `bread-theme generate`, or a freshly rendered fallback if absent) at
|
||||
/// APPLICATION priority, and watch the file so the whole UI recolours live when
|
||||
/// the palette changes — no app rebuild or restart needed.
|
||||
///
|
||||
/// Call once at startup; then add the app's own CSS provider *after* this so
|
||||
/// app-specific rules win on equal specificity.
|
||||
pub fn apply_shared() {
|
||||
reload_shared();
|
||||
SHARED_MONITOR.with(|cell| {
|
||||
if cell.borrow().is_some() {
|
||||
return;
|
||||
}
|
||||
*cell.borrow_mut() = watch_theme_file(reload_shared);
|
||||
});
|
||||
}
|
||||
|
||||
/// Apply a CSS string to the default display at APPLICATION priority.
|
||||
/// Re-uses an existing provider if one is passed in (for SIGHUP reloads).
|
||||
pub fn apply_css(css: &str, provider: &RefCell<Option<CssProvider>>) {
|
||||
|
|
|
|||
|
|
@ -54,165 +54,6 @@ pub fn css_vars(p: &Palette) -> String {
|
|||
)
|
||||
}
|
||||
|
||||
/// Relative luminance (WCAG, sRGB) of a `#rrggbb` colour, 0.0 (black) – 1.0 (white).
|
||||
pub fn luminance(hex: &str) -> f32 {
|
||||
let h = hex.trim_start_matches('#');
|
||||
let lin = |i: usize| -> f32 {
|
||||
let c = u8::from_str_radix(h.get(i..i + 2).unwrap_or("00"), 16).unwrap_or(0) as f32 / 255.0;
|
||||
if c <= 0.04045 { c / 12.92 } else { ((c + 0.055) / 1.055).powf(2.4) }
|
||||
};
|
||||
0.2126 * lin(0) + 0.7152 * lin(2) + 0.0722 * lin(4)
|
||||
}
|
||||
|
||||
/// Pick a legible ink (near-black or near-white) for text drawn on `hex`.
|
||||
/// 0.179 is the WCAG crossover where contrast against black equals contrast
|
||||
/// against white — so whichever side we pick always wins. This is what keeps
|
||||
/// text readable no matter how light or dark pywal makes a given palette slot,
|
||||
/// without altering the palette colours themselves.
|
||||
pub fn ink_on(hex: &str) -> &'static str {
|
||||
if luminance(hex) > 0.179 { "#11111b" } else { "#f5f5f5" }
|
||||
}
|
||||
|
||||
/// Canonical `@define-color` block: the single naming all bread apps share.
|
||||
/// `surface` = color0 (darkest surface), `overlay` = color7 (muted), and
|
||||
/// `accent` = color4. Apps must use these names, not raw palette slots, so the
|
||||
/// whole ecosystem recolours together.
|
||||
///
|
||||
/// The `on-*` colours are computed ink (black/white) guaranteed to be legible on
|
||||
/// the matching background — use `@on-surface` for text on a `@surface` panel,
|
||||
/// `@on-accent` on an `@accent` button, etc. They exist because pywal can emit a
|
||||
/// light value in any slot, and white text on a light surface disappears.
|
||||
fn define_colors(p: &Palette) -> String {
|
||||
format!(
|
||||
"@define-color bg {bg};\n\
|
||||
@define-color fg {fg};\n\
|
||||
@define-color surface {c0};\n\
|
||||
@define-color overlay {c7};\n\
|
||||
@define-color accent {c4};\n\
|
||||
@define-color red {c1};\n\
|
||||
@define-color green {c2};\n\
|
||||
@define-color yellow {c3};\n\
|
||||
@define-color blue {c4};\n\
|
||||
@define-color pink {c5};\n\
|
||||
@define-color teal {c6};\n\
|
||||
@define-color on-bg {on_bg};\n\
|
||||
@define-color on-surface {on_surface};\n\
|
||||
@define-color on-accent {on_accent};\n\
|
||||
@define-color on-red {on_red};\n\
|
||||
@define-color on-overlay {on_overlay};\n",
|
||||
bg = p.background, fg = p.foreground,
|
||||
c0 = p.color0, c1 = p.color1, c2 = p.color2, c3 = p.color3,
|
||||
c4 = p.color4, c5 = p.color5, c6 = p.color6, c7 = p.color7,
|
||||
on_bg = ink_on(&p.background),
|
||||
on_surface = ink_on(&p.color0),
|
||||
on_accent = ink_on(&p.color4),
|
||||
on_red = ink_on(&p.color1),
|
||||
on_overlay = ink_on(&p.color7),
|
||||
)
|
||||
}
|
||||
|
||||
/// The full shared component stylesheet — the single source of truth for how
|
||||
/// every bread GUI (bos-settings, breadbar, breadbox, breadpad, breadman) styles
|
||||
/// common widgets. Apps load this, then append only their own *layout* rules.
|
||||
///
|
||||
/// Built entirely from the design tokens (font, spacing, radii) and the
|
||||
/// `@define-color` palette, so changing the palette recolours every app.
|
||||
pub fn stylesheet(p: &Palette) -> String {
|
||||
use tokens::*;
|
||||
format!(
|
||||
"{vars}\
|
||||
* {{ font-family: '{font}'; font-size: {base}px; }}\n\
|
||||
/* Colour is set on containers; labels inherit it, so text on any panel,\
|
||||
button, or accent is always the legible ink for that background. Bare\
|
||||
`label {{ color }}` is deliberately avoided — as a type selector it\
|
||||
would override a container's colour on its own child labels. */\n\
|
||||
window {{ background-color: @bg; color: @on-bg; }}\n\
|
||||
.dim-label, .dim {{ opacity: 0.6; font-size: {sec}px; }}\n\
|
||||
.title {{ font-size: 1.4em; font-weight: bold; }}\n\
|
||||
.heading {{ font-weight: bold; opacity: 0.85; }}\n\
|
||||
.subtitle {{ opacity: 0.7; font-size: {sec}px; }}\n\
|
||||
button {{ background-color: @surface; color: @on-surface; border: none;\
|
||||
border-radius: {r1}px; padding: {sm}px {lg}px; }}\n\
|
||||
button:hover {{ background-color: alpha(@on-surface, 0.14); }}\n\
|
||||
button:active {{ background-color: alpha(@on-surface, 0.20); }}\n\
|
||||
button:disabled {{ opacity: 0.5; }}\n\
|
||||
button.flat {{ background-color: transparent; color: @on-bg; }}\n\
|
||||
button.suggested-action {{ background-color: @accent; color: @on-accent; }}\n\
|
||||
button.suggested-action:hover {{ background-color: alpha(@accent, 0.85); }}\n\
|
||||
button.destructive-action {{ background-color: @red; color: @on-red; }}\n\
|
||||
button.destructive-action:hover {{ background-color: alpha(@red, 0.85); }}\n\
|
||||
entry, spinbutton {{ background-color: @surface; color: @on-surface;\
|
||||
border: 1px solid @overlay; border-radius: {r2}px;\
|
||||
padding: {xs}px {sm}px; caret-color: @on-surface; }}\n\
|
||||
entry:focus-within, spinbutton:focus-within {{ border-color: @accent; outline: none; }}\n\
|
||||
entry image, spinbutton button {{ color: @on-surface; }}\n\
|
||||
dropdown > button {{ background-color: @surface; color: @on-surface; border-radius: {r2}px; }}\n\
|
||||
popover > contents {{ background-color: @surface; color: @on-surface; border-radius: {r1}px; }}\n\
|
||||
switch {{ background-color: @overlay; border-radius: {pill}px; }}\n\
|
||||
switch:checked {{ background-color: @accent; }}\n\
|
||||
switch slider {{ background-color: @on-surface; border-radius: {pill}px; }}\n\
|
||||
list, listbox {{ background-color: transparent; }}\n\
|
||||
row {{ border-radius: {r2}px; }}\n\
|
||||
row:selected, list row:selected {{ background-color: @accent; color: @on-accent; }}\n\
|
||||
.sidebar {{ background-color: @surface; color: @on-surface; }}\n\
|
||||
.sidebar row {{ padding: {sm}px {md}px; }}\n\
|
||||
.sidebar row:selected {{ background-color: @accent; color: @on-accent; }}\n\
|
||||
.sidebar .section-header {{ padding: {md}px {md}px {xs}px {md}px;\
|
||||
font-size: {sec}px; font-weight: bold; opacity: 0.55; }}\n\
|
||||
.card {{ background-color: @surface; color: @on-surface; border-radius: {r1}px; padding: {md}px; }}\n\
|
||||
.chip, .pill {{ background-color: @overlay; color: @on-overlay; border-radius: {pill}px;\
|
||||
padding: {xs}px {md}px; font-size: {sec}px; }}\n\
|
||||
.chip.active, .pill.active {{ background-color: @accent; color: @on-accent; }}\n\
|
||||
scrollbar {{ background-color: transparent; }}\n\
|
||||
scrollbar slider {{ background-color: alpha(@on-bg, 0.25); border-radius: {pill}px;\
|
||||
min-width: 6px; min-height: 6px; }}\n\
|
||||
scrollbar slider:hover {{ background-color: alpha(@on-bg, 0.45); }}\n\
|
||||
textview, .mono {{ font-family: monospace; }}\n\
|
||||
textview text {{ background-color: @surface; color: @on-surface; }}\n",
|
||||
vars = define_colors(p),
|
||||
font = FONT_FAMILY,
|
||||
base = FONT_SIZE_BASE,
|
||||
sec = FONT_SIZE_SECONDARY,
|
||||
xs = SPACE_XS, sm = SPACE_SM, md = SPACE_MD, lg = SPACE_LG,
|
||||
r1 = RADIUS_PRIMARY, r2 = RADIUS_SECONDARY, pill = RADIUS_PILL,
|
||||
)
|
||||
}
|
||||
|
||||
/// Render the shared stylesheet for the current (pywal) palette. Used by the
|
||||
/// `bread-theme` generator and as the in-app fallback when the generated file
|
||||
/// isn't present yet.
|
||||
pub fn render() -> String {
|
||||
stylesheet(&load_palette())
|
||||
}
|
||||
|
||||
/// Canonical path of the generated shared stylesheet. Apps load it; the
|
||||
/// `bread-theme generate` CLI writes it. Per-session under `XDG_RUNTIME_DIR`,
|
||||
/// falling back to the cache dir.
|
||||
pub fn shared_css_path() -> std::path::PathBuf {
|
||||
if let Ok(rt) = std::env::var("XDG_RUNTIME_DIR") {
|
||||
if !rt.is_empty() {
|
||||
return std::path::PathBuf::from(rt).join("bread").join("theme.css");
|
||||
}
|
||||
}
|
||||
dirs::cache_dir()
|
||||
.unwrap_or_else(|| std::path::PathBuf::from("/tmp"))
|
||||
.join("bread")
|
||||
.join("theme.css")
|
||||
}
|
||||
|
||||
/// Write the shared stylesheet to [`shared_css_path`] (atomic rename). Returns
|
||||
/// the path written. Used by the `bread-theme` CLI.
|
||||
pub fn write_shared_css() -> std::io::Result<std::path::PathBuf> {
|
||||
let path = shared_css_path();
|
||||
if let Some(parent) = path.parent() {
|
||||
std::fs::create_dir_all(parent)?;
|
||||
}
|
||||
let tmp = path.with_extension("css.tmp");
|
||||
std::fs::write(&tmp, render())?;
|
||||
std::fs::rename(&tmp, &path)?;
|
||||
Ok(path)
|
||||
}
|
||||
|
||||
/// Convert a `#rrggbb` hex colour to `rgba(r, g, b, alpha)`.
|
||||
pub fn hex_to_rgba(hex: &str, alpha: f32) -> String {
|
||||
let h = hex.trim_start_matches('#');
|
||||
|
|
@ -241,66 +82,6 @@ mod tests {
|
|||
assert!(css.contains("14px"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stylesheet_defines_canonical_colors_and_components() {
|
||||
let css = stylesheet(&Palette::default());
|
||||
for name in &["bg", "fg", "surface", "overlay", "accent", "red", "blue"] {
|
||||
assert!(css.contains(&format!("@define-color {name} ")), "missing @define-color {name}");
|
||||
}
|
||||
// a representative spread of the shared component selectors
|
||||
for sel in &["button", "entry", "switch:checked", ".card", ".sidebar", "scrollbar slider", ".title"] {
|
||||
assert!(css.contains(sel), "stylesheet missing selector: {sel}");
|
||||
}
|
||||
assert!(css.contains("Varela Round"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn luminance_black_and_white_are_extremes() {
|
||||
assert!(luminance("#000000") < 0.01);
|
||||
assert!(luminance("#ffffff") > 0.99);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ink_on_picks_dark_text_for_light_backgrounds() {
|
||||
// Light pywal slots (the case that made white text vanish) get dark ink.
|
||||
assert_eq!(ink_on("#ffffff"), "#11111b");
|
||||
assert_eq!(ink_on("#f9e2af"), "#11111b"); // pale yellow
|
||||
assert_eq!(ink_on("#a6e3a1"), "#11111b"); // pale green
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ink_on_picks_light_text_for_dark_backgrounds() {
|
||||
assert_eq!(ink_on("#000000"), "#f5f5f5");
|
||||
assert_eq!(ink_on("#1e1e2e"), "#f5f5f5"); // catppuccin base
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stylesheet_defines_on_colors() {
|
||||
let css = stylesheet(&Palette::default());
|
||||
for name in &["on-bg", "on-surface", "on-accent", "on-red", "on-overlay"] {
|
||||
assert!(css.contains(&format!("@define-color {name} ")), "missing @define-color {name}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stylesheet_has_no_blanket_label_color_rule() {
|
||||
// A bare `label { color: ... }` would override container colours on child
|
||||
// labels — the bug that made coloured-background text illegible.
|
||||
let css = stylesheet(&Palette::default());
|
||||
assert!(!css.contains("label { color:"), "blanket label colour rule reintroduced");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shared_css_path_uses_runtime_dir() {
|
||||
std::env::set_var("XDG_RUNTIME_DIR", "/run/user/1234");
|
||||
assert_eq!(shared_css_path(), std::path::PathBuf::from("/run/user/1234/bread/theme.css"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_is_nonempty_css() {
|
||||
assert!(render().contains("@define-color bg "));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hex_to_rgba_known_value() {
|
||||
assert_eq!(hex_to_rgba("#1e1e2e", 1.0), "rgba(30, 30, 46, 1)");
|
||||
|
|
|
|||
|
|
@ -1,28 +0,0 @@
|
|||
# Maintainer: Breadway <rileyhorsham@gmail.com>
|
||||
|
||||
pkgname=bakery
|
||||
pkgver=0.2.3
|
||||
pkgrel=1
|
||||
pkgdesc="Package manager for the bread ecosystem"
|
||||
arch=('x86_64')
|
||||
url="https://github.com/Breadway/bread-ecosystem"
|
||||
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=('glibc' 'gcc-libs')
|
||||
makedepends=('rust' 'cargo')
|
||||
source=("${pkgname}-${pkgver}.tar.gz")
|
||||
sha256sums=('SKIP')
|
||||
|
||||
build() {
|
||||
cd "${srcdir}/${pkgname}-${pkgver}"
|
||||
cargo build --release --locked -p bakery
|
||||
}
|
||||
|
||||
package() {
|
||||
cd "${srcdir}/${pkgname}-${pkgver}"
|
||||
install -Dm755 target/release/bakery "${pkgdir}/usr/bin/bakery"
|
||||
install -Dm644 README.md "${pkgdir}/usr/share/doc/${pkgname}/README.md"
|
||||
}
|
||||
|
|
@ -36,8 +36,3 @@ description = "Profile-aware Wi-Fi state machine with Tailscale integration"
|
|||
name = "breadpad"
|
||||
repo = "Breadway/breadpad"
|
||||
description = "Quick-capture scratchpad and note viewer with AI classification"
|
||||
|
||||
[[products]]
|
||||
name = "breadpaper"
|
||||
repo = "Breadway/breadpaper"
|
||||
description = "Wallpaper manager for the bread desktop"
|
||||
|
|
|
|||
113
scripts/gen-index.sh
Executable file → Normal file
113
scripts/gen-index.sh
Executable file → Normal file
|
|
@ -1,28 +1,28 @@
|
|||
#!/usr/bin/env bash
|
||||
# Generate dl.breadway.dev/index.json from:
|
||||
# - registry/bread-ecosystem.toml (product list)
|
||||
# - <DL_DIR>/<name>/bakery.toml (per-product metadata, uploaded by release.yml)
|
||||
# - <DL_DIR>/ (built binaries + sha256 files)
|
||||
# - <repo>/bakery.toml (per-product metadata)
|
||||
# - /srv/breadway-dl/ (built binaries + sha256 files)
|
||||
#
|
||||
# Fallback for local dev: looks for ../name/bakery.toml (sibling repo checkout).
|
||||
# Run on hestia after each product build, before the dl server is refreshed.
|
||||
# Requires: jq, python3 (tomllib, stdlib since 3.11), sha256sum
|
||||
# Requires: jq, python3 (for toml parsing via tomllib), sha256sum
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="${SCRIPT_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)}"
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
DL_DIR="${DL_DIR:-/srv/breadway-dl}"
|
||||
DL_BASE="${DL_BASE:-https://dl.breadway.dev}"
|
||||
GH_BASE="https://github.com"
|
||||
OUT="${DL_DIR}/index.json"
|
||||
|
||||
# Read the product list from the registry TOML instead of a hardcoded array.
|
||||
mapfile -t products < <(python3 -c "
|
||||
import tomllib, sys
|
||||
with open('${SCRIPT_DIR}/registry/bread-ecosystem.toml', 'rb') as f:
|
||||
d = tomllib.load(f)
|
||||
for p in d['products']:
|
||||
print(p['name'], p['repo'])
|
||||
")
|
||||
# Products are read from the registry. Each line is "name repo".
|
||||
products=(
|
||||
"bakery Breadway/bread-ecosystem"
|
||||
"bread Breadway/bread"
|
||||
"breadbar Breadway/breadbar"
|
||||
"breadbox Breadway/breadbox"
|
||||
"breadcrumbs Breadway/breadcrumbs"
|
||||
"breadpad Breadway/breadpad"
|
||||
)
|
||||
|
||||
# Build a JSON package entry for one product.
|
||||
# $1 = product name, $2 = github repo slug
|
||||
|
|
@ -34,14 +34,14 @@ build_package_json() {
|
|||
local pkg_dir="${DL_DIR}/${name}"
|
||||
if [[ ! -d "${pkg_dir}" ]]; then
|
||||
echo " warning: no release dir for ${name} at ${pkg_dir}" >&2
|
||||
return 1
|
||||
return
|
||||
fi
|
||||
|
||||
# The latest symlink must point to the current version dir.
|
||||
local latest_link="${pkg_dir}/latest"
|
||||
if [[ ! -L "${latest_link}" ]]; then
|
||||
echo " warning: no 'latest' symlink for ${name}" >&2
|
||||
return 1
|
||||
return
|
||||
fi
|
||||
local version_dir
|
||||
version_dir="$(readlink -f "${latest_link}")"
|
||||
|
|
@ -51,11 +51,11 @@ build_package_json() {
|
|||
# Collect all binaries in the version dir (executables only; skip metadata files).
|
||||
local binaries_json="[]"
|
||||
for bin_path in "${version_dir}"/*; do
|
||||
[[ "${bin_path}" == *.sha256 ]] && continue
|
||||
[[ "${bin_path}" == *.toml ]] && continue
|
||||
[[ "${bin_path}" == *.sha256 ]] && continue
|
||||
[[ "${bin_path}" == *.toml ]] && continue
|
||||
[[ "${bin_path}" == *.service ]] && continue
|
||||
[[ "${bin_path}" == *.css ]] && continue
|
||||
[[ "${bin_path}" == *.txt ]] && continue
|
||||
[[ "${bin_path}" == *.css ]] && continue
|
||||
[[ "${bin_path}" == *.txt ]] && continue
|
||||
[[ -f "${bin_path}" ]] || continue
|
||||
local bin_name
|
||||
bin_name="$(basename "${bin_path}")"
|
||||
|
|
@ -77,78 +77,45 @@ build_package_json() {
|
|||
binaries_json="$(jq -n --argjson arr "${binaries_json}" --argjson e "${entry}" '$arr + [$e]')"
|
||||
done
|
||||
|
||||
# Locate bakery.toml. The release workflow copies it into the version dir
|
||||
# alongside the binaries (${version_dir}/bakery.toml). Fall back to a
|
||||
# sibling repo checkout for local dev use.
|
||||
local bakery_toml="${version_dir}/bakery.toml"
|
||||
# Read bakery.toml: the release workflow copies it to DL_DIR alongside the
|
||||
# binaries; fall back to a sibling checkout for local dev use.
|
||||
local bakery_toml="${DL_DIR}/${name}/bakery.toml"
|
||||
if [[ ! -f "${bakery_toml}" ]]; then
|
||||
bakery_toml="${SCRIPT_DIR}/../${name}/bakery.toml"
|
||||
fi
|
||||
if [[ ! -f "${bakery_toml}" ]]; then
|
||||
echo "ERROR: bakery.toml not found for ${name} — release.yml must copy it to \${DL_DIR}/${name}/\${VERSION}/bakery.toml" >&2
|
||||
return 1
|
||||
fi
|
||||
local description=""
|
||||
local system_deps="[]"
|
||||
local bread_deps="[]"
|
||||
local services="[]"
|
||||
local config="null"
|
||||
local post_install="[]"
|
||||
|
||||
local description system_deps optional_system_deps bread_deps services config post_install
|
||||
|
||||
description="$(python3 -c "
|
||||
import tomllib
|
||||
if [[ -f "${bakery_toml}" ]]; then
|
||||
description="$(python3 -c "
|
||||
import tomllib, sys
|
||||
with open('${bakery_toml}', 'rb') as f:
|
||||
d = tomllib.load(f)
|
||||
print(d.get('description', ''))
|
||||
" 2>/dev/null || true)"
|
||||
|
||||
system_deps="$(python3 -c "
|
||||
import tomllib, json
|
||||
system_deps="$(python3 -c "
|
||||
import tomllib, json, sys
|
||||
with open('${bakery_toml}', 'rb') as f:
|
||||
d = tomllib.load(f)
|
||||
print(json.dumps(d.get('system_deps', [])))
|
||||
" 2>/dev/null || echo "[]")"
|
||||
|
||||
optional_system_deps="$(python3 -c "
|
||||
import tomllib, json
|
||||
with open('${bakery_toml}', 'rb') as f:
|
||||
d = tomllib.load(f)
|
||||
print(json.dumps(d.get('optional_system_deps', [])))
|
||||
" 2>/dev/null || echo "[]")"
|
||||
|
||||
bread_deps="$(python3 -c "
|
||||
import tomllib, json
|
||||
bread_deps="$(python3 -c "
|
||||
import tomllib, json, sys
|
||||
with open('${bakery_toml}', 'rb') as f:
|
||||
d = tomllib.load(f)
|
||||
print(json.dumps(d.get('bread_deps', [])))
|
||||
" 2>/dev/null || echo "[]")"
|
||||
|
||||
# [[service]] entries → [{unit, enable}]
|
||||
services="$(python3 -c "
|
||||
import tomllib, json
|
||||
with open('${bakery_toml}', 'rb') as f:
|
||||
d = tomllib.load(f)
|
||||
svcs = d.get('service', [])
|
||||
print(json.dumps([{'unit': s['unit'], 'enable': s.get('enable', False)} for s in svcs]))
|
||||
" 2>/dev/null || echo "[]")"
|
||||
|
||||
# [config] → {dir, example?} or null
|
||||
config="$(python3 -c "
|
||||
import tomllib, json
|
||||
with open('${bakery_toml}', 'rb') as f:
|
||||
d = tomllib.load(f)
|
||||
cfg = d.get('config')
|
||||
if cfg:
|
||||
obj = {'dir': cfg['dir']}
|
||||
if 'example' in cfg:
|
||||
obj['example'] = cfg['example']
|
||||
print(json.dumps(obj))
|
||||
else:
|
||||
print('null')
|
||||
" 2>/dev/null || echo "null")"
|
||||
|
||||
post_install="$(python3 -c "
|
||||
import tomllib, json
|
||||
post_install="$(python3 -c "
|
||||
import tomllib, json, sys
|
||||
with open('${bakery_toml}', 'rb') as f:
|
||||
d = tomllib.load(f)
|
||||
print(json.dumps(d.get('install', {}).get('post_install', [])))
|
||||
" 2>/dev/null || echo "[]")"
|
||||
fi
|
||||
|
||||
jq -n \
|
||||
--arg name "${name}" \
|
||||
|
|
@ -156,10 +123,8 @@ print(json.dumps(d.get('install', {}).get('post_install', [])))
|
|||
--arg version "${version}" \
|
||||
--argjson binaries "${binaries_json}" \
|
||||
--argjson system_deps "${system_deps}" \
|
||||
--argjson optional_system_deps "${optional_system_deps}" \
|
||||
--argjson bread_deps "${bread_deps}" \
|
||||
--argjson services "${services}" \
|
||||
--argjson config "${config}" \
|
||||
--argjson post_install "${post_install}" \
|
||||
'{
|
||||
name: $name,
|
||||
|
|
@ -167,10 +132,8 @@ print(json.dumps(d.get('install', {}).get('post_install', [])))
|
|||
version: $version,
|
||||
binaries: $binaries,
|
||||
system_deps: $system_deps,
|
||||
optional_system_deps: $optional_system_deps,
|
||||
bread_deps: $bread_deps,
|
||||
services: $services,
|
||||
config: $config,
|
||||
post_install: $post_install
|
||||
}'
|
||||
}
|
||||
|
|
|
|||
33
scripts/get.sh
Executable file → Normal file
33
scripts/get.sh
Executable file → Normal file
|
|
@ -1,10 +1,12 @@
|
|||
#!/bin/sh
|
||||
# Bootstrap script: downloads and installs the `bakery` binary.
|
||||
# Bootstrap script: installs the `bakery` binary.
|
||||
# Usage: curl https://breadway.dev/get | sh
|
||||
# Or: curl -sSfL https://breadway.dev/get | sh
|
||||
set -eu
|
||||
|
||||
BAKERY_VERSION="${BAKERY_VERSION:-latest}"
|
||||
DL_PRIMARY="https://dl.breadway.dev/bakery/${BAKERY_VERSION}/bakery-x86_64"
|
||||
DL_FALLBACK="https://github.com/Breadway/bread-ecosystem/releases/download/${BAKERY_VERSION}/bakery-x86_64"
|
||||
BIN_DIR="${BAKERY_BIN_DIR:-$HOME/.local/bin}"
|
||||
|
||||
die() { echo "error: $*" >&2; exit 1; }
|
||||
|
|
@ -13,20 +15,6 @@ die() { echo "error: $*" >&2; exit 1; }
|
|||
uname -m | grep -q x86_64 || die "bakery only supports x86_64 (got $(uname -m))"
|
||||
uname -s | grep -q Linux || die "bakery only supports Linux (got $(uname -s))"
|
||||
|
||||
# Build download URLs. GitHub's "latest" redirect lives at a different path from
|
||||
# versioned releases, so we handle them separately and always prefix tags with 'v'.
|
||||
if [ "${BAKERY_VERSION}" = "latest" ]; then
|
||||
DL_PRIMARY="https://dl.breadway.dev/bakery/latest/bakery-x86_64"
|
||||
DL_FALLBACK="https://github.com/Breadway/bread-ecosystem/releases/latest/download/bakery-x86_64"
|
||||
SHA256_URL="https://dl.breadway.dev/bakery/latest/bakery-x86_64.sha256"
|
||||
else
|
||||
# Strip a leading 'v' if the caller included it, then add it back consistently.
|
||||
ver="${BAKERY_VERSION#v}"
|
||||
DL_PRIMARY="https://dl.breadway.dev/bakery/${ver}/bakery-x86_64"
|
||||
DL_FALLBACK="https://github.com/Breadway/bread-ecosystem/releases/download/v${ver}/bakery-x86_64"
|
||||
SHA256_URL="https://dl.breadway.dev/bakery/${ver}/bakery-x86_64.sha256"
|
||||
fi
|
||||
|
||||
# Pick a download tool.
|
||||
if command -v curl >/dev/null 2>&1; then
|
||||
fetch() { curl -fsSL "$1" -o "$2"; }
|
||||
|
|
@ -38,26 +26,13 @@ fi
|
|||
|
||||
mkdir -p "${BIN_DIR}"
|
||||
TMP="$(mktemp)"
|
||||
trap 'rm -f "${TMP}" "${TMP}.sha256"' EXIT
|
||||
trap 'rm -f "${TMP}"' EXIT
|
||||
|
||||
echo "downloading bakery…"
|
||||
if fetch "${DL_PRIMARY}" "${TMP}" 2>/dev/null; then
|
||||
echo " from dl.breadway.dev"
|
||||
# Verify checksum when available from primary.
|
||||
if fetch "${SHA256_URL}" "${TMP}.sha256" 2>/dev/null; then
|
||||
expected="$(awk '{print $1}' "${TMP}.sha256")"
|
||||
actual="$(sha256sum "${TMP}" | awk '{print $1}')"
|
||||
if [ "${expected}" != "${actual}" ]; then
|
||||
die "SHA-256 checksum mismatch (expected ${expected}, got ${actual})"
|
||||
fi
|
||||
echo " checksum verified"
|
||||
else
|
||||
echo " warning: could not fetch checksum — skipping verification"
|
||||
fi
|
||||
elif fetch "${DL_FALLBACK}" "${TMP}" 2>/dev/null; then
|
||||
echo " from GitHub (fallback)"
|
||||
# No .sha256 on the GitHub fallback path; proceed without verification.
|
||||
echo " warning: checksum not verified for GitHub fallback download"
|
||||
else
|
||||
die "failed to download bakery from both primary and fallback URLs"
|
||||
fi
|
||||
|
|
|
|||
|
|
@ -1,113 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
# Smoke-test gen-index.sh against a minimal fixture DL_DIR tree.
|
||||
# Verifies that services, config, system_deps, optional_system_deps,
|
||||
# description, and post_install are all populated correctly.
|
||||
set -euo pipefail
|
||||
|
||||
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
FIXTURE="$(mktemp -d)"
|
||||
FAKE_REGISTRY="$(mktemp -d)"
|
||||
trap 'rm -rf "${FIXTURE}" "${FAKE_REGISTRY}"' EXIT
|
||||
|
||||
fail() { echo "FAIL: $*" >&2; exit 1; }
|
||||
|
||||
# ── Build a minimal release tree for "fakepkg" ───────────────────────────────
|
||||
PKG_VER_DIR="${FIXTURE}/fakepkg/0.1.0"
|
||||
mkdir -p "${PKG_VER_DIR}"
|
||||
|
||||
printf 'fake-binary-content' > "${PKG_VER_DIR}/fakepkg-x86_64"
|
||||
sha256sum "${PKG_VER_DIR}/fakepkg-x86_64" | awk '{print $1}' \
|
||||
> "${PKG_VER_DIR}/fakepkg-x86_64.sha256"
|
||||
printf '[Unit]\nDescription=fakepkg\n' > "${PKG_VER_DIR}/fakepkg.service"
|
||||
printf '# example config\n' > "${PKG_VER_DIR}/fakepkg.example.toml"
|
||||
|
||||
cat > "${PKG_VER_DIR}/bakery.toml" <<'TOML'
|
||||
name = "fakepkg"
|
||||
description = "A fake package for testing"
|
||||
binaries = ["fakepkg"]
|
||||
system_deps = ["gtk4"]
|
||||
optional_system_deps = ["hyprland"]
|
||||
bread_deps = []
|
||||
|
||||
[[service]]
|
||||
unit = "fakepkg.service"
|
||||
enable = true
|
||||
|
||||
[config]
|
||||
dir = "~/.config/fakepkg"
|
||||
example = "fakepkg.example.toml"
|
||||
|
||||
[install]
|
||||
post_install = ["echo installed"]
|
||||
TOML
|
||||
|
||||
# gen-index looks for bakery.toml at ${DL_DIR}/<name>/bakery.toml (no version)
|
||||
cp "${PKG_VER_DIR}/bakery.toml" "${FIXTURE}/fakepkg/bakery.toml"
|
||||
ln -s "${PKG_VER_DIR}" "${FIXTURE}/fakepkg/latest"
|
||||
|
||||
# ── Minimal registry pointing only at fakepkg ────────────────────────────────
|
||||
mkdir -p "${FAKE_REGISTRY}/registry"
|
||||
cat > "${FAKE_REGISTRY}/registry/bread-ecosystem.toml" <<'TOML'
|
||||
[ecosystem]
|
||||
name = "test"
|
||||
|
||||
[[products]]
|
||||
name = "fakepkg"
|
||||
repo = "Test/fakepkg"
|
||||
description = "A fake package"
|
||||
TOML
|
||||
|
||||
# ── Run gen-index with overridden SCRIPT_DIR and DL_DIR ──────────────────────
|
||||
OUT="${FIXTURE}/index.json"
|
||||
SCRIPT_DIR="${FAKE_REGISTRY}" DL_DIR="${FIXTURE}" DL_BASE="https://dl.test" \
|
||||
bash "${REPO_ROOT}/scripts/gen-index.sh" 2>&1 | sed 's/^/ [gen-index] /'
|
||||
|
||||
[[ -f "${OUT}" ]] || fail "index.json was not produced"
|
||||
|
||||
# ── Assertions ────────────────────────────────────────────────────────────────
|
||||
jq -e '.packages.fakepkg' "${OUT}" > /dev/null \
|
||||
|| fail "fakepkg missing from index"
|
||||
|
||||
check() {
|
||||
local label="$1" expected="$2" actual="$3"
|
||||
[[ "${actual}" == "${expected}" ]] \
|
||||
|| fail "${label}: expected '${expected}', got '${actual}'"
|
||||
}
|
||||
|
||||
check "description" \
|
||||
"A fake package for testing" \
|
||||
"$(jq -r '.packages.fakepkg.description' "${OUT}")"
|
||||
|
||||
check "system_deps" \
|
||||
"gtk4" \
|
||||
"$(jq -r '.packages.fakepkg.system_deps | join(",")' "${OUT}")"
|
||||
|
||||
check "optional_system_deps" \
|
||||
"hyprland" \
|
||||
"$(jq -r '.packages.fakepkg.optional_system_deps | join(",")' "${OUT}")"
|
||||
|
||||
check "services[0].unit" \
|
||||
"fakepkg.service" \
|
||||
"$(jq -r '.packages.fakepkg.services[0].unit' "${OUT}")"
|
||||
|
||||
check "services[0].enable" \
|
||||
"true" \
|
||||
"$(jq -r '.packages.fakepkg.services[0].enable' "${OUT}")"
|
||||
|
||||
check "config.dir" \
|
||||
"~/.config/fakepkg" \
|
||||
"$(jq -r '.packages.fakepkg.config.dir' "${OUT}")"
|
||||
|
||||
check "config.example" \
|
||||
"fakepkg.example.toml" \
|
||||
"$(jq -r '.packages.fakepkg.config.example' "${OUT}")"
|
||||
|
||||
check "binaries[0].name" \
|
||||
"fakepkg-x86_64" \
|
||||
"$(jq -r '.packages.fakepkg.binaries[0].name' "${OUT}")"
|
||||
|
||||
check "post_install[0]" \
|
||||
"echo installed" \
|
||||
"$(jq -r '.packages.fakepkg.post_install[0]' "${OUT}")"
|
||||
|
||||
echo "OK: all gen-index assertions passed"
|
||||
Loading…
Add table
Add a link
Reference in a new issue