diff --git a/cosmic-conf/src/main.rs b/cosmic-conf/src/main.rs index 5c811dd..ea3d836 100644 --- a/cosmic-conf/src/main.rs +++ b/cosmic-conf/src/main.rs @@ -12,9 +12,17 @@ cosmic-conf — compile cosmic.conf into the cosmic-config tree USAGE: cosmic-conf apply [--diff] [--config ] + cosmic-conf watch [--config ] cosmic-conf import-theme [--out ] [--report] [--assets [--source ] [--overwrite] [--dry-run]] +COMMANDS: + apply Compile the config once and exit + watch Stay running and recompile on every edit, to the config + and to anything it sources. A malformed edit is reported + and waited past, not fatal. + import-theme Translate a HyDE theme into config keys + OPTIONS: --diff Show what would change without writing anything --config Config file (default: $XDG_CONFIG_HOME/hyprcosmic/cosmic.conf) @@ -99,12 +107,18 @@ fn main() -> ExitCode { } }; } - if args[0] != "apply" { - eprintln!("error: unknown command `{}`\n\n{USAGE}", args[0]); + let command = args[0].as_str(); + if !matches!(command, "apply" | "watch") { + eprintln!("error: unknown command `{command}`\n\n{USAGE}"); return ExitCode::from(2); } - if let Err(msg) = reject_unknown(&args[1..], &["--diff"], &["--config"]) { + // `--diff` belongs to `apply` alone: a daemon whose whole job is to notice + // a change and write it has nothing to do with a mode that declines to + // write. Passing it to `watch` is an error rather than a no-op, for the + // same reason `--diff-only` is. + let flags: &[&str] = if command == "apply" { &["--diff"] } else { &[] }; + if let Err(msg) = reject_unknown(&args[1..], flags, &["--config"]) { eprintln!("{msg}\n\n{USAGE}"); return ExitCode::from(2); } @@ -127,6 +141,16 @@ fn main() -> ExitCode { }, }; + if command == "watch" { + return match run_watch(&config_path) { + Ok(()) => ExitCode::SUCCESS, + Err(msg) => { + eprint!("{msg}"); + ExitCode::from(1) + } + }; + } + match run(&config_path, diff_only) { Ok(msg) => { println!("{msg}"); @@ -184,6 +208,19 @@ fn run(config_path: &Path, diff_only: bool) -> Result { )) } +/// Block, recompiling on every edit, until the watcher itself stops. +/// +/// Returns nothing to print on success because there is no success to report +/// until it is over: progress goes to stderr as it happens, from inside the +/// loop. A config error is not an error here either -- `watch` reports a +/// malformed edit and waits for the next one, which is the whole point of +/// leaving it running -- so the only failure that reaches this function is the +/// notify machinery failing to start. +fn run_watch(config_path: &Path) -> Result<(), String> { + let emitter = Emitter::from_env().map_err(|e| format!("error: {e}\n"))?; + watch::watch(config_path, &emitter).map_err(|e| format!("{e}\n")) +} + fn run_import(args: &[String]) -> Result { let Some(src_path) = args.first().filter(|a| !a.starts_with("--")) else { return Err(format!("error: import-theme needs a path\n\n{USAGE}")); diff --git a/cosmic-conf/src/watch.rs b/cosmic-conf/src/watch.rs index 9570f73..08a780a 100644 --- a/cosmic-conf/src/watch.rs +++ b/cosmic-conf/src/watch.rs @@ -344,6 +344,15 @@ pub fn watch(config: &Path, emitter: &Emitter) -> Result<(), WatchError> { let mut watcher: RecommendedWatcher = notify::recommended_watcher(tx)?; let mut watched: HashSet = HashSet::new(); + // The last diagnostic printed, so an unchanged one is not printed again. + // A single save arrives as several inotify events -- modify, then + // close_write, sometimes a rename when the editor writes atomically -- and + // they do not all land inside one debounce window, so a broken config + // otherwise reports itself three or four times per keystroke-save. Cleared + // on every successful compile, so the same error reappearing after a good + // one is still news and still printed. + let mut last_error: Option = None; + // Compile once up front: the desktop should reflect the config the // moment the daemon starts, and this also tells us the initial watch // set. If it fails, fall back to watching just `config` — that is the @@ -357,7 +366,9 @@ pub fn watch(config: &Path, emitter: &Emitter) -> Result<(), WatchError> { sync_watches(&mut watcher, &mut watched, &compiled.sources); } Err(e) => { - eprintln!("{e}"); + let text = e.to_string(); + eprintln!("{text}"); + last_error = Some(text); sync_watches( &mut watcher, &mut watched, @@ -381,6 +392,7 @@ pub fn watch(config: &Path, emitter: &Emitter) -> Result<(), WatchError> { match compile(config, emitter) { Ok(compiled) => { + last_error = None; if let Err(e) = emitter.apply(&compiled.planned) { eprintln!("{}", CompileError::Emit(vec![e])); } @@ -390,7 +402,11 @@ pub fn watch(config: &Path, emitter: &Emitter) -> Result<(), WatchError> { // Leave `watched` alone: the fix for a bad edit might land in // an already-sourced file, and dropping back to watching // only `config` would miss that. - eprintln!("{e}"); + let text = e.to_string(); + if last_error.as_deref() != Some(text.as_str()) { + eprintln!("{text}"); + last_error = Some(text); + } } } }