Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
48 changes: 47 additions & 1 deletion src/focus.test.ts
Original file line number Diff line number Diff line change
@@ -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", () => {
Expand Down Expand Up @@ -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([
Expand Down
33 changes: 27 additions & 6 deletions src/focus.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}"])
Expand Down Expand Up @@ -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
}
Expand Down
Loading