opencode-voice 0.1.4

A cli utility to control opencode using voice via the HTTP API
Documentation
// key-monitor — Lightweight global hotkey monitor for opencode-voice.
//
// Uses a CGEventTap (listen-only) to detect keydown/keyup for a single key,
// then prints JSON events to stdout.  The parent Bun process reads these
// lines to drive push-to-talk recording.
//
// Usage:  key-monitor <keycode>
//   keycode = macOS virtual keycode (decimal), e.g. 61 for Right Option.
//
// Output (one JSON object per line):
//   {"event":"ready"}              — tap is active, monitoring
//   {"event":"keydown"}            — target key pressed
//   {"event":"keyup"}              — target key released
//
// Requires Accessibility permission (System Settings > Privacy & Security).
// Exits with code 1 and a JSON error on stderr if not granted.
//
// Build:
//   swiftc -O -o key-monitor key-monitor.swift -framework Cocoa

import Cocoa

// ---------------------------------------------------------------------------
// Context struct passed to the C callback via userInfo/refcon.
// CGEventTapCallBack is a C function pointer so it cannot capture Swift
// variables — all mutable state lives here.
// ---------------------------------------------------------------------------

struct MonitorContext {
    var targetKeycode: Int64
    var isModifierKey: Bool
    var isPressed: Bool
    var eventTap: CFMachPort?
}

// ---------------------------------------------------------------------------
// Modifier key detection
// ---------------------------------------------------------------------------
// Modifier keys fire CGEventType.flagsChanged instead of keyDown/keyUp.

let modifierKeycodes: Set<Int64> = [
    0x37,  // Left Command
    0x36,  // Right Command
    0x38,  // Left Shift
    0x3C,  // Right Shift
    0x3A,  // Left Option
    0x3D,  // Right Option
    0x3B,  // Left Control
    0x3E,  // Right Control
    0x3F,  // Function (fn)
    0x39,  // Caps Lock
]

// ---------------------------------------------------------------------------
// Parse arguments
// ---------------------------------------------------------------------------

guard CommandLine.arguments.count >= 2,
      let targetKeycode = Int64(CommandLine.arguments[1]) else {
    fputs("{\"event\":\"error\",\"message\":\"Usage: key-monitor <keycode>\"}\n", stderr)
    exit(1)
}

// ---------------------------------------------------------------------------
// Accessibility check
// ---------------------------------------------------------------------------

let options: NSDictionary = [
    kAXTrustedCheckOptionPrompt.takeRetainedValue(): true
]

guard AXIsProcessTrustedWithOptions(options) else {
    fputs("{\"event\":\"error\",\"message\":\"Accessibility permission required. Grant access in System Settings > Privacy & Security > Accessibility.\"}\n", stderr)
    exit(1)
}

// ---------------------------------------------------------------------------
// Allocate context on the heap so the C callback can access it safely.
// ---------------------------------------------------------------------------

let ctxPtr = UnsafeMutablePointer<MonitorContext>.allocate(capacity: 1)
ctxPtr.initialize(to: MonitorContext(
    targetKeycode: targetKeycode,
    isModifierKey: modifierKeycodes.contains(targetKeycode),
    isPressed: false,
    eventTap: nil
))

// ---------------------------------------------------------------------------
// Event callback (C function pointer — no captures allowed)
// ---------------------------------------------------------------------------

func eventCallback(
    proxy: CGEventTapProxy,
    type: CGEventType,
    event: CGEvent,
    refcon: UnsafeMutableRawPointer?
) -> Unmanaged<CGEvent>? {
    guard let refcon = refcon else {
        return Unmanaged.passRetained(event)
    }

    let ctx = refcon.assumingMemoryBound(to: MonitorContext.self)

    // Re-enable tap if macOS disabled it (e.g. timeout).
    if type == .tapDisabledByTimeout || type == .tapDisabledByUserInput {
        if let tap = ctx.pointee.eventTap {
            CGEvent.tapEnable(tap: tap, enable: true)
        }
        return Unmanaged.passRetained(event)
    }

    let kc = event.getIntegerValueField(.keyboardEventKeycode)
    guard kc == ctx.pointee.targetKeycode else {
        return Unmanaged.passRetained(event)
    }

    if ctx.pointee.isModifierKey {
        // Modifier keys: flagsChanged fires once on press, once on release.
        if !ctx.pointee.isPressed {
            ctx.pointee.isPressed = true
            print("{\"event\":\"keydown\"}")
            fflush(stdout)
        } else {
            ctx.pointee.isPressed = false
            print("{\"event\":\"keyup\"}")
            fflush(stdout)
        }
    } else {
        // Regular keys: distinct keyDown / keyUp events.
        if type == .keyDown && !ctx.pointee.isPressed {
            ctx.pointee.isPressed = true
            print("{\"event\":\"keydown\"}")
            fflush(stdout)
        } else if type == .keyUp && ctx.pointee.isPressed {
            ctx.pointee.isPressed = false
            print("{\"event\":\"keyup\"}")
            fflush(stdout)
        }
    }

    return Unmanaged.passRetained(event)
}

// ---------------------------------------------------------------------------
// Create event tap
// ---------------------------------------------------------------------------

var eventMask: CGEventMask
if ctxPtr.pointee.isModifierKey {
    eventMask = (1 << CGEventType.flagsChanged.rawValue)
} else {
    eventMask = (1 << CGEventType.keyDown.rawValue) | (1 << CGEventType.keyUp.rawValue)
}

guard let tap = CGEvent.tapCreate(
    tap: .cgSessionEventTap,
    place: .headInsertEventTap,
    options: .listenOnly,
    eventsOfInterest: eventMask,
    callback: eventCallback,
    userInfo: ctxPtr
) else {
    fputs("{\"event\":\"error\",\"message\":\"Failed to create event tap. Check Accessibility permissions.\"}\n", stderr)
    ctxPtr.deallocate()
    exit(1)
}

ctxPtr.pointee.eventTap = tap

// ---------------------------------------------------------------------------
// Run loop
// ---------------------------------------------------------------------------

let runLoopSource = CFMachPortCreateRunLoopSource(kCFAllocatorDefault, tap, 0)
CFRunLoopAddSource(CFRunLoopGetCurrent(), runLoopSource, .commonModes)
CGEvent.tapEnable(tap: tap, enable: true)

// Signal readiness to parent process.
print("{\"event\":\"ready\"}")
fflush(stdout)

// Clean shutdown on SIGINT / SIGTERM.
signal(SIGINT)  { _ in exit(0) }
signal(SIGTERM) { _ in exit(0) }

CFRunLoopRun()