libtmux-easy 0.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. libtmux_easy-0.2.0/LICENSE +13 -0
  2. libtmux_easy-0.2.0/NOTICE +10 -0
  3. libtmux_easy-0.2.0/PKG-INFO +270 -0
  4. libtmux_easy-0.2.0/README.md +235 -0
  5. libtmux_easy-0.2.0/pyproject.toml +67 -0
  6. libtmux_easy-0.2.0/setup.cfg +4 -0
  7. libtmux_easy-0.2.0/src/libtmux_easy/__init__.py +36 -0
  8. libtmux_easy-0.2.0/src/libtmux_easy/_tmux.py +38 -0
  9. libtmux_easy-0.2.0/src/libtmux_easy/close.py +302 -0
  10. libtmux_easy-0.2.0/src/libtmux_easy/dash/__init__.py +46 -0
  11. libtmux_easy-0.2.0/src/libtmux_easy/dash/__main__.py +5 -0
  12. libtmux_easy-0.2.0/src/libtmux_easy/dash/app.py +519 -0
  13. libtmux_easy-0.2.0/src/libtmux_easy/dash/cli.py +114 -0
  14. libtmux_easy-0.2.0/src/libtmux_easy/dash/control.py +86 -0
  15. libtmux_easy-0.2.0/src/libtmux_easy/dash/homelog.py +39 -0
  16. libtmux_easy-0.2.0/src/libtmux_easy/dash/keys.py +153 -0
  17. libtmux_easy-0.2.0/src/libtmux_easy/dash/options.py +53 -0
  18. libtmux_easy-0.2.0/src/libtmux_easy/dash/viewport.py +145 -0
  19. libtmux_easy-0.2.0/src/libtmux_easy/dash/window.py +110 -0
  20. libtmux_easy-0.2.0/src/libtmux_easy/dash/winhook.py +62 -0
  21. libtmux_easy-0.2.0/src/libtmux_easy/launch.py +351 -0
  22. libtmux_easy-0.2.0/src/libtmux_easy/output.py +708 -0
  23. libtmux_easy-0.2.0/src/libtmux_easy/proc.py +135 -0
  24. libtmux_easy-0.2.0/src/libtmux_easy/starter.py +79 -0
  25. libtmux_easy-0.2.0/src/libtmux_easy/status.py +79 -0
  26. libtmux_easy-0.2.0/src/libtmux_easy/testing.py +442 -0
  27. libtmux_easy-0.2.0/src/libtmux_easy/tmux_lib.py +2514 -0
  28. libtmux_easy-0.2.0/src/libtmux_easy/watcher.py +277 -0
  29. libtmux_easy-0.2.0/src/libtmux_easy/wrap.py +513 -0
  30. libtmux_easy-0.2.0/src/libtmux_easy.egg-info/PKG-INFO +270 -0
  31. libtmux_easy-0.2.0/src/libtmux_easy.egg-info/SOURCES.txt +43 -0
  32. libtmux_easy-0.2.0/src/libtmux_easy.egg-info/dependency_links.txt +1 -0
  33. libtmux_easy-0.2.0/src/libtmux_easy.egg-info/entry_points.txt +7 -0
  34. libtmux_easy-0.2.0/src/libtmux_easy.egg-info/requires.txt +9 -0
  35. libtmux_easy-0.2.0/src/libtmux_easy.egg-info/top_level.txt +1 -0
  36. libtmux_easy-0.2.0/test/test_close.py +176 -0
  37. libtmux_easy-0.2.0/test/test_dash.py +615 -0
  38. libtmux_easy-0.2.0/test/test_keys.py +94 -0
  39. libtmux_easy-0.2.0/test/test_launch.py +172 -0
  40. libtmux_easy-0.2.0/test/test_output.py +420 -0
  41. libtmux_easy-0.2.0/test/test_proc.py +59 -0
  42. libtmux_easy-0.2.0/test/test_session.py +360 -0
  43. libtmux_easy-0.2.0/test/test_starter.py +62 -0
  44. libtmux_easy-0.2.0/test/test_testing.py +129 -0
  45. libtmux_easy-0.2.0/test/test_wrap_mode.py +261 -0
@@ -0,0 +1,13 @@
1
+ Copyright 2026 California Institute of Technology
2
+
3
+ Licensed under the Apache License, Version 2.0 (the "License");
4
+ you may not use this file except in compliance with the License.
5
+ You may obtain a copy of the License at
6
+
7
+ http://www.apache.org/licenses/LICENSE-2.0
8
+
9
+ Unless required by applicable law or agreed to in writing, software
10
+ distributed under the License is distributed on an "AS IS" BASIS,
11
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ See the License for the specific language governing permissions and
13
+ limitations under the License.
@@ -0,0 +1,10 @@
1
+ Copyright 2026, by the California Institute of Technology. ALL RIGHTS RESERVED.
2
+ United States Government Sponsorship acknowledged. Any commercial use must be
3
+ negotiated with the Office of Technology Transfer at the California Institute of
4
+ Technology.
5
+
6
+ This software may be subject to U.S. export control laws. By accepting this
7
+ software, the user agrees to comply with all applicable U.S. export laws and
8
+ regulations. User has the responsibility to obtain export licenses, or other
9
+ export authority as may be required before exporting such information to foreign
10
+ countries or providing access to foreign persons.
@@ -0,0 +1,270 @@
1
+ Metadata-Version: 2.4
2
+ Name: libtmux-easy
3
+ Version: 0.2.0
4
+ Summary: tmux sessions from Python: exact pane exit status, a live dashboard, pytest fixtures
5
+ Author-email: Spencer Diehl <docdiehl@gmail.com>
6
+ License: Apache-2.0
7
+ Project-URL: Homepage, https://github.com/nasa-jpl/libtmux_easy
8
+ Project-URL: Bug Tracker, https://github.com/nasa-jpl/libtmux_easy/issues
9
+ Project-URL: Repository, https://github.com/nasa-jpl/libtmux_easy
10
+ Keywords: tmux,terminal,multiplexer,session,dashboard,pytest
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: Apache Software License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.8
16
+ Classifier: Programming Language :: Python :: 3.9
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Topic :: System :: Shells
22
+ Classifier: Topic :: Terminals
23
+ Requires-Python: >=3.8.1
24
+ Description-Content-Type: text/markdown
25
+ License-File: LICENSE
26
+ License-File: NOTICE
27
+ Requires-Dist: libtmux>=0.17
28
+ Requires-Dist: psutil
29
+ Requires-Dist: packaging
30
+ Provides-Extra: test
31
+ Requires-Dist: pytest>=7.0; extra == "test"
32
+ Provides-Extra: dash
33
+ Requires-Dist: textual>=1.0; extra == "dash"
34
+ Dynamic: license-file
35
+
36
+ # libtmux-easy
37
+
38
+ Build tmux sessions from Python and supervise what runs in them:
39
+
40
+ - **exact exit status** for every pane, and an event the moment one exits;
41
+ - panes stay **interactive**: attach, type, Ctrl-C, pdb;
42
+ - a live **dashboard** of every pane;
43
+ - **output** captured to files, searched, and watched for patterns;
44
+ - a **pytest plugin** that launches the processes a test needs and fails
45
+ the test if one dies.
46
+
47
+ Linux only.
48
+
49
+ ## Install
50
+
51
+ | you want | install |
52
+ |---|---|
53
+ | library and commands, in a venv | `pip install 'libtmux-easy[dash]'` |
54
+ | the commands only, any distro | `pipx install 'libtmux-easy[dash]'` |
55
+ | Ubuntu system Python (ROS) | both `.deb`s from [Releases](https://github.com/nasa-jpl/libtmux_easy/releases) |
56
+ | to develop | `uv sync` in a checkout |
57
+
58
+ ```bash
59
+ pip install 'libtmux-easy[dash]'
60
+ # not on PyPI yet:
61
+ pip install 'libtmux-easy[dash] @ git+https://github.com/nasa-jpl/libtmux_easy'
62
+ # Ubuntu:
63
+ sudo apt install ./python3-libtmux-easy_0.2.0_all.deb ./libtmux-easy-dash_0.2.0_all.deb
64
+ ```
65
+
66
+ - Needs tmux. `[dash]` adds Textual for the dashboard; everything else works without it.
67
+ - Wrap-mode panes run `lte-run`: it must be on the panes' PATH, not only the
68
+ script's (the `.deb`, pipx, or `send_command_before("source .venv/bin/activate")`).
69
+ - ROS: a venv made with `python3 -m venv --system-site-packages` sees `rclpy`.
70
+ - More (pipx with the `.deb`, `--user` installs, building the `.deb`s):
71
+ [docs/packaging.md](https://github.com/nasa-jpl/libtmux_easy/blob/main/docs/packaging.md).
72
+
73
+ ## Quick start
74
+
75
+ ```python
76
+ from libtmux_easy import TmuxSession
77
+
78
+ session = TmuxSession.create("my-session")
79
+ session.new_window("work")
80
+ session.new_pane("editor", "nvim")
81
+ server = session.new_pane("server", "python3 -m http.server", mode="wrap")
82
+
83
+ session.new_window("logs", layout="main-horizontal")
84
+ session.new_pane("journal", "journalctl -f")
85
+
86
+ session.wait_for_output(server, r"Serving HTTP", timeout=10)
87
+ session.attach()
88
+ ```
89
+
90
+ Panes are named `window:pane`; anything that takes a pane also takes that
91
+ name, its id (`%3`) or the `Pane`. Each pane's border shows its state
92
+ (`server [running]`, `server [exited 3]`).
93
+
94
+ ## Pane modes
95
+
96
+ | `mode=` | the command | exit status | interactive |
97
+ |---|---|---|---|
98
+ | `"shell"` (default) | is typed into bash | from `$?`: a signal death is *inferred* from 128+N | yes |
99
+ | `"wrap"` | is typed as `lte-run <cmd>` | from `lte-run`, exact | yes |
100
+ | `"exec"` | is the pane's process | from tmux, when tmux records it (see below) | no |
101
+
102
+ **Use wrap** to monitor a process and get its exit status: exact, and the
103
+ pane stays a shell you can type into. Shell mode is fine when 128+N is good
104
+ enough.
105
+
106
+ **Avoid exec mode for status.** tmux (3.3a, 3.4) sometimes never collects
107
+ an exec pane's exited process, so the status is lost: `exit_status()`
108
+ returns unknown. It is often lost for a pane stopped with Ctrl-C. tmux can
109
+ also drop the last output printed just before such a process exits.
110
+ Details: [docs/KNOWN_ISSUES.md](https://github.com/nasa-jpl/libtmux_easy/blob/main/docs/KNOWN_ISSUES.md).
111
+
112
+ `create(mode=)` or `set_default_mode()` sets the default. Rerun a pane with
113
+ `respawn_pane()`: the library tracks the new run (wrap mode also tracks
114
+ reruns typed by hand).
115
+
116
+ ## API
117
+
118
+ | method | |
119
+ |---|---|
120
+ | `TmuxSession.create(name, env=None, mode="shell", capture=None, status_file=None)` | a new session (replaces one of that name) |
121
+ | `TmuxSession.current(env=None, mode="shell")` | adopt the session this process runs in |
122
+ | `new_window(name, layout="tiled", env=None, capture=None)` | a window; the next panes go in it |
123
+ | `new_pane(name, cmd, mode=None, on_exit=None, on_output=None, env=None, capture=None, tee=None)` | a pane running `cmd` |
124
+ | `send_keys(cmd, target_pane=None)`, `send_command_before(cmd, window=None)` | type into a pane; set up every new pane |
125
+ | `copy_environment(env=None)` | `env` (default `os.environ`) for every later pane |
126
+ | `is_running(pane)`, `exit_status(pane)`, `pane_status(pane)`, `status()`, `print_status()` | state: `ExitStatus` (`.code`, `.signal`, `.ok`), `PaneStatus` (`.state`: running / stopped / ok / failed / unknown) |
127
+ | `watch_pane(pane, on_exit)`, `wait_for_exit(pane, timeout)` | exit events |
128
+ | `stop_pane(pane)`, `stop_all()` | SIGINT, then SIGTERM, then SIGKILL; `stop_all()` in parallel |
129
+ | `respawn_pane(pane, cmd=None)` | rerun in place, any mode |
130
+ | `capture(target, path)`, `search_output(target, pattern)`, `output_text(pane)` | output to files; grep them |
131
+ | `watch_output(target, pattern, cb)`, `wait_for_output(target, pattern, timeout)`, `expect(target, pattern)` | output events |
132
+ | `dashboard()`, `dashboard_pane(cmd)`, `close_dashboard()` | the dashboard |
133
+ | `on_close(action)`, `close()`, `bind_close_key(key=None)` | graceful close |
134
+ | `kill_session()`, `release(keep_windows=False)` | end, or let go and leave windows up |
135
+ | `select_window()`, `select_pane()`, `attach()` | |
136
+
137
+ `env=` (e.g. `os.environ`) works at session, window and pane level, later
138
+ levels winning: panes otherwise get the tmux *server's* environment, which
139
+ may predate an activated venv. `status_file=` writes `status()` as JSON on
140
+ every change, for other programs.
141
+
142
+ `kill_session()` only sends SIGHUP: `stop_all()` or `close()` first for a
143
+ clean shutdown.
144
+
145
+ ## Output
146
+
147
+ ```python
148
+ sess.new_pane("api", cmd, on_output={r"Traceback": on_crash}) # callback per matching line
149
+ m = sess.wait_for_output("main:web", r"port (\d+)", timeout=10) # what it printed already counts
150
+ with sess.expect(pane, r"pong"): # watch first, then act
151
+ sess.send_keys("ping", target_pane=pane)
152
+
153
+ cap = sess.capture("main", "/var/log/app") # a window's panes to files, from now on
154
+ sess.search_output("main", r"ERROR") # [OutputMatch], later
155
+ ```
156
+
157
+ `target` is a pane, a window, or `None` for the whole session (panes made
158
+ later too). tmux pipes each captured pane to a file (`pipe-pane`); matching
159
+ is on plain text, colors dropped. Unwatched panes cost nothing. Details:
160
+ [docs/output-match.md](https://github.com/nasa-jpl/libtmux_easy/blob/main/docs/output-match.md).
161
+
162
+ ## Dashboard
163
+
164
+ ```python
165
+ session.dashboard() # window "dash", always first
166
+ session.dashboard_pane("htop", title="load", size="30%") # optional, beside the viewport
167
+ ```
168
+
169
+ Left: a sidebar of every pane by window, with running / stopped / failed
170
+ counts. Right: the selected pane itself, swapped in (`tmux swap-pane`), so
171
+ it is fully interactive. `home` shows the script's stdout and logging.
172
+
173
+ | key | action |
174
+ |---|---|
175
+ | `Up` / `Down`, `Enter` | browse (the viewport follows); focus the pane |
176
+ | `g` | go to the pane in its own window; `prefix g` comes back |
177
+ | `x` / `r` / `c` | stop / rerun the pane / copy its command |
178
+ | `X` `X` / `Q` `Q` | stop all / close the session gracefully |
179
+ | `[` / `]`, `?`, `q` | resize, help, quit |
180
+
181
+ Needs Textual. Options: `width=`, `initial=pane`, `mouse=`. More:
182
+ [docs/dashboard.md](https://github.com/nasa-jpl/libtmux_easy/blob/main/docs/dashboard.md).
183
+
184
+ ## Testing with pytest
185
+
186
+ ```python
187
+ # conftest.py
188
+ pytest_plugins = ["libtmux_easy.testing"]
189
+
190
+ # test_web.py
191
+ PROCESSES = {"servers:web": "python3 -m http.server 8000"}
192
+ READY = {"servers:web": r"Serving HTTP"} # optional: wait for it first
193
+ FAIL_ON = {"*": r"Traceback"} # optional: a match fails the test
194
+
195
+ def test_serves(processes):
196
+ ... # processes.output("servers:web"), .wait_for(name, pattern), .check()
197
+ ```
198
+
199
+ `processes` (module scope) starts them and waits for `READY`. A test fails
200
+ if a process exits early or prints a `FAIL_ON` line, even if the test never
201
+ noticed. Teardown stops them, and errors unless each exited 0 on SIGINT.
202
+
203
+ | `--lte-mode` | processes run as | after the run |
204
+ |---|---|---|
205
+ | `shell` | subprocesses, output prefixed `[name]` | stopped |
206
+ | `tmux` | wrap panes in a detached session | kept on failure (`--lte-keep`) |
207
+ | `dash` | panes in `lte-session`'s session and dashboard | kept with `lte-session --keep` |
208
+
209
+ Default: dash under lte-session, else tmux, else shell. Fixtures to
210
+ override, and `tmux_session` for a test's own session:
211
+ [docs/pytest-plugin.md](https://github.com/nasa-jpl/libtmux_easy/blob/main/docs/pytest-plugin.md).
212
+
213
+ ## Closing a session
214
+
215
+ tmux has no hook before a session closes, so a graceful close is a command:
216
+ `lte-close` runs the close actions, stops every pane (Ctrl-C, then SIGTERM,
217
+ SIGKILL) and kills the session, showing its progress in a `close` window.
218
+
219
+ ```python
220
+ sess.on_close("./cleanup.sh") # shell: runs even if the script died
221
+ sess.on_close(lambda s: save_logs(s), when="before") # Python: while the script runs
222
+ sess.close() # or close from the script
223
+ ```
224
+
225
+ `prefix Q` (sessions from `create()` or `lte-session`) and the dashboard's
226
+ `Q` `Q` run `lte-close`. Every way to stop and close:
227
+ [docs/session-close.md](https://github.com/nasa-jpl/libtmux_easy/blob/main/docs/session-close.md).
228
+
229
+ ## Commands
230
+
231
+ | command | does |
232
+ |---|---|
233
+ | `lte-run [--tee FILE [-a] [--raw]] [--] CMD` | runs CMD, reporting its pid and exact status to the session (wrap mode); elsewhere just runs it. `--tee` also copies the output to FILE as plain text (`-a` appends) |
234
+ | `lte-session [--keep] [--name N] [--no-dash] -- CMD` | runs CMD (e.g. `pytest -s`) in a new dashboard session's home pane: a real terminal, so `input()` and pdb work; panes its tests start appear in the sidebar. Exits with CMD's status |
235
+ | `lte-close [-t SESSION]`, `lte-close add -- CMD` | close a session gracefully; add a close action |
236
+ | `lte-dash` | the dashboard sidebar (started by `dashboard()`) |
237
+ | `lte-key borrow KEY OPTION -- CMD` | bind `prefix KEY` in sessions where OPTION is set, the old binding elsewhere |
238
+ | `lte-defaults` | print the starter tmux settings |
239
+
240
+ `lte-run` keeps the command's terminal: Ctrl-C, Ctrl-Z and stdin work as
241
+ usual. See [docs/wrap-mode.md](https://github.com/nasa-jpl/libtmux_easy/blob/main/docs/wrap-mode.md) and
242
+ [docs/lte-session.md](https://github.com/nasa-jpl/libtmux_easy/blob/main/docs/lte-session.md).
243
+
244
+ ## tmux defaults
245
+
246
+ A tmux server with **no config file** gets starter settings: vi copy mode,
247
+ `prefix |` / `-` split, `prefix h j k l` move, `prefix C-x` kill-session,
248
+ `mouse on`. `LTE_TMUX_DEFAULTS=0` turns them off; a server with a config is
249
+ left alone. The library's own keys are borrowed only if unbound, and
250
+ `~/.tmux.conf` can move them:
251
+
252
+ | key | in lte sessions | to change |
253
+ |---|---|---|
254
+ | `prefix g` | back to the dashboard | `set -g @lte_key_dash G` |
255
+ | `prefix Q` | close gracefully | `set -g @lte_key_close ""` (none) |
256
+
257
+ [docs/tmux-defaults.md](https://github.com/nasa-jpl/libtmux_easy/blob/main/docs/tmux-defaults.md).
258
+
259
+ ## More
260
+
261
+ - `demos/interactive/`: guided tours, one per feature; start with `01_session.py`.
262
+ `demos/examples/`: plain scripts.
263
+ - `docs/`: how it works and why ([index](https://github.com/nasa-jpl/libtmux_easy/blob/main/docs/README.md)), and
264
+ [known issues](https://github.com/nasa-jpl/libtmux_easy/blob/main/docs/KNOWN_ISSUES.md).
265
+ - Tested with libtmux 0.17 to 0.62, tmux 3.3a and 3.4, Python 3.8 to 3.13.
266
+ The `.deb`s need Ubuntu 24.04 or newer; on 22.04, use pipx or pip.
267
+
268
+ ## License
269
+
270
+ Apache-2.0
@@ -0,0 +1,235 @@
1
+ # libtmux-easy
2
+
3
+ Build tmux sessions from Python and supervise what runs in them:
4
+
5
+ - **exact exit status** for every pane, and an event the moment one exits;
6
+ - panes stay **interactive**: attach, type, Ctrl-C, pdb;
7
+ - a live **dashboard** of every pane;
8
+ - **output** captured to files, searched, and watched for patterns;
9
+ - a **pytest plugin** that launches the processes a test needs and fails
10
+ the test if one dies.
11
+
12
+ Linux only.
13
+
14
+ ## Install
15
+
16
+ | you want | install |
17
+ |---|---|
18
+ | library and commands, in a venv | `pip install 'libtmux-easy[dash]'` |
19
+ | the commands only, any distro | `pipx install 'libtmux-easy[dash]'` |
20
+ | Ubuntu system Python (ROS) | both `.deb`s from [Releases](https://github.com/nasa-jpl/libtmux_easy/releases) |
21
+ | to develop | `uv sync` in a checkout |
22
+
23
+ ```bash
24
+ pip install 'libtmux-easy[dash]'
25
+ # not on PyPI yet:
26
+ pip install 'libtmux-easy[dash] @ git+https://github.com/nasa-jpl/libtmux_easy'
27
+ # Ubuntu:
28
+ sudo apt install ./python3-libtmux-easy_0.2.0_all.deb ./libtmux-easy-dash_0.2.0_all.deb
29
+ ```
30
+
31
+ - Needs tmux. `[dash]` adds Textual for the dashboard; everything else works without it.
32
+ - Wrap-mode panes run `lte-run`: it must be on the panes' PATH, not only the
33
+ script's (the `.deb`, pipx, or `send_command_before("source .venv/bin/activate")`).
34
+ - ROS: a venv made with `python3 -m venv --system-site-packages` sees `rclpy`.
35
+ - More (pipx with the `.deb`, `--user` installs, building the `.deb`s):
36
+ [docs/packaging.md](https://github.com/nasa-jpl/libtmux_easy/blob/main/docs/packaging.md).
37
+
38
+ ## Quick start
39
+
40
+ ```python
41
+ from libtmux_easy import TmuxSession
42
+
43
+ session = TmuxSession.create("my-session")
44
+ session.new_window("work")
45
+ session.new_pane("editor", "nvim")
46
+ server = session.new_pane("server", "python3 -m http.server", mode="wrap")
47
+
48
+ session.new_window("logs", layout="main-horizontal")
49
+ session.new_pane("journal", "journalctl -f")
50
+
51
+ session.wait_for_output(server, r"Serving HTTP", timeout=10)
52
+ session.attach()
53
+ ```
54
+
55
+ Panes are named `window:pane`; anything that takes a pane also takes that
56
+ name, its id (`%3`) or the `Pane`. Each pane's border shows its state
57
+ (`server [running]`, `server [exited 3]`).
58
+
59
+ ## Pane modes
60
+
61
+ | `mode=` | the command | exit status | interactive |
62
+ |---|---|---|---|
63
+ | `"shell"` (default) | is typed into bash | from `$?`: a signal death is *inferred* from 128+N | yes |
64
+ | `"wrap"` | is typed as `lte-run <cmd>` | from `lte-run`, exact | yes |
65
+ | `"exec"` | is the pane's process | from tmux, when tmux records it (see below) | no |
66
+
67
+ **Use wrap** to monitor a process and get its exit status: exact, and the
68
+ pane stays a shell you can type into. Shell mode is fine when 128+N is good
69
+ enough.
70
+
71
+ **Avoid exec mode for status.** tmux (3.3a, 3.4) sometimes never collects
72
+ an exec pane's exited process, so the status is lost: `exit_status()`
73
+ returns unknown. It is often lost for a pane stopped with Ctrl-C. tmux can
74
+ also drop the last output printed just before such a process exits.
75
+ Details: [docs/KNOWN_ISSUES.md](https://github.com/nasa-jpl/libtmux_easy/blob/main/docs/KNOWN_ISSUES.md).
76
+
77
+ `create(mode=)` or `set_default_mode()` sets the default. Rerun a pane with
78
+ `respawn_pane()`: the library tracks the new run (wrap mode also tracks
79
+ reruns typed by hand).
80
+
81
+ ## API
82
+
83
+ | method | |
84
+ |---|---|
85
+ | `TmuxSession.create(name, env=None, mode="shell", capture=None, status_file=None)` | a new session (replaces one of that name) |
86
+ | `TmuxSession.current(env=None, mode="shell")` | adopt the session this process runs in |
87
+ | `new_window(name, layout="tiled", env=None, capture=None)` | a window; the next panes go in it |
88
+ | `new_pane(name, cmd, mode=None, on_exit=None, on_output=None, env=None, capture=None, tee=None)` | a pane running `cmd` |
89
+ | `send_keys(cmd, target_pane=None)`, `send_command_before(cmd, window=None)` | type into a pane; set up every new pane |
90
+ | `copy_environment(env=None)` | `env` (default `os.environ`) for every later pane |
91
+ | `is_running(pane)`, `exit_status(pane)`, `pane_status(pane)`, `status()`, `print_status()` | state: `ExitStatus` (`.code`, `.signal`, `.ok`), `PaneStatus` (`.state`: running / stopped / ok / failed / unknown) |
92
+ | `watch_pane(pane, on_exit)`, `wait_for_exit(pane, timeout)` | exit events |
93
+ | `stop_pane(pane)`, `stop_all()` | SIGINT, then SIGTERM, then SIGKILL; `stop_all()` in parallel |
94
+ | `respawn_pane(pane, cmd=None)` | rerun in place, any mode |
95
+ | `capture(target, path)`, `search_output(target, pattern)`, `output_text(pane)` | output to files; grep them |
96
+ | `watch_output(target, pattern, cb)`, `wait_for_output(target, pattern, timeout)`, `expect(target, pattern)` | output events |
97
+ | `dashboard()`, `dashboard_pane(cmd)`, `close_dashboard()` | the dashboard |
98
+ | `on_close(action)`, `close()`, `bind_close_key(key=None)` | graceful close |
99
+ | `kill_session()`, `release(keep_windows=False)` | end, or let go and leave windows up |
100
+ | `select_window()`, `select_pane()`, `attach()` | |
101
+
102
+ `env=` (e.g. `os.environ`) works at session, window and pane level, later
103
+ levels winning: panes otherwise get the tmux *server's* environment, which
104
+ may predate an activated venv. `status_file=` writes `status()` as JSON on
105
+ every change, for other programs.
106
+
107
+ `kill_session()` only sends SIGHUP: `stop_all()` or `close()` first for a
108
+ clean shutdown.
109
+
110
+ ## Output
111
+
112
+ ```python
113
+ sess.new_pane("api", cmd, on_output={r"Traceback": on_crash}) # callback per matching line
114
+ m = sess.wait_for_output("main:web", r"port (\d+)", timeout=10) # what it printed already counts
115
+ with sess.expect(pane, r"pong"): # watch first, then act
116
+ sess.send_keys("ping", target_pane=pane)
117
+
118
+ cap = sess.capture("main", "/var/log/app") # a window's panes to files, from now on
119
+ sess.search_output("main", r"ERROR") # [OutputMatch], later
120
+ ```
121
+
122
+ `target` is a pane, a window, or `None` for the whole session (panes made
123
+ later too). tmux pipes each captured pane to a file (`pipe-pane`); matching
124
+ is on plain text, colors dropped. Unwatched panes cost nothing. Details:
125
+ [docs/output-match.md](https://github.com/nasa-jpl/libtmux_easy/blob/main/docs/output-match.md).
126
+
127
+ ## Dashboard
128
+
129
+ ```python
130
+ session.dashboard() # window "dash", always first
131
+ session.dashboard_pane("htop", title="load", size="30%") # optional, beside the viewport
132
+ ```
133
+
134
+ Left: a sidebar of every pane by window, with running / stopped / failed
135
+ counts. Right: the selected pane itself, swapped in (`tmux swap-pane`), so
136
+ it is fully interactive. `home` shows the script's stdout and logging.
137
+
138
+ | key | action |
139
+ |---|---|
140
+ | `Up` / `Down`, `Enter` | browse (the viewport follows); focus the pane |
141
+ | `g` | go to the pane in its own window; `prefix g` comes back |
142
+ | `x` / `r` / `c` | stop / rerun the pane / copy its command |
143
+ | `X` `X` / `Q` `Q` | stop all / close the session gracefully |
144
+ | `[` / `]`, `?`, `q` | resize, help, quit |
145
+
146
+ Needs Textual. Options: `width=`, `initial=pane`, `mouse=`. More:
147
+ [docs/dashboard.md](https://github.com/nasa-jpl/libtmux_easy/blob/main/docs/dashboard.md).
148
+
149
+ ## Testing with pytest
150
+
151
+ ```python
152
+ # conftest.py
153
+ pytest_plugins = ["libtmux_easy.testing"]
154
+
155
+ # test_web.py
156
+ PROCESSES = {"servers:web": "python3 -m http.server 8000"}
157
+ READY = {"servers:web": r"Serving HTTP"} # optional: wait for it first
158
+ FAIL_ON = {"*": r"Traceback"} # optional: a match fails the test
159
+
160
+ def test_serves(processes):
161
+ ... # processes.output("servers:web"), .wait_for(name, pattern), .check()
162
+ ```
163
+
164
+ `processes` (module scope) starts them and waits for `READY`. A test fails
165
+ if a process exits early or prints a `FAIL_ON` line, even if the test never
166
+ noticed. Teardown stops them, and errors unless each exited 0 on SIGINT.
167
+
168
+ | `--lte-mode` | processes run as | after the run |
169
+ |---|---|---|
170
+ | `shell` | subprocesses, output prefixed `[name]` | stopped |
171
+ | `tmux` | wrap panes in a detached session | kept on failure (`--lte-keep`) |
172
+ | `dash` | panes in `lte-session`'s session and dashboard | kept with `lte-session --keep` |
173
+
174
+ Default: dash under lte-session, else tmux, else shell. Fixtures to
175
+ override, and `tmux_session` for a test's own session:
176
+ [docs/pytest-plugin.md](https://github.com/nasa-jpl/libtmux_easy/blob/main/docs/pytest-plugin.md).
177
+
178
+ ## Closing a session
179
+
180
+ tmux has no hook before a session closes, so a graceful close is a command:
181
+ `lte-close` runs the close actions, stops every pane (Ctrl-C, then SIGTERM,
182
+ SIGKILL) and kills the session, showing its progress in a `close` window.
183
+
184
+ ```python
185
+ sess.on_close("./cleanup.sh") # shell: runs even if the script died
186
+ sess.on_close(lambda s: save_logs(s), when="before") # Python: while the script runs
187
+ sess.close() # or close from the script
188
+ ```
189
+
190
+ `prefix Q` (sessions from `create()` or `lte-session`) and the dashboard's
191
+ `Q` `Q` run `lte-close`. Every way to stop and close:
192
+ [docs/session-close.md](https://github.com/nasa-jpl/libtmux_easy/blob/main/docs/session-close.md).
193
+
194
+ ## Commands
195
+
196
+ | command | does |
197
+ |---|---|
198
+ | `lte-run [--tee FILE [-a] [--raw]] [--] CMD` | runs CMD, reporting its pid and exact status to the session (wrap mode); elsewhere just runs it. `--tee` also copies the output to FILE as plain text (`-a` appends) |
199
+ | `lte-session [--keep] [--name N] [--no-dash] -- CMD` | runs CMD (e.g. `pytest -s`) in a new dashboard session's home pane: a real terminal, so `input()` and pdb work; panes its tests start appear in the sidebar. Exits with CMD's status |
200
+ | `lte-close [-t SESSION]`, `lte-close add -- CMD` | close a session gracefully; add a close action |
201
+ | `lte-dash` | the dashboard sidebar (started by `dashboard()`) |
202
+ | `lte-key borrow KEY OPTION -- CMD` | bind `prefix KEY` in sessions where OPTION is set, the old binding elsewhere |
203
+ | `lte-defaults` | print the starter tmux settings |
204
+
205
+ `lte-run` keeps the command's terminal: Ctrl-C, Ctrl-Z and stdin work as
206
+ usual. See [docs/wrap-mode.md](https://github.com/nasa-jpl/libtmux_easy/blob/main/docs/wrap-mode.md) and
207
+ [docs/lte-session.md](https://github.com/nasa-jpl/libtmux_easy/blob/main/docs/lte-session.md).
208
+
209
+ ## tmux defaults
210
+
211
+ A tmux server with **no config file** gets starter settings: vi copy mode,
212
+ `prefix |` / `-` split, `prefix h j k l` move, `prefix C-x` kill-session,
213
+ `mouse on`. `LTE_TMUX_DEFAULTS=0` turns them off; a server with a config is
214
+ left alone. The library's own keys are borrowed only if unbound, and
215
+ `~/.tmux.conf` can move them:
216
+
217
+ | key | in lte sessions | to change |
218
+ |---|---|---|
219
+ | `prefix g` | back to the dashboard | `set -g @lte_key_dash G` |
220
+ | `prefix Q` | close gracefully | `set -g @lte_key_close ""` (none) |
221
+
222
+ [docs/tmux-defaults.md](https://github.com/nasa-jpl/libtmux_easy/blob/main/docs/tmux-defaults.md).
223
+
224
+ ## More
225
+
226
+ - `demos/interactive/`: guided tours, one per feature; start with `01_session.py`.
227
+ `demos/examples/`: plain scripts.
228
+ - `docs/`: how it works and why ([index](https://github.com/nasa-jpl/libtmux_easy/blob/main/docs/README.md)), and
229
+ [known issues](https://github.com/nasa-jpl/libtmux_easy/blob/main/docs/KNOWN_ISSUES.md).
230
+ - Tested with libtmux 0.17 to 0.62, tmux 3.3a and 3.4, Python 3.8 to 3.13.
231
+ The `.deb`s need Ubuntu 24.04 or newer; on 22.04, use pipx or pip.
232
+
233
+ ## License
234
+
235
+ Apache-2.0
@@ -0,0 +1,67 @@
1
+ [project]
2
+ name = "libtmux-easy"
3
+ version = "0.2.0"
4
+ description = "tmux sessions from Python: exact pane exit status, a live dashboard, pytest fixtures"
5
+ readme = "README.md"
6
+ license = {text = "Apache-2.0"}
7
+ authors = [
8
+ { name = "Spencer Diehl", email = "docdiehl@gmail.com" }
9
+ ]
10
+ keywords = ["tmux", "terminal", "multiplexer", "session", "dashboard", "pytest"]
11
+ classifiers = [
12
+ "Development Status :: 4 - Beta",
13
+ "Intended Audience :: Developers",
14
+ "License :: OSI Approved :: Apache Software License",
15
+ "Programming Language :: Python :: 3",
16
+ "Programming Language :: Python :: 3.8",
17
+ "Programming Language :: Python :: 3.9",
18
+ "Programming Language :: Python :: 3.10",
19
+ "Programming Language :: Python :: 3.11",
20
+ "Programming Language :: Python :: 3.12",
21
+ "Programming Language :: Python :: 3.13",
22
+ "Topic :: System :: Shells",
23
+ "Topic :: Terminals",
24
+ ]
25
+ requires-python = ">=3.8.1"
26
+ dependencies = [
27
+ "libtmux>=0.17",
28
+ "psutil",
29
+ "packaging",
30
+ ]
31
+
32
+ [project.optional-dependencies]
33
+ test = [
34
+ "pytest>=7.0",
35
+ ]
36
+ # lte-dash, the dashboard sidebar. Ubuntu's python3-textual is too old, so the
37
+ # libtmux-easy-dash .deb bundles this from uv.lock (debian/dash-requirements.txt).
38
+ dash = [
39
+ "textual>=1.0",
40
+ ]
41
+
42
+ # 'uv sync' installs this group by default: a development environment with
43
+ # the dashboard and the tests. Not part of the package (pip, the .deb).
44
+ [dependency-groups]
45
+ dev = ["libtmux-easy[dash,test]"]
46
+
47
+ [project.scripts]
48
+ lte-run = "libtmux_easy.wrap:main"
49
+ lte-dash = "libtmux_easy.dash.cli:main"
50
+ lte-session = "libtmux_easy.launch:main"
51
+ lte-key = "libtmux_easy.dash.keys:main"
52
+ lte-close = "libtmux_easy.close:main"
53
+ lte-defaults = "libtmux_easy.starter:main"
54
+
55
+ # setuptools rather than a newer backend: it is packaged for Ubuntu 24.04, so
56
+ # the .deb (debian/) builds from this same file with apt-only dependencies.
57
+ [build-system]
58
+ requires = ["setuptools>=61"]
59
+ build-backend = "setuptools.build_meta"
60
+
61
+ [tool.setuptools.packages.find]
62
+ where = ["src"]
63
+
64
+ [project.urls]
65
+ "Homepage" = "https://github.com/nasa-jpl/libtmux_easy"
66
+ "Bug Tracker" = "https://github.com/nasa-jpl/libtmux_easy/issues"
67
+ "Repository" = "https://github.com/nasa-jpl/libtmux_easy"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,36 @@
1
+ # The names below are loaded on first use, not at import. The lte-run console
2
+ # script imports libtmux_easy.wrap, which runs this file first; importing
3
+ # tmux_lib here would drag libtmux and psutil into every wrapped pane (about
4
+ # 4x the startup time and +7 MB each) for a script that uses neither.
5
+ _EXPORTS = (
6
+ "TmuxSession",
7
+ "session_exists",
8
+ "set_server",
9
+ "tmux_command",
10
+ "DeprecatedError",
11
+ "libtmux_version",
12
+ "ExitStatus",
13
+ "PaneStatus",
14
+ "PANE_MODE_SHELL",
15
+ "PANE_MODE_EXEC",
16
+ "PANE_MODE_WRAP",
17
+ "Capture",
18
+ "OutputExpect",
19
+ "OutputMatch",
20
+ "OutputWatch",
21
+ "PaneExited",
22
+ )
23
+
24
+ __all__ = list(_EXPORTS)
25
+
26
+
27
+ def __getattr__(name):
28
+ if name in _EXPORTS:
29
+ from . import tmux_lib
30
+
31
+ return getattr(tmux_lib, name)
32
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
33
+
34
+
35
+ def __dir__():
36
+ return sorted(set(globals()) | set(_EXPORTS))