claude-dock 1.0.2 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/cli.js +5 -0
- package/init.lua +457 -26
- package/package.json +1 -1
package/bin/cli.js
CHANGED
|
@@ -159,6 +159,11 @@ async function main() {
|
|
|
159
159
|
log(` ${colors.dim}⌘⌥T${colors.reset} Toggle dock`);
|
|
160
160
|
log(` ${colors.dim}⌘⌥N${colors.reset} Add new terminal`);
|
|
161
161
|
log(` ${colors.dim}⌘⌥R${colors.reset} Reload config`);
|
|
162
|
+
log(` ${colors.dim}⌘⌥L${colors.reset} Move macOS Dock to left`);
|
|
163
|
+
log(` ${colors.dim}⌘⌥B${colors.reset} Move macOS Dock to bottom`);
|
|
164
|
+
log('');
|
|
165
|
+
log(`${colors.yellow}Tip:${colors.reset} If your macOS Dock is at the bottom, you'll be prompted to move it.`);
|
|
166
|
+
log(` You can change this anytime with ${colors.dim}⌘⌥L${colors.reset} (left) or ${colors.dim}⌘⌥B${colors.reset} (bottom).`);
|
|
162
167
|
log('');
|
|
163
168
|
}
|
|
164
169
|
|
package/init.lua
CHANGED
|
@@ -13,8 +13,8 @@ local config = {
|
|
|
13
13
|
addButtonWidth = 40,
|
|
14
14
|
utilityButtonWidth = 28,
|
|
15
15
|
initialSlots = 3,
|
|
16
|
-
elementsPerSlot =
|
|
17
|
-
baseElements =
|
|
16
|
+
elementsPerSlot = 5, -- bg, border, title, status, notification badge
|
|
17
|
+
baseElements = 6, -- dock bg + border + help btn + help text + minimize btn + minimize text
|
|
18
18
|
windowCaptureDelay = 0.3,
|
|
19
19
|
windowCaptureRetries = 5,
|
|
20
20
|
colors = {
|
|
@@ -30,7 +30,11 @@ local config = {
|
|
|
30
30
|
addBtnText = { red = 0.6, green = 0.8, blue = 0.6, alpha = 1 },
|
|
31
31
|
minBtnBg = { red = 0.18, green = 0.18, blue = 0.25, alpha = 1 },
|
|
32
32
|
minBtnText = { red = 0.6, green = 0.6, blue = 0.9, alpha = 1 },
|
|
33
|
-
|
|
33
|
+
helpBtnBg = { red = 0.2, green = 0.18, blue = 0.12, alpha = 1 },
|
|
34
|
+
helpBtnText = { red = 0.9, green = 0.8, blue = 0.5, alpha = 1 },
|
|
35
|
+
notificationBadge = { red = 1, green = 0.3, blue = 0.3, alpha = 1 },
|
|
36
|
+
},
|
|
37
|
+
notificationBadgeSize = 12,
|
|
34
38
|
}
|
|
35
39
|
|
|
36
40
|
-- State
|
|
@@ -38,6 +42,28 @@ local slotCount = config.initialSlots
|
|
|
38
42
|
local slots = {}
|
|
39
43
|
local dock = nil
|
|
40
44
|
local tooltip = nil
|
|
45
|
+
local helpPanel = nil
|
|
46
|
+
local macOSDockAtBottom = false
|
|
47
|
+
|
|
48
|
+
-- Check macOS dock position
|
|
49
|
+
local function getMacOSDockPosition()
|
|
50
|
+
local output, status = hs.execute("defaults read com.apple.dock orientation 2>/dev/null")
|
|
51
|
+
if status and output then
|
|
52
|
+
output = output:gsub("%s+", "")
|
|
53
|
+
if output == "left" or output == "right" then
|
|
54
|
+
return output
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
return "bottom" -- default
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
-- Get macOS dock size (tile size + magnification consideration)
|
|
61
|
+
local function getMacOSDockHeight()
|
|
62
|
+
local tileSize = hs.execute("defaults read com.apple.dock tilesize 2>/dev/null")
|
|
63
|
+
tileSize = tonumber(tileSize) or 48
|
|
64
|
+
-- Add padding for dock chrome and gaps
|
|
65
|
+
return tileSize + 20
|
|
66
|
+
end
|
|
41
67
|
|
|
42
68
|
-- Resource handles (for cleanup on reload)
|
|
43
69
|
local windowFilter = nil
|
|
@@ -66,13 +92,17 @@ local function cleanup()
|
|
|
66
92
|
tooltip:delete()
|
|
67
93
|
tooltip = nil
|
|
68
94
|
end
|
|
95
|
+
if helpPanel then
|
|
96
|
+
helpPanel:delete()
|
|
97
|
+
helpPanel = nil
|
|
98
|
+
end
|
|
69
99
|
end
|
|
70
100
|
cleanup() -- Clean up any previous instance
|
|
71
101
|
|
|
72
102
|
local function initSlots()
|
|
73
103
|
for i = 1, slotCount do
|
|
74
104
|
if not slots[i] then
|
|
75
|
-
slots[i] = { windowId = nil, customName = nil, pending = false }
|
|
105
|
+
slots[i] = { windowId = nil, customName = nil, pending = false, hasNotification = false }
|
|
76
106
|
end
|
|
77
107
|
end
|
|
78
108
|
end
|
|
@@ -96,9 +126,14 @@ local function getDockFrame()
|
|
|
96
126
|
local frame = screen:fullFrame()
|
|
97
127
|
local dockWidth = getDockWidth()
|
|
98
128
|
local dockHeight = getDockHeight()
|
|
129
|
+
local bottomOffset = config.bottomOffset
|
|
130
|
+
-- Add extra offset if macOS dock is at bottom
|
|
131
|
+
if macOSDockAtBottom then
|
|
132
|
+
bottomOffset = bottomOffset + getMacOSDockHeight()
|
|
133
|
+
end
|
|
99
134
|
return {
|
|
100
135
|
x = (frame.w - dockWidth) / 2,
|
|
101
|
-
y = frame.h - dockHeight -
|
|
136
|
+
y = frame.h - dockHeight - bottomOffset,
|
|
102
137
|
w = dockWidth,
|
|
103
138
|
h = dockHeight
|
|
104
139
|
}
|
|
@@ -119,6 +154,33 @@ local function getWindowTitle(win)
|
|
|
119
154
|
return title
|
|
120
155
|
end
|
|
121
156
|
|
|
157
|
+
-- Find slot index by window ID
|
|
158
|
+
local function findSlotByWindowId(windowId)
|
|
159
|
+
if not windowId then return nil end
|
|
160
|
+
for i, slot in ipairs(slots) do
|
|
161
|
+
if slot.windowId == windowId then
|
|
162
|
+
return i
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
return nil
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
-- Set notification for a slot (only if window is not focused)
|
|
169
|
+
local function setSlotNotification(slotIndex)
|
|
170
|
+
local slot = slots[slotIndex]
|
|
171
|
+
if not slot then return end
|
|
172
|
+
|
|
173
|
+
local win = getWindow(slot.windowId)
|
|
174
|
+
if win then
|
|
175
|
+
local focusedWin = hs.window.focusedWindow()
|
|
176
|
+
-- Only show notification if window is not currently focused
|
|
177
|
+
if not focusedWin or focusedWin:id() ~= slot.windowId then
|
|
178
|
+
slot.hasNotification = true
|
|
179
|
+
updateSlotDisplay(slotIndex)
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
|
|
122
184
|
-- Tooltip helpers
|
|
123
185
|
local function showTooltipAt(text, x, y)
|
|
124
186
|
if tooltip then tooltip:delete() end
|
|
@@ -155,11 +217,11 @@ local function showButtonTooltip(text, buttonId)
|
|
|
155
217
|
local tipHeight = 24
|
|
156
218
|
local btnX, btnWidth
|
|
157
219
|
|
|
158
|
-
if buttonId == "minBtn" then
|
|
159
|
-
-- Left side utility
|
|
220
|
+
if buttonId == "minBtn" or buttonId == "helpBtn" then
|
|
221
|
+
-- Left side utility buttons
|
|
160
222
|
btnX = dockFrame.x + config.margin
|
|
161
223
|
btnWidth = config.utilityButtonWidth
|
|
162
|
-
|
|
224
|
+
elseif buttonId == "addBtn" then
|
|
163
225
|
-- Right side add button
|
|
164
226
|
btnX = dockFrame.x + config.margin + config.utilityButtonWidth + config.gap + (slotCount * config.slotWidth) + (slotCount * config.gap)
|
|
165
227
|
btnWidth = config.addButtonWidth
|
|
@@ -178,6 +240,128 @@ local function hideTooltip()
|
|
|
178
240
|
end
|
|
179
241
|
end
|
|
180
242
|
|
|
243
|
+
-- Help panel
|
|
244
|
+
local function hideHelpPanel()
|
|
245
|
+
if helpPanel then
|
|
246
|
+
helpPanel:delete()
|
|
247
|
+
helpPanel = nil
|
|
248
|
+
end
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
local function showHelpPanel()
|
|
252
|
+
if helpPanel then
|
|
253
|
+
hideHelpPanel()
|
|
254
|
+
return
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
local screen = hs.screen.mainScreen()
|
|
258
|
+
if not screen then return end
|
|
259
|
+
local screenFrame = screen:fullFrame()
|
|
260
|
+
|
|
261
|
+
local panelWidth = 280
|
|
262
|
+
local panelHeight = 260
|
|
263
|
+
local panelX = (screenFrame.w - panelWidth) / 2
|
|
264
|
+
local panelY = (screenFrame.h - panelHeight) / 2
|
|
265
|
+
|
|
266
|
+
helpPanel = hs.canvas.new({ x = panelX, y = panelY, w = panelWidth, h = panelHeight })
|
|
267
|
+
|
|
268
|
+
-- Background
|
|
269
|
+
helpPanel:appendElements({
|
|
270
|
+
type = "rectangle",
|
|
271
|
+
action = "fill",
|
|
272
|
+
roundedRectRadii = { xRadius = 12, yRadius = 12 },
|
|
273
|
+
fillColor = { red = 0.1, green = 0.1, blue = 0.1, alpha = 0.95 },
|
|
274
|
+
frame = { x = 0, y = 0, w = "100%", h = "100%" },
|
|
275
|
+
})
|
|
276
|
+
|
|
277
|
+
-- Border
|
|
278
|
+
helpPanel:appendElements({
|
|
279
|
+
type = "rectangle",
|
|
280
|
+
action = "stroke",
|
|
281
|
+
roundedRectRadii = { xRadius = 12, yRadius = 12 },
|
|
282
|
+
strokeColor = { red = 1, green = 1, blue = 1, alpha = 0.2 },
|
|
283
|
+
strokeWidth = 1,
|
|
284
|
+
frame = { x = 0, y = 0, w = "100%", h = "100%" },
|
|
285
|
+
})
|
|
286
|
+
|
|
287
|
+
-- Title
|
|
288
|
+
helpPanel:appendElements({
|
|
289
|
+
type = "text",
|
|
290
|
+
frame = { x = 0, y = 15, w = panelWidth, h = 24 },
|
|
291
|
+
text = "Claude Dock Shortcuts",
|
|
292
|
+
textAlignment = "center",
|
|
293
|
+
textColor = { red = 1, green = 1, blue = 1, alpha = 1 },
|
|
294
|
+
textSize = 16,
|
|
295
|
+
textFont = ".AppleSystemUIFontBold",
|
|
296
|
+
})
|
|
297
|
+
|
|
298
|
+
-- Shortcuts list
|
|
299
|
+
local shortcuts = {
|
|
300
|
+
{ key = "⌘⌥T", desc = "Toggle dock" },
|
|
301
|
+
{ key = "⌘⌥N", desc = "Add new terminal" },
|
|
302
|
+
{ key = "⌘⌥M", desc = "Minimize all terminals" },
|
|
303
|
+
{ key = "⌘⌥R", desc = "Reload config" },
|
|
304
|
+
{ key = "⌘⌥L", desc = "Move macOS Dock left" },
|
|
305
|
+
{ key = "⌘⌥B", desc = "Move macOS Dock bottom" },
|
|
306
|
+
{ key = "⌥+Click", desc = "Rename slot" },
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
local startY = 50
|
|
310
|
+
for i, shortcut in ipairs(shortcuts) do
|
|
311
|
+
local y = startY + ((i - 1) * 24)
|
|
312
|
+
helpPanel:appendElements({
|
|
313
|
+
type = "text",
|
|
314
|
+
frame = { x = 20, y = y, w = 70, h = 20 },
|
|
315
|
+
text = shortcut.key,
|
|
316
|
+
textAlignment = "left",
|
|
317
|
+
textColor = { red = 0.6, green = 0.8, blue = 1, alpha = 1 },
|
|
318
|
+
textSize = 13,
|
|
319
|
+
textFont = ".AppleSystemUIFont",
|
|
320
|
+
})
|
|
321
|
+
helpPanel:appendElements({
|
|
322
|
+
type = "text",
|
|
323
|
+
frame = { x = 95, y = y, w = 170, h = 20 },
|
|
324
|
+
text = shortcut.desc,
|
|
325
|
+
textAlignment = "left",
|
|
326
|
+
textColor = { red = 0.8, green = 0.8, blue = 0.8, alpha = 1 },
|
|
327
|
+
textSize = 13,
|
|
328
|
+
textFont = ".AppleSystemUIFont",
|
|
329
|
+
})
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
-- Close button
|
|
333
|
+
local closeBtnY = panelHeight - 45
|
|
334
|
+
helpPanel:appendElements({
|
|
335
|
+
type = "rectangle",
|
|
336
|
+
action = "fill",
|
|
337
|
+
frame = { x = (panelWidth - 80) / 2, y = closeBtnY, w = 80, h = 30 },
|
|
338
|
+
roundedRectRadii = { xRadius = 6, yRadius = 6 },
|
|
339
|
+
fillColor = { red = 0.25, green = 0.25, blue = 0.25, alpha = 1 },
|
|
340
|
+
trackMouseUp = true,
|
|
341
|
+
id = "closeBtn",
|
|
342
|
+
})
|
|
343
|
+
helpPanel:appendElements({
|
|
344
|
+
type = "text",
|
|
345
|
+
frame = { x = (panelWidth - 80) / 2, y = closeBtnY + 6, w = 80, h = 20 },
|
|
346
|
+
text = "Close",
|
|
347
|
+
textAlignment = "center",
|
|
348
|
+
textColor = { red = 0.9, green = 0.9, blue = 0.9, alpha = 1 },
|
|
349
|
+
textSize = 13,
|
|
350
|
+
textFont = ".AppleSystemUIFont",
|
|
351
|
+
trackMouseUp = true,
|
|
352
|
+
id = "closeBtn",
|
|
353
|
+
})
|
|
354
|
+
|
|
355
|
+
helpPanel:mouseCallback(function(_, event, id)
|
|
356
|
+
if event == "mouseUp" and id == "closeBtn" then
|
|
357
|
+
hideHelpPanel()
|
|
358
|
+
end
|
|
359
|
+
end)
|
|
360
|
+
|
|
361
|
+
helpPanel:level(hs.canvas.windowLevels.modalPanel)
|
|
362
|
+
helpPanel:show()
|
|
363
|
+
end
|
|
364
|
+
|
|
181
365
|
-- Forward declarations
|
|
182
366
|
local createDock
|
|
183
367
|
local updateAllSlots
|
|
@@ -203,16 +387,19 @@ local function updateSlotDisplay(slotIndex)
|
|
|
203
387
|
status = "active"
|
|
204
388
|
bgColor = config.colors.slotActive
|
|
205
389
|
end
|
|
390
|
+
elseif slot.windowId then
|
|
391
|
+
-- Window exists but not visible (probably on another space)
|
|
392
|
+
title = slot.customName or "Terminal"
|
|
393
|
+
status = "(other space)"
|
|
394
|
+
bgColor = config.colors.slotMinimized
|
|
206
395
|
else
|
|
207
|
-
--
|
|
208
|
-
if
|
|
209
|
-
slot.windowId = nil
|
|
210
|
-
slot.customName = nil
|
|
211
|
-
title = "Empty"
|
|
212
|
-
status = "click to open"
|
|
213
|
-
else
|
|
396
|
+
-- No window assigned
|
|
397
|
+
if slot.pending then
|
|
214
398
|
title = slot.customName or "Opening..."
|
|
215
399
|
status = "launching"
|
|
400
|
+
else
|
|
401
|
+
title = "Empty"
|
|
402
|
+
status = "click to open"
|
|
216
403
|
end
|
|
217
404
|
bgColor = config.colors.slotEmpty
|
|
218
405
|
end
|
|
@@ -226,6 +413,12 @@ local function updateSlotDisplay(slotIndex)
|
|
|
226
413
|
if dock[baseIdx + 3] then
|
|
227
414
|
dock[baseIdx + 3].text = status
|
|
228
415
|
end
|
|
416
|
+
-- Update notification badge visibility
|
|
417
|
+
if dock[baseIdx + 4] then
|
|
418
|
+
dock[baseIdx + 4].fillColor = slot.hasNotification
|
|
419
|
+
and config.colors.notificationBadge
|
|
420
|
+
or { red = 0, green = 0, blue = 0, alpha = 0 }
|
|
421
|
+
end
|
|
229
422
|
end
|
|
230
423
|
|
|
231
424
|
updateAllSlots = function()
|
|
@@ -299,6 +492,9 @@ local function onSlotClick(slotIndex, isOptionClick)
|
|
|
299
492
|
return
|
|
300
493
|
end
|
|
301
494
|
|
|
495
|
+
-- Clear notification when clicked
|
|
496
|
+
slot.hasNotification = false
|
|
497
|
+
|
|
302
498
|
local win = getWindow(slot.windowId)
|
|
303
499
|
if win then
|
|
304
500
|
if win:isMinimized() then
|
|
@@ -336,7 +532,7 @@ end
|
|
|
336
532
|
-- Add a new slot and launch terminal
|
|
337
533
|
local function addSlot()
|
|
338
534
|
slotCount = slotCount + 1
|
|
339
|
-
slots[slotCount] = { windowId = nil, customName = nil, pending = false }
|
|
535
|
+
slots[slotCount] = { windowId = nil, customName = nil, pending = false, hasNotification = false }
|
|
340
536
|
|
|
341
537
|
if dock then
|
|
342
538
|
dock:delete()
|
|
@@ -374,14 +570,41 @@ createDock = function()
|
|
|
374
570
|
frame = { x = 0, y = 0, w = "100%", h = "100%" },
|
|
375
571
|
})
|
|
376
572
|
|
|
377
|
-
--
|
|
378
|
-
local
|
|
379
|
-
local
|
|
380
|
-
local
|
|
573
|
+
-- Utility buttons (left side, stacked: help on top, minimize on bottom)
|
|
574
|
+
local utilBtnX = config.margin
|
|
575
|
+
local btnGap = 4
|
|
576
|
+
local btnHeight = (config.slotHeight - btnGap) / 2
|
|
577
|
+
|
|
578
|
+
-- Help button (top)
|
|
579
|
+
local helpBtnY = config.margin
|
|
381
580
|
dock:appendElements({
|
|
382
581
|
type = "rectangle",
|
|
383
582
|
action = "fill",
|
|
384
|
-
frame = { x =
|
|
583
|
+
frame = { x = utilBtnX, y = helpBtnY, w = config.utilityButtonWidth, h = btnHeight },
|
|
584
|
+
roundedRectRadii = { xRadius = 6, yRadius = 6 },
|
|
585
|
+
fillColor = config.colors.helpBtnBg,
|
|
586
|
+
trackMouseUp = true,
|
|
587
|
+
trackMouseEnterExit = true,
|
|
588
|
+
id = "helpBtn",
|
|
589
|
+
})
|
|
590
|
+
dock:appendElements({
|
|
591
|
+
type = "text",
|
|
592
|
+
frame = { x = utilBtnX, y = helpBtnY + 4, w = config.utilityButtonWidth, h = btnHeight },
|
|
593
|
+
text = "?",
|
|
594
|
+
textAlignment = "center",
|
|
595
|
+
textColor = config.colors.helpBtnText,
|
|
596
|
+
textSize = 16,
|
|
597
|
+
textFont = ".AppleSystemUIFontBold",
|
|
598
|
+
trackMouseUp = true,
|
|
599
|
+
id = "helpBtn",
|
|
600
|
+
})
|
|
601
|
+
|
|
602
|
+
-- Minimize button (bottom)
|
|
603
|
+
local minBtnY = config.margin + btnHeight + btnGap
|
|
604
|
+
dock:appendElements({
|
|
605
|
+
type = "rectangle",
|
|
606
|
+
action = "fill",
|
|
607
|
+
frame = { x = utilBtnX, y = minBtnY, w = config.utilityButtonWidth, h = btnHeight },
|
|
385
608
|
roundedRectRadii = { xRadius = 6, yRadius = 6 },
|
|
386
609
|
fillColor = config.colors.minBtnBg,
|
|
387
610
|
trackMouseUp = true,
|
|
@@ -390,11 +613,11 @@ createDock = function()
|
|
|
390
613
|
})
|
|
391
614
|
dock:appendElements({
|
|
392
615
|
type = "text",
|
|
393
|
-
frame = { x =
|
|
616
|
+
frame = { x = utilBtnX, y = minBtnY + 4, w = config.utilityButtonWidth, h = btnHeight },
|
|
394
617
|
text = "⌄",
|
|
395
618
|
textAlignment = "center",
|
|
396
619
|
textColor = config.colors.minBtnText,
|
|
397
|
-
textSize =
|
|
620
|
+
textSize = 16,
|
|
398
621
|
textFont = ".AppleSystemUIFont",
|
|
399
622
|
trackMouseUp = true,
|
|
400
623
|
id = "minBtn",
|
|
@@ -443,6 +666,16 @@ createDock = function()
|
|
|
443
666
|
textSize = 10,
|
|
444
667
|
textFont = ".AppleSystemUIFont",
|
|
445
668
|
})
|
|
669
|
+
|
|
670
|
+
-- Notification badge (top-right corner of slot, overlapping edge like app badges)
|
|
671
|
+
local badgeSize = config.notificationBadgeSize
|
|
672
|
+
dock:appendElements({
|
|
673
|
+
type = "circle",
|
|
674
|
+
action = "fill",
|
|
675
|
+
center = { x = slotX + config.slotWidth - badgeSize/3, y = config.margin + badgeSize/3 },
|
|
676
|
+
radius = badgeSize / 2,
|
|
677
|
+
fillColor = { red = 0, green = 0, blue = 0, alpha = 0 }, -- Hidden by default
|
|
678
|
+
})
|
|
446
679
|
end
|
|
447
680
|
|
|
448
681
|
-- Add button (right side)
|
|
@@ -475,6 +708,8 @@ createDock = function()
|
|
|
475
708
|
addSlot()
|
|
476
709
|
elseif id == "minBtn" then
|
|
477
710
|
minimizeAllTerminals()
|
|
711
|
+
elseif id == "helpBtn" then
|
|
712
|
+
showHelpPanel()
|
|
478
713
|
elseif id and id:match("^slot") then
|
|
479
714
|
local idx = tonumber(id:match("%d+"))
|
|
480
715
|
if idx then
|
|
@@ -487,8 +722,10 @@ createDock = function()
|
|
|
487
722
|
showButtonTooltip("⌘⌥N", "addBtn")
|
|
488
723
|
elseif id == "minBtn" then
|
|
489
724
|
showButtonTooltip("⌘⌥M", "minBtn")
|
|
725
|
+
elseif id == "helpBtn" then
|
|
726
|
+
showButtonTooltip("Help", "helpBtn")
|
|
490
727
|
end
|
|
491
|
-
elseif event == "mouseExit" and (id == "addBtn" or id == "minBtn") then
|
|
728
|
+
elseif event == "mouseExit" and (id == "addBtn" or id == "minBtn" or id == "helpBtn") then
|
|
492
729
|
hideTooltip()
|
|
493
730
|
end
|
|
494
731
|
end)
|
|
@@ -524,12 +761,64 @@ end
|
|
|
524
761
|
-- Window event watcher for immediate updates
|
|
525
762
|
windowFilter = hs.window.filter.new("Terminal")
|
|
526
763
|
windowFilter:subscribe({
|
|
527
|
-
hs.window.filter.windowDestroyed,
|
|
528
764
|
hs.window.filter.windowMinimized,
|
|
529
765
|
hs.window.filter.windowUnminimized,
|
|
530
|
-
hs.window.filter.windowFocused,
|
|
531
766
|
}, updateAllSlots)
|
|
532
767
|
|
|
768
|
+
-- Handle window destruction - clear the slot
|
|
769
|
+
windowFilter:subscribe(hs.window.filter.windowDestroyed, function(win, appName, event)
|
|
770
|
+
-- win may be nil at this point, so we need to check all slots
|
|
771
|
+
for i, slot in ipairs(slots) do
|
|
772
|
+
if slot.windowId then
|
|
773
|
+
local existingWin = getWindow(slot.windowId)
|
|
774
|
+
if not existingWin then
|
|
775
|
+
-- Try to verify window is truly gone (not just on another space)
|
|
776
|
+
-- by checking all Terminal windows
|
|
777
|
+
local allTerminals = hs.window.filter.new("Terminal"):getWindows()
|
|
778
|
+
local found = false
|
|
779
|
+
for _, w in ipairs(allTerminals) do
|
|
780
|
+
if w:id() == slot.windowId then
|
|
781
|
+
found = true
|
|
782
|
+
break
|
|
783
|
+
end
|
|
784
|
+
end
|
|
785
|
+
if not found then
|
|
786
|
+
slot.windowId = nil
|
|
787
|
+
slot.customName = nil
|
|
788
|
+
slot.hasNotification = false
|
|
789
|
+
end
|
|
790
|
+
end
|
|
791
|
+
end
|
|
792
|
+
end
|
|
793
|
+
updateAllSlots()
|
|
794
|
+
end)
|
|
795
|
+
|
|
796
|
+
-- Clear notification when window is focused
|
|
797
|
+
windowFilter:subscribe(hs.window.filter.windowFocused, function(win)
|
|
798
|
+
if win then
|
|
799
|
+
local slotIndex = findSlotByWindowId(win:id())
|
|
800
|
+
if slotIndex then
|
|
801
|
+
slots[slotIndex].hasNotification = false
|
|
802
|
+
end
|
|
803
|
+
end
|
|
804
|
+
updateAllSlots()
|
|
805
|
+
end)
|
|
806
|
+
|
|
807
|
+
-- Watch for window title changes (indicates terminal activity)
|
|
808
|
+
windowFilter:subscribe(hs.window.filter.windowTitleChanged, function(win)
|
|
809
|
+
if win then
|
|
810
|
+
local slotIndex = findSlotByWindowId(win:id())
|
|
811
|
+
if slotIndex then
|
|
812
|
+
local focusedWin = hs.window.focusedWindow()
|
|
813
|
+
-- Only show notification if this window isn't focused
|
|
814
|
+
if not focusedWin or focusedWin:id() ~= win:id() then
|
|
815
|
+
slots[slotIndex].hasNotification = true
|
|
816
|
+
updateSlotDisplay(slotIndex)
|
|
817
|
+
end
|
|
818
|
+
end
|
|
819
|
+
end
|
|
820
|
+
end)
|
|
821
|
+
|
|
533
822
|
-- Periodic refresh as fallback
|
|
534
823
|
updateTimer = hs.timer.doEvery(2, function()
|
|
535
824
|
if dock and dock:isShowing() then
|
|
@@ -548,7 +837,53 @@ screenWatcher = hs.screen.watcher.new(function()
|
|
|
548
837
|
end)
|
|
549
838
|
screenWatcher:start()
|
|
550
839
|
|
|
840
|
+
-- Move macOS dock position
|
|
841
|
+
local function moveMacOSDockLeft()
|
|
842
|
+
hs.execute("defaults write com.apple.dock orientation left && killall Dock")
|
|
843
|
+
macOSDockAtBottom = false
|
|
844
|
+
hs.alert.show("macOS Dock moved to left")
|
|
845
|
+
-- Reposition Claude Dock
|
|
846
|
+
if dock then
|
|
847
|
+
local frame = getDockFrame()
|
|
848
|
+
if frame then dock:frame(frame) end
|
|
849
|
+
end
|
|
850
|
+
end
|
|
851
|
+
|
|
852
|
+
local function moveMacOSDockBottom()
|
|
853
|
+
hs.execute("defaults write com.apple.dock orientation bottom && killall Dock")
|
|
854
|
+
macOSDockAtBottom = true
|
|
855
|
+
hs.alert.show("macOS Dock moved to bottom")
|
|
856
|
+
-- Reposition Claude Dock
|
|
857
|
+
if dock then
|
|
858
|
+
local frame = getDockFrame()
|
|
859
|
+
if frame then dock:frame(frame) end
|
|
860
|
+
end
|
|
861
|
+
end
|
|
862
|
+
|
|
863
|
+
-- Check macOS dock and prompt user if at bottom
|
|
864
|
+
-- Returns true if user chose to keep dock at bottom
|
|
865
|
+
local function checkMacOSDockOnStartup()
|
|
866
|
+
local position = getMacOSDockPosition()
|
|
867
|
+
if position == "bottom" then
|
|
868
|
+
local button = hs.dialog.blockAlert(
|
|
869
|
+
"macOS Dock Position",
|
|
870
|
+
"Your macOS Dock is at the bottom of the screen, which may overlap with Claude Dock.\n\nWould you like to move it to the left side?",
|
|
871
|
+
"Move to Left",
|
|
872
|
+
"Keep at Bottom"
|
|
873
|
+
)
|
|
874
|
+
if button == "Move to Left" then
|
|
875
|
+
moveMacOSDockLeft()
|
|
876
|
+
return false
|
|
877
|
+
else
|
|
878
|
+
macOSDockAtBottom = true
|
|
879
|
+
return true
|
|
880
|
+
end
|
|
881
|
+
end
|
|
882
|
+
return false
|
|
883
|
+
end
|
|
884
|
+
|
|
551
885
|
-- Initialize
|
|
886
|
+
local showRepositionedMsg = checkMacOSDockOnStartup()
|
|
552
887
|
createDock()
|
|
553
888
|
|
|
554
889
|
-- Hotkeys
|
|
@@ -556,8 +891,35 @@ hs.hotkey.bind({"cmd", "alt"}, "T", toggleDock)
|
|
|
556
891
|
hs.hotkey.bind({"cmd", "alt"}, "N", addSlot)
|
|
557
892
|
hs.hotkey.bind({"cmd", "alt"}, "M", minimizeAllTerminals)
|
|
558
893
|
hs.hotkey.bind({"cmd", "alt"}, "R", hs.reload)
|
|
894
|
+
hs.hotkey.bind({"cmd", "alt"}, "L", moveMacOSDockLeft)
|
|
895
|
+
hs.hotkey.bind({"cmd", "alt"}, "B", moveMacOSDockBottom)
|
|
559
896
|
|
|
560
897
|
hs.alert.show("Claude Dock Ready")
|
|
898
|
+
if showRepositionedMsg then
|
|
899
|
+
hs.timer.doAfter(1.5, function()
|
|
900
|
+
hs.alert.show("Positioned above macOS Dock")
|
|
901
|
+
end)
|
|
902
|
+
end
|
|
903
|
+
|
|
904
|
+
-- Global function to trigger notification on a slot (for testing or external use)
|
|
905
|
+
-- Usage: triggerNotification(1) to trigger on slot 1
|
|
906
|
+
function triggerNotification(slotIndex)
|
|
907
|
+
if slotIndex and slots[slotIndex] then
|
|
908
|
+
setSlotNotification(slotIndex)
|
|
909
|
+
return true
|
|
910
|
+
end
|
|
911
|
+
return false
|
|
912
|
+
end
|
|
913
|
+
|
|
914
|
+
-- Global function to clear notification on a slot
|
|
915
|
+
function clearNotification(slotIndex)
|
|
916
|
+
if slotIndex and slots[slotIndex] then
|
|
917
|
+
slots[slotIndex].hasNotification = false
|
|
918
|
+
updateSlotDisplay(slotIndex)
|
|
919
|
+
return true
|
|
920
|
+
end
|
|
921
|
+
return false
|
|
922
|
+
end
|
|
561
923
|
|
|
562
924
|
-- ===================
|
|
563
925
|
-- TESTS (run with: hs -c "runTests()")
|
|
@@ -704,6 +1066,75 @@ function runTests()
|
|
|
704
1066
|
assert(ok, "should not error with invalid windowIds")
|
|
705
1067
|
end)
|
|
706
1068
|
|
|
1069
|
+
test("moveMacOSDockLeft is a function", function()
|
|
1070
|
+
assert(type(moveMacOSDockLeft) == "function", "moveMacOSDockLeft should be a function")
|
|
1071
|
+
end)
|
|
1072
|
+
|
|
1073
|
+
test("moveMacOSDockBottom is a function", function()
|
|
1074
|
+
assert(type(moveMacOSDockBottom) == "function", "moveMacOSDockBottom should be a function")
|
|
1075
|
+
end)
|
|
1076
|
+
|
|
1077
|
+
test("getMacOSDockPosition returns valid position", function()
|
|
1078
|
+
local pos = getMacOSDockPosition()
|
|
1079
|
+
assert(pos == "left" or pos == "right" or pos == "bottom", "position should be left, right, or bottom")
|
|
1080
|
+
end)
|
|
1081
|
+
|
|
1082
|
+
test("getMacOSDockHeight returns number", function()
|
|
1083
|
+
local height = getMacOSDockHeight()
|
|
1084
|
+
assert(type(height) == "number", "height should be a number")
|
|
1085
|
+
assert(height > 0, "height should be positive")
|
|
1086
|
+
end)
|
|
1087
|
+
|
|
1088
|
+
test("checkMacOSDockOnStartup is a function", function()
|
|
1089
|
+
assert(type(checkMacOSDockOnStartup) == "function", "checkMacOSDockOnStartup should be a function")
|
|
1090
|
+
end)
|
|
1091
|
+
|
|
1092
|
+
test("showHelpPanel is a function", function()
|
|
1093
|
+
assert(type(showHelpPanel) == "function", "showHelpPanel should be a function")
|
|
1094
|
+
end)
|
|
1095
|
+
|
|
1096
|
+
test("hideHelpPanel is a function", function()
|
|
1097
|
+
assert(type(hideHelpPanel) == "function", "hideHelpPanel should be a function")
|
|
1098
|
+
end)
|
|
1099
|
+
|
|
1100
|
+
-- Notification badge tests
|
|
1101
|
+
test("config has notification badge color", function()
|
|
1102
|
+
assert(config.colors.notificationBadge, "notificationBadge color should exist")
|
|
1103
|
+
assert(config.notificationBadgeSize, "notificationBadgeSize should exist")
|
|
1104
|
+
end)
|
|
1105
|
+
|
|
1106
|
+
test("slots have hasNotification field", function()
|
|
1107
|
+
slots = {}
|
|
1108
|
+
slotCount = 2
|
|
1109
|
+
initSlots()
|
|
1110
|
+
assert(slots[1].hasNotification == false, "slot should have hasNotification = false")
|
|
1111
|
+
restore()
|
|
1112
|
+
end)
|
|
1113
|
+
|
|
1114
|
+
test("findSlotByWindowId returns nil for unknown window", function()
|
|
1115
|
+
assertEqual(findSlotByWindowId(999999999), nil)
|
|
1116
|
+
end)
|
|
1117
|
+
|
|
1118
|
+
test("findSlotByWindowId returns nil for nil input", function()
|
|
1119
|
+
assertEqual(findSlotByWindowId(nil), nil)
|
|
1120
|
+
end)
|
|
1121
|
+
|
|
1122
|
+
test("triggerNotification is a function", function()
|
|
1123
|
+
assert(type(triggerNotification) == "function", "triggerNotification should be a function")
|
|
1124
|
+
end)
|
|
1125
|
+
|
|
1126
|
+
test("clearNotification is a function", function()
|
|
1127
|
+
assert(type(clearNotification) == "function", "clearNotification should be a function")
|
|
1128
|
+
end)
|
|
1129
|
+
|
|
1130
|
+
test("triggerNotification returns false for invalid slot", function()
|
|
1131
|
+
assertEqual(triggerNotification(9999), false)
|
|
1132
|
+
end)
|
|
1133
|
+
|
|
1134
|
+
test("clearNotification returns false for invalid slot", function()
|
|
1135
|
+
assertEqual(clearNotification(9999), false)
|
|
1136
|
+
end)
|
|
1137
|
+
|
|
707
1138
|
print("\n=== Results: " .. passed .. " passed, " .. failed .. " failed ===\n")
|
|
708
1139
|
return failed == 0
|
|
709
1140
|
end
|