-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathhotkeys.lua
More file actions
202 lines (183 loc) · 6.37 KB
/
hotkeys.lua
File metadata and controls
202 lines (183 loc) · 6.37 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
-- Voice Realtime - Hammerspoon Hotkey Configuration
-- Push-to-talk conversational AI with multiple personas
-- Auto-detect paths: look for voice-env in common locations
local HOME = os.getenv("HOME")
local function findPython()
local candidates = {
HOME .. "/voice-env/bin/python", -- Default install location
HOME .. "/.local/share/voice-realtime/venv/bin/python",
"/opt/homebrew/bin/python3", -- Homebrew fallback
"/usr/local/bin/python3", -- Intel Mac fallback
}
for _, path in ipairs(candidates) do
local f = io.open(path, "r")
if f then
f:close()
return path
end
end
return candidates[1] -- Default even if not found (will error clearly)
end
local function findMainScript()
-- Look for main.py relative to this Lua file's config, or common locations
local candidates = {
HOME .. "/voice-realtime/main.py", -- Symlink/clone location
HOME .. "/Projects/AI/nobody/main.py", -- Dev location
HOME .. "/.local/share/voice-realtime/main.py", -- XDG location
}
for _, path in ipairs(candidates) do
local f = io.open(path, "r")
if f then
f:close()
return path
end
end
return candidates[1]
end
local PYTHON = findPython()
local MAIN_SCRIPT = findMainScript()
-- Run Python command
local function runCommand(args)
local task = hs.task.new(PYTHON, function(exitCode, stdOut, stdErr)
if exitCode ~= 0 then
hs.notify.new({title = "Voice Error", informativeText = stdErr or "Command failed"}):send()
end
end, args)
task:start()
end
-- Run Python command and type output at cursor
local function runCommandAndType(args)
local task = hs.task.new(PYTHON, function(exitCode, stdOut, stdErr)
if exitCode ~= 0 then
hs.notify.new({title = "Voice Error", informativeText = stdErr or "Command failed"}):send()
return
end
if stdOut and stdOut ~= "" then
-- Trim whitespace
local text = stdOut:gsub("^%s+", ""):gsub("%s+$", "")
if text ~= "" then
-- Copy to clipboard and paste using AppleScript
hs.pasteboard.setContents(text)
hs.timer.doAfter(0.2, function()
hs.osascript.applescript('tell application "System Events" to keystroke "v" using command down')
end)
else
hs.alert.show("Empty text", 1)
end
else
hs.alert.show("No transcription", 1)
end
end, args)
task:start()
end
-- Push-to-dictate: Cmd+Shift+D (hold to speak, release to type at cursor)
local pushToDictate = hs.hotkey.new({"cmd", "shift"}, "D",
function()
hs.alert.show("📝 Dictating...", 1)
runCommand({MAIN_SCRIPT, "start"})
end,
function()
hs.alert.show("⌨️ Typing...", 1)
runCommandAndType({MAIN_SCRIPT, "dictate"})
end
)
pushToDictate:enable()
-- Push-to-talk: Cmd+Shift+T (hold to speak, release to process)
local pushToTalk = hs.hotkey.new({"cmd", "shift"}, "T",
function()
hs.alert.show("🎤 Listening...", 1)
runCommand({MAIN_SCRIPT, "start"})
end,
function()
hs.alert.show("🤖 Processing...", 1)
runCommand({MAIN_SCRIPT, "stop_and_process"})
end
)
pushToTalk:enable()
-- Stop: Cmd+Shift+X
hs.hotkey.bind({"cmd", "shift"}, "X", function()
hs.alert.show("⏹ Stopped", 1)
runCommand({MAIN_SCRIPT, "stop"})
end)
-- Read selection aloud: Cmd+Shift+S
hs.hotkey.bind({"cmd", "shift"}, "S", function()
-- Copy selected text
hs.eventtap.keyStroke({"cmd"}, "c")
-- Small delay to ensure clipboard is updated
hs.timer.doAfter(0.1, function()
local text = hs.pasteboard.getContents()
if text and text ~= "" then
hs.alert.show("🔊 Reading...", 1)
runCommand({MAIN_SCRIPT, "speak", text})
else
hs.alert.show("No text selected", 1)
end
end)
end)
-- Persona switches
hs.hotkey.bind({"cmd", "shift"}, "1", function()
hs.alert.show("👤 Assistant", 1)
runCommand({MAIN_SCRIPT, "persona", "assistant"})
end)
hs.hotkey.bind({"cmd", "shift"}, "2", function()
hs.alert.show("📚 Tutor", 1)
runCommand({MAIN_SCRIPT, "persona", "tutor"})
end)
hs.hotkey.bind({"cmd", "shift"}, "3", function()
hs.alert.show("🎨 Creative", 1)
runCommand({MAIN_SCRIPT, "persona", "creative"})
end)
hs.hotkey.bind({"cmd", "shift"}, "4", function()
hs.alert.show("😊 Casual", 1)
runCommand({MAIN_SCRIPT, "persona", "casual"})
end)
-- Model chooser: Cmd+Shift+M
hs.hotkey.bind({"cmd", "shift"}, "M", function()
-- Get models as JSON from Python
local task = hs.task.new(PYTHON, function(exitCode, stdOut, stdErr)
if exitCode ~= 0 or not stdOut or stdOut == "" then
hs.alert.show("Failed to load models", 2)
return
end
local ok, models = pcall(hs.json.decode, stdOut)
if not ok or not models then
hs.alert.show("Failed to parse models", 2)
return
end
-- Build chooser choices
local choices = {}
for _, model in ipairs(models) do
local text = model.name
if model.current then
text = "✓ " .. text
end
local subText = model.id .. " [" .. model.provider .. "]"
if #model.features > 0 then
subText = subText .. " - " .. table.concat(model.features, ", ")
end
table.insert(choices, {
text = text,
subText = subText,
modelId = model.id,
modelName = model.name
})
end
-- Show chooser
local chooser = hs.chooser.new(function(choice)
if choice then
hs.alert.show("🤖 " .. choice.modelName, 1)
runCommand({MAIN_SCRIPT, "model", choice.modelId})
end
end)
chooser:choices(choices)
chooser:placeholderText("Select a model...")
chooser:searchSubText(true)
chooser:show()
end, {MAIN_SCRIPT, "model_json"})
task:start()
end)
-- Reload: Cmd+Shift+R
hs.hotkey.bind({"cmd", "shift"}, "R", function()
hs.reload()
end)
hs.alert.show("Voice Realtime ready!", 2)