crates/dinggerd/src/main.rs (view raw)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 |
//! dingger: agent state watching for tmux panes.
mod manifest_update;
mod notify;
mod tmux;
mod watcher;
use clap::{Parser, Subcommand};
use tracing_subscriber::EnvFilter;
#[derive(Parser)]
#[command(name = "dingger", about = "Agent state watching for tmux panes")]
struct Cli {
#[command(subcommand)]
command: Command,
}
#[derive(Subcommand)]
enum Command {
/// Watch all tmux panes and notify on agent state transitions.
Watch {
/// Poll interval in seconds.
#[arg(long, default_value_t = 1)]
interval_secs: u64,
/// Disable notification sounds.
#[arg(long)]
no_sounds: bool,
},
/// Print the current agent state of every tmux pane.
Status,
/// Refresh detection manifests from herdr's published catalog.
Manifests {
#[command(subcommand)]
command: ManifestCommand,
},
/// Switch to the agent that has been waiting the longest (FIFO). Bound to
/// a mouse click on the tmux status line by the packaged config.
Focus {
/// Name of the tmux client that clicked, passed as #{client_name}.
client: Option<String>,
},
}
#[derive(Subcommand)]
enum ManifestCommand {
/// Download newer manifests from herdr's published catalog into the
/// local remote store; bundled manifests are the offline fallback.
/// Restart `dingger watch` to pick them up.
Update,
}
fn main() {
tracing_subscriber::fmt()
.with_env_filter(EnvFilter::try_from_default_env().unwrap_or_else(|_| "info".into()))
.init();
match Cli::parse().command {
Command::Watch {
interval_secs,
no_sounds,
} => {
let mut watcher = watcher::Watcher::new(interval_secs, !no_sounds);
if let Err(err) = watcher.run() {
eprintln!("dingger: {err}");
std::process::exit(1);
}
}
Command::Status => {
if let Err(err) = status() {
eprintln!("dingger: {err}");
std::process::exit(1);
}
}
Command::Manifests { command } => match command {
ManifestCommand::Update => {
if let Err(err) = manifest_update::update() {
eprintln!("dingger: {err}");
std::process::exit(1);
}
}
},
Command::Focus { client } => {
if let Err(err) = focus(client) {
eprintln!("dingger: {err}");
std::process::exit(1);
}
}
}
}
/// Jump to the head of the ready queue (oldest waiting agent) and ack it, so
/// the next focus moves on to the following one.
fn focus(client: Option<String>) -> Result<(), String> {
let queue = tmux::show_global("@dingger_queue")
.unwrap_or_default()
.split(',')
.filter(|entry| !entry.is_empty())
.map(|entry| entry.split(':').next().unwrap_or("").to_string())
.collect::<Vec<_>>();
let Some(pane_id) = queue.first() else {
return Ok(()); // nothing waiting
};
let target = tmux::show_pane_format(pane_id, "#{session_name}:#{window_index}.#{pane_index}")
.unwrap_or_default();
if target.is_empty() {
return Err("ready pane no longer exists".to_string());
}
tmux::ack(pane_id)?;
let mut args = vec!["switch-client".to_string()];
if let Some(client) = client {
args.push("-c".to_string());
args.push(client);
}
args.push("-t".to_string());
args.push(target);
tmux::run_tmux_client(
&args.iter().map(String::as_str).collect::<Vec<_>>(),
)
}
fn status() -> Result<(), String> {
let panes = tmux::list_panes()?;
if panes.is_empty() {
println!("no tmux panes");
return Ok(());
}
println!("{:<22} {:<10} {:<12} COMMAND", "TARGET", "AGENT", "STATE");
for pane in panes {
println!(
"{:<22} {:<10} {:<12} {}",
pane.target,
pane.agent.map(detect_core::agent_label).unwrap_or("-"),
if pane.written_state.is_empty() {
"-"
} else {
&pane.written_state
},
pane.current_command,
);
}
Ok(())
}
|