- otoji-tray: replace text title with SF Symbol mic icon
- otoji listen: idle CPU gate before RNNoise (76% → ~0%)
- macOS: "Launch at Login" tray toggle —
rs/adapters/macos/src/launch_at_login.rs(writes/removes~/Library/LaunchAgents/com.snomiao.capslockx.plist; enable only registers for next login, disable alsolaunchctl bootouts), checkbox item wired intray.rs/prefs.rs. Runsdist/CapsLockX-dev.app/Contents/MacOS/clx -f(branded .app identity for the Accessibility list) withORT_DYLIB_PATHset for ten-vad's dlopen; deliberately noKeepAlive(would fight clx's startup dedup). - Windows: "Launch at Login" tray toggle (mirror of the macOS feature above)
- Add a
CheckMenuItemBuilder-based checkbox item to the tray menu built inrs/adapters/windows/src/main.rs(alongside the existingprefs/config_dir/quititems around line 234-246), wired viaon_menu_event. - Backing store: a new module (e.g.
rs/adapters/windows/src/launch_at_login.rs) that writes/removes a value under the per-user registry keyHKCU\Software\Microsoft\Windows\CurrentVersion\Run(nameCapsLockX, value = path to the exe, quoted). This is the standard lightweight mechanism for per-user autostart — no admin rights, no Task Scheduler XML. Use thewindowscrate (already a dependency here) viaRegSetValueExW/RegDeleteValueWonHKEY_CURRENT_USER, or pull in theauto-launchcrate if it simplifies things. is_enabled()= registry value exists and points at the current exe path (like the mac version, treat the registry as the single source of truth — no separate config flag to desync).- Note: unlike macOS, Windows Run-key autostart just launches the exe directly at login — there's no separate watchdog/crash-restart wrapper on this platform to route through, so
Program= current exe path with no wrapper args.
- Add a
- otoji-tray: extract
objc-helpers.rsshared with CLXtray.rs(~150 lines duplicated) - CLX↔otoji-tray state channel: drop CLX's own tray, otoji-tray reflects CLX mode/PTT via state file
CapsLockX 的核心理念是:简化系统操作逻辑,提升操作效率,且不与原有习惯键位冲突。
- 按 CapsLockX + - 键显示对应帮助(目前的显示样式相当草率)
- i18n (eh 这个真得有)
- 自动更新(虽然 git pull 一下也不是不行)
- 初次使用上手教程(这个现在有点简陋……)
- 插件管理器(虽然文件系统也可以搞定)
- 自动配置同步功能(虽然一般来说扔 onedrive 就够)
- 易用的选项配置的 UI 界面(虽然改 ini 也不是什么难事)
- 执行外部代码(Python、Nodejs、外部 AHK、Bash、……)(虽然写个脚本 run 一下也并不算麻烦)
Problem: CapsLock+Shift+HJKL should select text (Shift+Arrow) but only moves cursor.
Root Cause: AccModel callback fires on background thread (clx-acc-ticker). SendInput from this thread causes the OS to insert phantom modifier key-up events between injected events, cancelling the Shift state before Arrow keys arrive.
Approaches tried & results:
| # | Approach | Result |
|---|---|---|
| 1 | Plain key_tap(Arrow) relying on held Shift |
No selection — phantom Shift UP from OS cancels Shift before Arrow arrives |
| 2 | Separate key_down(Shift) + key_tap(Arrow) |
Phantom Shift UP between separate SendInput calls |
| 3 | Atomic key_tap_n_with_mod (single SendInput batch) |
Phantoms still within batch; works on 2nd+ ticks but not 1st |
| 4 | Suppress phantom Shift UP in hook (GetAsyncKeyState) | Fixes selection but causes stuck Shift — our injected Shift keeps GetAsyncKeyState returning "down", blocking real releases |
| 5 | GetAsyncKeyState for shift detection in cursor_action |
Correctly detects shift, doesn't fix OS-level Shift state for arrows |
| 6 | Skip modifier injection if already held (GetAsyncKeyState check in key_tap_n_with_mod) | Phantom UP still appears after AHK's own Shift injection — not caused by our code |
Key finding: The phantom Shift UP (inj=false extra=0x0) appears immediately after ANY injected Shift DN — whether from our code or AHK. This is an OS-level behavior, possibly related to how WH_KEYBOARD_LL hooks interact with injected modifier keys. The phantom cancels the Shift state at the OS level before our Arrow key arrives.
Next steps:
- Try
PostMessage/SendMessageWM_KEYDOWN to target window with shift flag in lParam (bypasses SendInput entirely) - Try scan-code based injection (
KEYEVENTF_SCANCODEflag) - Try injecting on the hook thread (not the AccModel ticker thread) — use a message queue to marshal calls
- Check if the AHK version has the same phantom issue (it works, so how does it avoid it?)
E2E test infra: rs/test-shift-select.ahk creates its own AHK Gui Edit control (no Notepad dependency)
E2E test results (5/8 pass):
- PASS: CLX+L, CLX+H, CLX+K, CLX+J (plain cursor movement), Shift-not-stuck
- FAIL: CLX+Shift+L, Shift+CLX+L, CLX+Shift+H (selection)
- AHK + Rust: windows cycle within monitor -> next monitor -> virtual desktop switch
get_app_windows()sorts by(monitor_index, hwnd)for stable monitor-grouped ordering
--with-ahkflag required to spawn AHK module loader (default: Rust-only)
request_admin: truein%APPDATA%\CapsLockX\config.jsontriggers UAC elevation on startup
Plan: plan/voice-input/README.md | plan/voice-input/TODO.md
- CLX+V click = toggle continuous listening; CLX+V hold = listen while held
- VAD splits audio into utterances, sends chunks to server incrementally
- 3-stage streaming pipeline: local Whisper (fast draft) → server Whisper (refined) → LLM typo-fix (polished)
- Text typed at cursor, each stage replaces previous in-place
- Server: new endpoint on brainstorm.snomiao.com combining Whisper + LLM
- Client:
cpalfor cross-platform audio,webrtc-vadfor voice detection - Phases: hotkey → audio capture → VAD → server endpoint → HTTP client → local Whisper (optional)
Problem: Space+E (left click hold) + WASD to drag a window only moves the window when E is released, instead of moving it continuously like a trackpad drag.
Cause: mouse_move sends CGEventType::MouseMoved but while a button is held, macOS expects CGEventType::LeftMouseDragged events instead. Need to track button state and switch event type accordingly.