aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--README.md19
-rw-r--r--crates/dinggerd/src/tmux.rs5
-rw-r--r--crates/dinggerd/src/watcher.rs22
-rw-r--r--packaging/launchd/dev.dingger.dingger.plist30
4 files changed, 68 insertions, 8 deletions
diff --git a/README.md b/README.md
index f47f1d5..6e96683 100644
--- a/README.md
+++ b/README.md
@@ -56,6 +56,19 @@ screens such as Claude's transcript never change state.
## Status
-Early scaffold. Roadmap: menu bar UI (Swift shell over the daemon), tmux
-control-mode event source instead of polling, configurable notification
-policy.
+Early scaffold. Roadmap: menu bar UI (Swift shell over the daemon), configurable
+notification policy.
+
+### Running as a login agent (launchd)
+
+`packaging/launchd/dev.dingger.dingger.plist` runs `dingger watch` at login
+with auto-restart; adjust the user path, then:
+
+```bash
+launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/dev.dingger.dingger.plist
+```
+
+dingger does not require tmux to be running first — it waits for a server and
+attaches when one appears. All tmux invocations use `-u` (forced UTF-8): under
+launchd's C locale, tmux mangles tab-separated `-F` output into underscores,
+which would silently hide every pane.
diff --git a/crates/dinggerd/src/tmux.rs b/crates/dinggerd/src/tmux.rs
index 04a6c77..e833e3c 100644
--- a/crates/dinggerd/src/tmux.rs
+++ b/crates/dinggerd/src/tmux.rs
@@ -4,6 +4,7 @@ use std::process::Command;
use detect_core::manifest::agent_state_label;
use detect_core::{identify_agent, Agent, AgentState};
+use tracing::debug;
#[derive(Debug, Clone)]
pub struct Pane {
@@ -36,6 +37,10 @@ impl Pane {
fn run_tmux(args: &[&str]) -> Result<String, String> {
let mut command = Command::new("tmux");
+ // -u forces UTF-8 mode: without it, tmux mangles format output in
+ // non-UTF-8 locales (launchd/cron default to the C locale, turning the
+ // tab separators in our -F formats into underscores).
+ command.arg("-u");
// -L selects a named server socket; lets tests and multi-server setups
// point dingger at a specific server.
if let Some(socket) = std::env::var_os("DINGGER_TMUX_SOCKET") {
diff --git a/crates/dinggerd/src/watcher.rs b/crates/dinggerd/src/watcher.rs
index 46f5ebd..56107d6 100644
--- a/crates/dinggerd/src/watcher.rs
+++ b/crates/dinggerd/src/watcher.rs
@@ -46,13 +46,25 @@ impl Watcher {
}
pub fn run(&mut self) -> Result<(), String> {
- // Fail fast when tmux is unreachable at startup; after that, keep
- // retrying — the server may come back.
- self.tick()?;
info!(interval = self.interval_secs, "watching tmux panes");
+ // dingger is a client of the tmux server, which may not exist yet —
+ // it starts when the first terminal opens one. Wait for it, then
+ // keep watching across server restarts.
+ let mut had_server = false;
loop {
- if let Err(err) = self.tick() {
- warn!(%err, "watch tick failed");
+ match self.tick() {
+ Ok(()) => {
+ if !had_server {
+ info!("tmux server available");
+ had_server = true;
+ }
+ }
+ Err(err) => {
+ if had_server {
+ warn!(%err, "tmux server unavailable; waiting for it to return");
+ }
+ had_server = false;
+ }
}
std::thread::sleep(std::time::Duration::from_secs(self.interval_secs));
}
diff --git a/packaging/launchd/dev.dingger.dingger.plist b/packaging/launchd/dev.dingger.dingger.plist
new file mode 100644
index 0000000..212b903
--- /dev/null
+++ b/packaging/launchd/dev.dingger.dingger.plist
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>EnvironmentVariables</key>
+ <dict>
+ <key>PATH</key>
+ <string>/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin</string>
+ <key>RUST_LOG</key>
+ <string>debug</string>
+ </dict>
+ <key>KeepAlive</key>
+ <true/>
+ <key>Label</key>
+ <string>dev.dingger.dingger</string>
+ <key>ProgramArguments</key>
+ <array>
+ <string>/Users/YOURUSER/.local/bin/dingger</string>
+ <string>watch</string>
+ </array>
+ <key>RunAtLoad</key>
+ <true/>
+ <key>StandardErrorPath</key>
+ <string>/Users/YOURUSER/Library/Logs/dingger.log</string>
+ <key>StandardOutPath</key>
+ <string>/Users/YOURUSER/Library/Logs/dingger.log</string>
+ <key>ThrottleInterval</key>
+ <integer>10</integer>
+</dict>
+</plist>