rmux-server 0.10.0

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

#[tokio::test]
async fn move_window_without_s_moves_current_window_to_first_free_index() {
    let (handler, session) = handler_with_three_windows("move-current-free").await;
    let source_pane_id = pane_id_at(&handler, &session, 2).await;

    let parsed = CommandParser::new()
        .parse("move-window")
        .expect("commands parse");
    handler
        .execute_parsed_commands(
            std::process::id(),
            parsed,
            current_window_context(&session, 2),
        )
        .await
        .expect("move-window should use current window source and first free index");

    let state = handler.state.lock().await;
    let session_state = state.sessions.session(&session).expect("session exists");
    assert_eq!(
        session_state.windows().keys().copied().collect::<Vec<_>>(),
        vec![0, 1, 3]
    );
    assert_eq!(pane_id_in(session_state, 3), source_pane_id);
}

#[tokio::test]
async fn move_window_after_uses_current_window_as_source() {
    let (handler, session) = handler_with_three_windows("move-current-after").await;
    let source_pane_id = pane_id_at(&handler, &session, 2).await;

    let parsed = CommandParser::new()
        .parse("move-window -a -t move-current-after:0")
        .expect("commands parse");
    handler
        .execute_parsed_commands(
            std::process::id(),
            parsed,
            current_window_context(&session, 2),
        )
        .await
        .expect("move-window -a should use current window source");

    let state = handler.state.lock().await;
    let session_state = state.sessions.session(&session).expect("session exists");
    assert_eq!(pane_id_in(session_state, 1), source_pane_id);
}

#[tokio::test]
async fn move_window_before_uses_current_window_as_source() {
    let (handler, session) = handler_with_three_windows("move-current-before").await;
    let source_pane_id = pane_id_at(&handler, &session, 2).await;

    let parsed = CommandParser::new()
        .parse("move-window -b -t move-current-before:0")
        .expect("commands parse");
    handler
        .execute_parsed_commands(
            std::process::id(),
            parsed,
            current_window_context(&session, 2),
        )
        .await
        .expect("move-window -b should use current window source");

    let state = handler.state.lock().await;
    let session_state = state.sessions.session(&session).expect("session exists");
    assert_eq!(pane_id_in(session_state, 0), source_pane_id);
}

#[tokio::test]
async fn move_window_relative_collision_uses_tmux_error_shape() {
    let (handler, session) = handler_with_three_windows("move-current-collision").await;

    let parsed = CommandParser::new()
        .parse("move-window -t move-current-collision:1")
        .expect("commands parse");
    let error = handler
        .execute_parsed_commands(
            std::process::id(),
            parsed,
            current_window_context(&session, 2),
        )
        .await
        .expect_err("occupied relative target should fail");

    assert_eq!(
        error,
        rmux_proto::RmuxError::Server("index in use: 1".to_owned())
    );
}

#[tokio::test]
async fn move_window_trailing_colon_target_uses_first_free_index() {
    let handler = RequestHandler::new();
    let alpha = session_name("move-colon-alpha");
    let beta = session_name("move-colon-beta");
    for session in [&alpha, &beta] {
        assert!(matches!(
            handler
                .handle(Request::NewSession(NewSessionRequest {
                    session_name: session.clone(),
                    detached: true,
                    size: Some(TerminalSize { cols: 80, rows: 24 }),
                    environment: None,
                }))
                .await,
            Response::NewSession(_)
        ));
    }

    let parsed = CommandParser::new()
        .parse("move-window -s move-colon-alpha:0 -t move-colon-beta:")
        .expect("commands parse");
    assert_eq!(
        parsed.commands()[0].to_tmux_string(),
        "move-window -s move-colon-alpha:0 -t move-colon-beta:"
    );
    {
        let state = handler.state.lock().await;
        let request = crate::handler::scripting_support::parse_request_from_parts(
            "move-window".to_owned(),
            vec![
                "-s".to_owned(),
                "move-colon-alpha:0".to_owned(),
                "-t".to_owned(),
                "move-colon-beta:".to_owned(),
            ],
            None,
            &state.sessions,
            &state.options,
            &TargetFindContext::new(Some(Target::Pane(PaneTarget::with_window(
                beta.clone(),
                0,
                0,
            )))),
        )
        .expect("request parses");
        let Request::MoveWindow(request) = request else {
            panic!("expected move-window request");
        };
        assert_eq!(
            request.target,
            MoveWindowTarget::Session(beta.clone()),
            "trailing colon should stay a session target until execution"
        );
    }
    handler
        .execute_parsed_commands(std::process::id(), parsed, current_window_context(&beta, 0))
        .await
        .expect("trailing colon target should use first free window index");

    let state = handler.state.lock().await;
    let beta_state = state.sessions.session(&beta).expect("beta session exists");
    assert_eq!(
        beta_state.windows().keys().copied().collect::<Vec<_>>(),
        vec![0, 1]
    );
}

fn current_window_context(session: &SessionName, window_index: u32) -> QueueExecutionContext {
    QueueExecutionContext::without_caller_cwd().with_current_target(Some(Target::Pane(
        PaneTarget::with_window(session.clone(), window_index, 0),
    )))
}

async fn handler_with_three_windows(name: &str) -> (RequestHandler, SessionName) {
    let handler = RequestHandler::new();
    let session = session_name(name);
    assert!(matches!(
        handler
            .handle(Request::NewSession(NewSessionRequest {
                session_name: session.clone(),
                detached: true,
                size: Some(TerminalSize { cols: 80, rows: 24 }),
                environment: None,
            }))
            .await,
        Response::NewSession(_)
    ));
    for window_name in ["b", "c"] {
        assert!(matches!(
            handler
                .handle(Request::NewWindow(Box::new(NewWindowRequest {
                    target: session.clone(),
                    name: Some(window_name.to_owned()),
                    detached: true,
                    start_directory: None,
                    environment: None,
                    command: None,
                    process_command: None,
                    target_window_index: None,
                    insert_at_target: false,
                })))
                .await,
            Response::NewWindow(_)
        ));
    }
    (handler, session)
}

async fn pane_id_at(
    handler: &RequestHandler,
    session_name: &SessionName,
    window_index: u32,
) -> Option<rmux_core::PaneId> {
    let state = handler.state.lock().await;
    state
        .sessions
        .session(session_name)
        .and_then(|session| pane_id_in(session, window_index))
}

fn pane_id_in(session: &rmux_core::Session, window_index: u32) -> Option<rmux_core::PaneId> {
    session
        .window_at(window_index)
        .and_then(|window| window.pane(0))
        .map(|pane| pane.id())
}