use super::*;
use crossterm::event::{KeyCode, KeyEvent, KeyEventKind, KeyEventState, KeyModifiers};
fn key(code: KeyCode, modifiers: KeyModifiers) -> KeyEvent {
KeyEvent {
code,
modifiers,
kind: KeyEventKind::Press,
state: KeyEventState::NONE,
}
}
#[test]
fn altgr_backslash_german_layout() {
let ev = key(KeyCode::Char('\\'), KeyModifiers::CONTROL | KeyModifiers::ALT);
let bytes = encode_key_event(&ev).unwrap();
assert_eq!(bytes, b"\\", "AltGr+backslash must produce literal backslash");
}
#[test]
fn altgr_at_sign_german_layout() {
let ev = key(KeyCode::Char('@'), KeyModifiers::CONTROL | KeyModifiers::ALT);
let bytes = encode_key_event(&ev).unwrap();
assert_eq!(bytes, b"@", "AltGr+@ must produce literal @");
}
#[test]
fn altgr_open_curly_brace() {
let ev = key(KeyCode::Char('{'), KeyModifiers::CONTROL | KeyModifiers::ALT);
let bytes = encode_key_event(&ev).unwrap();
assert_eq!(bytes, b"{", "AltGr+{{ must produce literal {{");
}
#[test]
fn altgr_close_curly_brace() {
let ev = key(KeyCode::Char('}'), KeyModifiers::CONTROL | KeyModifiers::ALT);
let bytes = encode_key_event(&ev).unwrap();
assert_eq!(bytes, b"}", "AltGr+}} must produce literal }}");
}
#[test]
fn altgr_open_bracket() {
let ev = key(KeyCode::Char('['), KeyModifiers::CONTROL | KeyModifiers::ALT);
let bytes = encode_key_event(&ev).unwrap();
assert_eq!(bytes, b"[", "AltGr+[ must produce literal [");
}
#[test]
fn altgr_close_bracket() {
let ev = key(KeyCode::Char(']'), KeyModifiers::CONTROL | KeyModifiers::ALT);
let bytes = encode_key_event(&ev).unwrap();
assert_eq!(bytes, b"]", "AltGr+] must produce literal ]");
}
#[test]
fn altgr_pipe() {
let ev = key(KeyCode::Char('|'), KeyModifiers::CONTROL | KeyModifiers::ALT);
let bytes = encode_key_event(&ev).unwrap();
assert_eq!(bytes, b"|", "AltGr+| must produce literal |");
}
#[test]
fn altgr_tilde() {
let ev = key(KeyCode::Char('~'), KeyModifiers::CONTROL | KeyModifiers::ALT);
let bytes = encode_key_event(&ev).unwrap();
assert_eq!(bytes, b"~", "AltGr+~ must produce literal ~");
}
#[test]
fn altgr_euro_sign() {
let ev = key(KeyCode::Char('€'), KeyModifiers::CONTROL | KeyModifiers::ALT);
let bytes = encode_key_event(&ev).unwrap();
assert_eq!(bytes, "€".as_bytes(), "AltGr+euro must produce UTF-8 euro sign");
}
#[test]
fn altgr_dollar_czech_layout() {
let ev = key(KeyCode::Char('$'), KeyModifiers::CONTROL | KeyModifiers::ALT);
let bytes = encode_key_event(&ev).unwrap();
assert_eq!(bytes, b"$", "AltGr+$ must produce literal $");
}
#[test]
fn ctrl_alt_a_is_esc_ctrl_a() {
let ev = key(KeyCode::Char('a'), KeyModifiers::CONTROL | KeyModifiers::ALT);
let bytes = encode_key_event(&ev).unwrap();
assert_eq!(bytes, vec![0x1b, 0x01], "Ctrl+Alt+a → ESC + ^A");
}
#[test]
fn ctrl_alt_c_is_esc_ctrl_c() {
let ev = key(KeyCode::Char('c'), KeyModifiers::CONTROL | KeyModifiers::ALT);
let bytes = encode_key_event(&ev).unwrap();
assert_eq!(bytes, vec![0x1b, 0x03], "Ctrl+Alt+c → ESC + ^C");
}
#[test]
fn ctrl_alt_z_is_esc_ctrl_z() {
let ev = key(KeyCode::Char('z'), KeyModifiers::CONTROL | KeyModifiers::ALT);
let bytes = encode_key_event(&ev).unwrap();
assert_eq!(bytes, vec![0x1b, 0x1a], "Ctrl+Alt+z → ESC + ^Z");
}
#[test]
fn plain_char_no_modifiers() {
let ev = key(KeyCode::Char('a'), KeyModifiers::NONE);
let bytes = encode_key_event(&ev).unwrap();
assert_eq!(bytes, b"a");
}
#[test]
fn alt_a_produces_esc_a() {
let ev = key(KeyCode::Char('a'), KeyModifiers::ALT);
let bytes = encode_key_event(&ev).unwrap();
assert_eq!(bytes, b"\x1ba");
}
#[test]
fn ctrl_a_produces_soh() {
let ev = key(KeyCode::Char('a'), KeyModifiers::CONTROL);
let bytes = encode_key_event(&ev).unwrap();
assert_eq!(bytes, vec![0x01]); }
#[test]
fn plain_backslash_no_modifiers() {
let ev = key(KeyCode::Char('\\'), KeyModifiers::NONE);
let bytes = encode_key_event(&ev).unwrap();
assert_eq!(bytes, b"\\");
}
#[test]
fn plain_enter_produces_cr() {
let ev = key(KeyCode::Enter, KeyModifiers::NONE);
let bytes = encode_key_event(&ev).unwrap();
assert_eq!(bytes, b"\r", "plain Enter must produce CR");
}
#[test]
fn shift_enter_produces_correct_encoding() {
let ev = key(KeyCode::Enter, KeyModifiers::SHIFT);
let bytes = encode_key_event(&ev).unwrap();
#[cfg(windows)]
assert_eq!(bytes, b"\x1b\r", "Shift+Enter on Windows must produce ESC+CR for ConPTY");
#[cfg(not(windows))]
assert_eq!(bytes, b"\x1b[13;2~", "Shift+Enter must produce CSI 13;2~");
}
#[test]
fn ctrl_enter_produces_csi_13_5() {
let ev = key(KeyCode::Enter, KeyModifiers::CONTROL);
let bytes = encode_key_event(&ev).unwrap();
assert_eq!(bytes, b"\x1b[13;5~", "Ctrl+Enter must produce CSI 13;5~");
}
#[test]
fn ctrl_shift_enter_produces_csi_13_6() {
let ev = key(KeyCode::Enter, KeyModifiers::CONTROL | KeyModifiers::SHIFT);
let bytes = encode_key_event(&ev).unwrap();
assert_eq!(bytes, b"\x1b[13;6~", "Ctrl+Shift+Enter must produce CSI 13;6~");
}
#[test]
fn alt_enter_produces_correct_encoding() {
let ev = key(KeyCode::Enter, KeyModifiers::ALT);
let bytes = encode_key_event(&ev).unwrap();
#[cfg(windows)]
assert_eq!(bytes, b"\x1b\r", "Alt+Enter on Windows must produce ESC+CR for ConPTY");
#[cfg(not(windows))]
assert_eq!(bytes, b"\x1b[13;3~", "Alt+Enter must produce CSI 13;3~");
}
#[test]
fn parse_shift_enter() {
assert_eq!(parse_modified_special_key("S-Enter"), Some("\x1b[13;2~".to_string()));
}
#[test]
fn parse_ctrl_enter() {
assert_eq!(parse_modified_special_key("C-Enter"), Some("\x1b[13;5~".to_string()));
}
#[test]
fn parse_ctrl_shift_enter() {
assert_eq!(parse_modified_special_key("C-S-Enter"), Some("\x1b[13;6~".to_string()));
}
#[test]
fn parse_plain_enter_returns_none() {
assert_eq!(parse_modified_special_key("enter"), None, "no modifiers should return None");
}
#[test]
fn parse_shift_left_works() {
assert_eq!(parse_modified_special_key("S-Left"), Some("\x1b[1;2D".to_string()));
}
#[test]
fn parse_ctrl_tab_unchanged() {
assert_eq!(parse_modified_special_key("C-Tab"), Some("\x1b[9;5~".to_string()));
}
#[test]
fn parse_ctrl_left_unchanged() {
assert_eq!(parse_modified_special_key("C-Left"), Some("\x1b[1;5D".to_string()));
}
fn capture_paste(text: &[u8], bracket: bool) -> Vec<u8> {
let mut buf: Vec<u8> = Vec::new();
super::write_paste_chunked(&mut buf, text, bracket);
buf
}
#[test]
fn paste_lf_normalized_to_cr() {
let input = b"line1\nline2\nline3";
let output = capture_paste(input, false);
assert_eq!(output, b"line1\rline2\rline3",
"bare LF must be normalized to CR for ConPTY; got {:?}", String::from_utf8_lossy(&output));
}
#[test]
fn paste_crlf_normalized_to_cr() {
let input = b"line1\r\nline2\r\nline3";
let output = capture_paste(input, false);
assert_eq!(output, b"line1\rline2\rline3",
"CRLF must be normalized to CR for ConPTY; got {:?}", String::from_utf8_lossy(&output));
}
#[test]
fn paste_mixed_endings_normalized() {
let input = b"a\nb\r\nc";
let output = capture_paste(input, false);
assert_eq!(output, b"a\rb\rc",
"mixed line endings must all become CR; got {:?}", String::from_utf8_lossy(&output));
}
#[test]
fn paste_no_line_endings_unchanged() {
let input = b"hello world";
let output = capture_paste(input, false);
assert_eq!(output, b"hello world");
}
#[test]
fn paste_bracket_markers_with_normalization() {
let input = b"a\nb";
let output = capture_paste(input, true);
assert_eq!(output, b"\x1b[200~a\rb\x1b[201~",
"bracketed paste must normalize line endings; got {:?}", String::from_utf8_lossy(&output));
}
#[cfg(windows)]
#[test]
fn shift_enter_encoding_for_conpty() {
let ev = key(KeyCode::Enter, KeyModifiers::SHIFT);
let bytes = encode_key_event(&ev).unwrap();
assert_eq!(bytes, b"\x1b\r",
"Shift+Enter on Windows must produce ESC+CR for ConPTY compatibility; got {:?}", bytes);
}
#[cfg(windows)]
#[test]
fn alt_enter_encoding_for_conpty() {
let ev = key(KeyCode::Enter, KeyModifiers::ALT);
let bytes = encode_key_event(&ev).unwrap();
assert_eq!(bytes, b"\x1b\r",
"Alt+Enter on Windows must produce ESC+CR for ConPTY; got {:?}", bytes);
}
#[cfg(windows)]
#[test]
fn augment_enter_shift_noop_when_already_shift() {
use crossterm::event::KeyModifiers;
let mut ev = key(KeyCode::Enter, KeyModifiers::SHIFT);
crate::platform::augment_enter_shift(&mut ev);
assert!(ev.modifiers.contains(KeyModifiers::SHIFT),
"augment_enter_shift must preserve existing SHIFT modifier");
}
#[cfg(windows)]
#[test]
fn augment_enter_shift_ignores_non_enter() {
use crossterm::event::KeyModifiers;
let mut ev = key(KeyCode::Char('a'), KeyModifiers::ALT);
crate::platform::augment_enter_shift(&mut ev);
assert!(ev.modifiers.contains(KeyModifiers::ALT),
"augment_enter_shift must not change non-Enter keys");
assert!(!ev.modifiers.contains(KeyModifiers::SHIFT),
"augment_enter_shift must not add SHIFT to non-Enter keys");
}
#[cfg(windows)]
#[test]
fn shift_enter_no_ctrl_uses_vt_not_csi() {
let ev = key(KeyCode::Enter, KeyModifiers::SHIFT);
let bytes = encode_key_event(&ev).unwrap();
assert_eq!(bytes, b"\x1b\r",
"Shift+Enter (no Ctrl) on Windows must use VT encoding (ESC+CR); got {:?}", bytes);
}
#[cfg(windows)]
#[test]
fn alt_enter_no_ctrl_uses_vt_not_csi() {
let ev = key(KeyCode::Enter, KeyModifiers::ALT);
let bytes = encode_key_event(&ev).unwrap();
assert_eq!(bytes, b"\x1b\r",
"Alt+Enter (no Ctrl) on Windows must use VT encoding (ESC+CR); got {:?}", bytes);
}
#[test]
fn ctrl_enter_uses_csi_encoding() {
let ev = key(KeyCode::Enter, KeyModifiers::CONTROL);
let bytes = encode_key_event(&ev).unwrap();
assert_eq!(bytes, b"\x1b[13;5~",
"Ctrl+Enter must use CSI encoding; got {:?}", bytes);
}
#[test]
fn ctrl_shift_enter_vt_encoding_works() {
let ev = key(KeyCode::Enter, KeyModifiers::CONTROL | KeyModifiers::SHIFT);
let bytes = encode_key_event(&ev).unwrap();
assert_eq!(bytes, b"\x1b[13;6~",
"Ctrl+Shift+Enter VT encoding must be CSI 13;6~");
}
#[test]
fn ctrl_alt_enter_vt_encoding_works() {
let ev = key(KeyCode::Enter, KeyModifiers::CONTROL | KeyModifiers::ALT);
let bytes = encode_key_event(&ev).unwrap();
assert_eq!(bytes, b"\x1b[13;7~",
"Ctrl+Alt+Enter VT encoding must be CSI 13;7~");
}
#[test]
fn shift_alt_enter_on_non_windows_produces_csi() {
let ev = key(KeyCode::Enter, KeyModifiers::SHIFT | KeyModifiers::ALT);
let bytes = encode_key_event(&ev).unwrap();
#[cfg(windows)]
assert_eq!(bytes, b"\x1b\r", "Shift+Alt+Enter on Windows → ESC+CR");
#[cfg(not(windows))]
assert_eq!(bytes, b"\x1b[13;4~", "Shift+Alt+Enter on non-Windows → CSI 13;4~");
}
#[cfg(windows)]
#[test]
fn bug3_double_delivery_prevention() {
let shift_enter = key(KeyCode::Enter, KeyModifiers::SHIFT);
let alt_enter = key(KeyCode::Enter, KeyModifiers::ALT);
let ctrl_enter = key(KeyCode::Enter, KeyModifiers::CONTROL);
let shift_bytes = encode_key_event(&shift_enter).unwrap();
let alt_bytes = encode_key_event(&alt_enter).unwrap();
let ctrl_bytes = encode_key_event(&ctrl_enter).unwrap();
assert_eq!(shift_bytes, b"\x1b\r");
assert_eq!(alt_bytes, b"\x1b\r");
assert_eq!(ctrl_bytes, b"\x1b[13;5~");
assert!(!shift_enter.modifiers.contains(KeyModifiers::CONTROL),
"Shift+Enter must NOT trigger the ctrl guard (no native injection)");
assert!(!alt_enter.modifiers.contains(KeyModifiers::CONTROL),
"Alt+Enter must NOT trigger the ctrl guard (no native injection)");
assert!(ctrl_enter.modifiers.contains(KeyModifiers::CONTROL),
"Ctrl+Enter MUST trigger the ctrl guard (native injection allowed)");
}
fn two_pane_h_rects() -> Vec<(Vec<usize>, ratatui::layout::Rect)> {
use ratatui::layout::Rect;
vec![
(vec![0], Rect { x: 0, y: 0, width: 40, height: 24 }), (vec![1], Rect { x: 40, y: 0, width: 40, height: 24 }), ]
}
#[test]
fn issue134_wrap_right_from_rightmost_pane() {
let rects = two_pane_h_rects();
let ai = 1; let arect = &rects[ai].1;
let direct = find_best_pane_in_direction(
&rects, ai, arect, crate::types::FocusDir::Right, &[], &[],
);
assert!(direct.is_none(), "rightmost pane should have no direct Right neighbor");
let wrap = find_wrap_target(
&rects, ai, arect, crate::types::FocusDir::Right, &[], &[],
);
assert_eq!(wrap, Some(0), "wrap Right from rightmost should reach leftmost (index 0)");
}
#[test]
fn issue134_wrap_left_from_leftmost_pane() {
let rects = two_pane_h_rects();
let ai = 0; let arect = &rects[ai].1;
let direct = find_best_pane_in_direction(
&rects, ai, arect, crate::types::FocusDir::Left, &[], &[],
);
assert!(direct.is_none(), "leftmost pane should have no direct Left neighbor");
let wrap = find_wrap_target(
&rects, ai, arect, crate::types::FocusDir::Left, &[], &[],
);
assert_eq!(wrap, Some(1), "wrap Left from leftmost should reach rightmost (index 1)");
}
#[test]
fn issue134_direct_neighbor_takes_priority_over_wrap() {
let rects = two_pane_h_rects();
let ai = 0;
let arect = &rects[ai].1;
let direct = find_best_pane_in_direction(
&rects, ai, arect, crate::types::FocusDir::Right, &[], &[],
);
assert_eq!(direct, Some(1), "left pane should have direct Right neighbor (right pane)");
}
fn three_pane_h_rects() -> Vec<(Vec<usize>, ratatui::layout::Rect)> {
use ratatui::layout::Rect;
vec![
(vec![0], Rect { x: 0, y: 0, width: 60, height: 30 }), (vec![1], Rect { x: 61, y: 0, width: 29, height: 30 }), (vec![2], Rect { x: 91, y: 0, width: 30, height: 30 }), ]
}
#[test]
fn issue141_wrap_up_single_row_stays_on_self() {
let rects = three_pane_h_rects();
let ai = 1; let arect = &rects[ai].1;
let direct = find_best_pane_in_direction(
&rects, ai, arect, crate::types::FocusDir::Up, &[], &[],
);
assert!(direct.is_none(), "no pane above center in single row");
let wrap = find_wrap_target(
&rects, ai, arect, crate::types::FocusDir::Up, &[], &[],
);
assert!(wrap.is_none(), "wrap Up in single row must not jump columns (issue #141)");
}
#[test]
fn issue141_wrap_down_single_row_stays_on_self() {
let rects = three_pane_h_rects();
let ai = 1;
let arect = &rects[ai].1;
let direct = find_best_pane_in_direction(
&rects, ai, arect, crate::types::FocusDir::Down, &[], &[],
);
assert!(direct.is_none(), "no pane below center in single row");
let wrap = find_wrap_target(
&rects, ai, arect, crate::types::FocusDir::Down, &[], &[],
);
assert!(wrap.is_none(), "wrap Down in single row must not jump columns (issue #141)");
}
fn three_pane_v_rects() -> Vec<(Vec<usize>, ratatui::layout::Rect)> {
use ratatui::layout::Rect;
vec![
(vec![0], Rect { x: 0, y: 0, width: 80, height: 10 }), (vec![1], Rect { x: 0, y: 11, width: 80, height: 10 }), (vec![2], Rect { x: 0, y: 22, width: 80, height: 10 }), ]
}
#[test]
fn issue141_wrap_left_single_column_stays_on_self() {
let rects = three_pane_v_rects();
let ai = 1;
let arect = &rects[ai].1;
let direct = find_best_pane_in_direction(
&rects, ai, arect, crate::types::FocusDir::Left, &[], &[],
);
assert!(direct.is_none(), "no pane left of middle in single column");
let wrap = find_wrap_target(
&rects, ai, arect, crate::types::FocusDir::Left, &[], &[],
);
assert!(wrap.is_none(), "wrap Left in single column must not jump rows (issue #141)");
}
#[test]
fn issue141_wrap_right_single_column_stays_on_self() {
let rects = three_pane_v_rects();
let ai = 1;
let arect = &rects[ai].1;
let direct = find_best_pane_in_direction(
&rects, ai, arect, crate::types::FocusDir::Right, &[], &[],
);
assert!(direct.is_none(), "no pane right of middle in single column");
let wrap = find_wrap_target(
&rects, ai, arect, crate::types::FocusDir::Right, &[], &[],
);
assert!(wrap.is_none(), "wrap Right in single column must not jump rows (issue #141)");
}
#[test]
fn issue141_wrap_up_still_works_with_column_overlap() {
use ratatui::layout::Rect;
let rects: Vec<(Vec<usize>, Rect)> = vec![
(vec![0], Rect { x: 0, y: 0, width: 80, height: 12 }),
(vec![1], Rect { x: 0, y: 13, width: 80, height: 12 }),
];
let ai = 0; let arect = &rects[ai].1;
let wrap = find_wrap_target(
&rects, ai, arect, crate::types::FocusDir::Up, &[], &[],
);
assert_eq!(wrap, Some(1), "wrap Up from top should reach bottom when they share a column");
}