Compare commits
No commits in common. "main" and "v0.2.6" have entirely different histories.
4 changed files with 47 additions and 196 deletions
|
|
@ -5,26 +5,11 @@
|
||||||
//!
|
//!
|
||||||
//! bread-theme # same as `generate`
|
//! bread-theme # same as `generate`
|
||||||
//! bread-theme generate # render + write the shared stylesheet
|
//! 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 path # print the stylesheet path
|
||||||
//! bread-theme print # render to stdout (no write)
|
//! bread-theme print # render to stdout (no write)
|
||||||
|
|
||||||
use std::process::ExitCode;
|
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 {
|
fn main() -> ExitCode {
|
||||||
let cmd = std::env::args().nth(1).unwrap_or_else(|| "generate".into());
|
let cmd = std::env::args().nth(1).unwrap_or_else(|| "generate".into());
|
||||||
match cmd.as_str() {
|
match cmd.as_str() {
|
||||||
|
|
@ -36,18 +21,21 @@ fn main() -> ExitCode {
|
||||||
print!("{}", bread_theme::render());
|
print!("{}", bread_theme::render());
|
||||||
ExitCode::SUCCESS
|
ExitCode::SUCCESS
|
||||||
}
|
}
|
||||||
"generate" => write_and_report("wrote"),
|
"generate" => match bread_theme::write_shared_css() {
|
||||||
// `reload` is `generate` from the caller's view, but it's the verb to use
|
Ok(path) => {
|
||||||
// after changing pywal colours: rewriting the file (atomic rename) trips
|
eprintln!("bread-theme: wrote {}", path.display());
|
||||||
// the file monitor in every running bread GUI, so they all re-read the
|
ExitCode::SUCCESS
|
||||||
// palette and recolour live — shared widgets *and* each app's own rules.
|
}
|
||||||
"reload" => write_and_report("reloaded"),
|
Err(e) => {
|
||||||
|
eprintln!("bread-theme: failed to write stylesheet: {e}");
|
||||||
|
ExitCode::FAILURE
|
||||||
|
}
|
||||||
|
},
|
||||||
"-h" | "--help" | "help" => {
|
"-h" | "--help" | "help" => {
|
||||||
eprintln!(
|
eprintln!(
|
||||||
"bread-theme — shared stylesheet generator\n\n\
|
"bread-theme — shared stylesheet generator\n\n\
|
||||||
USAGE:\n bread-theme [generate|reload|path|print]\n\n\
|
USAGE:\n bread-theme [generate|path|print]\n\n\
|
||||||
generate render the pywal palette to the shared stylesheet (default)\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\
|
path print the stylesheet path ({})\n\
|
||||||
print render to stdout without writing",
|
print render to stdout without writing",
|
||||||
bread_theme::shared_css_path().display()
|
bread_theme::shared_css_path().display()
|
||||||
|
|
@ -55,7 +43,7 @@ fn main() -> ExitCode {
|
||||||
ExitCode::SUCCESS
|
ExitCode::SUCCESS
|
||||||
}
|
}
|
||||||
other => {
|
other => {
|
||||||
eprintln!("bread-theme: unknown command '{other}' (try generate|reload|path|print)");
|
eprintln!("bread-theme: unknown command '{other}' (try generate|path|print)");
|
||||||
ExitCode::FAILURE
|
ExitCode::FAILURE
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,10 +7,6 @@ use std::path::Path;
|
||||||
thread_local! {
|
thread_local! {
|
||||||
static SHARED_PROVIDER: RefCell<Option<CssProvider>> = const { RefCell::new(None) };
|
static SHARED_PROVIDER: RefCell<Option<CssProvider>> = const { RefCell::new(None) };
|
||||||
static SHARED_MONITOR: RefCell<Option<gio::FileMonitor>> = 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() {
|
fn reload_shared() {
|
||||||
|
|
@ -19,65 +15,6 @@ fn reload_shared() {
|
||||||
SHARED_PROVIDER.with(|cell| apply_css(&css, cell));
|
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
|
/// Load the ecosystem's shared stylesheet (the file written by
|
||||||
/// `bread-theme generate`, or a freshly rendered fallback if absent) at
|
/// `bread-theme generate`, or a freshly rendered fallback if absent) at
|
||||||
/// APPLICATION priority, and watch the file so the whole UI recolours live when
|
/// APPLICATION priority, and watch the file so the whole UI recolours live when
|
||||||
|
|
@ -91,7 +28,11 @@ pub fn apply_shared() {
|
||||||
if cell.borrow().is_some() {
|
if cell.borrow().is_some() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
*cell.borrow_mut() = watch_theme_file(reload_shared);
|
let file = gio::File::for_path(crate::shared_css_path());
|
||||||
|
if let Ok(monitor) = file.monitor_file(gio::FileMonitorFlags::NONE, gio::Cancellable::NONE) {
|
||||||
|
monitor.connect_changed(|_, _, _, _| reload_shared());
|
||||||
|
*cell.borrow_mut() = Some(monitor);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -54,34 +54,10 @@ 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.
|
/// Canonical `@define-color` block: the single naming all bread apps share.
|
||||||
/// `surface` = color0 (darkest surface), `overlay` = color7 (muted), and
|
/// `surface` = color0 (darkest surface), `overlay` = color7 (muted), and
|
||||||
/// `accent` = color4. Apps must use these names, not raw palette slots, so the
|
/// `accent` = color4. Apps must use these names, not raw palette slots, so the
|
||||||
/// whole ecosystem recolours together.
|
/// 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 {
|
fn define_colors(p: &Palette) -> String {
|
||||||
format!(
|
format!(
|
||||||
"@define-color bg {bg};\n\
|
"@define-color bg {bg};\n\
|
||||||
|
|
@ -94,20 +70,10 @@ fn define_colors(p: &Palette) -> String {
|
||||||
@define-color yellow {c3};\n\
|
@define-color yellow {c3};\n\
|
||||||
@define-color blue {c4};\n\
|
@define-color blue {c4};\n\
|
||||||
@define-color pink {c5};\n\
|
@define-color pink {c5};\n\
|
||||||
@define-color teal {c6};\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,
|
bg = p.background, fg = p.foreground,
|
||||||
c0 = p.color0, c1 = p.color1, c2 = p.color2, c3 = p.color3,
|
c0 = p.color0, c1 = p.color1, c2 = p.color2, c3 = p.color3,
|
||||||
c4 = p.color4, c5 = p.color5, c6 = p.color6, c7 = p.color7,
|
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),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -122,53 +88,50 @@ pub fn stylesheet(p: &Palette) -> String {
|
||||||
format!(
|
format!(
|
||||||
"{vars}\
|
"{vars}\
|
||||||
* {{ font-family: '{font}'; font-size: {base}px; }}\n\
|
* {{ font-family: '{font}'; font-size: {base}px; }}\n\
|
||||||
/* Colour is set on containers; labels inherit it, so text on any panel,\
|
window {{ background-color: @bg; color: @fg; }}\n\
|
||||||
button, or accent is always the legible ink for that background. Bare\
|
label {{ color: @fg; }}\n\
|
||||||
`label {{ color }}` is deliberately avoided — as a type selector it\
|
.dim-label, .dim {{ color: @fg; opacity: 0.6; font-size: {sec}px; }}\n\
|
||||||
would override a container's colour on its own child labels. */\n\
|
.title {{ font-size: 1.4em; font-weight: bold; color: @fg; }}\n\
|
||||||
window {{ background-color: @bg; color: @on-bg; }}\n\
|
.heading {{ font-weight: bold; color: @fg; opacity: 0.85; }}\n\
|
||||||
.dim-label, .dim {{ opacity: 0.6; font-size: {sec}px; }}\n\
|
.subtitle {{ color: @fg; opacity: 0.7; font-size: {sec}px; }}\n\
|
||||||
.title {{ font-size: 1.4em; font-weight: bold; }}\n\
|
button {{ background-color: @surface; color: @fg; border: none;\
|
||||||
.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\
|
border-radius: {r1}px; padding: {sm}px {lg}px; }}\n\
|
||||||
button:hover {{ background-color: alpha(@on-surface, 0.14); }}\n\
|
button:hover {{ background-color: alpha(@fg, 0.14); }}\n\
|
||||||
button:active {{ background-color: alpha(@on-surface, 0.20); }}\n\
|
button:active {{ background-color: alpha(@fg, 0.20); }}\n\
|
||||||
button:disabled {{ opacity: 0.5; }}\n\
|
button:disabled {{ opacity: 0.5; }}\n\
|
||||||
button.flat {{ background-color: transparent; color: @on-bg; }}\n\
|
button.flat {{ background-color: transparent; }}\n\
|
||||||
button.suggested-action {{ background-color: @accent; color: @on-accent; }}\n\
|
button.suggested-action {{ background-color: @accent; color: @bg; }}\n\
|
||||||
button.suggested-action:hover {{ background-color: alpha(@accent, 0.85); }}\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 {{ background-color: @red; color: @bg; }}\n\
|
||||||
button.destructive-action:hover {{ background-color: alpha(@red, 0.85); }}\n\
|
button.destructive-action:hover {{ background-color: alpha(@red, 0.85); }}\n\
|
||||||
entry, spinbutton {{ background-color: @surface; color: @on-surface;\
|
entry, spinbutton {{ background-color: @surface; color: @fg;\
|
||||||
border: 1px solid @overlay; border-radius: {r2}px;\
|
border: 1px solid @overlay; border-radius: {r2}px;\
|
||||||
padding: {xs}px {sm}px; caret-color: @on-surface; }}\n\
|
padding: {xs}px {sm}px; caret-color: @fg; }}\n\
|
||||||
entry:focus-within, spinbutton:focus-within {{ border-color: @accent; outline: none; }}\n\
|
entry:focus-within, spinbutton:focus-within {{ border-color: @accent; outline: none; }}\n\
|
||||||
entry image, spinbutton button {{ color: @on-surface; }}\n\
|
entry image, spinbutton button {{ color: @fg; }}\n\
|
||||||
dropdown > button {{ background-color: @surface; color: @on-surface; border-radius: {r2}px; }}\n\
|
dropdown > button {{ background-color: @surface; border-radius: {r2}px; }}\n\
|
||||||
popover > contents {{ background-color: @surface; color: @on-surface; border-radius: {r1}px; }}\n\
|
popover > contents {{ background-color: @surface; color: @fg; border-radius: {r1}px; }}\n\
|
||||||
switch {{ background-color: @overlay; border-radius: {pill}px; }}\n\
|
switch {{ background-color: @overlay; border-radius: {pill}px; }}\n\
|
||||||
switch:checked {{ background-color: @accent; }}\n\
|
switch:checked {{ background-color: @accent; }}\n\
|
||||||
switch slider {{ background-color: @on-surface; border-radius: {pill}px; }}\n\
|
switch slider {{ background-color: @fg; border-radius: {pill}px; }}\n\
|
||||||
list, listbox {{ background-color: transparent; }}\n\
|
list, listbox {{ background-color: transparent; }}\n\
|
||||||
row {{ border-radius: {r2}px; }}\n\
|
row {{ border-radius: {r2}px; }}\n\
|
||||||
row:selected, list row:selected {{ background-color: @accent; color: @on-accent; }}\n\
|
row:selected, list row:selected {{ background-color: @accent; color: @bg; }}\n\
|
||||||
.sidebar {{ background-color: @surface; color: @on-surface; }}\n\
|
.sidebar {{ background-color: @surface; }}\n\
|
||||||
.sidebar row {{ padding: {sm}px {md}px; }}\n\
|
.sidebar row {{ padding: {sm}px {md}px; color: @fg; }}\n\
|
||||||
.sidebar row:selected {{ background-color: @accent; color: @on-accent; }}\n\
|
.sidebar row:selected {{ background-color: @accent; color: @bg; }}\n\
|
||||||
.sidebar .section-header {{ padding: {md}px {md}px {xs}px {md}px;\
|
.sidebar .section-header {{ padding: {md}px {md}px {xs}px {md}px;\
|
||||||
font-size: {sec}px; font-weight: bold; opacity: 0.55; }}\n\
|
font-size: {sec}px; font-weight: bold; color: @fg; opacity: 0.55; }}\n\
|
||||||
.card {{ background-color: @surface; color: @on-surface; border-radius: {r1}px; padding: {md}px; }}\n\
|
.card {{ background-color: @surface; border-radius: {r1}px; padding: {md}px; }}\n\
|
||||||
.chip, .pill {{ background-color: @overlay; color: @on-overlay; border-radius: {pill}px;\
|
.chip, .pill {{ background-color: @overlay; color: @fg; border-radius: {pill}px;\
|
||||||
padding: {xs}px {md}px; font-size: {sec}px; }}\n\
|
padding: {xs}px {md}px; font-size: {sec}px; }}\n\
|
||||||
.chip.active, .pill.active {{ background-color: @accent; color: @on-accent; }}\n\
|
.chip.active, .pill.active {{ background-color: @accent; color: @bg; }}\n\
|
||||||
scrollbar {{ background-color: transparent; }}\n\
|
scrollbar {{ background-color: transparent; }}\n\
|
||||||
scrollbar slider {{ background-color: alpha(@on-bg, 0.25); border-radius: {pill}px;\
|
scrollbar slider {{ background-color: alpha(@fg, 0.25); border-radius: {pill}px;\
|
||||||
min-width: 6px; min-height: 6px; }}\n\
|
min-width: 6px; min-height: 6px; }}\n\
|
||||||
scrollbar slider:hover {{ background-color: alpha(@on-bg, 0.45); }}\n\
|
scrollbar slider:hover {{ background-color: alpha(@fg, 0.45); }}\n\
|
||||||
textview, .mono {{ font-family: monospace; }}\n\
|
textview, .mono {{ font-family: monospace; }}\n\
|
||||||
textview text {{ background-color: @surface; color: @on-surface; }}\n",
|
textview text {{ background-color: @surface; color: @fg; }}\n",
|
||||||
vars = define_colors(p),
|
vars = define_colors(p),
|
||||||
font = FONT_FAMILY,
|
font = FONT_FAMILY,
|
||||||
base = FONT_SIZE_BASE,
|
base = FONT_SIZE_BASE,
|
||||||
|
|
@ -254,42 +217,6 @@ mod tests {
|
||||||
assert!(css.contains("Varela Round"));
|
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]
|
#[test]
|
||||||
fn shared_css_path_uses_runtime_dir() {
|
fn shared_css_path_uses_runtime_dir() {
|
||||||
std::env::set_var("XDG_RUNTIME_DIR", "/run/user/1234");
|
std::env::set_var("XDG_RUNTIME_DIR", "/run/user/1234");
|
||||||
|
|
|
||||||
|
|
@ -36,8 +36,3 @@ 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"
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue