Repository navigation
Add support for Wayland by polling /dev/input - #12
Open
FichteFoll wants to merge 21 commits into
Open
FichteFoll wants to merge 21 commits into
FichteFoll wants to merge 21 commits into
Conversation
Native Wayland clients cannot query the global keyboard state, so Ctrl-release detection needs another source. `tabstack-keystate` reads the Ctrl state from `/dev/input/event*` with `EVIOCGKEY` and reports changes on stdout using protocol version 1. It is meant to be installed with `cap_dac_read_search=ep`, sets `PR_SET_NO_NEW_PRIVS` and drops all capabilities right after opening the devices. The new `keystate-helper` CI job builds it with `-Werror` and runs `tests/keystate_smoke.py` against a virtual `uinput` keyboard, with and without the capability. `tests/` is excluded from package exports. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add `HelperCtrlReleasePoller`, which spawns the helper once per session and reads protocol version 1 from its stdout. A wrong handshake, an `error` line, EOF or a `0` line fire the release exactly once, and never after `stop()`. Ctrl counts as held until the helper reports its first state, so a slow helper start does not drop the session. `probe_helper()` maps the helper's output to a `HelperStatus`. `plugin_loaded` runs it on the async thread via `refresh_availability()`, because it can take up to 2 s. Until the result arrives, Linux behaves as if the helper were missing. The probe records the helper path it ran with, sessions run exactly that path, and a change of `keystate_helper_path` triggers a new probe. On Linux, `create_poller` prefers the helper whenever its probe is `OK` and falls back to X11 otherwise. X11 is no longer considered available on native Wayland, where XWayland reports Ctrl as released while Sublime has focus. When Ctrl turns out to be released by the time the panel would open, the pending session is now committed like a quick press instead of being dropped. The helper path is configurable via `keystate_helper_path` in the new `TabStack.sublime-settings`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add the `tab_stack_setup_keystate_helper` command, available in the Command Palette as `TabStack: Set Up Ctrl Detection for Wayland`. It re-probes the helper in the background and reports success, or extracts the helper source to the cache directory, opens it for review, and opens a read-only view with the build, install, `setcap` and uninstall commands and the security implications. The plugin never runs any of these commands. On native Wayland without a working helper, the first `ctrl+tab` per plugin load offers the setup in a dialog instead of falling back to `next_view_in_stack`. "Don't Ask Again" persists `show_wayland_setup_dialog: false`. While the background probe is still running, the dialog is not offered. `plugin.ctrl_release` now exports a platform-neutral `is_native_wayland` that delegates to the Linux implementation. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Explain that Linux uses the `tabstack-keystate` helper when it works and falls back to X11 otherwise, how to set the helper up with the `TabStack: Set Up Ctrl Detection for Wayland` command, and the build, install, grant and uninstall commands. Also document the `keystate_helper_path` and `show_wayland_setup_dialog` settings, the security implications of `cap_dac_read_search` and of `input` group membership, and that Flatpak and Snap builds are unsupported. `AGENTS.md` gains the setup command, the dialog and how the helper is built and tested. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`plugin/ctrl_release/linux/` now holds one module per backend: `x11.py` for the X11 poller and `keystate_helper.py` for the helper. Its `__init__.py` keeps the backend selection (`create_poller`, `probe`) and `is_native_wayland`. No behavior changes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`sublime-package.json` contributes a schema for `TabStack.sublime-settings`, so LSP-json offers completions, hover documentation and validation when editing the settings. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`Preferences: TabStack Settings` opens the default and user settings side by side via `edit_settings`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The menu mirrors the Command Palette entries: opening the settings and setting up Ctrl detection for Wayland. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sublime Text imports packages from `Packages/`, so `main.py` is loaded as `TabStack.main`. Point ty's first-party root at the parent directory so that `from .plugin import *` resolves as it does at runtime. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Require `from __future__ import annotations` in every Python file through Ruff's isort `required-imports` and stop ignoring `UP045`. Annotations are no longer evaluated at runtime, so `Optional[X]` is rewritten to `X | None` despite the Python 3.8 target. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Re-lock with `uv lock --upgrade`: `ruff` 0.15.17 to 0.17.0, `basedpyright` 1.39.9 to 1.39.10 and `nodejs-wheel-binaries` 24.16.0 to 24.19.0. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The setup command now selects the instructions view and the extracted `tabstack-keystate.c` together, so Sublime Text shows them side by side with the instructions on the left. The instructions view is moved in front of the source tab, since the source may already be open, possibly in another group. Multi-sheet selection exists since build 4083, below the package's minimum of 4100, so no new-window fallback is needed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Running the setup command repeatedly used to open another instructions view each time. The view is now marked with the `tab_stack_setup_instructions` setting, so later runs locate it in any window, replace its contents via `tab_stack_replace_contents`, and show it next to the source again. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Closing an evdev device waits for an RCU grace period, so opening and closing every non-keyboard device delayed the initial state report by about 250 ms on a machine with 19 input devices. A Ctrl release was only noticed after that, which postponed the tab switch. The helper now reads each device's key capabilities from `/sys/class/input/<name>/device/capabilities/key` and opens only the devices with a Ctrl key, falling back to opening a device when sysfs cannot be read. A full run now takes about 26 ms instead of 258 ms. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The helper poller assumed Ctrl held until the helper reported the initial state. When that took longer than the panel delay, a quick Ctrl+Tab showed the panel and closed it again right away. `is_ctrl_down` now returns `None` until the state is known, and the pending session keeps waiting for it, up to one second, before it opens the panel. A release reported meanwhile commits the session directly. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`actions/setup-python@v5` still runs on Node 20, which triggers a deprecation warning. Update all actions to their latest releases, which run on Node 24, and pin them by commit SHA with the version as a comment. Add a Dependabot configuration that updates all actions weekly in a single grouped pull request. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Contributor
There was a problem hiding this comment.
🟡 Changes recommended
Session callback races, stale helper availability, and modifier-remapping behavior remain unresolved.
3 open findings
What changed in this PR
Adds Linux Ctrl-release detection for Wayland through a user-installed helper, integrated with TabStack’s existing session handling.
Changes:
- Adds an evdev helper, asynchronous probing, and X11 fallback selection.
- Provides setup instructions, settings, and menu commands.
- Adds helper smoke tests and CI coverage; updates annotations and development tooling.
| File | Description |
|---|---|
| uv.lock | Updates development dependencies. |
| tests/keystate_smoke.py | Tests helper output and capability handling. |
| TabStack.sublime-settings | Defines helper settings. |
| sublime-package.json | Adds settings schema. |
| README.md | Documents setup, security, and limitations. |
| pyproject.toml | Updates annotation and tooling configuration. |
| plugin/state.py | Extends the poller interface. |
| plugin/sheets.py | Modernizes optional annotations. |
| plugin/setup.py | Implements guided helper setup. |
| plugin/session.py | Handles delayed initial Ctrl state. |
| plugin/mru.py | Modernizes optional annotations. |
| plugin/models.py | Modernizes model annotations. |
| plugin/listener.py | Modernizes listener annotations. |
| plugin/history.py | Modernizes history annotations. |
| plugin/ctrl_release/windows.py | Modernizes Windows annotations. |
| plugin/ctrl_release/macos.py | Modernizes macOS annotations. |
| plugin/ctrl_release/linux/x11.py | Relocates and renames X11 detection. |
| plugin/ctrl_release/linux/keystate_helper.py | Probes and manages helper processes. |
| plugin/ctrl_release/linux/__init__.py | Selects Linux detection backend. |
| plugin/ctrl_release/_availability.py | Modernizes cached-state annotation. |
| plugin/ctrl_release/__init__.py | Coordinates probing and settings changes. |
| plugin/constants.py | Adds postponed annotations. |
| plugin/commands.py | Offers Wayland setup when unavailable. |
| plugin/__init__.py | Exports setup and initializes detection. |
| Main.sublime-menu | Adds settings and setup menus. |
| main.py | Adds postponed annotations. |
| keystate/tabstack-keystate.c | Implements capability-limited evdev helper. |
| Default.sublime-commands | Adds settings and setup commands. |
| AGENTS.md | Documents helper architecture and verification. |
| .github/workflows/ci.yml | Adds helper CI and pins actions. |
| .github/dependabot.yml | Enables grouped action updates. |
| .gitattributes | Excludes tests from package exports. |
🧠 Review effort: Balanced
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
A poller queues its release callback on the main thread. If the panel timer commits the session first and Ctrl+Tab starts a new one before the queued callback runs, the stale callback would commit the new session. Act only while the reporting poller is still the session's. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The helper was only probed on plugin load and on a change of `keystate_helper_path`. A helper that stopped working afterwards, for example after a reinstall dropped its file capability, stayed cached as available, so every later Ctrl+Tab committed immediately. A failing poller now invalidates the probe it was created from, unless a newer probe superseded it, which refreshes the availability. Later sessions then fall back to X11 or offer the setup. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The helper only watched the physical Ctrl keys, so with XKB options such as `ctrl:nocaps` or `ctrl:swapcaps` it reported Ctrl as released while the remapped key was still held, and every session committed at once. The helper now takes the keys that count as Ctrl as arguments. They default to both Ctrl keys and are restricted to the modifiers that the XKB `ctrl:*` options remap. The helper runs with its capability for any local user, so accepting arbitrary keys would turn it into a keylogger. Bump the protocol to version 2, so that an older helper, which ignores the arguments, is reported as outdated. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Pass the keys that count as Ctrl to the helper, so that layouts which remap Ctrl, such as `ctrl:nocaps` or `ctrl:swapcaps`, keep the panel open while the remapped key is held. The probe records the keys along with the path, the poller runs exactly the probed command, and a change of either setting triggers a new probe. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Only the setup dialog surfaced a helper that is installed but does not work, for example an outdated one after a package update, and only on native Wayland. Under X11 the fallback took over silently. Print the probe result for any failure other than a missing helper, which is the normal state without a setup. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
/dev/input*/dev/input
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.


Add a separate self-compilable helper that polls
/dev/inputfor keyboard events on Linux. Using this helper tool with elevated permissions allows to read the input events directly, granting us a side channel to poll for the ctrl release event. It also works on X11 and is preferred if configured.A "setup wizard" guides the user through the process, previews the C file we are compiling and also verifies that the setup works.
Some tooling cleanups and action updates are also included.
Closes #10.