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, } }