rmux-server 0.1.1

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

#[tokio::test]
async fn attached_session_mutations_emit_refresh_switches() {
    let handler = RequestHandler::new();
    let requester_pid = std::process::id();
    let alpha = session_name("alpha");

    let created = handler
        .handle(Request::NewSession(NewSessionRequest {
            session_name: alpha.clone(),
            detached: true,
            size: Some(TerminalSize {
                cols: 120,
                rows: 40,
            }),
            environment: None,
        }))
        .await;
    assert!(matches!(created, Response::NewSession(_)));

    let (control_tx, mut control_rx) = mpsc::unbounded_channel();
    let _attach_id = handler
        .register_attach(requester_pid, alpha.clone(), control_tx)
        .await;

    let split = handler
        .handle(Request::SplitWindow(SplitWindowRequest {
            target: SplitWindowTarget::Session(alpha.clone()),
            direction: rmux_proto::SplitDirection::Horizontal,
            before: false,
            environment: None,
        }))
        .await;
    assert_eq!(
        split,
        Response::SplitWindow(rmux_proto::SplitWindowResponse {
            pane: PaneTarget::new(alpha.clone(), 1),
        })
    );
    let split_frame = take_render_frame(control_rx.try_recv().expect("split refresh"));
    assert!(split_frame.contains(''));

    let resized = handler
        .handle(Request::ResizePane(rmux_proto::ResizePaneRequest {
            target: PaneTarget::new(alpha.clone(), 0),
            adjustment: ResizePaneAdjustment::AbsoluteWidth { columns: 34 },
        }))
        .await;
    assert_eq!(
        resized,
        Response::ResizePane(rmux_proto::ResizePaneResponse {
            target: PaneTarget::new(alpha.clone(), 0),
            adjustment: ResizePaneAdjustment::AbsoluteWidth { columns: 34 },
        })
    );
    let resize_frame = take_render_frame(control_rx.try_recv().expect("resize refresh"));
    assert!(resize_frame.contains(''));

    let selected_layout = handler
        .handle(Request::SelectLayout(SelectLayoutRequest {
            target: SelectLayoutTarget::Session(alpha.clone()),
            layout: LayoutName::MainVertical,
        }))
        .await;
    assert_eq!(
        selected_layout,
        Response::SelectLayout(rmux_proto::SelectLayoutResponse {
            layout: LayoutName::MainVertical,
        })
    );
    let layout_frame = take_render_frame(control_rx.try_recv().expect("layout refresh"));
    assert!(layout_frame.contains(''));

    let selected_pane = handler
        .handle(Request::SelectPane(SelectPaneRequest {
            target: PaneTarget::new(alpha, 1),
            title: None,
        }))
        .await;
    assert_eq!(
        selected_pane,
        Response::SelectPane(rmux_proto::SelectPaneResponse {
            target: PaneTarget::new(session_name("alpha"), 1),
        })
    );
    let select_frame = take_render_frame(control_rx.try_recv().expect("pane refresh"));
    assert!(select_frame.contains(''));
    assert!(matches!(control_rx.try_recv(), Err(TryRecvError::Empty)));
}

#[tokio::test]
async fn switch_client_updates_the_tracked_session_for_follow_up_refreshes() {
    let handler = RequestHandler::new();
    let requester_pid = std::process::id();
    let alpha = session_name("alpha");
    let beta = session_name("beta");

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

    let split = handler
        .handle(Request::SplitWindow(SplitWindowRequest {
            target: SplitWindowTarget::Session(beta.clone()),
            direction: rmux_proto::SplitDirection::Horizontal,
            before: false,
            environment: None,
        }))
        .await;
    assert!(matches!(split, Response::SplitWindow(_)));

    let (control_tx, mut control_rx) = mpsc::unbounded_channel();
    let _attach_id = handler
        .register_attach(requester_pid, alpha, control_tx)
        .await;

    let switched = handler
        .handle(Request::SwitchClient(SwitchClientRequest {
            target: beta.clone(),
        }))
        .await;
    assert_eq!(
        switched,
        Response::SwitchClient(rmux_proto::SwitchClientResponse {
            session_name: beta.clone(),
        })
    );
    let switch_frame = take_render_frame(control_rx.try_recv().expect("switch refresh"));
    assert!(switch_frame.contains(''));

    let global_border = handler
        .handle(Request::SetOption(SetOptionRequest {
            scope: ScopeSelector::Global,
            option: OptionName::PaneActiveBorderStyle,
            value: "red".to_owned(),
            mode: SetOptionMode::Replace,
        }))
        .await;
    assert!(matches!(global_border, Response::SetOption(_)));
    let global_frame = take_render_frame(control_rx.try_recv().expect("global refresh"));
    assert!(global_frame.contains("\u{1b}[31m"));

    let beta_window = WindowTarget::with_window(beta.clone(), 0);
    let session_border = handler
        .handle(Request::SetOption(SetOptionRequest {
            scope: ScopeSelector::Window(beta_window),
            option: OptionName::PaneActiveBorderStyle,
            value: "blue".to_owned(),
            mode: SetOptionMode::Replace,
        }))
        .await;
    assert!(matches!(session_border, Response::SetOption(_)));
    let session_frame = take_render_frame(control_rx.try_recv().expect("session refresh"));
    assert!(session_frame.contains("\u{1b}[34m"));

    let alpha_window = WindowTarget::with_window(session_name("alpha"), 0);
    let other_session = handler
        .handle(Request::SetOption(SetOptionRequest {
            scope: ScopeSelector::Window(alpha_window),
            option: OptionName::PaneBorderStyle,
            value: "green".to_owned(),
            mode: SetOptionMode::Replace,
        }))
        .await;
    assert!(matches!(other_session, Response::SetOption(_)));
    assert!(matches!(control_rx.try_recv(), Err(TryRecvError::Empty)));
}

#[tokio::test]
async fn terminal_feature_mutations_refresh_attached_targets_with_client_context() {
    let handler = RequestHandler::new();
    let requester_pid = 42;
    let alpha = session_name("alpha");

    let created = handler
        .handle(Request::NewSession(NewSessionRequest {
            session_name: alpha.clone(),
            detached: true,
            size: Some(TerminalSize { cols: 80, rows: 24 }),
            environment: None,
        }))
        .await;
    assert!(matches!(created, Response::NewSession(_)));

    let (control_tx, mut control_rx) = mpsc::unbounded_channel();
    let _attach_id = handler
        .register_attach_with_terminal_context(
            requester_pid,
            alpha,
            control_tx,
            OuterTerminalContext::from_pairs(&[("TERM", "xterm-256color")]),
        )
        .await;

    let set = handler
        .handle(Request::SetOption(SetOptionRequest {
            scope: ScopeSelector::Global,
            option: OptionName::TerminalFeatures,
            value: "xterm*:sync".to_owned(),
            mode: SetOptionMode::Append,
        }))
        .await;
    assert!(matches!(set, Response::SetOption(_)));

    let target = take_switch_target(control_rx.try_recv().expect("terminal feature refresh"));
    assert!(target.outer_terminal.features_string().contains("sync"));
    assert!(target
        .outer_terminal
        .wrap_render_frame(&target.render_frame)
        .starts_with(b"\x1b[?2026h"));
}

#[tokio::test]
async fn different_requester_pids_can_control_the_sole_active_attach() {
    let handler = RequestHandler::new();
    let owner_pid = 101;
    let intruder_pid = 202;
    let alpha = session_name("alpha");
    let beta = session_name("beta");

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

    let (control_tx, mut control_rx) = mpsc::unbounded_channel();
    let _attach_id = handler
        .register_attach(owner_pid, alpha.clone(), control_tx)
        .await;

    let switched = handler
        .dispatch(
            intruder_pid,
            Request::SwitchClient(SwitchClientRequest {
                target: beta.clone(),
            }),
        )
        .await
        .response;
    assert_eq!(
        switched,
        Response::SwitchClient(rmux_proto::SwitchClientResponse {
            session_name: beta.clone(),
        })
    );
    assert!(matches!(
        control_rx.try_recv(),
        Ok(AttachControl::Switch(_))
    ));

    let detached = handler
        .dispatch(
            intruder_pid,
            Request::DetachClient(rmux_proto::DetachClientRequest),
        )
        .await
        .response;
    assert_eq!(
        detached,
        Response::DetachClient(rmux_proto::DetachClientResponse)
    );
    assert!(matches!(control_rx.try_recv(), Ok(AttachControl::Detach)));
}

#[tokio::test]
async fn rename_session_preserves_ambiguity_rules_for_switch_and_detach() {
    let handler = RequestHandler::new();
    let alpha = session_name("alpha");
    let beta = session_name("beta");
    let gamma = session_name("gamma");

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

    let (first_tx, _first_rx) = mpsc::unbounded_channel();
    let (second_tx, _second_rx) = mpsc::unbounded_channel();
    let _first_attach = handler.register_attach(101, alpha.clone(), first_tx).await;
    let _second_attach = handler.register_attach(202, alpha.clone(), second_tx).await;

    let renamed = handler
        .handle(Request::RenameSession(RenameSessionRequest {
            target: alpha,
            new_name: gamma,
        }))
        .await;
    assert!(matches!(renamed, Response::RenameSession(_)));

    let switched = handler
        .dispatch(
            303,
            Request::SwitchClient(SwitchClientRequest {
                target: beta.clone(),
            }),
        )
        .await
        .response;
    assert_eq!(
        switched,
        Response::Error(ErrorResponse {
            error: RmuxError::Server(
                "switch-client requires an unambiguous attached client".to_owned(),
            ),
        })
    );

    let detached = handler
        .dispatch(303, Request::DetachClient(DetachClientRequest))
        .await
        .response;
    assert_eq!(
        detached,
        Response::Error(ErrorResponse {
            error: RmuxError::Server(
                "detach-client requires an unambiguous attached client".to_owned(),
            ),
        })
    );
}