Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
79 commits
Select commit Hold shift + click to select a range
c338b27
ai(claude[plugin]): add tmux-parity plugin for feature parity analysis
tony Mar 24, 2026
5fc6e5d
ai(claude[plugin]): move agents/commands/skills to project root for a…
tony Mar 26, 2026
e822d30
ai(claude[plugin]): move commands/agents to .claude/ for slash comman…
tony Mar 26, 2026
cf661bf
Pane(feat[send_keys]): add reset, copy-mode, repeat, hex, format, cli…
tony Mar 26, 2026
6881d32
Pane(feat[select]): add direction, last, zoom, mark, and input-toggle…
tony Mar 26, 2026
b2a7923
Pane(feat[display_message]): add format, verbose, delay, notify, list…
tony Mar 26, 2026
9dce2c4
Window(feat[select_layout]): add spread, next, and previous flags
tony Mar 26, 2026
a402132
Window(feat[move_window]): add after, before, no-select, kill, and re…
tony Mar 26, 2026
f279d1a
Server(feat[new_session]): add detach-others, no-size, config flags
tony Mar 26, 2026
8c4ca4d
Session(feat[new_window]): add kill-existing and select-existing flags
tony Mar 26, 2026
680ac34
Pane(feat[split]): add percentage parameter for split-window
tony Mar 26, 2026
240c88e
Pane(feat[capture_pane]): add alternate-screen, quiet, and escape-mar…
tony Mar 26, 2026
186ebd1
Options,Env(feat): add quiet/values-only to show_options, format/hidd…
tony Mar 26, 2026
7df463a
Pane(feat[clear_history]): add clear_history() wrapping tmux clear-hi…
tony Mar 26, 2026
663569f
Pane,Window(feat[swap]): add swap() wrapping tmux swap-pane and swap-…
tony Mar 26, 2026
992ec48
Pane(feat[break_pane]): add break_pane() wrapping tmux break-pane
tony Mar 26, 2026
20d81a3
Pane(feat[join]): add join() wrapping tmux join-pane
tony Mar 26, 2026
4d4b4ce
Pane,Window(feat[respawn]): add respawn() wrapping tmux respawn-pane/…
tony Mar 26, 2026
5d50f02
Pane(feat[pipe]): add pipe() wrapping tmux pipe-pane
tony Mar 26, 2026
c51df12
Server(feat[run_shell]): add run_shell() wrapping tmux run-shell
tony Mar 26, 2026
838c307
Session,Window(feat): add last_window, next_window, previous_window, …
tony Mar 26, 2026
86a3ed8
Window,Server(feat): add link, unlink, and wait_for commands
tony Mar 26, 2026
726ebbe
Server(feat[buffers]): add set_buffer, show_buffer, delete_buffer wra…
tony Mar 26, 2026
8842e7f
Server(feat[buffers]): add save_buffer, load_buffer, list_buffers for…
tony Mar 26, 2026
5456c71
Pane(feat[paste_buffer]): add paste_buffer() wrapping tmux paste-buffer
tony Mar 26, 2026
0dd5e75
Pane(feat[display_popup]): add display_popup() wrapping tmux display-…
tony Mar 26, 2026
b61ad52
Revert "Pane(feat[display_popup]): add display_popup() wrapping tmux …
tony Mar 26, 2026
efccbd7
Server(feat[list_clients]): add list_clients() wrapping tmux list-cli…
tony Mar 26, 2026
244c827
Server(feat[source_file]): add source_file() wrapping tmux source-file
tony Mar 26, 2026
f76de55
Server(feat[if_shell]): add if_shell() wrapping tmux if-shell
tony Mar 26, 2026
7dde33c
test(control_mode): add ControlMode context manager for client-depend…
tony Mar 26, 2026
d477f2b
Session(feat[detach_client]): add detach_client() wrapping tmux detac…
tony Mar 26, 2026
574fe78
Pane(feat[display_popup]): add display_popup() wrapping tmux display-…
tony Mar 26, 2026
e0b0d61
Server,Pane(feat): add show_messages, prompt_history, send_prefix com…
tony Mar 26, 2026
472ca32
Server(feat): add bind_key, unbind_key, list_keys, list_commands
tony Mar 26, 2026
b7a72c7
Server,Session(feat): add start_server, lock_session
tony Mar 26, 2026
1c31abb
Server(feat): add lock_server, lock_client, refresh_client, suspend_c…
tony Mar 26, 2026
4e1f254
Pane(feat): add copy_mode, clock_mode, choose_buffer/client/tree, cus…
tony Mar 26, 2026
8e46a88
docs(parity): update command-mapping reference to reflect current cov…
tony Mar 26, 2026
bb33ba5
feat: fill missing flag gaps on recently added commands
tony Mar 26, 2026
7b59620
Server(feat): add confirm_before, command_prompt wrapping interactive…
tony Mar 26, 2026
26d2a4a
Server(feat[command_prompt]): fill missing flag gaps
tony Mar 27, 2026
796f546
Pane(feat): fill missing flag gaps on interactive commands
tony Mar 27, 2026
78e6797
Server(feat[display_menu]): add display_menu() wrapping tmux display-…
tony Mar 27, 2026
4779811
docs(parity): update command-mapping to reflect 100% effective coverage
tony Mar 27, 2026
c16acb3
Window(feat): add next_layout, previous_layout wrapping real tmux com…
tony Mar 27, 2026
a9b5187
Window(feat[last_pane]): call last-pane directly instead of select-pa…
tony Mar 27, 2026
fd744b2
Pane(feat[move]): add move() wrapping tmux move-pane directly
tony Mar 27, 2026
e567e9f
docs(doctests): annotate untestable interactive commands
tony Mar 28, 2026
c744317
Pane(fix[display_popup]): correct border_style flag and add style param
tony Mar 28, 2026
65838ca
Pane(fix[display_popup]): use fused -e flag format for consistency
tony Mar 28, 2026
6caa471
docs(versionadded): annotate new params on existing methods with 0.45
tony Mar 28, 2026
f8571a6
Pane,Window(fix[respawn]): use fused -c flag format for consistency
tony Mar 28, 2026
cae1828
Window(fix[last_pane]): correct flag mapping for -d/-e flags
tony Mar 28, 2026
8b4496e
Window(fix[split]): forward percentage parameter to Pane.split
tony Mar 28, 2026
2ef0eb2
Window(fix[select_layout]): use -p flag for previous layout, not -o
tony Mar 28, 2026
b839e1c
Server(fix[new_session]): -f sets client flags, not config file path
tony Mar 28, 2026
e7c183a
Server(fix[run_shell]): -C means tmux command, not capture output
tony Mar 28, 2026
06f9430
Server(fix[command_prompt]): -N means numeric input, not no-execute
tony Mar 28, 2026
759470a
Pane(fix[capture_pane]): -M captures mode screen, not escape markup
tony Mar 28, 2026
fbd1d79
Pane(fix[clear_history]): -H resets hyperlinks, not clears pane content
tony Mar 28, 2026
21d9205
Pane(fix[copy_mode]): -q cancels modes, -e exits on scroll to bottom
tony Mar 28, 2026
008b8b2
Pane(fix[paste_buffer]): -r changes separator to newline, not no-trai…
tony Mar 28, 2026
d521438
Pane(fix[choose_tree]): -s/-w mean collapsed, not only-show
tony Mar 28, 2026
3ebd0b5
Window(fix[rotate]): don't always inject -D, respect tmux default
tony Mar 28, 2026
87ff927
Server(fix[confirm_before,command_prompt]): -b requires tmux 3.3+, no…
tony Mar 28, 2026
c4aba2a
Pane(fix[display_popup]): -C closes existing popup, not close-on-success
tony Mar 28, 2026
b00d5bb
Window(fix[move_window]): -r is standalone renumber, not renumber-aft…
tony Mar 28, 2026
1bcd261
Session(fix[detach_client]): preserve client targeting semantics
tony Mar 29, 2026
a35f523
Window(fix[move_window]): refresh moved window state
tony Mar 29, 2026
73cbfd0
ControlMode(fix[client_name]): bind client name to spawned client
tony Mar 29, 2026
51e74f2
ControlMode(fix[control_mode]): replace FIFO with os.pipe(), add clea…
tony Mar 29, 2026
4e84ef0
Pane(fix[display_popup]): raise ValueError when close_on_exit + close…
tony Mar 29, 2026
3991eb3
Server(fix[confirm_before,command_prompt]): gate -b behind has_gte_ve…
tony Mar 29, 2026
6cb381b
Server,Session(fix[version_guard,detach_scope]): raise on unsupported…
tony Mar 29, 2026
ec2c696
Pane(fix[display_popup]): correct ControlMode module path in docstring
tony Mar 29, 2026
78c9eed
Server(fix[show_prompt_history,clear_prompt_history]): guard tmux 3.3+
tony Mar 29, 2026
c49eb53
Window(fix[swap]): refresh self and target after swap-window
tony Mar 29, 2026
ca6eaba
Pane(fix[display_message]): no_expand→-l (literal), remove wrong list…
tony Mar 29, 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
11 changes: 11 additions & 0 deletions .claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"name": "tmux-parity",
"version": "0.1.0",
"description": "Analyze and close feature parity gaps between tmux C source and libtmux Python wrappers",
"author": {
"name": "libtmux contributors"
},
"repository": "https://github.com/tmux-python/libtmux",
"license": "MIT",
"keywords": ["tmux", "parity", "analysis", "code-generation"]
}
47 changes: 47 additions & 0 deletions .claude-plugin/scripts/extract-libtmux-methods.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
#!/usr/bin/env bash
# Extract tmux command invocations from libtmux Python source
# Usage: extract-libtmux-methods.sh [libtmux-src-dir]
# Output: unique tmux command names invoked via .cmd() or as command args
#
# Searches for .cmd("command"), tmux_cmd(..., "command"), and
# args = ["command"] patterns that represent actual tmux command calls.

set -euo pipefail

LIBTMUX_DIR="${1:-$HOME/work/python/libtmux/src/libtmux}"

if [[ ! -d "$LIBTMUX_DIR" ]]; then
echo "Error: libtmux source dir not found at $LIBTMUX_DIR" >&2
exit 1
fi

echo "# Unique tmux commands invoked by libtmux"
{
# Pattern 1: self.cmd("command-name", ...) or .cmd("command-name")
grep -rn '\.cmd(' "$LIBTMUX_DIR"/*.py 2>/dev/null | \
grep -oP '\.cmd\(\s*"([a-z]+-[a-z-]+)"' | \
sed 's/.*"\(.*\)"/\1/'

# Pattern 2: args/cmd = ["command-name", ...] or args/cmd += ["command-name"]
grep -rn '\(args\|cmd\)\s*[+=]\+\s*\["[a-z]\+-' "$LIBTMUX_DIR"/*.py 2>/dev/null | \
grep -oP '\["([a-z]+-[a-z-]+)"' | \
tr -d '["'

# Pattern 3: tmux_args += ("command-name",) or tmux_args = ("command-name",)
grep -rn 'tmux_args\s*[+=]\+.*"[a-z]\+-' "$LIBTMUX_DIR"/*.py 2>/dev/null | \
grep -oP '"([a-z]+-[a-z-]+)"' | \
tr -d '"'

# Pattern 4: string literals in command-building contexts (hooks.py, options.py, common.py)
# Match lines with command strings used in args lists or cmd() calls
grep -rn '^\s*"[a-z]\+-[a-z-]*",' "$LIBTMUX_DIR"/*.py 2>/dev/null | \
grep -oP '"([a-z]+-[a-z-]+)"' | \
tr -d '"' | \
grep -E '^(capture|kill|move|select|set|show|split|clear)-'
} | sort -u

echo ""
echo "# Detailed: command|file:line"
grep -rn '\.cmd(\|args\s*[+=]\+\s*\["\|tmux_args\s*[+=]' "$LIBTMUX_DIR"/*.py 2>/dev/null | \
perl -ne 'if (/^(.+?):(\d+):.*"([a-z]+-[a-z]+-?[a-z]*)"/ && $3 =~ /^(attach|break|capture|choose|clear|clock|command|confirm|copy|customize|delete|detach|display|find|has|if|join|kill|last|link|list|load|lock|move|new|next|paste|pipe|previous|refresh|rename|resize|respawn|rotate|run|save|select|send|server|set|show|source|split|start|suspend|swap|switch|unbind|unlink|wait)-/) { print "$3|$1:$2\n" }' | \
sort
43 changes: 43 additions & 0 deletions .claude-plugin/scripts/extract-tmux-commands.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
#!/usr/bin/env bash
# Extract tmux command entries from cmd-*.c files
# Usage: extract-tmux-commands.sh [tmux-source-dir]
# Output: command_name|alias|getopt_string|target_type
#
# Parses cmd_entry structs to enumerate all tmux commands with their
# flags and target types.

set -euo pipefail

TMUX_DIR="${1:-$HOME/study/c/tmux}"

if [[ ! -d "$TMUX_DIR" ]]; then
echo "Error: tmux source dir not found at $TMUX_DIR" >&2
exit 1
fi

# Process each cmd-*.c file (skip internal files)
for f in "$TMUX_DIR"/cmd-*.c; do
base=$(basename "$f" .c)
case "$base" in
cmd-parse|cmd-queue|cmd-find) continue ;;
esac

# Use perl for reliable multi-field extraction from cmd_entry structs
perl -0777 -ne '
while (/const\s+struct\s+cmd_entry\s+\w+\s*=\s*\{(.*?)\n\};/gs) {
my $block = $1;
my ($name, $alias, $args, $target) = ("", "-", "", "none");

$name = $1 if $block =~ /\.name\s*=\s*"([^"]+)"/;
$alias = $1 if $block =~ /\.alias\s*=\s*"([^"]+)"/;
$args = $1 if $block =~ /\.args\s*=\s*\{\s*"([^"]*)"/;

$target = "pane" if $block =~ /CMD_FIND_PANE/;
$target = "window" if $block =~ /CMD_FIND_WINDOW/;
$target = "session" if $block =~ /CMD_FIND_SESSION/;
$target = "client" if $block =~ /CMD_FIND_CLIENT/;

print "$name|$alias|$args|$target\n" if $name;
}
' "$f"
done | sort
132 changes: 132 additions & 0 deletions .claude/agents/parity-analyzer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
---
name: parity-analyzer
description: |
Use this agent when the user asks about "tmux parity", "what commands are missing", "coverage report", "what does libtmux wrap", "unwrapped commands", "missing tmux features", "does libtmux support X", "tmux feature coverage", or when the user wants to understand what tmux functionality libtmux does not yet expose.

<example>
Context: User wants to know parity status
user: "What tmux commands does libtmux not wrap yet?"
assistant: "I'll use the parity-analyzer agent to scan tmux source and cross-reference with libtmux."
<commentary>User asking about missing commands, trigger parity analysis.</commentary>
</example>

<example>
Context: User considering what to implement next
user: "Which unwrapped tmux commands would be most useful to add?"
assistant: "I'll use the parity-analyzer agent to analyze coverage and prioritize gaps."
<commentary>User wants prioritized gap analysis, trigger parity-analyzer.</commentary>
</example>

<example>
Context: User asks about specific command
user: "Does libtmux support break-pane?"
assistant: "I'll check with the parity-analyzer agent."
<commentary>Specific command inquiry, use parity-analyzer for accurate answer.</commentary>
</example>

<example>
Context: User working on parity branch
user: "What should I work on next for tmux parity?"
assistant: "I'll use the parity-analyzer agent to identify the highest-priority gaps."
<commentary>Planning parity work, trigger analysis for prioritization.</commentary>
</example>
model: sonnet
color: cyan
tools:
- Read
- Grep
- Glob
- Bash
---

You are a tmux/libtmux feature parity analysis specialist. Analyze the gap between tmux C source and libtmux Python wrappers.

## Source Locations

- **tmux C source (HEAD)**: ~/study/c/tmux/
- **tmux version worktrees**: ~/study/c/tmux-{version}/ (41 versions, 0.8 to 3.6a)
- **libtmux Python source**: src/libtmux/ (in the current project)

## Analysis Process

### Step 1: Extract tmux commands

Run the extraction script for current data:
```bash
bash .claude-plugin/scripts/extract-tmux-commands.sh ~/study/c/tmux
```
This outputs `command|alias|getopt|target` for all ~88 tmux commands.

### Step 2: Extract libtmux coverage

Run the libtmux extraction:
```bash
bash .claude-plugin/scripts/extract-libtmux-methods.sh
```
This outputs the unique tmux command strings that libtmux invokes.

Additionally, check mixin files for commands invoked via `tmux_cmd()`:
```bash
grep -rn '"set-environment"\|"show-environment"\|"set-hook"\|"set-option"\|"show-option"\|"capture-pane"\|"move-window"\|"select-layout"\|"kill-pane"' src/libtmux/*.py | grep -oP '"([a-z]+-[a-z-]+)"' | sort -u | tr -d '"'
```

### Step 3: Cross-reference

Classify each tmux command:
- **Wrapped**: Command string appears in libtmux source
- **Not Wrapped**: Command string does not appear

For wrapped commands, optionally compare the getopt string from tmux against the Python method parameters to identify missing flags.

### Step 4: Produce report

Output a structured report:

```markdown
## tmux/libtmux Parity Report

### Summary
- Total tmux commands: X
- Wrapped in libtmux: Y (Z%)
- Not wrapped: N

### Wrapped Commands
| Command | libtmux Location |

### Not Wrapped — High Priority
| Command | Alias | Target | Why Useful |
(Include: join-pane, swap-pane, swap-window, respawn-pane, respawn-window, run-shell, break-pane, move-pane, pipe-pane, display-popup, clear-history)

### Not Wrapped — Medium Priority
| Command | Alias | Target | Notes |
(Include: navigation commands, buffer management, wait-for, if-shell, detach-client)

### Not Wrapped — Low Priority
| Command | Alias | Target | Notes |
(Include: interactive UI commands, key bindings, lock commands, config commands)
```

### Priority Guidelines

**High priority** — Commands useful for programmatic tmux control and automation:
- Pane/window manipulation: join-pane, swap-pane, swap-window, break-pane, move-pane
- Process management: respawn-pane, respawn-window, run-shell
- I/O: pipe-pane, clear-history, display-popup

**Medium priority** — Navigation, buffers, and client management:
- Navigation: last-pane, last-window, next-window, previous-window
- Buffer ops: list-buffers, load-buffer, save-buffer, paste-buffer, set-buffer
- Window linking: link-window, unlink-window
- Synchronization: wait-for
- Conditional: if-shell

**Low priority** — Interactive UI and configuration (rarely needed in API):
- Interactive: choose-tree, choose-buffer, copy-mode, command-prompt
- Key binding: bind-key, unbind-key
- Security: lock-server, lock-session, lock-client
- Meta: list-commands, list-keys, show-messages
- Config: source-file, start-server

## Reference Data

The baseline command mapping is at `skills/tmux-parity/references/command-mapping.md`. Use this as a starting point, but always run the extraction scripts for the most current data.
127 changes: 127 additions & 0 deletions .claude/commands/implement-command.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
---
description: Guide implementing a new tmux command wrapper in libtmux
argument-hint: "<tmux-command-name> — e.g., 'break-pane', 'join-pane', 'swap-window'"
allowed-tools:
- Read
- Write
- Edit
- Grep
- Glob
- Bash
- AskUserQuestion
- Agent
---

# Implement Command

Guide wrapping a tmux command in libtmux, following project coding standards from CLAUDE.md.

Load the `tmux-parity` skill first for reference data and implementation patterns.

If `$ARGUMENTS` is empty, ask the user which tmux command to wrap. Consult `skills/tmux-parity/references/command-mapping.md` for the "Not Wrapped" list to suggest candidates.

## Phase 1: Analyze the tmux Command

1. Read `~/study/c/tmux/cmd-{command}.c` fully
2. Extract from the `cmd_entry` struct:
- **name** and **alias**
- **getopt string** — enumerate all flags, which take values, which are boolean
- **usage string** — human-readable flag descriptions
- **target type** — `CMD_FIND_PANE`, `CMD_FIND_WINDOW`, `CMD_FIND_SESSION`, or none
- **command flags** — `CMD_READONLY`, `CMD_AFTERHOOK`, etc.
3. Read the `exec` function to understand:
- What arguments it processes
- What side effects it has (creates objects, modifies state, produces output)
- What it returns or prints
- Error conditions

4. Present a summary to the user:
```
## tmux command: {name} ({alias})
Target: {pane|window|session|none} → libtmux class: {Pane|Window|Session|Server}
Flags: {table of flags with descriptions}
Behavior: {what the command does}
```

## Phase 2: Determine libtmux Placement

Map the target type to libtmux class:
| Target | Primary Class | File |
|--------|--------------|------|
| `CMD_FIND_PANE` | `Pane` | `src/libtmux/pane.py` |
| `CMD_FIND_WINDOW` | `Window` | `src/libtmux/window.py` |
| `CMD_FIND_SESSION` | `Session` | `src/libtmux/session.py` |
| none | `Server` | `src/libtmux/server.py` |

Some commands may also get convenience methods on parent classes. Ask the user if they want additional convenience methods.

## Phase 3: Find a Similar Implementation

Search libtmux for a wrapped command with similar characteristics:
- Same target type
- Similar flag pattern (boolean flags, value flags, creates objects, etc.)
- Read that method as a template

Consult `skills/tmux-parity/references/libtmux-patterns.md` for the five implementation patterns.

## Phase 4: Design the Method Signature

Present a proposed method signature to the user before implementing. Include:
- Method name (snake_case, derived from tmux command name)
- Parameters mapped from tmux flags (with Python-friendly names and types)
- Return type
- Which flags to include (not all flags need wrapping — ask user about ambiguous ones)

**This is a good point to ask the user to write the method signature and core logic (5-10 lines).** Present the trade-offs:
- Which flags to expose (all vs. commonly used)?
- Return type (Self vs. new object vs. None)?
- Naming conventions for parameters?

## Phase 5: Implement

Follow CLAUDE.md coding standards strictly:

1. **Imports**: `from __future__ import annotations`, `import typing as t`
2. **Method**: Add to the appropriate class file
3. **Docstring**: NumPy format with Parameters, Returns, Examples sections
4. **Doctests**: Working doctests using `doctest_namespace` fixtures (`server`, `session`, `window`, `pane`)
- Use `# doctest: +ELLIPSIS` for variable output
- NEVER use `# doctest: +SKIP`
5. **Logging**: `logger.info("descriptive msg", extra={"tmux_subcommand": "...", ...})`
6. **Error handling**: Check `proc.stderr`, raise `exc.LibTmuxException`

## Phase 6: Create Tests

Add tests in `tests/test_{class}.py` (or a new file if warranted):

1. **Functional tests only** — no test classes
2. **Use fixtures**: `server`, `session`, `window`, `pane` from conftest.py
3. **Test each parameter/flag** combination
4. **Test error cases** if applicable
5. **Use descriptive function names**: `test_{command}_{scenario}`

## Phase 7: Verify

Run the full verification workflow:

```bash
# Format
uv run ruff format .

# Lint
uv run ruff check . --fix --show-fixes

# Type check
uv run mypy src tests

# Test the specific file
uv run pytest tests/test_{class}.py -x -v

# Run doctests
uv run pytest --doctest-modules src/libtmux/{class}.py -v

# Full test suite
uv run pytest
```

All must pass before considering the implementation complete.
Loading
Loading