diff --git a/src/main.rs b/src/main.rs index af9e1b2..5f20983 100644 --- a/src/main.rs +++ b/src/main.rs @@ -84,7 +84,17 @@ enum Command { file: PathBuf, }, /// Pick a mode from a floating bar (Cmd+Shift+5). - Toolbar, + Toolbar { + /// Print the remembered choices as one line of JSON (for the Noctalia plugin). + #[arg(long, conflicts_with_all = ["set", "run"])] + state: bool, + /// Change a remembered choice: mode, timer, cursor, preview or sound. + #[arg(long, value_name = "KEY=VALUE", conflicts_with = "run")] + set: Vec, + /// Capture at once with the remembered choices, without the bar. + #[arg(long, value_name = "MODE", num_args = 0..=1)] + run: Option>, + }, /// The paint shader behind niri's overview (keep it running; see the README). Backdrop, /// Report what the compositor and system support. @@ -218,9 +228,24 @@ fn run(cli: Cli) -> Result<()> { // Handled in main, before logging starts. Command::CheckConfig { file } => config::check(&file), Command::ComboDemo => sound::demo(&config::load(&config::default_path())?.sound), - Command::Toolbar => { + Command::Toolbar { state, set, run } => { let config = config::load(&config::default_path())?; - let (mode, common) = from_toolbar(toolbar::run(&config)?); + let path = toolbar::state::default_path(); + if state { + println!( + "{}", + toolbar::state::to_json(&toolbar::state::load(&path, &config)) + ); + return Ok(()); + } + if !set.is_empty() { + return toolbar::state::set(&path, &config, &set); + } + let picked = match run { + Some(mode) => toolbar::run_remembered(&config, mode)?, + None => toolbar::run(&config)?, + }; + let (mode, common) = from_toolbar(picked); capture(mode, common) } Command::Clipboard => { @@ -473,6 +498,20 @@ mod tests { assert!(!parses(&["edit"])); assert!(parses(&["__clipboard"])); assert!(parses(&["toolbar"])); + assert!(parses(&["toolbar", "--state"])); + assert!(parses(&[ + "toolbar", + "--set", + "timer=5", + "--set", + "sound=false" + ])); + assert!(parses(&["toolbar", "--run"])); + assert!(parses(&["toolbar", "--run", "zoom"])); + assert!(!parses(&["toolbar", "--run", "video"])); + assert!(!parses(&["toolbar", "--state", "--run"])); + assert!(!parses(&["toolbar", "--state", "--set", "timer=5"])); + assert!(!parses(&["toolbar", "--set", "timer=5", "--run", "zoom"])); assert!(parses(&["backdrop"])); assert!(parses(&["window", "--pick"])); assert!(parses(&["__combo-demo"])); diff --git a/src/toolbar/mod.rs b/src/toolbar/mod.rs index c2d732c..93981fd 100644 --- a/src/toolbar/mod.rs +++ b/src/toolbar/mod.rs @@ -5,6 +5,7 @@ pub mod draw; pub mod layout; pub mod state; +use std::path::Path; use std::time::{Duration, Instant}; use anyhow::{Context, Result}; @@ -44,6 +45,17 @@ pub struct Picked { pub sound: bool, } +impl From for Picked { + fn from(s: ToolbarState) -> Self { + Picked { + mode: s.mode, + cursor: s.cursor, + preview: s.preview, + sound: s.sound, + } + } +} + /// Shows the bar, waits for a pick, saves it, runs the countdown. pub fn run(config: &Config) -> Result { let path = state::default_path(); @@ -58,13 +70,33 @@ pub fn run(config: &Config) -> Result { tracing::warn!("could not remember the toolbar choice: {e:#}"); } countdown(&mut wl, &output, picked.0.timer)?; - let s = picked.0; - Ok(Picked { - mode: picked.1, - cursor: s.cursor, - preview: s.preview, - sound: s.sound, - }) + Ok(Picked::from(picked.0)) +} + +/// The remembered state, with `mode` (if given) remembered as the new mode. +pub fn remember(path: &Path, config: &Config, mode: Option) -> ToolbarState { + let mut s = state::load(path, config); + if let Some(mode) = mode { + s.mode = mode; + if let Err(e) = state::save(path, &s) { + tracing::warn!("could not remember the toolbar choice: {e:#}"); + } + } + s +} + +/// `valw toolbar --run`: the remembered options without the bar (for the +/// Noctalia plugin), then the countdown. +pub fn run_remembered(config: &Config, mode: Option) -> Result { + let s = remember(&state::default_path(), config, mode); + if s.timer > 0 { + let mut wl = Wayland::connect()?; + let outputs = wl.outputs(); + anyhow::ensure!(!outputs.is_empty(), "the compositor reported no outputs"); + let output = outputs[crate::focused_output(&outputs)].clone(); + countdown(&mut wl, &output, s.timer)?; + } + Ok(Picked::from(s)) } pub struct Toolbar { @@ -388,3 +420,66 @@ impl Pill { } } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn a_pick_carries_the_state_options() { + let state = ToolbarState { + mode: Mode::Window, + timer: 5, + cursor: true, + preview: false, + sound: true, + }; + assert_eq!( + Picked::from(state), + Picked { + mode: Mode::Window, + cursor: true, + preview: false, + sound: true, + } + ); + } + + #[test] + fn run_remembers_a_given_mode() { + let dir = tempfile::tempdir().unwrap(); + let path = dir.path().join("toolbar.toml"); + let config = Config::default(); + state::save( + &path, + &ToolbarState { + timer: 5, + ..state::defaults(&config) + }, + ) + .unwrap(); + + let state = remember(&path, &config, Some(Mode::Zoom)); + assert_eq!( + (state.mode, state.timer), + (Mode::Zoom, 5), + "the options stay" + ); + assert_eq!(state::load(&path, &config).mode, Mode::Zoom, "saved"); + + assert_eq!( + remember(&path, &config, None).mode, + Mode::Zoom, + "no mode: the remembered one" + ); + } + + #[test] + fn run_without_a_mode_writes_nothing() { + let dir = tempfile::tempdir().unwrap(); + let path = dir.path().join("toolbar.toml"); + let config = Config::default(); + assert_eq!(remember(&path, &config, None), state::defaults(&config)); + assert!(!path.exists()); + } +}