claude-dock 1.1.0 → 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/init.lua +174 -13
- package/package.json +1 -1
package/init.lua
CHANGED
|
@@ -13,7 +13,7 @@ local config = {
|
|
|
13
13
|
addButtonWidth = 40,
|
|
14
14
|
utilityButtonWidth = 28,
|
|
15
15
|
initialSlots = 3,
|
|
16
|
-
elementsPerSlot =
|
|
16
|
+
elementsPerSlot = 5, -- bg, border, title, status, notification badge
|
|
17
17
|
baseElements = 6, -- dock bg + border + help btn + help text + minimize btn + minimize text
|
|
18
18
|
windowCaptureDelay = 0.3,
|
|
19
19
|
windowCaptureRetries = 5,
|
|
@@ -32,7 +32,9 @@ local config = {
|
|
|
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
34
|
helpBtnText = { red = 0.9, green = 0.8, blue = 0.5, alpha = 1 },
|
|
35
|
-
|
|
35
|
+
notificationBadge = { red = 1, green = 0.3, blue = 0.3, alpha = 1 },
|
|
36
|
+
},
|
|
37
|
+
notificationBadgeSize = 12,
|
|
36
38
|
}
|
|
37
39
|
|
|
38
40
|
-- State
|
|
@@ -100,7 +102,7 @@ cleanup() -- Clean up any previous instance
|
|
|
100
102
|
local function initSlots()
|
|
101
103
|
for i = 1, slotCount do
|
|
102
104
|
if not slots[i] then
|
|
103
|
-
slots[i] = { windowId = nil, customName = nil, pending = false }
|
|
105
|
+
slots[i] = { windowId = nil, customName = nil, pending = false, hasNotification = false }
|
|
104
106
|
end
|
|
105
107
|
end
|
|
106
108
|
end
|
|
@@ -152,6 +154,33 @@ local function getWindowTitle(win)
|
|
|
152
154
|
return title
|
|
153
155
|
end
|
|
154
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
|
+
|
|
155
184
|
-- Tooltip helpers
|
|
156
185
|
local function showTooltipAt(text, x, y)
|
|
157
186
|
if tooltip then tooltip:delete() end
|
|
@@ -358,16 +387,19 @@ local function updateSlotDisplay(slotIndex)
|
|
|
358
387
|
status = "active"
|
|
359
388
|
bgColor = config.colors.slotActive
|
|
360
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
|
|
361
395
|
else
|
|
362
|
-
--
|
|
363
|
-
if
|
|
364
|
-
slot.windowId = nil
|
|
365
|
-
slot.customName = nil
|
|
366
|
-
title = "Empty"
|
|
367
|
-
status = "click to open"
|
|
368
|
-
else
|
|
396
|
+
-- No window assigned
|
|
397
|
+
if slot.pending then
|
|
369
398
|
title = slot.customName or "Opening..."
|
|
370
399
|
status = "launching"
|
|
400
|
+
else
|
|
401
|
+
title = "Empty"
|
|
402
|
+
status = "click to open"
|
|
371
403
|
end
|
|
372
404
|
bgColor = config.colors.slotEmpty
|
|
373
405
|
end
|
|
@@ -381,6 +413,12 @@ local function updateSlotDisplay(slotIndex)
|
|
|
381
413
|
if dock[baseIdx + 3] then
|
|
382
414
|
dock[baseIdx + 3].text = status
|
|
383
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
|
|
384
422
|
end
|
|
385
423
|
|
|
386
424
|
updateAllSlots = function()
|
|
@@ -454,6 +492,9 @@ local function onSlotClick(slotIndex, isOptionClick)
|
|
|
454
492
|
return
|
|
455
493
|
end
|
|
456
494
|
|
|
495
|
+
-- Clear notification when clicked
|
|
496
|
+
slot.hasNotification = false
|
|
497
|
+
|
|
457
498
|
local win = getWindow(slot.windowId)
|
|
458
499
|
if win then
|
|
459
500
|
if win:isMinimized() then
|
|
@@ -491,7 +532,7 @@ end
|
|
|
491
532
|
-- Add a new slot and launch terminal
|
|
492
533
|
local function addSlot()
|
|
493
534
|
slotCount = slotCount + 1
|
|
494
|
-
slots[slotCount] = { windowId = nil, customName = nil, pending = false }
|
|
535
|
+
slots[slotCount] = { windowId = nil, customName = nil, pending = false, hasNotification = false }
|
|
495
536
|
|
|
496
537
|
if dock then
|
|
497
538
|
dock:delete()
|
|
@@ -625,6 +666,16 @@ createDock = function()
|
|
|
625
666
|
textSize = 10,
|
|
626
667
|
textFont = ".AppleSystemUIFont",
|
|
627
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
|
+
})
|
|
628
679
|
end
|
|
629
680
|
|
|
630
681
|
-- Add button (right side)
|
|
@@ -710,12 +761,64 @@ end
|
|
|
710
761
|
-- Window event watcher for immediate updates
|
|
711
762
|
windowFilter = hs.window.filter.new("Terminal")
|
|
712
763
|
windowFilter:subscribe({
|
|
713
|
-
hs.window.filter.windowDestroyed,
|
|
714
764
|
hs.window.filter.windowMinimized,
|
|
715
765
|
hs.window.filter.windowUnminimized,
|
|
716
|
-
hs.window.filter.windowFocused,
|
|
717
766
|
}, updateAllSlots)
|
|
718
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
|
+
|
|
719
822
|
-- Periodic refresh as fallback
|
|
720
823
|
updateTimer = hs.timer.doEvery(2, function()
|
|
721
824
|
if dock and dock:isShowing() then
|
|
@@ -798,6 +901,26 @@ if showRepositionedMsg then
|
|
|
798
901
|
end)
|
|
799
902
|
end
|
|
800
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
|
|
923
|
+
|
|
801
924
|
-- ===================
|
|
802
925
|
-- TESTS (run with: hs -c "runTests()")
|
|
803
926
|
-- ===================
|
|
@@ -974,6 +1097,44 @@ function runTests()
|
|
|
974
1097
|
assert(type(hideHelpPanel) == "function", "hideHelpPanel should be a function")
|
|
975
1098
|
end)
|
|
976
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
|
+
|
|
977
1138
|
print("\n=== Results: " .. passed .. " passed, " .. failed .. " failed ===\n")
|
|
978
1139
|
return failed == 0
|
|
979
1140
|
end
|