rmux-client 0.1.1

Blocking local client and attach-mode plumbing for the RMUX terminal multiplexer.
Documentation
use std::ffi::OsString;
use std::path::{Path, PathBuf};
use std::process::Command;

use rmux_proto::AttachShellCommand;

pub(super) fn command_from_spec(spec: &AttachShellCommand) -> Command {
    command_for_shell(
        Path::new(spec.shell()),
        Path::new(spec.cwd()),
        spec.command(),
    )
}

pub(super) fn command_from_legacy(command: &str) -> Command {
    let cwd = std::env::current_dir().unwrap_or_else(|_| fallback_cwd());
    let shell = std::env::var_os("SHELL")
        .or_else(|| std::env::var_os("COMSPEC"))
        .unwrap_or_else(|| OsString::from("cmd.exe"));
    command_for_shell(Path::new(&shell), &cwd, command)
}

fn command_for_shell(shell: &Path, cwd: &Path, command: &str) -> Command {
    let mut child = Command::new(shell);
    child.current_dir(cwd);
    match detect_shell_kind(shell) {
        ShellKind::Cmd => {
            child.arg("/D").arg("/S").arg("/C").arg(command);
        }
        ShellKind::PowerShell => {
            child.arg("-NoProfile").arg("-Command").arg(format!(
                "Set-Location -LiteralPath {}; {command}",
                powershell_single_quoted(cwd)
            ));
        }
        ShellKind::Posix => {
            child.arg("-lc").arg(command);
        }
        ShellKind::Nu => {
            child.arg("-c").arg(command);
        }
        ShellKind::Other => {
            child.arg(command);
        }
    }
    child
}

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum ShellKind {
    Cmd,
    PowerShell,
    Posix,
    Nu,
    Other,
}

fn detect_shell_kind(shell: &Path) -> ShellKind {
    match executable_name(shell)
        .as_deref()
        .map(str::to_ascii_lowercase)
        .as_deref()
    {
        Some("cmd.exe" | "cmd") => ShellKind::Cmd,
        Some("powershell.exe" | "powershell" | "pwsh.exe" | "pwsh") => ShellKind::PowerShell,
        Some("bash.exe" | "bash" | "sh.exe" | "sh" | "zsh.exe" | "zsh") => ShellKind::Posix,
        Some("nu.exe" | "nu") => ShellKind::Nu,
        _ => ShellKind::Other,
    }
}

fn executable_name(path: &Path) -> Option<String> {
    let name = path
        .file_name()
        .unwrap_or(path.as_os_str())
        .to_string_lossy();
    let trimmed = name.trim_start_matches('-');
    (!trimmed.is_empty()).then(|| trimmed.to_owned())
}

fn powershell_single_quoted(path: &Path) -> String {
    format!("'{}'", path.to_string_lossy().replace('\'', "''"))
}

fn fallback_cwd() -> PathBuf {
    std::env::var_os("USERPROFILE")
        .map(PathBuf::from)
        .unwrap_or_else(|| PathBuf::from(r"C:\"))
}

#[cfg(test)]
mod tests {
    use std::ffi::OsStr;

    use super::*;

    #[test]
    fn cmd_spec_uses_server_shell_and_preserves_payload() {
        let spec = AttachShellCommand::new(
            "echo lock command && exit /b 0".to_owned(),
            "cmd.exe".to_owned(),
            r"C:\work".to_owned(),
        );

        let child = command_from_spec(&spec);

        assert_eq!(args(&child), vec!["/D", "/S", "/C", spec.command()]);
        assert_eq!(child.get_current_dir(), Some(Path::new(r"C:\work")));
    }

    #[test]
    fn powershell_spec_uses_literal_cwd_wrapper() {
        let spec = AttachShellCommand::new(
            "Write-Output RMUX_OK".to_owned(),
            "pwsh.exe".to_owned(),
            r"C:\Users\RMUXUser's Workspace\rmux".to_owned(),
        );

        let child = command_from_spec(&spec);

        assert_eq!(
            args(&child),
            vec![
                "-NoProfile",
                "-Command",
                "Set-Location -LiteralPath 'C:\\Users\\RMUXUser''s Workspace\\rmux'; Write-Output RMUX_OK",
            ]
        );
    }

    #[test]
    fn posix_spec_uses_lc_instead_of_cmd_c() {
        let spec = AttachShellCommand::new(
            "echo RMUX_OK".to_owned(),
            "bash.exe".to_owned(),
            r"C:\work".to_owned(),
        );

        let child = command_from_spec(&spec);

        assert_eq!(args(&child), vec!["-lc", "echo RMUX_OK"]);
    }

    #[test]
    fn nushell_spec_uses_c_instead_of_cmd_c() {
        let spec = AttachShellCommand::new(
            "echo RMUX_OK".to_owned(),
            "nu.exe".to_owned(),
            r"C:\work".to_owned(),
        );

        let child = command_from_spec(&spec);

        assert_eq!(args(&child), vec!["-c", "echo RMUX_OK"]);
    }

    #[test]
    fn custom_shell_gets_only_the_payload() {
        let spec = AttachShellCommand::new(
            "echo RMUX_OK".to_owned(),
            "custom-shell.exe".to_owned(),
            r"C:\work".to_owned(),
        );

        let child = command_from_spec(&spec);

        assert_eq!(args(&child), vec!["echo RMUX_OK"]);
    }

    fn args(command: &Command) -> Vec<String> {
        command.get_args().map(os_to_string).collect()
    }

    fn os_to_string(value: &OsStr) -> String {
        value.to_string_lossy().into_owned()
    }
}