feat(theme): add theme randomizer and save-to-disk

Theme::random() conjures a fresh procedural vestment — a coherent HSV
palette (dark tinted surface, one bright accent, legible ink), a random
sigil, and a generated arcane name. Bound to Ctrl+Alt+P and `/theme random`.

Theme::save() persists the vestment you're wearing to themes/<slug>.toml
(via `/theme save [name]`), so a roll you like can be re-donned later with
`/theme <name>`. Theme now derives Serialize and slugify() sanitizes the
filename. Help text and the /theme usage line advertise both verbs.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
leetcrypt
2026-06-02 17:39:37 -07:00
parent 0417ef89cc
commit 00c1f1c3c9
3 changed files with 237 additions and 4 deletions
+29 -1
View File
@@ -625,6 +625,17 @@ pub async fn run(params: net::ConnParams, mut session: Session, mut theme: Theme
} else {
app.sys("kill switch is for the sandbox owner (you don't hold the PTY)");
}
} else if k.modifiers.contains(KeyModifiers::CONTROL)
&& k.modifiers.contains(KeyModifiers::ALT)
&& matches!(k.code, KeyCode::Char('p'))
{
// Conjure a brand-new procedural vestment (not a bundled
// preset): fresh palette + sigil, rolled from the clock.
theme = Theme::random();
app.sys(format!(
"{} conjured vestment '{}'",
theme.sigil, theme.name
));
} else if k.modifiers.contains(KeyModifiers::CONTROL)
&& matches!(k.code, KeyCode::Char('r'))
&& !app.connected
@@ -1007,9 +1018,26 @@ fn handle_command(
let name = rest.trim();
if name.is_empty() {
app.sys(format!(
"vestments: {} — /theme <name>",
"vestments: {} · random · save [name] — /theme <name>",
Theme::available().join(" · ")
));
} else if name == "random" {
// Same as Ctrl+Alt+P — roll a fresh procedural vestment.
*theme = Theme::random();
app.sys(format!("{} conjured vestment '{}'", theme.sigil, theme.name));
} else if name == "save" || name.starts_with("save ") {
// Persist the vestment you're currently wearing (e.g. a `random`
// roll you like) to themes/<slug>.toml so it sticks around. Bare
// `/theme save` reuses the theme's own generated name.
let want = name[4..].trim();
let want = if want.is_empty() { theme.name.clone() } else { want.to_string() };
match theme.save(&want) {
Ok(slug) => app.sys(format!(
"{} saved vestment '{slug}' — re-don it anytime with /theme {slug}",
theme.sigil
)),
Err(e) => app.err(format!("couldn't save vestment: {e}")),
}
} else {
match Theme::by_name(name) {
Ok(t) => {