-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathinit.lua
More file actions
491 lines (452 loc) · 21.4 KB
/
Copy pathinit.lua
File metadata and controls
491 lines (452 loc) · 21.4 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
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
--[[ Jason's hammerspoon hotkeys
change log:
2023-03-20 - initial creation with R=reload, W=notify, Q=alert, C=clock
2023-03-22 - added T=Teams linkinator
2023-03-24 - added HammerText, including ddd, dd, hhh, hh, ddhh, awsus, awsca, awsdk
changed output and hotkey for sleep-checker from hyper-Y to hyper-S
hyper-V brings up clipboard manager
2023-03-25 - split out worktools.lua, much cleanup
2023-05-16 - add variable "my_email" here; currently used in worktools.lua
2023-07-06 - add "Jason Schechner Y3WLD0C6NF" as alternate hostname for work laptop for loading worktools
2024-01-18 - add defeat paste blocking = opt-cmd-v
2024-01-18 - add spoon MicMute
2024-01-24 - meta-F shows relative and absolute mouse positions
2024-02-06 - add "hs.application.enableSpotlightForNameSearches(true)"; add front-most window to hyper-F
2024-02-13 - add URLs to play/pause Music
2024-02-28 - add bundleID to hyper-F; improve mouse cooridinate output in hyper-F
2024-03-06 - hyper-F adds vars for relative and absolute mouse positions - useful in console; save hotkey object when binding
much cleanup; merged Vader's bespoke init.lua by:
mic-mute, clipboard manager to worktools; music URLs to hometools; vader loads music-webserver
2024-03-18 - add mouseHighlight as hyper-shift-M; minor basement cleanup
2024-03-26 - hyper-F: add PIDs, roles, IDs
2024-06-03 - add m3po
2024-06-03 - move spoon ClipboardTool here from worktools.lua, excluding vader; use variable "hyper" where it belongs
2024-07-16 - reduce clipboard history to 50 items, 512 bytes
2024-07-19 - clipboard history back to 100/1k
2024-08-06 - variablize clipboardtool_hist_size, clipboardtool_max_entry_size
2024-08-28 - add shift-hyper-W - show window findenheimer; add pop-lips-to-findenheimer but too many false positives. disabled.
2024-09-10 - drop clipboardtool_hist_size fromn 100 to 25
2024-09-12 - add subrole to HyperF
2025-08-04 - add Traversal laptop ("Jason’s MacBook Pro") so it loads worktools
2025-09-03 - remove more tdh and move my_work_email to worktools
2026-01-12 - add hyper-P for clipboard-to-preview
2026-04-01 - disabled hyper-P. Hotkey added to shortcut directly.
2026-07-06 - require hs.ipc and install `hs` CLI in ~/bin;
- Properly add comments to Install:andUse (use message = "comment"), reload, hyper-v, cheetsheat
- Much cleaner hyper-F with nil-safe variables
2026-07-08 - AClock from Hyper-C to Hyper-T (for time) ; add hyper-C for clipboard reveal
- stop updating the repo then installing keychain and recursivebinder from git every time
2026-08-07 - w/claude - shift-hyperV clears clipboard entries larger than clipboard_purge_larger_than_kb; persist clipboard deletions across restarts
--]]
----------------------------------------------------------------------------------------------
-- some variables
hyper = {"cmd","alt","ctrl"}
shift_hyper = {"cmd","alt","ctrl","shift"}
ctrl_cmd = {"cmd","ctrl"}
my_email = "hammerspoonie@jasons.us"
-- work_logo = hs.image.imageFromPath(hs.configdir .. "/files/work_logo_2x.png")
clipboardtool_hist_size = 50
clipboardtool_max_entry_size = 1024
clipboard_purge_larger_than_kb = 200
----------------------------------------------------------------------------------------------
-- 2026-07-06 - enable IPC and hs CLI - also helps Claude)
require("hs.ipc")
if not hs.ipc.cliStatus(os.getenv("HOME"), true) then
os.execute('mkdir -p "$HOME/share/man/man1"')
hs.ipc.cliInstall(os.getenv("HOME"), true)
end
-- load some spoons
hs.loadSpoon("SpoonInstall")
Install=spoon.SpoonInstall
-- # Disabled 2026-07-08 because overkill - run this in the console if you want to update
-- # update the default spoons repo (hammerspoon.org/Spoons)
-- Install:updateRepo('default')
-- # aka: spoon.SpoonInstall:updateRepo('default')
hs.loadSpoon("FadeLogo"):start()
-- hs.loadSpoon("ReloadConfiguration"):start()
Install:andUse("ReloadConfiguration", { hotkeys = { reloadConfiguration = { hyper, "R", message = "Reload" } }})
----------------------------------------------------------------------------------------------
-- determine my hostname, so I know what to load
myhostname = hs.host.localizedName()
if myhostname == "Jason’s MacBook Pro" or myhostname == "JSON-M4" then
local workstuff = require('worktools')
elseif myhostname == "vader" or myhostname == "m3po" then
local homestuff = require('hometools')
local homestuff = require('music-webserver')
else
hs.alert("I don't recognize " .. myhostname .. " so not loading work or home tools.",4)
end
----------------------------------------------------------------------------------------------
-- clipboard manager
-- vader has a problem with the clipboard manager.... so
if myhostname ~= "vader" then
Install:andUse("ClipboardTool", {
config = { menubar_title = "\u{1f4ce}", hist_size = clipboardtool_hist_size, max_entry_size=clipboardtool_max_entry_size },
hotkeys = { show_clipboard = { hyper, "V", message = "Clipboard Tool" }}
})
spoon.ClipboardTool:start()
-- Fix: manageClip (right-click delete/move/reorder) never persisted its changes
local originalManageClip = spoon.ClipboardTool.manageClip
function spoon.ClipboardTool:manageClip(row, action)
originalManageClip(self, row, action)
_persistHistory()
end
-- Remove clipboard history entries larger than the given size (KB)
function spoon.ClipboardTool:purgeLargerThan(kb)
local maxBytes = kb * 1024
local rows = self:_populateChooser("")
local removed = 0
for i = #rows, 1, -1 do
local row = rows[i]
if (row.type == "text" or row.type == "image") and row.data and #row.data > maxBytes then
self:manageClip(i, 0)
removed = removed + 1
end
end
if self.selectorobj then self.selectorobj:refreshChoicesCallback() end
return removed
end
hotkey_ShiftHyperV = hs.hotkey.bind(shift_hyper, "V", "Purge large clipboard items", function()
local removed = spoon.ClipboardTool:purgeLargerThan(clipboard_purge_larger_than_kb)
hs.alert.show("Purged " .. removed .. " clipboard item(s) over " .. clipboard_purge_larger_than_kb .. "KB")
end)
end
Install:andUse("KSheet", { hotkeys = { toggle = { hyper, "/", message = "Cheat sheet" } } })
----------------------------------------------------------------------------------------------
-- pull items from keychain
-- # disabled 2026-07-08 because overkill
-- spoon.SpoonInstall:installSpoonFromRepo("Keychain")
-- hs.spoons.use("Keychain")
Install:andUse("Keychain")
----- recursive binder - this is going to take more time to grok, so start with a datenheimer
-- https://nethuml.github.io/posts/2022/04/hammerspoon-global-leader-key/
Install:andUse("RecursiveBinder")
local recursives = require("recursives")
--[[
spoon.RecursiveBinder.helperFormat = {
atScreenEdge = 2, -- Bottom edge (default value)
textStyle = { -- An hs.styledtext object
font = {
name = "Fira Code",
size = 18
}
}
}
--]]
hs.loadSpoon("AClock")
spoon.AClock:init() hs.hotkey.bind(hyper, "T", "A Clock", function()
spoon.AClock:toggleShow()
end)
----------------------------------------------------------------------------------------------
-- general purpose stuff --------------------------------------------------------------------
-- enable Spotlight support
hs.application.enableSpotlightForNameSearches(true)
-- show registered hotkeys
hotkey_HyperH = hs.hotkey.showHotkeys(hyper, "H")
-- show the name of the front-most application. Good for troubleshooting.
hotkey_HyperF = hs.hotkey.bind(hyper, "F", "front-most app+window; focused window", function()
-- These calls can be made in alert.show, but for troubleshooting you can ref them in the console
local fa = hs.application.frontmostApplication()
local fw = hs.window.frontmostWindow()
local focw = hs.window.focusedWindow()
local rm = hs.mouse.getRelativePosition()
local am = hs.mouse.absolutePosition()
local lines = {}
if fa then
lines[#lines+1] = string.format(
"- frontmost_app bundleID: %s\n- frontmost_app title: %s\n- frontmost_app pid, role: %s, %s",
fa:bundleID(), fa:title(), fa:pid(), fa:role())
else
lines[#lines+1] = "- frontmost_app: (none)"
end
if fw then
lines[#lines+1] = string.format(
"- frontmost_win title: %s\n- frontmost_win pid, id, role, subrole: %s, %s, %s, %s",
fw:title(), fw:pid(), fw:id(), fw:role(), fw:subrole())
else
lines[#lines+1] = "- frontmost_win: (none)"
end
if focw then
lines[#lines+1] = "- focused_win title: " .. focw:title()
else
lines[#lines+1] = "- focused_win: (none)"
end
lines[#lines+1] = string.format(
"- Mouse relative: x = %s, y = %s // absolute: x = %s, y = %s", rm.x, rm.y, am.x, am.y)
local msg = table.concat(lines, "\n")
print(msg)
hs.alert.show(msg, 8)
end)
-- Create menubar item to toggle disabling of sleep, create URLs to call from scripts
caffeine = hs.menubar.new()
function setCaffeineDisplay(state)
if state then
caffeine:setTitle("BUZZ")
else
caffeine:setTitle("zzz.")
end
end
function caffeineClicked()
setCaffeineDisplay(hs.caffeinate.toggle("displayIdle"))
end
if caffeine then
caffeine:setClickCallback(caffeineClicked)
setCaffeineDisplay(hs.caffeinate.get("displayIdle"))
end
-- bind URLs "hammerspoon://{stayup,chill}"
hs.urlevent.bind("stayup",function(eventName, params)
hs.caffeinate.set("displayIdle",true)
setCaffeineDisplay(hs.caffeinate.get("displayIdle"))
end)
hs.urlevent.bind("chill",function(eventName, params)
hs.caffeinate.set("displayIdle",false)
setCaffeineDisplay(hs.caffeinate.get("displayIdle"))
end)
-- HK to check whether sleep is enabled or not
hotkey_HyperS = hs.hotkey.bind(hyper, "S", "Is sleep disabled?", function()
if hs.caffeinate.get("displayIdle") then
hs.alert.show("DisplayIdle disabled. (No sleep till Brooklyn.)")
else
hs.alert.show("DisplayIdle enabled. (When is naptime?)")
end
end)
--[[ Added 2026-01-12; disabled 2026-04-01 - added hotkey directly to the shortcut
hotkey_HyperP = hs.hotkey.bind(hyper, "P", "Clipboard-to-Preview", function()
diditrun, clipboard_has_image = hs.osascript.applescriptFromFile("clipboard-to-preview.applescript")
print(clipboard_has_image)
if clipboard_has_image == "No image found in the clipboard." then
hs.alert.show(clipboard_has_image)
end
end)
--]]
-- work-in-progress to get a list of windows for finding focus and such; useful? dunno
--[[
hs.hotkey.bind({"cmd", "alt", "ctrl"}, "W", function()
--window_list = hs.window.list(allWindows)
window_list = hs.window.allWindows()
hs.alert.show(window_list)
end)
--]]
----------------------------------------------------------------------------------------------
-- defeat paste blocking - https://www.hammerspoon.org/go/#pasteblock - 2024-01-17
hotkey_CmdAltV = hs.hotkey.bind({"cmd", "alt"}, "V", "defeat paste block", function() hs.eventtap.keyStrokes(hs.pasteboard.getContents()) end)
----------------------------------------------------------------------------------------------
-- mouse finder - https://www.hammerspoon.org/go/#pasteblock - 2024-03-18
mouseCircle = nil
mouseCircleTimer = nil
function mouseHighlight()
-- Delete an existing highlight if it exists
if mouseCircle then
mouseCircle:delete()
if mouseCircleTimer then
mouseCircleTimer:stop()
end
end
-- Get the current co-ordinates of the mouse pointer
mousepoint = hs.mouse.absolutePosition()
-- Prepare a big red circle around the mouse pointer
mouseCircle = hs.drawing.circle(hs.geometry.rect(mousepoint.x-40, mousepoint.y-40, 80, 80))
mouseCircle:setStrokeColor({["red"]=1,["blue"]=0,["green"]=0,["alpha"]=1})
mouseCircle:setFill(false)
mouseCircle:setStrokeWidth(5)
mouseCircle:show()
-- Set a timer to delete the circle after 3 seconds
mouseCircleTimer = hs.timer.doAfter(3, function()
mouseCircle:delete()
mouseCircle = nil
end)
end
hotkey_ShiftHyperM = hs.hotkey.bind(shift_hyper, "M", "MouseFinder", mouseHighlight)
hs.hints.showTitleThresh=50
hotkey_ShiftHyperW = hs.hotkey.bind(shift_hyper, "W", "WindowFindenheimer", function() hs.hints.windowHints() end)
-- Show full clipboard contents in a pop-up
-- handles for the hyper-C reveal, so a re-press (or esc) can tear the old one down
clipboardCanvas = nil -- the whole reveal (text box + optional image box)
clipboardEscKey = nil -- temporary esc-to-dismiss hotkey
clipboardTimer = nil -- auto-dismiss timer
hotkey_HyperC = hs.hotkey.bind(hyper, "C", "Clipboard reveal-all", function()
local function wrapText(s, width)
local out = {}
for line in (s .. "\n"):gmatch("(.-)\n") do
local len = utf8.len(line)
if not len then
-- invalid UTF-8 in this line: fall back to byte wrapping so we don't error
repeat
out[#out+1] = line:sub(1, width)
line = line:sub(width + 1)
until #line == 0
elseif len <= width then
out[#out+1] = line -- short enough, keep as-is
else
local cp = 1 -- current start, in CODEPOINTS
while cp <= len do
local from = utf8.offset(line, cp) -- codepoint index → byte index
local to = (cp + width <= len)
and (utf8.offset(line, cp + width) - 1)
or #line
out[#out+1] = line:sub(from, to) -- :sub still takes BYTES
cp = cp + width
end
end
end
return table.concat(out, "\n")
end
-- tear down any reveal still on screen (prevents stacked alerts/previews) and the esc key
local function dismissReveal()
if clipboardTimer then clipboardTimer:stop(); clipboardTimer = nil end
if clipboardCanvas then clipboardCanvas:delete(); clipboardCanvas = nil end
if clipboardEscKey then clipboardEscKey:delete(); clipboardEscKey = nil end
end
dismissReveal()
-- classify each flavor: text gets wrapped/truncated; binary (images, etc.) is summarized
local out = {}
for uti, val in pairs(hs.pasteboard.readAllData()) do
local n = #val
if not utf8.len(val) then -- not valid UTF-8 → binary; don't dump bytes
out[#out+1] = string.format("%s → <binary, %d bytes>", uti, n)
elseif n > 800 then -- long text → truncate, then wrap
out[#out+1] = uti .. ":\n" .. wrapText(val:sub(1, 800), 100) .. string.format(" …(%d bytes)", n)
else
out[#out+1] = uti .. ":\n" .. wrapText(val, 100)
end
end
-- image (if any): size its preview now; it becomes a second box overlapping the text box
local img = hs.pasteboard.readImage()
local imgW, imgH
if img then
local isz = img:size()
out[#out+1] = string.format("(image: %.0f x %.0f)", isz.w, isz.h)
local maxDim = 420
local scale = math.min(maxDim / isz.w, maxDim / isz.h, 1) -- shrink to fit, never upscale
imgW, imgH = math.floor(isz.w * scale), math.floor(isz.h * scale)
end
local msg = "Clipboard flavors:\n" .. table.concat(out, "\n")
print(msg)
-- ---- one canvas we fully own: a text box + (optionally) an overlapping, right-shifted image box ----
local textSize = 28
local tpad, ipad = 16, 12 -- inner padding of the text box / image box
local overlap = 10 -- how far the image box rides up over the text box (shows they're coupled)
local rightShift = 90 -- nudge the image box to the right ("feels right")
local tf = hs.drawing.getTextDrawingSize(msg, { font = ".AppleSystemUIFont", size = textSize })
local textW, textH = math.ceil(tf.w) + 2, math.ceil(tf.h)
local tw, th = textW + tpad * 2, textH + tpad * 2 -- text box size
-- lay the boxes out in a local space (text box at origin), then normalize so nothing is negative
local ibx, iby, ibw, ibh = 0, 0, 0, 0
if img then
ibw, ibh = imgW + ipad * 2, imgH + ipad * 2
ibx = (tw - ibw) / 2 + rightShift -- centered under the text box, then shoved right
iby = th - overlap -- top rides up into the text box
end
local margin = 14 -- breathing room so the image-box shadow isn't clipped
local minX = math.min(0, ibx)
local W = math.max(tw, ibx + ibw) - minX + margin * 2
local H = math.max(th, iby + ibh) + margin * 2
local tx = -minX + margin -- text box local x (shifted in by the margin)
local ty = margin -- text box local y
local ix = ibx - minX + margin -- image box local x
local iy = iby + margin -- image box local y
local ff = hs.screen.mainScreen():fullFrame()
clipboardCanvas = hs.canvas.new({
x = math.floor(ff.x + (ff.w - W) / 2),
y = math.floor(ff.y + (ff.h - H) / 2),
w = W, h = H,
})
clipboardCanvas:appendElements(
{ type = "rectangle", action = "strokeAndFill", -- text box (the old alert look)
fillColor = { white = 0, alpha = 0.8 }, strokeColor = { white = 1 }, strokeWidth = 2,
roundedRectRadii = { xRadius = 12, yRadius = 12 },
frame = { x = tx, y = ty, w = tw, h = th } },
{ type = "text", text = msg,
textColor = { white = 1 }, textFont = ".AppleSystemUIFont", textSize = textSize,
frame = { x = tx + tpad, y = ty + tpad, w = textW, h = textH } })
if img then
clipboardCanvas:appendElements( -- drawn last → visibly overlaps the text box
{ type = "rectangle", action = "strokeAndFill",
fillColor = { white = 0, alpha = 0.85 }, strokeColor = { white = 1 }, strokeWidth = 2,
roundedRectRadii = { xRadius = 12, yRadius = 12 },
withShadow = true,
shadow = { blurRadius = 8, color = { alpha = 0.5 }, offset = { h = -4, w = 4 } },
frame = { x = ix, y = iy, w = ibw, h = ibh } },
{ type = "image", image = img, imageScaling = "scaleToFit",
frame = { x = ix + ipad, y = iy + ipad, w = imgW, h = imgH } })
end
clipboardCanvas:level(hs.canvas.windowLevels.overlay)
clipboardCanvas:show()
clipboardEscKey = hs.hotkey.bind({}, "escape", dismissReveal) -- esc dismisses the whole reveal early
clipboardTimer = hs.timer.doAfter(8, dismissReveal) -- otherwise auto-dismiss after 8s
end)
-- testCallbackFn = function(result) print("Callback Result: " .. result) end
-- or: function testCallbackFn(result) print("Callback Result: " .. result) end
-- 2024-08-28
-- silly setup to show windowHints when you pop your lips
function handle_noises(incoming)
print("handle_noises just got a " .. incoming)
if incoming == 3 then
hs.hints.windowHints()
end
incoming=0
end
popWindows=hs.noises.new(handle_noises)
-- popWindows:start()
--------------------
------------------------------------------------------------------------------------------------------------------------
--[[ basement: storage and references (and maybe hidden treasures)
---------- hello world --------------------
hs.hotkey.bind({"cmd", "alt", "ctrl"}, "Y", function()
_, reply = hs.dialog.textPrompt("Main message.", "Please enter something:")
hs.alert.show("you said "..reply)
end)
---- post message to notification center ----------------------------------------------------
hs.hotkey.bind({"cmd", "alt", "ctrl"}, "W", function()
hs.notify.new({title="Hammerspoon", informativeText="Hello World"}):send()
end)
----------------------------------------------------------------------------------------------
-- supplanted by ctrl_cmd-R in ReloadConfiguration spoon
-- reload the config - simple reload
hs.hotkey.bind({"cmd", "alt", "ctrl"}, "R", function()
hs.reload()
end)
-- hs.alert.show("Config loaded")
----------------------------------------------------------------------------------------------
-- test pop-up
hs.hotkey.bind({"cmd", "alt", "ctrl"}, "Y", function()
_, reply = hs.dialog.textPrompt("Main message.", "Please enter something:")
hs.alert.show("you said ".. reply)
end)
----------------------------------------------------------------------------------------------
-- hs.dialog.textPrompt("Main message.", "Please enter something:", "Default Value", "OK")
-- hs.dialog.textPrompt("Main message.", "Please enter something:", "Default Value", "OK", "Cancel")
-- hs.dialog.textPrompt("Main message.", "Please enter something:", "", "OK", "Cancel", true)
----------------------------------------------------------------------------------------------
hs.hotkey.bind({"cmd", "alt", "ctrl"}, "H", "move focused window a bit", function()
local win = hs.window.focusedWindow()
local f = win:frame()
f.x = f.x - 10
win:setFrame(f)
end)
----------------------------------------------------------------------------------------------
--[[ disabled in lieu of RecursiveBinder
ht=hs.loadSpoon("HammerText")
ht.keywords={
[ "ddd." ] = function() return os.date("%Y-%m-%d") end,
[ "dd." ] = function() return os.date("%Y%m%d") end,
[ "hhh." ] = function() return os.date("%H:%M") end,
[ "hh." ] = function() return os.date("%H%M") end,
[ "dh." ] = function() return os.date("%Y%m%d.%H%M") end,
[ "ddhh." ] = function() return os.date("%Y-%m-%d.%H%M") end,
}
ht:start()
--]]
----------------------------------------------------------------------------------------------
--[[ disabled 2024-03-06 - RecursiveBinder is better (I forgot this was even here.)
hotkey_CmdAltD = hs.hotkey.bind({"cmd","alt"}, "D", "Datenheimer", function()
focused_window = hs.window.focusedWindow()
diditrun, output, codestring = hs.osascript.applescriptFromFile("./dateomatic.applescript")
focused_window:focus()
if diditrun then
hs.eventtap.keyStrokes(output)
else
hs.alert.show("Say what?")
end
end)
--]]