rmux-client 0.1.1

Blocking local client and attach-mode plumbing for the RMUX terminal multiplexer.
Documentation
#![cfg(unix)]

mod common;

use std::error::Error;

use common::{session_name, start_server, TestHarness};
use rmux_client::connect;
use rmux_proto::{
    KillPaneResponse, KillWindowResponse, MoveWindowResponse, MoveWindowTarget, NewSessionRequest,
    NewWindowRequest, NewWindowResponse, PaneTarget, RenameWindowResponse, Request, Response,
    RotateWindowDirection, RotateWindowResponse, SelectWindowResponse, SplitDirection,
    SplitWindowResponse, SplitWindowTarget, SwapWindowResponse, TerminalSize, WindowTarget,
};

#[test]
fn window_commands_round_trip_through_connection_helpers() -> Result<(), Box<dyn Error>> {
    let harness = TestHarness::new("window-helpers");
    let mut server = start_server(&harness)?;
    let mut connection = connect(harness.socket_path())?;
    let session = session_name("alpha");

    let created = connection.roundtrip(&Request::NewSession(NewSessionRequest {
        session_name: session.clone(),
        detached: true,
        size: Some(TerminalSize { cols: 80, rows: 24 }),
        environment: None,
    }))?;
    assert!(matches!(created, Response::NewSession(_)));

    let created_window = connection.new_window(session.clone(), Some("logs".to_owned()), true)?;
    assert_eq!(
        created_window,
        Response::NewWindow(NewWindowResponse {
            target: WindowTarget::with_window(session.clone(), 1),
        })
    );

    let selected = connection.select_window(WindowTarget::with_window(session.clone(), 1))?;
    assert_eq!(
        selected,
        Response::SelectWindow(SelectWindowResponse {
            target: WindowTarget::with_window(session.clone(), 1),
        })
    );

    let renamed = connection.rename_window(
        WindowTarget::with_window(session.clone(), 1),
        "renamed".to_owned(),
    )?;
    assert_eq!(
        renamed,
        Response::RenameWindow(RenameWindowResponse {
            target: WindowTarget::with_window(session.clone(), 1),
        })
    );

    let killed = connection.kill_window(WindowTarget::with_window(session.clone(), 1), false)?;
    assert_eq!(
        killed,
        Response::KillWindow(KillWindowResponse {
            target: WindowTarget::with_window(session.clone(), 0),
        })
    );

    server.shutdown()?;
    Ok(())
}

#[test]
fn kill_window_all_others_preserves_the_target_window_for_session_commands(
) -> Result<(), Box<dyn Error>> {
    let harness = TestHarness::new("kill-window-all-others");
    let mut server = start_server(&harness)?;
    let mut connection = connect(harness.socket_path())?;
    let session = session_name("alpha");

    let created = connection.roundtrip(&Request::NewSession(NewSessionRequest {
        session_name: session.clone(),
        detached: true,
        size: Some(TerminalSize {
            cols: 120,
            rows: 40,
        }),
        environment: None,
    }))?;
    assert!(matches!(created, Response::NewSession(_)));

    assert!(matches!(
        connection.new_window(session.clone(), None, true)?,
        Response::NewWindow(_)
    ));
    assert!(matches!(
        connection.select_window(WindowTarget::with_window(session.clone(), 1))?,
        Response::SelectWindow(_)
    ));
    assert_eq!(
        connection.split_window(SplitWindowTarget::Session(session.clone()))?,
        Response::SplitWindow(SplitWindowResponse {
            pane: PaneTarget::with_window(session.clone(), 1, 1),
        })
    );
    assert!(matches!(
        connection.new_window(session.clone(), None, true)?,
        Response::NewWindow(_)
    ));

    let killed = connection.kill_window(WindowTarget::with_window(session.clone(), 1), true)?;
    assert_eq!(
        killed,
        Response::KillWindow(KillWindowResponse {
            target: WindowTarget::with_window(session.clone(), 1),
        })
    );

    let split = connection.split_window(SplitWindowTarget::Session(session.clone()))?;
    assert_eq!(
        split,
        Response::SplitWindow(SplitWindowResponse {
            pane: PaneTarget::with_window(session, 1, 2),
        })
    );

    server.shutdown()?;
    Ok(())
}

#[test]
fn horizontal_split_and_kill_pane_round_trip_through_connection_helpers(
) -> Result<(), Box<dyn Error>> {
    let harness = TestHarness::new("horizontal-split-and-kill-pane");
    let mut server = start_server(&harness)?;
    let mut connection = connect(harness.socket_path())?;
    let session = session_name("alpha");

    let created = connection.roundtrip(&Request::NewSession(NewSessionRequest {
        session_name: session.clone(),
        detached: true,
        size: Some(TerminalSize {
            cols: 120,
            rows: 40,
        }),
        environment: None,
    }))?;
    assert!(matches!(created, Response::NewSession(_)));

    assert_eq!(
        connection.split_window_with_direction(
            SplitWindowTarget::Session(session.clone()),
            SplitDirection::Horizontal,
        )?,
        Response::SplitWindow(SplitWindowResponse {
            pane: PaneTarget::new(session.clone(), 1),
        })
    );
    assert_eq!(
        connection.kill_pane(PaneTarget::new(session.clone(), 1))?,
        Response::KillPane(KillPaneResponse {
            target: PaneTarget::new(session.clone(), 1),
            window_destroyed: false,
        })
    );
    assert_eq!(
        connection.split_window(SplitWindowTarget::Session(session.clone()))?,
        Response::SplitWindow(SplitWindowResponse {
            pane: PaneTarget::new(session, 1),
        })
    );

    server.shutdown()?;
    Ok(())
}

#[test]
fn killing_the_last_pane_destroys_the_window_and_session_targets_fall_back(
) -> Result<(), Box<dyn Error>> {
    let harness = TestHarness::new("kill-pane-destroys-window");
    let mut server = start_server(&harness)?;
    let mut connection = connect(harness.socket_path())?;
    let session = session_name("alpha");

    let created = connection.roundtrip(&Request::NewSession(NewSessionRequest {
        session_name: session.clone(),
        detached: true,
        size: Some(TerminalSize {
            cols: 120,
            rows: 40,
        }),
        environment: None,
    }))?;
    assert!(matches!(created, Response::NewSession(_)));

    assert_eq!(
        connection.roundtrip(&Request::NewWindow(NewWindowRequest {
            target: session.clone(),
            name: Some("scratch".to_owned()),
            detached: true,
            start_directory: None,
            environment: None,
            command: None,
            target_window_index: None,
            insert_at_target: false,
        }))?,
        Response::NewWindow(NewWindowResponse {
            target: WindowTarget::with_window(session.clone(), 1),
        })
    );
    assert_eq!(
        connection.select_window(WindowTarget::with_window(session.clone(), 1))?,
        Response::SelectWindow(SelectWindowResponse {
            target: WindowTarget::with_window(session.clone(), 1),
        })
    );
    assert_eq!(
        connection.kill_pane(PaneTarget::with_window(session.clone(), 1, 0))?,
        Response::KillPane(KillPaneResponse {
            target: PaneTarget::with_window(session.clone(), 1, 0),
            window_destroyed: true,
        })
    );
    assert_eq!(
        connection.split_window(SplitWindowTarget::Session(session.clone()))?,
        Response::SplitWindow(SplitWindowResponse {
            pane: PaneTarget::with_window(session, 0, 1),
        })
    );

    server.shutdown()?;
    Ok(())
}

#[test]
fn window_navigation_and_listing_round_trip_through_connection_helpers(
) -> Result<(), Box<dyn Error>> {
    let harness = TestHarness::new("window-navigation-helpers");
    let mut server = start_server(&harness)?;
    let mut connection = connect(harness.socket_path())?;
    let session = session_name("alpha");

    let created = connection.roundtrip(&Request::NewSession(NewSessionRequest {
        session_name: session.clone(),
        detached: true,
        size: Some(TerminalSize { cols: 80, rows: 24 }),
        environment: None,
    }))?;
    assert!(matches!(created, Response::NewSession(_)));

    assert!(matches!(
        connection.new_window(session.clone(), Some("logs".to_owned()), true)?,
        Response::NewWindow(_)
    ));
    assert!(matches!(
        connection.new_window(session.clone(), Some("shell".to_owned()), true)?,
        Response::NewWindow(_)
    ));

    assert_eq!(
        connection.next_window(session.clone(), false)?,
        Response::NextWindow(rmux_proto::NextWindowResponse {
            target: WindowTarget::with_window(session.clone(), 1),
        })
    );
    assert_eq!(
        connection.previous_window(session.clone(), false)?,
        Response::PreviousWindow(rmux_proto::PreviousWindowResponse {
            target: WindowTarget::with_window(session.clone(), 0),
        })
    );
    assert_eq!(
        connection.last_window(session.clone())?,
        Response::LastWindow(rmux_proto::LastWindowResponse {
            target: WindowTarget::with_window(session.clone(), 1),
        })
    );

    let listed = connection.list_windows(
        session.clone(),
        Some("#{window_index}:#{window_id}:#{window_active}".to_owned()),
    )?;
    let Response::ListWindows(listed) = listed else {
        panic!("expected list-windows response");
    };
    assert_eq!(
        std::str::from_utf8(listed.output.stdout())?,
        "0:@0:0\n1:@1:1\n2:@2:0\n"
    );
    assert_eq!(listed.windows.len(), 3);
    assert_eq!(listed.windows[1].name.as_deref(), Some("logs"));
    assert_eq!(
        listed.windows[1].target,
        WindowTarget::with_window(session, 1)
    );

    server.shutdown()?;
    Ok(())
}

#[test]
fn move_swap_and_rotate_window_commands_round_trip_through_connection_helpers(
) -> Result<(), Box<dyn Error>> {
    let harness = TestHarness::new("window-movement-helpers");
    let mut server = start_server(&harness)?;
    let mut connection = connect(harness.socket_path())?;
    let alpha = session_name("alpha");
    let beta = session_name("beta");

    for session in [alpha.clone(), beta.clone()] {
        let created = connection.roundtrip(&Request::NewSession(NewSessionRequest {
            session_name: session,
            detached: true,
            size: Some(TerminalSize { cols: 80, rows: 24 }),
            environment: None,
        }))?;
        assert!(matches!(created, Response::NewSession(_)));
    }

    assert!(matches!(
        connection.new_window(alpha.clone(), Some("logs".to_owned()), true)?,
        Response::NewWindow(_)
    ));
    assert!(matches!(
        connection.new_window(alpha.clone(), Some("scratch".to_owned()), true)?,
        Response::NewWindow(_)
    ));
    assert_eq!(
        connection.move_window(
            Some(WindowTarget::with_window(alpha.clone(), 1)),
            MoveWindowTarget::Window(WindowTarget::with_window(beta.clone(), 4)),
            false,
            false,
            true,
        )?,
        Response::MoveWindow(MoveWindowResponse {
            session_name: beta.clone(),
            target: Some(WindowTarget::with_window(beta.clone(), 4)),
        })
    );
    assert_eq!(
        connection.swap_window(
            WindowTarget::with_window(alpha.clone(), 2),
            WindowTarget::with_window(beta.clone(), 4),
            true,
        )?,
        Response::SwapWindow(SwapWindowResponse {
            source: WindowTarget::with_window(alpha.clone(), 2),
            target: WindowTarget::with_window(beta.clone(), 4),
        })
    );
    assert_eq!(
        connection.split_window(SplitWindowTarget::Pane(PaneTarget::with_window(
            alpha.clone(),
            2,
            0,
        )))?,
        Response::SplitWindow(SplitWindowResponse {
            pane: PaneTarget::with_window(alpha.clone(), 2, 1),
        })
    );
    assert_eq!(
        connection.rotate_window(
            WindowTarget::with_window(alpha.clone(), 2),
            RotateWindowDirection::Up,
        )?,
        Response::RotateWindow(RotateWindowResponse {
            target: WindowTarget::with_window(alpha.clone(), 2),
        })
    );

    let alpha_windows = connection.list_windows(
        alpha.clone(),
        Some("#{window_index}:#{window_panes}".to_owned()),
    )?;
    let Response::ListWindows(alpha_windows) = alpha_windows else {
        panic!("expected list-windows response");
    };
    assert_eq!(
        std::str::from_utf8(alpha_windows.output.stdout())?,
        "0:1\n2:2\n"
    );

    let beta_windows = connection.list_windows(
        beta.clone(),
        Some("#{window_index}:#{window_panes}".to_owned()),
    )?;
    let Response::ListWindows(beta_windows) = beta_windows else {
        panic!("expected list-windows response");
    };
    assert_eq!(
        std::str::from_utf8(beta_windows.output.stdout())?,
        "0:1\n4:1\n"
    );

    drop(connection);
    server.shutdown()?;
    Ok(())
}