Skip to content

Commit b7442dc

Browse files
committed
Workspace(feat[lifecycle]): Add project hooks and stop command
why: Add lifecycle cleanup and restart hooks as a separate layer above runtime config primitives. what: - Run start and restart hooks from load while preserving append behavior - Store exit and stop hook metadata on newly created tmux sessions - Add tmuxp stop with on_project_stop execution and focused CLI/docs coverage
1 parent dd0eaad commit b7442dc

20 files changed

Lines changed: 944 additions & 17 deletions

File tree

conftest.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,8 @@ def socket_name(request: pytest.FixtureRequest) -> str:
114114

115115
# Modules that actually need tmux fixtures in their doctests
116116
DOCTEST_NEEDS_TMUX = {
117+
"tmuxp.cli.stop",
118+
"tmuxp.util",
117119
"tmuxp.workspace.builder",
118120
}
119121

docs/cli/index.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,12 @@ Interactive Python shell with tmux context.
2525
Export running sessions to config files.
2626
:::
2727

28+
:::{grid-item-card} tmuxp stop
29+
:link: stop
30+
:link-type: doc
31+
Stop running sessions with cleanup hooks.
32+
:::
33+
2834
:::{grid-item-card} tmuxp convert
2935
:link: convert
3036
:link-type: doc
@@ -53,6 +59,7 @@ load
5359
shell
5460
ls
5561
search
62+
stop
5663
```
5764

5865
```{toctree}

docs/cli/stop.md

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
(cli-stop)=
2+
3+
(cli-stop-reference)=
4+
5+
# tmuxp stop
6+
7+
Stop (kill) a running tmux session. If the session was created from a workspace
8+
with `on_project_stop`, that hook runs before the session is killed.
9+
10+
## Command
11+
12+
```{eval-rst}
13+
.. argparse::
14+
:module: tmuxp.cli
15+
:func: create_parser
16+
:prog: tmuxp
17+
:path: stop
18+
```
19+
20+
## Basic Usage
21+
22+
Stop a session by name:
23+
24+
```console
25+
$ tmuxp stop mysession
26+
```
27+
28+
Stop the currently attached session:
29+
30+
```console
31+
$ tmuxp stop
32+
```
33+
34+
Use a custom socket:
35+
36+
```console
37+
$ tmuxp stop -L mysocket mysession
38+
```

docs/configuration/examples.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -797,6 +797,19 @@ The `synchronize` window key provides a shorthand for enabling
797797
```
798798
````
799799

800+
## Lifecycle Hooks
801+
802+
Run shell commands at different stages of the session lifecycle:
803+
804+
````{tab} YAML
805+
```{literalinclude} ../../examples/lifecycle-hooks.yaml
806+
:language: yaml
807+
808+
```
809+
````
810+
811+
See {ref}`top-level` for full hook documentation.
812+
800813
## Pane Titles
801814

802815
Pane title keys turn on tmux pane border titles and label individual panes:

docs/configuration/top-level.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,40 @@ Notes:
4141

4242
Above: Use `tmux` directly to attach _banana_.
4343

44+
## Lifecycle Hooks
45+
46+
Workspace configs support four lifecycle hooks:
47+
48+
```yaml
49+
session_name: myproject
50+
on_project_start: notify-send "Starting myproject"
51+
on_project_restart: notify-send "Reattaching to myproject"
52+
on_project_exit: notify-send "Detached from myproject"
53+
on_project_stop: notify-send "Stopping myproject"
54+
windows:
55+
- window_name: main
56+
panes:
57+
-
58+
```
59+
60+
| Hook | When it runs |
61+
|------|-------------|
62+
| `on_project_start` | Before a new session is built. |
63+
| `on_project_restart` | Before reattaching to an existing session. |
64+
| `on_project_exit` | When the last client detaches. |
65+
| `on_project_stop` | Before `tmuxp stop` kills the session. |
66+
67+
Each hook accepts a string command or a list of command strings:
68+
69+
```yaml
70+
on_project_start:
71+
- notify-send "Starting"
72+
- ./setup.sh
73+
```
74+
75+
Hooks run through the shell and block tmuxp until they finish. Hook failures are
76+
logged and do not stop the tmuxp command.
77+
4478
## Pane Titles
4579

4680
Enable pane border titles to display labels on each pane:

docs/internals/api/cli/index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ ls
1919
progress
2020
search
2121
shell
22+
stop
2223
utils
2324
```
2425

docs/internals/api/cli/stop.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# tmuxp stop - `tmuxp.cli.stop`
2+
3+
```{eval-rst}
4+
.. automodule:: tmuxp.cli.stop
5+
:members:
6+
:show-inheritance:
7+
:undoc-members:
8+
```

examples/lifecycle-hooks.yaml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
session_name: lifecycle hooks
2+
on_project_start: echo "project starting"
3+
on_project_restart: echo "project restarting"
4+
on_project_exit: echo "project exiting"
5+
on_project_stop: echo "project stopping"
6+
windows:
7+
- window_name: main
8+
panes:
9+
-

src/tmuxp/cli/__init__.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,12 @@
5757
command_shell,
5858
create_shell_subparser,
5959
)
60+
from .stop import (
61+
STOP_DESCRIPTION,
62+
CLIStopNamespace,
63+
command_stop,
64+
create_stop_subparser,
65+
)
6066
from .utils import tmuxp_echo
6167

6268
logger = logging.getLogger(__name__)
@@ -130,6 +136,13 @@
130136
"tmuxp edit myproject",
131137
],
132138
),
139+
(
140+
"stop",
141+
[
142+
"tmuxp stop mysession",
143+
"tmuxp stop -L mysocket mysession",
144+
],
145+
),
133146
(
134147
"debug-info",
135148
[
@@ -155,6 +168,7 @@
155168
"import",
156169
"search",
157170
"shell",
171+
"stop",
158172
"debug-info",
159173
]
160174
CLIImportSubparserName: TypeAlias = t.Literal["teamocil", "tmuxinator"]
@@ -262,6 +276,14 @@ def create_parser() -> argparse.ArgumentParser:
262276
)
263277
create_freeze_subparser(freeze_parser)
264278

279+
stop_parser = subparsers.add_parser(
280+
"stop",
281+
help="stop (kill) a tmux session",
282+
description=STOP_DESCRIPTION,
283+
formatter_class=formatter_class,
284+
)
285+
create_stop_subparser(stop_parser)
286+
265287
return parser
266288

267289

@@ -353,6 +375,11 @@ def cli(_args: list[str] | None = None) -> None:
353375
args=CLIFreezeNamespace(**vars(args)),
354376
parser=parser,
355377
)
378+
elif args.subparser_name == "stop":
379+
command_stop(
380+
args=CLIStopNamespace(**vars(args)),
381+
parser=parser,
382+
)
356383
elif args.subparser_name == "ls":
357384
command_ls(
358385
args=CLILsNamespace(**vars(args)),

0 commit comments

Comments
 (0)