-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathinit.lua
More file actions
217 lines (185 loc) · 7.97 KB
/
Copy pathinit.lua
File metadata and controls
217 lines (185 loc) · 7.97 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
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
-- ============================================================
-- Kris Jenner Reply — double-tap Control to generate a warm,
-- Kris-Jenner-style reply for the Lark message on your clipboard,
-- then auto-paste it into the focused Lark message box.
-- ============================================================
local MODEL = "claude-opus-4-8"
local MAX_TOKENS = 400
local DEFAULT_BASE = "https://api.anthropic.com" -- overridden by $ANTHROPIC_BASE_URL
local DOUBLE_TAP_GAP = 0.35 -- seconds between the two Control taps
-- The warm-voice persona (condensed from the warm-voice skill) -------------
local SYSTEM_PROMPT_EN = [[
You write a reply the user can send in a chat. The voice is warm, genuine, and
upbeat — the kind of message that makes the other person feel good — but natural,
not gushing or over-the-top. Reply in the SAME language as the incoming message.
Voice rules:
- Open by acknowledging or affirming the other person, then handle the actual point.
- Be specific to THIS message; never generic, no empty flattery.
- Warm but measured: don't pile on adjectives or superlatives, don't overdo it.
- Go easy on exclamation points; emoji rarely, at most one.
- For work messages, keep it warm but composed and professional.
- Keep it tight — usually two or three sentences is plenty.
Output ONLY the reply text — no preamble, no quotes, no explanation.
]]
-- 中文版:温暖但自然、不浮夸 ---------------------------------------------
local SYSTEM_PROMPT_ZH = [[
你帮用户写一条可以直接发在聊天里的中文回复,语气温暖、真诚、得体——像一个让人
放心的人,而不是浮夸的"彩虹屁"。
原则:
- 先简单认可或回应对方,再把事情接住、说清楚。
- 真诚,落在具体的这条消息上,别套话、别空夸。
- 温暖但克制:不肉麻、不夸张、不堆形容词。
- 少用感叹号;表情通常不用,最多一个。
- 工作场景更要稳重专业,像一个温暖可靠的同事。
- 长度适中,一般两三句话就够。
只输出回复正文——不要前言、不要引号、不要解释。一律用中文。
]]
-- HUD helper ---------------------------------------------------------------
local function hud(text)
hs.alert.closeAll()
return hs.alert.show(text, {
radius = 12,
strokeWidth = 0,
fillColor = { white = 0, alpha = 0.85 },
textColor = { white = 1 },
textSize = 16,
}, 60) -- long duration; we close it manually
end
-- Does the text contain Chinese (CJK Unified Ideographs)? UTF-8 lead bytes 0xE4–0xE9.
local function hasChinese(s)
return s:find("[\228-\233][\128-\191][\128-\191]") ~= nil
end
-- Core: read clipboard -> call API -> auto-paste ---------------------------
-- mode = "zh" forces Chinese; mode = "auto" follows the message's language.
local function generateReply(mode)
local context = hs.pasteboard.getContents()
if not context or context:gsub("%s", "") == "" then
hs.alert.closeAll()
local msg = (mode == "zh")
and "先复制一条消息(⌘C),再双击 Option"
or "Copy a message first (⌘C), then double-tap Control"
hs.alert.show(msg, 2.5)
return
end
-- GUI-launched apps don't inherit the shell environment, so read the key
-- from a login shell (which sources your profile) and fall back to env.
local apiKey = os.getenv("ANTHROPIC_API_KEY")
if not apiKey or apiKey == "" then
apiKey = (hs.execute("echo -n $ANTHROPIC_API_KEY", true) or ""):gsub("%s+$", "")
end
if not apiKey or apiKey == "" then
hs.alert.closeAll()
hs.alert.show("ANTHROPIC_API_KEY not found", 3)
return
end
-- Respect a custom gateway via $ANTHROPIC_BASE_URL (defaults to api.anthropic.com).
local baseURL = os.getenv("ANTHROPIC_BASE_URL")
if not baseURL or baseURL == "" then
baseURL = (hs.execute("echo -n $ANTHROPIC_BASE_URL", true) or ""):gsub("%s+$", "")
end
if not baseURL or baseURL == "" then baseURL = DEFAULT_BASE end
local url = (baseURL:gsub("/+$", "")) .. "/v1/messages"
-- Decide the reply language: forced zh, or auto-detected from the message.
local useZh = (mode == "zh") or (mode == "auto" and hasChinese(context))
local systemPrompt = useZh and SYSTEM_PROMPT_ZH or SYSTEM_PROMPT_EN
hud(useZh and "💋 生成中…" or "💋 Generating…")
local userMsg = useZh
and ("我要回复的这条消息:\n\n" .. context .. "\n\n请用温暖但自然、不浮夸的语气,用中文帮我写这条回复。")
or ("Here is the message I'm replying to:\n\n" .. context
.. "\n\nWrite my reply in the warm voice, in the same language as the message.")
local body = hs.json.encode({
model = MODEL,
max_tokens = MAX_TOKENS,
system = systemPrompt,
messages = {
{ role = "user", content = userMsg },
},
})
local headers = {
["x-api-key"] = apiKey,
["anthropic-version"] = "2023-06-01",
["content-type"] = "application/json",
}
hs.http.asyncPost(url, body, headers, function(status, respBody, _)
if status ~= 200 then
hs.alert.closeAll()
hs.alert.show("API error (" .. tostring(status) .. ")\n" .. tostring(respBody):sub(1, 300), 4)
return
end
local ok, decoded = pcall(hs.json.decode, respBody)
if not ok or not decoded or not decoded.content or not decoded.content[1] then
hs.alert.closeAll()
hs.alert.show("Could not parse API response", 3)
return
end
-- Concatenate any text blocks
local reply = ""
for _, block in ipairs(decoded.content) do
if block.type == "text" and block.text then
reply = reply .. block.text
end
end
reply = reply:gsub("^%s+", ""):gsub("%s+$", "")
if reply == "" then
hs.alert.closeAll()
hs.alert.show("Empty reply from API", 3)
return
end
-- Auto-paste into the focused field (e.g. Lark message box).
local original = hs.pasteboard.getContents()
hs.pasteboard.setContents(reply)
hs.eventtap.keyStroke({ "cmd" }, "v", 0)
hs.alert.closeAll()
hs.alert.show("✨ Pasted", 1)
-- Restore the user's previous clipboard shortly after the paste lands.
hs.timer.doAfter(0.6, function()
if original then hs.pasteboard.setContents(original) end
end)
end)
end
-- Double-tap modifier detection -------------------------------------------
-- A "tap" = the modifier pressed and released cleanly, with no other key
-- pressed in between and no other modifier held. Watchers are kept in a global
-- table so Hammerspoon doesn't garbage-collect them (which silently stops them).
watchers = {}
local function registerDoubleTap(keycodes, flagName, onTrigger)
local lastUp = 0
local dirty = false
-- Mark the modifier "dirty" if any key is pressed while it's held (a real shortcut).
local keyW = hs.eventtap.new({ hs.eventtap.event.types.keyDown }, function(e)
if e:getFlags()[flagName] then dirty = true end
return false
end)
local flagW = hs.eventtap.new({ hs.eventtap.event.types.flagsChanged }, function(e)
if not keycodes[e:getKeyCode()] then return false end
local f = e:getFlags()
if f[flagName] then -- modifier went DOWN: start a fresh, clean tap
dirty = false
return false
end
-- modifier went UP
if dirty then return false end
if f.cmd or f.ctrl or f.alt or f.shift or f.fn then return false end -- must be a lone tap
local now = hs.timer.secondsSinceEpoch()
if (now - lastUp) < DOUBLE_TAP_GAP then
lastUp = 0
onTrigger()
else
lastUp = now
end
return false
end)
keyW:start()
flagW:start()
table.insert(watchers, keyW)
table.insert(watchers, flagW)
end
-- Double-tap CONTROL → follow the message's language (Chinese msg → Chinese reply)
registerDoubleTap({ [59] = true, [62] = true }, "ctrl", function()
generateReply("auto")
end)
-- Double-tap OPTION (⌥) → always Chinese
registerDoubleTap({ [58] = true, [61] = true }, "alt", function()
generateReply("zh")
end)
hs.alert.show("💋 已加载:双击 Control = 跟随语言,双击 Option = 中文", 2.5)