Compare commits
10 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
10f62fb1a6 | ||
|
|
5e58558dd3 | ||
|
|
0494650805 | ||
|
|
46db2c23cd | ||
|
|
8305b4a58b | ||
|
|
578067183b | ||
|
|
8b659bf83a | ||
|
|
b3a3b0609b | ||
|
|
ddfba38fc5 | ||
|
|
baf145db8a |
11 changed files with 627 additions and 4 deletions
21
.forgejo/workflows/mirror.yml
Normal file
21
.forgejo/workflows/mirror.yml
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
name: Mirror to GitHub
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: ['**']
|
||||||
|
tags: ['**']
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
mirror:
|
||||||
|
runs-on: [self-hosted, hestia]
|
||||||
|
steps:
|
||||||
|
- name: Mirror to GitHub
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
git clone --mirror "https://git.breadway.dev/${GITHUB_REPOSITORY}.git" repo.git
|
||||||
|
cd repo.git
|
||||||
|
# Mirror only branches and tags (not refs/pull/*, which GitHub rejects);
|
||||||
|
# --prune deletes GitHub refs that no longer exist on Forgejo.
|
||||||
|
git push --prune \
|
||||||
|
"https://x-access-token:${{ secrets.MIRROR_TOKEN }}@github.com/Breadway/bread-ecosystem.git" \
|
||||||
|
'+refs/heads/*:refs/heads/*' '+refs/tags/*:refs/tags/*'
|
||||||
40
.forgejo/workflows/package.yml
Normal file
40
.forgejo/workflows/package.yml
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
name: Build and publish package
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags: ['v*']
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
package:
|
||||||
|
runs-on: [self-hosted, hestia]
|
||||||
|
container:
|
||||||
|
image: archlinux:latest
|
||||||
|
steps:
|
||||||
|
# Note: no actions/checkout — the archlinux image has no Node, which JS
|
||||||
|
# actions require. Everything runs as shell steps and clones manually.
|
||||||
|
- name: Build and publish
|
||||||
|
env:
|
||||||
|
PUBLISH_TOKEN: ${{ secrets.REGISTRY_TOKEN }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
VERSION="${GITHUB_REF_NAME#v}"
|
||||||
|
pacman -Syu --noconfirm base-devel git rust cargo
|
||||||
|
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"
|
||||||
118
BREAD_DESIGN_SYSTEM.md
Normal file
118
BREAD_DESIGN_SYSTEM.md
Normal file
|
|
@ -0,0 +1,118 @@
|
||||||
|
# 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
|
||||||
30
README.md
30
README.md
|
|
@ -18,6 +18,36 @@ bakery install breadbar
|
||||||
| `breadcrumbs` | Profile-aware Wi-Fi state machine with Tailscale exit-node management and a self-healing watch daemon |
|
| `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`) |
|
| `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
|
## Installing bakery
|
||||||
|
|
||||||
`bakery` is the package manager for the ecosystem. Install it with the bootstrap script:
|
`bakery` is the package manager for the ecosystem. Install it with the bootstrap script:
|
||||||
|
|
|
||||||
|
|
@ -18,3 +18,9 @@ gtk4 = { version = "0.11", features = ["v4_12"], optional = true }
|
||||||
# Enable GTK4 CSS provider helpers (breadbar, breadbox, breadpad use this).
|
# Enable GTK4 CSS provider helpers (breadbar, breadbox, breadpad use this).
|
||||||
# bread (daemon) and breadcrumbs (CLI) depend on this crate without the feature.
|
# bread (daemon) and breadcrumbs (CLI) depend on this crate without the feature.
|
||||||
gtk = ["dep:gtk4"]
|
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"
|
||||||
|
|
|
||||||
62
bread-theme/src/bin/bread-theme.rs
Normal file
62
bread-theme/src/bin/bread-theme.rs
Normal file
|
|
@ -0,0 +1,62 @@
|
||||||
|
//! `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,7 +1,100 @@
|
||||||
|
use gtk4::gio;
|
||||||
|
use gtk4::prelude::*;
|
||||||
use gtk4::CssProvider;
|
use gtk4::CssProvider;
|
||||||
use std::cell::RefCell;
|
use std::cell::RefCell;
|
||||||
use std::path::Path;
|
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.
|
/// Apply a CSS string to the default display at APPLICATION priority.
|
||||||
/// Re-uses an existing provider if one is passed in (for SIGHUP reloads).
|
/// Re-uses an existing provider if one is passed in (for SIGHUP reloads).
|
||||||
pub fn apply_css(css: &str, provider: &RefCell<Option<CssProvider>>) {
|
pub fn apply_css(css: &str, provider: &RefCell<Option<CssProvider>>) {
|
||||||
|
|
|
||||||
|
|
@ -54,6 +54,165 @@ 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)`.
|
/// Convert a `#rrggbb` hex colour to `rgba(r, g, b, alpha)`.
|
||||||
pub fn hex_to_rgba(hex: &str, alpha: f32) -> String {
|
pub fn hex_to_rgba(hex: &str, alpha: f32) -> String {
|
||||||
let h = hex.trim_start_matches('#');
|
let h = hex.trim_start_matches('#');
|
||||||
|
|
@ -82,6 +241,66 @@ mod tests {
|
||||||
assert!(css.contains("14px"));
|
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]
|
#[test]
|
||||||
fn hex_to_rgba_known_value() {
|
fn hex_to_rgba_known_value() {
|
||||||
assert_eq!(hex_to_rgba("#1e1e2e", 1.0), "rgba(30, 30, 46, 1)");
|
assert_eq!(hex_to_rgba("#1e1e2e", 1.0), "rgba(30, 30, 46, 1)");
|
||||||
|
|
|
||||||
28
packaging/arch/PKGBUILD
Normal file
28
packaging/arch/PKGBUILD
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
# 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,3 +36,8 @@ description = "Profile-aware Wi-Fi state machine with Tailscale integration"
|
||||||
name = "breadpad"
|
name = "breadpad"
|
||||||
repo = "Breadway/breadpad"
|
repo = "Breadway/breadpad"
|
||||||
description = "Quick-capture scratchpad and note viewer with AI classification"
|
description = "Quick-capture scratchpad and note viewer with AI classification"
|
||||||
|
|
||||||
|
[[products]]
|
||||||
|
name = "breadpaper"
|
||||||
|
repo = "Breadway/breadpaper"
|
||||||
|
description = "Wallpaper manager for the bread desktop"
|
||||||
|
|
|
||||||
|
|
@ -77,14 +77,15 @@ build_package_json() {
|
||||||
binaries_json="$(jq -n --argjson arr "${binaries_json}" --argjson e "${entry}" '$arr + [$e]')"
|
binaries_json="$(jq -n --argjson arr "${binaries_json}" --argjson e "${entry}" '$arr + [$e]')"
|
||||||
done
|
done
|
||||||
|
|
||||||
# Locate bakery.toml: the release workflow copies it to DL_DIR alongside the
|
# Locate bakery.toml. The release workflow copies it into the version dir
|
||||||
# binaries. Fall back to a sibling repo checkout for local dev use.
|
# alongside the binaries (${version_dir}/bakery.toml). Fall back to a
|
||||||
local bakery_toml="${DL_DIR}/${name}/bakery.toml"
|
# sibling repo checkout for local dev use.
|
||||||
|
local bakery_toml="${version_dir}/bakery.toml"
|
||||||
if [[ ! -f "${bakery_toml}" ]]; then
|
if [[ ! -f "${bakery_toml}" ]]; then
|
||||||
bakery_toml="${SCRIPT_DIR}/../${name}/bakery.toml"
|
bakery_toml="${SCRIPT_DIR}/../${name}/bakery.toml"
|
||||||
fi
|
fi
|
||||||
if [[ ! -f "${bakery_toml}" ]]; then
|
if [[ ! -f "${bakery_toml}" ]]; then
|
||||||
echo "ERROR: bakery.toml not found for ${name} — release.yml must upload it to ${DL_DIR}/${name}/bakery.toml" >&2
|
echo "ERROR: bakery.toml not found for ${name} — release.yml must copy it to \${DL_DIR}/${name}/\${VERSION}/bakery.toml" >&2
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue