toolbar: state as JSON and KEY=VALUE sets, for the Noctalia plugin

This commit is contained in:
elwrcl committed 2026-10-04 05:18:18 +03:00
1 parent d0a12a2a5d
commit 3d7a4de10a
1 file changed
+157 -1
+157 -1
View File
@@ -8,7 +8,7 @@ use serde::{Deserialize, Serialize};
use crate::config::Config;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, clap::ValueEnum)]
#[serde(rename_all = "lowercase")]
pub enum Mode {
Screen,
@@ -101,6 +101,62 @@ pub fn save(path: &Path, state: &ToolbarState) -> Result<()> {
crate::output::write_atomic(path, text.as_bytes())
}
/// One line of JSON for the Noctalia plugin: the state and the timer choices.
pub fn to_json(state: &ToolbarState) -> String {
serde_json::json!({
"mode": state.mode,
"timer": state.timer,
"cursor": state.cursor,
"preview": state.preview,
"sound": state.sound,
"timers": TIMERS,
})
.to_string()
}
/// `state` with every `KEY=VALUE` applied in order; the first bad pair is
/// an error naming it.
pub fn apply_sets(mut state: ToolbarState, sets: &[String]) -> Result<ToolbarState> {
for pair in sets {
let Some((key, value)) = pair.split_once('=') else {
bail!("expected KEY=VALUE, not {pair:?}");
};
let flag = || match value {
"true" => Ok(true),
"false" => Ok(false),
_ => Err(anyhow::anyhow!(
"{key} must be true or false, not {value:?}"
)),
};
match key {
"mode" => {
state.mode = <Mode as clap::ValueEnum>::from_str(value, false).map_err(|_| {
anyhow::anyhow!("mode must be screen, window, region or zoom, not {value:?}")
})?;
}
"timer" => {
state.timer = value
.parse()
.ok()
.filter(|t| TIMERS.contains(t))
.with_context(|| format!("timer must be 0, 5 or 10, not {value:?}"))?;
}
"cursor" => state.cursor = flag()?,
"preview" => state.preview = flag()?,
"sound" => state.sound = flag()?,
_ => bail!("unknown toolbar key {key:?} (mode, timer, cursor, preview or sound)"),
}
}
Ok(state)
}
/// `valw toolbar --set`: `sets` applied to the remembered state and saved;
/// nothing changes if any pair is bad.
pub fn set(path: &Path, config: &Config, sets: &[String]) -> Result<()> {
let state = apply_sets(load(path, config), sets)?;
save(path, &state)
}
#[cfg(test)]
mod tests {
use super::*;
@@ -175,4 +231,104 @@ mod tests {
assert_eq!(state.timer, 0);
assert_eq!(state.preview, config.preview.enabled);
}
#[test]
fn json_for_the_plugin() {
let state = ToolbarState {
mode: Mode::Zoom,
timer: 5,
cursor: true,
preview: false,
sound: true,
};
let text = to_json(&state);
assert!(!text.contains('\n'), "one line");
let value: serde_json::Value = serde_json::from_str(&text).unwrap();
assert_eq!(
value,
serde_json::json!({
"mode": "zoom",
"timer": 5,
"cursor": true,
"preview": false,
"sound": true,
"timers": [0, 5, 10],
})
);
}
#[test]
fn sets_apply_every_key_in_order() {
let start = defaults(&Config::default());
let sets: Vec<String> = [
"mode=window",
"timer=10",
"cursor=true",
"preview=false",
"sound=false",
]
.map(String::from)
.into();
assert_eq!(
apply_sets(start, &sets).unwrap(),
ToolbarState {
mode: Mode::Window,
timer: 10,
cursor: true,
preview: false,
sound: false,
}
);
let twice = ["timer=5".to_string(), "timer=0".to_string()];
assert_eq!(
apply_sets(start, &twice).unwrap().timer,
0,
"the last one wins"
);
}
#[test]
fn bad_sets_are_rejected_by_name() {
let start = defaults(&Config::default());
for (pair, message) in [
("colour=red", "unknown toolbar key \"colour\""),
("mode=video", "mode must be screen, window, region or zoom"),
("timer=7", "timer must be 0, 5 or 10"),
("timer=soon", "timer must be 0, 5 or 10"),
("cursor=maybe", "cursor must be true or false"),
("cursor", "expected KEY=VALUE"),
] {
let err = format!("{:#}", apply_sets(start, &[pair.to_string()]).unwrap_err());
assert!(err.contains(message), "{pair}: {err}");
}
}
#[test]
fn set_writes_all_or_nothing() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("valw/toolbar.toml");
let config = Config::default();
let bad = ["timer=5".to_string(), "mode=video".to_string()];
assert!(set(&path, &config, &bad).is_err());
assert!(!path.exists(), "nothing written");
let good = ["timer=5".to_string(), "sound=false".to_string()];
set(&path, &config, &good).unwrap();
let state = load(&path, &config);
assert_eq!(
(state.timer, state.sound, state.mode),
(5, false, Mode::Region)
);
std::fs::write(&path, "not toml [").unwrap();
set(&path, &config, &["cursor=true".to_string()]).unwrap();
assert_eq!(
load(&path, &config),
ToolbarState {
cursor: true,
..defaults(&config)
},
"a broken file is replaced, starting from the defaults"
);
}
}