From 1eafe2cdb8451d29bc3776e5c2703de3b9500f39 Mon Sep 17 00:00:00 2001 From: FichteFoll Date: Mon, 29 Jun 2026 23:21:17 +0200 Subject: [PATCH 1/2] Make usage of `Sheet.name` optional Prepares for compatibility with the latest stable build which does not provide this method. --- plugin/captions.py | 8 +++++++- plugin/mru.py | 6 +++--- plugin/sheets.py | 17 ++++++++++++----- 3 files changed, 22 insertions(+), 9 deletions(-) diff --git a/plugin/captions.py b/plugin/captions.py index 2370d90..333b35a 100644 --- a/plugin/captions.py +++ b/plugin/captions.py @@ -4,7 +4,7 @@ def caption_for_sheet_identities(identities, window) -> list[str]: - titles = [identity["name"] for identity in identities] + titles = [identity["name"] or _title_from_path(identity["path"]) for identity in identities] paths = [ _relative_path(identity["path"], window) for identity in identities if identity["path"] ] @@ -19,6 +19,12 @@ def _title_from_sheet(sheet) -> str: return "Untitled" +def _title_from_path(file_name: str | None) -> str: + if file_name: + return Path(file_name).name + return "Untitled" + + def _relative_path(file_name: str, window) -> str: absolute = Path(file_name) for folder in window.folders(): diff --git a/plugin/mru.py b/plugin/mru.py index 1c743b9..d3bd55e 100644 --- a/plugin/mru.py +++ b/plugin/mru.py @@ -17,7 +17,7 @@ class Entry: def collect_entries(window, history: SheetSelectionHistory) -> list[Entry]: entries: list[Entry] = [] active_group = window.active_group() - seen_identities: set[tuple[str, int, str | None, str | None]] = set() + seen_identities: set[tuple[str | None, int, str | None, str | None]] = set() group_state_stack = history["groups"].get(str(active_group)) if group_state_stack: @@ -60,11 +60,11 @@ def _non_transient_sheets_in_group(window, group: int): def _selection_identity_keys( selection: GroupSelectionState, -) -> set[tuple[str, int, str | None, str | None]]: +) -> set[tuple[str | None, int, str | None, str | None]]: return {_identity_key(identity) for identity in selection["selected_sheets"]} -def _identity_key(identity: SheetIdentity) -> tuple[str, int, str | None, str | None]: +def _identity_key(identity: SheetIdentity) -> tuple[str | None, int, str | None, str | None]: return ( identity["name"], identity["occurrence"], diff --git a/plugin/sheets.py b/plugin/sheets.py index 3f45752..556303d 100644 --- a/plugin/sheets.py +++ b/plugin/sheets.py @@ -8,7 +8,9 @@ class SheetIdentity(TypedDict): - name: str + name: str | None + """The sheet name is `None` on ST builds <4205 + because the required API method does not exist there.""" occurrence: int kind: str | None path: str | None @@ -25,7 +27,7 @@ def find_sheet_by_identity_and_group(window, identity: SheetIdentity, group: int matching = [ sheet for sheet in window.sheets_in_group(group) - if (sheet.name() or sheet_title(sheet)) == identity["name"] + if sheet_name(sheet) == identity["name"] and type(sheet).__name__ == identity["kind"] and sheet.file_name() == identity["path"] ] @@ -37,7 +39,7 @@ def find_sheet_by_identity_and_group(window, identity: SheetIdentity, group: int def sheet_identity(sheet, window) -> SheetIdentity: return { - "name": sheet.name() or sheet_title(sheet), + "name": sheet_name(sheet), "occurrence": sheet_occurrence(sheet, window), "kind": type(sheet).__name__, "path": sheet.file_name(), @@ -48,14 +50,14 @@ def sheet_occurrence(sheet, window) -> int: group = sheet.group() if group is None: return 0 - name = sheet.name() or sheet_title(sheet) + name = sheet_name(sheet) kind = type(sheet).__name__ path = sheet.file_name() matching_sheet_ids = [ candidate.id() for candidate in window.sheets_in_group(group) - if (candidate.name() or sheet_title(candidate)) == name + if sheet_name(candidate) == name and type(candidate).__name__ == kind and candidate.file_name() == path ] @@ -72,6 +74,11 @@ def sheet_title(sheet) -> str: return "Untitled" +def sheet_name(sheet) -> str | None: + get_name = getattr(sheet, "name", None) + return get_name() if get_name else None + + def find_live_sheets(window, identities: list[SheetIdentity], group: int) -> list[object]: return [ live_sheet From 9542fe54e94f19281a76690530b6452c4cd8527f Mon Sep 17 00:00:00 2001 From: FichteFoll Date: Mon, 29 Jun 2026 23:47:06 +0200 Subject: [PATCH 2/2] Drop Python requirement to 3.8 This allows the package to run on ST builds back until 4100. Closes #3 --- .github/workflows/ci.yml | 3 +++ .python-version | 2 +- README.md | 2 +- plugin/ctrl_release/_availability.py | 3 ++- plugin/ctrl_release/linux.py | 10 +++++----- plugin/ctrl_release/macos.py | 8 ++++---- plugin/ctrl_release/windows.py | 8 ++++---- plugin/history.py | 16 ++++++++-------- plugin/mru.py | 11 +++++++---- plugin/sheets.py | 12 ++++++------ plugin/state.py | 10 +++++----- pyproject.toml | 5 +++-- uv.lock | 2 +- 13 files changed, 50 insertions(+), 42 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 57dcf80..272c7a8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,6 +9,9 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6.0.3 + - uses: actions/setup-python@v5 + with: + python-version: "3.8" - uses: astral-sh/setup-uv@v8.2.0 with: enable-cache: true diff --git a/.python-version b/.python-version index 6324d40..cc1923a 100644 --- a/.python-version +++ b/.python-version @@ -1 +1 @@ -3.14 +3.8 diff --git a/README.md b/README.md index 511bb31..5d1777d 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # TabStack -TabStack is a [Sublime Text][] (build 4205+) package +TabStack is a [Sublime Text][] (build 4100+) package for switching between open tabs using an MRU-style stack. It keeps tab order per window and previews tabs in the quick panel diff --git a/plugin/ctrl_release/_availability.py b/plugin/ctrl_release/_availability.py index 193f86a..0d3ae70 100644 --- a/plugin/ctrl_release/_availability.py +++ b/plugin/ctrl_release/_availability.py @@ -1,8 +1,9 @@ from __future__ import annotations from collections.abc import Callable +from typing import Optional -_AVAILABLE: bool | None = None +_AVAILABLE: Optional[bool] = None def set_unavailable() -> None: diff --git a/plugin/ctrl_release/linux.py b/plugin/ctrl_release/linux.py index 56ad51b..b748069 100644 --- a/plugin/ctrl_release/linux.py +++ b/plugin/ctrl_release/linux.py @@ -6,17 +6,17 @@ import threading from collections.abc import Callable from dataclasses import dataclass -from typing import Any +from typing import Any, Optional from .._compat import sublime from ._availability import set_unavailable _XK_Control_L = 0xFFE3 _XK_Control_R = 0xFFE4 -_X11: Any | None = None +_X11: Optional[Any] = None -@dataclass(slots=True) +@dataclass class _X11State: display: int keycode_left: int @@ -73,7 +73,7 @@ def _ctrl_down(self) -> bool: state.keycode_right, ) - def _open_x11_state(self) -> _X11State | None: + def _open_x11_state(self) -> Optional[_X11State]: display_name = os.environ.get("DISPLAY") if not display_name: return None @@ -107,7 +107,7 @@ def _keycode_is_down(keymap, keycode: int) -> bool: return bool(keymap[byte_index] & (1 << bit)) -def _get_x11() -> Any | None: +def _get_x11() -> Optional[Any]: global _X11 if _X11 is not None: return _X11 diff --git a/plugin/ctrl_release/macos.py b/plugin/ctrl_release/macos.py index 29cbe93..ffd4d38 100644 --- a/plugin/ctrl_release/macos.py +++ b/plugin/ctrl_release/macos.py @@ -4,7 +4,7 @@ import ctypes.util import threading from collections.abc import Callable -from typing import Any +from typing import Any, Optional from .._compat import sublime from ._availability import set_unavailable @@ -12,7 +12,7 @@ _CG_EVENT_SOURCE_STATE_COMBINED_SESSION_STATE = 0 _K_VK_CONTROL = 59 _K_VK_RIGHT_CONTROL = 62 -_CORE_GRAPHICS: Any | None = None +_CORE_GRAPHICS: Optional[Any] = None class CtrlReleasePoller(threading.Thread): @@ -59,7 +59,7 @@ def _ctrl_down(self) -> bool: ) ) - def _open_core_graphics(self) -> Any | None: + def _open_core_graphics(self) -> Optional[Any]: return _get_core_graphics() @@ -67,7 +67,7 @@ def probe() -> bool: return _get_core_graphics() is not None -def _get_core_graphics() -> Any | None: +def _get_core_graphics() -> Optional[Any]: global _CORE_GRAPHICS if _CORE_GRAPHICS is not None: return _CORE_GRAPHICS diff --git a/plugin/ctrl_release/windows.py b/plugin/ctrl_release/windows.py index 14b0875..8ebb4aa 100644 --- a/plugin/ctrl_release/windows.py +++ b/plugin/ctrl_release/windows.py @@ -3,13 +3,13 @@ import ctypes import threading from collections.abc import Callable -from typing import Any +from typing import Any, Optional from .._compat import sublime from ._availability import set_unavailable _VK_CONTROL = 0x11 -_USER32: Any | None = None +_USER32: Optional[Any] = None class CtrlReleasePoller(threading.Thread): @@ -44,7 +44,7 @@ def _fire_release(self) -> None: def _ctrl_down(self) -> bool: return bool(self._user32.GetAsyncKeyState(_VK_CONTROL) & 0x8000) - def _open_user32(self) -> Any | None: + def _open_user32(self) -> Optional[Any]: return _get_user32() @@ -52,7 +52,7 @@ def probe() -> bool: return _get_user32() is not None -def _get_user32() -> Any | None: +def _get_user32() -> Optional[Any]: global _USER32 if _USER32 is not None: return _USER32 diff --git a/plugin/history.py b/plugin/history.py index fd892f2..f7ed872 100644 --- a/plugin/history.py +++ b/plugin/history.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import Any, cast +from typing import Any, Optional, cast from ._compat import sublime from .constants import TAB_HISTORY_LIMIT @@ -44,7 +44,7 @@ def _poll(self) -> None: self._schedule() -def current_group_selection_state(window, group: int) -> GroupSelectionState | None: +def current_group_selection_state(window, group: int) -> Optional[GroupSelectionState]: selected_sheets = list(window.selected_sheets_in_group(group)) selected_identities = [ sheet_identity(sheet, window) for sheet in selected_sheets if sheet.group() is not None @@ -120,7 +120,7 @@ def sync_selection_history( for group_key, group_state_stack in list(groups.items()): try: group = int(group_key) - except TypeError, ValueError: + except (TypeError, ValueError): # Unexpected/corrupt key; drop it to avoid crashing. del groups[group_key] changed = True @@ -175,7 +175,7 @@ def prune_group_state_for_live_sheets( window, group: int, group_state: GroupSelectionState, -) -> GroupSelectionState | None: +) -> Optional[GroupSelectionState]: selected_sheets = [ identity for identity in group_state["selected_sheets"] @@ -195,10 +195,10 @@ def prune_group_state_for_live_sheets( def prune_active_sheet_index( - active_sheet_index: int | None, + active_sheet_index: Optional[int], original_selected_sheets: list[SheetIdentity], pruned_selected_sheets: list[SheetIdentity], -) -> int | None: +) -> Optional[int]: if active_sheet_index is None: return None if active_sheet_index < 0 or active_sheet_index >= len(original_selected_sheets): @@ -214,7 +214,7 @@ def prune_active_sheet_index( def prune_group_state( group_state: GroupSelectionState, seen_identities: list[SheetIdentity], -) -> GroupSelectionState | None: +) -> Optional[GroupSelectionState]: selected_sheets = [ identity for identity in group_state["selected_sheets"] if identity not in seen_identities ] @@ -234,7 +234,7 @@ def prune_group_state( def prune_history_stack( group_state_stack: list[GroupSelectionState], - removed_identities: list[SheetIdentity] | None = None, + removed_identities: Optional[list[SheetIdentity]] = None, ) -> list[GroupSelectionState]: updated_stack: list[GroupSelectionState] = [] seen_identities: list[SheetIdentity] = list(removed_identities or []) diff --git a/plugin/mru.py b/plugin/mru.py index d3bd55e..81b6135 100644 --- a/plugin/mru.py +++ b/plugin/mru.py @@ -1,6 +1,7 @@ from __future__ import annotations from dataclasses import dataclass +from typing import Optional from .captions import caption_for_sheet_identities from .constants import TAB_HISTORY_LIMIT @@ -8,7 +9,7 @@ from .state import GroupSelectionState, SheetSelectionHistory -@dataclass(slots=True) +@dataclass class Entry: selection: GroupSelectionState caption: list[str] @@ -17,7 +18,7 @@ class Entry: def collect_entries(window, history: SheetSelectionHistory) -> list[Entry]: entries: list[Entry] = [] active_group = window.active_group() - seen_identities: set[tuple[str | None, int, str | None, str | None]] = set() + seen_identities: set[tuple[Optional[str], int, Optional[str], Optional[str]]] = set() group_state_stack = history["groups"].get(str(active_group)) if group_state_stack: @@ -60,11 +61,13 @@ def _non_transient_sheets_in_group(window, group: int): def _selection_identity_keys( selection: GroupSelectionState, -) -> set[tuple[str | None, int, str | None, str | None]]: +) -> set[tuple[Optional[str], int, Optional[str], Optional[str]]]: return {_identity_key(identity) for identity in selection["selected_sheets"]} -def _identity_key(identity: SheetIdentity) -> tuple[str | None, int, str | None, str | None]: +def _identity_key( + identity: SheetIdentity, +) -> tuple[Optional[str], int, Optional[str], Optional[str]]: return ( identity["name"], identity["occurrence"], diff --git a/plugin/sheets.py b/plugin/sheets.py index 556303d..67a2799 100644 --- a/plugin/sheets.py +++ b/plugin/sheets.py @@ -1,22 +1,22 @@ from __future__ import annotations from pathlib import Path -from typing import TYPE_CHECKING, TypedDict +from typing import TYPE_CHECKING, Optional, TypedDict if TYPE_CHECKING: from .state import GroupSelectionState class SheetIdentity(TypedDict): - name: str | None + name: Optional[str] """The sheet name is `None` on ST builds <4205 because the required API method does not exist there.""" occurrence: int - kind: str | None - path: str | None + kind: Optional[str] + path: Optional[str] -def active_sheet_identity(window) -> SheetIdentity | None: +def active_sheet_identity(window) -> Optional[SheetIdentity]: sheet = window.active_sheet() if sheet is None: return None @@ -74,7 +74,7 @@ def sheet_title(sheet) -> str: return "Untitled" -def sheet_name(sheet) -> str | None: +def sheet_name(sheet) -> Optional[str]: get_name = getattr(sheet, "name", None) return get_name() if get_name else None diff --git a/plugin/state.py b/plugin/state.py index f8d3ddf..5a7717f 100644 --- a/plugin/state.py +++ b/plugin/state.py @@ -1,7 +1,7 @@ from __future__ import annotations from dataclasses import dataclass -from typing import TYPE_CHECKING, Any, TypedDict +from typing import TYPE_CHECKING, Any, Optional, TypedDict from .sheets import SheetIdentity @@ -10,7 +10,7 @@ class GroupSelectionState(TypedDict): - active_sheet_index: int | None + active_sheet_index: Optional[int] selected_sheets: list[SheetIdentity] @@ -19,13 +19,13 @@ class SheetSelectionHistory(TypedDict): groups: dict[str, list[GroupSelectionState]] -@dataclass(slots=True) +@dataclass class TabStackWindowState: session_active: bool = False - session_origin_selection: GroupSelectionState | None = None + session_origin_selection: Optional[GroupSelectionState] = None session_selected_index: int = 0 session_group: int = 0 - session_entries: list[Entry] | None = None + session_entries: Optional[list[Entry]] = None selection_poller: Any = None ctrl_release_poller: Any = None diff --git a/pyproject.toml b/pyproject.toml index d61455e..a148989 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,7 +2,7 @@ name = "tab-stack" version = "0.1.0" description = "Per-window MRU tab stack for Sublime Text" -requires-python = ">=3.14" +requires-python = ">=3.8" [dependency-groups] dev = [ @@ -12,7 +12,8 @@ dev = [ [tool.ruff] line-length = 100 -target-version = "py314" +target-version = "py38" [tool.ruff.lint] select = ["E", "F", "I", "UP"] +ignore = ["UP045"] diff --git a/uv.lock b/uv.lock index 7913a20..bd61ecb 100644 --- a/uv.lock +++ b/uv.lock @@ -1,6 +1,6 @@ version = 1 revision = 3 -requires-python = ">=3.14" +requires-python = ">=3.8" [[package]] name = "ruff"