Replace the stale Django CI template with a CI workflow that builds and tests both codebases: cargo fmt/clippy/build/test for the hh client and pytest across Python 3.10-3.12 for the server. Apply cargo fmt and fix all clippy lints so the gates pass. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
119 lines
4.2 KiB
Rust
119 lines
4.2 KiB
Rust
//! Loadable colour/layout themes ("vestments"). Default is "crypt": occult
|
|
//! monochrome — white/grey ink on a lifted slate surface, ✝ accents. Switch to
|
|
//! the neon "church" vestments with `/theme church` or a TOML via `--theme`.
|
|
|
|
use anyhow::Context;
|
|
use ratatui::style::Color;
|
|
use serde::Deserialize;
|
|
|
|
/// Where the bundled `*.toml` vestments live, so `/theme <name>` can resolve a
|
|
/// bare name to a file at runtime (mirrors sbx.rs's script path const).
|
|
pub const THEMES_DIR: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/themes");
|
|
|
|
#[derive(Debug, Clone, Deserialize)]
|
|
#[serde(default)]
|
|
pub struct Theme {
|
|
pub name: String,
|
|
pub border: Color,
|
|
pub title: Color,
|
|
pub accent: Color,
|
|
pub dim: Color,
|
|
pub me: Color,
|
|
pub other: Color,
|
|
pub system: Color,
|
|
pub input: Color,
|
|
pub roster_me: Color,
|
|
/// Panel/background fill. `Reset` = the terminal's own background (no fill);
|
|
/// a solid colour lifts the text onto a contrasting surface for legibility.
|
|
pub bg: Color,
|
|
/// Width of the roster column.
|
|
pub roster_width: u16,
|
|
/// Glyph flanking the "hack-house" title (and used for occult accents).
|
|
/// Each theme picks its own sigil (crypt: ✝, church: ⛧, …).
|
|
pub sigil: String,
|
|
}
|
|
|
|
impl Default for Theme {
|
|
/// "crypt" — occult monochrome: white ink on a lifted slate surface. Gray
|
|
/// window-chrome, readable mid-gray timestamps, muted-gray system lines, a
|
|
/// ✝ sigil. Neutral by default; switch to the neon "church" vestments with
|
|
/// `/theme church` or `--theme`.
|
|
fn default() -> Self {
|
|
Self {
|
|
name: "crypt".into(),
|
|
border: Color::Rgb(0x6e, 0x6e, 0x7a), // gray chrome, defined against the surface
|
|
title: Color::Rgb(0xff, 0xff, 0xff), // white
|
|
accent: Color::Rgb(0xff, 0xff, 0xff), // white (sigil / prompt)
|
|
dim: Color::Rgb(0x9a, 0x9a, 0xa6), // timestamps — readable mid-gray
|
|
me: Color::Rgb(0xff, 0xff, 0xff), // your messages = white
|
|
other: Color::Rgb(0xc8, 0xc8, 0xd0), // others = bright gray
|
|
system: Color::Rgb(0xb0, 0xb0, 0xbc), // system / occult = legible muted gray
|
|
input: Color::Rgb(0xff, 0xff, 0xff),
|
|
roster_me: Color::Rgb(0xff, 0xff, 0xff), // you / owner = white
|
|
bg: Color::Rgb(0x1c, 0x1c, 0x22), // slate panel lifts text off pure-black
|
|
roster_width: 22,
|
|
sigil: "✝".into(), // inverted cross / crypt
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Theme {
|
|
pub fn load(path: &str) -> anyhow::Result<Self> {
|
|
let s = std::fs::read_to_string(path)?;
|
|
Ok(toml::from_str(&s)?)
|
|
}
|
|
|
|
/// Resolve a bare vestment name (e.g. "neon") to a bundled `themes/<name>.toml`.
|
|
/// Used by the in-session `/theme` command for live switching.
|
|
pub fn by_name(name: &str) -> anyhow::Result<Self> {
|
|
let path = format!("{THEMES_DIR}/{name}.toml");
|
|
Self::load(&path).with_context(|| format!("theme '{name}' ({path})"))
|
|
}
|
|
|
|
/// Names of the bundled vestments, sorted, for `/theme` with no argument.
|
|
pub fn available() -> Vec<String> {
|
|
let mut names: Vec<String> = std::fs::read_dir(THEMES_DIR)
|
|
.into_iter()
|
|
.flatten()
|
|
.flatten()
|
|
.filter_map(|e| {
|
|
e.file_name()
|
|
.to_str()
|
|
.and_then(|f| f.strip_suffix(".toml"))
|
|
.map(str::to_string)
|
|
})
|
|
.collect();
|
|
names.sort();
|
|
names
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn default_is_crypt() {
|
|
let t = Theme::default();
|
|
assert_eq!(t.name, "crypt");
|
|
assert_eq!(t.accent, Color::Rgb(0xff, 0xff, 0xff));
|
|
}
|
|
|
|
#[test]
|
|
fn hex_theme_toml_deserializes() {
|
|
// The --theme files use #rrggbb; make sure ratatui's serde accepts it.
|
|
let toml = r##"
|
|
name = "x"
|
|
border = "#19b3ff"
|
|
accent = "#39ff14"
|
|
me = "#39ff14"
|
|
roster_width = 24
|
|
"##;
|
|
let t: Theme = toml::from_str(toml).expect("hex theme must parse");
|
|
assert_eq!(t.border, Color::Rgb(0x19, 0xb3, 0xff));
|
|
assert_eq!(t.roster_width, 24);
|
|
// missing fields fall back to the church default
|
|
assert_eq!(t.system, Theme::default().system);
|
|
}
|
|
}
|