feat(lua): owlry.theme(...) + owlry.profiles {} (Phase 3.5)
Implements the theme selection and named-profile surface per
docs/lua-api.md §4.5 and §6.5:
owlry.theme("catppuccin-mocha") -- string form: theme name
owlry.theme { background = "#1e1e2e", -- table form: colour overrides
accent = "#cba6f7",
badge_app = "#a6e3a1" }
owlry.profiles { dev = { "app", "cmd", "ssh" }, media = {...} }
LuaConfig (lua/config.rs):
- theme_name: Option<String> (set by string form, last-write-wins)
- theme_colors: ThemeColors (table form, per-key last-write-wins)
- unknown_theme_keys: Vec<String> (forward-compat for 2.2+ palette)
- profiles: Option<HashMap<String, Vec<String>>> (replace-on-call)
- New KNOWN_THEME_KEYS const lists every recognised colour name.
- merge_into() applies theme name + colour overlay (Some-only fields)
and replaces base profiles when Lua sets them. Omitting `owlry.profiles`
leaves existing TOML/default profiles intact.
api.rs:
- owlry.theme dispatches on mlua::Value: Value::String → set name,
Value::Table → read colour keys. Other types raise a clean Lua
runtime error.
- Pre-v2 `badge_sys` alias normalises to `badge_power` (mirrors the
serde alias on ThemeColors).
- owlry.profiles: every value must be a table of strings; non-table
values produce a clear "list of provider ids" error message.
Tests: 19 new
- lua::config (6): theme name override, colours overlay only Some
fields, name+colours compose, profiles replace base, empty profiles
clears, omitted profiles preserves base.
- lua::runtime (13): string form, full colour table, name + table
compose, per-key merge across calls, badge_sys alias, unknown
forward-compat keys, wrong-arg-type error, multi-profile capture,
profiles replace, non-list value error, numeric coercion docs.
Smoke test (release build, isolated XDG): `owlry config show` reports
`theme = "nord"`, the four colour overrides under [appearance.colors],
and all three named profiles. 300/300 lib tests with --features full,
no new clippy warnings.
This commit is contained in:
+142
-4
@@ -5,18 +5,24 @@
|
||||
//! - `owlry.providers(list)` — enabled provider list (replaces on call)
|
||||
//! - `owlry.tabs(list)` — tab order list (replaces on call)
|
||||
//!
|
||||
//! Phase 3.3 adds:
|
||||
//! Phase 3.3 added:
|
||||
//! - `owlry.provider(table)` — register a user-defined provider
|
||||
//!
|
||||
//! Phase 3.5 adds:
|
||||
//! - `owlry.theme(name_or_table)` — theme name and/or inline colour overrides
|
||||
//! - `owlry.profiles(table)` — named provider-id sets for `--profile`
|
||||
//!
|
||||
//! Each closure shares an `Arc<Mutex<LuaConfig>>` with [`super::runtime`].
|
||||
//! Later sub-phases add `owlry.theme` (3.5) and `owlry.util.*` (3.6).
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use log::warn;
|
||||
use mlua::{Function, Lua, Table};
|
||||
use mlua::{Function, Lua, Table, Value};
|
||||
|
||||
use super::config::{KNOWN_SET_KEYS, LuaConfig, LuaProviderSpec, is_valid_provider_id};
|
||||
use super::config::{
|
||||
KNOWN_SET_KEYS, KNOWN_THEME_KEYS, LuaConfig, LuaProviderSpec, is_valid_provider_id,
|
||||
};
|
||||
use super::error::LuaConfigError;
|
||||
|
||||
/// Build the `owlry` table, register all functions on it, install it as a
|
||||
@@ -28,6 +34,8 @@ pub(crate) fn install(lua: &Lua, state: Arc<Mutex<LuaConfig>>) -> Result<(), Lua
|
||||
register_providers(lua, &owlry, Arc::clone(&state))?;
|
||||
register_tabs(lua, &owlry, Arc::clone(&state))?;
|
||||
register_provider(lua, &owlry, Arc::clone(&state))?;
|
||||
register_theme(lua, &owlry, Arc::clone(&state))?;
|
||||
register_profiles(lua, &owlry, Arc::clone(&state))?;
|
||||
|
||||
lua.globals().set("owlry", owlry)?;
|
||||
|
||||
@@ -231,3 +239,133 @@ fn apply_provider(cfg: &mut LuaConfig, t: Table) -> mlua::Result<()> {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn register_theme(
|
||||
lua: &Lua,
|
||||
owlry: &Table,
|
||||
state: Arc<Mutex<LuaConfig>>,
|
||||
) -> Result<(), LuaConfigError> {
|
||||
let f = lua.create_function(move |_, v: Value| {
|
||||
let mut cfg = state.lock().expect("lua config state mutex poisoned");
|
||||
apply_theme(&mut cfg, v)
|
||||
})?;
|
||||
owlry.set("theme", f)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn register_profiles(
|
||||
lua: &Lua,
|
||||
owlry: &Table,
|
||||
state: Arc<Mutex<LuaConfig>>,
|
||||
) -> Result<(), LuaConfigError> {
|
||||
let f = lua.create_function(move |_, t: Table| {
|
||||
let mut cfg = state.lock().expect("lua config state mutex poisoned");
|
||||
apply_profiles(&mut cfg, t)
|
||||
})?;
|
||||
owlry.set("profiles", f)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// `owlry.theme(...)` — accepts either a string (theme name) or a table
|
||||
/// (inline colour overrides). Both forms can be combined across calls per
|
||||
/// §4.5: name set by string-form, table-form layered on top.
|
||||
fn apply_theme(cfg: &mut LuaConfig, value: Value) -> mlua::Result<()> {
|
||||
match value {
|
||||
Value::String(s) => {
|
||||
cfg.theme_name = Some(s.to_str()?.to_string());
|
||||
Ok(())
|
||||
}
|
||||
Value::Table(t) => apply_theme_table(cfg, t),
|
||||
other => Err(mlua::Error::RuntimeError(format!(
|
||||
"owlry.theme: expected a string (theme name) or table (colour overrides), got {}",
|
||||
other.type_name()
|
||||
))),
|
||||
}
|
||||
}
|
||||
|
||||
/// Read recognised colour keys from a theme table and overlay them on the
|
||||
/// accumulated [`super::config::LuaConfig::theme_colors`]. Unknown keys are
|
||||
/// recorded (forward-compat) — no error, mirroring `owlry.set` policy.
|
||||
fn apply_theme_table(cfg: &mut LuaConfig, t: Table) -> mlua::Result<()> {
|
||||
macro_rules! read_colour {
|
||||
($field:ident) => {
|
||||
if let Some(v) = t.get::<Option<String>>(stringify!($field))? {
|
||||
cfg.theme_colors.$field = Some(v);
|
||||
}
|
||||
};
|
||||
}
|
||||
read_colour!(background);
|
||||
read_colour!(background_secondary);
|
||||
read_colour!(border);
|
||||
read_colour!(text);
|
||||
read_colour!(text_secondary);
|
||||
read_colour!(accent);
|
||||
read_colour!(accent_bright);
|
||||
read_colour!(badge_app);
|
||||
read_colour!(badge_bookmark);
|
||||
read_colour!(badge_calc);
|
||||
read_colour!(badge_clip);
|
||||
read_colour!(badge_cmd);
|
||||
read_colour!(badge_dmenu);
|
||||
read_colour!(badge_emoji);
|
||||
read_colour!(badge_file);
|
||||
read_colour!(badge_script);
|
||||
read_colour!(badge_ssh);
|
||||
read_colour!(badge_power);
|
||||
read_colour!(badge_uuctl);
|
||||
read_colour!(badge_web);
|
||||
read_colour!(badge_media);
|
||||
read_colour!(badge_weather);
|
||||
read_colour!(badge_pomo);
|
||||
|
||||
// Pre-v2 alias: badge_sys → badge_power (matches the serde alias on
|
||||
// ThemeColors in config/mod.rs). Only applies when the canonical key
|
||||
// wasn't already set in this call.
|
||||
if cfg.theme_colors.badge_power.is_none()
|
||||
&& let Some(v) = t.get::<Option<String>>("badge_sys")?
|
||||
{
|
||||
cfg.theme_colors.badge_power = Some(v);
|
||||
}
|
||||
|
||||
// Track unknown theme keys for `owlry config validate`.
|
||||
for pair in t.pairs::<Value, Value>() {
|
||||
let (k, _) = pair?;
|
||||
if let Value::String(s) = k {
|
||||
let key = s.to_str()?.to_string();
|
||||
if !KNOWN_THEME_KEYS.contains(&key.as_str())
|
||||
&& !cfg.unknown_theme_keys.contains(&key)
|
||||
{
|
||||
cfg.unknown_theme_keys.push(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// `owlry.profiles { dev = { "app", "cmd" }, ... }` — captures named
|
||||
/// provider-id sets used by `--profile <name>`. Replaces the entire map
|
||||
/// on each call (consistent with providers/tabs replace-on-call semantics).
|
||||
fn apply_profiles(cfg: &mut LuaConfig, t: Table) -> mlua::Result<()> {
|
||||
let mut profiles: HashMap<String, Vec<String>> = HashMap::new();
|
||||
for pair in t.pairs::<String, Table>() {
|
||||
let (name, modes_tbl) = pair.map_err(|e| {
|
||||
mlua::Error::RuntimeError(format!(
|
||||
"owlry.profiles: every value must be a list of provider ids — {}",
|
||||
e
|
||||
))
|
||||
})?;
|
||||
let modes: Vec<String> = modes_tbl
|
||||
.sequence_values::<String>()
|
||||
.collect::<mlua::Result<Vec<_>>>()
|
||||
.map_err(|e| {
|
||||
mlua::Error::RuntimeError(format!(
|
||||
"owlry.profiles.{}: list entries must be strings — {}",
|
||||
name, e
|
||||
))
|
||||
})?;
|
||||
profiles.insert(name, modes);
|
||||
}
|
||||
cfg.profiles = Some(profiles);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
//! Data layer for the Lua config: the [`LuaConfig`] struct that accumulates
|
||||
//! state from `owlry.set` / `owlry.providers` / `owlry.tabs` calls, plus the
|
||||
//! merge that overlays it onto the existing TOML-derived [`crate::config::Config`].
|
||||
//! state from `owlry.set` / `owlry.providers` / `owlry.tabs` / `owlry.theme`
|
||||
//! / `owlry.profiles` / `owlry.provider` calls, plus the merge that overlays
|
||||
//! it onto the existing TOML-derived [`crate::config::Config`].
|
||||
//!
|
||||
//! Mapping mirrors `docs/lua-api.md` §4.1–§4.3. Every settable key is an
|
||||
//! `Option<T>`: `None` means "the user didn't touch it, keep the base value."
|
||||
//! Mapping mirrors `docs/lua-api.md` §4.1–§4.5 and §6.5. Every settable
|
||||
//! key is an `Option<T>`: `None` means "the user didn't touch it, keep the
|
||||
//! base value."
|
||||
|
||||
use crate::config::Config;
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::config::{Config, ProfileConfig, ThemeColors};
|
||||
|
||||
/// Accumulated state from `owlry.lua`. Built up by the closures registered in
|
||||
/// [`super::api::install`] and drained via [`super::runtime::LuaContext::snapshot`].
|
||||
@@ -41,6 +45,29 @@ pub struct LuaConfig {
|
||||
/// registration. Duplicates by `id` are resolved at registration time:
|
||||
/// later wins, earlier is dropped (warning logged).
|
||||
pub user_providers: Vec<LuaProviderSpec>,
|
||||
|
||||
// ─── owlry.theme(...) ─────────────────────────────────────────────────
|
||||
/// Theme name from the string form `owlry.theme("nord")`. Last write wins
|
||||
/// across multiple calls (string forms merge with table forms — see
|
||||
/// §4.5: "call owlry.theme(name) first, then owlry.theme { ... } to
|
||||
/// layer overrides on top").
|
||||
pub theme_name: Option<String>,
|
||||
|
||||
/// Inline colour overrides from the table form `owlry.theme { ... }`.
|
||||
/// Per-key last-write-wins. Empty by default (no overrides).
|
||||
pub theme_colors: ThemeColors,
|
||||
|
||||
/// Theme keys we don't recognise (forward-compat for 2.2+ palette
|
||||
/// additions). Surfaced by `owlry config validate` (Phase 3.9).
|
||||
pub unknown_theme_keys: Vec<String>,
|
||||
|
||||
// ─── owlry.profiles { ... } ───────────────────────────────────────────
|
||||
/// Named profiles from `owlry.profiles { dev = { ... }, ... }`. Each
|
||||
/// profile maps to a list of provider ids used when `--profile <name>`
|
||||
/// is passed. `None` = profiles were never set in Lua → keep TOML or
|
||||
/// defaults. `Some(map)` replaces the base profiles (consistent with
|
||||
/// `providers`/`tabs` replace-on-call semantics).
|
||||
pub profiles: Option<HashMap<String, Vec<String>>>,
|
||||
}
|
||||
|
||||
/// Captured spec from a single `owlry.provider { ... }` call.
|
||||
@@ -104,6 +131,36 @@ pub(crate) const KNOWN_SET_KEYS: &[&str] = &[
|
||||
"search_engine",
|
||||
];
|
||||
|
||||
/// Known keys accepted by `owlry.theme { ... }`. Mirrors the fields of
|
||||
/// [`ThemeColors`]; the pre-v2 `badge_sys` alias is included so existing
|
||||
/// configs translate cleanly (it maps to `badge_power` per the v2 rename).
|
||||
pub(crate) const KNOWN_THEME_KEYS: &[&str] = &[
|
||||
"background",
|
||||
"background_secondary",
|
||||
"border",
|
||||
"text",
|
||||
"text_secondary",
|
||||
"accent",
|
||||
"accent_bright",
|
||||
"badge_app",
|
||||
"badge_bookmark",
|
||||
"badge_calc",
|
||||
"badge_clip",
|
||||
"badge_cmd",
|
||||
"badge_dmenu",
|
||||
"badge_emoji",
|
||||
"badge_file",
|
||||
"badge_script",
|
||||
"badge_ssh",
|
||||
"badge_power",
|
||||
"badge_sys", // pre-v2 alias, normalised to badge_power at merge time
|
||||
"badge_uuctl",
|
||||
"badge_web",
|
||||
"badge_media",
|
||||
"badge_weather",
|
||||
"badge_pomo",
|
||||
];
|
||||
|
||||
impl LuaConfig {
|
||||
/// Apply the Lua-side overrides on top of an existing [`Config`].
|
||||
///
|
||||
@@ -158,9 +215,65 @@ impl LuaConfig {
|
||||
if let Some(list) = &self.tabs {
|
||||
cfg.general.tabs = list.clone();
|
||||
}
|
||||
|
||||
// ── owlry.theme(...) ───────────────────────────────────────────
|
||||
if let Some(name) = &self.theme_name {
|
||||
cfg.appearance.theme = Some(name.clone());
|
||||
}
|
||||
merge_theme_colors(&self.theme_colors, &mut cfg.appearance.colors);
|
||||
|
||||
// ── owlry.profiles { ... } ─────────────────────────────────────
|
||||
// Replace base profiles entirely (consistent with providers/tabs
|
||||
// replace-on-call). Profile values become `ProfileConfig { modes }`.
|
||||
if let Some(profiles) = &self.profiles {
|
||||
cfg.profiles.clear();
|
||||
for (name, modes) in profiles {
|
||||
cfg.profiles.insert(
|
||||
name.clone(),
|
||||
ProfileConfig {
|
||||
modes: modes.clone(),
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Overlay any `Some` colour fields from the Lua side onto the base
|
||||
/// [`ThemeColors`]. `None` fields are skipped so the base theme survives.
|
||||
fn merge_theme_colors(src: &ThemeColors, dst: &mut ThemeColors) {
|
||||
macro_rules! merge {
|
||||
($field:ident) => {
|
||||
if let Some(ref v) = src.$field {
|
||||
dst.$field = Some(v.clone());
|
||||
}
|
||||
};
|
||||
}
|
||||
merge!(background);
|
||||
merge!(background_secondary);
|
||||
merge!(border);
|
||||
merge!(text);
|
||||
merge!(text_secondary);
|
||||
merge!(accent);
|
||||
merge!(accent_bright);
|
||||
merge!(badge_app);
|
||||
merge!(badge_bookmark);
|
||||
merge!(badge_calc);
|
||||
merge!(badge_clip);
|
||||
merge!(badge_cmd);
|
||||
merge!(badge_dmenu);
|
||||
merge!(badge_emoji);
|
||||
merge!(badge_file);
|
||||
merge!(badge_script);
|
||||
merge!(badge_ssh);
|
||||
merge!(badge_power);
|
||||
merge!(badge_uuctl);
|
||||
merge!(badge_web);
|
||||
merge!(badge_media);
|
||||
merge!(badge_weather);
|
||||
merge!(badge_pomo);
|
||||
}
|
||||
|
||||
/// Translate a list of provider IDs (as the user wrote them in
|
||||
/// `owlry.providers { ... }`) into the per-provider boolean flags on
|
||||
/// [`crate::config::ProvidersConfig`]. Ids not present in the list are
|
||||
@@ -393,4 +506,124 @@ mod tests {
|
||||
assert!(!is_valid_provider_id("colon:bad"));
|
||||
assert!(!is_valid_provider_id("emoji_💀"));
|
||||
}
|
||||
|
||||
// ── owlry.theme(...) merge semantics ────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn theme_name_overrides_base_appearance_theme() {
|
||||
let mut cfg = Config::default();
|
||||
let lc = LuaConfig {
|
||||
theme_name: Some("catppuccin-mocha".into()),
|
||||
..Default::default()
|
||||
};
|
||||
lc.merge_into(&mut cfg);
|
||||
assert_eq!(
|
||||
cfg.appearance.theme.as_deref(),
|
||||
Some("catppuccin-mocha")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn theme_colours_overlay_only_set_fields() {
|
||||
let mut cfg = Config::default();
|
||||
// Base theme has colours already set:
|
||||
cfg.appearance.colors.background = Some("#000000".into());
|
||||
cfg.appearance.colors.text = Some("#ffffff".into());
|
||||
|
||||
// Lua overlays only the accent — background and text must survive.
|
||||
let mut lua_colors = ThemeColors::default();
|
||||
lua_colors.accent = Some("#ff00ff".into());
|
||||
let lc = LuaConfig {
|
||||
theme_colors: lua_colors,
|
||||
..Default::default()
|
||||
};
|
||||
lc.merge_into(&mut cfg);
|
||||
|
||||
assert_eq!(cfg.appearance.colors.background.as_deref(), Some("#000000"));
|
||||
assert_eq!(cfg.appearance.colors.text.as_deref(), Some("#ffffff"));
|
||||
assert_eq!(cfg.appearance.colors.accent.as_deref(), Some("#ff00ff"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn theme_name_and_colours_compose() {
|
||||
let mut cfg = Config::default();
|
||||
let mut lua_colors = ThemeColors::default();
|
||||
lua_colors.background = Some("#1e1e2e".into());
|
||||
let lc = LuaConfig {
|
||||
theme_name: Some("nord".into()),
|
||||
theme_colors: lua_colors,
|
||||
..Default::default()
|
||||
};
|
||||
lc.merge_into(&mut cfg);
|
||||
assert_eq!(cfg.appearance.theme.as_deref(), Some("nord"));
|
||||
assert_eq!(cfg.appearance.colors.background.as_deref(), Some("#1e1e2e"));
|
||||
}
|
||||
|
||||
// ── owlry.profiles { ... } merge semantics ──────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn profiles_map_replaces_base_profiles() {
|
||||
let mut cfg = Config::default();
|
||||
// Base TOML had a "legacy" profile that must be cleared.
|
||||
cfg.profiles.insert(
|
||||
"legacy".into(),
|
||||
ProfileConfig {
|
||||
modes: vec!["app".into()],
|
||||
},
|
||||
);
|
||||
|
||||
let mut profiles: HashMap<String, Vec<String>> = HashMap::new();
|
||||
profiles.insert("dev".into(), vec!["app".into(), "cmd".into(), "ssh".into()]);
|
||||
profiles.insert("media".into(), vec!["emoji".into(), "clipboard".into()]);
|
||||
let lc = LuaConfig {
|
||||
profiles: Some(profiles),
|
||||
..Default::default()
|
||||
};
|
||||
lc.merge_into(&mut cfg);
|
||||
|
||||
assert!(
|
||||
!cfg.profiles.contains_key("legacy"),
|
||||
"Lua profiles must replace base entries entirely"
|
||||
);
|
||||
assert_eq!(cfg.profiles.len(), 2);
|
||||
assert_eq!(
|
||||
cfg.profiles.get("dev").map(|p| p.modes.as_slice()),
|
||||
Some(&["app".to_string(), "cmd".to_string(), "ssh".to_string()][..])
|
||||
);
|
||||
assert_eq!(
|
||||
cfg.profiles.get("media").map(|p| p.modes.as_slice()),
|
||||
Some(&["emoji".to_string(), "clipboard".to_string()][..])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_profiles_clears_base_profiles() {
|
||||
let mut cfg = Config::default();
|
||||
cfg.profiles.insert(
|
||||
"legacy".into(),
|
||||
ProfileConfig {
|
||||
modes: vec!["app".into()],
|
||||
},
|
||||
);
|
||||
let lc = LuaConfig {
|
||||
profiles: Some(HashMap::new()),
|
||||
..Default::default()
|
||||
};
|
||||
lc.merge_into(&mut cfg);
|
||||
assert!(cfg.profiles.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn omitted_profiles_keeps_base_profiles() {
|
||||
let mut cfg = Config::default();
|
||||
cfg.profiles.insert(
|
||||
"legacy".into(),
|
||||
ProfileConfig {
|
||||
modes: vec!["app".into()],
|
||||
},
|
||||
);
|
||||
let lc = LuaConfig::default(); // profiles: None
|
||||
lc.merge_into(&mut cfg);
|
||||
assert!(cfg.profiles.contains_key("legacy"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -346,4 +346,166 @@ mod tests {
|
||||
other => panic!("expected Read error, got {:?}", other),
|
||||
}
|
||||
}
|
||||
|
||||
// ── owlry.theme(...) end-to-end ─────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn theme_string_form_sets_theme_name() {
|
||||
let s = snapshot_after(r#"owlry.theme("catppuccin-mocha")"#);
|
||||
assert_eq!(s.theme_name.as_deref(), Some("catppuccin-mocha"));
|
||||
// Colours and unknowns unaffected.
|
||||
assert!(s.theme_colors.background.is_none());
|
||||
assert!(s.unknown_theme_keys.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn theme_table_form_captures_known_colour_keys() {
|
||||
let s = snapshot_after(
|
||||
r##"
|
||||
owlry.theme {
|
||||
background = "#1e1e2e",
|
||||
background_secondary = "#313244",
|
||||
border = "#45475a",
|
||||
text = "#cdd6f4",
|
||||
text_secondary = "#a6adc8",
|
||||
accent = "#cba6f7",
|
||||
accent_bright = "#f5c2e7",
|
||||
badge_app = "#a6e3a1",
|
||||
badge_power = "#f38ba8",
|
||||
badge_uuctl = "#9ece6a",
|
||||
}
|
||||
"##,
|
||||
);
|
||||
assert_eq!(s.theme_colors.background.as_deref(), Some("#1e1e2e"));
|
||||
assert_eq!(s.theme_colors.accent.as_deref(), Some("#cba6f7"));
|
||||
assert_eq!(s.theme_colors.badge_app.as_deref(), Some("#a6e3a1"));
|
||||
assert_eq!(s.theme_colors.badge_power.as_deref(), Some("#f38ba8"));
|
||||
assert_eq!(s.theme_colors.badge_uuctl.as_deref(), Some("#9ece6a"));
|
||||
assert!(s.unknown_theme_keys.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn theme_string_and_table_compose() {
|
||||
let s = snapshot_after(
|
||||
r##"
|
||||
owlry.theme("nord")
|
||||
owlry.theme { background = "#1e1e2e" }
|
||||
"##,
|
||||
);
|
||||
assert_eq!(s.theme_name.as_deref(), Some("nord"));
|
||||
assert_eq!(s.theme_colors.background.as_deref(), Some("#1e1e2e"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn theme_table_calls_merge_per_colour_key() {
|
||||
let s = snapshot_after(
|
||||
r##"
|
||||
owlry.theme { background = "#000000", accent = "#ff00ff" }
|
||||
owlry.theme { accent = "#00ffff" }
|
||||
"##,
|
||||
);
|
||||
assert_eq!(s.theme_colors.background.as_deref(), Some("#000000"));
|
||||
assert_eq!(s.theme_colors.accent.as_deref(), Some("#00ffff"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn theme_badge_sys_alias_maps_to_badge_power() {
|
||||
let s = snapshot_after(r##"owlry.theme { badge_sys = "#ff8800" }"##);
|
||||
assert_eq!(s.theme_colors.badge_power.as_deref(), Some("#ff8800"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn theme_unknown_keys_recorded_without_failing() {
|
||||
let s = snapshot_after(
|
||||
r##"
|
||||
owlry.theme {
|
||||
background = "#000000",
|
||||
future_v22_key = "#ffffff",
|
||||
another_unknown = "x",
|
||||
}
|
||||
"##,
|
||||
);
|
||||
assert_eq!(s.theme_colors.background.as_deref(), Some("#000000"));
|
||||
assert!(s.unknown_theme_keys.contains(&"future_v22_key".to_string()));
|
||||
assert!(s.unknown_theme_keys.contains(&"another_unknown".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn theme_wrong_argument_type_errors() {
|
||||
let ctx = LuaContext::new().expect("must build");
|
||||
let err = ctx.eval_str(r#"owlry.theme(42)"#).unwrap_err();
|
||||
let msg = format!("{}", err);
|
||||
assert!(
|
||||
msg.contains("expected a string") || msg.contains("table"),
|
||||
"should mention expected shapes; got: {}",
|
||||
msg
|
||||
);
|
||||
}
|
||||
|
||||
// ── owlry.profiles { ... } end-to-end ───────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn profiles_captures_multiple_named_sets() {
|
||||
let s = snapshot_after(
|
||||
r#"
|
||||
owlry.profiles {
|
||||
dev = { "app", "cmd", "ssh" },
|
||||
media = { "emoji", "clipboard" },
|
||||
minimal = { "app" },
|
||||
}
|
||||
"#,
|
||||
);
|
||||
let map = s.profiles.expect("profiles must be Some");
|
||||
assert_eq!(map.len(), 3);
|
||||
assert_eq!(
|
||||
map.get("dev"),
|
||||
Some(&vec!["app".to_string(), "cmd".to_string(), "ssh".to_string()])
|
||||
);
|
||||
assert_eq!(
|
||||
map.get("minimal"),
|
||||
Some(&vec!["app".to_string()])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn profiles_called_twice_replaces_the_map() {
|
||||
let s = snapshot_after(
|
||||
r#"
|
||||
owlry.profiles { dev = { "app", "cmd" } }
|
||||
owlry.profiles { media = { "emoji" } }
|
||||
"#,
|
||||
);
|
||||
let map = s.profiles.expect("profiles must be Some");
|
||||
assert_eq!(map.len(), 1);
|
||||
assert!(map.contains_key("media"));
|
||||
assert!(!map.contains_key("dev"), "second call must fully replace");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn profiles_with_non_list_value_errors() {
|
||||
let ctx = LuaContext::new().expect("must build");
|
||||
let err = ctx
|
||||
.eval_str(r#"owlry.profiles { broken = "not a list" }"#)
|
||||
.unwrap_err();
|
||||
let msg = format!("{}", err);
|
||||
assert!(
|
||||
msg.contains("owlry.profiles") || msg.contains("list"),
|
||||
"should indicate list-of-strings expectation; got: {}",
|
||||
msg
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn profiles_coerce_numeric_entries_via_lua_tostring() {
|
||||
// mlua's String::from_lua follows Lua semantics (tostring coercion),
|
||||
// so `{ "app", 42 }` becomes ["app", "42"]. Document the behaviour
|
||||
// here so it isn't accidentally tightened — config validate (3.9)
|
||||
// can flag "42" as an unknown provider id if needed.
|
||||
let s = snapshot_after(r#"owlry.profiles { dev = { "app", 42 } }"#);
|
||||
let map = s.profiles.expect("profiles must be Some");
|
||||
assert_eq!(
|
||||
map.get("dev"),
|
||||
Some(&vec!["app".to_string(), "42".to_string()])
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user