Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ serde_json = "1"
dirs = "6"
chrono = { version = "0.4", features = ["serde"] }
tempfile = "3"
unicode-width = "0.2"

[target.'cfg(target_vendor = "apple")'.dependencies]
proc_pidinfo = "0.1"
Expand Down
288 changes: 281 additions & 7 deletions src/jump/cmux.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,29 +2,303 @@
//!
//! Each cmux surface exports `CMUX_WORKSPACE_ID` (a UUID), inherited by the
//! agent process. We read it from the process environment and focus the
//! workspace via the cmux CLI, which accepts the UUID directly as `--workspace`.
//! workspace via the cmux CLI's `workspace select` command.

use super::{pid_env_var, JumpAttempt, TerminalJumper};
use std::process::Command;

pub struct CmuxJumper;

#[derive(Debug, PartialEq, Eq)]
struct CmuxCommandPlan {
workspace_id: String,
terminal_id: Option<String>,
program: String,
args: Vec<String>,
envs: Vec<(String, String)>,
}

impl TerminalJumper for CmuxJumper {
fn name(&self) -> &'static str {
"cmux"
}

fn try_jump(&self, pid: u32) -> JumpAttempt {
let Some(workspace) = pid_env_var(pid, "CMUX_WORKSPACE_ID") else {
let Some(plan) = command_plan_from_env(|name| pid_env_var(pid, name)) else {
return JumpAttempt::NotApplicable;
};
match Command::new("cmux")
.args(["select-workspace", "--workspace", &workspace])
.output()
{
let mut command = Command::new(&plan.program);
command.args(&plan.args);
for key in cmux_env_removals(std::env::vars().map(|(key, _)| key)) {
command.env_remove(key);
}
command.envs(plan.envs.iter().map(|(key, value)| (key, value)));

match command.output() {
Ok(o) if o.status.success() => JumpAttempt::Jumped,
Ok(o) => JumpAttempt::Failed(format!("select-workspace exited {}", o.status)),
Ok(o) if command_output_has_broken_pipe(&o.stdout, &o.stderr) => {
jump_via_applescript_after_socket_failure(&plan)
}
Ok(o) => JumpAttempt::Failed(format_command_failure(
"workspace select",
&o.status.to_string(),
&o.stdout,
&o.stderr,
)),
Err(e) => JumpAttempt::Failed(format!("cmux CLI not runnable ({e})")),
}
}
}

fn command_plan_from_env(mut env: impl FnMut(&str) -> Option<String>) -> Option<CmuxCommandPlan> {
let workspace = non_empty(env("CMUX_WORKSPACE_ID"))?;
let terminal_id = non_empty(env("CMUX_PANEL_ID")).or_else(|| non_empty(env("CMUX_SURFACE_ID")));
let program = non_empty(env("CMUX_BUNDLED_CLI_PATH")).unwrap_or_else(|| "cmux".to_string());
let args = vec![
"workspace".to_string(),
"select".to_string(),
workspace.clone(),
];
let envs = ["CMUX_SOCKET_PATH", "CMUX_SOCKET", "CMUX_SOCKET_PASSWORD"]
.into_iter()
.filter_map(|name| non_empty(env(name)).map(|value| (name.to_string(), value)))
.collect();

Some(CmuxCommandPlan {
workspace_id: workspace,
terminal_id,
program,
args,
envs,
})
}

fn non_empty(value: Option<String>) -> Option<String> {
value.filter(|v| !v.is_empty())
}

fn cmux_env_removals(env_keys: impl IntoIterator<Item = String>) -> Vec<String> {
env_keys
.into_iter()
.filter(|key| key.starts_with("CMUX_"))
.collect()
}

fn jump_via_applescript(plan: &CmuxCommandPlan) -> JumpAttempt {
let script = applescript_focus_script(&plan.workspace_id, plan.terminal_id.as_deref());
match Command::new("osascript").arg("-e").arg(script).output() {
Ok(o) if o.status.success() => JumpAttempt::Jumped,
Ok(o) => JumpAttempt::Failed(format_command_failure(
"AppleScript focus",
&o.status.to_string(),
&o.stdout,
&o.stderr,
)),
Err(e) => JumpAttempt::Failed(format!("AppleScript not runnable ({e})")),
}
}

fn jump_via_applescript_after_socket_failure(plan: &CmuxCommandPlan) -> JumpAttempt {
jump_via_applescript_after_socket_failure_with(plan, jump_via_applescript)
}

fn jump_via_applescript_after_socket_failure_with(
plan: &CmuxCommandPlan,
jump: impl FnOnce(&CmuxCommandPlan) -> JumpAttempt,
) -> JumpAttempt {
match jump(plan) {
JumpAttempt::Failed(msg) if msg.starts_with("AppleScript not runnable") => {
JumpAttempt::Failed("socket broken; restart cmux".to_string())
}
attempt => attempt,
}
}

fn applescript_focus_script(workspace_id: &str, terminal_id: Option<&str>) -> String {
let workspace_id = applescript_string(workspace_id);
let focus_terminal = terminal_id.map(|id| {
format!(
"\n focus (first terminal of w whose id is {})",
applescript_string(id)
)
});

format!(
"tell application \"cmux\"\n repeat with w in windows\n repeat with candidate in tabs of w\n if id of candidate is {} then\n select tab candidate{}\n activate window w\n return true\n end if\n end repeat\n end repeat\n error \"cmux workspace not found\"\nend tell",
workspace_id,
focus_terminal.unwrap_or_default()
)
}

fn applescript_string(value: &str) -> String {
let escaped = value.replace('\\', "\\\\").replace('"', "\\\"");
format!("\"{escaped}\"")
}

fn format_command_failure(action: &str, status: &str, stdout: &[u8], stderr: &[u8]) -> String {
if command_output_has_broken_pipe(stdout, stderr) {
return "socket broken; restart cmux".to_string();
}

let mut msg = format!("{action} exited {status}");
if let Some(detail) = command_output_detail(stderr).or_else(|| command_output_detail(stdout)) {
msg.push_str(": ");
msg.push_str(&detail);
}
msg
}

fn command_output_contains(output: &[u8], needle: &str) -> bool {
String::from_utf8_lossy(output).contains(needle)
}

fn command_output_has_broken_pipe(stdout: &[u8], stderr: &[u8]) -> bool {
command_output_contains(stderr, "Broken pipe") || command_output_contains(stdout, "Broken pipe")
}

fn command_output_detail(output: &[u8]) -> Option<String> {
let text = String::from_utf8_lossy(output);
let detail = text
.lines()
.map(str::trim)
.filter(|line| !line.is_empty())
.collect::<Vec<_>>()
.join("; ");
if detail.is_empty() {
None
} else {
Some(detail)
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn command_failure_includes_stderr_detail() {
let msg = format_command_failure(
"workspace select",
"exit status: 1",
b"",
b"Error: workspace not found\n",
);

assert_eq!(
msg,
"workspace select exited exit status: 1: Error: workspace not found"
);
}

#[test]
fn command_failure_summarizes_broken_pipe_socket_failure() {
let msg = format_command_failure(
"workspace select",
"exit status: 1",
b"",
b"Error: Failed to write to socket (Broken pipe, errno 32)\n",
);

assert_eq!(msg, "socket broken; restart cmux");
}

#[test]
fn command_plan_uses_target_cmux_context() {
let env = |name: &str| match name {
"CMUX_WORKSPACE_ID" => Some("workspace-1".to_string()),
"CMUX_PANEL_ID" => Some("terminal-1".to_string()),
"CMUX_BUNDLED_CLI_PATH" => {
Some("/Applications/cmux.app/Contents/Resources/bin/cmux".to_string())
}
"CMUX_SOCKET_PATH" => Some("/tmp/cmux.sock".to_string()),
"CMUX_SOCKET_PASSWORD" => Some("pw".to_string()),
_ => None,
};

let plan = command_plan_from_env(env).unwrap();

assert_eq!(
plan.program,
"/Applications/cmux.app/Contents/Resources/bin/cmux"
);
assert_eq!(plan.args, ["workspace", "select", "workspace-1"]);
assert_eq!(
plan.envs,
[
("CMUX_SOCKET_PATH".to_string(), "/tmp/cmux.sock".to_string()),
("CMUX_SOCKET_PASSWORD".to_string(), "pw".to_string()),
]
);
assert_eq!(plan.workspace_id, "workspace-1");
assert_eq!(plan.terminal_id.as_deref(), Some("terminal-1"));
}

#[test]
fn applescript_fallback_selects_workspace_and_terminal() {
let script = applescript_focus_script("workspace-1", Some("terminal-1"));

assert!(script.contains("if id of candidate is \"workspace-1\" then"));
assert!(script.contains("select tab candidate"));
assert!(script.contains("focus (first terminal of w whose id is \"terminal-1\")"));
assert!(script.contains("activate window w"));
}

#[test]
fn applescript_string_escapes_quotes_and_backslashes() {
assert_eq!(applescript_string(r#"a\b"c"#), r#""a\\b\"c""#);
}

#[test]
fn cmux_env_removals_only_removes_cmux_keys() {
let removals = cmux_env_removals([
"CMUX_WORKSPACE_ID".to_string(),
"PATH".to_string(),
"CMUX_SOCKET_PATH".to_string(),
"HOME".to_string(),
]);

assert_eq!(removals, ["CMUX_WORKSPACE_ID", "CMUX_SOCKET_PATH"]);
}

#[test]
fn broken_pipe_attempts_applescript_fallback() {
let plan = CmuxCommandPlan {
workspace_id: "workspace-1".to_string(),
terminal_id: Some("terminal-1".to_string()),
program: "cmux".to_string(),
args: vec![],
envs: vec![],
};
let mut called = false;

let result = jump_via_applescript_after_socket_failure_with(&plan, |fallback_plan| {
called = true;
assert_eq!(fallback_plan.workspace_id, "workspace-1");
assert_eq!(fallback_plan.terminal_id.as_deref(), Some("terminal-1"));
JumpAttempt::Jumped
});

assert_eq!(result, JumpAttempt::Jumped);
assert!(called);
}

#[test]
fn broken_pipe_reports_socket_failure_when_applescript_is_unavailable() {
let plan = CmuxCommandPlan {
workspace_id: "workspace-1".to_string(),
terminal_id: Some("terminal-1".to_string()),
program: "cmux".to_string(),
args: vec![],
envs: vec![],
};

let result = jump_via_applescript_after_socket_failure_with(&plan, |_| {
JumpAttempt::Failed("AppleScript not runnable (No such file or directory)".to_string())
});

assert_eq!(
result,
JumpAttempt::Failed("socket broken; restart cmux".to_string())
);
}
}
Loading
Loading