claude-color 0.1.0__tar.gz

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 (46) hide show
  1. claude_color-0.1.0/LICENSE +21 -0
  2. claude_color-0.1.0/PKG-INFO +143 -0
  3. claude_color-0.1.0/README.md +119 -0
  4. claude_color-0.1.0/pyproject.toml +36 -0
  5. claude_color-0.1.0/setup.cfg +4 -0
  6. claude_color-0.1.0/src/claude_color/__init__.py +1 -0
  7. claude_color-0.1.0/src/claude_color/__main__.py +4 -0
  8. claude_color-0.1.0/src/claude_color/cli.py +169 -0
  9. claude_color-0.1.0/src/claude_color/config.py +59 -0
  10. claude_color-0.1.0/src/claude_color/events.py +128 -0
  11. claude_color-0.1.0/src/claude_color/install.py +213 -0
  12. claude_color-0.1.0/src/claude_color/keyboard/__init__.py +0 -0
  13. claude_color-0.1.0/src/claude_color/keyboard/base.py +21 -0
  14. claude_color-0.1.0/src/claude_color/keyboard/factory.py +20 -0
  15. claude_color-0.1.0/src/claude_color/keyboard/hidapi.py +126 -0
  16. claude_color-0.1.0/src/claude_color/keyboard/qmkhid.py +53 -0
  17. claude_color-0.1.0/src/claude_color/lock.py +43 -0
  18. claude_color-0.1.0/src/claude_color/logging_setup.py +24 -0
  19. claude_color-0.1.0/src/claude_color/paths.py +45 -0
  20. claude_color-0.1.0/src/claude_color/procinfo.py +136 -0
  21. claude_color-0.1.0/src/claude_color/states.py +18 -0
  22. claude_color-0.1.0/src/claude_color/statestore.py +90 -0
  23. claude_color-0.1.0/src/claude_color.egg-info/PKG-INFO +143 -0
  24. claude_color-0.1.0/src/claude_color.egg-info/SOURCES.txt +44 -0
  25. claude_color-0.1.0/src/claude_color.egg-info/dependency_links.txt +1 -0
  26. claude_color-0.1.0/src/claude_color.egg-info/entry_points.txt +2 -0
  27. claude_color-0.1.0/src/claude_color.egg-info/requires.txt +4 -0
  28. claude_color-0.1.0/src/claude_color.egg-info/top_level.txt +1 -0
  29. claude_color-0.1.0/tests/test_baseline.py +44 -0
  30. claude_color-0.1.0/tests/test_cli_hook.py +74 -0
  31. claude_color-0.1.0/tests/test_cli_reconcile.py +58 -0
  32. claude_color-0.1.0/tests/test_cli_test.py +67 -0
  33. claude_color-0.1.0/tests/test_config.py +49 -0
  34. claude_color-0.1.0/tests/test_events.py +93 -0
  35. claude_color-0.1.0/tests/test_hidapi.py +94 -0
  36. claude_color-0.1.0/tests/test_install.py +117 -0
  37. claude_color-0.1.0/tests/test_install_task.py +27 -0
  38. claude_color-0.1.0/tests/test_liveness.py +129 -0
  39. claude_color-0.1.0/tests/test_lock.py +44 -0
  40. claude_color-0.1.0/tests/test_migration.py +39 -0
  41. claude_color-0.1.0/tests/test_notifications.py +94 -0
  42. claude_color-0.1.0/tests/test_procinfo.py +77 -0
  43. claude_color-0.1.0/tests/test_qmkhid.py +51 -0
  44. claude_color-0.1.0/tests/test_states.py +19 -0
  45. claude_color-0.1.0/tests/test_statestore.py +59 -0
  46. claude_color-0.1.0/tests/test_via_devices.py +127 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Edocksil
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,143 @@
1
+ Metadata-Version: 2.4
2
+ Name: claude-color
3
+ Version: 0.1.0
4
+ Summary: Drive any wired VIA-compatible RGB keyboard from Claude Code status
5
+ Author: Edocksil
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/Edocksil/claude-color
8
+ Project-URL: Repository, https://github.com/Edocksil/claude-color
9
+ Project-URL: Issues, https://github.com/Edocksil/claude-color/issues
10
+ Keywords: claude-code,qmk,via,hid,rgb,keyboard,hooks,status
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Programming Language :: Python :: 3.11
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: Environment :: Console
15
+ Classifier: Operating System :: Microsoft :: Windows
16
+ Classifier: Topic :: Utilities
17
+ Requires-Python: >=3.11
18
+ Description-Content-Type: text/markdown
19
+ License-File: LICENSE
20
+ Requires-Dist: hidapi>=0.14
21
+ Provides-Extra: dev
22
+ Requires-Dist: pytest>=8; extra == "dev"
23
+ Dynamic: license-file
24
+
25
+ # claude-color
26
+
27
+ ![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)
28
+ ![Python 3.11+](https://img.shields.io/badge/python-3.11%2B-blue.svg)
29
+ ![Platform: Windows](https://img.shields.io/badge/platform-Windows-blue.svg)
30
+
31
+ **[Live docs & demo → edocksil.github.io/claude-color](https://edocksil.github.io/claude-color/)**
32
+
33
+ Drive a wired **VIA-compatible keyboard's** RGB from Claude Code status — see at a glance
34
+ whether Claude is working, waiting for you, or idle, without looking at the terminal.
35
+
36
+ - **working** → blue-purple
37
+ - **waiting for you** (permission / input) → amber
38
+ - **idle** (turn done, session alive) → deep green
39
+ - **Claude Code exits / no session running** → your normal lighting is restored
40
+
41
+ ## Why claude-color
42
+
43
+ Built to be **correct under real conditions**, not just a demo:
44
+
45
+ - **Multi-session aware** — one keyboard, many Claude Code sessions, resolved by
46
+ priority (`waiting` > `working` > `idle`).
47
+ - **Survives crashes & sleep** — sessions are tracked by their Claude process;
48
+ dead ones are pruned, and a boot/logon/unlock reconcile restores your lighting
49
+ when no session is running.
50
+ - **Accurate status** — distinguishes a real permission prompt (amber) from a
51
+ finished-but-idle session (green) via Claude Code's `notification_type`.
52
+ - **Restores your lighting** — captures your real keyboard color and puts it back,
53
+ with a guard against capturing its own status colors.
54
+ - **Windows-first**, keyboard-generic (any wired VIA RGB keyboard), 85+ tests.
55
+
56
+ ## Requirements
57
+
58
+ - **Windows 11**, Python 3.11+ — installed and run via [uv](https://docs.astral.sh/uv/)
59
+ (no system Python needed).
60
+ - **Any wired keyboard exposing a VIA raw-HID interface with an RGB matrix**,
61
+ connected by **USB cable**. VIA support is most commonly provided by QMK
62
+ firmware with VIA enabled, but only the VIA protocol is required — not QMK
63
+ itself. Developed and validated on the **Keychron Q6 Max**. **Wireless is not
64
+ supported** (wired only). See [docs/COMPATIBILITY.md](docs/COMPATIBILITY.md).
65
+ - Optional but recommended: the [`qmk_hid`](https://github.com/FrameworkComputer/qmk_hid)
66
+ binary on your PATH (primary backend). Without it, the built-in Python `hidapi`
67
+ backend is used.
68
+
69
+ ## Install
70
+
71
+ ```powershell
72
+ uv run claude-color doctor # confirm the keyboard is detected
73
+ uv run claude-color test # cycle working/waiting/idle, then restore
74
+ uv run claude-color install # add hooks to %USERPROFILE%\.claude\settings.json
75
+ ```
76
+
77
+ To make `claude-color` a global command (so Claude Code can invoke the hooks from any
78
+ terminal), install it as a uv tool — `--editable` keeps it tracking this repo:
79
+
80
+ ```powershell
81
+ uv tool uninstall kbstatus # remove the old command (if previously installed)
82
+ uv tool install --editable . # exposes `claude-color` on PATH (~\.local\bin)
83
+ claude-color install # wire hooks (migrates any old kbstatus wiring)
84
+ ```
85
+
86
+ Ensure the uv tool bin dir is on PATH so Claude Code and the scheduled task can
87
+ invoke `claude-color`.
88
+
89
+ ## Demo
90
+
91
+ Run the color cycle any time to see (or show off) all three statuses. It cycles
92
+ **working → waiting → idle** and then restores your current lighting:
93
+
94
+ ```powershell
95
+ claude-color test # ~2s per color (quick check)
96
+ claude-color test --dwell 30 # hold each color 30s (slow demo)
97
+ ```
98
+
99
+ ## Configure
100
+
101
+ Edit `%LOCALAPPDATA%\claude-color\config.toml`:
102
+
103
+ - `backend` — `auto` (default), `qmk_hid`, or `hidapi`
104
+ - `solid_effect_index` — the VIA "solid color" effect index for your keymap. If
105
+ `claude-color test` shows an animation instead of a solid color, change this and
106
+ rerun `claude-color test`.
107
+ - `vid` / `pid` — hex, only needed if multiple HID devices match. Find yours with
108
+ `claude-color doctor` or `qmk_hid -l`.
109
+ - `[colors]` — H/S/V (0–255) per state
110
+ - `staleness_minutes` — a stuck session is forgotten after this (default 120)
111
+
112
+ ## How it works
113
+
114
+ ```mermaid
115
+ flowchart LR
116
+ CC["Claude Code hooks"] -->|"SessionStart · UserPromptSubmit · Notification · Stop · SessionEnd"| K["claude-color hook"]
117
+ K --> S["state.json<br/>(per session, by PID)"]
118
+ S -->|"winner: waiting &gt; working &gt; idle"| KB["keyboard RGB<br/>(VIA raw-HID)"]
119
+ R["claude-color reconcile<br/>(logon / unlock)"] --> S
120
+ ```
121
+
122
+ Claude Code fires hooks (`SessionStart`, `UserPromptSubmit`, `Notification`,
123
+ `Stop`, `SessionEnd`). Each runs `claude-color hook`, which updates a per-session
124
+ state file (`%LOCALAPPDATA%\claude-color\state.json`) and pushes the highest-priority
125
+ color (`waiting` > `working` > `idle`) across all live sessions. Sessions are
126
+ tracked by their Claude process id; dead ones are pruned, and a scheduled
127
+ `claude-color reconcile` (logon/unlock) restores your lighting when no Claude is
128
+ running. When the last session ends, the lighting captured at first
129
+ `SessionStart` is restored.
130
+
131
+ ## Troubleshooting
132
+
133
+ - **No color changes:** run `claude-color doctor`. If `available: False`, check the USB
134
+ cable (wireless is not supported) and try `backend = "hidapi"` or set `vid`/`pid`.
135
+ - **Animation instead of solid color:** fix `solid_effect_index`.
136
+ - **Colors never restore:** baseline capture may be unsupported on your firmware
137
+ via the active backend; try `backend = "hidapi"`. Logs are in
138
+ `%LOCALAPPDATA%\claude-color\claude-color.log`.
139
+ - **Uninstall:** `claude-color uninstall`.
140
+
141
+ ## License
142
+
143
+ [MIT](LICENSE) — © 2026 Edocksil.
@@ -0,0 +1,119 @@
1
+ # claude-color
2
+
3
+ ![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)
4
+ ![Python 3.11+](https://img.shields.io/badge/python-3.11%2B-blue.svg)
5
+ ![Platform: Windows](https://img.shields.io/badge/platform-Windows-blue.svg)
6
+
7
+ **[Live docs & demo → edocksil.github.io/claude-color](https://edocksil.github.io/claude-color/)**
8
+
9
+ Drive a wired **VIA-compatible keyboard's** RGB from Claude Code status — see at a glance
10
+ whether Claude is working, waiting for you, or idle, without looking at the terminal.
11
+
12
+ - **working** → blue-purple
13
+ - **waiting for you** (permission / input) → amber
14
+ - **idle** (turn done, session alive) → deep green
15
+ - **Claude Code exits / no session running** → your normal lighting is restored
16
+
17
+ ## Why claude-color
18
+
19
+ Built to be **correct under real conditions**, not just a demo:
20
+
21
+ - **Multi-session aware** — one keyboard, many Claude Code sessions, resolved by
22
+ priority (`waiting` > `working` > `idle`).
23
+ - **Survives crashes & sleep** — sessions are tracked by their Claude process;
24
+ dead ones are pruned, and a boot/logon/unlock reconcile restores your lighting
25
+ when no session is running.
26
+ - **Accurate status** — distinguishes a real permission prompt (amber) from a
27
+ finished-but-idle session (green) via Claude Code's `notification_type`.
28
+ - **Restores your lighting** — captures your real keyboard color and puts it back,
29
+ with a guard against capturing its own status colors.
30
+ - **Windows-first**, keyboard-generic (any wired VIA RGB keyboard), 85+ tests.
31
+
32
+ ## Requirements
33
+
34
+ - **Windows 11**, Python 3.11+ — installed and run via [uv](https://docs.astral.sh/uv/)
35
+ (no system Python needed).
36
+ - **Any wired keyboard exposing a VIA raw-HID interface with an RGB matrix**,
37
+ connected by **USB cable**. VIA support is most commonly provided by QMK
38
+ firmware with VIA enabled, but only the VIA protocol is required — not QMK
39
+ itself. Developed and validated on the **Keychron Q6 Max**. **Wireless is not
40
+ supported** (wired only). See [docs/COMPATIBILITY.md](docs/COMPATIBILITY.md).
41
+ - Optional but recommended: the [`qmk_hid`](https://github.com/FrameworkComputer/qmk_hid)
42
+ binary on your PATH (primary backend). Without it, the built-in Python `hidapi`
43
+ backend is used.
44
+
45
+ ## Install
46
+
47
+ ```powershell
48
+ uv run claude-color doctor # confirm the keyboard is detected
49
+ uv run claude-color test # cycle working/waiting/idle, then restore
50
+ uv run claude-color install # add hooks to %USERPROFILE%\.claude\settings.json
51
+ ```
52
+
53
+ To make `claude-color` a global command (so Claude Code can invoke the hooks from any
54
+ terminal), install it as a uv tool — `--editable` keeps it tracking this repo:
55
+
56
+ ```powershell
57
+ uv tool uninstall kbstatus # remove the old command (if previously installed)
58
+ uv tool install --editable . # exposes `claude-color` on PATH (~\.local\bin)
59
+ claude-color install # wire hooks (migrates any old kbstatus wiring)
60
+ ```
61
+
62
+ Ensure the uv tool bin dir is on PATH so Claude Code and the scheduled task can
63
+ invoke `claude-color`.
64
+
65
+ ## Demo
66
+
67
+ Run the color cycle any time to see (or show off) all three statuses. It cycles
68
+ **working → waiting → idle** and then restores your current lighting:
69
+
70
+ ```powershell
71
+ claude-color test # ~2s per color (quick check)
72
+ claude-color test --dwell 30 # hold each color 30s (slow demo)
73
+ ```
74
+
75
+ ## Configure
76
+
77
+ Edit `%LOCALAPPDATA%\claude-color\config.toml`:
78
+
79
+ - `backend` — `auto` (default), `qmk_hid`, or `hidapi`
80
+ - `solid_effect_index` — the VIA "solid color" effect index for your keymap. If
81
+ `claude-color test` shows an animation instead of a solid color, change this and
82
+ rerun `claude-color test`.
83
+ - `vid` / `pid` — hex, only needed if multiple HID devices match. Find yours with
84
+ `claude-color doctor` or `qmk_hid -l`.
85
+ - `[colors]` — H/S/V (0–255) per state
86
+ - `staleness_minutes` — a stuck session is forgotten after this (default 120)
87
+
88
+ ## How it works
89
+
90
+ ```mermaid
91
+ flowchart LR
92
+ CC["Claude Code hooks"] -->|"SessionStart · UserPromptSubmit · Notification · Stop · SessionEnd"| K["claude-color hook"]
93
+ K --> S["state.json<br/>(per session, by PID)"]
94
+ S -->|"winner: waiting &gt; working &gt; idle"| KB["keyboard RGB<br/>(VIA raw-HID)"]
95
+ R["claude-color reconcile<br/>(logon / unlock)"] --> S
96
+ ```
97
+
98
+ Claude Code fires hooks (`SessionStart`, `UserPromptSubmit`, `Notification`,
99
+ `Stop`, `SessionEnd`). Each runs `claude-color hook`, which updates a per-session
100
+ state file (`%LOCALAPPDATA%\claude-color\state.json`) and pushes the highest-priority
101
+ color (`waiting` > `working` > `idle`) across all live sessions. Sessions are
102
+ tracked by their Claude process id; dead ones are pruned, and a scheduled
103
+ `claude-color reconcile` (logon/unlock) restores your lighting when no Claude is
104
+ running. When the last session ends, the lighting captured at first
105
+ `SessionStart` is restored.
106
+
107
+ ## Troubleshooting
108
+
109
+ - **No color changes:** run `claude-color doctor`. If `available: False`, check the USB
110
+ cable (wireless is not supported) and try `backend = "hidapi"` or set `vid`/`pid`.
111
+ - **Animation instead of solid color:** fix `solid_effect_index`.
112
+ - **Colors never restore:** baseline capture may be unsupported on your firmware
113
+ via the active backend; try `backend = "hidapi"`. Logs are in
114
+ `%LOCALAPPDATA%\claude-color\claude-color.log`.
115
+ - **Uninstall:** `claude-color uninstall`.
116
+
117
+ ## License
118
+
119
+ [MIT](LICENSE) — © 2026 Edocksil.
@@ -0,0 +1,36 @@
1
+ [project]
2
+ name = "claude-color"
3
+ version = "0.1.0"
4
+ description = "Drive any wired VIA-compatible RGB keyboard from Claude Code status"
5
+ readme = "README.md"
6
+ requires-python = ">=3.11"
7
+ license = { text = "MIT" }
8
+ authors = [{ name = "Edocksil" }]
9
+ keywords = ["claude-code", "qmk", "via", "hid", "rgb", "keyboard", "hooks", "status"]
10
+ classifiers = [
11
+ "License :: OSI Approved :: MIT License",
12
+ "Programming Language :: Python :: 3.11",
13
+ "Programming Language :: Python :: 3.12",
14
+ "Environment :: Console",
15
+ "Operating System :: Microsoft :: Windows",
16
+ "Topic :: Utilities",
17
+ ]
18
+ dependencies = ["hidapi>=0.14"]
19
+
20
+ [project.optional-dependencies]
21
+ dev = ["pytest>=8"]
22
+
23
+ [project.scripts]
24
+ claude-color = "claude_color.cli:main"
25
+
26
+ [project.urls]
27
+ Homepage = "https://github.com/Edocksil/claude-color"
28
+ Repository = "https://github.com/Edocksil/claude-color"
29
+ Issues = "https://github.com/Edocksil/claude-color/issues"
30
+
31
+ [build-system]
32
+ requires = ["setuptools>=68"]
33
+ build-backend = "setuptools.build_meta"
34
+
35
+ [tool.setuptools.packages.find]
36
+ where = ["src"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1 @@
1
+ __version__ = "0.1.0"
@@ -0,0 +1,4 @@
1
+ from claude_color.cli import main
2
+
3
+ if __name__ == "__main__":
4
+ raise SystemExit(main())
@@ -0,0 +1,169 @@
1
+ import argparse
2
+ import json
3
+ import sys
4
+ import time
5
+
6
+ from claude_color import config, events, install, paths, procinfo, statestore
7
+ from claude_color.keyboard.factory import get_backend
8
+ from claude_color.keyboard.hidapi import list_via_devices
9
+ from claude_color.lock import DirLock
10
+ from claude_color.logging_setup import get_logger
11
+
12
+ log = get_logger()
13
+
14
+
15
+ def now() -> float:
16
+ return time.time()
17
+
18
+
19
+ def cmd_hook() -> int:
20
+ try:
21
+ raw = sys.stdin.read()
22
+ payload = json.loads(raw) if raw.strip() else {}
23
+ event_name = payload.get("hook_event_name")
24
+ session_id = payload.get("session_id") or "default"
25
+ if not event_name:
26
+ return 0
27
+ cfg = config.load_config()
28
+ keyboard = get_backend(cfg)
29
+ pid = procinfo.session_pid()
30
+ live = procinfo.live_claude_pids()
31
+ boot = procinfo.boot_time()
32
+ with DirLock(paths.lock_path()) as acquired:
33
+ if not acquired:
34
+ return 0
35
+ state = statestore.load_state(paths.state_path())
36
+ before = json.dumps(state, sort_keys=True)
37
+ events.handle_event(
38
+ event_name, session_id, now(), cfg, state, keyboard,
39
+ notification_type=payload.get("notification_type"),
40
+ pid=pid, live_pids=live, boot_time=boot,
41
+ )
42
+ after = json.dumps(state, sort_keys=True)
43
+ if before != after: # write-on-change
44
+ statestore.save_state(paths.state_path(), state)
45
+ except Exception as exc:
46
+ log.warning("hook error: %s", exc, exc_info=True)
47
+ return 0
48
+
49
+
50
+ def cmd_reconcile() -> int:
51
+ """Prune sessions whose Claude process has exited and restore the user's
52
+ lighting if none remain. Meant to run on boot/unlock (scheduled task) so the
53
+ keyboard recovers after ungraceful exits that never fired SessionEnd."""
54
+ try:
55
+ cfg = config.load_config()
56
+ keyboard = get_backend(cfg)
57
+ live = procinfo.live_claude_pids()
58
+ boot = procinfo.boot_time()
59
+ with DirLock(paths.lock_path()) as acquired:
60
+ if not acquired:
61
+ return 0
62
+ state = statestore.load_state(paths.state_path())
63
+ before = json.dumps(state, sort_keys=True)
64
+ events.reconcile(cfg, state, keyboard, live, now(), boot)
65
+ after = json.dumps(state, sort_keys=True)
66
+ if before != after:
67
+ statestore.save_state(paths.state_path(), state)
68
+ except Exception as exc:
69
+ log.warning("reconcile error: %s", exc, exc_info=True)
70
+ return 0
71
+
72
+
73
+ def _device_label(manufacturer: str, product: str) -> str:
74
+ """Human-readable device name, avoiding a doubled brand when the product
75
+ string already starts with the manufacturer (e.g. "Keychron" + "Keychron
76
+ Q6 Max" -> "Keychron Q6 Max")."""
77
+ m, p = (manufacturer or "").strip(), (product or "").strip()
78
+ if not m:
79
+ return p
80
+ if not p:
81
+ return m
82
+ if p.lower().startswith(m.lower()):
83
+ return p
84
+ return f"{m} {p}"
85
+
86
+
87
+ def cmd_doctor() -> int:
88
+ cfg = config.load_config()
89
+ keyboard = get_backend(cfg)
90
+ print(f"backend: {type(keyboard).__name__}")
91
+ print(f"available: {keyboard.available()}")
92
+ current = keyboard.get_hsv()
93
+ print(f"current lighting (baseline capture): {current}")
94
+ print(f"solid_effect_index (config): {cfg.get('solid_effect_index')}")
95
+ devices = list_via_devices()
96
+ if devices:
97
+ print("Detected VIA devices (usage page 0xFF60):")
98
+ for d in devices:
99
+ label = _device_label(d["manufacturer"], d["product"])
100
+ print(f" vid={d['vid']} pid={d['pid']} {label}".rstrip())
101
+ print("If the wrong device is picked, set vid/pid in config.toml.")
102
+ else:
103
+ print("No VIA devices detected (check the USB cable; wireless is not supported).")
104
+ print("If colors look wrong, adjust solid_effect_index in config.toml and rerun `claude-color test`.")
105
+ return 0
106
+
107
+
108
+ def cmd_test(dwell: float = 2.0) -> int:
109
+ """Demo/test: cycle working -> waiting -> idle on the keyboard, holding each
110
+ color `dwell` seconds, then restore the current lighting as the baseline."""
111
+ cfg = config.load_config()
112
+ keyboard = get_backend(cfg)
113
+ effect = cfg.get("solid_effect_index", 1)
114
+ baseline = keyboard.get_hsv()
115
+ for name in ("working", "waiting", "idle"):
116
+ h, s, v = cfg["colors"][name]
117
+ print(f"{name:>8}: hsv=({h},{s},{v}) (holding {dwell}s)", flush=True)
118
+ keyboard.set_hsv(effect, h, s, v)
119
+ time.sleep(dwell)
120
+ if baseline is not None:
121
+ print(f"restoring baseline: {baseline}", flush=True)
122
+ keyboard.set_hsv(baseline.effect, baseline.hue, baseline.sat, baseline.val)
123
+ else:
124
+ print("baseline unavailable (backend can't read current lighting) - left on idle", flush=True)
125
+ return 0
126
+
127
+
128
+ def cmd_status() -> int:
129
+ state = statestore.load_state(paths.state_path())
130
+ print(json.dumps(state, indent=2))
131
+ return 0
132
+
133
+
134
+ def main(argv: list[str] | None = None) -> int:
135
+ parser = argparse.ArgumentParser(prog="claude-color")
136
+ sub = parser.add_subparsers(dest="command", required=True)
137
+ sub.add_parser("hook", help="hook entrypoint (reads JSON on stdin)")
138
+ sub.add_parser("install", help="add hooks to Claude Code settings.json")
139
+ sub.add_parser("uninstall", help="remove hooks from Claude Code settings.json")
140
+ sub.add_parser("doctor", help="detect keyboard and report status")
141
+ test_parser = sub.add_parser(
142
+ "test", help="demo: cycle status colors on the keyboard, then restore"
143
+ )
144
+ test_parser.add_argument(
145
+ "--dwell", type=float, default=2.0,
146
+ help="seconds to hold each color (default: 2.0; e.g. 30 for a slow demo)",
147
+ )
148
+ sub.add_parser("status", help="print current state.json")
149
+ sub.add_parser(
150
+ "reconcile",
151
+ help="prune dead sessions and restore lighting if no Claude is running",
152
+ )
153
+
154
+ args = parser.parse_args(argv)
155
+ if args.command == "hook":
156
+ return cmd_hook()
157
+ if args.command == "reconcile":
158
+ return cmd_reconcile()
159
+ if args.command == "install":
160
+ return install.cmd_install()
161
+ if args.command == "uninstall":
162
+ return install.cmd_uninstall()
163
+ if args.command == "doctor":
164
+ return cmd_doctor()
165
+ if args.command == "test":
166
+ return cmd_test(args.dwell)
167
+ if args.command == "status":
168
+ return cmd_status()
169
+ return 1
@@ -0,0 +1,59 @@
1
+ import copy
2
+ import tomllib
3
+
4
+ from claude_color import paths
5
+
6
+ DEFAULTS: dict = {
7
+ "backend": "auto", # auto | qmk_hid | hidapi
8
+ "qmk_hid_path": "qmk_hid", # binary name or full path
9
+ "vid": "", # hex string, e.g. "3434"; "" = no filter
10
+ "pid": "", # hex string; "" = no filter
11
+ "solid_effect_index": 1, # VIA RGB-matrix "solid color" effect index
12
+ "staleness_minutes": 120,
13
+ "colors": {
14
+ "working": [180, 255, 200],
15
+ "waiting": [30, 255, 220],
16
+ "idle": [90, 255, 120],
17
+ },
18
+ }
19
+
20
+ _TEMPLATE = """\
21
+ # claude-color configuration
22
+ backend = "auto" # auto | qmk_hid | hidapi
23
+ qmk_hid_path = "qmk_hid" # binary name on PATH, or a full path
24
+ vid = "" # hex, e.g. "3434"; empty = no device filter
25
+ pid = "" # hex; empty = no device filter
26
+ solid_effect_index = 1 # VIA RGB-matrix solid-color effect index (confirm via `claude-color doctor`)
27
+ staleness_minutes = 120
28
+
29
+ [colors] # H, S, V on QMK 0-255 scale
30
+ working = [180, 255, 200]
31
+ waiting = [30, 255, 220]
32
+ idle = [90, 255, 120]
33
+ """
34
+
35
+
36
+ def _merge(base: dict, over: dict) -> dict:
37
+ out = copy.deepcopy(base)
38
+ for k, v in over.items():
39
+ if isinstance(v, dict) and isinstance(out.get(k), dict):
40
+ out[k] = _merge(out[k], v)
41
+ else:
42
+ out[k] = v
43
+ return out
44
+
45
+
46
+ def load_config() -> dict:
47
+ path = paths.config_path()
48
+ if not path.exists():
49
+ try:
50
+ path.write_text(_TEMPLATE, encoding="utf-8")
51
+ except Exception:
52
+ return copy.deepcopy(DEFAULTS)
53
+ return copy.deepcopy(DEFAULTS)
54
+ try:
55
+ with path.open("rb") as fh:
56
+ loaded = tomllib.load(fh)
57
+ return _merge(DEFAULTS, loaded)
58
+ except Exception:
59
+ return copy.deepcopy(DEFAULTS)