diff --git a/CHANGELOG.md b/CHANGELOG.md index e55fb949..307d730c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) ### Fixed +* Fix Ctrl+C handling to use SIGINT signal instead of keypress #491 - @chiranjeevi-max * Update CONTRIBUTING information #438 - @YJDoc2 @cyqsimon * Fix new clippy lint #457 - @cyqsimon * Apply new clippy lints #468 - @cyqsimon diff --git a/Cargo.lock b/Cargo.lock index 186f689d..a8d29d29 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -166,6 +166,7 @@ dependencies = [ "clap_complete", "clap_mangen", "crossterm 0.29.0", + "ctrlc", "derive_more", "eyre", "http_req", @@ -240,6 +241,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 = "bumpalo" version = "3.19.0" @@ -318,6 +328,12 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + [[package]] name = "chrono" version = "0.4.41" @@ -329,7 +345,7 @@ dependencies = [ "js-sys", "num-traits", "wasm-bindgen", - "windows-link", + "windows-link 0.1.3", ] [[package]] @@ -577,6 +593,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 = "darling" version = "0.20.11" @@ -688,6 +715,18 @@ dependencies = [ "subtle", ] +[[package]] +name = "dispatch2" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec" +dependencies = [ + "bitflags 2.9.1", + "block2", + "libc", + "objc2", +] + [[package]] name = "displaydoc" version = "0.2.5" @@ -1251,9 +1290,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.174" +version = "0.2.180" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" +checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" [[package]] name = "libloading" @@ -1467,6 +1506,18 @@ dependencies = [ "thiserror 2.0.12", ] +[[package]] +name = "nix" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225e7cfe711e0ba79a68baeddb2982723e4235247aefce1482f2f16c27865b66" +dependencies = [ + "bitflags 2.9.1", + "cfg-if", + "cfg_aliases", + "libc", +] + [[package]] name = "no-std-net" version = "0.6.0" @@ -1527,6 +1578,15 @@ dependencies = [ "libc", ] +[[package]] +name = "objc2" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c2599ce0ec54857b29ce62166b0ed9b4f6f1a70ccc9a71165b6154caca8c05" +dependencies = [ + "objc2-encode", +] + [[package]] name = "objc2-core-foundation" version = "0.3.1" @@ -1536,6 +1596,12 @@ dependencies = [ "bitflags 2.9.1", ] +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + [[package]] name = "objc2-io-kit" version = "0.3.1" @@ -2788,7 +2854,7 @@ dependencies = [ "windows-collections", "windows-core", "windows-future", - "windows-link", + "windows-link 0.1.3", "windows-numerics", ] @@ -2809,7 +2875,7 @@ checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" dependencies = [ "windows-implement", "windows-interface", - "windows-link", + "windows-link 0.1.3", "windows-result", "windows-strings", ] @@ -2821,7 +2887,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" dependencies = [ "windows-core", - "windows-link", + "windows-link 0.1.3", "windows-threading", ] @@ -2853,6 +2919,12 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + [[package]] name = "windows-numerics" version = "0.2.0" @@ -2860,7 +2932,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" dependencies = [ "windows-core", - "windows-link", + "windows-link 0.1.3", ] [[package]] @@ -2869,7 +2941,7 @@ version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" dependencies = [ - "windows-link", + "windows-link 0.1.3", ] [[package]] @@ -2878,7 +2950,7 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" dependencies = [ - "windows-link", + "windows-link 0.1.3", ] [[package]] @@ -2917,6 +2989,15 @@ dependencies = [ "windows-targets 0.53.2", ] +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link 0.2.1", +] + [[package]] name = "windows-targets" version = "0.48.5" @@ -2970,7 +3051,7 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" dependencies = [ - "windows-link", + "windows-link 0.1.3", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 29c2a630..da3ff857 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,6 +31,7 @@ chrono = "0.4" clap-verbosity-flag = "3.0.3" clap = { version = "4.5.41", features = ["derive"] } crossterm = "0.29.0" +ctrlc = "3.4" derive_more = { version = "2.0.1", features = ["debug"] } eyre = "0.6.12" itertools = "0.14.0" diff --git a/src/main.rs b/src/main.rs index 8ace86fb..400f5480 100644 --- a/src/main.rs +++ b/src/main.rs @@ -70,6 +70,11 @@ fn main() -> eyre::Result<()> { let _ = crossterm::execute!(&mut stdout, terminal::EnterAlternateScreen); let terminal_backend = CrosstermBackend::new(stdout); start(terminal_backend, os_input, opts); + + // Ensure terminal is restored after exit (handles SIGINT case). + // These operations are idempotent, so safe to call even if 'q' already cleaned up. + let _ = terminal::disable_raw_mode(); + let _ = crossterm::execute!(std::io::stdout(), terminal::LeaveAlternateScreen); } Ok(()) } @@ -96,6 +101,17 @@ where let cumulative_time = Arc::new(RwLock::new(Duration::new(0, 0))); let table_cycle_offset = Arc::new(AtomicUsize::new(0)); + // handle SIGINT properly instead of as a keypress + // see https://github.com/imsnif/bandwhich/issues/487 + #[cfg(not(test))] + { + let running = running.clone(); + ctrlc::set_handler(move || { + running.store(false, Ordering::Release); + }) + .expect("failed to set SIGINT handler"); + } + let mut active_threads = vec![]; let terminal_events = os_input.terminal_events; @@ -175,7 +191,11 @@ where let display_handler = display_handler.thread().clone(); move || { - for evt in terminal_events { + let mut terminal_events = terminal_events; + while running.load(Ordering::Acquire) { + let Some(evt) = terminal_events.next() else { + continue; + }; let mut ui = ui.lock().unwrap(); match evt { @@ -192,12 +212,6 @@ where ); } Event::Key(KeyEvent { - modifiers: KeyModifiers::CONTROL, - code: KeyCode::Char('c'), - kind: KeyEventKind::Press, - .. - }) - | Event::Key(KeyEvent { modifiers: KeyModifiers::NONE, code: KeyCode::Char('q'), kind: KeyEventKind::Press, diff --git a/src/os/shared.rs b/src/os/shared.rs index 511ffc1b..23eeeecc 100644 --- a/src/os/shared.rs +++ b/src/os/shared.rs @@ -1,10 +1,10 @@ use std::{ io::{self, ErrorKind, Write}, net::Ipv4Addr, - time, + time::{self, Duration}, }; -use crossterm::event::{read, Event}; +use crossterm::event::{poll, read, Event}; use eyre::{bail, eyre}; use itertools::Itertools; use log::{debug, warn}; @@ -35,12 +35,25 @@ impl ProcessInfo { } } +/// Poll timeout for terminal events. +/// This allows the event loop to periodically check the `running` flag +/// for graceful shutdown on SIGINT. +const POLL_TIMEOUT: Duration = Duration::from_millis(100); + pub struct TerminalEvents; impl Iterator for TerminalEvents { type Item = Event; + /// Returns the next terminal event, or `None` if no event is available + /// within the poll timeout. + /// + /// Note: `None` here means "no event right now", not "iteration complete". + /// The consumer should use `while running` instead of `for evt in ...`. fn next(&mut self) -> Option { - read().ok() + match poll(POLL_TIMEOUT) { + Ok(true) => read().ok(), + Ok(false) | Err(_) => None, + } } } diff --git a/src/tests/cases/test_utils.rs b/src/tests/cases/test_utils.rs index be2b2c0b..36f87836 100644 --- a/src/tests/cases/test_utils.rs +++ b/src/tests/cases/test_utils.rs @@ -25,8 +25,8 @@ use crate::{ pub fn sleep_and_quit_events(sleep_num: usize) -> Box { let events = iter::repeat_n(None, sleep_num) .chain([Some(Event::Key(KeyEvent::new( - KeyCode::Char('c'), - KeyModifiers::CONTROL, + KeyCode::Char('q'), + KeyModifiers::NONE, )))]) .collect(); Box::new(TerminalEvents::new(events)) @@ -37,8 +37,8 @@ pub fn sleep_resize_and_quit_events(sleep_num: usize) -> Box { .chain([ Some(Event::Resize(100, 100)), Some(Event::Key(KeyEvent::new( - KeyCode::Char('c'), - KeyModifiers::CONTROL, + KeyCode::Char('q'), + KeyModifiers::NONE, ))), ]) .collect();