tmuxctl 0.1.1__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.
- tmuxctl-0.1.1/PKG-INFO +245 -0
- tmuxctl-0.1.1/README.md +223 -0
- tmuxctl-0.1.1/pyproject.toml +48 -0
- tmuxctl-0.1.1/setup.cfg +4 -0
- tmuxctl-0.1.1/tests/test_cli.py +159 -0
- tmuxctl-0.1.1/tests/test_scheduler.py +98 -0
- tmuxctl-0.1.1/tests/test_storage.py +83 -0
- tmuxctl-0.1.1/tests/test_utils.py +38 -0
- tmuxctl-0.1.1/tmuxctl/__init__.py +3 -0
- tmuxctl-0.1.1/tmuxctl/__main__.py +5 -0
- tmuxctl-0.1.1/tmuxctl/cli.py +442 -0
- tmuxctl-0.1.1/tmuxctl/models.py +40 -0
- tmuxctl-0.1.1/tmuxctl/scheduler.py +78 -0
- tmuxctl-0.1.1/tmuxctl/storage.py +330 -0
- tmuxctl-0.1.1/tmuxctl/tmux_api.py +139 -0
- tmuxctl-0.1.1/tmuxctl/utils.py +57 -0
- tmuxctl-0.1.1/tmuxctl.egg-info/PKG-INFO +245 -0
- tmuxctl-0.1.1/tmuxctl.egg-info/SOURCES.txt +20 -0
- tmuxctl-0.1.1/tmuxctl.egg-info/dependency_links.txt +1 -0
- tmuxctl-0.1.1/tmuxctl.egg-info/entry_points.txt +2 -0
- tmuxctl-0.1.1/tmuxctl.egg-info/requires.txt +1 -0
- tmuxctl-0.1.1/tmuxctl.egg-info/top_level.txt +1 -0
tmuxctl-0.1.1/PKG-INFO
ADDED
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: tmuxctl
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: Small tmux session controller with recurring sends
|
|
5
|
+
Author: Alexey Grigorev
|
|
6
|
+
Project-URL: Homepage, https://github.com/alexeygrigorev/tmuxctl
|
|
7
|
+
Project-URL: Issues, https://github.com/alexeygrigorev/tmuxctl/issues
|
|
8
|
+
Keywords: tmux,cli,scheduler,automation
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Environment :: Console
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Topic :: System :: Shells
|
|
18
|
+
Classifier: Topic :: Utilities
|
|
19
|
+
Requires-Python: >=3.10
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
Requires-Dist: typer<1,>=0.12
|
|
22
|
+
|
|
23
|
+
# tmuxctl
|
|
24
|
+
|
|
25
|
+
Small tmux session controller with recurring sends.
|
|
26
|
+
|
|
27
|
+
`tmuxctl` lets you:
|
|
28
|
+
|
|
29
|
+
- list tmux sessions by name
|
|
30
|
+
- show the most recent sessions by creation time or activity
|
|
31
|
+
- attach to a named session or jump to the most recent ones quickly
|
|
32
|
+
- send a message to a session's active pane
|
|
33
|
+
- store recurring jobs in SQLite
|
|
34
|
+
- run a lightweight daemon loop that executes due jobs
|
|
35
|
+
|
|
36
|
+
## Install
|
|
37
|
+
|
|
38
|
+
Install from PyPI with `uv`:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
uv tool install tmuxctl
|
|
42
|
+
tmuxctl --help
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Or with `pip`:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
pip install tmuxctl
|
|
49
|
+
tmuxctl --help
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Install from GitHub with `uv`:
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
uv tool install git+https://github.com/alexeygrigorev/tmuxctl.git
|
|
56
|
+
tmuxctl --help
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Install from a local checkout in editable mode:
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
git clone https://github.com/alexeygrigorev/tmuxctl.git
|
|
63
|
+
cd tmuxctl
|
|
64
|
+
uv tool install -e .
|
|
65
|
+
tmuxctl --help
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
If you update the local checkout later, reinstall with:
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
uv tool install -e . --force
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
For development, tests, and builds:
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
uv sync --dev
|
|
78
|
+
uv run pytest
|
|
79
|
+
uv build
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Usage
|
|
83
|
+
|
|
84
|
+
List sessions:
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
tmuxctl list
|
|
88
|
+
tmuxctl recent --limit 10
|
|
89
|
+
tmuxctl recent --limit 10 --by activity
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Attach to a session directly or jump to the newest one:
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
tmuxctl attach codex
|
|
96
|
+
tmuxctl create-or-attach codex
|
|
97
|
+
tmuxctl :codex
|
|
98
|
+
tmuxctl attach-last
|
|
99
|
+
tmuxctl attach-last --by activity
|
|
100
|
+
tmuxctl attach-recent 2
|
|
101
|
+
tmuxctl attach-recent 3 --by activity
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
`tmuxctl :codex` is shorthand for `tmuxctl create-or-attach codex`.
|
|
105
|
+
|
|
106
|
+
There are also short hidden aliases for the most recent sessions:
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
tmuxctl a1
|
|
110
|
+
tmuxctl a2
|
|
111
|
+
tmuxctl a3
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Send one message now:
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
tmuxctl send codex "check status and fix if something is broken or stuck"
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Send one message from a file:
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
tmuxctl send rk-codex --message-file prompts/rk-codex-progress.txt
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
By default, `tmuxctl send` waits `200ms` before pressing Return. Override that with `--enter-delay-ms` or disable Return with `--no-enter`.
|
|
127
|
+
|
|
128
|
+
Create a recurring job:
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
tmuxctl add codex --every 15m --message "check status and fix if something is broken or stuck"
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Recurring jobs also store an Enter delay. By default that is `200ms`, and you can override it with `--enter-delay-ms`.
|
|
135
|
+
|
|
136
|
+
Example: send an automated follow-up to `rk-codex` every 30 minutes:
|
|
137
|
+
|
|
138
|
+
```bash
|
|
139
|
+
tmuxctl add rk-codex --every 30m --message-file prompts/rk-codex-progress.txt
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
You can load message text from a file with `--message-file` for `tmuxctl send`, `tmuxctl add`, and `tmuxctl edit`.
|
|
143
|
+
|
|
144
|
+
For `tmuxctl add` and `tmuxctl edit`, the file path is stored with the job. Scheduled runs read the file at send time, so updating the prompt file changes future runs without recreating the job.
|
|
145
|
+
|
|
146
|
+
To switch an existing job to the shared prompt file:
|
|
147
|
+
|
|
148
|
+
```bash
|
|
149
|
+
tmuxctl jobs
|
|
150
|
+
tmuxctl edit <job_id> --message-file prompts/rk-codex-progress.txt
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
Example: check a worker session every 30 minutes and unblock stalled progress:
|
|
154
|
+
|
|
155
|
+
```bash
|
|
156
|
+
tmuxctl add lnewly-57 --every 30m --message "Status check for litehive: report current progress, current task, and the last meaningful change. Check whether progress is stalled, not whether you personally feel stuck. Identify blockers, lack of movement, repeated retries, failing commands, broken states, or missing dependencies. If progress is stalled, choose the best next concrete action to unblock litehive and execute it. Fix any problems you can fix now, then continue the work and summarize what changed."
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
Edit an existing job:
|
|
160
|
+
|
|
161
|
+
```bash
|
|
162
|
+
tmuxctl edit 2 --every 45m
|
|
163
|
+
tmuxctl edit 2 --message "check status and continue"
|
|
164
|
+
tmuxctl edit 3 --message-file prompts/rk-codex-progress.txt
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
Remove a job:
|
|
168
|
+
|
|
169
|
+
```bash
|
|
170
|
+
tmuxctl remove 3
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
List jobs and logs:
|
|
174
|
+
|
|
175
|
+
```bash
|
|
176
|
+
tmuxctl jobs
|
|
177
|
+
tmuxctl logs --limit 20
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
`tmuxctl jobs` shows whether a job uses inline text or a linked file prompt.
|
|
181
|
+
|
|
182
|
+
Logs include the target session, whether the send was manual or scheduled, whether Return was sent, the Enter delay used, and any recorded error text.
|
|
183
|
+
|
|
184
|
+
If a scheduled job fails 3 runs in a row, `tmuxctl daemon` removes it automatically.
|
|
185
|
+
|
|
186
|
+
Run the scheduler:
|
|
187
|
+
|
|
188
|
+
```bash
|
|
189
|
+
tmuxctl daemon
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
## Shortcuts
|
|
193
|
+
|
|
194
|
+
Useful shortcuts for hopping between recent sessions:
|
|
195
|
+
|
|
196
|
+
- `tmuxctl attach-last`
|
|
197
|
+
- `tmuxctl attach-recent 2`
|
|
198
|
+
- `tmuxctl attach-recent 3`
|
|
199
|
+
- `tmuxctl a1`
|
|
200
|
+
- `tmuxctl a2`
|
|
201
|
+
- `tmuxctl a3`
|
|
202
|
+
|
|
203
|
+
## How Scheduling Works
|
|
204
|
+
|
|
205
|
+
`tmuxctl` does not create cron entries and it does not require editing `crontab`.
|
|
206
|
+
|
|
207
|
+
Recurring jobs are stored in SQLite at:
|
|
208
|
+
|
|
209
|
+
```text
|
|
210
|
+
~/.config/tmuxctl/tmuxctl.db
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
The commands work like this:
|
|
214
|
+
|
|
215
|
+
- `tmuxctl add ...` inserts a recurring job into the database
|
|
216
|
+
- `tmuxctl edit`, `pause`, `resume`, and `remove` update that stored job
|
|
217
|
+
- `tmuxctl daemon` polls the database for due jobs and runs them
|
|
218
|
+
|
|
219
|
+
That means recurring sends only happen while the daemon is running.
|
|
220
|
+
|
|
221
|
+
If you want jobs to keep running after logout or reboot, use an external process manager to keep the daemon alive, for example:
|
|
222
|
+
|
|
223
|
+
- `systemd --user` on Linux
|
|
224
|
+
- `launchd` on macOS
|
|
225
|
+
- `cron @reboot` as a fallback
|
|
226
|
+
|
|
227
|
+
Even in those setups, cron or systemd only starts `tmuxctl daemon`. The recurring schedule itself still lives in the `tmuxctl` database.
|
|
228
|
+
|
|
229
|
+
## Bash Completion
|
|
230
|
+
|
|
231
|
+
`tmuxctl` includes shell completion through Typer.
|
|
232
|
+
|
|
233
|
+
Install completion for your current Bash setup:
|
|
234
|
+
|
|
235
|
+
```bash
|
|
236
|
+
tmuxctl --install-completion
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
Preview or manually wire the Bash completion script:
|
|
240
|
+
|
|
241
|
+
```bash
|
|
242
|
+
tmuxctl --show-completion bash
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
Session-taking commands also complete existing tmux session names in Bash.
|
tmuxctl-0.1.1/README.md
ADDED
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
# tmuxctl
|
|
2
|
+
|
|
3
|
+
Small tmux session controller with recurring sends.
|
|
4
|
+
|
|
5
|
+
`tmuxctl` lets you:
|
|
6
|
+
|
|
7
|
+
- list tmux sessions by name
|
|
8
|
+
- show the most recent sessions by creation time or activity
|
|
9
|
+
- attach to a named session or jump to the most recent ones quickly
|
|
10
|
+
- send a message to a session's active pane
|
|
11
|
+
- store recurring jobs in SQLite
|
|
12
|
+
- run a lightweight daemon loop that executes due jobs
|
|
13
|
+
|
|
14
|
+
## Install
|
|
15
|
+
|
|
16
|
+
Install from PyPI with `uv`:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
uv tool install tmuxctl
|
|
20
|
+
tmuxctl --help
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Or with `pip`:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
pip install tmuxctl
|
|
27
|
+
tmuxctl --help
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Install from GitHub with `uv`:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
uv tool install git+https://github.com/alexeygrigorev/tmuxctl.git
|
|
34
|
+
tmuxctl --help
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Install from a local checkout in editable mode:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
git clone https://github.com/alexeygrigorev/tmuxctl.git
|
|
41
|
+
cd tmuxctl
|
|
42
|
+
uv tool install -e .
|
|
43
|
+
tmuxctl --help
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
If you update the local checkout later, reinstall with:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
uv tool install -e . --force
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
For development, tests, and builds:
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
uv sync --dev
|
|
56
|
+
uv run pytest
|
|
57
|
+
uv build
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Usage
|
|
61
|
+
|
|
62
|
+
List sessions:
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
tmuxctl list
|
|
66
|
+
tmuxctl recent --limit 10
|
|
67
|
+
tmuxctl recent --limit 10 --by activity
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Attach to a session directly or jump to the newest one:
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
tmuxctl attach codex
|
|
74
|
+
tmuxctl create-or-attach codex
|
|
75
|
+
tmuxctl :codex
|
|
76
|
+
tmuxctl attach-last
|
|
77
|
+
tmuxctl attach-last --by activity
|
|
78
|
+
tmuxctl attach-recent 2
|
|
79
|
+
tmuxctl attach-recent 3 --by activity
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
`tmuxctl :codex` is shorthand for `tmuxctl create-or-attach codex`.
|
|
83
|
+
|
|
84
|
+
There are also short hidden aliases for the most recent sessions:
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
tmuxctl a1
|
|
88
|
+
tmuxctl a2
|
|
89
|
+
tmuxctl a3
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Send one message now:
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
tmuxctl send codex "check status and fix if something is broken or stuck"
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Send one message from a file:
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
tmuxctl send rk-codex --message-file prompts/rk-codex-progress.txt
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
By default, `tmuxctl send` waits `200ms` before pressing Return. Override that with `--enter-delay-ms` or disable Return with `--no-enter`.
|
|
105
|
+
|
|
106
|
+
Create a recurring job:
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
tmuxctl add codex --every 15m --message "check status and fix if something is broken or stuck"
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Recurring jobs also store an Enter delay. By default that is `200ms`, and you can override it with `--enter-delay-ms`.
|
|
113
|
+
|
|
114
|
+
Example: send an automated follow-up to `rk-codex` every 30 minutes:
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
tmuxctl add rk-codex --every 30m --message-file prompts/rk-codex-progress.txt
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
You can load message text from a file with `--message-file` for `tmuxctl send`, `tmuxctl add`, and `tmuxctl edit`.
|
|
121
|
+
|
|
122
|
+
For `tmuxctl add` and `tmuxctl edit`, the file path is stored with the job. Scheduled runs read the file at send time, so updating the prompt file changes future runs without recreating the job.
|
|
123
|
+
|
|
124
|
+
To switch an existing job to the shared prompt file:
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
tmuxctl jobs
|
|
128
|
+
tmuxctl edit <job_id> --message-file prompts/rk-codex-progress.txt
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Example: check a worker session every 30 minutes and unblock stalled progress:
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
tmuxctl add lnewly-57 --every 30m --message "Status check for litehive: report current progress, current task, and the last meaningful change. Check whether progress is stalled, not whether you personally feel stuck. Identify blockers, lack of movement, repeated retries, failing commands, broken states, or missing dependencies. If progress is stalled, choose the best next concrete action to unblock litehive and execute it. Fix any problems you can fix now, then continue the work and summarize what changed."
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
Edit an existing job:
|
|
138
|
+
|
|
139
|
+
```bash
|
|
140
|
+
tmuxctl edit 2 --every 45m
|
|
141
|
+
tmuxctl edit 2 --message "check status and continue"
|
|
142
|
+
tmuxctl edit 3 --message-file prompts/rk-codex-progress.txt
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
Remove a job:
|
|
146
|
+
|
|
147
|
+
```bash
|
|
148
|
+
tmuxctl remove 3
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
List jobs and logs:
|
|
152
|
+
|
|
153
|
+
```bash
|
|
154
|
+
tmuxctl jobs
|
|
155
|
+
tmuxctl logs --limit 20
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
`tmuxctl jobs` shows whether a job uses inline text or a linked file prompt.
|
|
159
|
+
|
|
160
|
+
Logs include the target session, whether the send was manual or scheduled, whether Return was sent, the Enter delay used, and any recorded error text.
|
|
161
|
+
|
|
162
|
+
If a scheduled job fails 3 runs in a row, `tmuxctl daemon` removes it automatically.
|
|
163
|
+
|
|
164
|
+
Run the scheduler:
|
|
165
|
+
|
|
166
|
+
```bash
|
|
167
|
+
tmuxctl daemon
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
## Shortcuts
|
|
171
|
+
|
|
172
|
+
Useful shortcuts for hopping between recent sessions:
|
|
173
|
+
|
|
174
|
+
- `tmuxctl attach-last`
|
|
175
|
+
- `tmuxctl attach-recent 2`
|
|
176
|
+
- `tmuxctl attach-recent 3`
|
|
177
|
+
- `tmuxctl a1`
|
|
178
|
+
- `tmuxctl a2`
|
|
179
|
+
- `tmuxctl a3`
|
|
180
|
+
|
|
181
|
+
## How Scheduling Works
|
|
182
|
+
|
|
183
|
+
`tmuxctl` does not create cron entries and it does not require editing `crontab`.
|
|
184
|
+
|
|
185
|
+
Recurring jobs are stored in SQLite at:
|
|
186
|
+
|
|
187
|
+
```text
|
|
188
|
+
~/.config/tmuxctl/tmuxctl.db
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
The commands work like this:
|
|
192
|
+
|
|
193
|
+
- `tmuxctl add ...` inserts a recurring job into the database
|
|
194
|
+
- `tmuxctl edit`, `pause`, `resume`, and `remove` update that stored job
|
|
195
|
+
- `tmuxctl daemon` polls the database for due jobs and runs them
|
|
196
|
+
|
|
197
|
+
That means recurring sends only happen while the daemon is running.
|
|
198
|
+
|
|
199
|
+
If you want jobs to keep running after logout or reboot, use an external process manager to keep the daemon alive, for example:
|
|
200
|
+
|
|
201
|
+
- `systemd --user` on Linux
|
|
202
|
+
- `launchd` on macOS
|
|
203
|
+
- `cron @reboot` as a fallback
|
|
204
|
+
|
|
205
|
+
Even in those setups, cron or systemd only starts `tmuxctl daemon`. The recurring schedule itself still lives in the `tmuxctl` database.
|
|
206
|
+
|
|
207
|
+
## Bash Completion
|
|
208
|
+
|
|
209
|
+
`tmuxctl` includes shell completion through Typer.
|
|
210
|
+
|
|
211
|
+
Install completion for your current Bash setup:
|
|
212
|
+
|
|
213
|
+
```bash
|
|
214
|
+
tmuxctl --install-completion
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
Preview or manually wire the Bash completion script:
|
|
218
|
+
|
|
219
|
+
```bash
|
|
220
|
+
tmuxctl --show-completion bash
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
Session-taking commands also complete existing tmux session names in Bash.
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "tmuxctl"
|
|
3
|
+
version = "0.1.1"
|
|
4
|
+
description = "Small tmux session controller with recurring sends"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.10"
|
|
7
|
+
authors = [
|
|
8
|
+
{ name = "Alexey Grigorev" },
|
|
9
|
+
]
|
|
10
|
+
keywords = ["tmux", "cli", "scheduler", "automation"]
|
|
11
|
+
classifiers = [
|
|
12
|
+
"Development Status :: 3 - Alpha",
|
|
13
|
+
"Environment :: Console",
|
|
14
|
+
"Intended Audience :: Developers",
|
|
15
|
+
"Operating System :: POSIX :: Linux",
|
|
16
|
+
"Programming Language :: Python :: 3",
|
|
17
|
+
"Programming Language :: Python :: 3.10",
|
|
18
|
+
"Programming Language :: Python :: 3.11",
|
|
19
|
+
"Programming Language :: Python :: 3.12",
|
|
20
|
+
"Topic :: System :: Shells",
|
|
21
|
+
"Topic :: Utilities",
|
|
22
|
+
]
|
|
23
|
+
dependencies = [
|
|
24
|
+
"typer>=0.12,<1",
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
[project.scripts]
|
|
28
|
+
tmuxctl = "tmuxctl.cli:main"
|
|
29
|
+
|
|
30
|
+
[project.urls]
|
|
31
|
+
Homepage = "https://github.com/alexeygrigorev/tmuxctl"
|
|
32
|
+
Issues = "https://github.com/alexeygrigorev/tmuxctl/issues"
|
|
33
|
+
|
|
34
|
+
[dependency-groups]
|
|
35
|
+
dev = [
|
|
36
|
+
"hatch>=1,<2",
|
|
37
|
+
"pytest>=8,<9",
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
[tool.pytest.ini_options]
|
|
41
|
+
testpaths = ["tests"]
|
|
42
|
+
|
|
43
|
+
[tool.setuptools.packages.find]
|
|
44
|
+
include = ["tmuxctl*"]
|
|
45
|
+
|
|
46
|
+
[build-system]
|
|
47
|
+
requires = ["setuptools>=68"]
|
|
48
|
+
build-backend = "setuptools.build_meta"
|
tmuxctl-0.1.1/setup.cfg
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from typer.testing import CliRunner
|
|
7
|
+
|
|
8
|
+
from tmuxctl import cli
|
|
9
|
+
from tmuxctl.cli import app
|
|
10
|
+
from tmuxctl.models import Job
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
runner = CliRunner()
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def test_send_reads_message_from_file(monkeypatch, tmp_path: Path) -> None:
|
|
17
|
+
message_file = tmp_path / "message.txt"
|
|
18
|
+
message_file.write_text("line 1\nline 2\n", encoding="utf-8")
|
|
19
|
+
captured: dict[str, object] = {}
|
|
20
|
+
|
|
21
|
+
monkeypatch.setattr("tmuxctl.cli._conn", lambda: object())
|
|
22
|
+
monkeypatch.setattr("tmuxctl.cli.tmux_api.session_exists", lambda session_name: True)
|
|
23
|
+
|
|
24
|
+
def fake_send_keys(session_name: str, message: str, press_enter: bool, enter_delay_ms: int) -> None:
|
|
25
|
+
captured["session_name"] = session_name
|
|
26
|
+
captured["message"] = message
|
|
27
|
+
captured["press_enter"] = press_enter
|
|
28
|
+
captured["enter_delay_ms"] = enter_delay_ms
|
|
29
|
+
|
|
30
|
+
monkeypatch.setattr("tmuxctl.cli.tmux_api.send_keys", fake_send_keys)
|
|
31
|
+
monkeypatch.setattr("tmuxctl.cli.storage.insert_log", lambda *args, **kwargs: None)
|
|
32
|
+
|
|
33
|
+
result = runner.invoke(app, ["send", "rk-codex", "--message-file", str(message_file)])
|
|
34
|
+
|
|
35
|
+
assert result.exit_code == 0
|
|
36
|
+
assert captured["session_name"] == "rk-codex"
|
|
37
|
+
assert captured["message"] == "line 1\nline 2"
|
|
38
|
+
assert captured["press_enter"] is True
|
|
39
|
+
assert captured["enter_delay_ms"] == 200
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def test_add_rejects_both_message_and_message_file(monkeypatch, tmp_path: Path) -> None:
|
|
43
|
+
message_file = tmp_path / "message.txt"
|
|
44
|
+
message_file.write_text("hello", encoding="utf-8")
|
|
45
|
+
|
|
46
|
+
monkeypatch.setattr("tmuxctl.cli.tmux_api.session_exists", lambda session_name: True)
|
|
47
|
+
|
|
48
|
+
result = runner.invoke(
|
|
49
|
+
app,
|
|
50
|
+
[
|
|
51
|
+
"add",
|
|
52
|
+
"rk-codex",
|
|
53
|
+
"--every",
|
|
54
|
+
"30m",
|
|
55
|
+
"--message",
|
|
56
|
+
"hello",
|
|
57
|
+
"--message-file",
|
|
58
|
+
str(message_file),
|
|
59
|
+
],
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
assert result.exit_code == 1
|
|
63
|
+
assert "choose either --message or --message-file, not both" in result.output
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def test_add_stores_message_file_path(monkeypatch, tmp_path: Path) -> None:
|
|
67
|
+
message_file = tmp_path / "message.txt"
|
|
68
|
+
message_file.write_text("hello from file\n", encoding="utf-8")
|
|
69
|
+
captured: dict[str, object] = {}
|
|
70
|
+
|
|
71
|
+
monkeypatch.setattr("tmuxctl.cli.tmux_api.session_exists", lambda session_name: True)
|
|
72
|
+
monkeypatch.setattr("tmuxctl.cli._conn", lambda: object())
|
|
73
|
+
monkeypatch.setattr("tmuxctl.cli.parse_interval", lambda value: 1800)
|
|
74
|
+
|
|
75
|
+
class DummyJob:
|
|
76
|
+
id = 7
|
|
77
|
+
session_name = "rk-codex"
|
|
78
|
+
interval_seconds = 1800
|
|
79
|
+
|
|
80
|
+
def fake_create_job(conn, **kwargs):
|
|
81
|
+
captured.update(kwargs)
|
|
82
|
+
return DummyJob()
|
|
83
|
+
|
|
84
|
+
monkeypatch.setattr("tmuxctl.cli.storage.create_job", fake_create_job)
|
|
85
|
+
|
|
86
|
+
result = runner.invoke(
|
|
87
|
+
app,
|
|
88
|
+
["add", "rk-codex", "--every", "30m", "--message-file", str(message_file)],
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
assert result.exit_code == 0
|
|
92
|
+
assert captured["message"] == "hello from file"
|
|
93
|
+
assert captured["message_file_path"] == str(message_file)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def test_jobs_shows_inline_and_file_sources(monkeypatch) -> None:
|
|
97
|
+
monkeypatch.setattr("tmuxctl.cli._conn", lambda: object())
|
|
98
|
+
monkeypatch.setattr(
|
|
99
|
+
"tmuxctl.cli.storage.list_jobs",
|
|
100
|
+
lambda conn: [
|
|
101
|
+
Job(
|
|
102
|
+
id=1,
|
|
103
|
+
session_name="inline",
|
|
104
|
+
message="short inline prompt",
|
|
105
|
+
message_file_path=None,
|
|
106
|
+
interval_seconds=900,
|
|
107
|
+
enabled=True,
|
|
108
|
+
send_enter=True,
|
|
109
|
+
enter_delay_ms=200,
|
|
110
|
+
created_at="2026-04-03T00:00:00+00:00",
|
|
111
|
+
updated_at="2026-04-03T00:00:00+00:00",
|
|
112
|
+
last_run_at=None,
|
|
113
|
+
next_run_at="2026-04-03T00:15:00+00:00",
|
|
114
|
+
),
|
|
115
|
+
Job(
|
|
116
|
+
id=2,
|
|
117
|
+
session_name="linked",
|
|
118
|
+
message="stored snapshot",
|
|
119
|
+
message_file_path="prompts/rk-codex-progress.txt",
|
|
120
|
+
interval_seconds=1800,
|
|
121
|
+
enabled=True,
|
|
122
|
+
send_enter=True,
|
|
123
|
+
enter_delay_ms=200,
|
|
124
|
+
created_at="2026-04-03T00:00:00+00:00",
|
|
125
|
+
updated_at="2026-04-03T00:00:00+00:00",
|
|
126
|
+
last_run_at=None,
|
|
127
|
+
next_run_at="2026-04-03T00:30:00+00:00",
|
|
128
|
+
),
|
|
129
|
+
],
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
result = runner.invoke(app, ["jobs"])
|
|
133
|
+
|
|
134
|
+
assert result.exit_code == 0
|
|
135
|
+
assert "SOURCE" in result.output
|
|
136
|
+
assert "inline" in result.output
|
|
137
|
+
assert "file" in result.output
|
|
138
|
+
assert "short inline prompt" in result.output
|
|
139
|
+
assert "prompts/rk-codex-progress.txt" in result.output
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def test_complete_session_names_filters_matches(monkeypatch) -> None:
|
|
143
|
+
monkeypatch.setattr("tmuxctl.cli.tmux_api.list_sessions", lambda: ["rk-codex", "rk-worker", "other"])
|
|
144
|
+
|
|
145
|
+
assert cli._complete_session_names("rk-") == ["rk-codex", "rk-worker"]
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def test_main_rewrites_colon_shortcut(monkeypatch) -> None:
|
|
149
|
+
captured: dict[str, object] = {}
|
|
150
|
+
|
|
151
|
+
def fake_app(*, args):
|
|
152
|
+
captured["args"] = args
|
|
153
|
+
|
|
154
|
+
monkeypatch.setattr(cli, "app", fake_app)
|
|
155
|
+
monkeypatch.setattr(sys, "argv", ["tmuxctl", ":rk-codex"])
|
|
156
|
+
|
|
157
|
+
cli.main()
|
|
158
|
+
|
|
159
|
+
assert captured["args"] == ["create-or-attach", "rk-codex"]
|