repositories / pi-ext
pi-ext
bugabingas pi extensions
owned by admin
extensions/os-notifier/focus.ts
Raw/** Terminal focus detection via DECSET ?1004. Shared via globalThis. */
const FOCUS_ENABLE = "\x1b[?1004h";
const FOCUS_DISABLE = "\x1b[?1004l";
const FOCUS_IN_SEQ = "\x1b[I";
const FOCUS_OUT_SEQ = "\x1b[O";
export type FocusListener = (focused: boolean) => void;
interface FocusState {
refs: number;
focused: boolean;
listener?: (chunk: Buffer) => void;
listeners: Set<FocusListener>;
buf: string;
}
const G = globalThis as unknown as { __bugabinga_pi_focus?: FocusState };
G.__bugabinga_pi_focus ??= {
refs: 0,
focused: true,
listeners: new Set(),
buf: "",
};
const state = G.__bugabinga_pi_focus;
function notify(next: boolean): void {
if (next === state.focused) return;
state.focused = next;
for (const fn of state.listeners) {
try {
fn(state.focused);
} catch {
/* best-effort */
}
}
}
export function isFocused(): boolean {
return state.focused;
}
export function onFocus(fn: FocusListener): void {
state.listeners.add(fn);
}
export function offFocus(fn: FocusListener): void {
state.listeners.delete(fn);
}
export function attach(): void {
if (!process.stdout.isTTY || !process.stdin.isTTY) return;
state.refs++;
if (state.refs > 1) return;
try {
process.stdout.write(FOCUS_ENABLE);
} catch {
state.refs--;
return;
}
const maxSeq = Math.max(FOCUS_IN_SEQ.length, FOCUS_OUT_SEQ.length);
const handler = (chunk: Buffer) => {
try {
state.buf += chunk.toString("binary");
let i = 0;
while (i + maxSeq <= state.buf.length) {
if (state.buf.startsWith(FOCUS_IN_SEQ, i)) {
notify(true);
i += FOCUS_IN_SEQ.length;
} else if (state.buf.startsWith(FOCUS_OUT_SEQ, i)) {
notify(false);
i += FOCUS_OUT_SEQ.length;
} else {
i++;
}
}
state.buf = state.buf.slice(i);
if (state.buf.length > 64) state.buf = state.buf.slice(-(maxSeq - 1));
} catch {
/* best-effort */
}
};
process.stdin.on("data", handler);
state.listener = handler;
}
export function detach(): void {
if (state.refs <= 0) return;
state.refs--;
if (state.refs > 0) return;
if (state.listener) {
try {
process.stdin.off("data", state.listener);
} catch {
/* noop */
}
state.listener = undefined;
}
try {
process.stdout.write(FOCUS_DISABLE);
} catch {
/* noop */
}
}