Skip to content
Open
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
2 changes: 2 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
34 changes: 34 additions & 0 deletions Main.sublime-menu
Original file line number Diff line number Diff line change
@@ -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"

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure whether we really need these "Settings - Default", but other packages contain that

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We would want to use the edit_settings command like #12 does instead of providing two separate commands.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm afraid I don't understand how to adapt the edit_settings.
Should I just revert my version of "Main.sublime-menu" since it's already present in your PR?
Or should I copy-paste your implementation and add something there? If so, what should I add?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't worry about it. It was just meant as an FYI since I won't be merging this PR either way since it has too many conflicts and the one thing still left, the preview_file setting, I would rather do on my own.

},
{
"command": "open_file",
"args": {"file": "${packages}/User/TabStack.sublime-settings"},
"caption": "Settings – User"
}
]
}
]
}
]
}
]
5 changes: 5 additions & 0 deletions TabStack.sublime-settings
Original file line number Diff line number Diff line change
@@ -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
}
31 changes: 29 additions & 2 deletions plugin/session.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -101,7 +103,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
Expand All @@ -115,6 +117,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()

Expand Down Expand Up @@ -156,4 +178,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)
20 changes: 20 additions & 0 deletions plugin/settings.py
Original file line number Diff line number Diff line change
@@ -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
7 changes: 7 additions & 0 deletions plugin/state.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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()
Expand Down