rmux 0.10.0

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};

#[derive(Debug, PartialEq, Eq)]
struct WindowSnapshot {
    name: String,
    pane_command: String,
    automatic_rename: String,
}

fn set_global_automatic_rename(harness: &CliHarness, enabled: bool) -> Result<(), Box<dyn Error>> {
    let value = if enabled { "on" } else { "off" };
    assert_success(&harness.run(&["set-option", "-gw", "automatic-rename", value])?);
    Ok(())
}

fn snapshot(harness: &CliHarness, target: &str) -> Result<WindowSnapshot, Box<dyn Error>> {
    let output = harness.run(&[
        "display-message",
        "-p",
        "-t",
        target,
        "#{window_name}|#{pane_current_command}|#{automatic-rename}",
    ])?;
    assert_eq!(
        output.status.code(),
        Some(0),
        "stdout={:?} stderr={:?}",
        stdout(&output),
        stderr(&output)
    );
    assert!(stderr(&output).is_empty(), "stderr={:?}", stderr(&output));
    let rendered = stdout(&output);
    let mut fields = rendered.trim_end_matches('\n').splitn(3, '|');
    Ok(WindowSnapshot {
        name: fields.next().unwrap_or_default().to_owned(),
        pane_command: fields.next().unwrap_or_default().to_owned(),
        automatic_rename: fields.next().unwrap_or_default().to_owned(),
    })
}

fn assert_useful_initial_name(snapshot: &WindowSnapshot, expected: &str) {
    assert_eq!(snapshot.name, expected, "snapshot={snapshot:?}");
    assert_eq!(snapshot.pane_command, expected, "snapshot={snapshot:?}");
}

#[test]
fn automatic_rename_off_assigns_initial_names_to_new_sessions() -> Result<(), Box<dyn Error>> {
    let harness = CliHarness::new("automatic-rename-initial-session")?;
    let _daemon = harness.start_hidden_daemon()?;

    assert_success(&harness.run(&["new-session", "-d", "-s", "seed", "-n", "seed", "sleep 60"])?);
    set_global_automatic_rename(&harness, false)?;

    for session in ["command-first", "command-next"] {
        assert_success(&harness.run(&["new-session", "-d", "-s", session, "sleep 60"])?);
    }
    assert_success(&harness.run(&["new-session", "-d", "-s", "default-shell"])?);
    assert_success(&harness.run(&[
        "new-session",
        "-d",
        "-s",
        "explicit",
        "-n",
        "fixed",
        "sleep 60",
    ])?);
    assert_useful_initial_name(&snapshot(&harness, "command-first:0")?, "sleep");
    assert_useful_initial_name(&snapshot(&harness, "command-next:0")?, "sleep");
    let shell = snapshot(&harness, "default-shell:0")?;
    assert!(!shell.name.is_empty(), "snapshot={shell:?}");
    assert_eq!(shell.name, shell.pane_command, "snapshot={shell:?}");
    assert_eq!(snapshot(&harness, "explicit:0")?.name, "fixed");

    std::thread::sleep(Duration::from_millis(150));
    assert_eq!(snapshot(&harness, "command-first:0")?.name, "sleep");
    Ok(())
}

#[test]
fn automatic_rename_initial_new_window_matrix_and_shared_queue() -> Result<(), Box<dyn Error>> {
    let harness = CliHarness::new("automatic-rename-initial-window")?;
    let _daemon = harness.start_hidden_daemon()?;

    assert_success(&harness.run(&[
        "new-session",
        "-d",
        "-s",
        "windows",
        "-n",
        "seed",
        "sleep 60",
    ])?);
    assert_success(&harness.run(&["new-window", "-d", "-t", "windows", "sleep 60"])?);

    let automatic_on = snapshot(&harness, "windows:1")?;
    assert!(!automatic_on.name.is_empty(), "snapshot={automatic_on:?}");

    set_global_automatic_rename(&harness, false)?;
    for _ in 0..2 {
        assert_success(&harness.run(&["new-window", "-d", "-t", "windows", "sleep 60"])?);
    }
    assert_success(&harness.run(&[
        "new-window",
        "-d",
        "-t",
        "windows",
        "-n",
        "fixed",
        "sleep 60",
    ])?);

    let source_file = harness.tmpdir().join("automatic-rename-queue.conf");
    fs::write(&source_file, "new-window -d -t windows sleep 60\n")?;
    assert_success(&harness.run(&[
        "source-file",
        source_file.to_str().expect("test path is utf-8"),
    ])?);
    assert_success(&harness.run(&["run-shell", "-C", "new-window -d -t windows sleep 60"])?);
    let control = harness.run(&["-C", "new-window", "-d", "-t", "windows", "sleep 60"])?;
    assert_eq!(
        control.status.code(),
        Some(0),
        "stdout={:?} stderr={:?}",
        stdout(&control),
        stderr(&control)
    );
    assert!(stderr(&control).is_empty(), "stderr={:?}", stderr(&control));

    assert_success(&harness.run(&[
        "split-window",
        "-d",
        "-t",
        "windows:2",
        "tail -f /dev/null",
    ])?);
    assert_success(&harness.run(&[
        "respawn-window",
        "-k",
        "-t",
        "windows:3",
        "tail -f /dev/null",
    ])?);

    assert_useful_initial_name(&snapshot(&harness, "windows:2")?, "sleep");
    assert_eq!(snapshot(&harness, "windows:2")?.automatic_rename, "0");
    assert_eq!(snapshot(&harness, "windows:3")?.name, "sleep");
    assert_eq!(snapshot(&harness, "windows:4")?.name, "fixed");
    assert_useful_initial_name(&snapshot(&harness, "windows:5")?, "sleep");
    assert_useful_initial_name(&snapshot(&harness, "windows:6")?, "sleep");
    assert_useful_initial_name(&snapshot(&harness, "windows:7")?, "sleep");
    Ok(())
}

#[test]
fn automatic_rename_off_assigns_initial_names_only_to_created_break_windows(
) -> Result<(), Box<dyn Error>> {
    let harness = CliHarness::new("automatic-rename-initial-break")?;
    let _daemon = harness.start_hidden_daemon()?;

    assert_success(&harness.run(&["new-session", "-d", "-s", "same", "-n", "seed", "sleep 60"])?);
    set_global_automatic_rename(&harness, false)?;
    for _ in 0..2 {
        assert_success(&harness.run(&["split-window", "-d", "-t", "same:0", "sleep 60"])?);
    }
    assert_success(&harness.run(&["break-pane", "-d", "-s", "same:0.2"])?);
    assert_success(&harness.run(&["break-pane", "-d", "-s", "same:0.1"])?);

    assert_useful_initial_name(&snapshot(&harness, "same:1")?, "sleep");
    assert_useful_initial_name(&snapshot(&harness, "same:2")?, "sleep");

    assert_success(&harness.run(&[
        "new-session",
        "-d",
        "-s",
        "shell-break",
        "-n",
        "seed",
        "sleep 60",
    ])?);
    assert_success(&harness.run(&["split-window", "-d", "-t", "shell-break:0"])?);
    assert_success(&harness.run(&["break-pane", "-d", "-s", "shell-break:0.1"])?);
    let shell = snapshot(&harness, "shell-break:1")?;
    assert!(!shell.name.is_empty(), "snapshot={shell:?}");
    assert_eq!(shell.name, shell.pane_command, "snapshot={shell:?}");

    assert_success(&harness.run(&[
        "new-session",
        "-d",
        "-s",
        "cross-source",
        "-n",
        "source",
        "sleep 60",
    ])?);
    assert_success(&harness.run(&[
        "new-session",
        "-d",
        "-s",
        "cross-target",
        "-n",
        "target",
        "sleep 60",
    ])?);
    assert_success(&harness.run(&["split-window", "-d", "-t", "cross-source:0", "sleep 60"])?);
    assert_success(&harness.run(&[
        "break-pane",
        "-d",
        "-s",
        "cross-source:0.1",
        "-t",
        "cross-target:",
    ])?);
    assert_useful_initial_name(&snapshot(&harness, "cross-target:1")?, "sleep");

    assert_success(&harness.run(&[
        "new-session",
        "-d",
        "-s",
        "mono-same",
        "-n",
        "carried-same",
        "sleep 60",
    ])?);
    assert_success(&harness.run(&[
        "break-pane",
        "-d",
        "-s",
        "mono-same:0.0",
        "-t",
        "mono-same:3",
    ])?);
    assert_eq!(snapshot(&harness, "mono-same:3")?.name, "carried-same");

    assert_success(&harness.run(&[
        "new-session",
        "-d",
        "-s",
        "mono-source",
        "-n",
        "carried-cross",
        "sleep 60",
    ])?);
    assert_success(&harness.run(&[
        "new-session",
        "-d",
        "-s",
        "mono-target",
        "-n",
        "target",
        "sleep 60",
    ])?);
    assert_success(&harness.run(&[
        "break-pane",
        "-d",
        "-s",
        "mono-source:0.0",
        "-t",
        "mono-target:",
    ])?);
    assert_eq!(snapshot(&harness, "mono-target:1")?.name, "carried-cross");

    assert_success(&harness.run(&[
        "new-session",
        "-d",
        "-s",
        "explicit-break",
        "-n",
        "seed",
        "sleep 60",
    ])?);
    assert_success(&harness.run(&["split-window", "-d", "-t", "explicit-break:0", "sleep 60"])?);
    assert_success(&harness.run(&[
        "break-pane",
        "-d",
        "-s",
        "explicit-break:0.1",
        "-n",
        "fixed",
    ])?);
    assert_eq!(snapshot(&harness, "explicit-break:1")?.name, "fixed");

    set_global_automatic_rename(&harness, true)?;
    assert_success(&harness.run(&[
        "new-session",
        "-d",
        "-s",
        "automatic-on",
        "-n",
        "seed",
        "sleep 60",
    ])?);
    assert_success(&harness.run(&["split-window", "-d", "-t", "automatic-on:0", "sleep 60"])?);
    assert_success(&harness.run(&["break-pane", "-d", "-s", "automatic-on:0.1"])?);
    let automatic_on = snapshot(&harness, "automatic-on:1")?;
    assert!(!automatic_on.name.is_empty(), "snapshot={automatic_on:?}");
    Ok(())
}

#[test]
fn explicit_empty_initial_names_are_preserved_product_divergence() -> Result<(), Box<dyn Error>> {
    let harness = CliHarness::new("automatic-rename-explicit-empty")?;
    let _daemon = harness.start_hidden_daemon()?;

    assert_success(&harness.run(&["new-session", "-d", "-s", "empty-on", "-n", "", "sleep 60"])?);
    assert_success(&harness.run(&["new-window", "-d", "-t", "empty-on", "-n", "", "sleep 60"])?);
    for target in ["empty-on:0", "empty-on:1"] {
        let snapshot = snapshot(&harness, target)?;
        assert_eq!(snapshot.name, "", "target={target}");
        assert_eq!(snapshot.automatic_rename, "0", "target={target}");
    }

    set_global_automatic_rename(&harness, false)?;
    assert_success(&harness.run(&[
        "new-session",
        "-d",
        "-s",
        "empty-off",
        "-n",
        "",
        "sleep 60",
    ])?);
    assert_success(&harness.run(&["new-window", "-d", "-t", "empty-off", "-n", "", "sleep 60"])?);
    assert_success(&harness.run(&["split-window", "-d", "-t", "empty-off:0", "sleep 60"])?);
    assert_success(&harness.run(&["break-pane", "-d", "-s", "empty-off:0.1", "-n", ""])?);

    for target in ["empty-off:0", "empty-off:1", "empty-off:2"] {
        let snapshot = snapshot(&harness, target)?;
        assert_eq!(snapshot.name, "", "target={target}");
        assert_eq!(snapshot.automatic_rename, "0", "target={target}");
    }
    Ok(())
}