From 12596e8ca334bf95a330da8e1bfa9e8efa96d5ba Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Tue, 24 Mar 2026 04:23:02 -0500 Subject: [PATCH 1/7] ai(claude[plugin]): add tmux-parity plugin for feature parity analysis why: libtmux wraps ~28 of tmux's ~88 commands (~32% coverage). Need tooling to systematically audit gaps, compare across tmux versions, and guide implementation of new command wrappers. what: - Add .claude-plugin/ with manifest, commands, agent, skill, and scripts - /parity-audit: generate full feature parity report (commands, flags, format variables, options) - /version-diff: compare tmux features across 41 version worktrees - /implement-command: guided workflow for wrapping new tmux commands - parity-analyzer agent: auto-triggers on natural language parity queries - tmux-parity skill: shared domain knowledge with reference files (command mapping, implementation patterns, C source navigation) - Extraction scripts: parse tmux cmd-*.c and libtmux .cmd() invocations --- .claude-plugin/agents/parity-analyzer.md | 129 ++++++++++++ .claude-plugin/commands/implement-command.md | 127 +++++++++++ .claude-plugin/commands/parity-audit.md | 84 ++++++++ .claude-plugin/commands/version-diff.md | 106 ++++++++++ .claude-plugin/plugin.json | 14 ++ .../scripts/extract-libtmux-methods.sh | 47 +++++ .../scripts/extract-tmux-commands.sh | 43 ++++ .claude-plugin/skills/tmux-parity/SKILL.md | 81 +++++++ .../tmux-parity/references/command-mapping.md | 119 +++++++++++ .../references/libtmux-patterns.md | 198 ++++++++++++++++++ .../references/tmux-command-table.md | 101 +++++++++ 11 files changed, 1049 insertions(+) create mode 100644 .claude-plugin/agents/parity-analyzer.md create mode 100644 .claude-plugin/commands/implement-command.md create mode 100644 .claude-plugin/commands/parity-audit.md create mode 100644 .claude-plugin/commands/version-diff.md create mode 100644 .claude-plugin/plugin.json create mode 100755 .claude-plugin/scripts/extract-libtmux-methods.sh create mode 100755 .claude-plugin/scripts/extract-tmux-commands.sh create mode 100644 .claude-plugin/skills/tmux-parity/SKILL.md create mode 100644 .claude-plugin/skills/tmux-parity/references/command-mapping.md create mode 100644 .claude-plugin/skills/tmux-parity/references/libtmux-patterns.md create mode 100644 .claude-plugin/skills/tmux-parity/references/tmux-command-table.md diff --git a/.claude-plugin/agents/parity-analyzer.md b/.claude-plugin/agents/parity-analyzer.md new file mode 100644 index 000000000..de9ebdc26 --- /dev/null +++ b/.claude-plugin/agents/parity-analyzer.md @@ -0,0 +1,129 @@ +--- +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. + + + 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." + User asking about missing commands, trigger parity analysis. + + + + 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." + User wants prioritized gap analysis, trigger parity-analyzer. + + + + Context: User asks about specific command + user: "Does libtmux support break-pane?" + assistant: "I'll check with the parity-analyzer agent." + Specific command inquiry, use parity-analyzer for accurate answer. + + + + 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." + Planning parity work, trigger analysis for prioritization. + +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 | + +### 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 `.claude-plugin/skills/tmux-parity/references/command-mapping.md`. Use this as a starting point, but always run the extraction scripts for the most current data. diff --git a/.claude-plugin/commands/implement-command.md b/.claude-plugin/commands/implement-command.md new file mode 100644 index 000000000..c9e0ed209 --- /dev/null +++ b/.claude-plugin/commands/implement-command.md @@ -0,0 +1,127 @@ +--- +description: Guide implementing a new tmux command wrapper in libtmux +argument-hint: " — 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 `.claude-plugin/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 `.claude-plugin/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. diff --git a/.claude-plugin/commands/parity-audit.md b/.claude-plugin/commands/parity-audit.md new file mode 100644 index 000000000..652f4e778 --- /dev/null +++ b/.claude-plugin/commands/parity-audit.md @@ -0,0 +1,84 @@ +--- +description: Generate a feature parity report between tmux commands and libtmux wrappers +argument-hint: "[command-name] — audit a specific command, or leave empty for full audit" +allowed-tools: + - Read + - Grep + - Glob + - Bash + - Agent +--- + +# Parity Audit + +Load the `tmux-parity` skill first to access reference data and domain knowledge. + +## Single Command Audit (when $ARGUMENTS specifies a command name) + +1. **Read the tmux C source** for the specified command: + - Read `~/study/c/tmux/cmd-{command}.c` to find the `cmd_entry` struct + - Extract: name, alias, getopt string, usage, target type, command flags + - Parse the getopt string to enumerate all flags (boolean vs value-taking) + - Read the `exec` function to understand behavior and return semantics + +2. **Search libtmux source** for the command: + - Grep `src/libtmux/*.py` for the command string (e.g., `"send-keys"`) + - For each match, read the surrounding method to understand which flags are exposed as Python parameters + - Check mixins: `src/libtmux/common.py` (EnvironmentMixin), `src/libtmux/options.py`, `src/libtmux/hooks.py` + +3. **Produce a detailed report**: + - Command name, alias, target type + - Table of all tmux flags: flag | description (from usage string) | exposed in libtmux? | Python parameter name + - Missing flags with notes on what they do + - Recommendations for which missing flags to add + +## Full Audit (when no arguments given) + +1. **Run extraction scripts** for up-to-date data: + ```bash + bash .claude-plugin/scripts/extract-tmux-commands.sh ~/study/c/tmux + bash .claude-plugin/scripts/extract-libtmux-methods.sh + ``` + +2. **Cross-reference the results**: + - Parse script output to classify each command: Wrapped, Not Wrapped + - For wrapped commands, compare getopt strings against Python method signatures to find partially-covered commands + +3. **Audit format variables** (optional, if specifically requested): + - Read `~/study/c/tmux/format.c` and search for `format_add` calls to list all format variables + - Compare against `src/libtmux/formats.py` + - Report missing format variables + +4. **Audit options table** (optional, if specifically requested): + - Read `~/study/c/tmux/options-table.c` to list all options with their scopes + - Compare against libtmux options handling + - Report missing options + +5. **Produce the full parity report**: + + ``` + ## tmux/libtmux Parity Report + + ### Summary + - Commands: X/Y wrapped (Z%) + - Partially wrapped: N commands (some flags missing) + + ### Coverage by Category + | Category | Wrapped | Total | % | + |----------|---------|-------|---| + | Session mgmt | ... | ... | ... | + | Window mgmt | ... | ... | ... | + | Pane mgmt | ... | ... | ... | + | ... + + ### Not Wrapped — High Priority + | Command | Alias | Target | Why Important | + + ### Not Wrapped — Medium Priority + ... + + ### Partially Wrapped (Missing Flags) + | Command | libtmux Method | Missing Flags | + ``` + +Consult `.claude-plugin/skills/tmux-parity/references/command-mapping.md` for the baseline mapping data. Run the extraction scripts for the most current data. diff --git a/.claude-plugin/commands/version-diff.md b/.claude-plugin/commands/version-diff.md new file mode 100644 index 000000000..53db7d711 --- /dev/null +++ b/.claude-plugin/commands/version-diff.md @@ -0,0 +1,106 @@ +--- +description: Compare tmux features across versions using source worktrees +argument-hint: " [command-name] — e.g., '3.0 3.6a' or '3.0 3.6a send-keys'" +allowed-tools: + - Read + - Grep + - Glob + - Bash +--- + +# Version Diff + +Compare tmux features between two versions using the source worktrees at `~/study/c/tmux-{version}/`. + +## Parse Arguments + +Extract `version1`, `version2`, and optional `command-name` from `$ARGUMENTS`. + +If no arguments provided, list available versions: +```bash +ls -d ~/study/c/tmux-*/ | sed 's|.*/tmux-||;s|/$||' | sort -V +``` +Then ask the user which two versions to compare. + +## Validate Worktrees + +Verify both worktrees exist: +```bash +ls -d ~/study/c/tmux-{version1}/ ~/study/c/tmux-{version2}/ 2>/dev/null +``` + +## Single Command Comparison (when command-name given) + +1. Check if the command file exists in both versions: + ```bash + ls ~/study/c/tmux-{v1}/cmd-{command}.c ~/study/c/tmux-{v2}/cmd-{command}.c 2>/dev/null + ``` + If missing in v1, the command was introduced between v1 and v2. + +2. Read both `cmd_entry` structs and compare: + - Name/alias changes + - Getopt string differences (new flags, removed flags) + - Usage string changes + - Target type changes + - Flag changes + +3. Diff the exec function to identify behavioral changes: + ```bash + diff ~/study/c/tmux-{v1}/cmd-{command}.c ~/study/c/tmux-{v2}/cmd-{command}.c + ``` + +4. Report: + ``` + ## send-keys: v3.0 → v3.6a + + ### Flag Changes + | Flag | v3.0 | v3.6a | Notes | + | -K | No | Yes | Added: ... | + + ### Behavioral Changes + - [description of exec function changes] + ``` + +## Broad Version Comparison (no command filter) + +1. **List cmd-*.c files in each version**: + ```bash + ls ~/study/c/tmux-{v1}/cmd-*.c | xargs -n1 basename | sort > /tmp/tmux-v1-cmds.txt + ls ~/study/c/tmux-{v2}/cmd-*.c | xargs -n1 basename | sort > /tmp/tmux-v2-cmds.txt + ``` + +2. **Identify new and removed command files**: + ```bash + comm -23 /tmp/tmux-v2-cmds.txt /tmp/tmux-v1-cmds.txt # New in v2 + comm -23 /tmp/tmux-v1-cmds.txt /tmp/tmux-v2-cmds.txt # Removed in v2 + ``` + +3. **For shared commands, compare getopt strings**: + Run `.claude-plugin/scripts/extract-tmux-commands.sh` on both versions and diff the output. + +4. **Compare options-table.c** (if it exists in both versions): + ```bash + diff ~/study/c/tmux-{v1}/options-table.c ~/study/c/tmux-{v2}/options-table.c + ``` + +5. **Report**: + ``` + ## tmux Version Diff: v{v1} → v{v2} + + ### New Commands + | Command | Alias | Getopt | Target | + + ### Removed Commands + ... + + ### Modified Commands (Flag Changes) + | Command | Added Flags | Removed Flags | + + ### New Options + | Option | Scope | Type | Default | + + ### Impact on libtmux + - Commands libtmux wraps that changed: [list] + - New commands worth wrapping: [recommendations] + - Minimum version implications: [notes] + ``` diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json new file mode 100644 index 000000000..12af82007 --- /dev/null +++ b/.claude-plugin/plugin.json @@ -0,0 +1,14 @@ +{ + "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"], + "commands": "./.claude-plugin/commands", + "agents": "./.claude-plugin/agents", + "skills": "./.claude-plugin/skills" +} diff --git a/.claude-plugin/scripts/extract-libtmux-methods.sh b/.claude-plugin/scripts/extract-libtmux-methods.sh new file mode 100755 index 000000000..5588dd507 --- /dev/null +++ b/.claude-plugin/scripts/extract-libtmux-methods.sh @@ -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 diff --git a/.claude-plugin/scripts/extract-tmux-commands.sh b/.claude-plugin/scripts/extract-tmux-commands.sh new file mode 100755 index 000000000..fd5ffd4f5 --- /dev/null +++ b/.claude-plugin/scripts/extract-tmux-commands.sh @@ -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 diff --git a/.claude-plugin/skills/tmux-parity/SKILL.md b/.claude-plugin/skills/tmux-parity/SKILL.md new file mode 100644 index 000000000..069480d14 --- /dev/null +++ b/.claude-plugin/skills/tmux-parity/SKILL.md @@ -0,0 +1,81 @@ +--- +name: tmux-parity +description: This skill should be used when analyzing tmux/libtmux feature parity, comparing tmux C source against libtmux Python wrappers, implementing new tmux command wrappers, understanding "what commands are missing", "what does libtmux wrap", reviewing tmux command flags, or comparing tmux versions. Also relevant for queries about "parity", "coverage", "unwrapped commands", "missing features", "tmux source", or "implement command". +version: 0.1.0 +--- + +# tmux/libtmux Feature Parity Analysis + +Analyze and close feature parity gaps between the tmux terminal multiplexer (C source) and the libtmux Python wrapper library. + +## Key Locations + +| Resource | Path | +|----------|------| +| tmux source (HEAD) | `~/study/c/tmux/` | +| tmux version worktrees | `~/study/c/tmux-{0.8..3.6a}/` (41 versions) | +| libtmux source | `src/libtmux/` (relative to project root) | +| libtmux tests | `tests/` | +| Extraction scripts | `.claude-plugin/scripts/extract-tmux-commands.sh`, `.claude-plugin/scripts/extract-libtmux-methods.sh` | + +## How tmux Commands Are Structured + +Each tmux command is defined in a `cmd-{name}.c` file via a `cmd_entry` struct: + +```c +const struct cmd_entry cmd_send_keys_entry = { + .name = "send-keys", + .alias = "send", + .args = { "c:FHKlMN:Rt:X", 0, -1, NULL }, // getopt string + .usage = "[-FHKlMRX] [-c target-client] ...", + .target = { 't', CMD_FIND_PANE, 0 }, // target type + .flags = CMD_AFTERHOOK|CMD_READONLY, + .exec = cmd_send_keys_exec +}; +``` + +Key fields: +- **`.args` getopt string**: Single char = boolean flag, char + `:` = flag with value +- **`.target`**: `CMD_FIND_PANE`, `CMD_FIND_WINDOW`, `CMD_FIND_SESSION`, `CMD_FIND_CLIENT`, or none +- **Command table**: All entries registered in `~/study/c/tmux/cmd.c` as `cmd_table[]` + +## How libtmux Wraps Commands + +libtmux methods call tmux via two patterns: + +1. **Object method**: `self.cmd("command-name", *args)` — on Server/Session/Window/Pane, auto-adds `-t target` +2. **Standalone**: `tmux_cmd("command-name", *args)` — in mixins (EnvironmentMixin, etc.) + +Class hierarchy mapping from tmux target types: +- `CMD_FIND_PANE` → `Pane` class (`src/libtmux/pane.py`) +- `CMD_FIND_WINDOW` → `Window` class (`src/libtmux/window.py`) +- `CMD_FIND_SESSION` → `Session` class (`src/libtmux/session.py`) +- No target / server-level → `Server` class (`src/libtmux/server.py`) +- Environment ops → `EnvironmentMixin` (`src/libtmux/common.py`) +- Option ops → `OptionsMixin` (`src/libtmux/options.py`) +- Hook ops → `HooksMixin` (`src/libtmux/hooks.py`) + +## Current Coverage Summary + +**~28 of ~88 tmux commands wrapped** (~32% coverage, approximate — run extraction scripts for current data). High-priority unwrapped commands include: `join-pane`, `swap-pane`, `swap-window`, `respawn-pane`, `respawn-window`, `run-shell`, `break-pane`, `move-pane`, `pipe-pane`, `display-popup`. + +## Extraction Scripts + +Run these for up-to-date data: + +```bash +# All tmux commands with flags and target types +bash .claude-plugin/scripts/extract-tmux-commands.sh ~/study/c/tmux + +# All tmux commands libtmux currently wraps +bash .claude-plugin/scripts/extract-libtmux-methods.sh +``` + +## Additional Resources + +### Reference Files + +For detailed data, consult: +- **`references/command-mapping.md`** — Complete mapping of all ~88 tmux commands to libtmux methods, with flag coverage +- **`references/libtmux-patterns.md`** — Implementation patterns for wrapping new commands (method signatures, doctests, logging, error handling) +- **`references/tmux-command-table.md`** — Guide to navigating tmux C source: cmd_entry fields, getopt format, target types, options-table.c, format.c diff --git a/.claude-plugin/skills/tmux-parity/references/command-mapping.md b/.claude-plugin/skills/tmux-parity/references/command-mapping.md new file mode 100644 index 000000000..bc2cc61ab --- /dev/null +++ b/.claude-plugin/skills/tmux-parity/references/command-mapping.md @@ -0,0 +1,119 @@ +# tmux Command → libtmux Method Mapping + +Generated from tmux HEAD and libtmux source. Re-generate with: +```bash +bash .claude-plugin/scripts/extract-tmux-commands.sh ~/study/c/tmux +bash .claude-plugin/scripts/extract-libtmux-methods.sh +``` + +## Wrapped Commands (28/88) + +| tmux Command | Alias | Getopt Flags | Target | libtmux Location | Methods | +|---|---|---|---|---|---| +| `attach-session` | `attach` | `c:dEf:rt:x` | none | `server.py` | `Server.attach_session()` | +| `capture-pane` | `capturep` | `ab:CeE:JMNpPqS:Tt:` | pane | `pane.py` | `Pane.capture_pane()` | +| `display-message` | `display` | `aCc:d:lINpt:F:v` | pane | `pane.py` | `Pane.display_message()` | +| `has-session` | `has` | `t:` | session | `server.py` | `Server.has_session()` | +| `kill-pane` | `killp` | `at:` | pane | `pane.py` | `Pane.kill()` | +| `kill-server` | — | (none) | none | `server.py` | `Server.kill()` | +| `kill-session` | — | `aCt:` | session | `server.py`, `session.py` | `Server.kill_session()`, `Session.kill()` | +| `kill-window` | `killw` | `at:` | window | `session.py` | `Session.kill_window()`, `Window.kill()` | +| `list-sessions` | `ls` | `F:f:O:r` | none | `server.py`, `neo.py` | `Server.sessions`, internal fetch | +| `list-windows` | `lsw` | `aF:f:O:rst:` | window | `neo.py` | Internal fetch for `Session.windows` | +| `list-panes` | `lsp` | `aF:f:O:rst:` | window | `neo.py` | Internal fetch for `Window.panes` | +| `move-window` | `movew` | `abdkrs:t:` | window | `window.py` | `Window.move_window()` | +| `new-session` | `new` | `Ac:dDe:EF:f:n:Ps:t:x:Xy:` | session | `server.py` | `Server.new_session()` | +| `new-window` | `neww` | `abc:de:F:kn:PSt:` | window | `session.py` | `Session.new_window()` | +| `rename-session` | `rename` | `t:` | session | `session.py` | `Session.rename_session()` | +| `rename-window` | `renamew` | `t:` | window | `window.py` | `Window.rename_window()` | +| `resize-pane` | `resizep` | `DLMRTt:Ux:y:Z` | pane | `pane.py` | `Pane.resize()` | +| `resize-window` | `resizew` | `aADLRt:Ux:y:` | window | `window.py` | `Window.resize()` | +| `select-layout` | `selectl` | `Enopt:` | pane | `window.py` | `Window.select_layout()` | +| `select-pane` | `selectp` | `DdegLlMmP:RT:t:UZ` | pane | `window.py`, `pane.py` | `Window.select_pane()`, `Pane.select()`, `Pane.set_title()` | +| `select-window` | `selectw` | `lnpTt:` | window | `session.py`, `window.py` | `Session.select_window()`, `Window.select()` | +| `send-keys` | `send` | `c:FHKlMN:Rt:X` | pane | `pane.py` | `Pane.send_keys()` | +| `set-environment` | `setenv` | `Fhgrt:u` | session | `common.py` | `EnvironmentMixin.set_environment()`, `.unset_environment()`, `.remove_environment()` | +| `set-hook` | — | `agpRt:uw` | pane | `hooks.py` | `HooksMixin.set_hook()`, `.unset_hook()` | +| `set-option` | `set` | `aFgopqst:uUw` | pane | `options.py` | `OptionsMixin.set_option()`, `.unset_option()` | +| `show-environment` | `showenv` | `hgst:` | session | `common.py` | `EnvironmentMixin.show_environment()`, `.getenv()` | +| `show-hooks` | — | `gpt:w` | pane | `hooks.py` | `HooksMixin.show_hooks()`, `.show_hook()` | +| `show-options` | `show` | `AgHpqst:vw` | pane | `options.py` | `OptionsMixin.show_options()`, `.show_option()` | +| `split-window` | `splitw` | `bc:de:fF:hIl:p:Pt:vZ` | pane | `pane.py` | `Pane.split()`, `Window.split()` | +| `switch-client` | `switchc` | `c:EFlnO:pt:rT:Z` | none | `server.py`, `session.py` | `Server.switch_client()`, `Session.switch_client()` | + +## Not Wrapped Commands (60/88) + +### High Priority (useful for programmatic/scripting use) + +| tmux Command | Alias | Getopt | Target | Notes | +|---|---|---|---|---| +| `break-pane` | `breakp` | `abdPF:n:s:t:` | window | Move pane to its own window | +| `join-pane` | `joinp` | `bdfhvp:l:s:t:` | pane | Merge pane into another window | +| `move-pane` | `movep` | `bdfhvp:l:s:t:` | pane | Move pane between windows (like join-pane) | +| `respawn-pane` | `respawnp` | `c:e:kt:` | pane | Re-run command in pane | +| `respawn-window` | `respawnw` | `c:e:kt:` | window | Re-run command in all window panes | +| `run-shell` | `run` | `bd:Ct:Es:c:` | pane | Execute shell command in background | +| `swap-pane` | `swapp` | `dDs:t:UZ` | pane | Swap two panes | +| `swap-window` | `swapw` | `ds:t:` | window | Swap two windows | +| `display-popup` | `popup` | `Bb:Cc:d:e:Eh:kNs:S:t:T:w:x:y:` | pane | Create popup overlay (tmux 3.2+) | +| `pipe-pane` | `pipep` | `IOot:` | pane | Pipe pane output to command | +| `clear-history` | `clearhist` | `Ht:` | pane | Clear pane scrollback buffer | + +### Medium Priority (navigation, buffers, info) + +| tmux Command | Alias | Getopt | Target | Notes | +|---|---|---|---|---| +| `last-pane` | `lastp` | `det:Z` | window | Select previous pane | +| `last-window` | `last` | `t:` | session | Select previous window | +| `next-window` | `next` | `at:` | session | Select next window | +| `previous-window` | `prev` | `at:` | session | Select previous window | +| `link-window` | `linkw` | `abdks:t:` | window | Link window to another session | +| `unlink-window` | `unlinkw` | `kt:` | window | Unlink window from session | +| `rotate-window` | `rotatew` | `Dt:UZ` | window | Rotate pane positions | +| `list-buffers` | `lsb` | `F:f:O:r` | none | List paste buffers | +| `list-clients` | `lsc` | `F:f:O:rt:` | session | List connected clients | +| `load-buffer` | `loadb` | `b:t:w` | none | Load file into paste buffer | +| `save-buffer` | `saveb` | `ab:` | none | Save paste buffer to file | +| `set-buffer` | `setb` | `ab:t:n:w` | none | Set paste buffer contents | +| `show-buffer` | `showb` | `b:` | none | Show paste buffer contents | +| `delete-buffer` | `deleteb` | `b:` | none | Delete a paste buffer | +| `paste-buffer` | `pasteb` | `db:prSs:t:` | pane | Paste buffer into pane | +| `wait-for` | `wait` | `LSU` | none | Wait for/signal/lock a channel | +| `if-shell` | `if` | `bFt:` | pane | Conditional command execution | +| `detach-client` | `detach` | `aE:s:t:P` | session | Detach client from session | +| `refresh-client` | `refresh` | `A:B:cC:Df:r:F:lLRSt:U` | none | Refresh client display | +| `show-window-options` | `showw` | `gvt:` | window | Show window options (alias for show-options -w) | +| `set-window-option` | `setw` | `aFgoqt:u` | window | Set window option (alias for set-option -w) | + +### Low Priority (interactive UI, config, rarely scripted) + +| tmux Command | Alias | Getopt | Target | Notes | +|---|---|---|---|---| +| `bind-key` | `bind` | `nrN:T:` | none | Bind key to command | +| `unbind-key` | `unbind` | `anqT:` | none | Unbind a key | +| `choose-buffer` | — | `F:f:K:NO:rt:yZ` | pane | Interactive buffer chooser | +| `choose-client` | — | `F:f:K:NO:rt:yZ` | pane | Interactive client chooser | +| `choose-tree` | — | `F:f:GK:NO:rst:wyZ` | pane | Interactive session/window tree | +| `clock-mode` | — | `t:` | pane | Show clock in pane | +| `command-prompt` | — | `1beFiklI:Np:t:T:` | none | Open command prompt | +| `confirm-before` | `confirm` | `bc:p:t:y` | none | Confirm before running command | +| `copy-mode` | — | `deHMqSs:t:u` | pane | Enter copy mode | +| `customize-mode` | — | `F:f:Nt:yZ` | pane | Enter customize mode | +| `display-menu` | `menu` | `b:c:C:H:s:S:MOt:T:x:y:` | pane | Display popup menu | +| `display-panes` | `displayp` | `bd:Nt:` | none | Show pane numbers | +| `find-window` | `findw` | `CiNrt:TZ` | pane | Search window contents | +| `list-commands` | `lscm` | `F:` | none | List tmux commands | +| `list-keys` | `lsk` | `1aF:NO:P:rT:` | none | List key bindings | +| `lock-client` | `lockc` | `t:` | none | Lock a client | +| `lock-server` | `lock` | (none) | none | Lock the server | +| `lock-session` | `locks` | `t:` | session | Lock a session | +| `next-layout` | `nextl` | `t:` | window | Cycle to next layout | +| `previous-layout` | `prevl` | `t:` | window | Cycle to previous layout | +| `send-prefix` | — | `2t:` | pane | Send prefix key | +| `server-access` | — | `adlrw` | none | Manage server access control | +| `show-messages` | `showmsgs` | `JTt:` | none | Show message log | +| `show-prompt-history` | `showphist` | `T:` | none | Show prompt history | +| `clear-prompt-history` | `clearphist` | `T:` | none | Clear prompt history | +| `source-file` | `source` | `t:Fnqv` | pane | Source a config file | +| `start-server` | `start` | (none) | none | Start server (usually implicit) | +| `suspend-client` | `suspendc` | `t:` | none | Suspend a client | diff --git a/.claude-plugin/skills/tmux-parity/references/libtmux-patterns.md b/.claude-plugin/skills/tmux-parity/references/libtmux-patterns.md new file mode 100644 index 000000000..a18b6b1d4 --- /dev/null +++ b/.claude-plugin/skills/tmux-parity/references/libtmux-patterns.md @@ -0,0 +1,198 @@ +# libtmux Implementation Patterns + +Reference for wrapping new tmux commands in libtmux. Study these patterns when implementing. + +## Pattern 1: Simple Command (No Return Value) + +Example: `Pane.select()` wrapping `select-pane` + +```python +def select(self) -> Self: + """Select pane. Wraps ``$ tmux select-pane``.""" + proc = self.cmd("select-pane") + if proc.stderr: + raise exc.LibTmuxException(proc.stderr) + return self +``` + +Key elements: +- Calls `self.cmd("command-name")` which auto-adds `-t {pane_id}` +- Checks `proc.stderr` for errors +- Returns `self` for chaining + +## Pattern 2: Command with Flag Arguments + +Example: `Pane.send_keys()` wrapping `send-keys` + +```python +def send_keys( + self, + cmd: str, + enter: bool = True, + suppress_history: bool = True, + literal: bool = False, +) -> None: + tmux_args: tuple[str | int, ...] = () + if literal: + tmux_args += ("-l",) + tmux_args += (cmd,) + self.cmd("send-keys", *tmux_args) + if enter: + self.cmd("send-keys", "Enter") +``` + +Key elements: +- Map Python kwargs to tmux flags (`literal` → `-l`) +- Build `tmux_args` tuple conditionally +- Boolean params for toggle flags, typed params for value flags + +## Pattern 3: Command Returning a New Object + +Example: `Session.new_window()` wrapping `new-window` + +```python +def new_window( + self, + window_name: str | None = None, + start_directory: StrPath | None = None, + attach: bool = True, + ... +) -> Window: + window_args: tuple[str, ...] = () + if not attach: + window_args += ("-d",) + if window_name is not None: + window_args += ("-n", window_name) + if start_directory is not None: + window_args += ("-c", str(start_directory)) + # Use -P -F to capture created object info + window_args += ("-P", "-F#{window_id}") + proc = self.cmd("new-window", *window_args) + if proc.stderr: + raise exc.LibTmuxException(proc.stderr) + window_id = proc.stdout[0].strip() + return fetch_obj("window_id", window_id, self.server) +``` + +Key elements: +- Uses `-P -F#{format}` to capture the new object's ID +- Parses stdout to get the created ID +- Calls `fetch_obj()` to return a fully populated object +- Raises on stderr + +## Pattern 4: Command with Direction/Enum Args + +Example: `Pane.resize()` wrapping `resize-pane` + +```python +def resize( + self, + adjustment_direction: ResizeAdjustmentDirection | None = None, + adjustment: int = 1, + height: int | None = None, + width: int | None = None, + zoom: bool | None = None, +) -> Self: + tmux_args: tuple[str | int, ...] = () + if adjustment_direction: + tmux_args += (RESIZE_ADJUSTMENT_DIRECTION_FLAG_MAP[adjustment_direction],) + tmux_args += (str(adjustment),) + if height is not None: + tmux_args += ("-y", str(height)) + if width is not None: + tmux_args += ("-x", str(width)) + if zoom is True: + tmux_args += ("-Z",) + proc = self.cmd("resize-pane", *tmux_args) + ... +``` + +Key elements: +- Uses constants from `libtmux.constants` for flag mapping +- Enum-based direction parameters +- Optional numeric arguments with explicit None checks + +## Pattern 5: Mixin Command (EnvironmentMixin) + +Example: `set_environment()` in `src/libtmux/common.py` + +```python +def set_environment(self, name: str, value: str) -> None: + args = ["set-environment"] + if hasattr(self, "session_id"): + args += ["-t", str(self.session_id)] + else: + args += ["-g"] + args += [name, value] + cmd = tmux_cmd(*args) # Uses standalone tmux_cmd, not self.cmd() +``` + +Key elements: +- Uses standalone `tmux_cmd()` function (not `self.cmd()`) +- Determines scope from object type (session → `-t`, server → `-g`) + +## Doctest Requirements + +All methods MUST have working doctests using fixtures from `doctest_namespace`: + +```python +def swap_pane(self, target: str) -> Self: + """Swap this pane with target. Wraps ``$ tmux swap-pane``. + + Parameters + ---------- + target : str + Target pane identifier + + Returns + ------- + :class:`Pane` + + Examples + -------- + >>> pane = window.active_pane + >>> pane2 = window.split() + >>> pane.swap_pane(pane2.pane_id) # doctest: +ELLIPSIS + Pane(...) + """ +``` + +Available fixtures: `server`, `session`, `window`, `pane`, `Server`, `Session`, `Window`, `Pane` + +Rules: +- Use `# doctest: +ELLIPSIS` for variable output +- Session IDs: `$...`, Window IDs: `@...`, Pane IDs: `%...` +- Never use `# doctest: +SKIP` +- Never convert to `.. code-block::` + +## Logging Pattern + +```python +logger.info( + "pane created", + extra={ + "tmux_subcommand": "split-window", + "tmux_pane": pane_id, + }, +) +``` + +- Use `logger.debug()` for command details, `logger.info()` for lifecycle events +- Always use `extra` dict with `tmux_` prefixed keys +- Use lazy formatting: `logger.debug("msg %s", val)` not f-strings + +## Error Handling + +```python +proc = self.cmd("command-name", *args) +if proc.stderr: + raise exc.LibTmuxException(proc.stderr) +``` + +- Check `proc.stderr` after command execution +- Raise `libtmux.exc.LibTmuxException` +- Do NOT catch-log-reraise without adding context + +## Coding Standards + +See the project's `CLAUDE.md` "Coding Standards" section. diff --git a/.claude-plugin/skills/tmux-parity/references/tmux-command-table.md b/.claude-plugin/skills/tmux-parity/references/tmux-command-table.md new file mode 100644 index 000000000..4fb7ff3fc --- /dev/null +++ b/.claude-plugin/skills/tmux-parity/references/tmux-command-table.md @@ -0,0 +1,101 @@ +# Navigating tmux C Source + +## Command Table (cmd.c) + +File: `~/study/c/tmux/cmd.c` + +The `cmd_table[]` array lists all registered commands as `extern const struct cmd_entry` references. Each entry is defined in the corresponding `cmd-*.c` file. + +Some cmd-*.c files define multiple commands: +- `cmd-send-keys.c`: `send-keys` + `send-prefix` +- `cmd-new-session.c`: `new-session` + `has-session` +- `cmd-capture-pane.c`: `capture-pane` + `clear-history` +- `cmd-choose-tree.c`: `choose-tree` + `choose-client` + `choose-buffer` + `customize-mode` +- `cmd-copy-mode.c`: `copy-mode` + `clock-mode` +- `cmd-detach-client.c`: `detach-client` + `suspend-client` +- `cmd-display-menu.c`: `display-menu` + `display-popup` +- `cmd-set-option.c`: `set-option` + `set-window-option` +- `cmd-show-options.c`: `show-options` + `show-window-options` + +## cmd_entry Struct Fields + +| Field | Type | Description | +|-------|------|-------------| +| `.name` | `const char *` | Full command name (e.g., `"new-session"`) | +| `.alias` | `const char *` | Short alias (e.g., `"new"`) or `NULL` | +| `.args` | `struct args_parse` | `{ getopt_string, min_args, max_args, NULL }` | +| `.usage` | `const char *` | Human-readable usage string | +| `.target` | `struct cmd_find_target` | `{ flag_char, CMD_FIND_TYPE, flags }` | +| `.flags` | `int` | Behavior flags (bitfield) | +| `.exec` | `enum cmd_retval (*)(struct cmd *, struct cmdq_item *)` | Implementation | + +## Getopt String Format + +The first element of `.args` is a `getopt(3)` option string: +- Single char = boolean flag: `d` means `-d` is a boolean toggle +- Char followed by `:` = flag with argument: `t:` means `-t ` +- Example: `"Ac:dDe:EF:f:n:Ps:t:x:Xy:"` means: + - Boolean: `-A`, `-d`, `-D`, `-E`, `-P`, `-X` + - With value: `-c val`, `-e val`, `-F val`, `-f val`, `-n val`, `-s val`, `-t val`, `-x val`, `-y val` + +## Target Types + +| Constant | Meaning | libtmux Class | +|----------|---------|---------------| +| `CMD_FIND_PANE` | Targets a pane (`-t pane_id`) | `Pane` | +| `CMD_FIND_WINDOW` | Targets a window (`-t window_id`) | `Window` | +| `CMD_FIND_SESSION` | Targets a session (`-t session_id`) | `Session` | +| `CMD_FIND_CLIENT` | Targets a client (`-c client`) | (no direct class) | +| (none) | No target required | `Server` | + +## Command Flags + +| Flag | Meaning | +|------|---------| +| `CMD_STARTSERVER` | Command starts server if not running | +| `CMD_READONLY` | Command doesn't modify state | +| `CMD_AFTERHOOK` | Command triggers after-hooks | +| `CMD_CLIENT_CFLAG` | Uses `-c` for client targeting | +| `CMD_CLIENT_CANFAIL` | Client lookup failure is non-fatal | + +## options-table.c + +File: `~/study/c/tmux/options-table.c` + +Defines all tmux options. Each entry specifies: +- **name**: Option name (e.g., `"status-style"`) +- **type**: `OPTIONS_TABLE_STRING`, `OPTIONS_TABLE_NUMBER`, `OPTIONS_TABLE_FLAG`, etc. +- **scope**: `OPTIONS_TABLE_SERVER`, `OPTIONS_TABLE_SESSION`, `OPTIONS_TABLE_WINDOW`, `OPTIONS_TABLE_PANE` +- **default**: Default value +- **minimum/maximum**: For numeric options + +Search pattern: `grep '\.name = "' ~/study/c/tmux/options-table.c` + +## format.c + +File: `~/study/c/tmux/format.c` + +Registers all format variables (`#{variable_name}`) used in `-F` format strings. + +Search for registrations: `grep 'format_add\|format_add_cb' ~/study/c/tmux/format.c` + +Compare against libtmux: `src/libtmux/formats.py` + +## Version Worktrees + +41 versions available at `~/study/c/tmux-{version}/`: +- 0.8, 0.9 +- 1.0 through 1.9, 1.9a +- 2.0 through 2.9, 2.9a +- 3.0, 3.0a, 3.1 through 3.1c, 3.2, 3.2a, 3.3, 3.3a, 3.4, 3.5, 3.5a, 3.6, 3.6a + +To check if a command exists in a version: +```bash +ls ~/study/c/tmux-3.0/cmd-display-popup.c 2>/dev/null # Not found = added later +ls ~/study/c/tmux-3.3/cmd-display-popup.c 2>/dev/null # Found = exists in 3.3 +``` + +To diff a command across versions: +```bash +diff ~/study/c/tmux-3.0/cmd-send-keys.c ~/study/c/tmux-3.6a/cmd-send-keys.c +``` From 29634fce73deab786b413bf9fbb4f54771c85a1c Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Thu, 26 Mar 2026 04:08:02 -0500 Subject: [PATCH 2/7] ai(claude[plugin]): move agents/commands/skills to project root for auto-discovery why: Claude Code auto-discovers plugin components at the project root, not inside .claude-plugin/. Agent wasn't showing up because it was nested under .claude-plugin/agents/. what: - Move agents/, commands/, skills/ to project root - Keep scripts/ in .claude-plugin/ (not auto-discovered) - Remove custom path overrides from plugin.json - Update cross-references between components --- .claude-plugin/plugin.json | 5 +---- {.claude-plugin/agents => agents}/parity-analyzer.md | 2 +- {.claude-plugin/commands => commands}/implement-command.md | 4 ++-- {.claude-plugin/commands => commands}/parity-audit.md | 2 +- {.claude-plugin/commands => commands}/version-diff.md | 0 {.claude-plugin/skills => skills}/tmux-parity/SKILL.md | 0 .../tmux-parity/references/command-mapping.md | 0 .../tmux-parity/references/libtmux-patterns.md | 0 .../tmux-parity/references/tmux-command-table.md | 0 9 files changed, 5 insertions(+), 8 deletions(-) rename {.claude-plugin/agents => agents}/parity-analyzer.md (95%) rename {.claude-plugin/commands => commands}/implement-command.md (94%) rename {.claude-plugin/commands => commands}/parity-audit.md (94%) rename {.claude-plugin/commands => commands}/version-diff.md (100%) rename {.claude-plugin/skills => skills}/tmux-parity/SKILL.md (100%) rename {.claude-plugin/skills => skills}/tmux-parity/references/command-mapping.md (100%) rename {.claude-plugin/skills => skills}/tmux-parity/references/libtmux-patterns.md (100%) rename {.claude-plugin/skills => skills}/tmux-parity/references/tmux-command-table.md (100%) diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index 12af82007..e6293f617 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -7,8 +7,5 @@ }, "repository": "https://github.com/tmux-python/libtmux", "license": "MIT", - "keywords": ["tmux", "parity", "analysis", "code-generation"], - "commands": "./.claude-plugin/commands", - "agents": "./.claude-plugin/agents", - "skills": "./.claude-plugin/skills" + "keywords": ["tmux", "parity", "analysis", "code-generation"] } diff --git a/.claude-plugin/agents/parity-analyzer.md b/agents/parity-analyzer.md similarity index 95% rename from .claude-plugin/agents/parity-analyzer.md rename to agents/parity-analyzer.md index de9ebdc26..10dc9fe3b 100644 --- a/.claude-plugin/agents/parity-analyzer.md +++ b/agents/parity-analyzer.md @@ -126,4 +126,4 @@ Output a structured report: ## Reference Data -The baseline command mapping is at `.claude-plugin/skills/tmux-parity/references/command-mapping.md`. Use this as a starting point, but always run the extraction scripts for the most current 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. diff --git a/.claude-plugin/commands/implement-command.md b/commands/implement-command.md similarity index 94% rename from .claude-plugin/commands/implement-command.md rename to commands/implement-command.md index c9e0ed209..9e70407c9 100644 --- a/.claude-plugin/commands/implement-command.md +++ b/commands/implement-command.md @@ -18,7 +18,7 @@ Guide wrapping a tmux command in libtmux, following project coding standards fro 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 `.claude-plugin/skills/tmux-parity/references/command-mapping.md` for the "Not Wrapped" list to suggest candidates. +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 @@ -62,7 +62,7 @@ Search libtmux for a wrapped command with similar characteristics: - Similar flag pattern (boolean flags, value flags, creates objects, etc.) - Read that method as a template -Consult `.claude-plugin/skills/tmux-parity/references/libtmux-patterns.md` for the five implementation patterns. +Consult `skills/tmux-parity/references/libtmux-patterns.md` for the five implementation patterns. ## Phase 4: Design the Method Signature diff --git a/.claude-plugin/commands/parity-audit.md b/commands/parity-audit.md similarity index 94% rename from .claude-plugin/commands/parity-audit.md rename to commands/parity-audit.md index 652f4e778..8994cdb51 100644 --- a/.claude-plugin/commands/parity-audit.md +++ b/commands/parity-audit.md @@ -81,4 +81,4 @@ Load the `tmux-parity` skill first to access reference data and domain knowledge | Command | libtmux Method | Missing Flags | ``` -Consult `.claude-plugin/skills/tmux-parity/references/command-mapping.md` for the baseline mapping data. Run the extraction scripts for the most current data. +Consult `skills/tmux-parity/references/command-mapping.md` for the baseline mapping data. Run the extraction scripts for the most current data. diff --git a/.claude-plugin/commands/version-diff.md b/commands/version-diff.md similarity index 100% rename from .claude-plugin/commands/version-diff.md rename to commands/version-diff.md diff --git a/.claude-plugin/skills/tmux-parity/SKILL.md b/skills/tmux-parity/SKILL.md similarity index 100% rename from .claude-plugin/skills/tmux-parity/SKILL.md rename to skills/tmux-parity/SKILL.md diff --git a/.claude-plugin/skills/tmux-parity/references/command-mapping.md b/skills/tmux-parity/references/command-mapping.md similarity index 100% rename from .claude-plugin/skills/tmux-parity/references/command-mapping.md rename to skills/tmux-parity/references/command-mapping.md diff --git a/.claude-plugin/skills/tmux-parity/references/libtmux-patterns.md b/skills/tmux-parity/references/libtmux-patterns.md similarity index 100% rename from .claude-plugin/skills/tmux-parity/references/libtmux-patterns.md rename to skills/tmux-parity/references/libtmux-patterns.md diff --git a/.claude-plugin/skills/tmux-parity/references/tmux-command-table.md b/skills/tmux-parity/references/tmux-command-table.md similarity index 100% rename from .claude-plugin/skills/tmux-parity/references/tmux-command-table.md rename to skills/tmux-parity/references/tmux-command-table.md From 223668655daca384fd5a199d0293b171b98be86f Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Thu, 26 Mar 2026 04:08:04 -0500 Subject: [PATCH 3/7] ai(claude[plugin]): move commands/agents to .claude/ for slash command discovery why: Claude Code discovers project commands from .claude/commands/ and agents from .claude/agents/, not top-level directories. what: - Move 3 commands to .claude/commands/ - Move parity-analyzer agent to .claude/agents/ - Remove now-empty top-level commands/ and agents/ dirs --- {agents => .claude/agents}/parity-analyzer.md | 0 {commands => .claude/commands}/implement-command.md | 0 {commands => .claude/commands}/parity-audit.md | 0 {commands => .claude/commands}/version-diff.md | 0 4 files changed, 0 insertions(+), 0 deletions(-) rename {agents => .claude/agents}/parity-analyzer.md (100%) rename {commands => .claude/commands}/implement-command.md (100%) rename {commands => .claude/commands}/parity-audit.md (100%) rename {commands => .claude/commands}/version-diff.md (100%) diff --git a/agents/parity-analyzer.md b/.claude/agents/parity-analyzer.md similarity index 100% rename from agents/parity-analyzer.md rename to .claude/agents/parity-analyzer.md diff --git a/commands/implement-command.md b/.claude/commands/implement-command.md similarity index 100% rename from commands/implement-command.md rename to .claude/commands/implement-command.md diff --git a/commands/parity-audit.md b/.claude/commands/parity-audit.md similarity index 100% rename from commands/parity-audit.md rename to .claude/commands/parity-audit.md diff --git a/commands/version-diff.md b/.claude/commands/version-diff.md similarity index 100% rename from commands/version-diff.md rename to .claude/commands/version-diff.md From eb6b903ef7d27f78034d3120568f446e85a4ead2 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Thu, 26 Mar 2026 06:42:56 -0500 Subject: [PATCH 4/7] docs(parity): update command-mapping reference to reflect current coverage why: The mapping doc was stale (said 28/88). Update to reflect 79/90 directly wrapped, 8 covered by alias/flag, 3 truly unwrappable. what: - Update summary to 87/90 effective coverage (96%) - Add table of 8 commands covered by alias/flag with explanation - Add table of 3 unwrappable commands with rationale --- .../tmux-parity/references/command-mapping.md | 128 ++++-------------- 1 file changed, 24 insertions(+), 104 deletions(-) diff --git a/skills/tmux-parity/references/command-mapping.md b/skills/tmux-parity/references/command-mapping.md index bc2cc61ab..e938a049d 100644 --- a/skills/tmux-parity/references/command-mapping.md +++ b/skills/tmux-parity/references/command-mapping.md @@ -6,114 +6,34 @@ bash .claude-plugin/scripts/extract-tmux-commands.sh ~/study/c/tmux bash .claude-plugin/scripts/extract-libtmux-methods.sh ``` -## Wrapped Commands (28/88) +## Summary -| tmux Command | Alias | Getopt Flags | Target | libtmux Location | Methods | -|---|---|---|---|---|---| -| `attach-session` | `attach` | `c:dEf:rt:x` | none | `server.py` | `Server.attach_session()` | -| `capture-pane` | `capturep` | `ab:CeE:JMNpPqS:Tt:` | pane | `pane.py` | `Pane.capture_pane()` | -| `display-message` | `display` | `aCc:d:lINpt:F:v` | pane | `pane.py` | `Pane.display_message()` | -| `has-session` | `has` | `t:` | session | `server.py` | `Server.has_session()` | -| `kill-pane` | `killp` | `at:` | pane | `pane.py` | `Pane.kill()` | -| `kill-server` | — | (none) | none | `server.py` | `Server.kill()` | -| `kill-session` | — | `aCt:` | session | `server.py`, `session.py` | `Server.kill_session()`, `Session.kill()` | -| `kill-window` | `killw` | `at:` | window | `session.py` | `Session.kill_window()`, `Window.kill()` | -| `list-sessions` | `ls` | `F:f:O:r` | none | `server.py`, `neo.py` | `Server.sessions`, internal fetch | -| `list-windows` | `lsw` | `aF:f:O:rst:` | window | `neo.py` | Internal fetch for `Session.windows` | -| `list-panes` | `lsp` | `aF:f:O:rst:` | window | `neo.py` | Internal fetch for `Window.panes` | -| `move-window` | `movew` | `abdkrs:t:` | window | `window.py` | `Window.move_window()` | -| `new-session` | `new` | `Ac:dDe:EF:f:n:Ps:t:x:Xy:` | session | `server.py` | `Server.new_session()` | -| `new-window` | `neww` | `abc:de:F:kn:PSt:` | window | `session.py` | `Session.new_window()` | -| `rename-session` | `rename` | `t:` | session | `session.py` | `Session.rename_session()` | -| `rename-window` | `renamew` | `t:` | window | `window.py` | `Window.rename_window()` | -| `resize-pane` | `resizep` | `DLMRTt:Ux:y:Z` | pane | `pane.py` | `Pane.resize()` | -| `resize-window` | `resizew` | `aADLRt:Ux:y:` | window | `window.py` | `Window.resize()` | -| `select-layout` | `selectl` | `Enopt:` | pane | `window.py` | `Window.select_layout()` | -| `select-pane` | `selectp` | `DdegLlMmP:RT:t:UZ` | pane | `window.py`, `pane.py` | `Window.select_pane()`, `Pane.select()`, `Pane.set_title()` | -| `select-window` | `selectw` | `lnpTt:` | window | `session.py`, `window.py` | `Session.select_window()`, `Window.select()` | -| `send-keys` | `send` | `c:FHKlMN:Rt:X` | pane | `pane.py` | `Pane.send_keys()` | -| `set-environment` | `setenv` | `Fhgrt:u` | session | `common.py` | `EnvironmentMixin.set_environment()`, `.unset_environment()`, `.remove_environment()` | -| `set-hook` | — | `agpRt:uw` | pane | `hooks.py` | `HooksMixin.set_hook()`, `.unset_hook()` | -| `set-option` | `set` | `aFgopqst:uUw` | pane | `options.py` | `OptionsMixin.set_option()`, `.unset_option()` | -| `show-environment` | `showenv` | `hgst:` | session | `common.py` | `EnvironmentMixin.show_environment()`, `.getenv()` | -| `show-hooks` | — | `gpt:w` | pane | `hooks.py` | `HooksMixin.show_hooks()`, `.show_hook()` | -| `show-options` | `show` | `AgHpqst:vw` | pane | `options.py` | `OptionsMixin.show_options()`, `.show_option()` | -| `split-window` | `splitw` | `bc:de:fF:hIl:p:Pt:vZ` | pane | `pane.py` | `Pane.split()`, `Window.split()` | -| `switch-client` | `switchc` | `c:EFlnO:pt:rT:Z` | none | `server.py`, `session.py` | `Server.switch_client()`, `Session.switch_client()` | +- **Directly wrapped**: 79/90 commands (87%) +- **Covered by alias/flag**: 8 additional commands +- **Truly unwrappable**: 3 commands (block waiting for interactive input) +- **Total effective coverage**: 87/90 (96%) -## Not Wrapped Commands (60/88) +## Covered by Alias/Flag (8 commands) -### High Priority (useful for programmatic/scripting use) +These commands are not called directly but their functionality is available: -| tmux Command | Alias | Getopt | Target | Notes | -|---|---|---|---|---| -| `break-pane` | `breakp` | `abdPF:n:s:t:` | window | Move pane to its own window | -| `join-pane` | `joinp` | `bdfhvp:l:s:t:` | pane | Merge pane into another window | -| `move-pane` | `movep` | `bdfhvp:l:s:t:` | pane | Move pane between windows (like join-pane) | -| `respawn-pane` | `respawnp` | `c:e:kt:` | pane | Re-run command in pane | -| `respawn-window` | `respawnw` | `c:e:kt:` | window | Re-run command in all window panes | -| `run-shell` | `run` | `bd:Ct:Es:c:` | pane | Execute shell command in background | -| `swap-pane` | `swapp` | `dDs:t:UZ` | pane | Swap two panes | -| `swap-window` | `swapw` | `ds:t:` | window | Swap two windows | -| `display-popup` | `popup` | `Bb:Cc:d:e:Eh:kNs:S:t:T:w:x:y:` | pane | Create popup overlay (tmux 3.2+) | -| `pipe-pane` | `pipep` | `IOot:` | pane | Pipe pane output to command | -| `clear-history` | `clearhist` | `Ht:` | pane | Clear pane scrollback buffer | +| tmux Command | Covered By | How | +|---|---|---| +| `last-pane` | `Window.last_pane()`, `Pane.select(last=True)` | `-l` flag on select-pane | +| `list-panes` | `Window.panes` property | Used internally by `neo.py` | +| `list-windows` | `Session.windows` property | Used internally by `neo.py` | +| `move-pane` | `Pane.join()` | Same C source as join-pane | +| `next-layout` | `Window.select_layout(next_layout=True)` | `-n` flag on select-layout | +| `previous-layout` | `Window.select_layout(previous_layout=True)` | `-o` flag on select-layout | +| `set-window-option` | `OptionsMixin.set_option(scope=OptionScope.Window)` | Alias for `set-option -w` | +| `show-window-options` | `OptionsMixin.show_options(scope=OptionScope.Window)` | Alias for `show-options -w` | -### Medium Priority (navigation, buffers, info) +## Not Wrappable (3 commands) -| tmux Command | Alias | Getopt | Target | Notes | -|---|---|---|---|---| -| `last-pane` | `lastp` | `det:Z` | window | Select previous pane | -| `last-window` | `last` | `t:` | session | Select previous window | -| `next-window` | `next` | `at:` | session | Select next window | -| `previous-window` | `prev` | `at:` | session | Select previous window | -| `link-window` | `linkw` | `abdks:t:` | window | Link window to another session | -| `unlink-window` | `unlinkw` | `kt:` | window | Unlink window from session | -| `rotate-window` | `rotatew` | `Dt:UZ` | window | Rotate pane positions | -| `list-buffers` | `lsb` | `F:f:O:r` | none | List paste buffers | -| `list-clients` | `lsc` | `F:f:O:rt:` | session | List connected clients | -| `load-buffer` | `loadb` | `b:t:w` | none | Load file into paste buffer | -| `save-buffer` | `saveb` | `ab:` | none | Save paste buffer to file | -| `set-buffer` | `setb` | `ab:t:n:w` | none | Set paste buffer contents | -| `show-buffer` | `showb` | `b:` | none | Show paste buffer contents | -| `delete-buffer` | `deleteb` | `b:` | none | Delete a paste buffer | -| `paste-buffer` | `pasteb` | `db:prSs:t:` | pane | Paste buffer into pane | -| `wait-for` | `wait` | `LSU` | none | Wait for/signal/lock a channel | -| `if-shell` | `if` | `bFt:` | pane | Conditional command execution | -| `detach-client` | `detach` | `aE:s:t:P` | session | Detach client from session | -| `refresh-client` | `refresh` | `A:B:cC:Df:r:F:lLRSt:U` | none | Refresh client display | -| `show-window-options` | `showw` | `gvt:` | window | Show window options (alias for show-options -w) | -| `set-window-option` | `setw` | `aFgoqt:u` | window | Set window option (alias for set-option -w) | +These block forever waiting for interactive user input: -### Low Priority (interactive UI, config, rarely scripted) - -| tmux Command | Alias | Getopt | Target | Notes | -|---|---|---|---|---| -| `bind-key` | `bind` | `nrN:T:` | none | Bind key to command | -| `unbind-key` | `unbind` | `anqT:` | none | Unbind a key | -| `choose-buffer` | — | `F:f:K:NO:rt:yZ` | pane | Interactive buffer chooser | -| `choose-client` | — | `F:f:K:NO:rt:yZ` | pane | Interactive client chooser | -| `choose-tree` | — | `F:f:GK:NO:rst:wyZ` | pane | Interactive session/window tree | -| `clock-mode` | — | `t:` | pane | Show clock in pane | -| `command-prompt` | — | `1beFiklI:Np:t:T:` | none | Open command prompt | -| `confirm-before` | `confirm` | `bc:p:t:y` | none | Confirm before running command | -| `copy-mode` | — | `deHMqSs:t:u` | pane | Enter copy mode | -| `customize-mode` | — | `F:f:Nt:yZ` | pane | Enter customize mode | -| `display-menu` | `menu` | `b:c:C:H:s:S:MOt:T:x:y:` | pane | Display popup menu | -| `display-panes` | `displayp` | `bd:Nt:` | none | Show pane numbers | -| `find-window` | `findw` | `CiNrt:TZ` | pane | Search window contents | -| `list-commands` | `lscm` | `F:` | none | List tmux commands | -| `list-keys` | `lsk` | `1aF:NO:P:rT:` | none | List key bindings | -| `lock-client` | `lockc` | `t:` | none | Lock a client | -| `lock-server` | `lock` | (none) | none | Lock the server | -| `lock-session` | `locks` | `t:` | session | Lock a session | -| `next-layout` | `nextl` | `t:` | window | Cycle to next layout | -| `previous-layout` | `prevl` | `t:` | window | Cycle to previous layout | -| `send-prefix` | — | `2t:` | pane | Send prefix key | -| `server-access` | — | `adlrw` | none | Manage server access control | -| `show-messages` | `showmsgs` | `JTt:` | none | Show message log | -| `show-prompt-history` | `showphist` | `T:` | none | Show prompt history | -| `clear-prompt-history` | `clearphist` | `T:` | none | Clear prompt history | -| `source-file` | `source` | `t:Fnqv` | pane | Source a config file | -| `start-server` | `start` | (none) | none | Start server (usually implicit) | -| `suspend-client` | `suspendc` | `t:` | none | Suspend a client | +| tmux Command | Why | +|---|---| +| `command-prompt` | Opens interactive prompt, blocks until user types | +| `confirm-before` | Blocks waiting for y/n confirmation (even `-y` blocks in control mode) | +| `display-menu` | Opens interactive menu, blocks until selection | From 0401348a50c14d5c07ec5547afd23c5cae6de781 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Fri, 27 Mar 2026 05:19:49 -0500 Subject: [PATCH 5/7] docs(parity): update command-mapping to reflect 100% effective coverage why: All 90 tmux commands are now either directly wrapped (82) or covered by aliases/flags (8). display-menu is wrapped but has a test gap. what: - Update summary to 82/90 directly wrapped (91%), 90/90 effective (100%) - Move display-menu from "Not Wrappable" to "Test Gaps" section - Remove command-prompt and confirm-before from unwrappable (now testable via send-keys -K) - Add "Notable Test Innovations" section documenting the send-keys -K approach and ControlMode testing patterns --- .../tmux-parity/references/command-mapping.md | 22 +++++++++++-------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/skills/tmux-parity/references/command-mapping.md b/skills/tmux-parity/references/command-mapping.md index e938a049d..6670211a7 100644 --- a/skills/tmux-parity/references/command-mapping.md +++ b/skills/tmux-parity/references/command-mapping.md @@ -8,10 +8,9 @@ bash .claude-plugin/scripts/extract-libtmux-methods.sh ## Summary -- **Directly wrapped**: 79/90 commands (87%) +- **Directly wrapped**: 82/90 commands (91%) - **Covered by alias/flag**: 8 additional commands -- **Truly unwrappable**: 3 commands (block waiting for interactive input) -- **Total effective coverage**: 87/90 (96%) +- **Total effective coverage**: 90/90 (100%) ## Covered by Alias/Flag (8 commands) @@ -28,12 +27,17 @@ These commands are not called directly but their functionality is available: | `set-window-option` | `OptionsMixin.set_option(scope=OptionScope.Window)` | Alias for `set-option -w` | | `show-window-options` | `OptionsMixin.show_options(scope=OptionScope.Window)` | Alias for `show-options -w` | -## Not Wrappable (3 commands) +## Test Gaps (1 command) -These block forever waiting for interactive user input: +| tmux Command | Method | Why | +|---|---|---| +| `display-menu` | `Server.display_menu()` | Requires TTY-backed client. Control-mode clients have `tty.sy=0`, causing `menu_prepare()` to return NULL. Method exists but cannot be tested hermetically. | + +## Notable Test Innovations -| tmux Command | Why | +| Command | Testing Approach | |---|---| -| `command-prompt` | Opens interactive prompt, blocks until user types | -| `confirm-before` | Blocks waiting for y/n confirmation (even `-y` blocks in control mode) | -| `display-menu` | Opens interactive menu, blocks until selection | +| `confirm-before` | `send-keys -K -c ` injects 'y' into status prompt handler (tmux 3.4+) | +| `command-prompt` | `send-keys -K -c ` types text + Enter into status prompt (tmux 3.4+) | +| `display-popup` | ControlMode client + marker file side-effect verification | +| `detach-client` | ControlMode client + `list-clients` count assertion | From 15b971fae6a69b6e3764bed30d00d48f01aaaa91 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sat, 2 May 2026 18:28:13 -0500 Subject: [PATCH 6/7] docs(tmux-parity[skill]): update stale coverage claims MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit why: SKILL.md said "~28 of ~88 tmux commands wrapped" and listed join-pane, swap-pane, run-shell, display-popup as high-priority unwrapped — all of which the tmux-parity branch wraps. command-mapping.md likewise listed last-pane, next-layout, previous-layout, move-pane as alias/flag-covered, despite the branch adding direct wrappers for each (commits dd8c65f0, 2ab4c65f, aa00c45d). Static numbers in generated docs go stale fast; static "unwrapped" lists go stale faster. what: - Rewrite SKILL.md "Current Coverage Summary" to point at the extraction scripts rather than baking in a number/list that immediately rots. Note that effective coverage is 100%. - Rewrite command-mapping.md "Covered by Alias/Flag" — there are now exactly four indirect cases (list-panes, list-windows, set-window-option, show-window-options), all reached through internal queries / option scoping. Updated count from 8/82 to 4/86. - Convert ```bash command blocks to ```console with $ prefix per AGENTS.md. --- skills/tmux-parity/SKILL.md | 20 ++++++---- .../tmux-parity/references/command-mapping.md | 40 ++++++++++--------- 2 files changed, 34 insertions(+), 26 deletions(-) diff --git a/skills/tmux-parity/SKILL.md b/skills/tmux-parity/SKILL.md index 069480d14..c695cf286 100644 --- a/skills/tmux-parity/SKILL.md +++ b/skills/tmux-parity/SKILL.md @@ -57,18 +57,24 @@ Class hierarchy mapping from tmux target types: ## Current Coverage Summary -**~28 of ~88 tmux commands wrapped** (~32% coverage, approximate — run extraction scripts for current data). High-priority unwrapped commands include: `join-pane`, `swap-pane`, `swap-window`, `respawn-pane`, `respawn-window`, `run-shell`, `break-pane`, `move-pane`, `pipe-pane`, `display-popup`. +Coverage is effectively 100% — every tmux command is reachable from +the Python API, either directly or via internal queries / option +scoping. The four indirect cases are listed in +`references/command-mapping.md`. + +Static numbers go stale fast. **Run the extraction scripts** when you +need current counts before making coverage claims. ## Extraction Scripts Run these for up-to-date data: -```bash -# All tmux commands with flags and target types -bash .claude-plugin/scripts/extract-tmux-commands.sh ~/study/c/tmux +```console +$ bash .claude-plugin/scripts/extract-tmux-commands.sh ~/study/c/tmux +``` -# All tmux commands libtmux currently wraps -bash .claude-plugin/scripts/extract-libtmux-methods.sh +```console +$ bash .claude-plugin/scripts/extract-libtmux-methods.sh ``` ## Additional Resources @@ -76,6 +82,6 @@ bash .claude-plugin/scripts/extract-libtmux-methods.sh ### Reference Files For detailed data, consult: -- **`references/command-mapping.md`** — Complete mapping of all ~88 tmux commands to libtmux methods, with flag coverage +- **`references/command-mapping.md`** — Mapping of every tmux command to its libtmux entrypoint, including the four reached indirectly - **`references/libtmux-patterns.md`** — Implementation patterns for wrapping new commands (method signatures, doctests, logging, error handling) - **`references/tmux-command-table.md`** — Guide to navigating tmux C source: cmd_entry fields, getopt format, target types, options-table.c, format.c diff --git a/skills/tmux-parity/references/command-mapping.md b/skills/tmux-parity/references/command-mapping.md index 6670211a7..ec281a72e 100644 --- a/skills/tmux-parity/references/command-mapping.md +++ b/skills/tmux-parity/references/command-mapping.md @@ -1,31 +1,33 @@ # tmux Command → libtmux Method Mapping -Generated from tmux HEAD and libtmux source. Re-generate with: -```bash -bash .claude-plugin/scripts/extract-tmux-commands.sh ~/study/c/tmux -bash .claude-plugin/scripts/extract-libtmux-methods.sh +Run the extraction scripts for current data — these numbers shift as +tmux and libtmux evolve: + +```console +$ bash .claude-plugin/scripts/extract-tmux-commands.sh ~/study/c/tmux +``` + +```console +$ bash .claude-plugin/scripts/extract-libtmux-methods.sh ``` ## Summary -- **Directly wrapped**: 82/90 commands (91%) -- **Covered by alias/flag**: 8 additional commands -- **Total effective coverage**: 90/90 (100%) +- **Directly invoked**: 86 of 90 tmux commands +- **Covered indirectly via internal queries / option scoping**: 4 +- **Total effective coverage**: 90 / 90 (100%) -## Covered by Alias/Flag (8 commands) +## Covered Indirectly (4 commands) -These commands are not called directly but their functionality is available: +These four tmux commands aren't called by name in `libtmux` source but +their functionality is reachable through other primitives: -| tmux Command | Covered By | How | -|---|---|---| -| `last-pane` | `Window.last_pane()`, `Pane.select(last=True)` | `-l` flag on select-pane | -| `list-panes` | `Window.panes` property | Used internally by `neo.py` | -| `list-windows` | `Session.windows` property | Used internally by `neo.py` | -| `move-pane` | `Pane.join()` | Same C source as join-pane | -| `next-layout` | `Window.select_layout(next_layout=True)` | `-n` flag on select-layout | -| `previous-layout` | `Window.select_layout(previous_layout=True)` | `-o` flag on select-layout | -| `set-window-option` | `OptionsMixin.set_option(scope=OptionScope.Window)` | Alias for `set-option -w` | -| `show-window-options` | `OptionsMixin.show_options(scope=OptionScope.Window)` | Alias for `show-options -w` | +| tmux Command | Reached Through | +|---|---| +| `list-panes` | `Window.panes` property (issued internally by `neo.py` queries) | +| `list-windows` | `Session.windows` property (issued internally by `neo.py` queries) | +| `set-window-option` | `OptionsMixin.set_option(scope=OptionScope.Window)` — `set-option -w` | +| `show-window-options` | `OptionsMixin.show_options(scope=OptionScope.Window)` — `show-options -w` | ## Test Gaps (1 command) From 6d7b01801eb5660548329ee0513b4e8fc0998bb4 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Sat, 2 May 2026 18:30:35 -0500 Subject: [PATCH 7/7] docs(tmux-parity[plugin]): use console fences with $ prefix per AGENTS.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit why: AGENTS.md "Shell Command Formatting" requires command examples in docs to use ```console with a "$ " prefix (not ```bash) and to keep one command per code block so the prompt is unambiguous and copy-pastable. The plugin docs added in this branch had multi-command ```bash blocks scattered across .claude/commands and skills/tmux-parity references. what: - .claude/agents/parity-analyzer.md: 3 blocks → console+$. - .claude/commands/version-diff.md: 2 blocks → console+$. - .claude/commands/implement-command.md: split the 6-command verification block into 6 console+$ blocks. - skills/tmux-parity/references/tmux-command-table.md: 2 blocks → console+$ (one of which had two ls invocations — split per AGENTS.md "one command per block"). --- .claude/agents/parity-analyzer.md | 19 +++++++----- .claude/commands/implement-command.md | 30 +++++++++++-------- .claude/commands/version-diff.md | 11 ++++--- .../references/tmux-command-table.md | 17 +++++++---- 4 files changed, 47 insertions(+), 30 deletions(-) diff --git a/.claude/agents/parity-analyzer.md b/.claude/agents/parity-analyzer.md index 10dc9fe3b..ba0b6872a 100644 --- a/.claude/agents/parity-analyzer.md +++ b/.claude/agents/parity-analyzer.md @@ -52,22 +52,27 @@ You are a tmux/libtmux feature parity analysis specialist. Analyze the gap betwe ### Step 1: Extract tmux commands Run the extraction script for current data: -```bash -bash .claude-plugin/scripts/extract-tmux-commands.sh ~/study/c/tmux + +```console +$ bash .claude-plugin/scripts/extract-tmux-commands.sh ~/study/c/tmux ``` -This outputs `command|alias|getopt|target` for all ~88 tmux commands. + +This outputs `command|alias|getopt|target` for every tmux command. ### Step 2: Extract libtmux coverage Run the libtmux extraction: -```bash -bash .claude-plugin/scripts/extract-libtmux-methods.sh + +```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()`: -```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 '"' + +```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 diff --git a/.claude/commands/implement-command.md b/.claude/commands/implement-command.md index 9e70407c9..4c39cfd18 100644 --- a/.claude/commands/implement-command.md +++ b/.claude/commands/implement-command.md @@ -104,24 +104,28 @@ Add tests in `tests/test_{class}.py` (or a new file if warranted): Run the full verification workflow: -```bash -# Format -uv run ruff format . +```console +$ uv run ruff format . +``` -# Lint -uv run ruff check . --fix --show-fixes +```console +$ uv run ruff check . --fix --show-fixes +``` -# Type check -uv run mypy src tests +```console +$ uv run mypy src tests +``` -# Test the specific file -uv run pytest tests/test_{class}.py -x -v +```console +$ uv run pytest tests/test_{class}.py -x -v +``` -# Run doctests -uv run pytest --doctest-modules src/libtmux/{class}.py -v +```console +$ uv run pytest --doctest-modules src/libtmux/{class}.py -v +``` -# Full test suite -uv run pytest +```console +$ uv run pytest ``` All must pass before considering the implementation complete. diff --git a/.claude/commands/version-diff.md b/.claude/commands/version-diff.md index 53db7d711..c3f4ae6cb 100644 --- a/.claude/commands/version-diff.md +++ b/.claude/commands/version-diff.md @@ -17,16 +17,19 @@ Compare tmux features between two versions using the source worktrees at `~/stud Extract `version1`, `version2`, and optional `command-name` from `$ARGUMENTS`. If no arguments provided, list available versions: -```bash -ls -d ~/study/c/tmux-*/ | sed 's|.*/tmux-||;s|/$||' | sort -V + +```console +$ ls -d ~/study/c/tmux-*/ | sed 's|.*/tmux-||;s|/$||' | sort -V ``` + Then ask the user which two versions to compare. ## Validate Worktrees Verify both worktrees exist: -```bash -ls -d ~/study/c/tmux-{version1}/ ~/study/c/tmux-{version2}/ 2>/dev/null + +```console +$ ls -d ~/study/c/tmux-{version1}/ ~/study/c/tmux-{version2}/ 2>/dev/null ``` ## Single Command Comparison (when command-name given) diff --git a/skills/tmux-parity/references/tmux-command-table.md b/skills/tmux-parity/references/tmux-command-table.md index 4fb7ff3fc..b810bb0ff 100644 --- a/skills/tmux-parity/references/tmux-command-table.md +++ b/skills/tmux-parity/references/tmux-command-table.md @@ -89,13 +89,18 @@ Compare against libtmux: `src/libtmux/formats.py` - 2.0 through 2.9, 2.9a - 3.0, 3.0a, 3.1 through 3.1c, 3.2, 3.2a, 3.3, 3.3a, 3.4, 3.5, 3.5a, 3.6, 3.6a -To check if a command exists in a version: -```bash -ls ~/study/c/tmux-3.0/cmd-display-popup.c 2>/dev/null # Not found = added later -ls ~/study/c/tmux-3.3/cmd-display-popup.c 2>/dev/null # Found = exists in 3.3 +To check if a command exists in a version (not-found = added later): + +```console +$ ls ~/study/c/tmux-3.0/cmd-display-popup.c 2>/dev/null +``` + +```console +$ ls ~/study/c/tmux-3.3/cmd-display-popup.c 2>/dev/null ``` To diff a command across versions: -```bash -diff ~/study/c/tmux-3.0/cmd-send-keys.c ~/study/c/tmux-3.6a/cmd-send-keys.c + +```console +$ diff ~/study/c/tmux-3.0/cmd-send-keys.c ~/study/c/tmux-3.6a/cmd-send-keys.c ```