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
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion .python-version
Original file line number Diff line number Diff line change
@@ -1 +1 @@
3.14
3.8
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
8 changes: 7 additions & 1 deletion plugin/captions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
]
Expand All @@ -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():
Expand Down
3 changes: 2 additions & 1 deletion plugin/ctrl_release/_availability.py
Original file line number Diff line number Diff line change
@@ -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:
Expand Down
10 changes: 5 additions & 5 deletions plugin/ctrl_release/linux.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
8 changes: 4 additions & 4 deletions plugin/ctrl_release/macos.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,15 @@
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

_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):
Expand Down Expand Up @@ -59,15 +59,15 @@ def _ctrl_down(self) -> bool:
)
)

def _open_core_graphics(self) -> Any | None:
def _open_core_graphics(self) -> Optional[Any]:
return _get_core_graphics()


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
Expand Down
8 changes: 4 additions & 4 deletions plugin/ctrl_release/windows.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -44,15 +44,15 @@ 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()


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
Expand Down
16 changes: 8 additions & 8 deletions plugin/history.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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"]
Expand All @@ -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):
Expand All @@ -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
]
Expand All @@ -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 [])
Expand Down
11 changes: 7 additions & 4 deletions plugin/mru.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
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
from .sheets import SheetIdentity, sheet_identity
from .state import GroupSelectionState, SheetSelectionHistory


@dataclass(slots=True)
@dataclass
class Entry:
selection: GroupSelectionState
caption: list[str]
Expand All @@ -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, 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:
Expand Down Expand Up @@ -60,11 +61,13 @@ 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[Optional[str], int, Optional[str], Optional[str]]]:
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[Optional[str], int, Optional[str], Optional[str]]:
return (
identity["name"],
identity["occurrence"],
Expand Down
25 changes: 16 additions & 9 deletions plugin/sheets.py
Original file line number Diff line number Diff line change
@@ -1,20 +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
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
Expand All @@ -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"]
]
Expand All @@ -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(),
Expand All @@ -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
]
Expand All @@ -72,6 +74,11 @@ def sheet_title(sheet) -> str:
return "Untitled"


def sheet_name(sheet) -> Optional[str]:
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
Expand Down
10 changes: 5 additions & 5 deletions plugin/state.py
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -10,7 +10,7 @@


class GroupSelectionState(TypedDict):
active_sheet_index: int | None
active_sheet_index: Optional[int]
selected_sheets: list[SheetIdentity]


Expand All @@ -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

Expand Down
5 changes: 3 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
Expand All @@ -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"]
Loading
Loading