Skip to content

Commit 1b48807

Browse files
ian-rossClaude Opus 4.7
andcommitted
Gate desktop-notify focus tracking on raw interactive terminals
Co-authored-by: Claude Opus 4.7 <anthropic-claude-opus-4-7@pi.local> Pi-Model: anthropic/claude-opus-4-7
1 parent 11bb9d3 commit 1b48807

4 files changed

Lines changed: 24 additions & 4 deletions

File tree

packages/desktop-notify/README.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,11 @@ Send a desktop notification.
7676

7777
### `startFocusTracking()`
7878

79-
Start tracking terminal focus via DECSET 1004. Idempotent — safe to call multiple times. Listens on `process.stdin` for focus in/out escape sequences.
79+
Start tracking terminal focus via DECSET 1004. Idempotent — safe to call multiple times. No-ops unless stdin/stdout are TTYs and stdin is already in raw mode, to avoid echoed focus escape sequences in non-interactive runs. Listens on `process.stdin` for focus in/out escape sequences.
80+
81+
### `canTrackTerminalFocus()`
82+
83+
Returns `true` when focus tracking can be enabled safely for the current process.
8084

8185
### `stopFocusTracking()`
8286

@@ -96,7 +100,7 @@ Focus the previously captured terminal window. Called automatically on notificat
96100

97101
## How it works
98102

99-
**Focus tracking** uses the DECSET 1004 terminal escape sequence. When enabled, the terminal sends `\x1b[I` (focus gained) and `\x1b[O` (focus lost). These are intercepted on `process.stdin` before other handlers see them. Works with kitty, wezterm, foot, alacritty, iTerm2, Zed, and most modern terminals. Also works through tmux and abduco.
103+
**Focus tracking** uses the DECSET 1004 terminal escape sequence. When enabled, the terminal sends `\x1b[I` (focus gained) and `\x1b[O` (focus lost). Tracking is enabled only in raw interactive terminals; in non-raw/non-interactive mode those sequences would be echoed by the terminal and corrupt output. Works with kitty, wezterm, foot, alacritty, iTerm2, Zed, and most modern terminals. Also works through tmux and abduco.
100104

101105
**Click-to-focus** captures the terminal's window ID at initialization, then uses compositor-specific commands to focus it when a notification is clicked:
102106

packages/desktop-notify/src/extension.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,14 @@
99
*/
1010

1111
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
12-
import { startFocusTracking, stopFocusTracking } from "./focus.js";
12+
import { canTrackTerminalFocus, startFocusTracking, stopFocusTracking } from "./focus.js";
1313
import { sendNotification } from "./notify.js";
1414
import { captureWindowId } from "./window.js";
1515

1616
export default function (pi: ExtensionAPI) {
1717
pi.on("session_start", async (_event, _ctx) => {
18+
if (!canTrackTerminalFocus()) return;
19+
1820
startFocusTracking();
1921
await captureWindowId();
2022
});

packages/desktop-notify/src/focus.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,25 @@ const FOCUS_OUT = "\x1b[O";
1818
const ENABLE = "\x1b[?1004h";
1919
const DISABLE = "\x1b[?1004l";
2020

21+
/**
22+
* Returns true when DECSET 1004 focus tracking can be enabled safely.
23+
*
24+
* Focus events are terminal input. In canonical/non-raw mode the terminal line
25+
* discipline echoes those escape sequences, which corrupts non-interactive
26+
* output. Only enable reporting when Pi's interactive TUI has placed stdin in
27+
* raw mode and both stdio streams are attached to a terminal.
28+
*/
29+
export function canTrackTerminalFocus(): boolean {
30+
return Boolean(process.stdin.isTTY && process.stdout.isTTY && process.stdin.isRaw);
31+
}
32+
2133
/**
2234
* Start tracking terminal focus. Idempotent — safe to call multiple times.
35+
* No-ops unless the process is attached to an interactive raw terminal.
2336
*/
2437
export function startFocusTracking(): void {
2538
if (stdinListener) return;
39+
if (!canTrackTerminalFocus()) return;
2640

2741
process.stdout.write(ENABLE);
2842

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
export { sendNotification, type NotifyOptions } from "./notify.js";
2-
export { startFocusTracking, stopFocusTracking, isTerminalFocused } from "./focus.js";
2+
export { canTrackTerminalFocus, startFocusTracking, stopFocusTracking, isTerminalFocused } from "./focus.js";
33
export { captureWindowId, focusWindow } from "./window.js";

0 commit comments

Comments
 (0)