diff --git a/README.md b/README.md index 8a7e2f3..f0d6dde 100644 --- a/README.md +++ b/README.md @@ -381,7 +381,7 @@ To disable this and always get notified: **Unsupported compositors**: Wayland has no standard protocol for querying the focused window. Each compositor has its own IPC, and GNOME intentionally doesn't expose focus information. Unsupported compositors fall back to always notifying. -**tmux/screen**: When running inside tmux, focus detection uses tmux pane state (`session_attached`, `window_active`, `pane_active`) via `tmux display-message`. This keeps suppression accurate when switching panes/windows/sessions. GNU Screen is not currently handled (falls back to always notifying). +**tmux/screen**: When running inside tmux, focus detection uses tmux pane state (`session_attached`, `window_active`, `pane_active`) via `tmux display-message`. This keeps suppression accurate when switching panes/windows/sessions. On Linux setups where window focus cannot be detected at all, tmux pane state is also used as a best-effort fallback. GNU Screen is not currently handled (falls back to always notifying). **WezTerm panes**: When running in WezTerm with `WEZTERM_PANE` set, focus suppression is pane-aware via `wezterm cli list-clients --format json`. This means notifications are shown when you switch to a different WezTerm pane/tab. diff --git a/src/focus.test.ts b/src/focus.test.ts index ba92190..aa9292a 100644 --- a/src/focus.test.ts +++ b/src/focus.test.ts @@ -1,5 +1,5 @@ import { describe, test, expect } from "bun:test" -import { isMacTerminalAppFocused, isTmuxPaneFocused, parseWezTermFocusedPaneId } from "./focus" +import { isLinuxTerminalFocused, isMacTerminalAppFocused, isTmuxPaneFocused, parseWezTermFocusedPaneId } from "./focus" describe("isMacTerminalAppFocused", () => { test("matches Terminal when TERM_PROGRAM is Apple_Terminal", () => { @@ -75,6 +75,52 @@ describe("isTmuxPaneFocused", () => { }) }) +describe("isLinuxTerminalFocused", () => { + test("falls back to tmux pane state when window id is unavailable", () => { + expect( + isLinuxTerminalFocused({ + cachedWindowId: null, + currentWindowId: null, + wezTermPaneActive: true, + tmuxPaneActive: true, + }) + ).toBe(true) + }) + + test("does not suppress without tmux when window id is unavailable", () => { + expect( + isLinuxTerminalFocused({ + cachedWindowId: null, + currentWindowId: null, + wezTermPaneActive: true, + tmuxPaneActive: null, + }) + ).toBe(false) + }) + + test("does not suppress when wezterm pane is inactive", () => { + expect( + isLinuxTerminalFocused({ + cachedWindowId: null, + currentWindowId: null, + wezTermPaneActive: false, + tmuxPaneActive: true, + }) + ).toBe(false) + }) + + test("keeps existing window-id check when available", () => { + expect( + isLinuxTerminalFocused({ + cachedWindowId: "123", + currentWindowId: "456", + wezTermPaneActive: true, + tmuxPaneActive: true, + }) + ).toBe(false) + }) +}) + describe("parseWezTermFocusedPaneId", () => { test("returns pane id from valid list-clients JSON", () => { const output = JSON.stringify([ diff --git a/src/focus.ts b/src/focus.ts index a429c09..ec1a844 100644 --- a/src/focus.ts +++ b/src/focus.ts @@ -207,6 +207,26 @@ export function isTmuxPaneFocused(tmuxPane: string | null | undefined, probeResu return sessionAttached === "1" && windowActive === "1" && paneActive === "1" } +export function isLinuxTerminalFocused(params: { + cachedWindowId: string | null + currentWindowId: string | null + wezTermPaneActive: boolean + tmuxPaneActive: boolean | null +}): boolean { + const { cachedWindowId, currentWindowId, wezTermPaneActive, tmuxPaneActive } = params + + if (!cachedWindowId) { + if (!wezTermPaneActive) return false + if (tmuxPaneActive !== null) return tmuxPaneActive + return false + } + + if (currentWindowId !== cachedWindowId) return false + if (!wezTermPaneActive) return false + if (tmuxPaneActive !== null) return tmuxPaneActive + return true +} + function isTmuxPaneActive(): boolean { const tmuxPane = process.env.TMUX_PANE ?? null const result = execFileWithTimeout("tmux", ["display-message", "-t", tmuxPane ?? "", "-p", "#{session_attached} #{window_active} #{pane_active}"]) @@ -239,12 +259,13 @@ export function isTerminalFocused(): boolean { return true } - if (!cachedWindowId) return false - const currentId = getActiveWindowId() - if (currentId !== cachedWindowId) return false - if (!isWezTermPaneActive()) return false - if (process.env.TMUX) return isTmuxPaneActive() - return true + const tmuxPaneActive = process.env.TMUX ? isTmuxPaneActive() : null + return isLinuxTerminalFocused({ + cachedWindowId, + currentWindowId: getActiveWindowId(), + wezTermPaneActive: isWezTermPaneActive(), + tmuxPaneActive, + }) } catch { return false }