diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json new file mode 100644 index 000000000..e6293f617 --- /dev/null +++ b/.claude-plugin/plugin.json @@ -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"] +} 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/agents/parity-analyzer.md b/.claude/agents/parity-analyzer.md new file mode 100644 index 000000000..ba0b6872a --- /dev/null +++ b/.claude/agents/parity-analyzer.md @@ -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. + + + 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: + +```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. diff --git a/.claude/commands/implement-command.md b/.claude/commands/implement-command.md new file mode 100644 index 000000000..4c39cfd18 --- /dev/null +++ b/.claude/commands/implement-command.md @@ -0,0 +1,131 @@ +--- +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 `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. diff --git a/.claude/commands/parity-audit.md b/.claude/commands/parity-audit.md new file mode 100644 index 000000000..8994cdb51 --- /dev/null +++ b/.claude/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 `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/commands/version-diff.md b/.claude/commands/version-diff.md new file mode 100644 index 000000000..c3f4ae6cb --- /dev/null +++ b/.claude/commands/version-diff.md @@ -0,0 +1,109 @@ +--- +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: + +```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: + +```console +$ 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/skills/tmux-parity/SKILL.md b/skills/tmux-parity/SKILL.md new file mode 100644 index 000000000..c695cf286 --- /dev/null +++ b/skills/tmux-parity/SKILL.md @@ -0,0 +1,87 @@ +--- +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 + +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: + +```console +$ bash .claude-plugin/scripts/extract-tmux-commands.sh ~/study/c/tmux +``` + +```console +$ bash .claude-plugin/scripts/extract-libtmux-methods.sh +``` + +## Additional Resources + +### Reference Files + +For detailed data, consult: +- **`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 new file mode 100644 index 000000000..ec281a72e --- /dev/null +++ b/skills/tmux-parity/references/command-mapping.md @@ -0,0 +1,45 @@ +# tmux Command → libtmux Method Mapping + +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 invoked**: 86 of 90 tmux commands +- **Covered indirectly via internal queries / option scoping**: 4 +- **Total effective coverage**: 90 / 90 (100%) + +## Covered Indirectly (4 commands) + +These four tmux commands aren't called by name in `libtmux` source but +their functionality is reachable through other primitives: + +| 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) + +| 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 + +| Command | Testing Approach | +|---|---| +| `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 | diff --git a/skills/tmux-parity/references/libtmux-patterns.md b/skills/tmux-parity/references/libtmux-patterns.md new file mode 100644 index 000000000..a18b6b1d4 --- /dev/null +++ b/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/skills/tmux-parity/references/tmux-command-table.md b/skills/tmux-parity/references/tmux-command-table.md new file mode 100644 index 000000000..b810bb0ff --- /dev/null +++ b/skills/tmux-parity/references/tmux-command-table.md @@ -0,0 +1,106 @@ +# 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 (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: + +```console +$ diff ~/study/c/tmux-3.0/cmd-send-keys.c ~/study/c/tmux-3.6a/cmd-send-keys.c +```