rmux 0.1.1

A local terminal multiplexer with a tmux-style CLI, daemon runtime, Rust SDK, and ratatui integration.
#![cfg(unix)]

mod common;

use std::error::Error;
use std::fs;
use std::time::Duration;

use common::{assert_success, stderr, stdout, CliHarness};

#[test]
fn foreground_run_shell_writes_captured_stdout() -> Result<(), Box<dyn Error>> {
    let harness = CliHarness::new("run-shell-stdout")?;
    let _daemon = harness.start_hidden_daemon()?;

    let output = harness.run(&["run-shell", "printf hello"])?;

    assert_eq!(output.status.code(), Some(0));
    assert_eq!(stdout(&output), "hello");
    assert!(stderr(&output).is_empty());
    Ok(())
}

#[test]
fn run_shell_nonzero_exits_one_without_stdout() -> Result<(), Box<dyn Error>> {
    let harness = CliHarness::new("run-shell-nonzero")?;
    let _daemon = harness.start_hidden_daemon()?;

    let output = harness.run(&["run-shell", "printf hidden; exit 9"])?;

    assert_eq!(output.status.code(), Some(1));
    assert!(stdout(&output).is_empty());
    assert!(!stderr(&output).is_empty());
    Ok(())
}

#[test]
fn run_shell_preserves_spaced_path_arguments() -> Result<(), Box<dyn Error>> {
    let harness = CliHarness::new("run-shell-spaced-path")?;
    let _daemon = harness.start_hidden_daemon()?;
    let spaced_path = harness.tmpdir().join("name with spaces");

    let output = harness.run(&[
        "run-shell",
        "env",
        "-C",
        harness.tmpdir().to_str().expect("utf-8 test path"),
        "touch",
        "name with spaces",
    ])?;

    assert_eq!(output.status.code(), Some(0));
    assert!(stderr(&output).is_empty());
    assert!(spaced_path.is_file());
    assert!(!harness.tmpdir().join("name").exists());
    assert!(!harness.tmpdir().join("with").exists());
    assert!(!harness.tmpdir().join("spaces").exists());
    Ok(())
}

#[test]
fn run_shell_preserves_shell_metacharacters_and_backslashes() -> Result<(), Box<dyn Error>> {
    let harness = CliHarness::new("run-shell-metacharacters")?;
    let _daemon = harness.start_hidden_daemon()?;

    let output = harness.run(&["run-shell", "printf", "%s", "x;y\\z"])?;

    assert_eq!(output.status.code(), Some(0));
    assert_eq!(stdout(&output), "x;y\\z");
    assert!(stderr(&output).is_empty());
    Ok(())
}

#[test]
fn if_shell_dispatches_nested_supported_command() -> Result<(), Box<dyn Error>> {
    let harness = CliHarness::new("if-shell-dispatch")?;
    let _daemon = harness.start_hidden_daemon()?;

    assert_success(&harness.run(&[
        "if-shell",
        "-F",
        "1",
        "set-buffer -b selected yes",
        "set-buffer -b selected no",
    ])?);

    let output = harness.run(&["show-buffer", "-b", "selected"])?;
    assert_eq!(output.status.code(), Some(0));
    assert_eq!(stdout(&output), "yes");
    assert!(stderr(&output).is_empty());
    Ok(())
}

#[test]
fn if_shell_preserves_nested_stdout_from_output_commands() -> Result<(), Box<dyn Error>> {
    let harness = CliHarness::new("if-shell-output")?;
    let _daemon = harness.start_hidden_daemon()?;
    let marker = "if_shell_capture_marker";

    assert_success(&harness.run(&["new-session", "-d", "-s", "alpha"])?);
    assert_success(&harness.run(&["set-buffer", "-b", "selected", "yes"])?);

    let display = harness.run(&[
        "if-shell",
        "-F",
        "-t",
        "alpha:0.0",
        "1",
        "display-message -p -t alpha:0.0 #{session_name}",
    ])?;
    assert_eq!(display.status.code(), Some(0));
    assert_eq!(stdout(&display), "alpha\n");
    assert!(stderr(&display).is_empty());

    let show_buffer = harness.run(&["if-shell", "-F", "1", "show-buffer -b selected"])?;
    assert_eq!(show_buffer.status.code(), Some(0));
    assert_eq!(stdout(&show_buffer), "yes");
    assert!(stderr(&show_buffer).is_empty());

    let list_sessions =
        harness.run(&["if-shell", "-F", "1", "list-sessions -F #{session_name}"])?;
    assert_eq!(list_sessions.status.code(), Some(0));
    assert_eq!(stdout(&list_sessions), "alpha\n");
    assert!(stderr(&list_sessions).is_empty());

    assert_success(&harness.run(&[
        "send-keys",
        "-t",
        "alpha:0.0",
        &format!("printf '{marker}\\n'"),
        "Enter",
    ])?);

    let capture = wait_for_if_shell_capture(&harness, marker)?;
    assert!(stdout(&capture).contains(marker));
    assert!(stderr(&capture).is_empty());

    Ok(())
}

#[test]
fn if_shell_nested_run_shell_preserves_spaced_path_arguments() -> Result<(), Box<dyn Error>> {
    let harness = CliHarness::new("if-shell-run-shell-spaced-path")?;
    let _daemon = harness.start_hidden_daemon()?;
    let spaced_path = harness.tmpdir().join("nested name with spaces");
    let nested_command = format!(
        "run-shell env -C {} touch 'nested name with spaces'",
        harness.tmpdir().display()
    );

    let output = harness.run(&["if-shell", "-F", "1", &nested_command])?;

    assert_eq!(output.status.code(), Some(0));
    assert!(stderr(&output).is_empty());
    assert!(spaced_path.is_file());
    assert!(!harness.tmpdir().join("nested").exists());
    assert!(!harness.tmpdir().join("name").exists());
    assert!(!harness.tmpdir().join("with").exists());
    assert!(!harness.tmpdir().join("spaces").exists());
    Ok(())
}

#[test]
fn if_shell_nested_run_shell_preserves_shell_metacharacters_and_backslashes(
) -> Result<(), Box<dyn Error>> {
    let harness = CliHarness::new("if-shell-run-shell-metacharacters")?;
    let _daemon = harness.start_hidden_daemon()?;

    let output = harness.run(&["if-shell", "-F", "1", "run-shell printf %s 'x;y\\z'"])?;

    assert_eq!(output.status.code(), Some(0));
    assert_eq!(stdout(&output), "x;y\\z");
    assert!(stderr(&output).is_empty());
    Ok(())
}

#[test]
fn source_file_rejects_non_tmux_switch_client_f_flag() -> Result<(), Box<dyn Error>> {
    let harness = CliHarness::new("source-file-switch-client-f")?;
    let _daemon = harness.start_hidden_daemon()?;
    let config = harness.tmpdir().join("unsupported-switch-client.conf");
    fs::write(&config, "switch-client -f read-only\n")?;

    let output = harness.run(&["source-file", config.to_str().expect("utf-8 config path")])?;

    assert_eq!(output.status.code(), Some(1));
    assert!(stdout(&output).is_empty());
    let stderr = stderr(&output);
    assert!(stderr.contains("switch-client"));
    assert!(stderr.contains("-f"));
    Ok(())
}

#[test]
fn source_file_missing_path_reports_plain_no_such_file_surface() -> Result<(), Box<dyn Error>> {
    let harness = CliHarness::new("source-file-missing")?;
    let _daemon = harness.start_hidden_daemon()?;
    let missing = harness.tmpdir().join("missing.conf");

    let output = harness.run(&["source-file", missing.to_str().expect("utf-8 config path")])?;

    assert_eq!(output.status.code(), Some(1));
    assert!(stdout(&output).is_empty());
    assert_eq!(
        stderr(&output),
        format!("{}: No such file or directory\n", missing.display())
    );
    Ok(())
}

#[test]
fn if_shell_nested_load_buffer_resolves_relative_paths_against_caller_cwd(
) -> Result<(), Box<dyn Error>> {
    let harness = CliHarness::new("if-shell-load-buffer-relative")?;
    let _daemon = harness.start_hidden_daemon()?;
    let caller_dir = harness.tmpdir().join("caller");
    let nested_dir = caller_dir.join("nested");
    fs::create_dir_all(&nested_dir)?;
    fs::write(nested_dir.join("input.txt"), "loaded via nested if-shell")?;

    assert_success(&harness.run_with(
        &[
            "if-shell",
            "-F",
            "1",
            "load-buffer -b loaded nested/input.txt",
        ],
        |command| {
            command.current_dir(&caller_dir);
        },
    )?);

    let show = harness.run(&["show-buffer", "-b", "loaded"])?;
    assert_eq!(show.status.code(), Some(0));
    assert_eq!(stdout(&show), "loaded via nested if-shell");
    assert!(stderr(&show).is_empty());
    Ok(())
}

#[test]
fn if_shell_supports_representative_public_commands() -> Result<(), Box<dyn Error>> {
    let harness = CliHarness::new("if-shell-surface")?;
    let _daemon = harness.start_hidden_daemon()?;
    let buffer_path = harness.tmpdir().join("loaded-buffer.txt");

    fs::write(&buffer_path, "loaded from file")?;

    assert_success(&harness.run(&["new-session", "-d", "-s", "alpha"])?);
    assert_success(&harness.run(&["split-window", "-h", "-t", "alpha"])?);

    assert_success(&harness.run(&["if-shell", "-F", "1", "set-option -g status off"])?);

    let show_options = harness.run(&["if-shell", "-F", "1", "show-options -g"])?;
    assert_eq!(show_options.status.code(), Some(0));
    assert!(stdout(&show_options).contains("status off"));
    assert!(stderr(&show_options).is_empty());

    let load_buffer_command = format!("load-buffer -b loaded {}", buffer_path.display());
    assert_success(&harness.run(&["if-shell", "-F", "1", &load_buffer_command])?);

    let show_buffer = harness.run(&["show-buffer", "-b", "loaded"])?;
    assert_eq!(show_buffer.status.code(), Some(0));
    assert_eq!(stdout(&show_buffer), "loaded from file");
    assert!(stderr(&show_buffer).is_empty());

    assert_success(&harness.run(&[
        "if-shell",
        "-F",
        "1",
        "select-layout -t alpha:0 even-horizontal",
    ])?);

    let windows = harness.run(&["list-windows", "-t", "alpha", "-F", "#{window_layout}"])?;
    assert_eq!(windows.status.code(), Some(0));
    assert_eq!(
        stdout(&windows),
        "89f5,80x24,0,0{39x24,0,0,0,40x24,40,0,1}\n"
    );
    assert!(stderr(&windows).is_empty());

    assert_success(&harness.run(&["if-shell", "-F", "1", "select-pane -t alpha:0.1"])?);

    let panes = harness.run(&[
        "list-panes",
        "-t",
        "alpha",
        "-F",
        "#{pane_index}:#{pane_active}",
    ])?;
    assert_eq!(panes.status.code(), Some(0));
    assert!(stdout(&panes).contains("1:1"));
    assert!(stderr(&panes).is_empty());

    Ok(())
}

#[test]
fn hook_surface_smoke_matches_supported_cli_behavior() -> Result<(), Box<dyn Error>> {
    let harness = CliHarness::new("hook-surface-smoke")?;
    let _daemon = harness.start_hidden_daemon()?;

    assert_success(&harness.run(&["new-session", "-d", "-s", "alpha"])?);
    assert_success(&harness.run(&[
        "set-hook",
        "-t",
        "alpha",
        "client-attached",
        "display-message hi",
    ])?);

    let rejected = harness.run(&["set-hook", "-g", "window-resized", "display-message hi"])?;
    assert_eq!(rejected.status.code(), Some(1));
    assert!(stdout(&rejected).is_empty());
    assert_eq!(
        stderr(&rejected),
        "window-resized is not supported: rmux does not dispatch this hook\n"
    );

    let output = harness.run(&["show-hooks", "-t", "alpha", "client-attached"])?;
    assert_eq!(output.status.code(), Some(0));
    assert_eq!(stdout(&output), "client-attached[0] display-message hi\n");
    assert!(stderr(&output).is_empty());

    let bindings = harness.run(&["list-keys", "-T", "prefix", "C-b"])?;
    assert_eq!(bindings.status.code(), Some(0));
    assert_eq!(stdout(&bindings), "bind-key -T prefix C-b send-prefix\n");
    assert!(stderr(&bindings).is_empty());

    Ok(())
}

#[test]
fn wait_for_signal_succeeds_without_waiters() -> Result<(), Box<dyn Error>> {
    let harness = CliHarness::new("wait-for-signal")?;
    let _daemon = harness.start_hidden_daemon()?;

    let output = harness.run(&["wait-for", "-S", "no-waiters"])?;

    assert_success(&output);
    Ok(())
}

fn wait_for_if_shell_capture(
    harness: &CliHarness,
    marker: &str,
) -> Result<std::process::Output, Box<dyn Error>> {
    let mut last = None;
    for _ in 0..100 {
        let output = harness.run(&["if-shell", "-F", "1", "capture-pane -p -t alpha:0.0"])?;
        if output.status.code() == Some(0) && stdout(&output).contains(marker) {
            return Ok(output);
        }
        last = Some(output);
        std::thread::sleep(Duration::from_millis(20));
    }

    let last = last.expect("capture was attempted");
    Err(format!(
        "if-shell capture output never contained marker {marker}; status={:?} stdout={:?} stderr={:?}",
        last.status.code(),
        stdout(&last),
        stderr(&last)
    )
    .into())
}