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::path::Path;
use std::time::{Duration, Instant};

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

const FILE_TIMEOUT: Duration = Duration::from_secs(5);

#[test]
fn pane_transfer_commands_round_trip_through_the_binary() -> Result<(), Box<dyn Error>> {
    let harness = CliHarness::new("pane-transfer-binary")?;
    let mut daemon = harness.start_hidden_daemon()?;
    let join_path = harness.tmpdir().join("join.txt");
    let break_path = harness.tmpdir().join("break.txt");
    let swap_source_path = harness.tmpdir().join("swap-source.txt");
    let swap_target_path = harness.tmpdir().join("swap-target.txt");

    assert_success(&harness.run(&["new-session", "-d", "-s", "alpha"])?);
    assert_success(&harness.run(&["split-window", "-v", "-t", "alpha"])?);
    assert_success(&harness.run(&["select-pane", "-t", "alpha:0.1"])?);
    assert_success(&harness.run(&["select-pane", "-t", "alpha:0.0"])?);
    assert_success(&harness.run(&["last-pane", "-t", "alpha:0"])?);
    assert_success(&harness.run(&[
        "send-keys",
        "-t",
        "alpha:0.1",
        "export RMUX_TRANSFER_MARK=joined",
        "Enter",
    ])?);

    assert_success(&harness.run(&["new-window", "-t", "alpha", "-d", "-n", "dest"])?);
    assert_success(&harness.run(&["join-pane", "-d", "-s", "alpha:0.1", "-t", "alpha:1.0"])?);
    assert_success(&harness.run(&[
        "send-keys",
        "-t",
        "alpha:1.1",
        &format!(
            "printf \"$RMUX_TRANSFER_MARK\" > {}",
            shell_quote(&join_path)
        ),
        "Enter",
    ])?);
    wait_for_file_contents(&join_path, "joined", FILE_TIMEOUT)?;

    assert_success(&harness.run(&[
        "break-pane",
        "-d",
        "-s",
        "alpha:1.1",
        "-t",
        "alpha:2",
        "-n",
        "broken",
    ])?);
    assert_success(&harness.run(&[
        "send-keys",
        "-t",
        "alpha:2.0",
        &format!(
            "printf \"$RMUX_TRANSFER_MARK\" > {}",
            shell_quote(&break_path)
        ),
        "Enter",
    ])?);
    wait_for_file_contents(&break_path, "joined", FILE_TIMEOUT)?;

    assert_success(&harness.run(&["new-window", "-t", "alpha", "-d", "-n", "swap"])?);
    assert_success(&harness.run(&["split-window", "-v", "-t", "alpha:3.0"])?);
    assert_success(&harness.run(&["kill-pane", "-t", "alpha:3.0"])?);
    assert_success(&harness.run(&[
        "send-keys",
        "-t",
        "alpha:3.0",
        "export RMUX_TRANSFER_MARK=swapped",
        "Enter",
    ])?);
    assert_success(&harness.run(&["swap-pane", "-d", "-s", "alpha:2.0", "-t", "alpha:3.0"])?);
    assert_success(&harness.run(&[
        "send-keys",
        "-t",
        "alpha:2.0",
        &format!(
            "printf \"$RMUX_TRANSFER_MARK\" > {}",
            shell_quote(&swap_source_path)
        ),
        "Enter",
    ])?);
    assert_success(&harness.run(&[
        "send-keys",
        "-t",
        "alpha:3.0",
        &format!(
            "printf \"$RMUX_TRANSFER_MARK\" > {}",
            shell_quote(&swap_target_path)
        ),
        "Enter",
    ])?);
    wait_for_file_contents(&swap_source_path, "swapped", FILE_TIMEOUT)?;
    wait_for_file_contents(&swap_target_path, "joined", FILE_TIMEOUT)?;

    assert_success(&harness.run(&["new-window", "-t", "alpha", "-d", "-n", "relative"])?);
    assert_success(&harness.run(&["split-window", "-v", "-t", "alpha:4.0"])?);
    let relative = harness.run(&["swap-pane", "-U", "-t", "alpha:4.1"])?;
    assert_success(&relative);
    assert!(
        stderr(&relative).is_empty(),
        "relative swap-pane should stay silent on success"
    );

    terminate_child(daemon.child_mut())?;
    Ok(())
}

fn wait_for_file_contents(
    path: &Path,
    expected: &str,
    timeout: Duration,
) -> Result<(), Box<dyn Error>> {
    let deadline = Instant::now() + timeout;

    while Instant::now() < deadline {
        match fs::read_to_string(path) {
            Ok(contents) if contents == expected => return Ok(()),
            Ok(_) | Err(_) => std::thread::sleep(Duration::from_millis(25)),
        }
    }

    Err(std::io::Error::other(format!(
        "timed out waiting for '{}' to contain '{}'",
        path.display(),
        expected
    ))
    .into())
}

fn shell_quote(path: &Path) -> String {
    let path = path.display().to_string();
    format!("'{}'", path.replace('\'', r"'\''"))
}