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.
Files changed (2) hide show
  1. package/init.lua +174 -13
  2. 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 = 4, -- bg, border, title, status
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
- -- Don't clear customName if we're waiting for a window to spawn
363
- if not slot.pending then
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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-dock",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "description": "A lightweight terminal dock for macOS to manage Claude Code sessions",
5
5
  "bin": {
6
6
  "claude-dock": "bin/cli.js"