Add split-wezterm: split WezTerm pane into a rows x cols grid
Splits the current pane into evenly-sized grid using wezterm cli split-pane. Supports optional command per pane, dryrun mode, and auto-detects native or Flatpak WezTerm CLI. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
146
scripts/split-wezterm
Executable file
146
scripts/split-wezterm
Executable file
@@ -0,0 +1,146 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# split-wezterm — Split the current WezTerm pane into a rows x cols grid
|
||||
|
||||
DRYRUN=false
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage: split-wezterm [--dryrun|-n] <rows> <cols> [--] [command...]
|
||||
|
||||
Split the current WezTerm pane into an evenly-sized grid.
|
||||
|
||||
Arguments:
|
||||
rows Number of rows (1-10)
|
||||
cols Number of columns (1-10)
|
||||
command... Optional command to run in each new pane
|
||||
|
||||
Flags:
|
||||
--dryrun, -n Preview commands without executing
|
||||
--help, -h Show this help
|
||||
|
||||
Examples:
|
||||
split-wezterm 3 4
|
||||
split-wezterm 2 2 -- htop
|
||||
split-wezterm --dryrun 3 4 -- claude
|
||||
EOF
|
||||
}
|
||||
|
||||
die() { echo "Error: $*" >&2; exit 1; }
|
||||
|
||||
# Parse flags
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--help|-h) usage; exit 0 ;;
|
||||
--dryrun|-n) DRYRUN=true; shift ;;
|
||||
--) shift; break ;;
|
||||
-*) die "Unknown flag: $1" ;;
|
||||
*) break ;;
|
||||
esac
|
||||
done
|
||||
|
||||
[[ $# -lt 2 ]] && { usage >&2; exit 1; }
|
||||
|
||||
ROWS="$1"; shift
|
||||
COLS="$1"; shift
|
||||
# Skip -- separator if present
|
||||
[[ "${1:-}" == "--" ]] && shift
|
||||
CMD=("$@")
|
||||
|
||||
# Validate dimensions
|
||||
[[ "$ROWS" =~ ^[0-9]+$ ]] || die "rows must be a positive integer, got: $ROWS"
|
||||
[[ "$COLS" =~ ^[0-9]+$ ]] || die "cols must be a positive integer, got: $COLS"
|
||||
(( ROWS >= 1 && ROWS <= 10 )) || die "rows must be 1-10, got: $ROWS"
|
||||
(( COLS >= 1 && COLS <= 10 )) || die "cols must be 1-10, got: $COLS"
|
||||
|
||||
if (( ROWS == 1 && COLS == 1 )); then
|
||||
echo "Already a single pane, nothing to do."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Detect WezTerm CLI
|
||||
detect_cli() {
|
||||
if command -v wezterm &>/dev/null; then
|
||||
echo "wezterm"
|
||||
elif flatpak info org.wezfurlong.wezterm &>/dev/null 2>&1; then
|
||||
echo "flatpak run org.wezfurlong.wezterm"
|
||||
else
|
||||
die "WezTerm CLI not found. Install WezTerm or ensure it's in PATH."
|
||||
fi
|
||||
}
|
||||
|
||||
WEZTERM_CLI=$(detect_cli)
|
||||
|
||||
# Get the starting pane ID
|
||||
if [[ -n "${WEZTERM_PANE:-}" ]]; then
|
||||
START_PANE="$WEZTERM_PANE"
|
||||
else
|
||||
# Try to find the active pane from wezterm cli list
|
||||
START_PANE=$($WEZTERM_CLI cli list --format json 2>/dev/null \
|
||||
| python3 -c "import sys,json; panes=json.load(sys.stdin); print(panes[0]['pane_id'])" 2>/dev/null) \
|
||||
|| die "Cannot determine current pane. Set WEZTERM_PANE or run from inside WezTerm."
|
||||
fi
|
||||
|
||||
# Run or preview a wezterm cli command; capture stdout (the new pane ID)
|
||||
run_cli() {
|
||||
if $DRYRUN; then
|
||||
echo "[dryrun] $WEZTERM_CLI cli $*" >&2
|
||||
echo "dry-$$-$RANDOM" # fake pane ID for dryrun tracking
|
||||
else
|
||||
$WEZTERM_CLI cli "$@"
|
||||
fi
|
||||
}
|
||||
|
||||
# Split a pane into N equal parts along an axis.
|
||||
# Usage: split_axis <pane_id> <count> <direction_flag> <result_array_name>
|
||||
# direction_flag: --bottom (rows) or --horizontal (cols)
|
||||
# Populates the named array with all pane IDs (original + new)
|
||||
split_axis() {
|
||||
local pane_id="$1" count="$2" direction="$3" result_var="$4"
|
||||
local -a pane_ids=("$pane_id")
|
||||
|
||||
local i percent new_pane
|
||||
for (( i = 0; i < count - 1; i++ )); do
|
||||
percent=$(( (count - 1 - i) * 100 / (count - i) ))
|
||||
local cmd_args=(split-pane --pane-id "$pane_id" "$direction" --percent "$percent")
|
||||
if [[ ${#CMD[@]} -gt 0 ]]; then
|
||||
cmd_args+=(-- "${CMD[@]}")
|
||||
fi
|
||||
new_pane=$(run_cli "${cmd_args[@]}")
|
||||
pane_id="$new_pane"
|
||||
pane_ids+=("$new_pane")
|
||||
done
|
||||
|
||||
# Copy results to the named variable
|
||||
eval "$result_var=(\"\${pane_ids[@]}\")"
|
||||
}
|
||||
|
||||
# Step 1: Split into rows
|
||||
split_axis "$START_PANE" "$ROWS" "--bottom" row_panes
|
||||
|
||||
# Step 2: Split each row into columns
|
||||
for row_pane in "${row_panes[@]}"; do
|
||||
if (( COLS > 1 )); then
|
||||
split_axis "$row_pane" "$COLS" "--horizontal" _col_panes
|
||||
fi
|
||||
done
|
||||
|
||||
# Step 3: If a command was given, run it in the original pane too
|
||||
if [[ ${#CMD[@]} -gt 0 ]]; then
|
||||
cmd_text="${CMD[*]}"
|
||||
if $DRYRUN; then
|
||||
echo "[dryrun] $WEZTERM_CLI cli send-text --pane-id $START_PANE --no-paste -- \"$cmd_text\"" >&2
|
||||
else
|
||||
# Send the command text followed by enter
|
||||
$WEZTERM_CLI cli send-text --pane-id "$START_PANE" --no-paste -- "$cmd_text
|
||||
"
|
||||
fi
|
||||
fi
|
||||
|
||||
TOTAL=$(( ROWS * COLS ))
|
||||
if $DRYRUN; then
|
||||
echo "[dryrun] Would create ${ROWS}x${COLS} grid ($TOTAL panes)"
|
||||
else
|
||||
echo "Created ${ROWS}x${COLS} grid ($TOTAL panes)"
|
||||
fi
|
||||
89
specs/split-wezterm.spec.md
Normal file
89
specs/split-wezterm.spec.md
Normal file
@@ -0,0 +1,89 @@
|
||||
# split-wezterm — OpenSpec
|
||||
|
||||
## Purpose
|
||||
|
||||
Split the current WezTerm pane into an evenly-sized grid of rows × columns, optionally running a command in each new pane.
|
||||
|
||||
## Usage
|
||||
|
||||
```
|
||||
split-wezterm <rows> <cols> [--] [command...]
|
||||
split-wezterm --help
|
||||
split-wezterm --dryrun <rows> <cols> [--] [command...]
|
||||
```
|
||||
|
||||
### Arguments
|
||||
|
||||
| Argument | Required | Description |
|
||||
|----------|----------|-------------|
|
||||
| `rows` | Yes | Number of rows (1-10) |
|
||||
| `cols` | Yes | Number of columns (1-10) |
|
||||
| `command...` | No | Command to run in each new pane. If omitted, panes open the default shell. |
|
||||
|
||||
### Flags
|
||||
|
||||
| Flag | Description |
|
||||
|------|-------------|
|
||||
| `--help`, `-h` | Show usage information |
|
||||
| `--dryrun`, `-n` | Preview the split-pane commands without executing them |
|
||||
|
||||
## Behaviour
|
||||
|
||||
1. Validate that `rows` and `cols` are integers between 1 and 10
|
||||
2. Detect the WezTerm CLI binary (`wezterm` or `flatpak run org.wezfurlong.wezterm`)
|
||||
3. Get the current pane ID from `$WEZTERM_PANE` or fall back to `wezterm cli list` to find the active pane
|
||||
4. Create the grid using the following algorithm:
|
||||
a. **Create rows first:** Split the original pane vertically (rows-1) times. Each split uses `--bottom --percent P` where P is calculated to produce equal-height rows. The first split takes `(rows-1)/rows` as the bottom percent, the next takes `(rows-2)/(rows-1)`, etc.
|
||||
b. **Collect row pane IDs:** The original pane becomes row 0. Each `split-pane --bottom` returns the new pane ID.
|
||||
c. **Create columns:** For each row pane, split horizontally (cols-1) times using the same proportional math for equal-width columns.
|
||||
d. **Run command:** If a command was specified, pass it as the program argument to each `split-pane` call. The first pane (original) doesn't get split — if a command is specified, use `wezterm cli send-text` to run it there.
|
||||
5. Output a summary: "Created {rows}x{cols} grid ({total} panes)"
|
||||
|
||||
### Proportional splitting math
|
||||
|
||||
To split a pane into N equal parts:
|
||||
- Split i (0-indexed, i=0..N-2): `percent = (N - 1 - i) * 100 / (N - i)`
|
||||
- This ensures each resulting section is 1/N of the original.
|
||||
|
||||
Example for 3 splits: percent = 66, 50 (producing 33/33/33 split).
|
||||
|
||||
## Dryrun behaviour
|
||||
|
||||
In `--dryrun` mode, print each `wezterm cli split-pane` command that would be executed, prefixed with `[dryrun]`. Do not execute any commands. Still validate inputs and detect the CLI binary.
|
||||
|
||||
Example:
|
||||
```
|
||||
[dryrun] wezterm cli split-pane --pane-id 0 --bottom --percent 66
|
||||
[dryrun] wezterm cli split-pane --pane-id 0 --bottom --percent 50
|
||||
[dryrun] wezterm cli split-pane --pane-id 0 --horizontal --percent 75
|
||||
...
|
||||
[dryrun] Would create 3x4 grid (12 panes)
|
||||
```
|
||||
|
||||
## Edge cases
|
||||
|
||||
- `1 1` — No splits needed. Print "Already a single pane, nothing to do." and exit 0.
|
||||
- `1 N` — Only horizontal splits (columns only, no row splits).
|
||||
- `N 1` — Only vertical splits (rows only, no column splits).
|
||||
- No WezTerm CLI available — exit 1 with error message.
|
||||
- `$WEZTERM_PANE` not set and no active pane found — exit 1 with error.
|
||||
- Non-integer or out-of-range arguments — exit 1 with usage hint.
|
||||
|
||||
## Examples
|
||||
|
||||
```bash
|
||||
# Split into 3 rows x 4 columns
|
||||
split-wezterm 3 4
|
||||
|
||||
# Split into 2x2 grid, each pane running htop
|
||||
split-wezterm 2 2 -- htop
|
||||
|
||||
# Split into 2x3 grid, each running claude with a profile
|
||||
split-wezterm 2 3 -- claude-with-profiles
|
||||
|
||||
# Preview what would happen
|
||||
split-wezterm --dryrun 3 4
|
||||
|
||||
# Single row, 3 columns
|
||||
split-wezterm 1 3
|
||||
```
|
||||
67
tests/test-split-wezterm.sh
Executable file
67
tests/test-split-wezterm.sh
Executable file
@@ -0,0 +1,67 @@
|
||||
#!/usr/bin/env bash
|
||||
set -uo pipefail
|
||||
|
||||
SCRIPT="$(cd "$(dirname "$0")/.." && pwd)/scripts/split-wezterm"
|
||||
PASS=0; FAIL=0
|
||||
|
||||
pass() { echo -e "\e[32m PASS\e[0m $1"; (( PASS++ )); }
|
||||
fail() { echo -e "\e[31m FAIL\e[0m $1: $2"; (( FAIL++ )); }
|
||||
|
||||
assert_exit() {
|
||||
local desc="$1" expected="$2"; shift 2
|
||||
local actual
|
||||
"$@" >/dev/null 2>&1 && actual=0 || actual=$?
|
||||
if [[ "$actual" == "$expected" ]]; then pass "$desc"; else fail "$desc" "expected exit $expected, got $actual"; fi
|
||||
}
|
||||
|
||||
assert_output_contains() {
|
||||
local desc="$1" pattern="$2"; shift 2
|
||||
local output
|
||||
output=$("$@" 2>&1) || true
|
||||
if echo "$output" | grep -qF "$pattern"; then pass "$desc"; else fail "$desc" "output missing: $pattern"; fi
|
||||
}
|
||||
|
||||
assert_line_count() {
|
||||
local desc="$1" expected="$2"; shift 2
|
||||
local count
|
||||
count=$("$@" 2>&1 | grep -c '^\[dryrun\]') || true
|
||||
if [[ "$count" == "$expected" ]]; then pass "$desc"; else fail "$desc" "expected $expected dryrun lines, got $count"; fi
|
||||
}
|
||||
|
||||
export WEZTERM_PANE=0
|
||||
|
||||
echo "=== split-wezterm tests ==="
|
||||
|
||||
# Help
|
||||
assert_exit "help flag exits 0" 0 "$SCRIPT" --help
|
||||
assert_output_contains "help shows usage" "Usage:" "$SCRIPT" --help
|
||||
|
||||
# Validation
|
||||
assert_exit "no args exits 1" 1 "$SCRIPT"
|
||||
assert_exit "one arg exits 1" 1 "$SCRIPT" 3
|
||||
assert_exit "zero rows exits 1" 1 "$SCRIPT" 0 3
|
||||
assert_exit "rows > 10 exits 1" 1 "$SCRIPT" 11 3
|
||||
assert_exit "non-integer rows exits 1" 1 "$SCRIPT" abc 3
|
||||
|
||||
# 1x1 — no-op
|
||||
assert_exit "1x1 exits 0" 0 "$SCRIPT" 1 1
|
||||
assert_output_contains "1x1 says nothing to do" "nothing to do" "$SCRIPT" 1 1
|
||||
|
||||
# Dryrun counts (each split-pane = 1 dryrun line + 1 summary line)
|
||||
# 2x1 = 1 row split + 1 summary = 2 lines
|
||||
assert_line_count "2x1 produces 1 split" 2 "$SCRIPT" --dryrun 2 1
|
||||
# 1x3 = 2 col splits + 1 summary = 3 lines
|
||||
assert_line_count "1x3 produces 2 splits" 3 "$SCRIPT" --dryrun 1 3
|
||||
# 2x2 = 1 row + 2 col + 1 summary = 4 lines
|
||||
assert_line_count "2x2 produces 3 splits" 4 "$SCRIPT" --dryrun 2 2
|
||||
# 3x4 = 2 row + 9 col + 1 summary = 12 lines
|
||||
assert_line_count "3x4 produces 11 splits" 12 "$SCRIPT" --dryrun 3 4
|
||||
# 3x4 with command = 11 splits + 1 send-text + 1 summary = 13 lines
|
||||
assert_line_count "3x4 with cmd produces 13 lines" 13 "$SCRIPT" --dryrun 3 4 -- echo hi
|
||||
|
||||
# Summary line
|
||||
assert_output_contains "dryrun summary" "Would create 3x4 grid (12 panes)" "$SCRIPT" --dryrun 3 4
|
||||
|
||||
echo ""
|
||||
echo "Results: $PASS passed, $FAIL failed"
|
||||
(( FAIL == 0 ))
|
||||
Reference in New Issue
Block a user