use std::collections::HashMap;
use std::io::{BufRead, BufReader, Read, Write};
use std::path::PathBuf;
use std::process::Command;
use std::time::Duration;
fn resurrect_main(file: &str, dry: bool, verbose: bool) -> i32 {
let path = std::path::Path::new(file);
if !path.is_file() {
return 0;
}
if taimux_core::tmux::ask(&["has-session"]).is_none() {
return 0; }
let log = path
.parent()
.unwrap_or(std::path::Path::new("."))
.join("taimux-resurrect.log");
let name = path
.file_name()
.unwrap_or_default()
.to_string_lossy()
.into_owned();
let note = |line: &str| {
use std::io::Write as _;
if let Ok(mut f) = std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(&log)
{
let _ = writeln!(f, "{} {} {}", taimux_core::log::stamp(), name, line);
}
};
let Some(panes) = taimux_core::tmux::ask_raw(&[
"list-panes",
"-a",
"-F",
"#{pane_id}\t#{session_name}\t#{window_index}\t#{pane_index}",
]) else {
return 1;
};
let state_of = |id: &str, pid: i32| -> String {
let screen = taimux_core::tmux::capture(id).unwrap_or_default();
taimux_core::state::merge(
&screen,
taimux_core::hook::hook_entry(id, pid)
.as_ref()
.map(|(st, _)| st.as_str()),
)
.as_str()
.to_string()
};
let records = taimux_core::conv::print_cmds(&taimux_core::panes::agent_rows(), &state_of);
let Ok(save) = std::fs::read_to_string(path) else {
return 1;
};
let o = taimux_cli::resurrect::decide(&panes, &records, &save);
if o.total() == 0 {
if verbose {
println!("no claude panes");
}
note("no claude panes");
return 0;
}
if dry {
if !o.map.is_empty() {
let body = taimux_cli::resurrect::rewrite(&o.map, &save);
let tmp = std::env::temp_dir().join(format!("taimux-rw.{}", std::process::id()));
if std::fs::write(&tmp, &body).is_ok() {
let _ = Command::new("diff")
.args([
"-u",
"--label",
file,
"--label",
&format!("{} (rewritten)", file),
file,
&tmp.to_string_lossy(),
])
.status();
let _ = std::fs::remove_file(&tmp);
}
}
println!("taimux resurrect: {} (dry run)", o.summary());
for n in &o.notes {
println!(" {}", n);
}
return 0;
}
if !o.map.is_empty() {
let target = std::fs::canonicalize(path).unwrap_or_else(|_| path.to_path_buf());
let original = std::fs::read_to_string(&target).unwrap_or_default();
let body = taimux_cli::resurrect::rewrite(&o.map, &original);
if let Err(why) = taimux_cli::resurrect::commit(&target, &body, &original) {
note(&why);
return 1;
}
}
note(&o.summary());
for n in &o.notes {
use std::io::Write as _;
if let Ok(mut f) = std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(&log)
{
let _ = writeln!(f, " {}", n);
}
}
taimux_core::log::trim_log(&log, 600, 400);
if verbose {
println!("taimux resurrect: {}", o.summary());
}
0
}
fn handoff_main(a: &[String]) -> i32 {
let flag = |n: &str| -> Option<String> {
a.iter()
.position(|x| x == n)
.and_then(|i| a.get(i + 1))
.cloned()
};
let Some(id) = a.first().filter(|x| !x.starts_with('-')) else {
eprintln!("taimux handoff: which conversation? (see `taimux dead-rows`)");
return 1;
};
let (agent, key) = match taimux_cli::act::conversation_of(id) {
Ok(v) => v,
Err(why) => {
eprintln!("taimux handoff: {}", why);
return 1;
}
};
let turns = flag("--turns")
.and_then(|v| v.parse().ok())
.or_else(|| taimux_core::env::var("TAIMUX_HANDOFF_TURNS").and_then(|v| v.parse().ok()))
.unwrap_or(taimux_core::handoff::DEFAULT_TURNS);
let prompt = taimux_core::handoff::build(&agent, &key, turns);
let Some(to) = flag("--to") else {
print!("{}", prompt);
return 0;
};
let cmd = match taimux_core::handoff::launch(&to, &prompt) {
Ok(c) => c,
Err(why) => {
eprintln!("taimux handoff: {}", why);
return 1;
}
};
if a.iter().any(|x| x == "--print") {
println!("{}", cmd);
return 0;
}
let cwd = taimux_core::agents::meta(&agent, &key).cwd;
if !std::path::Path::new(&cwd).is_dir() {
eprintln!(
"taimux handoff: {} is gone, so there is nowhere to start {}",
if cwd.is_empty() {
"its directory"
} else {
&cwd
},
to
);
return 1;
}
if taimux_core::tmux::run(&["new-window", "-c", &cwd, &cmd]) {
0
} else {
eprintln!("taimux handoff: tmux would not open a window");
1
}
}
fn restart_main(o: &taimux_cli::restart::Opts, go: bool, ask: bool) -> i32 {
let vdir = taimux_cli::restart::versions_dir();
let launcher = taimux_cli::restart::launcher();
let newver = match taimux_cli::restart::installed(&launcher, &vdir) {
Ok(v) => v,
Err(e) => {
eprintln!("{}", e);
return 1;
}
};
if let Some(t) = &o.force_transcript {
if o.only_panes.len() != 1 {
eprintln!("restart: --transcript needs exactly one --pane");
return 1;
}
if !std::path::Path::new(t).is_file() {
eprintln!("restart: no such transcript: {}", t);
return 1;
}
}
let env = taimux_cli::restart::Live {
versions_dir: vdir.clone(),
};
let agents = || -> String {
Command::new("claude")
.args(["agents", "--json"])
.output()
.ok()
.filter(|o| o.status.success())
.map(|o| String::from_utf8_lossy(&o.stdout).into_owned())
.unwrap_or_else(|| "[]".to_string())
};
let plan = taimux_cli::restart::plan(
&taimux_core::panes::agent_rows(),
&newver,
&launcher,
o,
&env,
&vdir,
&agents,
);
print!("{}", taimux_cli::restart::render(&plan));
if plan.go.is_empty() {
return 0;
}
if !go {
if !ask {
println!("\ndry run. pass -y to restart.");
return 0;
}
let Ok(tty) = std::fs::File::open("/dev/tty") else {
println!("\ndry run, no terminal to ask on. pass -y to restart.");
return 0;
};
let n = plan.go.len();
print!(
"\nrestart {} session{}? [Y/n] ",
n,
if n == 1 { "" } else { "s" }
);
let _ = std::io::stdout().flush();
let mut ans = String::new();
if BufReader::new(tty).read_line(&mut ans).is_err()
|| !taimux_cli::restart::confirm_yes(&ans)
{
println!("nothing restarted.");
return 0;
}
}
println!();
let (mut ok, mut bad) = (0, 0);
for g in &plan.go {
print!("restarting {} ({}) ... ", g.pane, g.target);
let _ = std::io::stdout().flush();
if !taimux_cli::restart::restart_pane(&g.pane, g.pid, &g.cmd) {
println!("did not exit, left alone");
bad += 1;
continue;
}
let mut newpid = None;
for _ in 0..40 {
std::thread::sleep(Duration::from_millis(500));
let Some(ppid) =
taimux_core::tmux::ask(&["display-message", "-p", "-t", &g.pane, "#{pane_pid}"])
else {
break;
};
newpid = taimux_core::proc::children_of(ppid.trim().parse().unwrap_or(0))
.into_iter()
.find(|c| {
taimux_cli::restart::version_of_pid(*c, &taimux_cli::restart::versions_dir())
.is_some()
});
if newpid.is_some() {
break;
}
}
match newpid {
Some(pid) => {
println!(
"up on {} (pid {})",
taimux_cli::restart::version_of_pid(pid, &taimux_cli::restart::versions_dir())
.unwrap_or_default(),
pid
);
ok += 1;
}
None => {
println!("exited but did not come back, check the pane");
bad += 1;
}
}
}
println!("\n{} restarted, {} needing a look", ok, bad);
if bad == 0 {
0
} else {
1
}
}
fn switch(id: &str) -> i32 {
let id = id.to_string();
if id.is_empty() {
1 } else if id == "dead:!" {
0 } else if let Some((agent, key)) = taimux_core::index::split_past_id(&id) {
let cwd = taimux_core::tmux::past_cwd(agent, key);
match taimux_core::tmux::resume_dead(agent, key, &cwd) {
Ok(()) => 0,
Err(why) => {
eprintln!("taimux: {}", why);
1
}
}
} else if id.starts_with('%') {
let zoom = taimux_core::env::on("TAIMUX_ZOOM");
if taimux_core::tmux::switch_local(&id, zoom) {
0
} else {
1
}
} else if let Some(host) = taimux_cli::remote::pane_host(&id) {
let zoom = taimux_core::env::on("TAIMUX_ZOOM");
let local = taimux_cli::remote::ssh_tmux_panes(&taimux_cli::remote::tmux_pane_ttys())
.into_iter()
.find(|(h, _)| h == host)
.map(|(_, p)| p)
.unwrap_or_default();
match taimux_cli::remote::switch_remote(
host,
taimux_cli::remote::pane_local(&id),
&local,
zoom,
) {
Ok(()) => 0,
Err(why) => {
eprintln!("taimux: {}", why);
1
}
}
} else {
eprintln!("taimux: {} is not a pane id", id);
2
}
}
fn detach(exe: &str, arg: &str) {
let quiet = |c: &mut Command| -> std::io::Result<std::process::Child> {
c.stdin(std::process::Stdio::null())
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.spawn()
};
let _ = quiet(Command::new("setsid").arg(exe).arg(arg))
.or_else(|_| quiet(Command::new(exe).arg(arg)));
}
fn pick() -> i32 {
let arg = std::env::args().nth(2).unwrap_or_default();
let cur = if arg.starts_with('%') && arg[1..].bytes().all(|b| b.is_ascii_digit()) {
arg
} else {
taimux_core::tmux::ask(&["display-message", "-p", "#{pane_id}"]).unwrap_or_default()
};
let panes = taimux_cli::remote::tmux_pane_ttys();
let about = taimux_core::tmux::ask(&[
"display-message",
"-p",
"-t",
&cur,
"#{pane_current_command}\t#{pane_current_path}\t#{session_name}:#{window_index}.#{pane_index}",
])
.unwrap_or_default();
let mut f = about.split('\t');
let cmd = f.next().unwrap_or_default().to_string();
let cur_cwd = f.next().unwrap_or_default().to_string();
let cur_target = f.next().unwrap_or_default().to_string();
let cur = taimux_cli::remote::resolve_cur(&cur, &panes, cmd.trim());
let search = taimux_core::env::on("TAIMUX_SEARCH");
let sessions = taimux_core::env::on("TAIMUX_SESSIONS");
if search || sessions {
detach(&self_exe(), "index");
}
let home = std::env::var("HOME").unwrap_or_default();
let exe = self_exe();
let started = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
let daemon_exe = exe.clone();
let src = taimux_cli::tui::Source {
fetch: std::sync::Arc::new(move || {
let local = taimux_daemon::protocol::rows().unwrap_or_else(|| {
if taimux_core::env::on("TAIMUX_DAEMON")
&& !started.swap(true, std::sync::atomic::Ordering::SeqCst)
{
detach(&daemon_exe, "serve");
}
let mut p = taimux_core::version::Prober::new();
let mut c = HashMap::new();
taimux_core::panes::list_rows(&mut p, &mut c)
});
taimux_cli::remote::all_panes(&local, &taimux_cli::remote::tmux_pane_ttys())
}),
ended: taimux_cli::tui::ended_source(),
cur: cur.clone(),
cur_cwd,
cur_target,
newver: taimux_core::version::installed_claude(&home).unwrap_or_default(),
home,
script: Some(exe.clone()),
popup: taimux_core::env::is("TAIMUX_POPUP", "1"),
state: restored_state(),
};
match taimux_cli::tui::run(src) {
Ok(taimux_cli::tui::Outcome::Chosen(id)) => {
switch(&id)
}
Ok(taimux_cli::tui::Outcome::Aborted) => 0, Ok(taimux_cli::tui::Outcome::Resize(state)) => {
reopen_popup(&exe, &cur, &state);
0
}
Err(e) => {
eprintln!("taimux: {}", e);
1
}
}
}
fn restored_state() -> taimux_cli::tui::State {
let get = |k: &str| taimux_core::env::var(k).unwrap_or_default();
taimux_cli::tui::State {
query: get("TAIMUX_STATE_QUERY"),
mode: Box::leak(get("TAIMUX_STATE_MODE").into_boxed_str()),
search: get("TAIMUX_STATE_SEARCH") == "1",
preview: taimux_core::env::var("TAIMUX_STATE_PREVIEW").is_none_or(|v| v == "1"),
on: get("TAIMUX_STATE_ON"),
client: String::new(),
}
}
fn reopen_popup(exe: &str, cur: &str, state: &taimux_cli::tui::State) {
let client = state.client.clone();
if client.is_empty() {
return;
}
let path = taimux_core::paths::runtime_dir().join("resize-state");
if let Some(d) = path.parent() {
let _ = std::fs::create_dir_all(d);
}
let _ = std::fs::write(
&path,
format!(
"mode\t{}\nsearch\t{}\npreview\t{}\non\t{}\nquery\t{}\n",
state.mode,
state.search as u8,
state.preview as u8,
state.on,
state.query.replace(['\n', '\r'], " "),
),
);
let _ = taimux_core::tmux::run(&[
"run-shell",
"-b",
&format!(
"'{}' _repopup '{}' '{}'",
sh_quote(exe),
sh_quote(client.trim()),
sh_quote(cur)
),
]);
}
fn sh_quote(s: &str) -> String {
s.replace('\'', "'\\''")
}
fn repopup(client: &str, cur: &str) -> i32 {
let path = taimux_core::paths::runtime_dir().join("resize-state");
let saved = std::fs::read_to_string(&path).unwrap_or_default();
let _ = std::fs::remove_file(&path);
let field = |k: &str| -> String {
saved
.lines()
.find_map(|l| l.strip_prefix(k).and_then(|r| r.strip_prefix('\t')))
.unwrap_or("")
.to_string()
};
let width: usize =
taimux_core::tmux::ask(&["display-message", "-c", client, "-p", "#{client_width}"])
.and_then(|w| w.trim().parse().ok())
.unwrap_or(0);
if width == 0 {
return 1; }
let (pw, ph) = taimux_cli::install::popup_geometry(width);
let exe = self_exe();
std::thread::sleep(Duration::from_millis(250));
for _ in 0..6 {
let ok = Command::new("tmux")
.args([
"display-popup",
"-c",
client,
"-w",
&format!("{}%", pw),
"-h",
&format!("{}%", ph),
"-E",
"-e",
"TAIMUX_POPUP=1",
"-e",
&format!("TAIMUX_STATE_QUERY={}", field("query")),
"-e",
&format!("TAIMUX_STATE_MODE={}", field("mode")),
"-e",
&format!("TAIMUX_STATE_SEARCH={}", field("search")),
"-e",
&format!("TAIMUX_STATE_PREVIEW={}", field("preview")),
"-e",
&format!("TAIMUX_STATE_ON={}", field("on")),
&format!("'{}' pick {}", sh_quote(&exe), cur),
])
.status()
.map(|s| s.success())
.unwrap_or(false);
if ok {
return 0;
}
std::thread::sleep(Duration::from_millis(300));
}
taimux_cli::act::note("resize: could not reopen the picker's popup");
1
}
fn self_exe() -> String {
std::env::current_exe()
.map(|p| p.to_string_lossy().into_owned())
.unwrap_or_else(|_| "taimux".into())
}
fn main() {
if std::env::var_os("MISE_OFFLINE").is_none() {
std::env::set_var("MISE_OFFLINE", "1");
}
let arg = std::env::args().nth(1).unwrap_or_else(|| "help".into());
let rc = match arg.as_str() {
"serve" => taimux_daemon::protocol::serve()
.map(|_| 0)
.unwrap_or_else(|e| {
eprintln!("taimux: {}", e);
1
}),
"panes" | "ping" | "quit" => taimux_daemon::protocol::query(&arg).map(|_| 0).unwrap_or(1),
"list" => {
match taimux_daemon::protocol::rows() {
Some(body) => print!("{}", body),
None => {
let mut p = taimux_core::version::Prober::new();
let mut c = HashMap::new();
print!("{}", taimux_core::panes::list_rows(&mut p, &mut c));
}
}
0
}
"list-local" => {
let mut p = taimux_core::version::Prober::new();
let mut c = HashMap::new();
print!("{}", taimux_core::panes::list_rows(&mut p, &mut c));
0
}
"scan" => {
print!("{}", taimux_core::panes::agent_rows());
0
}
"hook" => taimux_core::hook::run(taimux_core::paths::runtime_dir()),
"extract" => match std::env::args().nth(2) {
Some(path) => match std::fs::read_to_string(&path) {
Ok(text) => {
print!("{}", taimux_core::transcript::extract(&text));
0
}
Err(e) => {
eprintln!("taimux: {}: {}", path, e);
1
}
},
None => {
let mut buf = String::new();
let _ = std::io::stdin().read_to_string(&mut buf);
print!("{}", taimux_core::transcript::extract(&buf));
0
}
},
"classify" => {
let mut buf = String::new();
let _ = std::io::stdin().read_to_string(&mut buf);
println!("{}", taimux_core::state::classify(&buf).as_str());
0
}
"tui" => {
let home = std::env::var("HOME").unwrap_or_default();
let script = taimux_core::env::var("TAIMUX_SELF").filter(|s| !s.is_empty());
let via = script.clone();
let src = taimux_cli::tui::Source {
fetch: std::sync::Arc::new(move || {
if let Some(s) = &via {
if let Ok(o) = Command::new(s).arg("_panes").output() {
if o.status.success() {
return String::from_utf8_lossy(&o.stdout).into_owned();
}
}
}
taimux_daemon::protocol::rows().unwrap_or_else(|| {
let mut p = taimux_core::version::Prober::new();
let mut c = HashMap::new();
taimux_core::panes::list_rows(&mut p, &mut c)
})
}),
ended: taimux_cli::tui::ended_source(),
cur: std::env::args().nth(2).unwrap_or_default(),
cur_cwd: String::new(),
cur_target: String::new(),
newver: taimux_core::version::installed_claude(&home).unwrap_or_default(),
home,
script,
popup: false,
state: Default::default(),
};
match taimux_cli::tui::run(src) {
Ok(taimux_cli::tui::Outcome::Chosen(id)) => {
println!("{}", id);
0
}
Ok(taimux_cli::tui::Outcome::Resize(_)) | Ok(taimux_cli::tui::Outcome::Aborted) => {
130
} Err(e) => {
eprintln!("taimux: {}", e);
1
}
}
}
"index-pane" => {
let a: Vec<String> = std::env::args().skip(2).collect();
match (a.first(), a.get(1)) {
(Some(id), Some(tr)) => {
taimux_daemon::indexer::index_pane(id, std::path::Path::new(tr));
0
}
_ => {
eprintln!("taimux: index-pane <pane id> <transcript>");
1
}
}
}
"session-meta" => match std::env::args().nth(2) {
Some(path) => {
let a: Vec<String> = std::env::args().skip(2).collect();
let agent = a
.iter()
.position(|x| x == "--agent")
.and_then(|i| a.get(i + 1))
.cloned()
.unwrap_or_else(|| "claude".into());
let m = taimux_core::agents::meta(&agent, &path);
println!("{}\t{}\t{}\t{}", m.cwd, m.version, m.title, m.src);
0
}
None => {
eprintln!("taimux: session-meta <transcript>");
1
}
},
"index-sessions" => {
let mut buf = String::new();
let _ = std::io::stdin().read_to_string(&mut buf);
let live: Vec<(String, PathBuf)> = buf
.lines()
.filter_map(|l| l.split_once('\t'))
.filter(|(_, p)| !p.is_empty())
.map(|(pane, p)| (pane.to_string(), PathBuf::from(p)))
.collect();
taimux_daemon::indexer::sessions_scan(&live);
0
}
"index-dead" => {
taimux_daemon::indexer::index_dead();
0
}
"index-prune" => {
taimux_daemon::indexer::prune();
0
}
"resurrect" => {
let a: Vec<String> = std::env::args().skip(2).collect();
let (mut dry, mut verbose, mut file) = (false, false, String::new());
let mut bad = None;
for x in &a {
match x.as_str() {
"-n" | "--dry-run" => dry = true,
"-v" | "--verbose" => verbose = true,
o if o.starts_with('-') => bad = Some(o.to_string()),
o => file = o.to_string(),
}
}
if let Some(b) = bad {
eprintln!("resurrect: unknown option {}", b);
1
} else {
if file.is_empty() {
file = format!(
"{}/.tmux/resurrect/last",
std::env::var("HOME").unwrap_or_default()
);
}
resurrect_main(&file, dry, verbose)
}
}
"restart" => {
let a: Vec<String> = std::env::args().skip(2).collect();
let mut o = taimux_cli::restart::Opts {
include_busy: false,
only_panes: Vec::new(),
force_transcript: None,
self_pane: std::env::var("TMUX_PANE").unwrap_or_default(),
};
let (mut go, mut ask) = (false, true);
let mut i = 0;
let mut bad: Option<String> = None;
while i < a.len() {
match a[i].as_str() {
"-y" | "--yes" => go = true,
"-n" | "--dry-run" => {
go = false;
ask = false;
}
"--include-busy" => o.include_busy = true,
"--pane" => {
i += 1;
o.only_panes.push(a.get(i).cloned().unwrap_or_default());
}
"--transcript" => {
i += 1;
o.force_transcript = a.get(i).cloned();
}
other => bad = Some(other.to_string()),
}
i += 1;
}
if let Some(b) = bad {
eprintln!("restart: unknown option {}", b);
1
} else {
restart_main(&o, go, ask)
}
}
"print-cmds" => {
let state_of = |id: &str, pid: i32| -> String {
let screen = taimux_core::tmux::capture(id).unwrap_or_default();
taimux_core::state::merge(
&screen,
taimux_core::hook::hook_entry(id, pid)
.as_ref()
.map(|(st, _)| st.as_str()),
)
.as_str()
.to_string()
};
print!(
"{}",
taimux_core::conv::print_cmds(&taimux_core::panes::agent_rows(), &state_of)
);
0
}
"all-panes" => {
let mut p = taimux_core::version::Prober::new();
let mut c = HashMap::new();
let local = taimux_core::panes::list_rows(&mut p, &mut c);
print!(
"{}",
taimux_cli::remote::all_panes(&local, &taimux_cli::remote::tmux_pane_ttys())
);
0
}
"resolve-cur" => {
let pane = std::env::args().nth(2).unwrap_or_default();
let cmd = taimux_core::tmux::ask(&[
"display-message",
"-p",
"-t",
&pane,
"#{pane_current_command}",
])
.unwrap_or_default();
println!(
"{}",
taimux_cli::remote::resolve_cur(
&pane,
&taimux_cli::remote::tmux_pane_ttys(),
cmd.trim()
)
);
0
}
"index-dump" => {
print!("{}", taimux_cli::remote::index_dump());
0
}
"preview" => {
let id = std::env::args().nth(2).unwrap_or_default();
let q = std::env::args().nth(3).unwrap_or_default();
if id.is_empty() {
0
} else if id.starts_with("dead:") {
print!("{}", taimux_core::tmux::preview_dead(&id, &q));
0
} else if id.starts_with('%') {
print!("{}", taimux_core::tmux::preview_live(&id, &q));
0
} else if let Some(host) = taimux_cli::remote::pane_host(&id) {
println!("\x1b[1;35m{}\x1b[0m", host);
let lid = taimux_cli::remote::pane_local(&id);
if lid.starts_with('%') {
let (rc, out) = taimux_cli::remote::ssh(
host,
&format!("{} preview {}", taimux_cli::remote::remote_taimux(), lid),
taimux_core::env::var("TAIMUX_SSH_TIMEOUT")
.and_then(|v| v.parse().ok())
.unwrap_or(4),
);
if rc == 0 {
print!("{}", out);
} else {
println!("\x1b[31mno answer from {}\x1b[0m", host);
}
} else {
println!(
"\x1b[90mnothing to show: this row is about the host, not a session\x1b[0m"
);
}
0
} else {
0
}
}
"switch" => switch(&std::env::args().nth(2).unwrap_or_default()),
"transcript-by-id" => match std::env::args()
.nth(2)
.and_then(|id| taimux_core::conv::transcript_by_id(&id))
{
Some(p) => {
println!("{}", p.display());
0
}
None => 1,
},
"install" => taimux_cli::install::install(&self_exe()),
"bind" => taimux_cli::install::bind(&self_exe()),
"install-hooks" => taimux_cli::install::install_hooks(&self_exe()),
"pick" => {
if std::env::var("TMUX").map(|v| v.is_empty()).unwrap_or(true) {
eprintln!("Not inside tmux: run this from a tmux client (or use prefix + a).");
1
} else {
pick()
}
}
"_restart" => {
taimux_cli::act::restart_one(&self_exe(), &std::env::args().nth(2).unwrap_or_default());
0
}
"_sweep" => {
taimux_cli::act::sweep(&self_exe());
0
}
"_handoff" => {
taimux_cli::act::handoff_one(&std::env::args().nth(2).unwrap_or_default());
0
}
"handoff" => handoff_main(&std::env::args().skip(2).collect::<Vec<_>>()),
"_repopup" => {
let a: Vec<String> = std::env::args().skip(2).collect();
match (a.first(), a.get(1)) {
(Some(client), cur) => repopup(client, cur.map(String::as_str).unwrap_or("")),
_ => {
eprintln!("taimux: _repopup <client tty> [pane]");
1
}
}
}
"resolve" => {
let a: Vec<String> = std::env::args().skip(2).collect();
let (pane, cwd) = (
a.first().cloned().unwrap_or_default(),
a.get(1).cloned().unwrap_or_default(),
);
let pid: i32 = a.get(2).and_then(|p| p.parse().ok()).unwrap_or(0);
match taimux_core::conv::resolve_from_pane(&pane, &cwd, pid) {
Some(r) => {
println!("{}\t{}", r.transcript.display(), r.why);
0
}
None => 1,
}
}
"index" => {
let search = taimux_core::env::on("TAIMUX_SEARCH");
let sessions = taimux_core::env::on("TAIMUX_SESSIONS");
if !search && !sessions {
0
} else {
let lockfile = taimux_core::index::index_dir().join(".lock");
match taimux_daemon::indexer::Lock::take(&lockfile) {
None => 0, Some(mut lock) => {
let ttl = taimux_core::env::var("TAIMUX_SEARCH_TTL")
.and_then(|v| v.parse().ok())
.unwrap_or(5u64);
let forced = std::env::args().any(|a| a == "--force");
if !forced && lock.age() < ttl {
0
} else {
lock.stamp();
taimux_daemon::indexer::pass(
&taimux_core::panes::agent_rows(),
search,
sessions,
);
if search {
taimux_cli::remote::index_remote(
&taimux_cli::remote::tmux_pane_ttys(),
);
}
0
}
}
}
}
}
"snips" => {
let q = std::env::args().skip(2).collect::<Vec<_>>().join(" ");
let mut out: Vec<(String, String)> =
taimux_core::index::snippets(&taimux_core::index::Query::new(&q))
.into_iter()
.collect();
out.sort(); for (pane, snip) in out {
println!("{}\t{}", pane, snip);
}
0
}
"dead-rows" => {
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs() as i64)
.unwrap_or(0);
print!("{}", taimux_core::index::dead_rows(now));
0
}
"rows" => {
let a: Vec<String> = std::env::args().skip(2).collect();
let flag = |name: &str| -> Option<String> {
a.iter()
.position(|x| x == name)
.and_then(|i| a.get(i + 1))
.cloned()
};
let pairs = |var: &str| -> HashMap<String, String> {
taimux_core::env::var(var)
.unwrap_or_default()
.lines()
.filter_map(|l| l.split_once('\t'))
.map(|(k, v)| (k.to_string(), v.to_string()))
.collect()
};
let (cur, home, newver, only) = (
flag("--cur").unwrap_or_default(),
flag("--home").unwrap_or_default(),
flag("--newver").unwrap_or_default(),
flag("--only").unwrap_or_default(),
);
let query = taimux_core::env::var("TAIMUX_Q").unwrap_or_default();
let mut buf = String::new();
let _ = std::io::stdin().read_to_string(&mut buf);
let input = taimux_cli::rows::Input {
cur: &cur,
width: flag("--width").and_then(|w| w.parse().ok()).unwrap_or(0),
home: &home,
newver: &newver,
only: &only,
outdated: a.iter().any(|x| x == "--outdated"),
query: &query,
snips: pairs("TAIMUX_SNIPS"),
ptitles: pairs("TAIMUX_PTITLES"),
restarting: Default::default(),
};
for row in taimux_cli::rows::build(&buf, &input) {
println!("{}", row.to_ansi());
}
0
}
"scan-list" => {
let mut p = taimux_core::version::Prober::new();
let mut c = HashMap::new();
print!("{}", taimux_core::panes::list_rows(&mut p, &mut c));
0
}
"version" | "--version" | "-V" => {
println!("taimux {}", env!("CARGO_PKG_VERSION"));
0
}
"help" | "--help" | "-h" => {
print!("{}", help());
0
}
_ => {
eprintln!("taimux: no such command: {}\n", arg);
eprint!("{}", help());
1
}
};
std::process::exit(rc);
}
fn help() -> String {
format!(
"taimux {}, a picker for live AI coding-agent sessions\n\
\n\
pick the picker (what prefix+a and F1 run)\n\
switch <id> jump to a pane id, local or <host>:<pane>\n\
list the agent sessions here, tab-separated\n\
preview <id> what a session's pane, or transcript, is showing\n\
handoff <id> carry a conversation into a different agent\n\
\n\
print-cmds which conversation each claude pane is on\n\
restart [-n] restart idle claude panes on their own conversation\n\
resurrect rewrite a tmux-resurrect save to resume conversations\n\
\n\
index [--force] one indexing pass: transcripts, sessions, past rows\n\
hook a session reporting its own turn boundary\n\
serve the collection daemon (socket: {})\n\
\n\
install symlink the launcher and bind prefix+a and F1\n\
bind bind a running tmux server, by absolute path\n\
install-hooks register the self-reporting hook with Claude Code\n\
version print the version\n",
env!("CARGO_PKG_VERSION"),
taimux_core::paths::socket_path().display()
)
}