From ff0574a6fd204cc1f1df27533f254c953fb1e294 Mon Sep 17 00:00:00 2001 From: d0vgan Date: Sun, 11 Oct 2026 21:02:38 +0300 Subject: [PATCH 1/2] adding "TabStack.sublime-settings" and 'preview_file' setting --- AGENTS.md | 2 ++ Main.sublime-menu | 34 ++++++++++++++++++++++++++++++++++ TabStack.sublime-settings | 5 +++++ plugin/session.py | 36 +++++++++++++++++++++++++++++++++--- plugin/settings.py | 20 ++++++++++++++++++++ plugin/state.py | 7 +++++++ 6 files changed, 101 insertions(+), 3 deletions(-) create mode 100644 Main.sublime-menu create mode 100644 TabStack.sublime-settings create mode 100644 plugin/settings.py diff --git a/AGENTS.md b/AGENTS.md index 3c73538..6a706a8 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -17,6 +17,8 @@ - Ctrl-release detection lives in `plugin/ctrl_release/`. - Uner Linux, Ctrl-release polling uses the X11 key state API, so Sublime must run under X11 or XWayland; a native Wayland session has no pollable global key state and Sublime exposes no key-release API. - `tab_stack_open` arms a pending session, starts a Ctrl-release poller, and opens the quick panel after a short delay unless Ctrl is released first. +- Package defaults live in `TabStack.sublime-settings` and are read through `sublime.load_settings` in `plugin/settings.py`. +- `preview_file` controls whether the highlighted entry is shown while the list is open. When it is false, the file changes only when the list closes. - 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_active` is the quick-panel session context key. diff --git a/Main.sublime-menu b/Main.sublime-menu new file mode 100644 index 0000000..4fcf76f --- /dev/null +++ b/Main.sublime-menu @@ -0,0 +1,34 @@ +[ + { + "caption": "Preferences", + "mnemonic": "n", + "id": "preferences", + "children": + [ + { + "caption": "Package Settings", + "mnemonic": "P", + "id": "package-settings", + "children": + [ + { + "caption": "TabStack", + "children": + [ + { + "command": "open_file", + "args": {"file": "${packages}/TabStack/TabStack.sublime-settings"}, + "caption": "Settings – Default" + }, + { + "command": "open_file", + "args": {"file": "${packages}/User/TabStack.sublime-settings"}, + "caption": "Settings – User" + } + ] + } + ] + } + ] + } +] diff --git a/TabStack.sublime-settings b/TabStack.sublime-settings new file mode 100644 index 0000000..8deb2be --- /dev/null +++ b/TabStack.sublime-settings @@ -0,0 +1,5 @@ +{ + // Show the highlighted file while the list is open. + // When false, the file changes only after the list closes. + "preview_file": true +} diff --git a/plugin/session.py b/plugin/session.py index aca481a..78e974b 100644 --- a/plugin/session.py +++ b/plugin/session.py @@ -4,6 +4,7 @@ from .constants import KIND_MODIFIED from .ctrl_release import CtrlReleasePoller +from .settings import preview_file from .sheets import apply_group_selection from .state import TabStackWindowState @@ -50,7 +51,8 @@ def on_highlight(index: int) -> None: if index < 0 or index >= len(entries): return state.session_selected_index = index - preview_entry(window, state) + if preview_file(): + preview_entry(window, state) window.show_quick_panel( items, @@ -70,7 +72,10 @@ def open_if_still_pending() -> None: if not state.session_pending or state.session_pending_token != token: return if state.ctrl_release_poller is not None and not state.ctrl_release_poller.is_ctrl_down(): - state.clear_session() + # A fast tap releases Ctrl before this deadline. Committing here + # covers the case where the poller's callback has not run yet; + # clearing instead drops the tab switch. + _commit_pending_session(window, state) return state.session_active = True state.session_pending = False @@ -101,7 +106,7 @@ def _commit_pending_session(window: sublime.Window, state: TabStackWindowState) def _commit_session(window: sublime.Window, state: TabStackWindowState) -> None: - if not state.session_active: + if not state.session_active or state.session_committing: return entries = state.session_entries @@ -115,6 +120,26 @@ def _commit_session(window: sublime.Window, state: TabStackWindowState) -> None: cancel_session(window, state) return + # Preview already focused this sheet while the list was open, so closing + # now does not postpone a file change. + if preview_file(): + window.run_command("hide_overlay", {"cancel": True}) + state.clear_session() + return + + # Focusing and hiding in this same turn does not show the new sheet until + # the list has finished closing. Yield once so the focus lands first. + token = state.session_pending_token + sublime.set_timeout(lambda: _hide_committed_panel(window, state, token), 0) + + +def _hide_committed_panel(window: sublime.Window, state: TabStackWindowState, token: int | None) -> None: + if not state.session_active: + return + if token is not None and state.session_pending_token != token: + return + + state.session_committing = True window.run_command("hide_overlay", {"cancel": True}) state.clear_session() @@ -156,4 +181,9 @@ def handle_panel_closed(window: sublime.Window, state: TabStackWindowState) -> N state.session_panel_reopening = False return + # Commit already focused the chosen sheet. This close is that commit + # finishing, not an abort, so the original sheet must stay put. + if state.session_committing: + return + cancel_session(window, state) diff --git a/plugin/settings.py b/plugin/settings.py new file mode 100644 index 0000000..8882611 --- /dev/null +++ b/plugin/settings.py @@ -0,0 +1,20 @@ +from __future__ import annotations + +import sublime + +SETTINGS_FILE = "TabStack.sublime-settings" +_settings = sublime.load_settings(SETTINGS_FILE) + + +def save_settings() -> None: + sublime.save_settings(SETTINGS_FILE) + + +def preview_file() -> bool: + """Whether the highlighted list entry is shown before the list closes.""" + return _bool_setting("preview_file", default=True) + + +def _bool_setting(key: str, *, default: bool) -> bool: + value = _settings.get(key, default) + return value if isinstance(value, bool) else default diff --git a/plugin/state.py b/plugin/state.py index 772c932..620967e 100644 --- a/plugin/state.py +++ b/plugin/state.py @@ -29,6 +29,12 @@ class TabStackWindowState: session_group: int = 0 session_entries: Optional[list[Entry]] = None session_panel_reopening: bool = False + session_committing: bool = False + """A commit has focused its sheet and is closing the list. + + The overlay's close callback must not treat that close as a cancel, + or it puts the original sheet back. + """ selection_poller: Optional[Poller] = None ctrl_release_poller: Optional[CtrlPoller] = None overlay_views: set[int] = field(default_factory=set) @@ -53,6 +59,7 @@ def clear_session(self) -> None: self.session_group = 0 self.session_entries = None self.session_panel_reopening = False + self.session_committing = False self.session_pending_token += 1 if self.ctrl_release_poller is not None: self.ctrl_release_poller.stop() From 303ebc157045183585ef917902cfa5aff33a2b62 Mon Sep 17 00:00:00 2001 From: d0vgan Date: Thu, 15 Oct 2026 19:21:26 +0300 Subject: [PATCH 2/2] reverting the unrelated change --- plugin/session.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/plugin/session.py b/plugin/session.py index 78e974b..e85ac23 100644 --- a/plugin/session.py +++ b/plugin/session.py @@ -72,10 +72,7 @@ def open_if_still_pending() -> None: if not state.session_pending or state.session_pending_token != token: return if state.ctrl_release_poller is not None and not state.ctrl_release_poller.is_ctrl_down(): - # A fast tap releases Ctrl before this deadline. Committing here - # covers the case where the poller's callback has not run yet; - # clearing instead drops the tab switch. - _commit_pending_session(window, state) + state.clear_session() return state.session_active = True state.session_pending = False