rmux 0.7.1

A local terminal multiplexer with a tmux-style CLI, daemon runtime, Rust SDK, and ratatui integration.
use std::ffi::{OsStr, OsString};
use std::io::{self, ErrorKind, Write};
use std::path::Path;

use super::ExitFailure;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum DropinInvocation {
    DoctorTmuxDropin,
    SetupTmuxShim,
}

pub(super) fn parse_invocation(
    arguments: &[OsString],
) -> Result<Option<DropinInvocation>, ExitFailure> {
    let Some(command_index) = split_top_level_prefix(arguments) else {
        return Ok(None);
    };
    let Some(command) = arguments
        .get(command_index)
        .and_then(|value| value.to_str())
    else {
        return Ok(None);
    };

    match command {
        "doctor" => parse_doctor(&arguments[command_index + 1..]).map(Some),
        "setup" => parse_setup(&arguments[command_index + 1..]).map(Some),
        _ => Ok(None),
    }
}

pub(super) fn run(
    invocation: DropinInvocation,
    argv0: Option<&OsString>,
) -> Result<i32, ExitFailure> {
    match invocation {
        DropinInvocation::DoctorTmuxDropin => run_doctor(argv0),
        DropinInvocation::SetupTmuxShim => run_setup_tmux_shim(),
    }
}

fn parse_doctor(arguments: &[OsString]) -> Result<DropinInvocation, ExitFailure> {
    if arguments
        .first()
        .and_then(|argument| argument.to_str())
        .is_some_and(|argument| argument == "--help")
    {
        return Err(ExitFailure::new_stdout(0, "usage: rmux doctor tmux-dropin"));
    }
    match single_subcommand(arguments, "doctor", "tmux-dropin")? {
        "tmux-dropin" => Ok(DropinInvocation::DoctorTmuxDropin),
        other => Err(ExitFailure::new(
            1,
            format!("rmux doctor: unknown check '{other}'"),
        )),
    }
}

fn parse_setup(arguments: &[OsString]) -> Result<DropinInvocation, ExitFailure> {
    if arguments
        .first()
        .and_then(|argument| argument.to_str())
        .is_some_and(|argument| argument == "--help")
    {
        return Err(ExitFailure::new_stdout(0, "usage: rmux setup tmux-shim"));
    }
    match single_subcommand(arguments, "setup", "tmux-shim")? {
        "tmux-shim" => Ok(DropinInvocation::SetupTmuxShim),
        other => Err(ExitFailure::new(
            1,
            format!("rmux setup: unknown action '{other}'"),
        )),
    }
}

fn single_subcommand<'a>(
    arguments: &'a [OsString],
    command: &str,
    expected: &str,
) -> Result<&'a str, ExitFailure> {
    let Some(subcommand) = arguments.first().and_then(|value| value.to_str()) else {
        return Err(ExitFailure::new(
            1,
            format!("rmux {command}: expected {expected}"),
        ));
    };
    if arguments.len() != 1 {
        return Err(ExitFailure::new(
            1,
            format!("rmux {command}: expected exactly one argument"),
        ));
    }
    Ok(subcommand)
}

fn split_top_level_prefix(arguments: &[OsString]) -> Option<usize> {
    let mut index = 0;

    while let Some(argument) = arguments.get(index) {
        let value = argument.to_str()?;
        if value == "--" {
            return Some(index + 1);
        }
        if !value.starts_with('-') || value == "-" {
            return Some(index);
        }

        match value {
            "-2" | "-D" | "-N" | "-l" | "-u" => {}
            "-C" | "-v" => {}
            "-c" | "-f" | "-L" | "-S" | "-T" => {
                index += 1;
            }
            _ if value.starts_with("-L") && value.len() > 2 => {}
            _ if value.starts_with("-S") && value.len() > 2 => {}
            _ if value.starts_with("-c") && value.len() > 2 => {}
            _ if value.starts_with("-f") && value.len() > 2 => {}
            _ if value.starts_with("-T") && value.len() > 2 => {}
            _ if is_short_flag_cluster(value, "2CDNluv") => {}
            _ => return Some(index),
        }

        index += 1;
    }

    None
}

fn is_short_flag_cluster(value: &str, allowed: &str) -> bool {
    value.len() > 2
        && value.starts_with('-')
        && !value.starts_with("--")
        && value.chars().skip(1).all(|flag| allowed.contains(flag))
}

fn run_doctor(argv0: Option<&OsString>) -> Result<i32, ExitFailure> {
    let argv0_name = argv0
        .and_then(|value| Path::new(value).file_name())
        .and_then(OsStr::to_str)
        .unwrap_or("rmux");
    let shim_detected = Path::new(argv0_name)
        .file_stem()
        .and_then(OsStr::to_str)
        .is_some_and(|stem| stem == "tmux");
    let shim = if shim_detected {
        "detected"
    } else {
        "not detected"
    };

    let mut output = String::new();
    output.push_str("rmux tmux-dropin doctor\n");
    output.push_str(&format!("shim:        {shim}   (argv[0]={argv0_name})\n"));
    if !shim_detected {
        output.push_str("suggested:   ln -s $(command -v rmux) ~/.local/bin/tmux\n");
        output.push_str("setup:       rmux setup tmux-shim\n");
    }
    write_stdout(&output, "doctor")
}

#[cfg(unix)]
fn run_setup_tmux_shim() -> Result<i32, ExitFailure> {
    use std::fs;
    use std::os::unix::fs::symlink;
    use std::path::PathBuf;

    let home = std::env::var_os("HOME")
        .filter(|value| !value.is_empty())
        .ok_or_else(|| ExitFailure::new(1, "rmux setup tmux-shim: HOME is not set"))?;
    let bin_dir = PathBuf::from(home).join(".local").join("bin");
    fs::create_dir_all(&bin_dir).map_err(|error| {
        ExitFailure::new(
            1,
            format!(
                "rmux setup tmux-shim: failed to create '{}': {error}",
                bin_dir.display()
            ),
        )
    })?;

    let target = std::env::current_exe().map_err(|error| {
        ExitFailure::new(
            1,
            format!("rmux setup tmux-shim: failed to resolve current rmux binary: {error}"),
        )
    })?;
    let shim = bin_dir.join("tmux");
    match fs::symlink_metadata(&shim) {
        Ok(metadata) if metadata.file_type().is_symlink() && symlink_points_to(&shim, &target) => {
            write_stdout(
                &format!("exists:      {} -> {}\n", shim.display(), target.display()),
                "setup tmux-shim",
            )
        }
        Ok(_) => Err(ExitFailure::new(
            1,
            format!(
                "rmux setup tmux-shim: '{}' already exists; refusing to overwrite",
                shim.display()
            ),
        )),
        Err(error) if error.kind() == io::ErrorKind::NotFound => {
            symlink(&target, &shim).map_err(|error| {
                ExitFailure::new(
                    1,
                    format!(
                        "rmux setup tmux-shim: failed to create '{}': {error}",
                        shim.display()
                    ),
                )
            })?;
            write_stdout(
                &format!(
                    "created:     {} -> {}\nnext:        ensure ~/.local/bin is before tmux in PATH\n",
                    shim.display(),
                    target.display()
                ),
                "setup tmux-shim",
            )
        }
        Err(error) => Err(ExitFailure::new(
            1,
            format!(
                "rmux setup tmux-shim: failed to inspect '{}': {error}",
                shim.display()
            ),
        )),
    }
}

#[cfg(unix)]
fn symlink_points_to(shim: &Path, target: &Path) -> bool {
    let Ok(link_target) = std::fs::read_link(shim) else {
        return false;
    };
    let resolved = if link_target.is_absolute() {
        link_target
    } else {
        shim.parent()
            .unwrap_or_else(|| Path::new("."))
            .join(link_target)
    };
    paths_resolve_to_same_file(&resolved, target)
}

#[cfg(unix)]
fn paths_resolve_to_same_file(left: &Path, right: &Path) -> bool {
    let Ok(left) = std::fs::canonicalize(left) else {
        return false;
    };
    let Ok(right) = std::fs::canonicalize(right) else {
        return false;
    };
    left == right
}

#[cfg(not(unix))]
fn run_setup_tmux_shim() -> Result<i32, ExitFailure> {
    Err(ExitFailure::new(
        1,
        "rmux setup tmux-shim is only supported on Unix-like systems",
    ))
}

fn write_stdout(output: &str, context: &str) -> Result<i32, ExitFailure> {
    match io::stdout().lock().write_all(output.as_bytes()) {
        Ok(()) => Ok(0),
        Err(error) if error.kind() == ErrorKind::BrokenPipe => Ok(0),
        Err(error) => Err(ExitFailure::new(
            1,
            format!("failed to write {context} output: {error}"),
        )),
    }
}

#[cfg(test)]
mod tests {
    use super::{parse_invocation, DropinInvocation};
    use std::ffi::OsString;

    fn args(values: &[&str]) -> Vec<OsString> {
        values.iter().map(OsString::from).collect()
    }

    #[test]
    fn parses_doctor_after_top_level_socket_flags() {
        let invocation = parse_invocation(&args(&["-Ldemo", "doctor", "tmux-dropin"]))
            .expect("parse succeeds")
            .expect("drop-in invocation");

        assert_eq!(invocation, DropinInvocation::DoctorTmuxDropin);
    }

    #[test]
    fn parses_doctor_after_glued_start_directory_flag() {
        let invocation = parse_invocation(&args(&["-c/tmp", "doctor", "tmux-dropin"]))
            .expect("parse succeeds")
            .expect("drop-in invocation");

        assert_eq!(invocation, DropinInvocation::DoctorTmuxDropin);
    }

    #[test]
    fn parses_setup_tmux_shim() {
        let invocation = parse_invocation(&args(&["setup", "tmux-shim"]))
            .expect("parse succeeds")
            .expect("drop-in invocation");

        assert_eq!(invocation, DropinInvocation::SetupTmuxShim);
    }

    #[test]
    fn ignores_other_commands() {
        assert!(parse_invocation(&args(&["list-sessions"]))
            .expect("parse succeeds")
            .is_none());
    }
}