Luigit
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 */
	}
}