Phase 4: radial Actions Ring overlay#3
Merged
ChristopherLandaverde merged 34 commits intomainfrom Apr 27, 2026
Merged
Conversation
Brainstorm output for the radial Actions Ring overlay (PyQt6, X11-only v1). Covers trigger model (hold-to-show / release-to-fire), variable segment count per ring (3-12, default 8), polymorphic binding targets (action:NAME | ring:NAME) with backward compat for the legacy action = "..." form, and a worker-thread evdev listener feeding a Qt main thread that owns the overlay widget. Phase 3 concerns (per-app profiles, new action types) and Wayland support explicitly deferred. Icons accepted in schema, not rendered in v1. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
26 bite-sized tasks following TDD per-cycle commits, mirroring Phase 2's discipline. Covers schema migration (polymorphic targets + legacy shim), read_loop key-up extension, three pure-geometry units, RingWidget + CursorPoller + RingController, and the worker-thread/Qt-main-thread split in cli/listen.py. PyQt6 stays an optional [ring] extra; command-only configs run on the existing non-Qt path. Phase 2's 35 tests must remain green at every commit. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… 'action = ...' form
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… active wedge highlight
Code quality reviewer flagged the local `import math as _m` inside paintEvent — Python caches module lookups but it's a smell. Move to module top and use math.radians for the degree→radian conversion. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…g in dispatch_event Quality reviewer flagged an unused InputEvent import and an unnecessarily defensive getattr() chain on run_action's result (ActionResult always has .ok and .detail; the None/getattr guards added nothing). 106 tests still pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ests Phase 4 introduces requires_display tests (RingWidget, CursorPoller, Qt listener smoke). The conftest hook skips them when DISPLAY is unset; in CI we provide one via xvfb-run. Pulls in [ring] extra for PyQt6. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The Phase 4 ring controller never opened on real hardware. Diagnosis: the worker-thread evdev signal was connected to a free Python function, which PyQt6 routed via DirectConnection. That meant ring_controller.open() — and the QWidget calls inside it — ran on the worker thread. Qt silently no-ops most GUI calls from a non-main thread, so the ring widget never appeared even though dispatch was reaching the controller. Fix: introduce _MainBridge(QObject) with a @pyqtSlot decorator, parented to the main thread, and connect with explicit Qt.QueuedConnection. Also move QCursor.pos() out of the worker thread (queried in the main-thread slot now). Also adds INFO logs for ring open/close so the next time something silently fails, the listener log shows whether dispatch is even reaching the controller. Verified end-to-end on real Logitech MX hardware (event25): ring opens on key-down, follows cursor, fires the highlighted segment on key-up, cancels on release-in-deadzone. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…isibility On real hardware (Pop!_OS / GNOME / Mutter), enabling WA_TranslucentBackground combined with translucent BG colors caused the entire ring widget to render as an invisible black square — paintEvent ran, geometry was right, but nothing was visible on screen. Disable WA_TranslucentBackground in v1 and ship with opaque colors: - BG_COLOR = rgb(40, 40, 40) - ACTIVE_BG_COLOR = rgb(80, 80, 80) - DEAD_ZONE_COLOR = rgb(20, 20, 20) — extracted as a named constant instead of an inline magic QColor inside paintEvent Window-level fade-in via setWindowOpacity stays — that uses the WM composite path and works regardless of WA_TranslucentBackground. Theming + opt-in transparency was already deferred to a polish phase in spec §2 / §10. Updated test_widget_can_be_constructed_and_shown to assert the new attribute state and explain why. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Helper for diagnosing which evdev codes a given input device emits when buttons are pressed. Used during the Phase 4 hardware test to confirm that this MX hardware (event25, "Logitech USB Receiver Mouse") does NOT emit BTN_TASK on the gesture button — it only exposes BTN_LEFT/RIGHT/MIDDLE, BTN_BACK/SIDE/EXTRA/FORWARD. That clears the Phase 2 carry-forward note about BTN_TASK and explains why ring bindings on this MX must use one of the thumb codes (with the dual-fire trade-off vs browser-back) until device grabbing lands in a later phase. Run: ./scripts/dump-keys.py /dev/input/eventNN Also adds venv/ to .gitignore (the project venv is .venv/; venv/ was created accidentally). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Hardware test on real MX revealed BTN_TASK is advertised by the kernel but never fires on at least some MX models — Logitech routes the gesture button through proprietary HID reports the kernel doesn't translate. Example config now points users at scripts/dump-keys.py to find out what their MX actually emits, with a note on BTN_SIDE/BTN_EXTRA fallback and the dual-fire trade-off vs browser back/forward. Also flags gnome-screenshot as a non-portable default — the example keeps it (most common Linux desktop) but tells users to swap for flameshot/scrot/notify-send when it's not installed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Earned during the Phase 4 hardware test ("let's do brazil colors"). Ships
two named themes in a `_THEMES` dict at module load:
- dark (default) — opaque grays, the production v1 palette
- brazil — bandeira green / yellow dead zone / blue active wedge
Picked via LOGITECHMOUSE_THEME=<name>; unknown names fall back to dark
without raising. Themes are baked at import time (intentional — no per-event
overhead). Full TOML-driven theming stays a polish-phase item per spec §2/§10;
this is a tasteful escape hatch users can grab without editing code.
3 new requires_display tests verify default, brazil swap, and unknown
fallback. Full suite: 110 passed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…endering Without WA_TranslucentBackground, the 368x368 widget rectangle was being drawn opaque — the colored ring filled the inscribed circle, but the four corner triangles showed up as a black/system-bg square framing the ring. Setting QRegion.Ellipse as the widget mask tells the WM only the circular area is part of the widget; the corners aren't drawn at all and the desktop shows through. Pairs cleanly with the WA_TranslucentBackground= off decision from a2c7ec5. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
PyQt6 6.11 (which the Ubuntu runner pulls in) ships Qt 6.11; the xcb platform plugin has required libxcb-cursor since Qt 6.5. Without it, QApplication() aborts the process with no error message — which is what the pytest (3.12) job hit after the 38 non-Qt tests passed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Binding.actionwith polymorphicBinding.target = "ring:NAME" | "action:NAME". Legacyaction = "..."form preserved with a one-line deprecation log.device.read_loopto emit both key-down and key-up viaInputEvent.pressed.overlay/package: pure geometry (wedge_index,is_in_dead_zone,shifted_center_for_screen),RingWidget(frameless top-level, paint),CursorPoller(8ms QTimer),RingControllerstate machine.Qt.QueuedConnectionto a_MainBridgeslot for safe GUI dispatch).[ring]is an optional extra; PyQt6 missing + ring binding configured is caught atcheck-config.xvfb-run.scripts/dump-keys.pyfor mapping evdev codes on Logitech hardware.Spec:
docs/superpowers/specs/2026-04-26-phase4-actions-ring-design.mdPlan:
docs/superpowers/plans/2026-04-26-phase4-actions-ring.mdCaveats found during hardware test (documented in commit messages + example config)
scripts/dump-keys.py.WA_TranslucentBackgroundinteracts badly with Mutter (rendered as invisible black square) — disabled in v1; transparency moves to a future theming phase.Test plan
🤖 Generated with Claude Code