aboutsummaryrefslogtreecommitdiff
path: root/crates/dinggerd/src/main.rs
diff options
context:
space:
mode:
Diffstat (limited to 'crates/dinggerd/src/main.rs')
-rw-r--r--crates/dinggerd/src/main.rs109
1 files changed, 71 insertions, 38 deletions
diff --git a/crates/dinggerd/src/main.rs b/crates/dinggerd/src/main.rs
index 2783899..e43f9a7 100644
--- a/crates/dinggerd/src/main.rs
+++ b/crates/dinggerd/src/main.rs
@@ -8,6 +8,7 @@ mod notify;
mod platform;
mod restore;
mod snapshot;
+mod terminals;
mod tmux;
mod watcher;
@@ -22,6 +23,20 @@ struct Cli {
}
#[derive(Subcommand)]
+enum TerminalAction {
+ /// The client gained focus: learn its terminal (tmux's client-focus-in).
+ Focused {
+ #[arg(long)]
+ client: String,
+ },
+ /// Raise the client's terminal.
+ Raise {
+ #[arg(long)]
+ client: String,
+ },
+}
+
+#[derive(Subcommand)]
enum Command {
/// Watch all tmux panes and notify on agent state transitions.
Watch {
@@ -85,6 +100,13 @@ enum Command {
/// `pi`, `claude`, or `zsh`.
agent: String,
},
+ /// The terminal window of a tmux client (tmux runs these; see
+ /// `terminals`).
+ #[command(hide = true)]
+ Terminal {
+ #[command(subcommand)]
+ action: TerminalAction,
+ },
/// An agent's hook, run by the agent (`dingger install` sets it up).
#[command(hide = true)]
Hook {
@@ -170,30 +192,43 @@ fn main() {
std::process::exit(1);
}
}
+ Command::Terminal { action } => {
+ let result = match &action {
+ TerminalAction::Focused { client } => terminals::learn(client),
+ TerminalAction::Raise { client } => terminals::raise(client),
+ };
+ if let Err(err) = result {
+ eprintln!("dingger: {err}");
+ std::process::exit(1);
+ }
+ }
// Never fails the agent: exits 0, prints nothing.
Command::Hook { agent } => integrations::run_hook(&agent),
}
}
/// Show a pane in a client and mark it attended (`@dingger_ack`, consumed
-/// by the watcher on its next tick). This is the single code path behind
-/// every click: the tmux status line and the menu bar rows.
+/// by the watcher on its next tick), then raise that client's terminal
+/// window. The single code path behind every click: the tmux status line,
+/// the menu bar rows, the panel, search, and the shortcuts.
+///
+/// The client: the named one (a status-line click names the clicking
+/// client); otherwise one already on a session holding the pane's window
+/// (several terminal windows can show one session), else any; the focused
+/// one first, then the most recently used (see `terminals::choose`).
fn focus(client: Option<String>, pane: Option<String>) -> Result<(), String> {
- let clients = tmux::clients()?;
- // The named client if it is attached; otherwise the most recently active
- // one (a click on the status line makes the clicking client the most
- // recent, so this is right even if the name did not expand).
- let chosen = client
- .and_then(|name| clients.iter().find(|(n, _)| *n == name).cloned())
- .or_else(|| clients.first().cloned());
- let Some((client, client_pid)) = chosen else {
+ let clients = terminals::clients()?;
+ if clients.is_empty() {
return Err("no tmux client attached".to_string());
- };
+ }
+ let named = client.and_then(|name| clients.iter().find(|c| c.name == name));
let pane_id = match pane {
Some(pane) => pane,
None => {
- let current_window = tmux::client_format(&client, "#{window_id}");
+ // The queue head, skipping the window the user looks at.
+ let looking = named.or_else(|| terminals::choose(&clients, &[]).map(|(c, _)| c));
+ let current_window = looking.and_then(|c| tmux::client_format(&c.name, "#{window_id}"));
let queue = tmux::show_global("@dingger_queue").unwrap_or_default();
let next = queue.split(',').filter(|id| !id.is_empty()).find(|id| {
tmux::pane_format(id, "#{window_id}").as_deref() != current_window.as_deref()
@@ -205,39 +240,37 @@ fn focus(client: Option<String>, pane: Option<String>) -> Result<(), String> {
}
};
+ let showing = tmux::sessions_with_pane(&pane_id);
+ let (client, keeps_session) = match named {
+ Some(client) => (client, showing.contains(&client.session_id)),
+ None => terminals::choose(&clients, &showing).ok_or("no tmux client attached")?,
+ };
tmux::set_pane_option(&pane_id, "@dingger_ack", "1")?;
- tmux::show_pane(&client, &pane_id)?;
+ tmux::show_pane(&client.name, &pane_id, keeps_session.then_some(client.session_id.as_str()))?;
let _ = hooks::poke();
- activate_terminal_app(client_pid);
+ raise(client);
Ok(())
}
-/// Bring the terminal app hosting a tmux client to the front: walk the
-/// client's process ancestry up to the first `*.app` bundle (Ghostty,
-/// Alacritty, ...) and `open -a` it.
-fn activate_terminal_app(client_pid: u32) {
- let mut pid = client_pid;
- for _ in 0..16 {
- let Ok(out) = std::process::Command::new("ps")
- .args(["-o", "ppid=,comm=", "-p", &pid.to_string()])
- .output()
- else {
- return;
- };
- let line = String::from_utf8_lossy(&out.stdout).trim().to_string();
- let Some((ppid, comm)) = line.split_once(char::is_whitespace) else {
- return;
- };
- if let Some(end) = comm.find(".app/") {
- let bundle = &comm[..end + 4];
- let _ = std::process::Command::new("open").args(["-a", bundle]).status();
- return;
- }
- match ppid.trim().parse::<u32>() {
- Ok(parent) if parent > 1 => pid = parent,
- _ => return,
+/// Bring a client's terminal forward: its exact window, tab, and split if
+/// dingger learned which it is (raised by the tmux server, see
+/// `terminals`); otherwise the terminal app.
+fn raise(client: &terminals::Client) {
+ if terminals::pairing(&client.tty).is_some() {
+ if let Ok(exe) = std::env::current_exe() {
+ let command = format!(
+ "{} terminal raise --client {} >/dev/null 2>&1",
+ snapshot::shell_quote(&exe.to_string_lossy()),
+ snapshot::shell_quote(&client.name)
+ );
+ if tmux::run(&["run-shell", "-b", &command]).is_ok() {
+ return;
+ }
}
}
+ if let Some(app) = terminals::terminal_app(client.pid) {
+ terminals::open_app(&app);
+ }
}
/// Every agent pane in herdr's priority order (blocked, done, working,