diff options
Diffstat (limited to 'crates/dinggerd/src/tmux.rs')
| -rw-r--r-- | crates/dinggerd/src/tmux.rs | 137 |
1 files changed, 79 insertions, 58 deletions
diff --git a/crates/dinggerd/src/tmux.rs b/crates/dinggerd/src/tmux.rs index f6c2f17..326eda9 100644 --- a/crates/dinggerd/src/tmux.rs +++ b/crates/dinggerd/src/tmux.rs @@ -2,7 +2,7 @@ use std::process::Command; -use detect_core::{identify_agent, Agent, AgentState}; +use detect_core::{identify_agent, Agent}; #[derive(Debug, Clone)] pub struct Pane { @@ -10,38 +10,27 @@ pub struct Pane { pub pane_id: String, /// Human-readable target, e.g. `main:work` (`.pane` suffix when split). pub display_target: String, - /// Foreground command name in the pane, from `#{pane_current_command}`. - pub current_command: String, /// Last OSC title the pane's application set, from `#{pane_title}`. pub pane_title: String, pub dead: bool, - /// The pane's window is the active window of its session. + /// The pane's window is the current window of its session. pub window_active: bool, /// The pane's session has at least one attached client. pub session_attached: bool, pub agent: Option<Agent>, - /// The `@agent_state` user option previously written for this pane, - /// read straight from the pane format. + /// Pane options written by dingger (see the watcher module docs). pub written_state: String, - /// The `@agent` user option (agent label) previously written, if any. pub written_agent: String, - /// The `@dingger_ack` user option: "1" when the user already jumped to - /// this pane in its current ready episode. pub written_ack: String, - /// The pane is the active pane of the active window. - pub pane_active: bool, + pub written_since: String, + pub written_hook: String, } impl Pane { - /// Panes the user is actively looking at suppress finished notifications, - /// mirroring herdr's active-tab suppression. - pub fn suppresses_notifications(&self) -> bool { - self.window_active && self.session_attached - } - - /// The pane is fully focused: the user is looking at this exact pane. + /// The user is looking at this pane's window. herdr marks every pane of + /// the active tab seen (`mark_active_tab_seen`); a tmux window is a tab. pub fn attended(&self) -> bool { - self.pane_active && self.suppresses_notifications() + self.window_active && self.session_attached } } @@ -80,42 +69,42 @@ pub fn list_panes() -> Result<Vec<Pane>, String> { "#{@agent_state}\t", "#{@agent}\t", "#{@dingger_ack}\t", - "#{pane_active}\t", + "#{@dingger_since}\t", + "#{@dingger_hook}\t", "#{session_name}:#{window_name}\t", "#{window_panes}\t", "#{pane_index}", ); let out = run_tmux(&["list-panes", "-a", "-F", format])?; let mut panes = Vec::new(); + let mut process_tree = None; for line in out.lines() { let fields: Vec<&str> = line.split('\t').collect(); - if fields.len() < 15 { + if fields.len() < 16 { continue; } - let current_command = fields[1].to_string(); - let start_command = fields[6].to_string(); + let current_command = fields[1]; + let start_command = fields[6]; let pane_pid: Option<u32> = fields[7].trim().parse().ok(); // An agent may be started via a wrapper or still be initializing // behind the shell; `pane_start_command` covers launch-command cases. let mut agent = - identify_agent(¤t_command).or_else(|| identify_agent(&start_command)); + identify_agent(current_command).or_else(|| identify_agent(start_command)); // Some agents rename their process (claude sets its title to the // version string), so `pane_current_command` alone is not enough. - let mut process_tree = None; if agent.is_none() { let tree = process_tree.get_or_insert_with(fetch_process_tree); agent = resolve_agent_in_process_tree(pane_pid, tree); } // Display target: session:window-name, pane index only when split. - let mut display_target = fields[12].to_string(); - if fields[13].parse::<u32>().map(|n| n > 1).unwrap_or(false) { + let mut display_target = fields[13].to_string(); + if fields[14].parse::<u32>().map(|n| n > 1).unwrap_or(false) { display_target.push('.'); - display_target.push_str(fields[14]); + display_target.push_str(fields[15]); } panes.push(Pane { pane_id: fields[0].to_string(), display_target, - current_command, pane_title: fields[2].to_string(), dead: fields[3] == "1", window_active: fields[4] == "1", @@ -124,7 +113,8 @@ pub fn list_panes() -> Result<Vec<Pane>, String> { written_state: fields[8].to_string(), written_agent: fields[9].to_string(), written_ack: fields[10].to_string(), - pane_active: fields[11] == "1", + written_since: fields[11].to_string(), + written_hook: fields[12].to_string(), }); } Ok(panes) @@ -187,35 +177,18 @@ fn fetch_process_tree() -> ProcessTree { (children, comm) } -/// Run a tmux command whose effect is an action (e.g. switch-client), not -/// output to parse. -pub fn run_tmux_client(args: &[&str]) -> Result<(), String> { - run_tmux(args).map(|_| ()) -} - -/// Read a format expansion for one pane. -pub fn show_pane_format(pane_id: &str, format: &str) -> Option<String> { - run_tmux(&["display-message", "-p", "-t", pane_id, format]) - .ok() - .map(|value| value.trim().to_string()) -} - -/// Capture the pane's visible screen as plain text — the exact input shape +/// Capture the pane's visible screen as plain text: the exact input shape /// the detection manifests expect. pub fn capture_pane(pane_id: &str) -> Result<String, String> { run_tmux(&["capture-pane", "-p", "-q", "-t", pane_id]) } -/// Write the pane's display status (herdr's vocabulary: done/idle/...). -pub fn set_agent_state(pane_id: &str, label: &str) -> Result<(), String> { - run_tmux(&["set-option", "-p", "-t", pane_id, "@agent_state", label]).map(|_| ()) +pub fn set_pane_option(pane_id: &str, name: &str, value: &str) -> Result<(), String> { + run_tmux(&["set-option", "-p", "-t", pane_id, name, value]).map(|_| ()) } -/// Label the pane with the agent running in it, for tmux status-line and -/// menu-bar rendering; clears the label when the pane stops running one. -pub fn set_agent(pane_id: &str, agent: Option<Agent>) -> Result<(), String> { - let label = agent.map(detect_core::agent_label).unwrap_or(""); - run_tmux(&["set-option", "-p", "-t", pane_id, "@agent", label]).map(|_| ()) +pub fn unset_pane_option(pane_id: &str, name: &str) -> Result<(), String> { + run_tmux(&["set-option", "-p", "-u", "-t", pane_id, name]).map(|_| ()) } /// Write a server-global option (readable from any client and from formats). @@ -230,12 +203,60 @@ pub fn show_global(name: &str) -> Option<String> { .filter(|value| !value.is_empty()) } -/// Mark a pane as seen by the user after `dingger focus`: taken out of the -/// ready queue until its next state transition. -pub fn ack(pane_id: &str) -> Result<(), String> { - run_tmux(&["set-option", "-p", "-t", pane_id, "@dingger_ack", "1"]).map(|_| ()) +/// Expand a format in the context of a pane. +pub fn pane_format(pane_id: &str, format: &str) -> Option<String> { + run_tmux(&["display-message", "-p", "-t", pane_id, format]) + .ok() + .map(|value| value.trim().to_string()) + .filter(|value| !value.is_empty()) } -pub fn clear_ack(pane_id: &str) -> Result<(), String> { - run_tmux(&["set-option", "-p", "-t", pane_id, "-u", "@dingger_ack"]).map(|_| ()) +/// Expand a format in the context of a client (its current pane/window). +pub fn client_format(client: &str, format: &str) -> Option<String> { + run_tmux(&["display-message", "-p", "-c", client, format]) + .ok() + .map(|value| value.trim().to_string()) + .filter(|value| !value.is_empty()) +} + +/// Attached clients as (name, pid), most recently active first. +pub fn clients() -> Vec<(String, u32)> { + let Ok(out) = run_tmux(&[ + "list-clients", + "-F", + "#{client_activity}\t#{client_name}\t#{client_pid}", + ]) else { + return Vec::new(); + }; + let mut clients: Vec<(u64, String, u32)> = out + .lines() + .filter_map(|line| { + let mut fields = line.split('\t'); + let activity = fields.next()?.parse().ok()?; + let name = fields.next()?.to_string(); + let pid = fields.next()?.parse().ok()?; + Some((activity, name, pid)) + }) + .collect(); + clients.sort_by_key(|client| std::cmp::Reverse(client.0)); + clients.into_iter().map(|(_, name, pid)| (name, pid)).collect() +} + +/// Show a pane in a client: switch the client to the pane's window and make +/// the pane the active one. +pub fn show_pane(client: &str, pane_id: &str) -> Result<(), String> { + run_tmux(&["switch-client", "-c", client, "-t", pane_id])?; + run_tmux(&["select-pane", "-t", pane_id]).map(|_| ()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn parses_ps_tree() { + let (children, comm) = fetch_process_tree(); + assert!(comm.contains_key(&1u32), "launchd should be present"); + assert!(!children.is_empty()); + } } |
