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
69e3bba
ai(claude[plugin]): add tmux-parity plugin for feature parity analysis
tony Mar 24, 2026
ca5ba54
ai(claude[plugin]): move agents/commands/skills to project root for a…
tony Mar 26, 2026
f57ba88
ai(claude[plugin]): move commands/agents to .claude/ for slash comman…
tony Mar 26, 2026
96b5547
Pane(feat[send_keys]): add reset, copy-mode, repeat, hex, format, cli…
tony Mar 26, 2026
7726579
Pane(feat[select]): add direction, last, zoom, mark, and input-toggle…
tony Mar 26, 2026
1db91ea
Pane(feat[display_message]): add format, verbose, delay, notify, list…
tony Mar 26, 2026
20e165e
Window(feat[select_layout]): add spread, next, and previous flags
tony Mar 26, 2026
d4a1610
Window(feat[move_window]): add after, before, no-select, kill, and re…
tony Mar 26, 2026
a0183df
Server(feat[new_session]): add detach-others, no-size, config flags
tony Mar 26, 2026
955c0d5
Session(feat[new_window]): add kill-existing and select-existing flags
tony Mar 26, 2026
cdb7a5b
Pane(feat[split]): add percentage parameter for split-window
tony Mar 26, 2026
deca3e9
Pane(feat[capture_pane]): add alternate-screen, quiet, and escape-mar…
tony Mar 26, 2026
65e5383
Options,Env(feat): add quiet/values-only to show_options, format/hidd…
tony Mar 26, 2026
d3caba9
Pane(feat[clear_history]): add clear_history() wrapping tmux clear-hi…
tony Mar 26, 2026
37f1c7a
Pane,Window(feat[swap]): add swap() wrapping tmux swap-pane and swap-…
tony Mar 26, 2026
e33dc1d
Pane(feat[break_pane]): add break_pane() wrapping tmux break-pane
tony Mar 26, 2026
2e57837
Pane(feat[join]): add join() wrapping tmux join-pane
tony Mar 26, 2026
38f3e2a
Pane,Window(feat[respawn]): add respawn() wrapping tmux respawn-pane/…
tony Mar 26, 2026
ec82b3c
Pane(feat[pipe]): add pipe() wrapping tmux pipe-pane
tony Mar 26, 2026
6093906
Server(feat[run_shell]): add run_shell() wrapping tmux run-shell
tony Mar 26, 2026
e4cf7ae
Session,Window(feat): add last_window, next_window, previous_window, …
tony Mar 26, 2026
7a56f11
Window,Server(feat): add link, unlink, and wait_for commands
tony Mar 26, 2026
12557db
Server(feat[buffers]): add set_buffer, show_buffer, delete_buffer wra…
tony Mar 26, 2026
4e1ae06
Server(feat[buffers]): add save_buffer, load_buffer, list_buffers for…
tony Mar 26, 2026
c7b364f
Pane(feat[paste_buffer]): add paste_buffer() wrapping tmux paste-buffer
tony Mar 26, 2026
0c478af
Pane(feat[display_popup]): add display_popup() wrapping tmux display-…
tony Mar 26, 2026
e748790
Revert "Pane(feat[display_popup]): add display_popup() wrapping tmux …
tony Mar 26, 2026
0b36b60
Server(feat[list_clients]): add list_clients() wrapping tmux list-cli…
tony Mar 26, 2026
dfec9da
Server(feat[source_file]): add source_file() wrapping tmux source-file
tony Mar 26, 2026
aa64236
Server(feat[if_shell]): add if_shell() wrapping tmux if-shell
tony Mar 26, 2026
a72b392
test(control_mode): add ControlMode context manager for client-depend…
tony Mar 26, 2026
ad0dc22
Session(feat[detach_client]): add detach_client() wrapping tmux detac…
tony Mar 26, 2026
cc1608d
Pane(feat[display_popup]): add display_popup() wrapping tmux display-…
tony Mar 26, 2026
8a5d522
Server,Pane(feat): add show_messages, prompt_history, send_prefix com…
tony Mar 26, 2026
ec3a47f
Server(feat): add bind_key, unbind_key, list_keys, list_commands
tony Mar 26, 2026
bde69d0
Server,Session(feat): add start_server, lock_session
tony Mar 26, 2026
d21f4ef
Server(feat): add lock_server, lock_client, refresh_client, suspend_c…
tony Mar 26, 2026
372a6bf
Pane(feat): add copy_mode, clock_mode, choose_buffer/client/tree, cus…
tony Mar 26, 2026
957e013
docs(parity): update command-mapping reference to reflect current cov…
tony Mar 26, 2026
ed18827
feat: fill missing flag gaps on recently added commands
tony Mar 26, 2026
58dc472
Server(feat): add confirm_before, command_prompt wrapping interactive…
tony Mar 26, 2026
7754c78
Server(feat[command_prompt]): fill missing flag gaps
tony Mar 27, 2026
525e8f0
Pane(feat): fill missing flag gaps on interactive commands
tony Mar 27, 2026
0f9e8fa
Server(feat[display_menu]): add display_menu() wrapping tmux display-…
tony Mar 27, 2026
ffc61e2
docs(parity): update command-mapping to reflect 100% effective coverage
tony Mar 27, 2026
2277cb6
Window(feat): add next_layout, previous_layout wrapping real tmux com…
tony Mar 27, 2026
761c682
Window(feat[last_pane]): call last-pane directly instead of select-pa…
tony Mar 27, 2026
3acc3a2
Pane(feat[move]): add move() wrapping tmux move-pane directly
tony Mar 27, 2026
a569500
docs(doctests): annotate untestable interactive commands
tony Mar 28, 2026
7d76871
Pane(fix[display_popup]): correct border_style flag and add style param
tony Mar 28, 2026
5cd5ea4
Pane(fix[display_popup]): use fused -e flag format for consistency
tony Mar 28, 2026
a4203c2
docs(versionadded): annotate new params on existing methods with 0.45
tony Mar 28, 2026
6b41ff2
Pane,Window(fix[respawn]): use fused -c flag format for consistency
tony Mar 28, 2026
03809bd
Window(fix[last_pane]): correct flag mapping for -d/-e flags
tony Mar 28, 2026
ad5a4d5
Window(fix[split]): forward percentage parameter to Pane.split
tony Mar 28, 2026
9fb1bee
Window(fix[select_layout]): use -p flag for previous layout, not -o
tony Mar 28, 2026
45f8b5d
Server(fix[new_session]): -f sets client flags, not config file path
tony Mar 28, 2026
14b105b
Server(fix[run_shell]): -C means tmux command, not capture output
tony Mar 28, 2026
7b606a2
Server(fix[command_prompt]): -N means numeric input, not no-execute
tony Mar 28, 2026
15002da
Pane(fix[capture_pane]): -M captures mode screen, not escape markup
tony Mar 28, 2026
f04eb77
Pane(fix[clear_history]): -H resets hyperlinks, not clears pane content
tony Mar 28, 2026
41b04e2
Pane(fix[copy_mode]): -q cancels modes, -e exits on scroll to bottom
tony Mar 28, 2026
4288549
Pane(fix[paste_buffer]): -r changes separator to newline, not no-trai…
tony Mar 28, 2026
9b5b3b8
Pane(fix[choose_tree]): -s/-w mean collapsed, not only-show
tony Mar 28, 2026
41e0432
Window(fix[rotate]): don't always inject -D, respect tmux default
tony Mar 28, 2026
fbe819e
Server(fix[confirm_before,command_prompt]): -b requires tmux 3.3+, no…
tony Mar 28, 2026
eb97e47
Pane(fix[display_popup]): -C closes existing popup, not close-on-success
tony Mar 28, 2026
bc14702
Window(fix[move_window]): -r is standalone renumber, not renumber-aft…
tony Mar 28, 2026
b574416
Session(fix[detach_client]): preserve client targeting semantics
tony Mar 29, 2026
4fb8926
Window(fix[move_window]): refresh moved window state
tony Mar 29, 2026
1c32608
ControlMode(fix[client_name]): bind client name to spawned client
tony Mar 29, 2026
bc0510a
ControlMode(fix[control_mode]): replace FIFO with os.pipe(), add clea…
tony Mar 29, 2026
9cf51d7
Pane(fix[display_popup]): raise ValueError when close_on_exit + close…
tony Mar 29, 2026
a8b7ec6
Server(fix[confirm_before,command_prompt]): gate -b behind has_gte_ve…
tony Mar 29, 2026
34dfced
Server,Session(fix[version_guard,detach_scope]): raise on unsupported…
tony Mar 29, 2026
41ecff5
Pane(fix[display_popup]): correct ControlMode module path in docstring
tony Mar 29, 2026
9e2a5a5
Server(fix[show_prompt_history,clear_prompt_history]): guard tmux 3.3+
tony Mar 29, 2026
5bbc1ef
Window(fix[swap]): refresh self and target after swap-window
tony Mar 29, 2026
bc12f79
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