[eric] dictation: fn dead-key deadlock broken; a no-prompt IOHIDCheckAccess boot probe arms fn on granted machines and intent arming covers the rest (Haik, 99% of devices)

This commit is contained in:
ciregenz
2026-08-19 17:24:23 -07:00
parent 7a4af2c419
commit 46fe42674f
3 changed files with 67 additions and 4 deletions
+10
View File
@@ -3,6 +3,16 @@
// tap: needs the same Input Monitoring grant the app already requests, never swallows anything.
import CoreGraphics
import Foundation
import IOKit.hid
// --no-prompt: the boot-time probe must never raise the Input Monitoring TCC prompt (ENG-341);
// IOHIDCheckAccess answers silently, so granted machines arm and everyone else exits clean.
if CommandLine.arguments.contains("--no-prompt")
&& IOHIDCheckAccess(kIOHIDRequestTypeListenEvent) != kIOHIDAccessTypeGranted {
print("e no-permission")
fflush(stdout)
exit(0)
}
var fnDown = false
var tapRef: CFMachPort?
+17 -4
View File
@@ -139,12 +139,12 @@ function installVoiceHotkey(getMainWindow) {
// ---- fn/Globe primary tier (macOS): the native watcher, since no JS tap can see keycode 63 ----
let fnProc = null;
let quitReaperWired = false;
const startFnWatcher = () => {
const startFnWatcher = (noPrompt) => {
if (process.platform !== 'darwin' || combo.special !== 'fn' || fnProc) return;
resolveFnWatcherBinary((bin) => {
if (!bin) { console.log('[voice] no fn watcher binary, legacy hotkey stays primary'); return; }
if (combo.special !== 'fn' || fnProc) return; // rebound or raced while compiling
startFnWatcherWith(bin);
startFnWatcherWith(bin, noPrompt === true);
});
};
// Kill fn-watchers left by a previous OpenSwarm that died badly. will-quit is the ONLY thing that
@@ -170,11 +170,11 @@ function installVoiceHotkey(getMainWindow) {
lastFnPokeMs = now;
try { fnProc.stdin.write('r\n'); } catch (_) {}
};
const startFnWatcherWith = (bin) => {
const startFnWatcherWith = (bin, noPrompt) => {
sweepStrayFnWatchers(bin);
try {
// stdin stays open on purpose: "r\n" re-arms the tap, and EOF tells an orphaned watcher to die.
fnProc = spawn(bin, [], { stdio: ['pipe', 'pipe', 'ignore'] });
fnProc = spawn(bin, noPrompt ? ['--no-prompt'] : [], { stdio: ['pipe', 'pipe', 'ignore'] });
} catch (e) {
console.log('[voice] fn watcher spawn failed:', e && e.message);
fnProc = null;
@@ -304,6 +304,10 @@ function installVoiceHotkey(getMainWindow) {
};
try {
if (fs.existsSync(path.join(app.getPath('userData'), 'dictation-used'))) armNativeTiers();
// No marker yet: fn is the DEFAULT hotkey, and the watcher is the only thing that can see it,
// so probe it in --no-prompt mode. Machines with Input Monitoring already granted get a working
// fn immediately; ungranted machines exit silently and never see a boot-time TCC prompt (ENG-341).
else if (combo.special === 'fn') startFnWatcher(true);
} catch (_) {}
app.on('browser-window-focus', () => { unregisterFallbackShortcut(); pokeFnWatcher(); });
app.on('browser-window-blur', registerVoiceShortcut);
@@ -326,6 +330,14 @@ function installVoiceHotkey(getMainWindow) {
const installVoiceHoldRelay = (contents) => {
contents.on('before-input-event', (event, input) => {
if (input.type !== 'keyDown' || input.isAutoRepeat) return;
// A bare Fn keydown Chromium happens to deliver while focused is both dictation intent
// (full-arm the tiers, prompt lands with context) and a press that must WORK right now.
if (combo.special === 'fn' && !primaryProven() && (input.key === 'Fn' || input.code === 'Fn')) {
armNativeTiers();
sendFallbackToggle();
event.preventDefault();
return;
}
if (inputMatchesCombo(input)) {
if (!primaryProven()) sendFallbackToggle();
event.preventDefault();
@@ -347,6 +359,7 @@ function installVoiceHotkey(getMainWindow) {
fallbackCombo = combo.special ? parseCombo(LEGACY_COMBO) : combo;
if (UiohookKeyRef && !combo.special) tapKeycode = uiohookKeycodeFor(combo.key, UiohookKeyRef);
if (tiersArmed) startFnWatcher();
else if (combo.special === 'fn') startFnWatcher(true);
unregisterFallbackShortcut();
registerVoiceShortcut();
console.log('[voice] hotkey set to', combo.accel);
+40
View File
@@ -0,0 +1,40 @@
// The fn dead-key deadlock (Haik, exp.16, ~99% of devices): the dictation-used marker was only
// written by a successful transcription, the fn watcher only started once the marker existed, and
// fn was the default hotkey, so a never-dictated install could never dictate. The fix is a boot
// probe that arms fn WITHOUT ever prompting (IOHIDCheckAccess preflight) plus intent arming.
const test = require('node:test');
const assert = require('node:assert');
const fs = require('node:fs');
const path = require('node:path');
const swiftSrc = fs.readFileSync(path.join(__dirname, 'native', 'fn-watcher.swift'), 'utf8');
const hotkeySrc = fs.readFileSync(path.join(__dirname, 'voiceHotkey.js'), 'utf8');
test('the swift preflight refuses to prompt in --no-prompt mode', () => {
const preflight = swiftSrc.indexOf('--no-prompt');
const firstTap = swiftSrc.indexOf('func armTap');
assert.ok(preflight > -1, 'preflight missing');
assert.ok(preflight < firstTap, 'preflight must run before any tap can raise the TCC prompt');
assert.match(swiftSrc, /IOHIDCheckAccess\(kIOHIDRequestTypeListenEvent\)/);
});
test('boot probes fn silently when the marker is absent', () => {
const bootBlock = hotkeySrc.split("fs.existsSync(path.join(app.getPath('userData'), 'dictation-used'))")[1].slice(0, 400);
assert.match(bootBlock, /combo\.special === 'fn'.*startFnWatcher\(true\)/s,
'a fresh install with the default fn hotkey must get the no-prompt probe at boot');
});
test('rebinding to fn before tiers are armed also probes', () => {
assert.match(hotkeySrc, /else if \(combo\.special === 'fn'\) startFnWatcher\(true\);\n unregisterFallbackShortcut/);
});
test('a focused bare Fn keydown arms the tiers and toggles', () => {
const relay = hotkeySrc.split('installVoiceHoldRelay')[1].slice(0, 900);
assert.match(relay, /input\.key === 'Fn'/);
assert.match(relay, /armNativeTiers\(\)/);
assert.match(relay, /sendFallbackToggle\(\)/);
});
test('the probe flag reaches the spawn argv', () => {
assert.match(hotkeySrc, /spawn\(bin, noPrompt \? \['--no-prompt'\] : \[\]/);
});