Refactor theme onto bread-theme; add bakery.toml and release workflow

- breadpad-shared/Cargo.toml: depend on bread-theme (no gtk feature needed
  in the shared crate)
- breadpad-shared/src/theme.rs: re-export Palette and load_palette from
  bread-theme; retain all breadpad-specific CSS in build_css()
- bakery.toml: describes breadpad for bakery install
- release.yml: builds on hestia self-hosted runner, publishes binaries to
  dl.breadway.dev and GitHub Releases on v* tags

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Breadway 2026-06-06 22:31:38 +08:00
parent 347508828f
commit a2281607bb
5 changed files with 92 additions and 214 deletions

61
.github/workflows/release.yml vendored Normal file
View file

@ -0,0 +1,61 @@
name: release
on:
push:
tags: ["v*"]
env:
DL_DIR: /srv/breadway-dl
ECOSYSTEM_DIR: /home/breadway/Projects/bread-ecosystem
jobs:
build:
runs-on: [self-hosted, hestia]
steps:
- uses: actions/checkout@v4
- name: install system deps
run: sudo pacman -S --noconfirm gtk4 gtk4-layer-shell 2>/dev/null || true
- name: build
run: cargo build --release --locked
- name: prepare artifacts
run: |
VERSION="${GITHUB_REF_NAME#v}"
PKG_DIR="${DL_DIR}/breadpad/${VERSION}"
mkdir -p "${PKG_DIR}"
for bin in breadpad breadman; do
cp "target/release/${bin}" "${PKG_DIR}/${bin}-x86_64"
strip "${PKG_DIR}/${bin}-x86_64"
sha256sum "${PKG_DIR}/${bin}-x86_64" | awk '{print $1}' \
> "${PKG_DIR}/${bin}-x86_64.sha256"
done
cp breadpad.example.toml "${PKG_DIR}/"
cp bakery.toml "${PKG_DIR}/bakery.toml"
ln -sfn "${PKG_DIR}" "${DL_DIR}/breadpad/latest"
- name: ensure bread-ecosystem
run: |
if [[ -d "${ECOSYSTEM_DIR}/.git" ]]; then
git -C "${ECOSYSTEM_DIR}" pull --ff-only
else
mkdir -p "$(dirname "${ECOSYSTEM_DIR}")"
git clone https://github.com/Breadway/bread-ecosystem.git "${ECOSYSTEM_DIR}"
fi
- name: regenerate index.json
run: bash "${ECOSYSTEM_DIR}/scripts/gen-index.sh"
- name: upload to GitHub Release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
VERSION="${GITHUB_REF_NAME#v}"
PKG_DIR="${DL_DIR}/breadpad/${VERSION}"
gh release upload "${GITHUB_REF_NAME}" \
"${PKG_DIR}/breadpad-x86_64" \
"${PKG_DIR}/breadman-x86_64" \
"${PKG_DIR}/breadpad-x86_64.sha256" \
"${PKG_DIR}/breadman-x86_64.sha256" \
--clobber

10
Cargo.lock generated
View file

@ -302,6 +302,15 @@ dependencies = [
"piper", "piper",
] ]
[[package]]
name = "bread-theme"
version = "0.1.0"
dependencies = [
"dirs 5.0.1",
"serde",
"serde_json",
]
[[package]] [[package]]
name = "breadman" name = "breadman"
version = "0.1.0" version = "0.1.0"
@ -343,6 +352,7 @@ name = "breadpad-shared"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bread-theme",
"chrono", "chrono",
"dirs 5.0.1", "dirs 5.0.1",
"ical", "ical",

14
bakery.toml Normal file
View file

@ -0,0 +1,14 @@
name = "breadpad"
description = "Quick-capture scratchpad and note viewer with AI classification"
binaries = ["breadpad", "breadman"]
system_deps = ["gtk4", "gtk4-layer-shell"]
bread_deps = []
[config]
dir = "~/.config/breadpad"
example = "breadpad.example.toml"
[install]
post_install = [
"mkdir -p ~/.local/share/breadpad/model",
]

View file

@ -7,6 +7,9 @@ authors.workspace = true
[dependencies] [dependencies]
# Path dep for local dev; replace with git dep on first tag:
# bread-theme = { git = "https://github.com/Breadway/bread-ecosystem", tag = "theme-v0.1.0" }
bread-theme = { path = "../../bread-ecosystem/bread-theme" }
anyhow.workspace = true anyhow.workspace = true
tracing.workspace = true tracing.workspace = true
serde.workspace = true serde.workspace = true

View file

@ -1,83 +1,7 @@
use serde::Deserialize; pub use bread_theme::{load_palette, Palette};
use std::collections::HashMap;
use std::path::PathBuf;
#[derive(Debug, Clone)]
pub struct Palette {
pub background: String,
pub foreground: String,
pub color0: String,
pub color1: String,
pub color2: String,
pub color3: String,
pub color4: String,
pub color5: String,
pub color6: String,
pub color7: String,
}
// Catppuccin Mocha fallback
impl Default for Palette {
fn default() -> Self {
Palette {
background: "#1e1e2e".into(),
foreground: "#cdd6f4".into(),
color0: "#45475a".into(),
color1: "#f38ba8".into(),
color2: "#a6e3a1".into(),
color3: "#f9e2af".into(),
color4: "#89b4fa".into(),
color5: "#f5c2e7".into(),
color6: "#94e2d5".into(),
color7: "#bac2de".into(),
}
}
}
#[derive(Debug, Deserialize)]
struct WalColors {
#[serde(default)]
colors: HashMap<String, String>,
special: Option<WalSpecial>,
}
#[derive(Debug, Deserialize)]
struct WalSpecial {
background: Option<String>,
foreground: Option<String>,
}
pub(crate) fn palette_from_wal_json(json: &str) -> Option<Palette> {
let wal: WalColors = serde_json::from_str(json).ok()?;
Some(Palette {
background: wal.special.as_ref().and_then(|s| s.background.clone()).unwrap_or_else(|| "#1e1e2e".into()),
foreground: wal.special.as_ref().and_then(|s| s.foreground.clone()).unwrap_or_else(|| "#cdd6f4".into()),
color0: wal.colors.get("color0").cloned().unwrap_or_else(|| "#45475a".into()),
color1: wal.colors.get("color1").cloned().unwrap_or_else(|| "#f38ba8".into()),
color2: wal.colors.get("color2").cloned().unwrap_or_else(|| "#a6e3a1".into()),
color3: wal.colors.get("color3").cloned().unwrap_or_else(|| "#f9e2af".into()),
color4: wal.colors.get("color4").cloned().unwrap_or_else(|| "#89b4fa".into()),
color5: wal.colors.get("color5").cloned().unwrap_or_else(|| "#f5c2e7".into()),
color6: wal.colors.get("color6").cloned().unwrap_or_else(|| "#94e2d5".into()),
color7: wal.colors.get("color7").cloned().unwrap_or_else(|| "#bac2de".into()),
})
}
pub fn load_palette() -> Palette {
let wal_path = wal_colors_path();
if !wal_path.exists() {
return Palette::default();
}
match std::fs::read_to_string(&wal_path)
.ok()
.and_then(|s| palette_from_wal_json(&s))
{
Some(wal) => wal,
None => Palette::default(),
}
}
/// Generate the full breadpad CSS string. The base colour variables come from
/// `bread-theme`; the widget rules below are breadpad-specific.
pub fn build_css(palette: &Palette, user_css: Option<&str>) -> String { pub fn build_css(palette: &Palette, user_css: Option<&str>) -> String {
let mut css = format!( let mut css = format!(
r#" r#"
@ -317,125 +241,21 @@ entry:focus {
css css
} }
fn wal_colors_path() -> PathBuf {
dirs::cache_dir()
.unwrap_or_else(|| PathBuf::from("~/.cache"))
.join("wal")
.join("colors.json")
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
const TOKYO_NIGHT_WAL: &str = r##"{
"special": {
"background": "#1a1b26",
"foreground": "#c0caf5"
},
"colors": {
"color0": "#15161e",
"color1": "#f7768e",
"color2": "#9ece6a",
"color3": "#e0af68",
"color4": "#7aa2f7",
"color5": "#bb9af7",
"color6": "#7dcfff",
"color7": "#a9b1d6"
}
}"##;
// ---- Default palette (Catppuccin Mocha) ----
#[test]
fn default_background_is_catppuccin_mocha() {
assert_eq!(Palette::default().background, "#1e1e2e");
}
#[test]
fn default_foreground_is_catppuccin_mocha() {
assert_eq!(Palette::default().foreground, "#cdd6f4");
}
#[test]
fn default_red_is_catppuccin_mocha() {
assert_eq!(Palette::default().color1, "#f38ba8");
}
#[test]
fn default_blue_is_catppuccin_mocha() {
assert_eq!(Palette::default().color4, "#89b4fa");
}
#[test]
fn default_teal_is_catppuccin_mocha() {
assert_eq!(Palette::default().color6, "#94e2d5");
}
// ---- palette_from_wal_json ----
#[test]
fn wal_json_parses_special_background() {
let p = palette_from_wal_json(TOKYO_NIGHT_WAL).unwrap();
assert_eq!(p.background, "#1a1b26");
}
#[test]
fn wal_json_parses_special_foreground() {
let p = palette_from_wal_json(TOKYO_NIGHT_WAL).unwrap();
assert_eq!(p.foreground, "#c0caf5");
}
#[test]
fn wal_json_parses_numbered_colors() {
let p = palette_from_wal_json(TOKYO_NIGHT_WAL).unwrap();
assert_eq!(p.color0, "#15161e");
assert_eq!(p.color1, "#f7768e");
assert_eq!(p.color4, "#7aa2f7");
assert_eq!(p.color7, "#a9b1d6");
}
#[test]
fn wal_json_missing_special_falls_back_to_defaults() {
let json = r##"{"colors":{"color0":"#000000"}}"##;
let p = palette_from_wal_json(json).unwrap();
assert_eq!(p.background, "#1e1e2e");
assert_eq!(p.foreground, "#cdd6f4");
}
#[test]
fn wal_json_missing_color_falls_back_to_default() {
let json = r##"{"special":{"background":"#ff0000","foreground":"#ffffff"},"colors":{}}"##;
let p = palette_from_wal_json(json).unwrap();
assert_eq!(p.color4, "#89b4fa"); // default blue
}
#[test]
fn invalid_wal_json_returns_none() {
assert!(palette_from_wal_json("not json").is_none());
assert!(palette_from_wal_json("").is_none());
assert!(palette_from_wal_json("{}").is_some()); // empty but valid → all defaults
}
// ---- build_css ----
#[test] #[test]
fn css_defines_bg_color() { fn css_defines_bg_color() {
let css = build_css(&Palette::default(), None); let css = build_css(&Palette::default(), None);
assert!(css.contains("@define-color bg #1e1e2e"), "css missing bg: {}", &css[..300]); assert!(css.contains("@define-color bg #1e1e2e"), "css missing bg: {}", &css[..300]);
} }
#[test]
fn css_defines_fg_color() {
let css = build_css(&Palette::default(), None);
assert!(css.contains("@define-color fg #cdd6f4"));
}
#[test] #[test]
fn css_defines_all_named_colors() { fn css_defines_all_named_colors() {
let css = build_css(&Palette::default(), None); let css = build_css(&Palette::default(), None);
for name in &["red", "green", "yellow", "blue", "pink", "teal", "overlay"] { for name in &["red", "green", "yellow", "blue", "pink", "teal", "overlay"] {
assert!(css.contains(&format!("@define-color {} ", name)), "missing @define-color {}", name); assert!(css.contains(&format!("@define-color {name} ")), "missing @define-color {name}");
} }
} }
@ -458,18 +278,6 @@ mod tests {
assert!(css.contains(".note-card {")); assert!(css.contains(".note-card {"));
} }
#[test]
fn css_contains_type_chip_class() {
let css = build_css(&Palette::default(), None);
assert!(css.contains(".type-chip {"));
}
#[test]
fn css_contains_sidebar_row_class() {
let css = build_css(&Palette::default(), None);
assert!(css.contains(".sidebar-row {"));
}
#[test] #[test]
fn css_appends_user_css() { fn css_appends_user_css() {
let user = ".my-override { color: hotpink; }"; let user = ".my-override { color: hotpink; }";
@ -492,22 +300,4 @@ mod tests {
assert!(css.contains("@define-color bg #deadbe"), "css: {}", &css[..300]); assert!(css.contains("@define-color bg #deadbe"), "css: {}", &css[..300]);
assert!(css.contains("@define-color blue #cafe00"), "css: {}", &css[..300]); assert!(css.contains("@define-color blue #cafe00"), "css: {}", &css[..300]);
} }
#[test]
fn css_from_wal_palette_uses_wal_colors() {
let p = palette_from_wal_json(TOKYO_NIGHT_WAL).unwrap();
let css = build_css(&p, None);
assert!(css.contains("@define-color bg #1a1b26"), "css: {}", &css[..300]);
assert!(css.contains("@define-color fg #c0caf5"));
}
#[test]
fn load_palette_returns_valid_palette() {
// No wal file in CI/test env; should return non-empty strings starting with #
let palette = load_palette();
assert!(!palette.background.is_empty());
assert!(palette.background.starts_with('#'), "bg: {}", palette.background);
assert!(!palette.foreground.is_empty());
assert!(palette.color4.starts_with('#'));
}
} }