rmux 0.1.2

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

use std::error::Error;
use std::fs::{self, File};
use std::process::{Child, Command, ExitStatus, Stdio};
use std::thread;
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};

use rmux_proto::{CONTROL_CONTROL_END, CONTROL_CONTROL_START};

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

#[test]
fn control_control_mode_uses_tmux_text_protocol() -> Result<(), Box<dyn Error>> {
    let label = unique_label("control-mode-windows")?;
    let _server = ServerGuard::new(label.clone());

    assert_status_success(
        rmux_command()
            .args(["-L", &label, "new-session", "-d", "-s", "alpha"])
            .stdin(Stdio::null())
            .stdout(Stdio::null())
            .stderr(Stdio::null())
            .status()?,
        "create detached session",
    )?;

    let input_path = temp_file_path(&label, "in");
    let output_path = temp_file_path(&label, "out");
    let error_path = temp_file_path(&label, "err");
    fs::write(&input_path, b"list-sessions\nbad-command\n")?;

    let mut child = rmux_command()
        .args(["-L", &label, "-CC"])
        .stdin(Stdio::from(File::open(&input_path)?))
        .stdout(Stdio::from(File::create(&output_path)?))
        .stderr(Stdio::from(File::create(&error_path)?))
        .spawn()?;
    let status = wait_for_child_exit(&mut child, CONTROL_TIMEOUT)?;
    let rendered = fs::read_to_string(&output_path)?;
    let stderr = fs::read_to_string(&error_path)?;
    let _ = fs::remove_file(&input_path);
    let _ = fs::remove_file(&output_path);
    let _ = fs::remove_file(&error_path);

    assert_eq!(status.code(), Some(0));
    assert!(
        stderr.is_empty(),
        "control-mode stderr should stay empty, got: {stderr:?}"
    );
    assert!(rendered.starts_with(CONTROL_CONTROL_START));
    assert!(rendered.contains("%begin "));
    assert!(rendered.contains("%end "));
    assert!(rendered.contains("%error "));
    assert!(rendered.contains("parse error:"));
    assert!(rendered.contains("bad-command"));
    assert!(rendered.contains("alpha"));
    assert!(rendered.contains("%exit"));
    assert!(rendered.ends_with(CONTROL_CONTROL_END));

    Ok(())
}

#[test]
fn plain_control_mode_exits_after_stdin_eof_without_empty_line() -> Result<(), Box<dyn Error>> {
    let label = unique_label("plain-control-mode-windows")?;
    let _server = ServerGuard::new(label.clone());

    assert_status_success(
        rmux_command()
            .args(["-L", &label, "new-session", "-d", "-s", "alpha"])
            .stdin(Stdio::null())
            .stdout(Stdio::null())
            .stderr(Stdio::null())
            .status()?,
        "create detached session",
    )?;

    let input_path = temp_file_path(&label, "in");
    let output_path = temp_file_path(&label, "out");
    let error_path = temp_file_path(&label, "err");
    fs::write(&input_path, b"list-sessions\n")?;

    let mut child = rmux_command()
        .args(["-L", &label, "-C"])
        .stdin(Stdio::from(File::open(&input_path)?))
        .stdout(Stdio::from(File::create(&output_path)?))
        .stderr(Stdio::from(File::create(&error_path)?))
        .spawn()?;
    let status = wait_for_child_exit(&mut child, CONTROL_TIMEOUT)?;
    let rendered = fs::read_to_string(&output_path)?;
    let stderr = fs::read_to_string(&error_path)?;
    let _ = fs::remove_file(&input_path);
    let _ = fs::remove_file(&output_path);
    let _ = fs::remove_file(&error_path);

    assert_eq!(status.code(), Some(0));
    assert!(
        stderr.is_empty(),
        "control-mode stderr should stay empty, got: {stderr:?}"
    );
    assert!(!rendered.starts_with(CONTROL_CONTROL_START));
    assert!(rendered.contains("%begin "));
    assert!(rendered.contains("%end "));
    assert!(rendered.contains("alpha"));
    assert!(rendered.contains("%exit"));

    Ok(())
}

fn rmux_command() -> Command {
    Command::new(env!("CARGO_BIN_EXE_rmux"))
}

fn unique_label(prefix: &str) -> Result<String, Box<dyn Error>> {
    let now = SystemTime::now().duration_since(UNIX_EPOCH)?.as_millis();
    Ok(format!("{prefix}-{}-{now}", std::process::id()))
}

fn temp_file_path(label: &str, extension: &str) -> std::path::PathBuf {
    std::env::temp_dir().join(format!("{label}.{extension}"))
}

fn assert_status_success(status: ExitStatus, context: &str) -> Result<(), Box<dyn Error>> {
    if status.success() {
        return Ok(());
    }

    Err(format!("{context} failed with status {:?}", status.code()).into())
}

fn wait_for_child_exit(child: &mut Child, timeout: Duration) -> Result<ExitStatus, Box<dyn Error>> {
    let deadline = Instant::now() + timeout;
    loop {
        if let Some(status) = child.try_wait()? {
            return Ok(status);
        }
        if Instant::now() >= deadline {
            child.kill()?;
            let _ = child.wait();
            return Err("control-mode client did not exit".into());
        }
        thread::sleep(Duration::from_millis(25));
    }
}

struct ServerGuard {
    label: String,
}

impl ServerGuard {
    fn new(label: String) -> Self {
        Self { label }
    }
}

impl Drop for ServerGuard {
    fn drop(&mut self) {
        let _ = rmux_command()
            .args(["-L", &self.label, "kill-server"])
            .stdout(Stdio::null())
            .stderr(Stdio::null())
            .status();
    }
}