Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
134 changes: 134 additions & 0 deletions .claude/agents/parity-analyzer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
---
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:

```console
$ bash .claude-plugin/scripts/extract-tmux-commands.sh ~/study/c/tmux
```

This outputs `command|alias|getopt|target` for every tmux command.

### Step 2: Extract libtmux coverage

Run the libtmux extraction:

```console
$ 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()`:

```console
$ 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 |

### Not Wrapped — Medium Priority
| Command | Alias | Target | Notes |

### Not Wrapped — Low Priority
| Command | Alias | Target | Notes |
```

### 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.
131 changes: 131 additions & 0 deletions .claude/commands/implement-command.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
---
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:

```console
$ uv run ruff format .
```

```console
$ uv run ruff check . --fix --show-fixes
```

```console
$ uv run mypy src tests
```

```console
$ uv run pytest tests/test_{class}.py -x -v
```

```console
$ uv run pytest --doctest-modules src/libtmux/{class}.py -v
```

```console
$ uv run pytest
```

All must pass before considering the implementation complete.
Loading