-- rvoice.lua — Right-Option push-to-talk for the rvoice helper. -- -- Install: -- 1. Hammerspoon → Preferences → enable "Launch Hammerspoon at login" -- 2. Add this line to ~/.hammerspoon/init.lua: -- require("rvoice") -- 3. Symlink this file so init.lua can find it: -- ln -sfn ~/Code/@scripts/session-tools/hammerspoon/rvoice.lua \ -- ~/.hammerspoon/rvoice.lua -- 4. Reload Hammerspoon config (menu bar → Reload Config) -- 5. Grant Accessibility + Microphone permissions when prompted. -- -- Behavior: hold Right-Option to talk. Release to transcribe + inject into -- the active iTerm2 tab's remote tmux session. Taps shorter than 200ms are -- ignored (configurable via RVOICE_MIN_MS env in rvoice config). local M = {} -- Resolve `rvoice` once at load. Hammerspoon's task PATH is barebones, so -- prefer an explicit symlink in ~/.local/bin or fall back to the repo path. local function resolveRvoice() local candidates = { os.getenv("HOME") .. "/.local/bin/rvoice", os.getenv("HOME") .. "/Code/@scripts/session-tools/bin/rvoice", } for _, p in ipairs(candidates) do local f = io.open(p, "r") if f then f:close(); return p end end return "rvoice" end local RVOICE = resolveRvoice() local holding = false -- Run rvoice in the background; capture stderr to the system log so -- failures are visible via Hammerspoon's console. local function run(cmd) local t = hs.task.new("/bin/sh", function(exit, _, err) if exit ~= 0 then hs.printf("[rvoice] %s exited %d: %s", cmd, exit, err or "") end end, {"-c", RVOICE .. " " .. cmd}) -- Inherit user shell env so GROQ_API_KEY (and PATH for ffmpeg/jq) work. t:setEnvironment(hs.execute("env", true):gsub("\n$", "") and nil or nil) t:start() end -- Right-Option keyDown/keyUp. Hammerspoon delivers modifier changes through -- eventtap.flagsChanged; we watch for the rightAlt flag transitioning. M.tap = hs.eventtap.new({ hs.eventtap.event.types.flagsChanged }, function(e) -- macOS exposes the side via a per-key mask. Right-Option is 0x40 in the -- raw `keyCode` event of type flagsChanged (code 61). local code = e:getKeyCode() if code ~= 61 then return false end -- 61 = Right Option local flags = e:getFlags() local pressed = flags.alt or false if pressed and not holding then holding = true run("start") elseif (not pressed) and holding then holding = false run("stop") end return false -- don't swallow the modifier; other apps may use it end) M.tap:start() hs.alert.show("rvoice: Right ⌥ to talk") return M