psmux 3.3.2

Terminal multiplexer for Windows - tmux alternative for PowerShell and Windows Terminal
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
use super::*;
use crossterm::event::{KeyCode, KeyEvent, KeyEventKind, KeyEventState, KeyModifiers};

/// Helper: build a KeyEvent with the given code and modifiers.
fn key(code: KeyCode, modifiers: KeyModifiers) -> KeyEvent {
    KeyEvent {
        code,
        modifiers,
        kind: KeyEventKind::Press,
        state: KeyEventState::NONE,
    }
}

// ── AltGr characters (Ctrl+Alt on Windows) should be forwarded verbatim ──

#[test]
fn altgr_backslash_german_layout() {
    // German: AltGr+ß → '\'   reported as Ctrl+Alt+'\'
    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() {
    // German: AltGr+Q → '@'   reported as Ctrl+Alt+'@'
    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() {
    // German: AltGr+7 → '{'   reported as Ctrl+Alt+'{'
    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() {
    // German: AltGr+0 → '}'
    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() {
    // German: AltGr+8 → '['
    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() {
    // German: AltGr+9 → ']'
    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() {
    // German: AltGr+< → '|'
    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() {
    // German: AltGr++ → '~'
    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() {
    // German: AltGr+E → '€'   (multi-byte UTF-8)
    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() {
    // Czech: AltGr produces '$'
    let ev = key(KeyCode::Char('$'), KeyModifiers::CONTROL | KeyModifiers::ALT);
    let bytes = encode_key_event(&ev).unwrap();
    assert_eq!(bytes, b"$", "AltGr+$ must produce literal $");
}

// ── Genuine Ctrl+Alt+letter must still produce ESC + ctrl-char ──

#[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");
}

// ── Plain characters / other modifier combos (regression checks) ──

#[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]); // ^A = SOH
}

#[test]
fn plain_backslash_no_modifiers() {
    let ev = key(KeyCode::Char('\\'), KeyModifiers::NONE);
    let bytes = encode_key_event(&ev).unwrap();
    assert_eq!(bytes, b"\\");
}

// ── Modified Enter key tests (PR #115) ──

#[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~");
}

// ── parse_modified_special_key tests (PR #115) ──

#[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() {
    // Regression: S-Left was broken because m started at 1 and S- did m|=1 (no-op)
    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()));
}

// ── PR #131: paste line-ending normalization tests ──

/// Helper: capture what write_paste_chunked writes to a Vec<u8>.
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() {
    // Multi-line paste with LF line endings should produce 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() {
    // Multi-line paste with CRLF line endings should produce CR (not CRLF)
    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() {
    // Mixed: some lines LF, some CRLF
    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() {
    // Text without newlines should pass through 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() {
    // Bracketed paste should still wrap with markers AND normalize
    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));
}

// ── PR #132: Shift+Enter ConPTY encoding tests ──

#[cfg(windows)]
#[test]
fn shift_enter_encoding_for_conpty() {
    // On Windows, Shift+Enter should produce \x1b\r (ESC+CR) instead of
    // \x1b[13;2~ which ConPTY drops (code 13 is non-standard).
    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() {
    // Alt+Enter should also produce \x1b\r on Windows
    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);
}

// ── Issue #121 (whil0012 follow-up): PSReadLine Shift+Enter via native injection ──

/// augment_enter_shift must remap Alt+Enter → Shift+Enter when physical Shift
/// is held (ConPTY misreports Shift+Enter as Alt+Enter).
#[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");
}

/// Issue #121 follow-up Bug #3: Shift/Alt+Enter (no Ctrl) must use VT encoding
/// only; Ctrl combos should still use CSI encoding (and native injection in the
/// live code path).  Verify encode_key_event produces the right sequences.
#[cfg(windows)]
#[test]
fn shift_enter_no_ctrl_uses_vt_not_csi() {
    // Shift+Enter → \x1b\r (VT), NOT \x1b[13;2~ (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() {
    // Alt+Enter → \x1b\r (VT), NOT \x1b[13;3~ (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() {
    // Ctrl+Enter → CSI 13;5~ (must use CSI, not ESC+CR)
    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);
}

/// VT fallback encoding for modified Enter still works (encode_key_event path).
#[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() {
    // On non-Windows, Shift+Alt+Enter should use CSI encoding
    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~");
}

/// Issue #121 Bug #3 double-delivery proof: verify that VT-encoded Shift+Enter
/// is distinct from plain CR (which is what native WriteConsoleInputW injection
/// produces after ConPTY translation).  Before the fix, forward_key_to_active
/// sent BOTH \x1b\r (VT) and a native VK_RETURN injection for Shift+Enter,
/// causing the child process to receive two Enter events.  After the fix,
/// only VT encoding is used for Shift/Alt+Enter (no Ctrl), preventing double
/// delivery.  Ctrl+Enter still uses native injection (with CSI fallback).
#[cfg(windows)]
#[test]
fn bug3_double_delivery_prevention() {
    // Native injection produces a KEY_EVENT_RECORD → ConPTY translates to \r.
    // VT encoding for Shift+Enter is \x1b\r (ESC + CR).
    // If both paths fire, child sees: ESC + CR (VT) + CR (native) = 2 Enters.
    // The fix ensures only ONE path fires for each modifier combination.

    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();

    // VT path (Shift/Alt+Enter): produces \x1b\r
    assert_eq!(shift_bytes, b"\x1b\r");
    assert_eq!(alt_bytes, b"\x1b\r");

    // Native injection path (Ctrl+Enter): produces CSI sequence
    // In the live code, forward_key_to_active only calls
    // send_modified_enter_event when ctrl==true.  This CSI encoding
    // is the FALLBACK when native injection fails.
    assert_eq!(ctrl_bytes, b"\x1b[13;5~");

    // The critical guard in forward_key_to_active:
    //   let ctrl = key.modifiers.contains(KeyModifiers::CONTROL);
    //   if ctrl { /* native injection */ }
    //   // else: fall through to encode_key_event (VT)
    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)");
}

// ── Issue #134: wrapped directional navigation geometry tests ──

/// Build a two-pane horizontal layout (left | right) for geometry tests.
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 }), // left
        (vec![1], Rect { x: 40, y: 0, width: 40, height: 24 }), // right
    ]
}

#[test]
fn issue134_wrap_right_from_rightmost_pane() {
    // From the rightmost pane (index 1), going Right should find no direct
    // neighbor but find a wrap target (the leftmost pane, index 0).
    let rects = two_pane_h_rects();
    let ai = 1; // rightmost pane
    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; // leftmost pane
    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() {
    // From left pane (index 0), going Right should find a direct neighbor (index 1),
    // ensuring wrap is NOT used when a direct neighbor exists.
    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)");
}

// ── Issue #141: wrapped nav must not jump columns/rows ──

/// Build a three-pane horizontal layout (left | center | right) for issue #141.
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 }), // %1 left
        (vec![1], Rect { x: 61, y: 0, width: 29, height: 30 }), // %2 center
        (vec![2], Rect { x: 91, y: 0, width: 30, height: 30 }), // %3 right
    ]
}

#[test]
fn issue141_wrap_up_single_row_stays_on_self() {
    // Three panes in a single row. From %2 (center), select-pane -U should
    // not jump to %3 or %1. There is no pane above or below, so wrapping
    // should return None (stay on the current pane).
    let rects = three_pane_h_rects();
    let ai = 1; // center pane
    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)");
}

/// Build a three-pane vertical layout (top / middle / bottom) for 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 }), // top
        (vec![1], Rect { x: 0, y: 11, width: 80, height: 10 }), // middle
        (vec![2], Rect { x: 0, y: 22, width: 80, height: 10 }), // bottom
    ]
}

#[test]
fn issue141_wrap_left_single_column_stays_on_self() {
    // Three panes stacked vertically. From middle, select-pane -L should
    // stay on self since there are no panes to the left or right.
    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() {
    // Two panes stacked vertically. Wrap Up from bottom should still reach top
    // because they overlap on the perpendicular (x) axis.
    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; // top pane
    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");
}