claude-dock 1.0.0 → 1.0.2

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 (3) hide show
  1. package/README.md +22 -7
  2. package/init.lua +124 -18
  3. package/package.json +3 -3
package/README.md CHANGED
@@ -3,6 +3,7 @@
3
3
  A lightweight, expandable terminal dock for macOS built with [Hammerspoon](https://www.hammerspoon.org/). Designed for managing multiple Claude Code terminal sessions.
4
4
 
5
5
  ![Claude Dock](https://img.shields.io/badge/macOS-Hammerspoon-blue)
6
+ [![npm version](https://img.shields.io/npm/v/claude-dock.svg)](https://www.npmjs.com/package/claude-dock)
6
7
 
7
8
  ## Features
8
9
 
@@ -16,7 +17,22 @@ A lightweight, expandable terminal dock for macOS built with [Hammerspoon](https
16
17
 
17
18
  ## Installation
18
19
 
19
- ### Prerequisites
20
+ ```bash
21
+ npx claude-dock
22
+ ```
23
+
24
+ That's it! The installer will:
25
+ - Install Hammerspoon (if needed)
26
+ - Set up the dock configuration
27
+ - Launch Hammerspoon
28
+
29
+ **Note:** You'll need to grant Accessibility permissions when prompted:
30
+ System Settings → Privacy & Security → Accessibility → Enable Hammerspoon
31
+
32
+ ### Manual Installation
33
+
34
+ <details>
35
+ <summary>Click to expand manual setup instructions</summary>
20
36
 
21
37
  1. Install [Hammerspoon](https://www.hammerspoon.org/):
22
38
  ```bash
@@ -27,16 +43,15 @@ A lightweight, expandable terminal dock for macOS built with [Hammerspoon](https
27
43
  - System Settings > Privacy & Security > Accessibility
28
44
  - Enable Hammerspoon
29
45
 
30
- ### Setup
31
-
32
- 1. Clone this repo (or download the ZIP from GitHub)
33
-
34
- 2. Copy to Hammerspoon config:
46
+ 3. Clone this repo and copy the config:
35
47
  ```bash
48
+ git clone https://github.com/matthewmolinar/claude-dock.git
36
49
  cp claude-dock/init.lua ~/.hammerspoon/init.lua
37
50
  ```
38
51
 
39
- 3. Launch Hammerspoon (or reload if already running)
52
+ 4. Launch Hammerspoon (or reload if already running)
53
+
54
+ </details>
40
55
 
41
56
  ## Usage
42
57
 
package/init.lua CHANGED
@@ -11,9 +11,10 @@ local config = {
11
11
  margin = 10,
12
12
  bottomOffset = 5,
13
13
  addButtonWidth = 40,
14
+ utilityButtonWidth = 28,
14
15
  initialSlots = 3,
15
16
  elementsPerSlot = 4, -- bg, border, title, status
16
- baseElements = 2, -- dock bg + border
17
+ baseElements = 4, -- dock bg + border + minimize btn + minimize text
17
18
  windowCaptureDelay = 0.3,
18
19
  windowCaptureRetries = 5,
19
20
  colors = {
@@ -27,6 +28,8 @@ local config = {
27
28
  textSecondary = { red = 0.5, green = 0.5, blue = 0.5, alpha = 1 },
28
29
  addBtnBg = { red = 0.2, green = 0.25, blue = 0.2, alpha = 1 },
29
30
  addBtnText = { red = 0.6, green = 0.8, blue = 0.6, alpha = 1 },
31
+ minBtnBg = { red = 0.18, green = 0.18, blue = 0.25, alpha = 1 },
32
+ minBtnText = { red = 0.6, green = 0.6, blue = 0.9, alpha = 1 },
30
33
  }
31
34
  }
32
35
 
@@ -77,7 +80,10 @@ initSlots()
77
80
 
78
81
  -- Calculate dock dimensions
79
82
  local function getDockWidth()
80
- return (config.slotWidth * slotCount) + (config.gap * slotCount) + (config.margin * 2) + config.addButtonWidth
83
+ -- Left: small utility button (minimize), Right: add button
84
+ local leftButtonWidth = config.utilityButtonWidth + config.gap
85
+ local rightButtonWidth = config.addButtonWidth
86
+ return leftButtonWidth + (config.slotWidth * slotCount) + (config.gap * (slotCount - 1)) + config.gap + rightButtonWidth + (config.margin * 2)
81
87
  end
82
88
 
83
89
  local function getDockHeight()
@@ -114,18 +120,13 @@ local function getWindowTitle(win)
114
120
  end
115
121
 
116
122
  -- Tooltip helpers
117
- local function showTooltip(text)
123
+ local function showTooltipAt(text, x, y)
118
124
  if tooltip then tooltip:delete() end
119
- local dockFrame = getDockFrame()
120
- if not dockFrame then return end
121
125
 
122
126
  local tipWidth = 50
123
127
  local tipHeight = 24
124
- local addBtnX = dockFrame.x + config.margin + (slotCount * (config.slotWidth + config.gap))
125
- local tipX = addBtnX + (config.addButtonWidth - tipWidth) / 2
126
- local tipY = dockFrame.y - tipHeight - 5
127
128
 
128
- tooltip = hs.canvas.new({ x = tipX, y = tipY, w = tipWidth, h = tipHeight })
129
+ tooltip = hs.canvas.new({ x = x, y = y, w = tipWidth, h = tipHeight })
129
130
  tooltip:appendElements({
130
131
  type = "rectangle",
131
132
  action = "fill",
@@ -146,6 +147,30 @@ local function showTooltip(text)
146
147
  tooltip:show()
147
148
  end
148
149
 
150
+ local function showButtonTooltip(text, buttonId)
151
+ local dockFrame = getDockFrame()
152
+ if not dockFrame then return end
153
+
154
+ local tipWidth = 50
155
+ local tipHeight = 24
156
+ local btnX, btnWidth
157
+
158
+ if buttonId == "minBtn" then
159
+ -- Left side utility button
160
+ btnX = dockFrame.x + config.margin
161
+ btnWidth = config.utilityButtonWidth
162
+ else
163
+ -- Right side add button
164
+ btnX = dockFrame.x + config.margin + config.utilityButtonWidth + config.gap + (slotCount * config.slotWidth) + (slotCount * config.gap)
165
+ btnWidth = config.addButtonWidth
166
+ end
167
+
168
+ local tipX = btnX + (btnWidth - tipWidth) / 2
169
+ local tipY = dockFrame.y - tipHeight - 5
170
+
171
+ showTooltipAt(text, tipX, tipY)
172
+ end
173
+
149
174
  local function hideTooltip()
150
175
  if tooltip then
151
176
  tooltip:delete()
@@ -156,6 +181,7 @@ end
156
181
  -- Forward declarations
157
182
  local createDock
158
183
  local updateAllSlots
184
+ local minimizeAllTerminals
159
185
 
160
186
  -- Update slot display
161
187
  local function updateSlotDisplay(slotIndex)
@@ -348,9 +374,36 @@ createDock = function()
348
374
  frame = { x = 0, y = 0, w = "100%", h = "100%" },
349
375
  })
350
376
 
351
- -- Slots
377
+ -- Minimize button (left side, small utility style)
378
+ local minBtnX = config.margin
379
+ local minBtnHeight = 28
380
+ local minBtnY = config.margin + (config.slotHeight - minBtnHeight) / 2
381
+ dock:appendElements({
382
+ type = "rectangle",
383
+ action = "fill",
384
+ frame = { x = minBtnX, y = minBtnY, w = config.utilityButtonWidth, h = minBtnHeight },
385
+ roundedRectRadii = { xRadius = 6, yRadius = 6 },
386
+ fillColor = config.colors.minBtnBg,
387
+ trackMouseUp = true,
388
+ trackMouseEnterExit = true,
389
+ id = "minBtn",
390
+ })
391
+ dock:appendElements({
392
+ type = "text",
393
+ frame = { x = minBtnX, y = minBtnY + 2, w = config.utilityButtonWidth, h = minBtnHeight },
394
+ text = "⌄",
395
+ textAlignment = "center",
396
+ textColor = config.colors.minBtnText,
397
+ textSize = 18,
398
+ textFont = ".AppleSystemUIFont",
399
+ trackMouseUp = true,
400
+ id = "minBtn",
401
+ })
402
+
403
+ -- Slots (offset by utility button)
404
+ local slotsStartX = config.margin + config.utilityButtonWidth + config.gap
352
405
  for i = 1, slotCount do
353
- local slotX = config.margin + ((i - 1) * (config.slotWidth + config.gap))
406
+ local slotX = slotsStartX + ((i - 1) * (config.slotWidth + config.gap))
354
407
 
355
408
  dock:appendElements({
356
409
  type = "rectangle",
@@ -392,8 +445,8 @@ createDock = function()
392
445
  })
393
446
  end
394
447
 
395
- -- Add button
396
- local addBtnX = config.margin + (slotCount * (config.slotWidth + config.gap))
448
+ -- Add button (right side)
449
+ local addBtnX = slotsStartX + (slotCount * (config.slotWidth + config.gap))
397
450
  dock:appendElements({
398
451
  type = "rectangle",
399
452
  action = "fill",
@@ -412,12 +465,16 @@ createDock = function()
412
465
  textColor = config.colors.addBtnText,
413
466
  textSize = 28,
414
467
  textFont = ".AppleSystemUIFont",
468
+ trackMouseUp = true,
469
+ id = "addBtn",
415
470
  })
416
471
 
417
472
  dock:mouseCallback(function(_, event, id)
418
473
  if event == "mouseUp" then
419
474
  if id == "addBtn" then
420
475
  addSlot()
476
+ elseif id == "minBtn" then
477
+ minimizeAllTerminals()
421
478
  elseif id and id:match("^slot") then
422
479
  local idx = tonumber(id:match("%d+"))
423
480
  if idx then
@@ -425,9 +482,13 @@ createDock = function()
425
482
  onSlotClick(idx, mods.alt)
426
483
  end
427
484
  end
428
- elseif event == "mouseEnter" and id == "addBtn" then
429
- showTooltip("⌘⌥N")
430
- elseif event == "mouseExit" and id == "addBtn" then
485
+ elseif event == "mouseEnter" then
486
+ if id == "addBtn" then
487
+ showButtonTooltip("⌘⌥N", "addBtn")
488
+ elseif id == "minBtn" then
489
+ showButtonTooltip("⌘⌥M", "minBtn")
490
+ end
491
+ elseif event == "mouseExit" and (id == "addBtn" or id == "minBtn") then
431
492
  hideTooltip()
432
493
  end
433
494
  end)
@@ -444,6 +505,22 @@ local function toggleDock()
444
505
  end
445
506
  end
446
507
 
508
+ -- Minimize all terminal windows
509
+ minimizeAllTerminals = function()
510
+ local minimizedCount = 0
511
+ for _, slot in ipairs(slots) do
512
+ local win = getWindow(slot.windowId)
513
+ if win and not win:isMinimized() then
514
+ win:minimize()
515
+ minimizedCount = minimizedCount + 1
516
+ end
517
+ end
518
+ if minimizedCount > 0 then
519
+ hs.alert.show("Minimized " .. minimizedCount .. " terminal" .. (minimizedCount > 1 and "s" or ""))
520
+ end
521
+ updateAllSlots()
522
+ end
523
+
447
524
  -- Window event watcher for immediate updates
448
525
  windowFilter = hs.window.filter.new("Terminal")
449
526
  windowFilter:subscribe({
@@ -477,6 +554,7 @@ createDock()
477
554
  -- Hotkeys
478
555
  hs.hotkey.bind({"cmd", "alt"}, "T", toggleDock)
479
556
  hs.hotkey.bind({"cmd", "alt"}, "N", addSlot)
557
+ hs.hotkey.bind({"cmd", "alt"}, "M", minimizeAllTerminals)
480
558
  hs.hotkey.bind({"cmd", "alt"}, "R", hs.reload)
481
559
 
482
560
  hs.alert.show("Claude Dock Ready")
@@ -487,7 +565,11 @@ hs.alert.show("Claude Dock Ready")
487
565
 
488
566
  function runTests()
489
567
  local passed, failed = 0, 0
490
- local savedSlotCount, savedSlots = slotCount, slots
568
+ local savedSlotCount = slotCount
569
+ local savedSlots = {}
570
+ for i, s in ipairs(slots) do
571
+ savedSlots[i] = { windowId = s.windowId, customName = s.customName, pending = s.pending }
572
+ end
491
573
 
492
574
  local function test(name, fn)
493
575
  local ok, err = pcall(fn)
@@ -507,7 +589,11 @@ function runTests()
507
589
  end
508
590
 
509
591
  local function restore()
510
- slotCount, slots = savedSlotCount, savedSlots
592
+ slotCount = savedSlotCount
593
+ slots = {}
594
+ for i, s in ipairs(savedSlots) do
595
+ slots[i] = { windowId = s.windowId, customName = s.customName, pending = s.pending }
596
+ end
511
597
  end
512
598
 
513
599
  print("\n=== Claude Dock Tests ===\n")
@@ -598,6 +684,26 @@ function runTests()
598
684
  assert(frame.x and frame.y and frame.w and frame.h, "frame should have x,y,w,h")
599
685
  end)
600
686
 
687
+ test("minimizeAllTerminals is a function", function()
688
+ assert(type(minimizeAllTerminals) == "function", "minimizeAllTerminals should be a function")
689
+ end)
690
+
691
+ test("minimizeAllTerminals handles empty slots", function()
692
+ slots = {{ windowId = nil }, { windowId = nil }}
693
+ slotCount = 2
694
+ local ok = pcall(minimizeAllTerminals)
695
+ restore()
696
+ assert(ok, "should not error with empty slots")
697
+ end)
698
+
699
+ test("minimizeAllTerminals handles invalid windowIds", function()
700
+ slots = {{ windowId = 999999999 }, { windowId = 888888888 }}
701
+ slotCount = 2
702
+ local ok = pcall(minimizeAllTerminals)
703
+ restore()
704
+ assert(ok, "should not error with invalid windowIds")
705
+ end)
706
+
601
707
  print("\n=== Results: " .. passed .. " passed, " .. failed .. " failed ===\n")
602
708
  return failed == 0
603
709
  end
package/package.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "claude-dock",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "A lightweight terminal dock for macOS to manage Claude Code sessions",
5
5
  "bin": {
6
- "claude-dock": "./bin/cli.js"
6
+ "claude-dock": "bin/cli.js"
7
7
  },
8
8
  "keywords": [
9
9
  "claude",
@@ -17,7 +17,7 @@
17
17
  "license": "MIT",
18
18
  "repository": {
19
19
  "type": "git",
20
- "url": "https://github.com/matthewmolinar/claude-dock.git"
20
+ "url": "git+https://github.com/matthewmolinar/claude-dock.git"
21
21
  },
22
22
  "homepage": "https://github.com/matthewmolinar/claude-dock",
23
23
  "engines": {