Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 22 additions & 22 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,29 +1,29 @@
# TabStack Agent Notes

## Entry Points

- This is a Sublime Text package, not a standalone app.
- Runtime target is Sublime Text's Python 3.14 plugin host.
- Root `main.py` is the only plugin entry file.
- `main.py` clears cached `plugin.*` modules before star-importing `plugin`, so keep exposed plugin symbols in `plugin/__init__.py`.
- Real implementation lives under `plugin/`.
- Modifier-release detection lives in `plugin/ctrl_release/`.
- Per-window state is in-memory only, keyed by `window.id()` in `plugin/state.py`.
- `TabStackListener.on_activated` updates MRU order, but ignores activation during an active quick-panel session.
- `TabStackListener.on_close` removes closed views from all window stacks.
- Key bindings are shared among all platforms and live in `Default.sublime-keymap`.
- The quick-panel session context key is `setting.tab_stack_quick_panel`.
- When that setting is `false`, `ctrl+tab`, `ctrl+shift+tab`, `ctrl+up`, and `ctrl+down` start `show_tab_stack`.
- When that setting is `true`, those keys call `tab_stack_move`, and `escape` or `ctrl+escape` call `tab_stack_cancel`.
- `plugin/ctrl_release/linux.py` uses X11/XWayland polling via `ctypes`; native Wayland without XWayland is unsupported.
- `plugin/ctrl_release/windows.py` uses `GetAsyncKeyState` polling via `ctypes`.
- `plugin/ctrl_release/macos.py` uses CoreGraphics polling via `ctypes`.
- `show_tab_stack` starts a Ctrl-release poller and commits when Ctrl is released.
- Tab captions come from `plugin/captions.py`; views that are widgets, panels, or non-tab elements are excluded from the MRU list.
- Keep the MRU list per window and preserve the current preview behavior: preview must not reorder the stack.
- `main.py` is the only entry file; it clears cached `plugin.*` modules before star-importing `plugin`.
- Keep exported plugin symbols in `plugin/__init__.py`; implementation lives under `plugin/`.

## Behavior

- State is in-memory per window, keyed by `window.id()` in `plugin/state.py`.
- MRU order is per window.
- `TabStackListener.on_activated` updates history unless a quick-panel session is active.
- `TabStackListener.on_close` prunes 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/`.
- Linux needs X11 or XWayland for Ctrl-release polling; native Wayland is not supported.
- `show_tab_stack` opens the quick panel, starts a Ctrl-release poller, and commits when Ctrl is released.
- The quick-panel context key is `tab_stack.quick_panel`.
- When `tab_stack.quick_panel` is `false`, `ctrl+tab` starts `show_tab_stack`. `ctrl+shift+tab` is bound as a no-op to prevent accidental presses.
start `show_tab_stack`.
- When `tab_stack.quick_panel` is `true`, `ctrl+tab`, `ctrl+shift+tab`, `ctrl+up`, and `ctrl+down` call `move`, and `ctrl+escape` calls `tab_stack_cancel`.

## Verification

- Run dev tools with `uv run`, for example `uv run ruff check .` and `uv run ruff format .`.
- `pyproject.toml` only defines `ruff` settings.
- Use `ruff check .` for linting via `uv run`.
- Use `ruff format .` for formatting via `uv run`.
- No repo-local test runner or task file is currently present.
- Use `uv run ruff check .` for linting.
- Use `uv run ruff format .` for formatting.
- `pyproject.toml` only defines Ruff settings; there is no repo-local test runner.
12 changes: 8 additions & 4 deletions Default.sublime-keymap
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,21 @@
{
"keys": ["ctrl+tab"],
"command": "show_tab_stack",
"args": {"forward": true},
"context": [
{"key": "tab_stack.quick_panel", "operator": "equal", "operand": false},
{"key": "tab_stack.ctrl_release_available", "operator": "equal", "operand": true}
// This context cannot be evaluated on non-view sheets.
// Window settings cannot be queried via `settings.*` either.
// https://github.com/sublimehq/sublime_text/issues/5377
// {"key": "tab_stack.ctrl_release_available", "operator": "equal", "operand": true}
]
},
// We just override this binding to prevent accidental presses of it.
{
"keys": ["ctrl+shift+tab"],
"command": "show_tab_stack",
"args": {"forward": false},
"context": [
{"key": "tab_stack.quick_panel", "operator": "equal", "operand": false},
{"key": "tab_stack.ctrl_release_available", "operator": "equal", "operand": true}
// {"key": "tab_stack.ctrl_release_available", "operator": "equal", "operand": true}
]
},
{
Expand Down
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ It keeps tab order per window
and previews tabs in the quick panel
before committing the switch.
It also preserves tab ordering across sessions
by storing each view's last activation time.
and supports multiple selections.
When closing a tab,
TabStack focuses the most recently used remaining tab.
TabStack restores the previous selection.

[Video Showcase][]

Expand All @@ -27,6 +27,7 @@ git clone https://github.com/FichteFoll/TabStack
To update later, run `git pull` inside the `TabStack` directory
and restart Sublime Text.


## Usage

The key bindings are:
Expand All @@ -44,6 +45,7 @@ Use the same keys to move through the list while it is open.
Release `ctrl` to commit the selected tab.
Press `ctrl+escape` to cancel.


## Platform Support

TabStack supports **Linux, Windows, and macOS**.
Expand Down
13 changes: 8 additions & 5 deletions plugin/__init__.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
from .commands import * # noqa: F401,F403
from .ctrl_release import plugin_unloaded as _ctrl_release_plugin_unloaded
from .listener import * # noqa: F401,F403
from .state import iter_states
from .state import iter_states as _iter_states


def plugin_unloaded() -> None:
for state in iter_states():
if state.poller is not None:
state.poller.stop()
state.poller = None
for state in _iter_states():
if state.selection_poller is not None:
state.selection_poller.stop()
state.selection_poller = None
if state.ctrl_release_poller is not None:
state.ctrl_release_poller.stop()
state.ctrl_release_poller = None

_ctrl_release_plugin_unloaded()
19 changes: 13 additions & 6 deletions plugin/captions.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,20 @@
from pathlib import Path


def caption_for_view(view, window) -> list[str]:
title = view.name() or (Path(view.file_name()).name if view.file_name() else "Untitled")
path = ""
file_name = view.file_name()
def caption_for_sheet_identities(identities, window) -> list[str]:
titles = [identity["name"] for identity in identities]
paths = [
_relative_path(identity["path"], window) for identity in identities if identity["path"]
]

return [" | ".join(titles), " | ".join(paths)]


def _title_from_sheet(sheet) -> str:
file_name = sheet.file_name()
if file_name:
path = _relative_path(file_name, window)
return [title, path]
return Path(file_name).name
return "Untitled"


def _relative_path(file_name: str, window) -> str:
Expand Down
Loading