aboutsummaryrefslogtreecommitdiff
path: root/crates/dinggerd/src/tmux.rs
diff options
context:
space:
mode:
Diffstat (limited to 'crates/dinggerd/src/tmux.rs')
-rw-r--r--crates/dinggerd/src/tmux.rs137
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(&current_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());
+ }
}