Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
73 commits
Select commit Hold shift + click to select a range
c39f180
neo(feat[parse]): Add _split_records
tony Aug 29, 2026
f118a36
neo(fix[parse]): Regroup records on the separator
tony Aug 29, 2026
f8d0ee8
Common(feat[timeout]): Let a caller bound a tmux command
tony Aug 29, 2026
628011c
Server(feat[timeout]): Set the bound once, for every command
tony Aug 29, 2026
41b5659
Command(feat): Add a separate process runner
tony Sep 13, 2026
f76e4aa
Query(feat): Expose typed public lookups
tony Sep 13, 2026
739ad6d
Models(feat): Add decoded scalar properties
tony Sep 13, 2026
6cc47c3
Server(feat): Add explicitly owned scopes
tony Sep 13, 2026
34bd5b0
Server(fix): Preserve owned endpoints after failed cleanup
tony Sep 13, 2026
3c1c1e3
Docs(fix): Restore context manager walkthrough
tony Sep 13, 2026
10379be
Tests(fix): Control the retry test clock
tony Sep 13, 2026
6f9cd64
CI(fix): Measure pytest plugin imports
tony Sep 13, 2026
8d87555
Tests(test): Cover command and cleanup errors
tony Sep 13, 2026
a90a9bb
CI(fix): Cache tmux by resolved revision
tony Sep 13, 2026
efaeeea
Hooks(fix): Decode tmux 3.8 events
tony Sep 13, 2026
1619969
Docs(fix): Explain floating pane borders
tony Sep 13, 2026
01aedb8
Coverage(test): Measure executable branches
tony Sep 13, 2026
f3479a3
Control(test): Drain buffered replies
tony Sep 13, 2026
39e29e4
Capture(test): Wait for emitted markers
tony Sep 13, 2026
c910a50
Ownership(test): Exercise cleanup refusals
tony Sep 13, 2026
f44c35d
Popup(test): Attach a terminal client
tony Sep 13, 2026
fe3fe70
Ownership(test): Cover unused server scopes
tony Sep 13, 2026
cddd333
Docs(fix): Verify automation completion
tony Sep 13, 2026
3a9a50a
Docs(fix): Match padded completion markers
tony Sep 13, 2026
30aa1ad
Control(fix): Close client streams on exit
tony Sep 13, 2026
02b3911
Server(test): Require successful status messages
tony Sep 13, 2026
33c760a
Tests(pane): Wait for capture prerequisites
tony Sep 13, 2026
4c1cc3d
Tests(window): Wait for split command output
tony Sep 13, 2026
dcf1ddc
Docs(changelog): Describe commands and ownership
tony Sep 13, 2026
d658025
Tests: Wait for tmux terminal state
tony Sep 13, 2026
b20ea25
Server(fix[new_session]): Regroup the -P -F reply before parsing
tony Sep 13, 2026
0c5ec27
Tests(fix[conftest]): Quote the interpolated path in hanging_tmux
tony Sep 13, 2026
3345e4c
Common(fix[timeout]): Bound the post-kill drain
tony Sep 13, 2026
139cd05
Exc(fix[TmuxTimeout]): Keep self.args shaped like the constructor
tony Sep 13, 2026
9712e84
Neo(fix[parse]): Distinguish a malformed record from an unreachable s…
tony Sep 13, 2026
c8bd040
Server(fix[timeout]): Make liveness primitives honor Server.timeout
tony Sep 13, 2026
4631951
Server(fix[owned_session]): Guard identity-building against leaking t…
tony Sep 13, 2026
c2160b7
Control(fix): SIGCONT a stopped client before waiting on termination
tony Sep 13, 2026
8b5ecc6
Tests(fix[server]): Guard cleanup-failure test's finally against unbo…
tony Sep 13, 2026
e4855c6
Tests(fix[common]): Exercise the timeout=None path it claims to test
tony Sep 13, 2026
15204b3
Server(fix): Give Server.timeout a class-level default
tony Sep 13, 2026
507c119
Server(fix[owned]): Store socket_path as str, matching every other co…
tony Sep 13, 2026
4944d0d
Tests(fix[server]): Type the new_session monkeypatch stub as Any
tony Sep 13, 2026
175842b
Server(fix[cmd]): Distinguish an omitted timeout from an explicit None
tony Sep 13, 2026
df722ec
Tests(fix[control_mode]): Bound cleanup time for a stopped client
tony Sep 15, 2026
88d6b9c
Tests(fix[control_mode]): Time only _stop(), not spawn/registration
tony Sep 15, 2026
33572c1
Server(fix[server_access]): Emit flags before the positional user
tony Sep 16, 2026
2803e1e
Tests(fix[3.8]): Pin pane_pid emptiness and the window_layout round trip
tony Sep 16, 2026
9999b57
CI(fix[tests]): Gate the master-tmux matrix lane on its own test step
tony Sep 16, 2026
a5caede
Docs(feat[examples]): Add a runnable examples/ directory, executed by…
tony Sep 16, 2026
c38eea1
Docs(feat[control-mode]): Decide ControlMode stays internal, document…
tony Sep 16, 2026
af4bda8
Bench(feat): Add a pytest-benchmark suite for dispatch, listing, and …
tony Sep 16, 2026
634b6b1
Window(fix[select_layout]): Refuse an empty layout, guard hostile one…
tony Sep 16, 2026
27c04d7
Pane(fix[send_keys,capture_pane]): Surface tmux failures, add left/top
tony Sep 16, 2026
af958cb
Pane(docs[is_dead]): Show the local-read contract, not just state it
tony Sep 16, 2026
ab37525
Window(docs[select_layout]): State the round-trip identity caveat (D8)
tony Sep 16, 2026
e5b379d
Server(fix[wait_for]): Accept a timeout, rename set_flag to signal
tony Sep 16, 2026
fb76ab2
Docs(fix[relations]): Document which list accessors are lenient, and …
tony Sep 16, 2026
ebabafe
Examples(fix[command_results]): Probe an isolated socket, not the def…
tony Sep 16, 2026
c2c27c0
Tests(fix[benchmarks]): Error instead of silently collecting nothing
tony Sep 16, 2026
1f6e029
Server(fix[owned]): Trap SIGTERM/SIGHUP so cleanup still runs
tony Sep 16, 2026
45a2338
Docs(feat[changelog]): Add entries for the hand-test fix round (PY-1.…
tony Sep 16, 2026
43a0b79
Docs(fix[relations,is_dead]): Fix root AGENTS.md's blanket claim; wid…
tony Sep 16, 2026
0aa2802
Tests(fix[benchmarks]): Drop --benchmark-only from the collect-only p…
tony Sep 16, 2026
5eed514
Window(fix[select_layout]): Refuse a leading-dash layout before tmux …
tony Sep 16, 2026
d0955d7
Window(fix[select_layout]): Accept only a layout tmux can parse
tony Sep 17, 2026
49c3cc0
Neo(fix[refresh]): Clear a field that became empty
tony Sep 17, 2026
a0838c7
Pane(fix[reset,split]): Surface tmux failures instead of a silent suc…
tony Sep 17, 2026
bfc9b71
Window(fix[select_layout]): Accept a unique preset abbreviation
tony Sep 17, 2026
b945f07
Common(fix[run_command]): Reject a non-positive timeout instead of sk…
tony Sep 17, 2026
59de6fa
Server(fix[owned]): Run cleanup from the signal handler, then die by …
tony Sep 17, 2026
17a2991
Docs(fix[readme]): Make the first .cmd() snippet runnable as published
tony Sep 17, 2026
1edb22d
Docs(feat[changelog]): Add entries for the round-2 hand-test fix roun…
tony Sep 17, 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
28 changes: 28 additions & 0 deletions .github/CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,16 @@ Test:
$ uv run pytest
```

Start coverage before pytest so plugin imports and worker processes are
measured:

```console
$ uv run coverage erase && \
uv run coverage run -m pytest -n auto && \
uv run coverage combine && \
uv run coverage xml
```

Documentation is a gate, not a courtesy. Examples in docstrings,
documentation pages, and `README.md` are executed by `pytest`; the
doctest flags live in `pyproject.toml`, so there is no separate doctest
Expand Down Expand Up @@ -128,6 +138,24 @@ Include doctests in the watch loop:
$ uv run ptw . --now --doctest-modules
```

## Benchmarks

`benchmarks/` holds [pytest-benchmark] microbenchmarks for command
dispatch, listing, snapshot capture, and format decoding. It is not a
gate — performance work is a separate tier from the gates above, not
part of them — and it is not in `testpaths`, so a plain `uv run pytest`
never runs it.

```console
$ just bench
```

Report a regression with the printed numbers, not a guess. A number
that lands in a commit message or `CHANGES` is a measurement someone
ran, not a target to defend in the next one.

[pytest-benchmark]: https://pytest-benchmark.readthedocs.io/

## Debugging

Stuck in a debugging loop: pause and acknowledge it rather than trying
Expand Down
36 changes: 26 additions & 10 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ jobs:
uses: astral-sh/setup-uv@v10.0.1
with:
enable-cache: true
save-cache: ${{ strategy.job-index == 0 }}

- name: Set up Python ${{ matrix.python-version }}
run: uv python install ${{ matrix.python-version }}
Expand All @@ -36,24 +37,41 @@ jobs:
- name: Install dependencies
run: uv sync --all-extras --dev

- name: Resolve tmux revision
id: tmux-source
env:
TMUX_REF: ${{ matrix.tmux-version }}
run: |
if [[ "$TMUX_REF" == master ]]; then
ref=refs/heads/master
else
ref="refs/tags/$TMUX_REF"
fi
revision=$(git ls-remote --refs https://github.com/tmux/tmux.git "$ref")
revision=${revision%%[[:space:]]*}
[[ "$revision" =~ ^[0-9a-f]{40}$ ]]
printf 'revision=%s\n' "$revision" >> "$GITHUB_OUTPUT"

- name: Setup tmux build cache for tmux ${{ matrix.tmux-version }}
id: tmux-build-cache
uses: actions/cache@v6
with:
path: ~/tmux-builds/tmux-${{ matrix.tmux-version }}
key: tmux-${{ matrix.tmux-version }}
key: tmux-${{ runner.os }}-${{ runner.arch }}-${{ matrix.tmux-version }}-${{ steps.tmux-source.outputs.revision }}

- name: Build tmux ${{ matrix.tmux-version }}
if: steps.tmux-build-cache.outputs.cache-hit != 'true'
env:
TMUX_REVISION: ${{ steps.tmux-source.outputs.revision }}
run: |
sudo apt install libevent-dev libncurses5-dev libtinfo-dev libutempter-dev bison
mkdir ~/tmux-builds
mkdir ~/tmux-src
git clone https://github.com/tmux/tmux.git ~/tmux-src/tmux-${{ matrix.tmux-version }}
cd ~/tmux-src/tmux-${{ matrix.tmux-version }}
git checkout ${{ matrix.tmux-version }}
git checkout "$TMUX_REVISION"
sh autogen.sh
./configure --prefix=$HOME/tmux-builds/tmux-${{ matrix.tmux-version }} && make && make install
./configure --prefix="$HOME/tmux-builds/tmux-${{ matrix.tmux-version }}" && make && make install
export PATH=$HOME/tmux-builds/tmux-${{ matrix.tmux-version }}/bin:$PATH
cd ~
tmux -V
Expand All @@ -73,17 +91,15 @@ jobs:
uv run python -V

- name: Test with pytest
continue-on-error: ${{ matrix.tmux-version == 'master' }}
run: |
sudo apt install libevent-2.1-7
export PATH=$HOME/tmux-builds/tmux-${{ matrix.tmux-version }}/bin:$PATH
ls $HOME/tmux-builds/tmux-${{ matrix.tmux-version }}/bin
ls "$HOME/tmux-builds/tmux-${{ matrix.tmux-version }}/bin"
tmux -V
uv run py.test --cov=./ --cov-append --cov-report=xml -n auto --verbose
env:
COV_CORE_SOURCE: .
COV_CORE_CONFIG: .coveragerc
COV_CORE_DATAFILE: .coverage.eager
uv run coverage erase
uv run coverage run -m pytest -n auto --verbose
uv run coverage combine
uv run coverage xml
- uses: codecov/codecov-action@v7
with:
token: ${{ secrets.CODECOV_TOKEN }}
Expand Down
18 changes: 13 additions & 5 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,11 +55,19 @@ be stated twice, the file listed above is the one that governs.
tmux >= 3.2a is the compatibility floor (see `tests.yml`'s build
matrix). `Server.sessions`, `Server.clients`, and
`Server.attached_sessions` return an empty `QueryList` rather than
raising when the underlying tmux list command fails for any reason —
list-shaped accessors are lenient by default; `Server.is_alive()` and
`Server.raise_if_dead()` are the explicit, loud-failure primitives. See
`src/libtmux/AGENTS.md` for the full contract and this package's
logging conventions.
raising when the underlying tmux list invocation fails for any reason.
This does not generalize to every list-shaped accessor: `Server.windows`
and `Server.panes` are lenient only for a not-yet-started daemon or a
missing socket, and `Session.windows`, `Session.panes`, `Window.panes`,
and `Window.search_panes` are not lenient at all — any tmux failure
there raises. `Server.is_alive()` and `Server.raise_if_dead()` are the
explicit, loud-failure primitives; a dead server reading as an empty
live one through the lenient accessors never implies a `Session`/
`Window` relation obtained beforehand will also read empty rather than
raise. A parse failure (`exc.TmuxRecordParseError`) or a timeout
(`exc.TmuxTimeout`) still propagates through the lenient ones — see
`src/libtmux/AGENTS.md` for the full, precise contract and this
package's logging conventions.

## References

Expand Down
Loading