rmux-server 0.10.0

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

#[tokio::test]
async fn list_sessions_returns_empty_output_when_no_sessions_exist() {
    let handler = RequestHandler::new();

    let response = handler
        .handle(Request::ListSessions(ListSessionsRequest {
            format: None,
            filter: None,
            sort_order: None,
            reversed: false,
        }))
        .await;

    let output = response
        .command_output()
        .expect("list-sessions returns command output");
    assert!(output.stdout().is_empty());
}

#[tokio::test]
async fn list_sessions_sorts_sessions_by_name() {
    let handler = RequestHandler::new();
    for name in ["charlie", "alpha", "bravo"] {
        let created = handler
            .handle(Request::NewSession(NewSessionRequest {
                session_name: session_name(name),
                detached: true,
                size: None,

                environment: None,
            }))
            .await;
        assert!(matches!(created, Response::NewSession(_)));
    }

    let response = handler
        .handle(Request::ListSessions(ListSessionsRequest {
            format: Some("#{session_name}".to_owned()),
            filter: None,
            sort_order: None,
            reversed: false,
        }))
        .await;

    let output = response
        .command_output()
        .expect("list-sessions returns command output");
    assert_eq!(
        std::str::from_utf8(output.stdout()).expect("utf-8"),
        "alpha\nbravo\ncharlie\n"
    );
}

#[tokio::test]
async fn list_sessions_format_uses_each_sessions_active_pane_context() {
    let handler = RequestHandler::new();
    let root =
        std::env::temp_dir().join(format!("rmux-list-sessions-context-{}", std::process::id()));
    let alpha_dir = root.join("alpha");
    let beta_dir = root.join("beta");
    std::fs::create_dir_all(&alpha_dir).expect("create alpha dir");
    std::fs::create_dir_all(&beta_dir).expect("create beta dir");
    let alpha_dir = canonical_context_path(&alpha_dir);
    let beta_dir = canonical_context_path(&beta_dir);

    for (name, path) in [("alpha", &alpha_dir), ("beta", &beta_dir)] {
        let created = handler
            .handle(Request::NewSessionExt(Box::new(NewSessionExtRequest {
                session_name: Some(session_name(name)),
                working_directory: Some(path.to_string_lossy().into_owned()),
                detached: true,
                size: None,
                environment: None,
                group_target: None,
                attach_if_exists: false,
                detach_other_clients: false,
                kill_other_clients: false,
                flags: None,
                window_name: None,
                print_session_info: false,
                print_format: None,
                command: None,
                process_command: None,
                client_environment: None,
                skip_environment_update: false,
            })))
            .await;
        assert!(matches!(created, Response::NewSession(_)));
    }

    let response = handler
        .handle(Request::ListSessions(ListSessionsRequest {
            format: Some("#{session_name}|#{session_path}|#{pane_current_path}".to_owned()),
            filter: None,
            sort_order: None,
            reversed: false,
        }))
        .await;

    let output = response
        .command_output()
        .expect("list-sessions returns command output");
    let stdout = std::str::from_utf8(output.stdout()).expect("utf-8");
    assert_eq!(
        stdout,
        format!(
            "alpha|{}|{}\nbeta|{}|{}\n",
            rendered_context_path(&alpha_dir),
            rendered_context_path(&alpha_dir),
            rendered_context_path(&beta_dir),
            rendered_context_path(&beta_dir)
        )
    );

    let _ = std::fs::remove_dir_all(root);
}

#[tokio::test]
async fn session_path_stays_at_session_cwd_when_pane_cwds_differ() {
    let handler = RequestHandler::new();
    let root =
        std::env::temp_dir().join(format!("rmux-session-path-context-{}", std::process::id()));
    let session_dir = root.join("session");
    let split_dir = root.join("split");
    std::fs::create_dir_all(&session_dir).expect("create session dir");
    std::fs::create_dir_all(&split_dir).expect("create split dir");
    let session_dir = canonical_context_path(&session_dir);
    let split_dir = canonical_context_path(&split_dir);
    let session = session_name("session-path-context");

    let created = handler
        .handle(Request::NewSessionExt(Box::new(NewSessionExtRequest {
            session_name: Some(session.clone()),
            working_directory: Some(session_dir.to_string_lossy().into_owned()),
            detached: true,
            size: None,
            environment: None,
            group_target: None,
            attach_if_exists: false,
            detach_other_clients: false,
            kill_other_clients: false,
            flags: None,
            window_name: None,
            print_session_info: false,
            print_format: None,
            command: None,
            process_command: None,
            client_environment: None,
            skip_environment_update: false,
        })))
        .await;
    assert!(matches!(created, Response::NewSession(_)), "{created:?}");

    let split = handler
        .handle(Request::SplitWindowExt(Box::new(
            rmux_proto::SplitWindowExtRequest {
                target: SplitWindowTarget::Session(session.clone()),
                direction: rmux_proto::SplitDirection::Vertical,
                before: false,
                environment: None,
                command: None,
                process_command: None,
                start_directory: Some(split_dir.clone()),
                keep_alive_on_exit: None,
                detached: true,
                size: None,
                preserve_zoom: false,
                full_size: false,
                stdin_payload: None,
            },
        )))
        .await;
    assert!(matches!(split, Response::SplitWindow(_)), "{split:?}");

    let response = handler
        .handle(Request::ListPanes(Box::new(ListPanesRequest {
            target: session,
            format: Some("#{pane_index}|#{session_path}|#{pane_current_path}".to_owned()),
            filter: None,
            sort_order: None,
            reversed: false,
            target_window_index: None,
        })))
        .await;
    let output = response
        .command_output()
        .expect("list-panes returns command output");
    let stdout = std::str::from_utf8(output.stdout()).expect("utf-8");
    assert_eq!(
        stdout,
        format!(
            "0|{session_path}|{session_path}\n1|{session_path}|{split_path}\n",
            session_path = rendered_context_path(&session_dir),
            split_path = rendered_context_path(&split_dir),
        )
    );

    let _ = std::fs::remove_dir_all(root);
}

fn canonical_context_path(path: &std::path::Path) -> std::path::PathBuf {
    let canonical = std::fs::canonicalize(path).expect("canonicalize context directory");
    #[cfg(windows)]
    {
        let rendered = canonical.to_string_lossy();
        if let Some(rest) = rendered.strip_prefix(r"\\?\UNC\") {
            return std::path::PathBuf::from(format!(r"\\{rest}"));
        }
        if let Some(rest) = rendered.strip_prefix(r"\\?\") {
            return std::path::PathBuf::from(rest);
        }
    }
    canonical
}

fn rendered_context_path(path: &std::path::Path) -> String {
    path.display().to_string()
}

#[tokio::test]
async fn list_panes_returns_error_for_missing_session() {
    let handler = RequestHandler::new();

    let response = handler
        .handle(Request::ListPanes(Box::new(ListPanesRequest {
            target: session_name("missing"),
            format: None,
            filter: None,
            sort_order: None,
            reversed: false,
            target_window_index: None,
        })))
        .await;

    assert_eq!(
        response,
        Response::Error(ErrorResponse {
            error: RmuxError::SessionNotFound("missing".to_owned()),
        })
    );
}

fn format_value<'a>(formats: &'a [(String, String)], name: &str) -> Option<&'a str> {
    formats
        .iter()
        .rev()
        .find(|(candidate, _)| candidate == name)
        .map(|(_, value)| value.as_str())
}

#[test]
fn after_hook_formats_preserve_repeated_flag_values() {
    let parsed =
        parse_command_string("new-window -d -e FOO=1 -e BAR=2 -t alpha").expect("command parses");
    let command = parsed.commands().first().expect("one command");

    let formats = after_hook_format_values(HookName::AfterNewWindow, Some(command));

    assert_eq!(format_value(&formats, "hook"), Some("after-new-window"));
    assert_eq!(
        format_value(&formats, "hook_arguments"),
        Some("-d -e FOO=1 -e BAR=2 -t alpha")
    );
    assert_eq!(format_value(&formats, "hook_flag_d"), Some("1"));
    assert_eq!(format_value(&formats, "hook_flag_e"), Some("BAR=2"));
    assert_eq!(format_value(&formats, "hook_flag_e_0"), Some("FOO=1"));
    assert_eq!(format_value(&formats, "hook_flag_e_1"), Some("BAR=2"));
    assert_eq!(format_value(&formats, "hook_flag_t"), Some("alpha"));
    assert_eq!(format_value(&formats, "hook_flag_t_0"), Some("alpha"));
}