rmux-server 0.1.2

Tokio daemon and request dispatcher for the RMUX terminal multiplexer.
Documentation
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
use super::*;

const ORACLE_YANK_BYTES: &[u8] = b"alpha ";
const ORACLE_OLD_BUFFER_BYTES: &[u8] = b"OLD";
const ORACLE_SINGLE_CELL_BYTES: &[u8] = b"a";
const ORACLE_MULTILINE_BYTES: &[u8] = b"alpha beta gamma\nsecond";

async fn set_vi_mode_keys(handler: &RequestHandler, session: &SessionName) {
    assert!(matches!(
        handler
            .handle(Request::SetOption(SetOptionRequest {
                scope: ScopeSelector::Window(WindowTarget::with_window(session.clone(), 0)),
                option: OptionName::ModeKeys,
                value: "vi".to_owned(),
                mode: SetOptionMode::Replace,
            }))
            .await,
        Response::SetOption(_)
    ));
}

async fn enter_copy_mode_with_selection_seed(
    handler: &RequestHandler,
    target: &PaneTarget,
) -> String {
    replace_transcript_contents(
        handler,
        target,
        TerminalSize { cols: 80, rows: 24 },
        b"alpha beta gamma\r\nsecond beta line\r\nthird alpha marker\r\nfourth delta marker\r\nfifth beta tail\r\n\x1b[1;1H",
    )
    .await;
    assert!(matches!(
        handler
            .handle(Request::CopyMode(CopyModeRequest {
                target: Some(target.clone()),
                page_down: false,
                exit_on_scroll: false,
                hide_position: false,
                mouse_drag_start: false,
                cancel_mode: false,
                scrollbar_scroll: false,
                source: None,
                page_up: false,
            }))
            .await,
        Response::CopyMode(_)
    ));
    copy_selection_status(handler, target.clone()).await
}

async fn copy_selection_status(handler: &RequestHandler, target: PaneTarget) -> String {
    display_target_format(
        handler,
        target,
        "#{pane_in_mode}:#{copy_cursor_x},#{copy_cursor_y}:#{selection_present}:#{selection_active}:#{selection_mode}:#{selection_start_x},#{selection_start_y}:#{selection_end_x},#{selection_end_y}",
    )
    .await
}

async fn send_copy_selection_key(
    handler: &RequestHandler,
    requester_pid: u32,
    pending_input: &mut Vec<u8>,
    bytes: &[u8],
) {
    let forwarded_to_pane = handler
        .handle_attached_live_input_inner(requester_pid, pending_input, bytes)
        .await
        .expect("copy-mode selection input");
    assert!(
        !forwarded_to_pane,
        "copy-mode selection/yank keys must be consumed instead of forwarded to pane IO"
    );
    assert!(
        pending_input.is_empty(),
        "copy-mode selection/yank input should fully decode and leave no pending bytes"
    );
}

async fn show_top_buffer_bytes(handler: &RequestHandler) -> Vec<u8> {
    let response = handler
        .handle(Request::ShowBuffer(rmux_proto::ShowBufferRequest {
            name: None,
        }))
        .await;
    let Response::ShowBuffer(response) = response else {
        panic!("expected show-buffer response, got {response:?}");
    };
    response.command_output().stdout().to_vec()
}

async fn set_top_buffer_bytes(handler: &RequestHandler, bytes: &[u8]) {
    assert!(matches!(
        handler
            .handle(Request::SetBuffer(rmux_proto::SetBufferRequest {
                name: None,
                content: bytes.to_vec(),
                append: false,
                new_name: None,
                set_clipboard: false,
            }))
            .await,
        Response::SetBuffer(_)
    ));
}

async fn enter_vi_selection_yank_fixture(
    handler: &RequestHandler,
    requester_pid: u32,
    session: &SessionName,
    target: &PaneTarget,
) -> (Vec<u8>, String) {
    set_vi_mode_keys(handler, session).await;
    assert_eq!(
        enter_copy_mode_with_selection_seed(handler, target).await,
        "1:0,0:0:0::,:,\n"
    );
    let before_capture = capture_pane_print(handler, target.clone()).await;
    let mut pending_input = Vec::new();

    send_copy_selection_key(handler, requester_pid, &mut pending_input, b" ").await;
    assert_eq!(
        copy_selection_status(handler, target.clone()).await,
        "1:0,0:1:1:char:0,0:0,0\n"
    );

    for expected_x in 1..=5 {
        send_copy_selection_key(handler, requester_pid, &mut pending_input, b"\x1b[C").await;
        assert_eq!(
            copy_selection_status(handler, target.clone()).await,
            format!("1:{expected_x},0:1:1:char:0,0:{expected_x},0\n")
        );
    }

    (pending_input, before_capture)
}

#[tokio::test]
async fn vi_copy_mode_selection_begin_marks_anchor_without_pane_leak() {
    let handler = RequestHandler::new();
    let requester_pid = std::process::id();
    let alpha = session_name("alpha");
    let _control_rx = create_quiet_attached_session(&handler, requester_pid, &alpha).await;
    let target = PaneTarget::new(alpha.clone(), 0);

    let (_pending_input, before_capture) =
        enter_vi_selection_yank_fixture(&handler, requester_pid, &alpha, &target).await;

    assert_eq!(
        capture_pane_print(&handler, target).await,
        before_capture,
        "selection begin and motion keys must not mutate the pane screen"
    );
}

#[tokio::test]
async fn vi_copy_mode_selection_yank_writes_internal_buffer_matching_tmux() {
    let handler = RequestHandler::new();
    let requester_pid = std::process::id();
    let alpha = session_name("alpha");
    let _control_rx = create_quiet_attached_session(&handler, requester_pid, &alpha).await;
    let target = PaneTarget::new(alpha.clone(), 0);

    let (mut pending_input, before_capture) =
        enter_vi_selection_yank_fixture(&handler, requester_pid, &alpha, &target).await;

    send_copy_selection_key(&handler, requester_pid, &mut pending_input, b"\r").await;
    assert_eq!(
        copy_selection_status(&handler, target.clone()).await,
        "0:,::::,:,\n",
        "vi Enter must copy the selection and exit copy-mode like tmux"
    );
    assert_eq!(
        show_top_buffer_bytes(&handler).await,
        ORACLE_YANK_BYTES,
        "RMUX internal buffer must match tmux save-buffer bytes exactly"
    );
    assert_eq!(
        capture_pane_print(&handler, target.clone()).await,
        before_capture,
        "selection/yank keys must not reach or mutate pane IO"
    );

    let forwarded_to_pane = handler
        .handle_attached_live_input_inner(
            requester_pid,
            &mut pending_input,
            b"RMUX_AFTER_COPY_SELECTION_YANK",
        )
        .await
        .expect("normal input resumes after copy-mode yank");
    assert!(
        forwarded_to_pane,
        "normal pane input should resume after copy-mode yank exits"
    );
}

#[tokio::test]
async fn copy_mode_selection_yank_does_not_depend_on_search() {
    let handler = RequestHandler::new();
    let requester_pid = std::process::id();
    let alpha = session_name("alpha");
    let _control_rx = create_quiet_attached_session(&handler, requester_pid, &alpha).await;
    let target = PaneTarget::new(alpha.clone(), 0);

    let (mut pending_input, _before_capture) =
        enter_vi_selection_yank_fixture(&handler, requester_pid, &alpha, &target).await;
    assert_eq!(
        copy_selection_status(&handler, target.clone()).await,
        "1:5,0:1:1:char:0,0:5,0\n",
        "the W3C slice positions by motion only before yanking"
    );

    send_copy_selection_key(&handler, requester_pid, &mut pending_input, b"\r").await;
    assert_eq!(show_top_buffer_bytes(&handler).await, ORACLE_YANK_BYTES);
}

#[tokio::test]
async fn copy_mode_vi_escape_clears_active_selection_without_exiting_or_leaking() {
    let handler = RequestHandler::new();
    let requester_pid = std::process::id();
    let alpha = session_name("alpha");
    let _control_rx = create_quiet_attached_session(&handler, requester_pid, &alpha).await;
    let target = PaneTarget::new(alpha.clone(), 0);

    set_top_buffer_bytes(&handler, ORACLE_OLD_BUFFER_BYTES).await;
    let (mut pending_input, _before_capture) =
        enter_vi_selection_yank_fixture(&handler, requester_pid, &alpha, &target).await;

    let forwarded_to_pane = handler
        .handle_attached_live_input_inner(requester_pid, &mut pending_input, b"\x1b")
        .await
        .expect("Escape clears active vi selection");
    assert!(
        !forwarded_to_pane,
        "Escape must be consumed by copy-mode instead of reaching pane IO"
    );
    assert!(
        pending_input.is_empty(),
        "Escape should fully decode and leave no pending input"
    );
    assert_eq!(
        copy_selection_status(&handler, target.clone()).await,
        "1:5,0:0:0::,:,\n",
        "tmux clears active vi selection on Escape but keeps copy-mode active"
    );
    assert_eq!(
        show_top_buffer_bytes(&handler).await,
        ORACLE_OLD_BUFFER_BYTES,
        "selection cancel must not mutate the top buffer"
    );

    send_copy_selection_key(&handler, requester_pid, &mut pending_input, b"q").await;
    assert_eq!(pane_mode_status(&handler, &alpha).await, "0:::\n");
    let forwarded_to_pane = handler
        .handle_attached_live_input_inner(
            requester_pid,
            &mut pending_input,
            b"RMUX_AFTER_COPY_SELECTION_CANCEL_ESCAPE",
        )
        .await
        .expect("normal input resumes after Escape then q");
    assert!(
        forwarded_to_pane,
        "normal pane input should resume after Escape clears selection and q exits"
    );
}

#[tokio::test]
async fn copy_mode_vi_q_exits_active_selection_without_leak_or_buffer_change() {
    let handler = RequestHandler::new();
    let requester_pid = std::process::id();
    let alpha = session_name("alpha");
    let _control_rx = create_quiet_attached_session(&handler, requester_pid, &alpha).await;
    let target = PaneTarget::new(alpha.clone(), 0);

    set_top_buffer_bytes(&handler, ORACLE_OLD_BUFFER_BYTES).await;
    let (mut pending_input, before_capture) =
        enter_vi_selection_yank_fixture(&handler, requester_pid, &alpha, &target).await;

    send_copy_selection_key(&handler, requester_pid, &mut pending_input, b"q").await;
    assert_eq!(
        copy_selection_status(&handler, target.clone()).await,
        "0:,::::,:,\n",
        "tmux exits copy-mode on q even when a vi selection is active"
    );
    assert_eq!(
        show_top_buffer_bytes(&handler).await,
        ORACLE_OLD_BUFFER_BYTES,
        "q cancel must leave the existing buffer unchanged"
    );
    assert_eq!(
        capture_pane_print(&handler, target.clone()).await,
        before_capture,
        "q must be consumed by copy-mode instead of reaching pane IO"
    );

    let forwarded_to_pane = handler
        .handle_attached_live_input_inner(
            requester_pid,
            &mut pending_input,
            b"RMUX_AFTER_COPY_SELECTION_CANCEL_Q",
        )
        .await
        .expect("normal input resumes after q");
    assert!(
        forwarded_to_pane,
        "normal pane input should resume after q exits copy-mode"
    );
}

#[tokio::test]
async fn copy_mode_emacs_escape_exits_active_selection_without_leak() {
    let handler = RequestHandler::new();
    let requester_pid = std::process::id();
    let alpha = session_name("alpha");
    let _control_rx = create_quiet_attached_session(&handler, requester_pid, &alpha).await;
    let target = PaneTarget::new(alpha.clone(), 0);

    set_top_buffer_bytes(&handler, ORACLE_OLD_BUFFER_BYTES).await;
    assert_eq!(
        enter_copy_mode_with_selection_seed(&handler, &target).await,
        "1:0,0:0:0::,:,\n"
    );
    let before_capture = capture_pane_print(&handler, target.clone()).await;
    let mut pending_input = Vec::new();

    send_copy_selection_key(&handler, requester_pid, &mut pending_input, b" ").await;
    send_copy_selection_key(&handler, requester_pid, &mut pending_input, b"\x1b[C").await;
    assert_eq!(
        copy_selection_status(&handler, target.clone()).await,
        "1:1,0:1:1:char:0,0:1,0\n",
        "test setup must have an active emacs selection before Escape"
    );

    handler
        .handle_attached_live_input_for_test(requester_pid, b"\x1b")
        .await
        .expect("Escape exits emacs copy-mode even with an active selection");
    assert_eq!(
        copy_selection_status(&handler, target.clone()).await,
        "0:,::::,:,\n",
        "tmux emacs exits copy-mode on Escape even when a selection is active"
    );
    assert_eq!(
        show_top_buffer_bytes(&handler).await,
        ORACLE_OLD_BUFFER_BYTES,
        "Escape cancel must leave the existing buffer unchanged"
    );
    assert_eq!(
        capture_pane_print(&handler, target.clone()).await,
        before_capture,
        "Escape must be consumed by emacs copy-mode instead of reaching pane IO"
    );

    let forwarded_to_pane = handler
        .handle_attached_live_input_inner(
            requester_pid,
            &mut pending_input,
            b"RMUX_AFTER_EMACS_COPY_SELECTION_ESCAPE",
        )
        .await
        .expect("normal input resumes after emacs Escape");
    assert!(
        forwarded_to_pane,
        "normal pane input should resume after emacs Escape exits copy-mode"
    );
}

#[tokio::test]
async fn copy_mode_vi_single_cell_yank_matches_tmux_buffer() {
    let handler = RequestHandler::new();
    let requester_pid = std::process::id();
    let alpha = session_name("alpha");
    let _control_rx = create_quiet_attached_session(&handler, requester_pid, &alpha).await;
    let target = PaneTarget::new(alpha.clone(), 0);

    set_vi_mode_keys(&handler, &alpha).await;
    assert_eq!(
        enter_copy_mode_with_selection_seed(&handler, &target).await,
        "1:0,0:0:0::,:,\n"
    );
    let before_capture = capture_pane_print(&handler, target.clone()).await;
    let mut pending_input = Vec::new();

    send_copy_selection_key(&handler, requester_pid, &mut pending_input, b" ").await;
    assert_eq!(
        copy_selection_status(&handler, target.clone()).await,
        "1:0,0:1:1:char:0,0:0,0\n"
    );
    send_copy_selection_key(&handler, requester_pid, &mut pending_input, b"\r").await;
    assert_eq!(pane_mode_status(&handler, &alpha).await, "0:::\n");
    assert_eq!(
        show_top_buffer_bytes(&handler).await,
        ORACLE_SINGLE_CELL_BYTES,
        "tmux yanks the cursor cell for a same-anchor vi selection"
    );
    assert_eq!(
        capture_pane_print(&handler, target.clone()).await,
        before_capture,
        "single-cell selection/yank keys must not reach pane IO"
    );
    let forwarded_to_pane = handler
        .handle_attached_live_input_inner(
            requester_pid,
            &mut pending_input,
            b"RMUX_AFTER_COPY_SELECTION_SINGLE_CELL",
        )
        .await
        .expect("normal input resumes after single-cell yank");
    assert!(
        forwarded_to_pane,
        "normal pane input should resume after single-cell yank exits"
    );
}

#[tokio::test]
async fn copy_mode_vi_empty_yank_keeps_existing_buffer_like_tmux() {
    let handler = RequestHandler::new();
    let requester_pid = std::process::id();
    let alpha = session_name("alpha");
    let _control_rx = create_quiet_attached_session(&handler, requester_pid, &alpha).await;
    let target = PaneTarget::new(alpha.clone(), 0);

    set_top_buffer_bytes(&handler, ORACLE_OLD_BUFFER_BYTES).await;
    set_vi_mode_keys(&handler, &alpha).await;
    assert_eq!(
        enter_copy_mode_with_selection_seed(&handler, &target).await,
        "1:0,0:0:0::,:,\n"
    );
    let before_capture = capture_pane_print(&handler, target.clone()).await;
    let mut pending_input = Vec::new();

    send_copy_selection_key(&handler, requester_pid, &mut pending_input, b"\r").await;
    assert_eq!(pane_mode_status(&handler, &alpha).await, "0:::\n");
    assert_eq!(
        show_top_buffer_bytes(&handler).await,
        ORACLE_OLD_BUFFER_BYTES,
        "tmux leaves the previous buffer unchanged when vi Enter yanks no selection"
    );
    assert_eq!(
        capture_pane_print(&handler, target.clone()).await,
        before_capture,
        "empty-yank Enter must not leak to the pane"
    );
    let forwarded_to_pane = handler
        .handle_attached_live_input_inner(
            requester_pid,
            &mut pending_input,
            b"RMUX_AFTER_COPY_SELECTION_EMPTY_YANK",
        )
        .await
        .expect("normal input resumes after empty yank");
    assert!(
        forwarded_to_pane,
        "normal pane input should resume after empty yank exits"
    );
}

#[tokio::test]
async fn copy_mode_vi_multiline_short_yank_matches_tmux_bytes() {
    let handler = RequestHandler::new();
    let requester_pid = std::process::id();
    let alpha = session_name("alpha");
    let _control_rx = create_quiet_attached_session(&handler, requester_pid, &alpha).await;
    let target = PaneTarget::new(alpha.clone(), 0);

    set_vi_mode_keys(&handler, &alpha).await;
    assert_eq!(
        enter_copy_mode_with_selection_seed(&handler, &target).await,
        "1:0,0:0:0::,:,\n"
    );
    let before_capture = capture_pane_print(&handler, target.clone()).await;
    let mut pending_input = Vec::new();

    send_copy_selection_key(&handler, requester_pid, &mut pending_input, b" ").await;
    send_copy_selection_key(&handler, requester_pid, &mut pending_input, b"\x1b[B").await;
    for expected_x in 1..=5 {
        send_copy_selection_key(&handler, requester_pid, &mut pending_input, b"\x1b[C").await;
        assert_eq!(
            copy_selection_status(&handler, target.clone()).await,
            format!("1:{expected_x},1:1:1:char:0,0:{expected_x},1\n")
        );
    }

    send_copy_selection_key(&handler, requester_pid, &mut pending_input, b"\r").await;
    assert_eq!(pane_mode_status(&handler, &alpha).await, "0:::\n");
    assert_eq!(
        show_top_buffer_bytes(&handler).await,
        ORACLE_MULTILINE_BYTES,
        "short visible multi-line vi selection must match tmux bytes exactly"
    );
    assert_eq!(
        capture_pane_print(&handler, target.clone()).await,
        before_capture,
        "multi-line selection/yank keys must not reach pane IO"
    );
    let forwarded_to_pane = handler
        .handle_attached_live_input_inner(
            requester_pid,
            &mut pending_input,
            b"RMUX_AFTER_COPY_SELECTION_MULTILINE_SHORT",
        )
        .await
        .expect("normal input resumes after multi-line yank");
    assert!(
        forwarded_to_pane,
        "normal pane input should resume after multi-line yank exits"
    );
}