Metadata-Version: 2.4
Name: tmuxctl
Version: 0.3.0
Summary: Small tmux session controller with recurring sends
Author: Alexey Grigorev
Project-URL: Homepage, https://github.com/alexeygrigorev/tmuxctl
Project-URL: Issues, https://github.com/alexeygrigorev/tmuxctl/issues
Keywords: tmux,cli,scheduler,automation
Classifier: Development Status :: 3 - Alpha
Classifier: Environment :: Console
Classifier: Intended Audience :: Developers
Classifier: Operating System :: POSIX :: Linux
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: System :: Shells
Classifier: Topic :: Utilities
Requires-Python: >=3.10
Description-Content-Type: text/markdown
Requires-Dist: click<9,>=8
Requires-Dist: typer<1,>=0.12
Requires-Dist: tomli<3,>=2; python_version < "3.11"

# tmuxctl

`tmuxctl` is a small tmux workflow helper for three things:

- finding the session you want
- jumping to it quickly
- sending recurring follow-ups to long-running agent or worker sessions

It installs two executables:

- `tmuxctl`
- `t`

`t` is just the shorter alias for the same CLI.

## Install

Primary install:

```bash
uv tool install tmuxctl
```

Then use either:

```bash
tmuxctl --help
t --help
```

Run without arguments to see the 10 most recent sessions plus shortcut hints:

```bash
tmuxctl
t
```

## Core Workflow

### 1. Find the session you want

Show all sessions, sorted by recency, with numeric IDs:

```bash
t list
```

Short form:

```bash
t l
tl
```

Typical output:

```text
IDX  SESSION               CREATED
1    codex                 2026-04-03 15:56:59
2    backend-worker        2026-04-03 15:22:10
3    docs                  2026-04-03 14:10:31
```

If you just want the recent view:

```bash
t
t r
t recent --limit 10
```

### 2. Jump into a session

Attach by name:

```bash
t codex
```

That is equivalent to:

```bash
t attach codex
```

Ask tmux to resize the window after attaching:

```bash
t git-llm-zoomcamp --resize-window
t 1 -r
```

Attach by recency index:

```bash
t 1
t 2
t 10
```

Those resolve to `attach-recent N`.

Attach to the newest session directly:

```bash
t attach-last
```

### 3. Create a session if it does not exist

Use a leading colon when you want create-or-attach behavior:

```bash
t :codex
```

That resolves to:

```bash
t create-or-attach codex
```

Rule of thumb:

- `t codex` means attach only
- `t :codex` means create or attach

Use `t -` to derive the session name from the current directory and create-or-attach it:

```bash
cd ~/git/workshops
t -
```

That resolves to:

```bash
t create-or-attach git-workshops
```

Pass a command after `t -` to run it only when a new session is created:

```bash
t - cy
```

If you want another session for the same folder, add any suffix:

```bash
cd ~/git/workshops
t -asd
```

That resolves to:

```bash
t create-or-attach git-workshops-asd
```

#### Create without attaching

`t create-detached` brings a memory-capped session into existence and returns
immediately, without occupying your terminal. It is for tools that attach over
their own transport (e.g. tmux `-CC` control mode) and would otherwise build raw,
uncapped `new-session` commands:

```bash
t create-detached myproj -c ~/git/myproj
```

It is idempotent (a no-op if the session already exists), resolves the memory cap
the same way as the other verbs (`--mem` flag → project `cgroups.toml` /
`pyproject [tool.tmuxctl]` → default), and prints the session name on success.

### 4. Send a one-off message

Send text directly:

```bash
t send codex --message "check status and continue"
```

Or send from a file:

```bash
t send rk-codex --message-file prompts/rk-codex-progress.txt
```

By default, `send` waits `200ms` before pressing Enter. You can change that:

```bash
t send codex --message "status?" --enter-delay-ms 500
t send codex --message "status?" --no-enter
```

## Automation Workflow

### 1. Add a recurring job

Inline message:

```bash
t jobs add codex --every 15m --message "check status and continue"
```

If you are already inside tmux, use `:current` to target the active session without typing its name:

```bash
t jobs add :current --every 15m --message \
  "Check project status and continue. Help any blocked agents, review CI, and \
  keep the pipeline moving. If nothing in the current batch needs attention, \
  pick the next two ready issues per _docs/PROCESS.md and run the full workflow."
```

Shared prompt file:

```bash
t jobs add rk-codex --every 30m --message-file prompts/rk-codex-progress.txt
```

When a job uses `--message-file`, `tmuxctl` stores the file path and reads the file at send time. Updating the file updates future scheduled runs.

### 2. Run the scheduler

```bash
t jobs daemon
```

Recurring jobs only run while the daemon is running.

### 3. Inspect and edit jobs

```bash
t jobs
t jobs list
t jobs show 2
t jobs logs --limit 20
t jobs edit 2 --every 45m
t jobs edit 2 --message "check status and continue"
t jobs edit 2 --session :current
t jobs edit 3 --message-file prompts/rk-codex-progress.txt
```

Useful job controls:

```bash
t jobs pause 3
t jobs pause-current
t jobs resume 3
t jobs resume-current
t jobs remove 3
```

If a scheduled job fails 3 runs in a row, `tmuxctl jobs daemon` removes it automatically.

## Session Cleanup

Kill a session by name:

```bash
t kill codex
```

Kill a session by the numeric ID shown in `t list`:

```bash
t kill 2
```

Skip confirmation:

```bash
t k 2 --yes
```

Rename a session and retarget any scheduled jobs bound to it:

```bash
t rename codex codex-main
t rename 2 archived-worker
```

## Inspect a Session

`describe` shows what is actually running inside a session — the process in each
pane, its working directory, the cgroup the session lives in, and (for sessions
started by `t` with a memory cap) live RAM and CPU usage read straight from that
cgroup. Target it by name, by the numeric ID from `t list`, or `:current`:

```bash
t describe codex            # by name
t describe 2                # by the numeric ID from `t list`
t describe :current         # the session you are in
```

For a capped session it reads memory and CPU from the session's
`tmuxctl-<name>.scope` cgroup, so the numbers cover the whole process tree, not
just the shell:

```
Session:  git-myproj  (1 window(s), 1 pane(s))

WIN.PANE  PID      COMMAND          DIRECTORY
0.0*      3564734  claude           /home/you/git/myproj

Scope:    tmuxctl-git-myproj.scope  (active)
Cgroup:   /user.slice/.../robust.slice/tmuxctl-git-myproj.scope
Memory:   3.4G / 12.0G  (peak 5.2G, swap 0B)
CPU time: 7m25s
Tasks:    222
```

A session you did **not** start through `t` has no memory cap. `describe` says so
and prints the real cgroup it found (e.g. a plain `session-NN.scope`), so you can
tell at a glance which sessions are protected and which can still take the whole
tmux server down under memory pressure:

```
Scope:    none — session is uncapped (not started by tmuxctl)
Cgroup:   /user.slice/.../session-7.scope
          No per-session RAM/CPU cap; the box-wide OOM-killer can
          take the whole tmux server. Start a capped one with:
          t :git-myproj --mem 24G
```

## Shell Setup

### Bash completion

Install completion:

```bash
t --install-completion
```

Preview the script:

```bash
t --show-completion bash
```

Completion works for:

- commands
- plain session names
- `:session` shortcuts

### Local checkout helper

If you are working from this repository and want its virtualenv binaries on your `PATH`, run:

```bash
./install.sh
```

That appends this repo's `.venv/bin` and `alias tl='t l'` to `~/.bashrc`, skipping any line that is already present.

## How Scheduling Works

Recurring jobs are stored in:

```text
~/.config/tmuxctl/tmuxctl.db
```

The scheduler is database-driven:

- `jobs add` creates jobs
- `jobs edit`, `jobs pause`, `jobs resume`, and `jobs remove` modify jobs
- `jobs daemon` polls for due jobs and runs them

If you want recurring jobs to survive logout or reboot, keep `t jobs daemon` running with something like:

- `systemd --user`
- `launchd`
- `cron @reboot`

### Running as a systemd user service (Linux)

Create `~/.config/systemd/user/tmuxctl.service`:

```ini
[Unit]
Description=tmuxctl scheduler daemon
After=default.target

[Service]
Type=simple
ExecStart=%h/.local/bin/tmuxctl jobs daemon
Restart=on-failure
RestartSec=5

[Install]
WantedBy=default.target
```

Adjust `ExecStart` to wherever `tmuxctl` is installed (for a local editable checkout, point at `.venv/bin/tmuxctl`). Then enable and start it:

```bash
systemctl --user daemon-reload
systemctl --user enable --now tmuxctl.service
systemctl --user status tmuxctl.service
```

To keep the daemon running after you log out, enable lingering for your user (needs sudo, one-time):

```bash
sudo loginctl enable-linger "$USER"
```

Logs are available via `journalctl --user -u tmuxctl -f`.

## Known Problems

### An orphaned scope can block recreating a session of the same name

Normally `t kill` tears a session's memory-capped scope down (`systemctl --user
stop tmuxctl-<name>.scope`), so the unit name is free for next time. Two things
have to go wrong together to defeat that:

1. the tmux **server dies uncleanly** (a crash or machine-wide OOM), so the
   normal kill path — and its scope teardown — never runs, **and**
2. a **disowned background process** (e.g. an `Xvfb`, a dev server, anything
   `nohup`/`&`-launched) is still running inside that session's scope.

The dead session's shell is gone, but the stray process keeps the
`tmuxctl-<name>.scope` cgroup alive. The next time you try to create a session
with the **same derived name** (e.g. `t -` from the same folder), tmuxctl asks
`systemd-run` for that unit name and it fails with *"Unit
tmuxctl-<name>.scope was already loaded"*. The new tmux pane's command dies on
launch, and instead of a clear error you usually see terminal escape codes
(a Device-Attributes reply such as `^[[?61;...c`) leak onto your prompt as the
aborted tmux client exits.

**Diagnose** — look for a scope whose tmux session no longer exists:

```bash
systemctl --user list-units --type=scope --all 'tmuxctl-*'
systemctl --user status tmuxctl-<name>.scope   # shows the stray process holding it open
```

**Fix** — stop the orphan scope (this also kills the stray process inside it),
then create the session again:

```bash
systemctl --user stop tmuxctl-<name>.scope
```

This is rare (it needs an unclean crash *and* a backgrounded process), so it is
documented rather than worked around. A future version may auto-recover by
clearing a stale scope whose session is gone before creating a new one.

## Alternatives

Install with `pip`:

```bash
pip install tmuxctl
```

Install directly from GitHub:

```bash
uv tool install git+https://github.com/alexeygrigorev/tmuxctl.git
```

Install from a local checkout in editable mode:

```bash
git clone https://github.com/alexeygrigorev/tmuxctl.git
cd tmuxctl
uv tool install -e .
```

If you use the local checkout install, also run:

```bash
./install.sh
```

Reinstall the local checkout after updates:

```bash
uv tool install -e . --force
```

For development:

```bash
uv sync --dev
uv run pytest
uv build
```
