diff --git a/CHANGELOG.md b/CHANGELOG.md index a35216a..d8dfcc2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). ### Changed - input mode (normal/insert/visual/search) is now tracked per tab; switching tabs restores each tab's own mode +- Ctrl+C now copies only when there is an active selection, otherwise it sends SIGINT ## [0.9.0] - 2026-07-05 diff --git a/README.md b/README.md index c55a473..58440e4 100644 --- a/README.md +++ b/README.md @@ -326,7 +326,7 @@ Navigate freely to position the cursor, press `v` to set the selection anchor, t | `g` / `G` | Top / bottom of viewport | | `v` | Set selection anchor at cursor (starts highlighting) | | `o` | Swap anchor and cursor | -| `y` / `Ctrl+C` | Copy selection and exit | +| `y` / `Ctrl+C` | Copy selection and exit (`Ctrl+C` copies only with an active selection; otherwise it sends SIGINT) | | `Y` | Yank (copy) the entire line at cursor | | `q` / `Escape` | Exit to Insert mode | diff --git a/doc/SPEC.md b/doc/SPEC.md index e3f10a6..adca21d 100644 --- a/doc/SPEC.md +++ b/doc/SPEC.md @@ -563,7 +563,7 @@ actions shift the anchor coordinates so the selected content stays stable. | `g` / `G` | Top / bottom of viewport | | `v` | Set selection anchor at cursor (activates highlight) | | `o` | Swap anchor and cursor (extend from either end) | -| `y` / `Ctrl+C` | Copy selection to clipboard, return to Insert mode | +| `y` / `Ctrl+C` | Copy selection to clipboard, return to Insert mode (`Ctrl+C` copies only with an active selection; otherwise it sends SIGINT) | | `Y` | Yank (copy) the entire line at the cursor, return to Insert mode | | `q` / `Escape` | Exit to Insert mode | diff --git a/src/input/keybindings.rs b/src/input/keybindings.rs index 5bb1dd3..a2fc01e 100644 --- a/src/input/keybindings.rs +++ b/src/input/keybindings.rs @@ -246,6 +246,16 @@ fn shift_scroll_action(key: &Key, grid_rows: usize) -> Option { } } +/// Ctrl+C copies only when there is a real (anchored) selection; in every +/// other mode (Insert, Normal, un-anchored Visual) it sends SIGINT (0x03). +fn ctrl_c_action(mode: &InputMode) -> Action { + if matches!(mode, InputMode::Visual { anchored: true, .. }) { + Action::Copy + } else { + Action::SendToPty(vec![3]) + } +} + fn handle_ctrl_only(key: &Key, alt: bool, mode: &InputMode) -> Option { if let Key::Character(s) = key { if s.eq_ignore_ascii_case("w") { @@ -254,8 +264,8 @@ fn handle_ctrl_only(key: &Key, alt: bool, mode: &InputMode) -> Option { if let Some(a) = ctrl_special_char_action(s, mode) { return Some(a); } - if s.eq_ignore_ascii_case("c") && matches!(mode, InputMode::Visual { .. }) { - return Some(Action::Copy); + if s.eq_ignore_ascii_case("c") { + return Some(ctrl_c_action(mode)); } } ctrl_char_action(key, alt) diff --git a/src/input/keybindings_test.rs b/src/input/keybindings_test.rs index cd48e61..c1dd58f 100644 --- a/src/input/keybindings_test.rs +++ b/src/input/keybindings_test.rs @@ -328,12 +328,39 @@ fn ctrl_shift_end_scrolls_to_bottom() { assert!(matches!(a, Action::ScrollToBottom)); } +fn visual_anchored() -> InputMode { + InputMode::Visual { + start_col: 0, + start_row: 0, + cur_col: 0, + cur_row: 0, + anchored: true, + } +} + #[test] -fn ctrl_c_in_visual_copies() { - let a = handle_key_inner(&char_key("c"), true, false, false, &visual(), 80, 24, false); +fn ctrl_c_in_anchored_visual_copies() { + let a = handle_key_inner( + &char_key("c"), + true, + false, + false, + &visual_anchored(), + 80, + 24, + false, + ); assert!(matches!(a, Action::Copy)); } +#[test] +fn ctrl_c_in_unanchored_visual_sends_sigint() { + // Un-anchored Visual has no highlighted selection; ctrl+c must send SIGINT, + // not a no-op Copy. + let a = handle_key_inner(&char_key("c"), true, false, false, &visual(), 80, 24, false); + assert!(matches!(a, Action::SendToPty(ref v) if v == &[3])); +} + #[test] fn ctrl_c_in_insert_does_not_copy() { // In insert mode ctrl+c is sent as byte 0x03 to the PTY @@ -341,6 +368,24 @@ fn ctrl_c_in_insert_does_not_copy() { assert!(matches!(a, Action::SendToPty(ref v) if v == &[3])); } +#[test] +fn ctrl_c_in_normal_sends_sigint() { + let a = handle_key_inner(&char_key("c"), true, false, false, &normal(), 80, 24, false); + assert!(matches!(a, Action::SendToPty(ref v) if v == &[3])); +} + +#[test] +fn ctrl_shift_c_in_insert_does_not_copy() { + let a = handle_key_inner(&char_key("c"), true, true, false, &insert(), 80, 24, false); + assert!(!matches!(a, Action::Copy)); +} + +#[test] +fn ctrl_alt_c_in_insert_does_not_copy() { + let a = handle_key_inner(&char_key("c"), true, false, true, &insert(), 80, 24, false); + assert!(!matches!(a, Action::Copy)); +} + #[test] fn ctrl_q_quits() { let a = handle_key_inner(&char_key("q"), true, false, false, &insert(), 80, 24, false);