rmux-server 0.1.2

Tokio daemon and request dispatcher for the RMUX terminal multiplexer.
Documentation
use super::*;

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_search_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 beta marker\r\nfifth beta tail\r\n",
    )
    .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_search_status(handler, target.clone()).await
}

async fn copy_search_status(handler: &RequestHandler, target: PaneTarget) -> String {
    display_target_format(
        handler,
        target,
        "#{pane_in_mode}:#{copy_cursor_x},#{copy_cursor_y}:#{search_match}",
    )
    .await
}

async fn send_copy_search_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 search input");
    assert!(
        !forwarded_to_pane,
        "copy-mode search keys must be consumed instead of forwarded to pane IO"
    );
    assert!(
        pending_input.is_empty(),
        "copy-mode search input should fully decode and leave no pending bytes"
    );
}

#[tokio::test]
async fn copy_mode_search_prompt_consumes_query_without_pane_leak() {
    let handler = RequestHandler::new();
    let requester_pid = std::process::id();
    let alpha = session_name("alpha");
    let mut 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_search_seed(&handler, &target).await,
        "1:0,5:\n"
    );
    drain_attach_controls(&mut control_rx);
    let before_capture = capture_pane_print(&handler, target.clone()).await;

    let mut pending_input = Vec::new();
    send_copy_search_key(&handler, requester_pid, &mut pending_input, b"/").await;
    let prompt = handler
        .attached_prompt_render(requester_pid)
        .await
        .expect("vi slash opens a copy-mode search prompt");
    assert!(
        prompt.prompt.contains("(search down)"),
        "copy-mode search prompt must be distinct from the shell prompt, got {prompt:?}"
    );
    drain_attach_controls(&mut control_rx);

    send_copy_search_key(&handler, requester_pid, &mut pending_input, b"beta\r").await;

    assert_eq!(
        capture_pane_print(&handler, target).await,
        before_capture,
        "copy-mode search query bytes must not mutate the pane screen"
    );
}

#[tokio::test]
async fn copy_mode_question_search_prompt_consumes_query_without_pane_leak() {
    let handler = RequestHandler::new();
    let requester_pid = std::process::id();
    let alpha = session_name("alpha");
    let mut 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_search_seed(&handler, &target).await,
        "1:0,5:\n"
    );
    drain_attach_controls(&mut control_rx);
    let before_capture = capture_pane_print(&handler, target.clone()).await;

    let mut pending_input = Vec::new();
    send_copy_search_key(&handler, requester_pid, &mut pending_input, b"?").await;
    let prompt = handler
        .attached_prompt_render(requester_pid)
        .await
        .expect("vi question mark opens a copy-mode search prompt");
    assert!(
        prompt.prompt.contains("(search up)"),
        "copy-mode backward search prompt must be distinct from the shell prompt, got {prompt:?}"
    );
    drain_attach_controls(&mut control_rx);

    send_copy_search_key(&handler, requester_pid, &mut pending_input, b"beta\r").await;
    tokio::task::yield_now().await;
    assert_eq!(
        copy_search_status(&handler, target.clone()).await,
        "1:6,4:beta\n",
        "primary ? search must land on the previous beta match from the copy cursor"
    );

    assert_eq!(
        capture_pane_print(&handler, target).await,
        before_capture,
        "? search query bytes must not mutate the pane screen"
    );
}

#[tokio::test]
async fn copy_mode_search_prompt_bounds_unterminated_sgr_mouse_without_pane_leak() {
    let handler = RequestHandler::new();
    let requester_pid = std::process::id();
    let alpha = session_name("alpha");
    let mut 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;

    let _ = enter_copy_mode_with_search_seed(&handler, &target).await;
    drain_attach_controls(&mut control_rx);
    let before_capture = capture_pane_print(&handler, target.clone()).await;

    let mut pending_input = Vec::new();
    send_copy_search_key(&handler, requester_pid, &mut pending_input, b"/").await;
    assert!(
        handler
            .attached_prompt_render(requester_pid)
            .await
            .is_some(),
        "slash should leave a search prompt active before the partial-input guard"
    );

    let partial = oversized_unterminated_sgr_mouse_input();
    let result = handler
        .handle_attached_live_input_inner(requester_pid, &mut pending_input, &partial)
        .await;
    assert_partial_control_bound(result, "prompt input");
    assert!(
        pending_input.is_empty(),
        "overflowing partial prompt input should be cleared after rejection"
    );
    assert_eq!(
        capture_pane_print(&handler, target.clone()).await,
        before_capture,
        "unterminated prompt control input must not mutate the pane screen"
    );

    let recovered = handler
        .handle_attached_live_input_inner(requester_pid, &mut pending_input, b"\x1b")
        .await
        .expect("escape should still be handled after partial-input rejection");
    assert!(
        !recovered,
        "search prompt escape must not be forwarded to pane IO"
    );
    assert!(
        handler
            .attached_prompt_render(requester_pid)
            .await
            .is_none(),
        "search prompt should remain recoverable after the partial-input guard fires"
    );
}

#[tokio::test]
async fn copy_mode_search_repeat_next_and_previous_match_tmux_order() {
    let handler = RequestHandler::new();
    let requester_pid = std::process::id();
    let alpha = session_name("alpha");
    let mut 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;
    let _ = enter_copy_mode_with_search_seed(&handler, &target).await;
    drain_attach_controls(&mut control_rx);
    let before_capture = capture_pane_print(&handler, target.clone()).await;

    handler
        .execute_copy_mode_command(
            requester_pid,
            target.clone(),
            "search-forward",
            &["--".to_owned(), "beta".to_owned()],
            1,
        )
        .await
        .expect("direct primary search-forward setup");
    assert_eq!(
        copy_search_status(&handler, target.clone()).await,
        "1:6,0:beta\n",
        "primary search-forward must match tmux oracle before testing n/N"
    );

    let mut pending_input = Vec::new();
    send_copy_search_key(&handler, requester_pid, &mut pending_input, b"n").await;
    assert_eq!(
        copy_search_status(&handler, target.clone()).await,
        "1:7,1:beta\n",
        "n must repeat the primary forward search direction"
    );

    send_copy_search_key(&handler, requester_pid, &mut pending_input, b"N").await;
    assert_eq!(
        copy_search_status(&handler, target.clone()).await,
        "1:6,0:beta\n",
        "N must reverse the primary forward search direction for one step"
    );

    assert_eq!(
        capture_pane_print(&handler, target.clone()).await,
        before_capture,
        "copy-mode search repeat keys must not reach or mutate pane IO"
    );

    send_copy_search_key(&handler, requester_pid, &mut pending_input, b"q").await;
    assert_eq!(
        copy_search_status(&handler, target.clone()).await,
        "0:,:\n",
        "q must exit copy-mode after search repeat navigation"
    );
    assert!(
        !capture_pane_print(&handler, target.clone())
            .await
            .contains("\nq"),
        "q must not appear in the pane capture after copy-mode search dismiss"
    );

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

#[tokio::test]
async fn copy_mode_question_search_repeat_next_and_reverse_match_tmux_order() {
    let handler = RequestHandler::new();
    let requester_pid = std::process::id();
    let alpha = session_name("alpha");
    let mut 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;
    let _ = enter_copy_mode_with_search_seed(&handler, &target).await;
    drain_attach_controls(&mut control_rx);
    let before_capture = capture_pane_print(&handler, target.clone()).await;

    handler
        .execute_copy_mode_command(
            requester_pid,
            target.clone(),
            "search-backward",
            &["--".to_owned(), "beta".to_owned()],
            1,
        )
        .await
        .expect("direct primary search-backward setup");
    assert_eq!(
        copy_search_status(&handler, target.clone()).await,
        "1:6,4:beta\n",
        "primary search-backward must match tmux oracle before testing n/N"
    );

    let mut pending_input = Vec::new();
    send_copy_search_key(&handler, requester_pid, &mut pending_input, b"n").await;
    assert_eq!(
        copy_search_status(&handler, target.clone()).await,
        "1:7,3:beta\n",
        "n must repeat the primary backward search direction"
    );

    send_copy_search_key(&handler, requester_pid, &mut pending_input, b"N").await;
    assert_eq!(
        copy_search_status(&handler, target.clone()).await,
        "1:6,4:beta\n",
        "N must reverse the primary backward search direction for one step"
    );

    assert_eq!(
        capture_pane_print(&handler, target.clone()).await,
        before_capture,
        "? search repeat keys must not reach or mutate pane IO"
    );

    send_copy_search_key(&handler, requester_pid, &mut pending_input, b"q").await;
    assert_eq!(
        copy_search_status(&handler, target.clone()).await,
        "0:,:\n",
        "q must exit copy-mode after ? search repeat navigation"
    );
    assert!(
        !capture_pane_print(&handler, target.clone())
            .await
            .contains("\nq"),
        "q must not appear in the pane capture after ? search dismiss"
    );

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