Skip to content

About

Caps Lock key LED as a live status indicator for the Qoder CLI agent (fork of insolowetrust/capslight)

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

17 Commits

Folders and files

Repository files navigation

capslight (Qoder CLI port)

English · 简体中文

The green LED on your Caps Lock key as a live status indicator for the Qoder CLI agent.

It blinks while the agent is working, goes solid when the turn is done, and blinks fast when it is waiting on you. It changes nothing about how your keyboard behaves — Caps Lock stays off and your typing stays lowercase.

State LED When
working slow blink (0.7s cycle, short flashes) the agent is thinking / running tools
waiting fast blink (0.22s) it needs you: permission prompt, question
done solid on turn finished
off dark session closed

A fork of insolowetrust/capslight, which targets Claude Code. Qoder's hook events differ, so the mapping was rebuilt — see Event mapping.

How it works

bin/capsled is a small IOKit utility in Swift. It has two ways to light the LED:

  1. hid (default) — writes straight to the keyboard's HID LED element (kHIDPage_LEDs / kHIDUsage_LED_CapsLock). It does not touch the modifier state, so your typing stays lowercase while the light blinks. That's the whole reason not to use the simpler approach below.
  2. modifier — IOHIDSetModifierLockState, kept as a fallback. More reliable, but it genuinely turns Caps Lock on, so text would jump to CAPS every time the LED flashes.

To see what your hardware supports: make probe. On a MacBook's built-in keyboard both work, with no root and no Input Monitoring permission.

bin/caps-indicator is a state machine on top of capsled. It owns the background blink process and its PID, and tracks state per session in ~/.qoder/capsled/sessions/. With several CLI windows open, the LED shows the highest-priority state (waiting > working > done > off) — so one session finishing won't switch off the indicator for another that's still running.

Install

All you need is the Xcode Command Line Tools (swiftc, python3). No other dependencies.

git clone https://github.com/AyaseEli-Bing/capslight-qoder.git
cd capslight-qoder
./install.sh

install.sh checks prerequisites, builds the binary, probes which backend your hardware supports, symlinks into ~/.qoder/bin/, and wires up the hooks. Pass --no-test to skip the visual test at the end.

Your existing hooks in ~/.qoder/settings.json are left alone — this adds its own separate entries. A backup goes to ~/.qoder/settings.json.bak-capslight before the first change. Re-running is safe: it replaces its own entries rather than duplicating them.

make install does the same thing without the checks and the test.

The hooks are picked up after you restart your Qoder session.

Event mapping

Hook State
UserPromptSubmit, PreToolUse, PostToolUse, PostToolUseFailure, SubagentStart, SubagentStop working
PreToolUse when the tool is AskUserQuestion waiting
PermissionRequest waiting
Stop done
SessionEnd off

Qoder has no Notification event, so nothing has to guess whether a notification was a real permission prompt or just an idle reminder — PermissionRequest reports that directly. Subagent and failed-tool events stay on working because the turn is still running through them.

The AskUserQuestion case is read out of the hook payload rather than wired as a separate hook: in a session that auto-approves tool calls, PermissionRequest never fires, and the agent waiting on your answer would otherwise look identical to it thinking.

Checking it

make test          # walks through every state with pauses — watch your keyboard
make probe         # which backend is available
bin/capsled on     # light it manually
bin/capsled off
bin/caps-indicator reset   # panic button: kill the blinker, turn the LED off

If nothing happens, start with make probe. If both backends come back unavailable, that keyboard won't hand over its LED (happens on some third-party Bluetooth models).

If the LED lights up but shows the wrong thing, turn on the breadcrumb log and watch what the hooks actually send:

touch ~/.qoder/capsled/debug          # start logging
tail -f ~/.qoder/capsled/debug.log    # one line per hook: argument, event, payload
rm ~/.qoder/capsled/debug             # stop (the log file stays until you delete it)

CAPSLIGHT_DEBUG=1 does the same for a single run.

Choosing which keyboard lights up

By default every HID keyboard on the machine is driven, external ones included. Limit that with a persisted switch — it survives restarts and applies to all sessions at once:

caps-indicator scope internal   # only the built-in keyboard
caps-indicator scope external   # only external keyboards
caps-indicator scope all        # every keyboard (default)
caps-indicator status           # show the current scope

To override for one run instead, set CAPS_KEYBOARD=internal|external|all in the environment the CLI is launched from.

A keyboard counts as built-in when its IOKit Transport is FIFO, which is how Apple Silicon reports the internal keyboard/trackpad; anything else (USB, Bluetooth) counts as external. Check what your machine has, and whether a device can be lit at all:

bin/capsled list
use  transport=FIFO product=Apple Internal Keyboard / Trackpad capsLockLEDs=1
use  transport=USB product=USB Receiver capsLockLEDs=1
use  transport=USB product=G502 HERO Gaming Mouse capsLockLEDs=0
use  transport=Virtual product=TouchBarUserDevice capsLockLEDs=0

capsLockLEDs=0 means the device exposes no Caps Lock LED element, so writes to it change nothing. Scope applies only to the hid backend: the modifier fallback goes through IOHIDSystem and cannot target one device.

Tuning the blink

Via environment variables (set them in ~/.qoder/settings.json under env):

CAPS_WORKING_INTERVAL=0.7   CAPS_WORKING_DUTY=0.35
CAPS_WAITING_INTERVAL=0.22  CAPS_WAITING_DUTY=0.5
CAPS_IDLE_SECONDS=90        CAPS_BLINK_MAX_SECONDS=900  CAPS_SESSION_TTL_MINUTES=10

DUTY is the fraction of each cycle the LED stays lit. 0.35 gives short flashes (less distracting in peripheral vision), 0.5 gives an even blink.

To force a backend: bin/capsled --backend modifier on.

Turning it off

Sometimes you don't want a blinking keyboard — a screen recording, a meeting, a demo. No need to uninstall:

caps-indicator disable   # LED goes dark and stays dark
caps-indicator enable    # back to normal on the next hook
caps-indicator status    # muted or active, and the current state

The hooks stay wired; they just return immediately and do nothing. The switch is a flag file at ~/.qoder/capsled/disabled, so it survives restarts and applies to every session at once. make disable / make enable / make status do the same from the checkout.

To mute a single session instead of all of them, launch the CLI with CAPSLIGHT_DISABLED=1 in its environment.

caps-indicator reset keeps working while muted — it's the way out if the LED ever gets stuck.

Uninstall

make uninstall

Turns the LED off, removes the hooks from ~/.qoder/settings.json, deletes the symlinks and clears the state directory. Your original settings are still in ~/.qoder/settings.json.bak-capslight.

What it touches

Worth knowing before you run a script that edits your config:

  • ~/.qoder/settings.json — adds one command hook per event listed above, nothing else. A pristine copy is kept as .bak-capslight before the first change, and writes go through a temp file plus atomic rename, so an interrupted run can't truncate your settings. If the file isn't valid JSON, the installer says so and changes nothing.
  • ~/.qoder/bin/ — two symlinks pointing back at this checkout.
  • ~/.qoder/capsled/ — the blinker's PID, per-session state and the mute flag.

Nothing runs as root, nothing is downloaded, nothing phones home.

Known limitations

  • Pressing the real Caps Lock key mid-blink makes the system overwrite the LED state. The next flash puts it back, but you may see one beat go out of step.
  • There is only one LED for the whole machine, so a session that ends abruptly can pin it. Three causes, three reclamation rules, all run on the next hook from any session:
    • The host process died → the pid stored with each record fails kill -0, record dropped.
    • A turn was stopped with the stop button → it emits neither Stop nor SessionEnd while the app stays alive. Its transcript then goes quiet, so a record whose transcript has not changed for CAPS_IDLE_SECONDS (90s) and whose last conversation entry is still the user's own prompt is dropped.
    • Owner pid unresolvable → the record ages out after CAPS_SESSION_TTL_MINUTES (10 min). One hole is left: stopping during a long tool call leaves an assistant entry as the last record, so only the blinker's own CAPS_BLINK_MAX_SECONDS (15 min) clears it. Run bin/caps-indicator reset to clear it immediately.
  • External keyboards are driven too — the command goes to every HID keyboard at once.

Credits

Upstream author: Max Solo — insolowetrust/capslight.

MIT. Do whatever you like with it.

About

Caps Lock key LED as a live status indicator for the Qoder CLI agent (fork of insolowetrust/capslight)

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages