- This is a Sublime Text package, not a standalone app.
- Runtime target is Sublime Text's Python 3.14 plugin host.
main.pyis the only entry file; it clears cachedplugin.*modules before star-importingplugin.- Keep exported plugin symbols in
plugin/__init__.py; implementation lives underplugin/.
- State is in-memory per window, keyed by
window.id()inplugin/state.py. - MRU order is per window.
TabStackListener.on_activatedupdates history unless a quick-panel session is active.TabStackListener.on_closeprunes closed sheets and removes empty window state.- Tab captions come from
plugin/captions.py; widgets, panels, and transient/non-tab views are excluded from MRU. - Ctrl-release detection lives in
plugin/ctrl_release/. - Under Linux, Ctrl-release detection uses the
tabstack-keystatehelper (plugin/ctrl_release/linux/keystate_helper.py) when its probe succeeds.plugin_loadedprobes it on the async thread, because running it can take up to 2 s; until the result arrives, Linux behaves as if the helper were missing, but without the setup dialog. The probe records the helper path and thekeystate_ctrl_keysit ran with, the poller runs exactly that command, and a change of either setting triggers a new probe. The helper only accepts the modifier names in itsallowed_keystable as arguments, so that it cannot serve as a keylogger; keep it that way. A helper that fails during a session (spawn error, bad handshake,errorline, early EOF) still commits that session, but triggers a new probe unless a newer one has superseded it. The helper reads evdev key state withcap_dac_read_search, is spawned once per session, and reports Ctrl state changes on stdout. - Without a working helper, Linux falls back to the X11 key state API (
plugin/ctrl_release/linux/x11.py), but not on native Wayland (is_native_wayland()inplugin/ctrl_release/linux/__init__.py), because XWayland reports Ctrl as released while a Wayland client has focus; Sublime exposes no key-release API. - On native Wayland without a working helper, the first
ctrl+tabper plugin load shows a dialog offering the setup (offer_wayland_setupinplugin/setup.py), unlessshow_wayland_setup_dialogisfalse. tab_stack_setup_keystate_helper(Command Palette:TabStack: Set Up Ctrl Detection for Wayland) re-probes, extractskeystate/tabstack-keystate.ctosublime.cache_path()/TabStack/keystate/, and shows the build, install andsetcapcommands. It reuses an open instructions view (marked by thetab_stack_setup_instructionsview setting) and selects it together with the source, instructions on the left. The plugin never runssudo, a compiler orsetcap; the user installs the helper tokeystate_helper_path(default/usr/local/libexec/tabstack-keystate) withcap_dac_read_search, which the helper drops right after opening the input devices.tab_stack_openarms a pending session, starts a Ctrl-release poller, and opens the quick panel after a short delay unless Ctrl is released first.- Highlighting a quick-panel entry previews its tab via
preview_entry, unlesspreview_tabsisfalse; the selection is then only applied on commit. - Wrap-around cycling is implemented by intentionally closing and reopening the quick panel at the opposite boundary. That handoff keeps the session active and reuses the existing Ctrl-release poller.
tab_stack.session_activeis the quick-panel session context key.tab_stack.session_pendingis the arming context before the panel appears.- Boundary-only contexts are
tab_stack.quick_panel_at_topandtab_stack.quick_panel_at_bottom. - When
tab_stack.session_activeisfalse,ctrl+tabstartstab_stack_open.ctrl+shift+tabis bound as a no-op to prevent accidental presses. - When
tab_stack.session_activeistrue, non-boundaryctrl+tab,ctrl+shift+tab,ctrl+up, andctrl+downcallmove, boundary presses calltab_stack_cycle, andctrl+escapecallstab_stack_cancel.
- Use
uv run ruff check .for linting. - Use
uv run ruff format .for formatting. - Use
uv run basedpyrightfor type checks. pyproject.tomlonly defines Ruff settings; there is no test runner for the plugin.- The helper builds with
cc -std=c11 -O2 -Wall -Wextra -Werror -o <scratch>/tabstack-keystate keystate/tabstack-keystate.c. tests/keystate_smoke.pytests the helper against a virtual uinput keyboard; it needs root and/dev/uinputand runs in CI (tests/is export-ignored).