From 03bc56007fb768a3c0183d20b0234ee2f74c9202 Mon Sep 17 00:00:00 2001 From: godsfryingpan Date: Mon, 23 Mar 2026 16:42:27 -0500 Subject: [PATCH 1/3] implement watch --- Cargo.lock | 221 ++++++++++++++++++++++++++++++++++++++++++++++++++++- Cargo.toml | 1 + src/app.rs | 90 ++++++++++++++++++++++ 3 files changed, 309 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e27ac29..f97272e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -38,7 +38,7 @@ version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -49,7 +49,7 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -63,6 +63,18 @@ dependencies = [ "syn", ] +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" + [[package]] name = "block-buffer" version = "0.10.4" @@ -155,7 +167,7 @@ version = "3.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "faf9468729b8cbcea668e36183cb69d317348c2e08e994829fb56ebfdfbaac34" dependencies = [ - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -193,6 +205,15 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" +[[package]] +name = "fsevent-sys" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76ee7a02da4d231650c7cea31349b889be2f45ddb3ef3032d2ec8185f6313fd2" +dependencies = [ + "libc", +] + [[package]] name = "generic-array" version = "0.14.7" @@ -237,6 +258,26 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "inotify" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd5b3eaf1a28b758ac0faa5a4254e8ab2705605496f1b1f3fbbc3988ad73d199" +dependencies = [ + "bitflags 2.11.0", + "inotify-sys", + "libc", +] + +[[package]] +name = "inotify-sys" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb" +dependencies = [ + "libc", +] + [[package]] name = "is_terminal_polyfill" version = "1.70.2" @@ -249,18 +290,83 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" +[[package]] +name = "kqueue" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eac30106d7dce88daf4a3fcb4879ea939476d5074a9b7ddd0fb97fa4bed5596a" +dependencies = [ + "kqueue-sys", + "libc", +] + +[[package]] +name = "kqueue-sys" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed9625ffda8729b85e45cf04090035ac368927b8cebc34898e7c120f52e4838b" +dependencies = [ + "bitflags 1.3.2", + "libc", +] + [[package]] name = "libc" version = "0.2.183" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + [[package]] name = "memchr" version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" +[[package]] +name = "mio" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" +dependencies = [ + "libc", + "log", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "notify" +version = "8.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d3d07927151ff8575b7087f245456e549fea62edf0ec4e565a5ee50c8402bc3" +dependencies = [ + "bitflags 2.11.0", + "fsevent-sys", + "inotify", + "kqueue", + "libc", + "log", + "mio", + "notify-types", + "walkdir", + "windows-sys 0.60.2", +] + +[[package]] +name = "notify-types" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42b8cfee0e339a0337359f3c88165702ac6e600dc01c0cc9579a92d62b08477a" +dependencies = [ + "bitflags 2.11.0", +] + [[package]] name = "once_cell_polyfill" version = "1.70.2" @@ -276,6 +382,7 @@ dependencies = [ "clap_complete_nushell", "colored", "glob", + "notify", "serde", "serde_json", "sha256", @@ -306,6 +413,15 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + [[package]] name = "serde" version = "1.0.228" @@ -472,12 +588,46 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "windows-link" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets", +] + [[package]] name = "windows-sys" version = "0.61.2" @@ -487,6 +637,71 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + [[package]] name = "winnow" version = "1.0.0" diff --git a/Cargo.toml b/Cargo.toml index a8e016f..5dc9c76 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,6 +10,7 @@ clap_complete = "4.6.0" clap_complete_nushell = "4.6.0" colored = "3.1.1" glob = "0.3.3" +notify = "8.2.0" serde = { version = "1.0.228", features = ["derive"] } serde_json = "1.0.149" sha256 = "1.6.0" diff --git a/src/app.rs b/src/app.rs index bdc6e06..a21d2af 100644 --- a/src/app.rs +++ b/src/app.rs @@ -60,6 +60,14 @@ enum Subcommand { /// The package to be added package: String, }, + /// Watches for source code changes and triggers a recompilation && re-run on change + Watch { + /// The build mode to use + mode: Option, + /// Arguments to pass to the project binary + #[arg(long, short)] + args: Option>, + }, } #[derive(clap::Subcommand)] @@ -186,10 +194,92 @@ impl App { std::process::exit(1); } } + Subcommand::Watch { mode, args } => { + if let Err(e) = watch(&mode, args) { + eprintln!("Error running watch: {e}"); + std::process::exit(1); + } + } } } } +fn watch(mode: &Option, args: Option>) -> std::io::Result<()> { + use notify::{Event, RecursiveMode, Watcher, recommended_watcher}; + use std::sync::mpsc; + use std::time::{Duration, Instant}; + + println!( + " {} project for changes (ctrl+c to stop)", + "Watching".green().bold() + ); + + // do an initial build+run before watching + build(mode, false)?; + run(mode, args.clone())?; + + let (tx, rx) = mpsc::channel::>(); + + let mut watcher = recommended_watcher(tx).map_err(|e| std::io::Error::other(format!("{e}")))?; + + watcher + .watch(Path::new("src"), RecursiveMode::Recursive) + .map_err(|e| std::io::Error::other(format!("{e}")))?; + + // debounce — ignore events within 500ms of the last one + // this prevents double-triggers from editors that write files in multiple steps + let mut last_build = Instant::now() + .checked_sub(Duration::from_secs(1)) + .unwrap_or(Instant::now()); + + loop { + match rx.recv() { + Ok(Ok(event)) => { + // only care about actual file modifications + let is_relevant = matches!( + event.kind, + notify::EventKind::Create(_) + | notify::EventKind::Modify(_) + | notify::EventKind::Remove(_) + ); + + // only care about .c and .h files + let affects_source = event.paths.iter().any(|p| { + matches!( + p.extension().and_then(|e| e.to_str()), + Some("c") | Some("h") + ) + }); + + let debounced = last_build.elapsed() > Duration::from_millis(500); + + if is_relevant && affects_source && debounced { + last_build = Instant::now(); + println!( + "\n {} changes detected, rebuilding...", + "Watch".green().bold() + ); + if let Err(e) = build(mode, false) { + eprintln!(" {} {e}", "Error".red().bold()); + // don't exit — keep watching even if build fails + continue; + } + if let Err(e) = run(mode, args.clone()) { + eprintln!(" {} {e}", "Error".red().bold()); + } + } + } + Ok(Err(e)) => eprintln!(" {} watch error: {e}", "Error".red().bold()), + Err(e) => { + eprintln!(" {} channel error: {e}", "Error".red().bold()); + break; + } + } + } + + Ok(()) +} + fn add_package(package: &str) -> std::io::Result<()> { let status = Command::new("pkg-config") .arg("--exists") -- 2.49.1 From b541865ff16436de3fff2f56d0984e6e1fd7948c Mon Sep 17 00:00:00 2001 From: godsfryingpan Date: Mon, 23 Mar 2026 16:50:24 -0500 Subject: [PATCH 2/3] refinements based on feedback --- src/app.rs | 115 +++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 89 insertions(+), 26 deletions(-) diff --git a/src/app.rs b/src/app.rs index a21d2af..94a022a 100644 --- a/src/app.rs +++ b/src/app.rs @@ -205,29 +205,91 @@ impl App { } fn watch(mode: &Option, args: Option>) -> std::io::Result<()> { + use std::io::Write; + + // save terminal state + let mut stdout = std::io::stdout(); + // enter alternate screen — this is a separate terminal buffer, + // exiting it restores everything exactly as it was + write!(stdout, "\x1b[?1049h")?; + stdout.flush()?; + + // make sure we always restore the terminal, even on panic + let result = watch_inner(mode, args); + + // exit alternate screen + write!(stdout, "\x1b[?1049l")?; + stdout.flush()?; + + result +} + +fn watch_inner(mode: &Option, args: Option>) -> std::io::Result<()> { use notify::{Event, RecursiveMode, Watcher, recommended_watcher}; + use std::io::Write; use std::sync::mpsc; use std::time::{Duration, Instant}; - println!( - " {} project for changes (ctrl+c to stop)", - "Watching".green().bold() - ); + let clear = || { + let mut stdout = std::io::stdout(); + write!(stdout, "\x1b[2J\x1b[H").ok(); + stdout.flush().ok(); + }; - // do an initial build+run before watching - build(mode, false)?; - run(mode, args.clone())?; + let print_header = |label: &str| { + println!( + " {} project (ctrl+c to stop) — {label}", + "Watching".green().bold() + ); + println!("{}", "─".repeat(50).dimmed()); + }; + + // track the running child process + let mut child: Option = None; + + let kill_child = |child: &mut Option| { + if let Some(c) = child { + c.kill().ok(); + c.wait().ok(); + } + *child = None; + }; + + let run_child = |mode: &Option, + args: &Option>| + -> std::io::Result { + let conf = get_config().ok_or(std::io::Error::new( + std::io::ErrorKind::NotFound, + "no Pallet.toml found", + ))?; + let build_config = conf.get_or_default(mode).ok_or(std::io::Error::new( + std::io::ErrorKind::NotFound, + "build layout not found", + ))?; + let mut command = Command::new(format!("target/{}/{}", build_config.name, conf.name)); + if let Some(a) = args { + for arg in a { + command.arg(arg); + } + } + command + .spawn() + .map_err(|e| std::io::Error::other(format!("{e}"))) + }; + + // initial build + run + clear(); + print_header("starting"); + if build(mode, false).is_ok() { + child = run_child(mode, &args).ok(); + } let (tx, rx) = mpsc::channel::>(); - let mut watcher = recommended_watcher(tx).map_err(|e| std::io::Error::other(format!("{e}")))?; - watcher .watch(Path::new("src"), RecursiveMode::Recursive) .map_err(|e| std::io::Error::other(format!("{e}")))?; - // debounce — ignore events within 500ms of the last one - // this prevents double-triggers from editors that write files in multiple steps let mut last_build = Instant::now() .checked_sub(Duration::from_secs(1)) .unwrap_or(Instant::now()); @@ -235,37 +297,37 @@ fn watch(mode: &Option, args: Option>) -> std::io::Result<() loop { match rx.recv() { Ok(Ok(event)) => { - // only care about actual file modifications let is_relevant = matches!( event.kind, notify::EventKind::Create(_) | notify::EventKind::Modify(_) | notify::EventKind::Remove(_) ); - - // only care about .c and .h files let affects_source = event.paths.iter().any(|p| { matches!( p.extension().and_then(|e| e.to_str()), Some("c") | Some("h") ) }); - let debounced = last_build.elapsed() > Duration::from_millis(500); if is_relevant && affects_source && debounced { last_build = Instant::now(); - println!( - "\n {} changes detected, rebuilding...", - "Watch".green().bold() - ); - if let Err(e) = build(mode, false) { - eprintln!(" {} {e}", "Error".red().bold()); - // don't exit — keep watching even if build fails - continue; - } - if let Err(e) = run(mode, args.clone()) { - eprintln!(" {} {e}", "Error".red().bold()); + + // kill whatever is currently running + kill_child(&mut child); + + clear(); + print_header("rebuilding"); + + match build(mode, false) { + Ok(_) => { + child = run_child(mode, &args).ok(); + } + Err(e) => { + eprintln!(" {} {e}", "Error".red().bold()); + // child stays None until next successful build + } } } } @@ -277,6 +339,7 @@ fn watch(mode: &Option, args: Option>) -> std::io::Result<() } } + kill_child(&mut child); Ok(()) } -- 2.49.1 From b4a28efcbabec7a9865dc58a61b98357bbe484ea Mon Sep 17 00:00:00 2001 From: godsfryingpan Date: Mon, 23 Mar 2026 16:57:26 -0500 Subject: [PATCH 3/3] attempt to fix issue w terminal --- Cargo.lock | 66 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 1 + src/app.rs | 37 +++++++++++++++++++----------- 3 files changed, 91 insertions(+), 13 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f97272e..170ca3d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -84,6 +84,15 @@ dependencies = [ "generic-array", ] +[[package]] +name = "block2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" +dependencies = [ + "objc2", +] + [[package]] name = "bytes" version = "1.11.1" @@ -96,6 +105,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + [[package]] name = "clap" version = "4.6.0" @@ -189,6 +204,17 @@ dependencies = [ "typenum", ] +[[package]] +name = "ctrlc" +version = "3.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0b1fab2ae45819af2d0731d60f2afe17227ebb1a1538a236da84c93e9a60162" +dependencies = [ + "dispatch2", + "nix", + "windows-sys 0.61.2", +] + [[package]] name = "digest" version = "0.10.7" @@ -199,6 +225,18 @@ dependencies = [ "crypto-common", ] +[[package]] +name = "dispatch2" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" +dependencies = [ + "bitflags 2.11.0", + "block2", + "libc", + "objc2", +] + [[package]] name = "equivalent" version = "1.0.2" @@ -340,6 +378,18 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "nix" +version = "0.31.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d6d0705320c1e6ba1d912b5e37cf18071b6c2e9b7fa8215a1e8a7651966f5d3" +dependencies = [ + "bitflags 2.11.0", + "cfg-if", + "cfg_aliases", + "libc", +] + [[package]] name = "notify" version = "8.2.0" @@ -367,6 +417,21 @@ dependencies = [ "bitflags 2.11.0", ] +[[package]] +name = "objc2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" +dependencies = [ + "objc2-encode", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + [[package]] name = "once_cell_polyfill" version = "1.70.2" @@ -381,6 +446,7 @@ dependencies = [ "clap_complete", "clap_complete_nushell", "colored", + "ctrlc", "glob", "notify", "serde", diff --git a/Cargo.toml b/Cargo.toml index 5dc9c76..d2a626d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,6 +9,7 @@ clap = { version = "4.6.0", features = ["derive"] } clap_complete = "4.6.0" clap_complete_nushell = "4.6.0" colored = "3.1.1" +ctrlc = "3.5.2" glob = "0.3.3" notify = "8.2.0" serde = { version = "1.0.228", features = ["derive"] } diff --git a/src/app.rs b/src/app.rs index 94a022a..9664f27 100644 --- a/src/app.rs +++ b/src/app.rs @@ -2,6 +2,7 @@ use std::{ env::set_current_dir, path::{Path, PathBuf}, process::Command, + sync::{Arc, atomic::Ordering}, }; use clap::CommandFactory; @@ -206,25 +207,37 @@ impl App { fn watch(mode: &Option, args: Option>) -> std::io::Result<()> { use std::io::Write; + use std::sync::Arc; + use std::sync::atomic::{AtomicBool, Ordering}; - // save terminal state let mut stdout = std::io::stdout(); - // enter alternate screen — this is a separate terminal buffer, - // exiting it restores everything exactly as it was + + // enter alternate screen write!(stdout, "\x1b[?1049h")?; stdout.flush()?; - // make sure we always restore the terminal, even on panic - let result = watch_inner(mode, args); + // set up ctrl+c handler BEFORE doing anything else + let running = Arc::new(AtomicBool::new(true)); + let running_clone = running.clone(); + ctrlc::set_handler(move || { + running_clone.store(false, Ordering::SeqCst); + }) + .map_err(|e| std::io::Error::other(format!("{e}")))?; - // exit alternate screen + let result = watch_inner(mode, args, running); + + // exit alternate screen — guaranteed to run now write!(stdout, "\x1b[?1049l")?; stdout.flush()?; result } -fn watch_inner(mode: &Option, args: Option>) -> std::io::Result<()> { +fn watch_inner( + mode: &Option, + args: Option>, + running: Arc, +) -> std::io::Result<()> { use notify::{Event, RecursiveMode, Watcher, recommended_watcher}; use std::io::Write; use std::sync::mpsc; @@ -294,8 +307,8 @@ fn watch_inner(mode: &Option, args: Option>) -> std::io::Res .checked_sub(Duration::from_secs(1)) .unwrap_or(Instant::now()); - loop { - match rx.recv() { + while running.load(Ordering::SeqCst) { + match rx.recv_timeout(Duration::from_millis(100)) { Ok(Ok(event)) => { let is_relevant = matches!( event.kind, @@ -332,10 +345,8 @@ fn watch_inner(mode: &Option, args: Option>) -> std::io::Res } } Ok(Err(e)) => eprintln!(" {} watch error: {e}", "Error".red().bold()), - Err(e) => { - eprintln!(" {} channel error: {e}", "Error".red().bold()); - break; - } + Err(mpsc::RecvTimeoutError::Timeout) => continue, // check running flag and loop + Err(mpsc::RecvTimeoutError::Disconnected) => break, } } -- 2.49.1