aboutsummaryrefslogtreecommitdiff
path: root/crates/dinggerd/src/main.rs
blob: 7ef601ffb109d17f7f241ed8e90de05fdbfa1d83 (plain)
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
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
//! dingger: agent state watching for tmux panes.

mod hooks;
mod integrations;
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>,
    },
    /// Install agent integrations (hook extensions that report state).
    Install {
        /// Agent to install the integration for.
        agent: 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,
        } => {
            if let Err(err) = hooks::spawn_listener() {
                eprintln!("dingger: {err}");
                std::process::exit(1);
            }
            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);
            }
        }
        Command::Install { agent } => {
            if let Err(err) = integrations::install(&agent) {
                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.display_target,
            pane.agent.map(detect_core::agent_label).unwrap_or("-"),
            if pane.written_state.is_empty() {
                "-"
            } else {
                &pane.written_state
            },
            pane.current_command,
        );
    }
    Ok(())
}