terminal commands are silent when tmux runs them tmux shows a hook's output and a non-zero exit in view mode over the pane. the focus hook's normal outcomes (focus moved on, Ghostty not in front) exited 1, and the one-second settling made "focus moved on" common: "'dingger terminal focused --client ...' returned 1" kept covering panes. `dingger terminal focused` and `terminal raise` now always exit 0 and print nothing unless run by hand. tmux::client_format checks that tmux answered for the client asked about: for a client that is gone, tmux answers for the current one, so a raise for a client that had just detached raised another client's terminal. e2e step 20: no pane in view mode after the hook's failures, a gone client raises nothing, and a focus held past the settling second; the old code fails the new checks.
Igor Soarez igor@soarez.org
Sun, 25 Oct 2026 07:44:25 +0200
3 files changed,
30 insertions(+),
8 deletions(-)
M
crates/dinggerd/src/main.rs
→
crates/dinggerd/src/main.rs
@@ -13,6 +13,7 @@ mod tmux;
mod watcher; use clap::{Parser, Subcommand}; +use std::io::IsTerminal; use tracing_subscriber::EnvFilter; #[derive(Parser)]@@ -128,7 +129,14 @@ platform::default_sigpipe();
let cli = Cli::parse(); // The daemon logs its work; commands print their own output and only // log problems. - let default_level = if matches!(cli.command, Command::Watch { .. }) { "info" } else { "warn" }; + // tmux shows whatever its hooks print, and a non-zero exit, as a + // message: the commands tmux runs are silent unless run by hand. + let by_tmux = matches!(cli.command, Command::Terminal { .. }) && !std::io::stderr().is_terminal(); + let default_level = match cli.command { + Command::Watch { .. } => "info", + _ if by_tmux => "off", + _ => "warn", + }; tracing_subscriber::fmt() .with_env_filter(EnvFilter::try_from_default_env().unwrap_or_else(|_| default_level.into())) .init();@@ -197,9 +205,12 @@ let result = match &action {
TerminalAction::Focused { client } => terminals::learn(client), TerminalAction::Raise { client } => terminals::raise(client), }; + // Best effort, run by tmux (the focus hook, a raise): an outcome + // like "focus moved on" or "terminal gone" is not a failure. if let Err(err) = result { - eprintln!("dingger: {err}"); - std::process::exit(1); + if !by_tmux { + eprintln!("dingger: {err}"); + } } } // Never fails the agent: exits 0, prints nothing.
M
crates/dinggerd/src/tmux.rs
→
crates/dinggerd/src/tmux.rs
@@ -274,10 +274,12 @@ }
/// 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()) + // For a client that is gone, tmux answers for the current one instead: + // the answer names its client, and must name this one. + let out = run_tmux(&["display-message", "-p", "-c", client, &format!("#{{client_name}}\t{format}")]).ok()?; + let (name, value) = out.trim_end_matches('\n').split_once('\t')?; + let value = value.trim(); + (name == client && !value.is_empty()).then(|| value.to_string()) } /// The sessions holding a pane's window (several: a session group, a linked
M
tests/e2e.sh
→
tests/e2e.sh
@@ -729,7 +729,8 @@ : >"$DIR/ghostty-raised"
renv "$DINGGER" focus --pane "$AG" expect "a jump raises the focused window's terminal" "tail -1 $DIR/ghostty-raised" BBBB-2222 50 expect "which shows the agent" "r display -p -c $TTY2 '#{window_name}'" agent2 -focus_out viewer2; focus_in viewer AAAA-1111; sleep 0.5; focus_out viewer +# Focus held past learn's settling second. +focus_out viewer2; focus_in viewer AAAA-1111; sleep 1.5; focus_out viewer r select-window -t '=0:other' renv "$DINGGER" focus --pane "$AG" expect "nothing focused: the most recently focused window's" "tail -1 $DIR/ghostty-raised" AAAA-1111 50@@ -762,6 +763,7 @@ focus_in viewer CCCC-3333
expect "Ghostty asked" "wc -l <$DIR/ghostty-queries | tr -d ' '" 1 50 sleep 1 expect "focus moved on while Ghostty answered: not paired" "paired $TTY1" AAAA-1111 +expect "which tmux does not report as an error (view mode over a pane)" "r list-panes -a -F '#{pane_mode}' | grep -c view-mode" 0 rm -f "$DIR/ghostty-race" focus_in viewer2 AAAA-1111 expect "a terminal holds one client: the newer pairing" "paired $TTY2" AAAA-1111 50@@ -771,10 +773,17 @@ touch "$DIR/ghostty-gone"; : >"$DIR/opened"
renv "$DINGGER" focus --pane "$AG" expect "a terminal that is gone is forgotten" "paired $TTY2" "" 50 expect "and the app is brought forward instead" "cat $DIR/opened" "$FAKE_APP" 50 +expect "nor that" "r list-panes -a -F '#{pane_mode}' | grep -c view-mode" 0 rm -f "$DIR/ghostty-gone"; : >"$DIR/opened"; : >"$DIR/ghostty-raised" renv DINGGER_TERMINAL_APP="$FAKE_APP" DINGGER_OPEN="$DIR/open" "$DINGGER" focus --pane "$AG" expect "no pairing: the app" "cat $DIR/opened" "$FAKE_APP" 50 expect "and no terminal raised" "cat $DIR/ghostty-raised" "" +# tmux answers for the current client when asked about one that is gone. +r set -g "@dingger_terminal_$(basename "$TTY1")" "$(date +%s)000 AAAA-1111 $FAKE_APP" +r set -g "@dingger_terminal_$(basename "$TTY2")" "$(date +%s)000 BBBB-2222 $FAKE_APP" +r run-shell "$DINGGER terminal raise --client /dev/gone" +sleep 0.5 +expect "a client that is gone raises no other client's terminal" "cat $DIR/ghostty-raised" "" # A window on a session of a group stays on it: a pane target alone lets # tmux pick a session of the group, the most recently used. r new-session -d -t grp -s grp-b