Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
4ee32f5
chore(uv[cooldown]): Exempt ruff from cooldown
tony Jul 26, 2026
0751203
py(deps[dev]): Require ruff>=0.16.0
tony Jul 26, 2026
1b44e95
style(md[format]): Format Python blocks in Markdown
tony Jul 26, 2026
92dcf3e
docs(CHANGES): Note the ruff floor bump
tony Jul 26, 2026
37e3205
Revert "chore(uv[cooldown]): Exempt ruff from cooldown"
tony Jul 26, 2026
483384c
chore(ruff[lint]): Adopt ruff's default rule set
tony Jul 26, 2026
e9a1269
fix(exc[__init__]): Drop return from super() calls
tony Jul 26, 2026
d0012d4
refactor(workspace[loader]): Drop no-op self-assignments
tony Jul 26, 2026
a3a3663
chore(ruff[lint]): Ignore PLW0127 in _compat
tony Jul 26, 2026
b749913
chore(ruff[lint]): Ignore S102 in docs/conf.py
tony Jul 26, 2026
af008a4
chore(ruff[lint]): Ignore S102 in cli/shell
tony Jul 26, 2026
6273216
chore(ruff[lint]): Ignore S102 in shell
tony Jul 26, 2026
8b5b36b
fix(typing[Self]): Return Self from __new__ and __enter__
tony Jul 26, 2026
06bf208
chore(ruff[lint]): Ignore BLE001 in log
tony Jul 26, 2026
80d55ea
style(docs[_ext]): Parenthesize the SVG open tag
tony Jul 26, 2026
e9f50ce
fix(builder[classic]): Pass str defaults to os.getenv
tony Jul 26, 2026
5de089b
test(cli[help]): State check=False on subprocess.run
tony Jul 26, 2026
7f32b76
refactor(_compat): Drop the pre-3.7 breakpoint fallback
tony Jul 26, 2026
b031997
fix(_compat[PY3]): Test the major version with >=
tony Jul 26, 2026
d9f6f6b
test(util): Drop the duplicate pathlib import
tony Jul 26, 2026
9203d8b
docs(CHANGES): Note the default rule set adoption
tony Jul 26, 2026
ff9334c
style(log): Drop the shebang from a library module
tony Jul 26, 2026
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
14 changes: 7 additions & 7 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -518,13 +518,13 @@ Global workspace directories: ← heading()

```python
colors = Colors()
colors.heading("Section:") # Cyan + bold (section headers)
colors.highlight("item") # Magenta + bold (primary content)
colors.info("/path/to/file") # Cyan (paths, supplementary info)
colors.muted("label:") # Blue (metadata, labels)
colors.success("ok") # Green (success states)
colors.warning("caution") # Yellow (warnings)
colors.error("failed") # Red (errors)
colors.heading("Section:") # Cyan + bold (section headers)
colors.highlight("item") # Magenta + bold (primary content)
colors.info("/path/to/file") # Cyan (paths, supplementary info)
colors.muted("label:") # Blue (metadata, labels)
colors.success("ok") # Green (success states)
colors.warning("caution") # Yellow (warnings)
colors.error("failed") # Red (errors)
```

### Key Rules
Expand Down
16 changes: 16 additions & 0 deletions CHANGES
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,22 @@ The workspace, plugin, and CLI types now say what each field holds. They
previously reached the rendered API reference as "Alias for field number 0"
or as a bare name carrying only its type.

#### Minimum `ruff>=0.16.0` (was unpinned) (#1081)

Contributors and CI now resolve the same linter release instead of whatever
each machine happened to install. ruff 0.16.0 also formats Python code blocks
inside Markdown, so documentation examples are held to the same style as
source.

#### ruff's default rule set is enabled (#1081)

The lint configuration now extends ruff's curated default rule set instead of
replacing it. An explicit `select` overrides those defaults, so the project had
been opting out of every rule outside its own prefix list. The findings this
surfaced are fixed in place, and the idioms it flagged as intentional — `exec`
in the Sphinx config and in `tmuxp shell`, the catch-all in the log formatter —
carry per-file ignores that record why.

## tmuxp 1.74.0 (2026-07-04)

tmuxp 1.74.0 pairs a libtmux upgrade with a documentation overhaul. It bumps libtmux to 0.61.0 — hardening tmux 3.7 patch-line support — and teaches `tmuxp debug-info` to report the exact tmux patch release (`3.7a`/`3.7b`) instead of the numeric-normalized version. The docs also gain theme-aware inline diagrams and a concept-first rewrite that leads with what each feature is before its configuration.
Expand Down
2 changes: 1 addition & 1 deletion docs/_ext/aafig.py
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ def render_aafig_images(app: Sphinx, doctree: nodes.Node) -> None:

class AafigureNotInstalled(AafigError):
def __init__(self, *args: object, **kwargs: object) -> None:
return super().__init__("aafigure module not installed", *args, **kwargs)
super().__init__("aafigure module not installed", *args, **kwargs)


def render_aafigure(
Expand Down
8 changes: 5 additions & 3 deletions docs/_ext/tmux_layout.py
Original file line number Diff line number Diff line change
Expand Up @@ -246,9 +246,11 @@ def render_layout(panes: list[list[str]], layout: str, size: tuple[int, int]) ->
vb_w, vb_h = w * _CW, h * _CH
svg_id = f"tmux-layout-{next(_SVG_IDS)}"
parts = [
f'<svg class="gp-tmux-layout" viewBox="0 0 {vb_w:g} {vb_h:g}" '
f'width="{vb_w:g}" height="{vb_h:g}" role="img" '
'xmlns="http://www.w3.org/2000/svg">',
(
f'<svg class="gp-tmux-layout" viewBox="0 0 {vb_w:g} {vb_h:g}" '
f'width="{vb_w:g}" height="{vb_h:g}" role="img" '
'xmlns="http://www.w3.org/2000/svg">'
),
]
parts.append("<defs>")
for index, rect in enumerate(rects):
Expand Down
10 changes: 5 additions & 5 deletions docs/topics/plugins.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,21 +116,21 @@ class MyTmuxpPlugin(TmuxpPlugin):
# Optional version-dependency configuration. See the Plugin API
# docs for every supported parameter.
config = {
'tmuxp_min_version': '1.6.2',
"tmuxp_min_version": "1.6.2",
}

TmuxpPlugin.__init__(
self,
plugin_name='tmuxp-plugin-my-tmuxp-plugin',
plugin_name="tmuxp-plugin-my-tmuxp-plugin",
**config,
)

def before_workspace_builder(self, session):
session.rename_session('my-new-session-name')
session.rename_session("my-new-session-name")

def reattach(self, session):
now = datetime.datetime.now().strftime('%Y-%m-%d')
session.rename_session('session_{}'.format(now))
now = datetime.datetime.now().strftime("%Y-%m-%d")
session.rename_session("session_{}".format(now))
```

Once it's installed in the same environment, name it in a workspace file:
Expand Down
31 changes: 27 additions & 4 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ dev = [
"coverage",
"pytest-cov",
# Lint
"ruff",
"ruff>=0.16.0",
"mypy",
"types-docutils",
"types-Pygments",
Expand Down Expand Up @@ -109,7 +109,7 @@ coverage =[
"pytest-cov",
]
lint = [
"ruff",
"ruff>=0.16.0",
"mypy",
"types-docutils",
"types-Pygments",
Expand Down Expand Up @@ -198,7 +198,10 @@ ignore_missing_imports = true
target-version = "py310"

[tool.ruff.lint]
select = [
# `select` is deliberately unset: ruff 0.16 enables a curated default rule
# set, and an explicit `select` would replace it rather than extend it.
# `extend-select` layers this project's additional linters on top.
extend-select = [
"E", # pycodestyle
"F", # pyflakes
"I", # isort
Expand All @@ -215,7 +218,7 @@ select = [
"PERF", # Perflint
"RUF", # Ruff-specific rules
"D", # pydocstyle
"FA100", # future annotations
"FA100", # future annotations
]
ignore = [
"COM812", # missing trailing comma, ruff format conflict
Expand Down Expand Up @@ -248,6 +251,26 @@ convention = "numpy"
"src/tmuxp/workspace/finders.py" = ["PTH"]
"src/tmuxp/cli/*.py" = ["PTH"]
"docs/_ext/aafig.py" = ["PTH"]
# `breakpoint = breakpoint` is not a no-op at module scope: the right-hand
# side resolves to the builtin, and the assignment binds it as a module
# attribute so `from tmuxp._compat import breakpoint` resolves. Module
# attribute lookup does not fall back to builtins, so dropping the line
# breaks the import in `tmuxp.cli.shell`.
"src/tmuxp/_compat.py" = ["PLW0127"]
# Sphinx reads the package's version metadata by `exec`-ing `__about__.py`
# into a dict, so `conf.py` never imports the package it documents.
"docs/conf.py" = ["S102"]
# `tmuxp shell -c` documents itself as "execute python code in libtmux and
# exit"; running the operator's own code in a tmux-aware namespace is the
# command's purpose, not an injection sink.
"src/tmuxp/cli/shell.py" = ["S102"]
# The interactive console honors `$PYTHONSTARTUP` and `~/.pythonrc.py` by
# `exec`-ing them, mirroring how CPython's own REPL sources them.
"src/tmuxp/shell.py" = ["S102"]
# A log formatter must never raise: `record.getMessage()` interpolates
# caller-supplied args, so anything their `__str__` throws has to be
# rendered into the line instead of propagating into the caller.
"src/tmuxp/log.py" = ["BLE001"]

[tool.pytest.ini_options]
addopts = "--reruns=0 --tb=short --no-header --showlocals --doctest-modules"
Expand Down
9 changes: 2 additions & 7 deletions src/tmuxp/_compat.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

logger = logging.getLogger(__name__)

PY3 = sys.version_info[0] == 3
PY3 = sys.version_info[0] >= 3
PYMINOR = sys.version_info[1]
PYPATCH = sys.version_info[2]

Expand All @@ -30,12 +30,7 @@ def _identity(x: object) -> object:
return x


if PY3 and PYMINOR >= 7:
breakpoint = breakpoint # noqa: A001
else:
import pdb

breakpoint = pdb.set_trace # noqa: A001
breakpoint = breakpoint # noqa: A001


implements_to_string = _identity
2 changes: 1 addition & 1 deletion src/tmuxp/_internal/colors.py
Original file line number Diff line number Diff line change
Expand Up @@ -729,7 +729,7 @@ class UnknownStyleColor(Exception):
"""

def __init__(self, color: CLIColour, *args: object, **kwargs: object) -> None:
return super().__init__(f"Unknown color {color!r}", *args, **kwargs)
super().__init__(f"Unknown color {color!r}", *args, **kwargs)


def style(
Expand Down
4 changes: 3 additions & 1 deletion src/tmuxp/_internal/private_path.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
logger = logging.getLogger(__name__)

if t.TYPE_CHECKING:
from typing_extensions import Self

PrivatePathBase = pathlib.Path
else:
PrivatePathBase = type(pathlib.Path())
Expand Down Expand Up @@ -45,7 +47,7 @@ class PrivatePath(PrivatePathBase):
'config: ~/.tmuxp/config.yaml'
"""

def __new__(cls, *args: t.Any, **kwargs: t.Any) -> PrivatePath:
def __new__(cls, *args: t.Any, **kwargs: t.Any) -> Self:
"""Create a new PrivatePath instance."""
return super().__new__(cls, *args, **kwargs)

Expand Down
4 changes: 3 additions & 1 deletion src/tmuxp/cli/_progress.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@
if t.TYPE_CHECKING:
import types

from typing_extensions import Self


# ANSI Escape Sequences
HIDE_CURSOR = "\033[?25l"
Expand Down Expand Up @@ -1117,7 +1119,7 @@ def success(self, text: str | None = None) -> None:
self.stream.write(f"{msg}\n")
self.stream.flush()

def __enter__(self) -> Spinner:
def __enter__(self) -> Self:
self.start()
return self

Expand Down
2 changes: 1 addition & 1 deletion src/tmuxp/cli/convert.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ class ConvertUnknownFileType(exc.TmuxpException):
"""Raise if tmuxp convert encounters an unknown filetype."""

def __init__(self, ext: str, *args: object, **kwargs: object) -> None:
return super().__init__(
super().__init__(
f"Unknown filetype: {ext} (valid: [.json, .yaml, .yml])",
)

Expand Down
22 changes: 11 additions & 11 deletions src/tmuxp/exc.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ def __init__(
msg = "Session not found"
if session_target is not None:
msg += f": {session_target}"
return super().__init__(msg, *args, **kwargs)
super().__init__(msg, *args, **kwargs)


class WindowNotFound(TmuxpException):
Expand All @@ -46,7 +46,7 @@ def __init__(
msg = "Window not found"
if window_target is not None:
msg += f": {window_target}"
return super().__init__(msg, *args, **kwargs)
super().__init__(msg, *args, **kwargs)


class PaneNotFound(TmuxpException):
Expand All @@ -61,7 +61,7 @@ def __init__(
msg = "Pane not found"
if pane_target is not None:
msg += f": {pane_target}"
return super().__init__(msg, *args, **kwargs)
super().__init__(msg, *args, **kwargs)


class EmptyWorkspaceException(WorkspaceError):
Expand All @@ -72,14 +72,14 @@ def __init__(
*args: object,
**kwargs: object,
) -> None:
return super().__init__("Session configuration is empty.", *args, **kwargs)
super().__init__("Session configuration is empty.", *args, **kwargs)


class SessionMissingWorkspaceException(WorkspaceError, ObjectDoesNotExist):
"""Session missing while loading tmuxp workspace."""

def __init__(self, *args: object, **kwargs: object) -> None:
return super().__init__(
super().__init__(
"No session object exists for WorkspaceBuilder. "
"Tip: Add session_name in constructor or run WorkspaceBuilder.build()",
*args,
Expand All @@ -91,7 +91,7 @@ class ActiveSessionMissingWorkspaceException(WorkspaceError):
"""Active session cannot be found while loading tmuxp workspace."""

def __init__(self, *args: object, **kwargs: object) -> None:
return super().__init__("No session active.", *args, **kwargs)
super().__init__("No session active.", *args, **kwargs)


class WorkspaceBuilderError(WorkspaceError):
Expand Down Expand Up @@ -121,7 +121,7 @@ def __init__(
" Provide a Python dotted path (e.g. 'package.module:Builder') "
"or register an entry point in the 'tmuxp.workspace_builders' group."
)
return super().__init__(msg, *args, **kwargs)
super().__init__(msg, *args, **kwargs)


class WorkspaceBuilderImportError(WorkspaceBuilderError):
Expand All @@ -148,7 +148,7 @@ def __init__(
" Confirm the builder is importable, or add its directory to "
"'workspace_builder_paths'."
)
return super().__init__(msg, *args, **kwargs)
super().__init__(msg, *args, **kwargs)


class InvalidWorkspaceBuilder(WorkspaceBuilderError):
Expand All @@ -167,7 +167,7 @@ def __init__(
) -> None:
msg = f"{target!r} is not a valid workspace builder"
msg += f": {reason}" if reason else "."
return super().__init__(msg, *args, **kwargs)
super().__init__(msg, *args, **kwargs)


class WorkspaceBuilderPathError(WorkspaceBuilderError):
Expand All @@ -186,7 +186,7 @@ def __init__(
) -> None:
msg = f"workspace_builder_paths entry is invalid: {path}."
msg += f" {reason}" if reason else " Each entry must be an existing directory."
return super().__init__(msg, *args, **kwargs)
super().__init__(msg, *args, **kwargs)


class InvalidWorkspaceBuilderOption(WorkspaceBuilderError):
Expand All @@ -197,7 +197,7 @@ class InvalidWorkspaceBuilderOption(WorkspaceBuilderError):
"""

def __init__(self, reason: str, *args: object, **kwargs: object) -> None:
return super().__init__(
super().__init__(
f"Invalid workspace_builder_options: {reason}",
*args,
**kwargs,
Expand Down
1 change: 0 additions & 1 deletion src/tmuxp/log.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
#!/usr/bin/env python
"""Log utilities for tmuxp."""

from __future__ import annotations
Expand Down
12 changes: 8 additions & 4 deletions src/tmuxp/workspace/builder/classic.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,16 +95,21 @@ def _wait_for_pane_ready(
def get_default_columns() -> int:
"""Return default session column size use when building new tmux sessions."""
return int(
os.getenv("TMUXP_DEFAULT_COLUMNS", os.getenv("COLUMNS", COLUMNS_FALLBACK)),
os.getenv(
"TMUXP_DEFAULT_COLUMNS",
os.getenv("COLUMNS", str(COLUMNS_FALLBACK)),
),
)


ROWS_FALLBACK = int(os.getenv("TMUXP_DEFAULT_ROWS", os.getenv("ROWS", 24)))
ROWS_FALLBACK = int(os.getenv("TMUXP_DEFAULT_ROWS", os.getenv("ROWS", "24")))


def get_default_rows() -> int:
"""Return default session row size use when building new tmux sessions."""
return int(os.getenv("TMUXP_DEFAULT_ROWS", os.getenv("ROWS", ROWS_FALLBACK)))
return int(
os.getenv("TMUXP_DEFAULT_ROWS", os.getenv("ROWS", str(ROWS_FALLBACK))),
)


class ClassicWorkspaceBuilder:
Expand Down Expand Up @@ -586,7 +591,6 @@ def build(self, session: Session | None = None, append: bool = False) -> None:
focus_pane = None
for pane, pane_config in self.iter_create_panes(window, window_config):
assert isinstance(pane, Pane)
pane = pane

if pane_config.get("focus"):
focus_pane = pane
Expand Down
1 change: 0 additions & 1 deletion src/tmuxp/workspace/loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,6 @@ def trickle(workspace_dict: dict[str, t.Any]) -> dict[str, t.Any]:
for window_dict in workspace_dict["windows"]:
# Prepend start_directory to relative window commands
if session_start_directory:
session_start_directory = session_start_directory
if "start_directory" not in window_dict:
window_dict["start_directory"] = session_start_directory
elif not any(
Expand Down
Loading
Loading