use std::path::PathBuf;
use anyhow::{Context, Result};
use crate::config::{TerminalChoice, VisualMode};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AttachTarget {
pub session_name: String,
pub socket_path: PathBuf,
pub cols: u16,
pub rows: u16,
pub exe: PathBuf,
}
impl AttachTarget {
#[must_use]
pub fn attach_command(&self) -> String {
format!(
"{} {} {} --socket {} --size {}x{}",
shell_quote(&self.exe.to_string_lossy()),
crate::shells::attach::INTERNAL_ATTACH_FLAG,
shell_quote(&self.session_name),
shell_quote(&self.socket_path.to_string_lossy()),
self.cols,
self.rows,
)
}
#[must_use]
pub fn attach_argv(&self) -> Vec<String> {
vec![
self.exe.to_string_lossy().into_owned(),
crate::shells::attach::INTERNAL_ATTACH_FLAG.to_string(),
self.session_name.clone(),
"--socket".to_string(),
self.socket_path.to_string_lossy().into_owned(),
"--size".to_string(),
format!("{}x{}", self.cols, self.rows),
]
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PresentCommand {
pub program: String,
pub args: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Presentation {
Spawned,
Headless,
AttachCommand(String),
}
fn shell_quote(value: &str) -> String {
#[cfg(windows)]
{
let escaped = value.replace('"', "\"\"");
format!("\"{escaped}\"")
}
#[cfg(not(windows))]
{
let escaped = value.replace('\'', "'\\''");
format!("'{escaped}'")
}
}
#[must_use]
pub fn build_present_command(
mode: VisualMode,
terminal: TerminalChoice,
target: &AttachTarget,
) -> Option<PresentCommand> {
match mode {
VisualMode::Headless | VisualMode::Web => None,
VisualMode::Current => build_for_surface(terminal, target, Surface::Tab),
VisualMode::Window => build_for_surface(terminal, target, Surface::Window),
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Surface {
Tab,
Window,
}
#[cfg(target_os = "macos")]
fn build_for_surface(
terminal: TerminalChoice,
target: &AttachTarget,
surface: Surface,
) -> Option<PresentCommand> {
let attach = target.attach_command();
let use_iterm = match terminal {
TerminalChoice::Iterm2 => true,
TerminalChoice::TerminalApp => false,
TerminalChoice::Auto => detected_macos_is_iterm(),
_ => false,
};
let script = if use_iterm {
macos_iterm_script(&attach, surface)
} else {
macos_terminal_app_script(&attach, surface)
};
Some(PresentCommand {
program: "osascript".to_string(),
args: vec!["-e".to_string(), script],
})
}
#[cfg(target_os = "macos")]
fn detected_macos_is_iterm() -> bool {
std::env::var("TERM_PROGRAM")
.map(|v| v == "iTerm.app")
.unwrap_or(false)
}
#[cfg(target_os = "macos")]
fn macos_iterm_script(attach: &str, surface: Surface) -> String {
let escaped = applescript_quote(attach);
match surface {
Surface::Tab => format!(
"tell application \"iTerm2\"\n\
tell current window to create tab with default profile\n\
tell current session of current window to write text {escaped}\n\
end tell"
),
Surface::Window => format!(
"tell application \"iTerm2\"\n\
set newWindow to (create window with default profile)\n\
tell current session of newWindow to write text {escaped}\n\
end tell"
),
}
}
#[cfg(target_os = "macos")]
fn macos_terminal_app_script(attach: &str, surface: Surface) -> String {
let escaped = applescript_quote(attach);
match surface {
Surface::Tab => format!(
"tell application \"Terminal\"\n\
activate\n\
do script {escaped} in front window\n\
end tell"
),
Surface::Window => format!(
"tell application \"Terminal\"\n\
activate\n\
do script {escaped}\n\
end tell"
),
}
}
#[cfg(target_os = "macos")]
fn applescript_quote(value: &str) -> String {
let mut escaped = String::with_capacity(value.len() + 2);
escaped.push('"');
for ch in value.chars() {
match ch {
'\\' => escaped.push_str("\\\\"),
'"' => escaped.push_str("\\\""),
'\n' => escaped.push_str("\\n"),
'\r' => escaped.push_str("\\r"),
'\t' => escaped.push_str("\\t"),
c if (c as u32) < 0x20 => escaped.push(' '),
c => escaped.push(c),
}
}
escaped.push('"');
escaped
}
#[cfg(target_os = "windows")]
fn build_for_surface(
terminal: TerminalChoice,
target: &AttachTarget,
surface: Surface,
) -> Option<PresentCommand> {
match terminal {
TerminalChoice::WindowsTerminal | TerminalChoice::Auto => {}
_ => return None,
}
Some(windows_terminal_command(target, surface))
}
#[cfg(target_os = "windows")]
fn windows_terminal_command(target: &AttachTarget, surface: Surface) -> PresentCommand {
let window = match surface {
Surface::Tab => "0",
Surface::Window => "new",
};
let mut args = vec![
"-w".to_string(),
window.to_string(),
"new-tab".to_string(),
"--".to_string(),
];
args.extend(target.attach_argv());
PresentCommand {
program: "wt.exe".to_string(),
args,
}
}
#[cfg(not(any(target_os = "macos", target_os = "windows")))]
fn build_for_surface(
terminal: TerminalChoice,
target: &AttachTarget,
surface: Surface,
) -> Option<PresentCommand> {
let attach = target.attach_command();
let emulator = resolve_linux_emulator(terminal);
Some(linux_command(emulator, &attach, surface))
}
#[cfg(not(any(target_os = "macos", target_os = "windows")))]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum LinuxEmulator {
GnomeTerminal,
Konsole,
Wezterm,
Alacritty,
Kitty,
Xterm,
}
#[cfg(not(any(target_os = "macos", target_os = "windows")))]
fn resolve_linux_emulator(terminal: TerminalChoice) -> LinuxEmulator {
match terminal {
TerminalChoice::GnomeTerminal => LinuxEmulator::GnomeTerminal,
TerminalChoice::Konsole => LinuxEmulator::Konsole,
TerminalChoice::Wezterm => LinuxEmulator::Wezterm,
TerminalChoice::Alacritty => LinuxEmulator::Alacritty,
TerminalChoice::Kitty => LinuxEmulator::Kitty,
TerminalChoice::Xterm => LinuxEmulator::Xterm,
TerminalChoice::Auto
| TerminalChoice::Iterm2
| TerminalChoice::TerminalApp
| TerminalChoice::WindowsTerminal => detect_linux_emulator(),
}
}
#[cfg(not(any(target_os = "macos", target_os = "windows")))]
fn detect_linux_emulator() -> LinuxEmulator {
if let Ok(term) = std::env::var("TERMINAL")
&& let Some(found) = match_emulator_name(&term)
{
return found;
}
if let Ok(term) = std::env::var("TERM_PROGRAM")
&& let Some(found) = match_emulator_name(&term)
{
return found;
}
LinuxEmulator::Xterm
}
#[cfg(not(any(target_os = "macos", target_os = "windows")))]
fn match_emulator_name(name: &str) -> Option<LinuxEmulator> {
let lower = name.to_ascii_lowercase();
if lower.contains("gnome-terminal") {
Some(LinuxEmulator::GnomeTerminal)
} else if lower.contains("konsole") {
Some(LinuxEmulator::Konsole)
} else if lower.contains("wezterm") {
Some(LinuxEmulator::Wezterm)
} else if lower.contains("alacritty") {
Some(LinuxEmulator::Alacritty)
} else if lower.contains("kitty") {
Some(LinuxEmulator::Kitty)
} else if lower.contains("xterm") {
Some(LinuxEmulator::Xterm)
} else {
None
}
}
#[cfg(not(any(target_os = "macos", target_os = "windows")))]
fn linux_command(emulator: LinuxEmulator, attach: &str, surface: Surface) -> PresentCommand {
let want_tab = surface == Surface::Tab;
match emulator {
LinuxEmulator::GnomeTerminal => {
let mut args = vec![if want_tab { "--tab" } else { "--window" }.to_string()];
args.push("--".to_string());
args.push("bash".to_string());
args.push("-lc".to_string());
args.push(attach.to_string());
PresentCommand {
program: "gnome-terminal".to_string(),
args,
}
}
LinuxEmulator::Konsole => {
let flag = if want_tab { "--new-tab" } else { "" };
let mut args = Vec::new();
if !flag.is_empty() {
args.push(flag.to_string());
}
args.push("-e".to_string());
args.push("bash".to_string());
args.push("-lc".to_string());
args.push(attach.to_string());
PresentCommand {
program: "konsole".to_string(),
args,
}
}
LinuxEmulator::Wezterm => {
let args = if want_tab {
vec![
"cli".to_string(),
"spawn".to_string(),
"--".to_string(),
"bash".to_string(),
"-lc".to_string(),
attach.to_string(),
]
} else {
vec![
"start".to_string(),
"--".to_string(),
"bash".to_string(),
"-lc".to_string(),
attach.to_string(),
]
};
PresentCommand {
program: "wezterm".to_string(),
args,
}
}
LinuxEmulator::Kitty => {
let args = if want_tab {
vec![
"@".to_string(),
"launch".to_string(),
"--type=tab".to_string(),
"bash".to_string(),
"-lc".to_string(),
attach.to_string(),
]
} else {
vec!["bash".to_string(), "-lc".to_string(), attach.to_string()]
};
PresentCommand {
program: "kitty".to_string(),
args,
}
}
LinuxEmulator::Alacritty => PresentCommand {
program: "alacritty".to_string(),
args: vec![
"-e".to_string(),
"bash".to_string(),
"-lc".to_string(),
attach.to_string(),
],
},
LinuxEmulator::Xterm => PresentCommand {
program: "xterm".to_string(),
args: vec![
"-e".to_string(),
"bash".to_string(),
"-lc".to_string(),
attach.to_string(),
],
},
}
}
pub fn present(
mode: VisualMode,
terminal: TerminalChoice,
target: &AttachTarget,
) -> Result<Presentation> {
if mode == VisualMode::Headless {
return Ok(Presentation::Headless);
}
let Some(command) = build_present_command(mode, terminal, target) else {
return Ok(Presentation::AttachCommand(target.attach_command()));
};
std::process::Command::new(&command.program)
.args(&command.args)
.spawn()
.with_context(|| format!("spawning terminal `{}` to present session", command.program))?;
Ok(Presentation::Spawned)
}
#[cfg(test)]
mod tests {
use super::*;
fn target() -> AttachTarget {
AttachTarget {
session_name: "bmsh-1-2".to_string(),
socket_path: PathBuf::from("/tmp/rmux.sock"),
cols: 200,
rows: 50,
exe: PathBuf::from("/usr/local/bin/basemind"),
}
}
#[cfg(not(windows))]
const EXPECTED_ATTACH: &str = "'/usr/local/bin/basemind' --__internal-attach 'bmsh-1-2' --socket '/tmp/rmux.sock' \
--size 200x50";
#[cfg(not(windows))]
#[test]
fn attach_command_has_expected_shape() {
let cmd = target().attach_command();
assert_eq!(cmd, EXPECTED_ATTACH);
}
#[test]
fn headless_builds_no_command() {
assert!(
build_present_command(VisualMode::Headless, TerminalChoice::Auto, &target()).is_none()
);
}
#[test]
fn web_builds_no_command() {
assert!(build_present_command(VisualMode::Web, TerminalChoice::Auto, &target()).is_none());
}
#[test]
fn present_headless_is_headless() {
let out = present(VisualMode::Headless, TerminalChoice::Auto, &target()).expect("ok");
assert_eq!(out, Presentation::Headless);
}
#[test]
fn present_web_returns_attach_command() {
let out = present(VisualMode::Web, TerminalChoice::Auto, &target()).expect("ok");
assert_eq!(out, Presentation::AttachCommand(target().attach_command()));
}
#[cfg(target_os = "macos")]
#[test]
fn macos_iterm_tab_drives_osascript() {
let cmd = build_present_command(VisualMode::Current, TerminalChoice::Iterm2, &target())
.expect("command");
assert_eq!(cmd.program, "osascript");
assert_eq!(cmd.args.len(), 2);
assert_eq!(cmd.args[0], "-e");
let script = &cmd.args[1];
assert!(script.contains("iTerm2"), "script: {script}");
assert!(script.contains("create tab"), "script: {script}");
assert!(
script.contains("--__internal-attach 'bmsh-1-2'"),
"script: {script}"
);
assert!(
script.contains("--socket '/tmp/rmux.sock'"),
"script: {script}"
);
assert!(script.contains("--size 200x50"), "script: {script}");
}
#[cfg(target_os = "macos")]
#[test]
fn applescript_quote_neutralizes_control_chars() {
let quoted = applescript_quote("attach\nrm -rf /\0");
assert!(quoted.starts_with('"') && quoted.ends_with('"'), "{quoted}");
assert!(quoted.contains("\\n"), "newline must be escaped: {quoted}");
assert!(
!quoted[1..quoted.len() - 1].contains('\n'),
"no raw newline inside the literal: {quoted}"
);
assert!(!quoted.contains('\0'), "NUL must be stripped: {quoted}");
}
#[cfg(target_os = "macos")]
#[test]
fn macos_iterm_window_creates_window() {
let cmd = build_present_command(VisualMode::Window, TerminalChoice::Iterm2, &target())
.expect("command");
assert_eq!(cmd.program, "osascript");
assert!(cmd.args[1].contains("create window"), "{}", cmd.args[1]);
}
#[cfg(target_os = "macos")]
#[test]
fn macos_terminal_app_tab_uses_front_window() {
let cmd =
build_present_command(VisualMode::Current, TerminalChoice::TerminalApp, &target())
.expect("command");
assert_eq!(cmd.program, "osascript");
let script = &cmd.args[1];
assert!(script.contains("Terminal"), "{script}");
assert!(script.contains("do script"), "{script}");
assert!(script.contains("in front window"), "{script}");
}
#[cfg(target_os = "macos")]
#[test]
fn macos_terminal_app_window_omits_front_window() {
let cmd = build_present_command(VisualMode::Window, TerminalChoice::TerminalApp, &target())
.expect("command");
let script = &cmd.args[1];
assert!(script.contains("do script"), "{script}");
assert!(!script.contains("in front window"), "{script}");
}
#[cfg(target_os = "windows")]
#[test]
fn windows_current_builds_wt_new_tab_in_current_window() {
let cmd = build_present_command(
VisualMode::Current,
TerminalChoice::WindowsTerminal,
&target(),
)
.expect("wt.exe command");
assert_eq!(cmd.program, "wt.exe");
assert_eq!(cmd.args[0], "-w");
assert_eq!(cmd.args[1], "0");
assert_eq!(cmd.args[2], "new-tab");
assert_eq!(cmd.args[3], "--");
assert_eq!(cmd.args[4], "/usr/local/bin/basemind");
assert_eq!(cmd.args[5], "--__internal-attach");
assert_eq!(cmd.args[6], "bmsh-1-2");
assert_eq!(cmd.args[7], "--socket");
assert_eq!(cmd.args[8], "/tmp/rmux.sock");
assert_eq!(cmd.args[9], "--size");
assert_eq!(cmd.args[10], "200x50");
}
#[cfg(target_os = "windows")]
#[test]
fn windows_window_forces_a_fresh_wt_window() {
let cmd = build_present_command(
VisualMode::Window,
TerminalChoice::WindowsTerminal,
&target(),
)
.expect("wt.exe command");
assert_eq!(cmd.program, "wt.exe");
assert_eq!(cmd.args[0], "-w");
assert_eq!(cmd.args[1], "new");
assert_eq!(cmd.args[2], "new-tab");
}
#[cfg(target_os = "windows")]
#[test]
fn windows_auto_uses_wt() {
let cmd = build_present_command(VisualMode::Current, TerminalChoice::Auto, &target())
.expect("auto -> wt.exe");
assert_eq!(cmd.program, "wt.exe");
assert!(
build_present_command(VisualMode::Current, TerminalChoice::Xterm, &target()).is_none()
);
}
#[cfg(target_os = "windows")]
#[test]
fn windows_attach_command_uses_double_quotes() {
let cmd = target().attach_command();
assert!(cmd.contains("\"/usr/local/bin/basemind\""), "{cmd}");
assert!(cmd.contains("\"bmsh-1-2\""), "{cmd}");
assert!(!cmd.contains('\''), "no single quotes on Windows: {cmd}");
}
#[cfg(not(any(target_os = "macos", target_os = "windows")))]
#[test]
fn linux_gnome_terminal_tab() {
let cmd = build_present_command(
VisualMode::Current,
TerminalChoice::GnomeTerminal,
&target(),
)
.expect("command");
assert_eq!(cmd.program, "gnome-terminal");
assert_eq!(cmd.args[0], "--tab");
assert_eq!(cmd.args[1], "--");
assert_eq!(cmd.args[2], "bash");
assert_eq!(cmd.args[3], "-lc");
assert_eq!(cmd.args[4], EXPECTED_ATTACH);
}
#[cfg(not(any(target_os = "macos", target_os = "windows")))]
#[test]
fn linux_gnome_terminal_window() {
let cmd =
build_present_command(VisualMode::Window, TerminalChoice::GnomeTerminal, &target())
.expect("command");
assert_eq!(cmd.args[0], "--window");
}
#[cfg(not(any(target_os = "macos", target_os = "windows")))]
#[test]
fn linux_konsole_new_tab() {
let cmd = build_present_command(VisualMode::Current, TerminalChoice::Konsole, &target())
.expect("command");
assert_eq!(cmd.program, "konsole");
assert_eq!(cmd.args[0], "--new-tab");
assert_eq!(cmd.args[1], "-e");
}
#[cfg(not(any(target_os = "macos", target_os = "windows")))]
#[test]
fn linux_wezterm_tab_spawns_cli() {
let cmd = build_present_command(VisualMode::Current, TerminalChoice::Wezterm, &target())
.expect("command");
assert_eq!(cmd.program, "wezterm");
assert_eq!(cmd.args[0], "cli");
assert_eq!(cmd.args[1], "spawn");
}
#[cfg(not(any(target_os = "macos", target_os = "windows")))]
#[test]
fn linux_alacritty_falls_back_to_window_for_tab() {
let cmd = build_present_command(VisualMode::Current, TerminalChoice::Alacritty, &target())
.expect("command");
assert_eq!(cmd.program, "alacritty");
assert_eq!(cmd.args[0], "-e");
}
#[cfg(not(any(target_os = "macos", target_os = "windows")))]
#[test]
fn linux_xterm_is_the_fallback() {
let cmd = build_present_command(VisualMode::Current, TerminalChoice::Xterm, &target())
.expect("command");
assert_eq!(cmd.program, "xterm");
assert_eq!(cmd.args[0], "-e");
}
}