From aff40753c7e7401b600cb06aa0777321571b2de8 Mon Sep 17 00:00:00 2001 From: Jun Peng <61768526+minorole@users.noreply.github.com> Date: Wed, 7 Jan 2026 10:24:55 -0500 Subject: [PATCH 1/2] feat: add tabs + panes hybrid layout support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add ability to create multiple tabs where each tab has its own pane layout. Users can now configure `tabs: N` alongside any layout to get N tabs, each with the specified pane configuration. Example: `tabs: 3` + `layout: duo` = 3 tabs × 2 panes = 6 workspaces Changes: - Add `tabs:` config parsing in lib/config.zsh - Create layout-hybrid.applescript for tabs with splits - Update setup wizard to ask about tabs before layout - Rename "panes" to "sections" in wizard for clarity - Update dry-run output to show tabs × panes breakdown BREAKING: Remove `layout: tabs` (deprecated) - Migration: Change `layout: tabs` to `layout: 1` and add `tabs: N` - Clear error message shown when old syntax detected --- CHANGELOG.md | 23 ++++ VERSION | 2 +- bin/gpane | 41 ++++-- config.example.yaml | 30 ++-- lib/config.zsh | 6 + lib/help.zsh | 19 ++- lib/layouts.zsh | 59 ++++---- lib/prompts.zsh | 122 ++++++++-------- lib/setup.zsh | 46 +++++-- scripts/layout-hybrid.applescript | 222 ++++++++++++++++++++++++++++++ scripts/layout-tabs.applescript | 100 -------------- 11 files changed, 449 insertions(+), 221 deletions(-) create mode 100644 scripts/layout-hybrid.applescript delete mode 100644 scripts/layout-tabs.applescript diff --git a/CHANGELOG.md b/CHANGELOG.md index d0382fe..76d5f3a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,29 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/), and this project adheres to [Semantic Versioning](https://semver.org/). +## [0.3.0] - 2025-01-07 + +### Added + +- **Tabs + Sections**: Combine multiple tabs with split sections in each tab + - Add `tabs: N` to config to create N tabs (2-10) + - Each tab uses your specified layout (duo, trio, quad, etc.) + - Example: `tabs: 3` + `layout: duo` = 3 tabs × 2 sections = 6 workspaces + - Commands fill in order: tab1-section1, tab1-section2, tab2-section1, ... + +### Changed + +- Setup wizard now asks "Do you want multiple tabs?" before layout selection +- Terminology: "panes" renamed to "sections" in wizard for clarity +- Help text updated to explain tabs + sections + +### Removed + +- **BREAKING**: `layout: tabs` is removed + - Migration: Change `layout: tabs` to `layout: 1` and add `tabs: N` + - Running with old config shows clear migration instructions + - The new system is more flexible: any layout can have tabs + ## [0.2.7] - 2025-12-30 ### Fixed diff --git a/VERSION b/VERSION index b003284..0d91a54 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.2.7 +0.3.0 diff --git a/bin/gpane b/bin/gpane index c760008..04f4542 100755 --- a/bin/gpane +++ b/bin/gpane @@ -44,19 +44,31 @@ open_session() { [[ "${reuse_window}" == "true" ]] && window_mode="current" echo " ${project_name}" echo " dir: ${project_dir}" - echo " layout: ${resolved_layout}" + + local panes_per_tab + panes_per_tab=$(compute_total_panes "${resolved_layout}") + + if (( TABS_COUNT > 1 )); then + echo " layout: ${resolved_layout} × ${TABS_COUNT} tabs (${panes_per_tab} panes/tab)" + else + echo " layout: ${resolved_layout}" + fi echo " window: ${window_mode}" - if is_tabs_layout "${resolved_layout}"; then - local num_tabs=${#PANE_COMMANDS[@]} - (( num_tabs > 10 )) && num_tabs=10 - for ((i = 1; i <= num_tabs; i++)); do - local cmd="${PANE_COMMANDS[$i]:-}" - echo " tab ${i}/${num_tabs}: ${cmd:-}" + + if (( TABS_COUNT > 1 )); then + # Tabs + panes mode + local cmd_idx=1 + for ((t = 1; t <= TABS_COUNT; t++)); do + echo " tab ${t}/${TABS_COUNT}:" + for ((p = 1; p <= panes_per_tab; p++)); do + local cmd="${PANE_COMMANDS[$cmd_idx]:-}" + echo " pane ${p}: ${cmd:-}" + cmd_idx=$((cmd_idx + 1)) + done done else - local total_panes - total_panes=$(compute_total_panes "${resolved_layout}") - for ((i = 1; i <= total_panes; i++)); do + # Single window with panes + for ((i = 1; i <= panes_per_tab; i++)); do local cmd="${PANE_COMMANDS[$i]:-}" echo " pane $i: ${cmd:-}" done @@ -64,9 +76,14 @@ open_session() { return 0 fi + # Check for deprecated layout: tabs syntax + if ! check_deprecated_tabs_layout "${resolved_layout}"; then + return 1 + fi + # Route to appropriate layout handler - if is_tabs_layout "${resolved_layout}"; then - layout_tabs "${project_name}" "${project_dir}" "${reuse_window}" "${PANE_COMMANDS[@]}" + if (( TABS_COUNT > 1 )); then + layout_hybrid "${project_name}" "${project_dir}" "${resolved_layout}" "${TABS_COUNT}" "${reuse_window}" "${PANE_COMMANDS[@]}" else layout_dynamic "${project_name}" "${project_dir}" "${resolved_layout}" "${reuse_window}" "${PANE_COMMANDS[@]}" fi diff --git a/config.example.yaml b/config.example.yaml index 1bad7fd..edf444d 100644 --- a/config.example.yaml +++ b/config.example.yaml @@ -5,13 +5,17 @@ # Root directory containing your projects projects_root: ~/Projects -# Default layout (see 'gpane help' for all options) -# Pane layouts: duo, trio, quad, dashboard, stacked, wide, or row notation (2-2, 1-3, etc.) -# Tab layout: tabs (one command per tab, up to 10) +# Default layout (sections per tab) +# Options: duo, trio, quad, dashboard, stacked, wide, or row notation (2-2, 1-3, etc.) default_layout: duo -# Default commands for each pane/tab (empty string = just shell) -# Commands are applied in order: left-to-right, top-to-bottom for panes; tab 1, 2, 3... for tabs +# Optional: Number of tabs (2-10) +# If omitted or set to 1, creates a single window with the layout above +# tabs: 3 + +# Default commands for each section (empty string = just shell) +# Commands fill in order: left-to-right, top-to-bottom +# For multiple tabs: tab1-section1, tab1-section2, tab2-section1, tab2-section2, ... default_commands: - "claude" - "" @@ -26,16 +30,20 @@ default_commands: # - "npm test" # - "" # +# # Example: 3 tabs, each with 2 sections (6 total workspaces) # webapp: -# layout: tabs +# layout: duo +# tabs: 3 # commands: -# - "claude" -# - "npm run dev" -# - "npm test" -# - "" +# - "claude" # Tab 1, Left +# - "" # Tab 1, Right +# - "npm run dev" # Tab 2, Left +# - "npm test" # Tab 2, Right +# - "htop" # Tab 3, Left +# - "" # Tab 3, Right # Tips: # - Use "clear && claude" for a cleaner startup # - For unattended workflows, Claude supports --dangerously-skip-permissions # (use with caution - this bypasses all confirmation prompts) -# - Leave a pane/tab empty ("") for a regular shell +# - Leave a section empty ("") for a regular shell diff --git a/lib/config.zsh b/lib/config.zsh index 0f801e7..3832a3a 100644 --- a/lib/config.zsh +++ b/lib/config.zsh @@ -18,6 +18,7 @@ CONFIG_FILE="${CONFIG_DIR}/config.yaml" # Global config variables (set by parse_config) PROJECTS_ROOT="" DEFAULT_LAYOUT="3-col" +TABS_COUNT=1 # Number of tabs (1 = single window, 2-10 = multiple tabs) # Array of commands in spatial order (left-to-right, top-to-bottom) typeset -a PANE_COMMANDS @@ -31,6 +32,7 @@ parse_config() { # Reset to defaults PROJECTS_ROOT="" DEFAULT_LAYOUT="3-col" + TABS_COUNT=1 PANE_COMMANDS=() local line in_commands=false @@ -57,6 +59,8 @@ parse_config() { PROJECTS_ROOT="${PROJECTS_ROOT/#\~/${HOME}}" elif [[ "${line}" =~ ^default_layout:[[:space:]]*(.+)$ ]]; then DEFAULT_LAYOUT="${match[1]}" + elif [[ "${line}" =~ ^tabs:[[:space:]]*([0-9]+)$ ]]; then + TABS_COUNT="${match[1]}" fi # Parse default commands (array format: "- value") @@ -129,6 +133,8 @@ parse_project_config() { if [[ "${in_project}" == true ]]; then if [[ "${line}" =~ ^[[:space:]]+layout:[[:space:]]*(.+)$ ]]; then DEFAULT_LAYOUT="${match[1]}" + elif [[ "${line}" =~ ^[[:space:]]+tabs:[[:space:]]*([0-9]+)$ ]]; then + TABS_COUNT="${match[1]}" # Handle commands array format elif [[ "${in_commands}" == true ]]; then if [[ "${line}" =~ ^[[:space:]]+-[[:space:]]+\"(.*)\"$ ]]; then diff --git a/lib/help.zsh b/lib/help.zsh index 0739360..e296d1f 100644 --- a/lib/help.zsh +++ b/lib/help.zsh @@ -26,13 +26,18 @@ Layouts: Row notation: "2" (2 side-by-side), "2-2" (4-pane grid), "1-3" (1 top + 3 bottom) Aliases: - tabs Multiple tabs (one command per tab, up to 10) - duo 2 panes side-by-side - trio 3 panes side-by-side - quad 2x2 grid (4 panes) - dashboard 1 top + 3 bottom (4 panes) - stacked 2 panes vertically - wide 3 top + 1 bottom (4 panes) + duo 2 sections side-by-side + trio 3 sections side-by-side + quad 2x2 grid (4 sections) + dashboard 1 top + 3 bottom (4 sections) + stacked 2 sections vertically + wide 3 top + 1 bottom (4 sections) + +Tabs (optional): + Add 'tabs: N' to your config (2-10) to create multiple tabs. + Each tab will have the layout you specify. + + Example: layout: duo + tabs: 3 = 3 tabs, each with 2 sections Examples: gpane setup # First-time setup diff --git a/lib/layouts.zsh b/lib/layouts.zsh index 04c5c5b..8b7de07 100644 --- a/lib/layouts.zsh +++ b/lib/layouts.zsh @@ -19,7 +19,6 @@ LAYOUT_ALIASES[stacked]="1-1" LAYOUT_ALIASES[quad]="2-2" LAYOUT_ALIASES[dashboard]="1-3" LAYOUT_ALIASES[wide]="3-1" -LAYOUT_ALIASES[tabs]="tabs" # Resolve layout alias to row notation # Usage: resolve_layout_alias "quad" -> "2-2" @@ -103,11 +102,6 @@ get_layout_info() { LAYOUT_PANE_LABELS=() case "${resolved}" in - tabs) - # Tabs layout: default to 4 tabs for setup wizard - LAYOUT_PANE_COUNT=4 - LAYOUT_PANE_LABELS=("Tab 1" "Tab 2" "Tab 3" "Tab 4") - ;; 2) LAYOUT_PANE_COUNT=2; LAYOUT_PANE_LABELS=("Left" "Right") ;; 3) LAYOUT_PANE_COUNT=3; LAYOUT_PANE_LABELS=("Left" "Middle" "Right") ;; 1-1) LAYOUT_PANE_COUNT=2; LAYOUT_PANE_LABELS=("Top" "Bottom") ;; @@ -127,41 +121,56 @@ get_layout_info() { } # ============================================================================= -# Tabs Layout +# Hybrid Layout (tabs + panes) # ============================================================================= -# Layout: tabs (one tab per command, no splits) -# Each command runs in its own tab -layout_tabs() { +# Layout: hybrid (multiple tabs, each with pane splits) +# Commands fill tabs in order: tab1-pane1, tab1-pane2, ..., tab2-pane1, ... +layout_hybrid() { local project_name=$1 local project_dir=$2 - local reuse_window=$3 - shift 3 + local layout_spec=$3 + local tabs_count=$4 + local reuse_window=$5 + shift 5 local -a cmds=("$@") local label="[${project_name}]" - # Require at least one command - if (( ${#cmds[@]} == 0 )); then - echo "Error: tabs layout requires at least one command" >&2 + # Validate layout + if ! validate_layout "$layout_spec"; then + echo "Error: Invalid layout '$layout_spec'" >&2 return 1 fi - # Max 10 tabs (matches Cmd+1 through Cmd+0 shortcuts) - if (( ${#cmds[@]} > 10 )); then - echo "Warning: Limiting to 10 tabs (${#cmds[@]} commands provided)" >&2 - cmds=("${cmds[@]:0:10}") + # Validate tabs count + if (( tabs_count < 1 || tabs_count > 10 )); then + echo "Error: tabs must be between 1 and 10 (got $tabs_count)" >&2 + return 1 fi - run_layout "layout-tabs" "${label}" "${reuse_window}" "${project_dir}" "${cmds[@]}" + run_layout "layout-hybrid" "${label}" "${reuse_window}" "${project_dir}" "${layout_spec}" "${tabs_count}" "${cmds[@]}" } -# Check if layout is tabs mode -is_tabs_layout() { +# Check for deprecated layout: tabs syntax +# Returns 1 and shows migration message if deprecated syntax detected +check_deprecated_tabs_layout() { local layout=$1 - local resolved - resolved=$(resolve_layout_alias "$layout") - [[ "$resolved" == "tabs" ]] + if [[ "$layout" == "tabs" ]]; then + echo "" >&2 + echo "Error: 'layout: tabs' is deprecated." >&2 + echo "" >&2 + echo "Update your config to use the new tabs + panes system:" >&2 + echo " layout: 1 # panes per tab (or duo, trio, quad, etc.)" >&2 + echo " tabs: N # number of tabs (2-10)" >&2 + echo "" >&2 + echo "Example - 3 tabs, each with 2 panes:" >&2 + echo " layout: duo" >&2 + echo " tabs: 3" >&2 + echo "" >&2 + return 1 + fi + return 0 } # ============================================================================= diff --git a/lib/prompts.zsh b/lib/prompts.zsh index e293b42..162490d 100644 --- a/lib/prompts.zsh +++ b/lib/prompts.zsh @@ -17,26 +17,20 @@ show_layout_menu() { local reset=$'\e[0m' if [[ "${style}" == "full" ]]; then - echo "Layouts:" - echo " ${dim}── Panes (split window) ──${reset}" - echo " 1) duo Two panes side-by-side [1|2]" - echo " 2) trio Three panes side-by-side [1|2|3]" - echo " 3) stacked Two panes vertically [1] / [2]" - echo " 4) quad 2x2 grid [1|2] / [3|4]" - echo " 5) dashboard 1 main + 3 below [ 1 ] / [2|3|4]" - echo " 6) wide 3 top + 1 bottom [1|2|3] / [ 4 ]" - echo " 7) custom Enter your own (e.g. 2-3, 1-2-1)" + echo "How many sections per tab?" echo "" - echo " ${cyan}── Tabs (separate tabs) ──${reset}" - echo " ${cyan}8) tabs One tab per command (up to 10)${reset}" + echo " 1) duo Two sections side-by-side [1|2]" + echo " 2) trio Three sections [1|2|3]" + echo " 3) stacked Two sections vertically [1] / [2]" + echo " 4) quad 2x2 grid [1|2] / [3|4]" + echo " 5) dashboard 1 main + 3 below [ 1 ] / [2|3|4]" + echo " 6) wide 3 top + 1 bottom [1|2|3] / [ 4 ]" + echo " 7) custom Enter your own (e.g. 2-3, 1-2-1)" else - echo "Layout:" - echo " ${dim}── Panes ──${reset}" + echo "Sections per tab:" echo " 1) duo 2) trio 3) stacked" echo " 4) quad 5) dashboard 6) wide" echo " 7) custom" - echo " ${cyan}── Tabs ──${reset}" - echo " ${cyan}8) tabs${reset}" fi echo "" } @@ -47,8 +41,7 @@ show_layout_menu() { # use "" to allow empty selection (keep current) # # Sets globals: -# SELECTED_LAYOUT - the layout name/spec (e.g., "duo", "tabs", "2-3"), empty if keeping default -# SELECTED_NUM_TABS - number of tabs if tabs layout (0 otherwise) +# SELECTED_LAYOUT - the layout name/spec (e.g., "duo", "2-3"), empty if keeping default # SELECTED_LAYOUT_VALID - true if selection was valid, false if invalid input prompt_layout_choice() { local default_choice="${1:-1}" @@ -58,7 +51,6 @@ prompt_layout_choice() { layout_choice="${layout_choice:-${default_choice}}" SELECTED_LAYOUT="" - SELECTED_NUM_TABS=0 SELECTED_LAYOUT_VALID=true case "${layout_choice}" in @@ -70,8 +62,8 @@ prompt_layout_choice() { 6) SELECTED_LAYOUT="wide" ;; 7) echo "" - echo "Custom layout: N-M-O (panes per row, max 10 panes, max 4 rows)" - echo "Examples: 2-2 (4 panes), 1-3 (4 panes), 2-3-1 (6 panes)" + echo "Custom layout: N-M-O (sections per row, max 10 sections, max 4 rows)" + echo "Examples: 2-2 (4 sections), 1-3 (4 sections), 2-3-1 (6 sections)" local custom_layout="" while true; do prompt_input "Layout: " custom_layout @@ -82,22 +74,6 @@ prompt_layout_choice() { echo "Invalid layout. Try again (e.g., 2-2, 1-3, 2-3-1)" done ;; - 8) - SELECTED_LAYOUT="tabs" - echo "" - local tabs_input="" - while true; do - prompt_input "How many tabs? [2-10, default 4]: " tabs_input - if [[ -z "${tabs_input}" ]]; then - SELECTED_NUM_TABS=4 - break - elif [[ "${tabs_input}" =~ ^[0-9]+$ ]] && (( tabs_input >= 2 && tabs_input <= 10 )); then - SELECTED_NUM_TABS="${tabs_input}" - break - fi - echo "Invalid number. Enter 2-10 or press Enter for default." - done - ;; "") # Empty input with no default - valid, means keep current SELECTED_LAYOUT="" @@ -111,6 +87,35 @@ prompt_layout_choice() { esac } +# ============================================================================= +# Tabs Prompt +# ============================================================================= + +# Prompt for tabs choice +# Usage: prompt_tabs_choice +# +# Sets global: +# SELECTED_TABS_COUNT - number of tabs (1 = no tabs, 2-10 = multiple tabs) +prompt_tabs_choice() { + SELECTED_TABS_COUNT=1 + + local want_tabs="" + prompt_input "Do you want multiple tabs? [y/N]: " want_tabs + + if [[ "${want_tabs}" =~ ^[Yy]$ ]]; then + local tabs_input="" + while true; do + prompt_input "How many tabs? [2-10, default 3]: " tabs_input + tabs_input="${tabs_input:-3}" + if [[ "${tabs_input}" =~ ^[0-9]+$ ]] && (( tabs_input >= 2 && tabs_input <= 10 )); then + SELECTED_TABS_COUNT="${tabs_input}" + break + fi + echo "Enter a number between 2 and 10." + done + fi +} + # ============================================================================= # Command Prompting # ============================================================================= @@ -141,36 +146,41 @@ prompt_pane_commands() { # Layout Info Helpers # ============================================================================= -# Get pane count and labels for a layout, with optional tab count override +# Get pane count and labels for a layout, with optional tab count # Usage: get_layout_info_for_prompts [num_tabs] # # Sets globals: -# PROMPT_PANE_COUNT - number of panes/tabs +# PROMPT_PANE_COUNT - total number of command slots # PROMPT_PANE_LABELS - array of labels -# PROMPT_UNIT - "pane" or "tab" +# PROMPT_UNIT - "section" (beginner-friendly) get_layout_info_for_prompts() { local layout=$1 - local num_tabs="${2:-0}" + local num_tabs="${2:-1}" PROMPT_PANE_COUNT=0 PROMPT_PANE_LABELS=() - PROMPT_UNIT="pane" - - if [[ "${layout}" == "tabs" ]]; then - PROMPT_UNIT="tab" - if (( num_tabs > 0 )); then - PROMPT_PANE_COUNT="${num_tabs}" - else - PROMPT_PANE_COUNT=4 # default - fi - local i=1 - while (( i <= PROMPT_PANE_COUNT )); do - PROMPT_PANE_LABELS+=("Tab ${i}") - i=$((i + 1)) + PROMPT_UNIT="section" + + # Get base layout info + get_layout_info "${layout}" + local panes_per_tab=$LAYOUT_PANE_COUNT + local -a base_labels=("${LAYOUT_PANE_LABELS[@]}") + + if (( num_tabs > 1 )); then + # Hybrid mode: tabs × panes + PROMPT_PANE_COUNT=$((num_tabs * panes_per_tab)) + local t=1 + while (( t <= num_tabs )); do + local p=1 + while (( p <= panes_per_tab )); do + PROMPT_PANE_LABELS+=("Tab ${t} - ${base_labels[$p]}") + p=$((p + 1)) + done + t=$((t + 1)) done else - get_layout_info "${layout}" - PROMPT_PANE_COUNT=$LAYOUT_PANE_COUNT - PROMPT_PANE_LABELS=("${LAYOUT_PANE_LABELS[@]}") + # Single window mode + PROMPT_PANE_COUNT=$panes_per_tab + PROMPT_PANE_LABELS=("${base_labels[@]}") fi } diff --git a/lib/setup.zsh b/lib/setup.zsh index 87571c2..4016e8f 100644 --- a/lib/setup.zsh +++ b/lib/setup.zsh @@ -103,7 +103,12 @@ setup_wizard() { local projects_root="${SELECTED_PROJECTS_ROOT}" echo "" - # Step 2: Layout selection + # Step 2: Tabs selection + prompt_tabs_choice + local tabs_count="${SELECTED_TABS_COUNT}" + echo "" + + # Step 3: Layout selection (sections per tab) show_layout_menu "full" while true; do prompt_layout_choice "1" @@ -111,10 +116,9 @@ setup_wizard() { done local default_layout="${SELECTED_LAYOUT}" - local num_tabs="${SELECTED_NUM_TABS}" - # Step 3: Get pane/tab info and prompt for commands - get_layout_info_for_prompts "${default_layout}" "${num_tabs}" + # Step 4: Get layout info and prompt for commands + get_layout_info_for_prompts "${default_layout}" "${tabs_count}" echo "" echo "Commands for each ${PROMPT_UNIT} (Enter = empty shell)" @@ -128,7 +132,11 @@ setup_wizard() { echo "" echo "Summary:" echo " Folder: ${projects_root}" - echo " Layout: ${default_layout}" + if (( tabs_count > 1 )); then + echo " Layout: ${default_layout} × ${tabs_count} tabs" + else + echo " Layout: ${default_layout}" + fi echo " Commands:" local i=1 while (( i <= PROMPT_PANE_COUNT )); do @@ -159,10 +167,17 @@ setup_wizard() { i=$((i + 1)) done + # Build tabs line (only include if > 1) + local tabs_yaml="" + if (( tabs_count > 1 )); then + tabs_yaml="tabs: ${tabs_count}" + fi + cat > "${CONFIG_FILE}" < 4 then + repeat with i from 5 to (count of argv) + set end of commands to item i of argv + end repeat + end if + + -- Parse layout spec "2-2" into list {2, 2} + set rowCounts to parseLayout(layoutSpec) + set numRows to count of rowCounts + + -- Calculate total panes per tab + set panesPerTab to 0 + repeat with r in rowCounts + set panesPerTab to panesPerTab + r + end repeat + + -- Activate Ghostty + try + tell application "Ghostty" to activate + on error errMsg + error "Cannot activate Ghostty: " & errMsg + end try + + delay 0.3 + + try + tell application "System Events" + if not (exists process "Ghostty") then + error "Ghostty process not found. Is Ghostty running?" + end if + + tell process "Ghostty" + -- New window (unless reusing current) + if reuseWindow is "false" then + keystroke "n" using {command down} + delay 1.0 -- Reliable window creation delay + end if + + -- Ensure focused before starting setup + set frontmost to true + delay 0.5 + + -- === TAB LOOP === + repeat with tabIdx from 1 to tabsCount + + -- Create new tab if not the first tab + if tabIdx > 1 then + keystroke "t" using {command down} + delay 0.4 + end if + + -- Calculate command base index for this tab + -- cmdIndex = (tabIdx - 1) * panesPerTab + spatialIdx + set tabCmdBase to (tabIdx - 1) * panesPerTab + + -- cd to project directory (this is pane 1 of current tab) + set the clipboard to "cd " & (quoted form of projectDir) & " && clear" + keystroke "v" using {command down} + key code 36 -- Enter + delay 0.5 + + -- === PHASE 1: Create row spine (n-1 vertical splits) === + -- This creates one anchor pane per row + if numRows > 1 then + repeat numRows - 1 times + keystroke "d" using {command down, shift down} + delay 0.4 + end repeat + + -- Navigate to top row using directional navigation + repeat numRows - 1 times + key code 126 using {command down, option down} -- Up + delay 0.3 + end repeat + end if + delay 0.3 + + -- === PHASE 2: Expand rows and type commands inline === + -- Visit order matches spatial order: row by row, left to right + -- We type each command immediately when we're in that pane + set spatialIdx to 1 -- 1-based index within current tab + + repeat with rowIdx from 1 to numRows + set panesInRow to item rowIdx of rowCounts + + -- Type command for this row's anchor (we're already here) + set cmdIndex to tabCmdBase + spatialIdx + if cmdIndex <= (count of commands) then + set cmd to item cmdIndex of commands + if cmd is not "" then + set the clipboard to cmd + keystroke "v" using {command down} + key code 36 -- Enter + delay 0.4 -- Reliable completion delay + end if + end if + set spatialIdx to spatialIdx + 1 + + -- Create horizontal splits and type commands for each new pane + if panesInRow > 1 then + repeat panesInRow - 1 times + keystroke "d" using {command down} -- Split right + delay 0.6 -- Increased for split stability + + -- Now in the new split pane, type its command + set cmdIndex to tabCmdBase + spatialIdx + if cmdIndex <= (count of commands) then + set cmd to item cmdIndex of commands + if cmd is not "" then + set the clipboard to cmd + keystroke "v" using {command down} + key code 36 -- Enter + delay 0.4 + end if + end if + set spatialIdx to spatialIdx + 1 + end repeat + end if + + -- Navigate to next row anchor (if not last row) + if rowIdx < numRows then + delay 0.2 + -- Go LEFT back to col 0 of current row + if panesInRow > 1 then + repeat panesInRow - 1 times + key code 123 using {command down, option down} -- Left + delay 0.15 + end repeat + end if + -- Go DOWN to next row + key code 125 using {command down, option down} -- Down + delay 0.2 + end if + end repeat + + -- === PHASE 3: Equalize splits === + if panesPerTab > 1 then + key code 24 using {command down, control down} + delay 0.4 + end if + + -- === PHASE 4: Navigate to first pane of this tab === + -- Position cursor at top-left for consistency + delay 0.2 + repeat numRows - 1 times + key code 126 using {command down, option down} -- Up + delay 0.12 + end repeat + set maxCols to 0 + repeat with r in rowCounts + if r > maxCols then set maxCols to r + end repeat + repeat maxCols - 1 times + key code 123 using {command down, option down} -- Left + delay 0.12 + end repeat + + end repeat + -- === END TAB LOOP === + + -- Return to Tab 1 for user convenience + if tabsCount > 1 then + delay 0.2 + keystroke "1" using {command down} + delay 0.2 + end if + + end tell + end tell + + -- Success: Restore original clipboard if we saved it + if originalClipboard is not missing value then + try + set the clipboard to originalClipboard + end try + end if + + on error errMsg + -- Error: Restore clipboard before re-throwing + if originalClipboard is not missing value then + try + set the clipboard to originalClipboard + end try + end if + error errMsg + end try +end run + +-- Parse "2-2" into {2, 2}, "3" into {3} +on parseLayout(spec) + set AppleScript's text item delimiters to "-" + set parts to text items of spec + set AppleScript's text item delimiters to "" + + set rowList to {} + repeat with p in parts + set end of rowList to p as integer + end repeat + return rowList +end parseLayout diff --git a/scripts/layout-tabs.applescript b/scripts/layout-tabs.applescript deleted file mode 100644 index f094228..0000000 --- a/scripts/layout-tabs.applescript +++ /dev/null @@ -1,100 +0,0 @@ --- gpane tabs layout: creates multiple tabs, one command per tab --- Args: reuseWindow, projectDir, cmd1, cmd2, cmd3, ... --- reuseWindow: "true" to use current window, "false" to create new --- First tab uses existing tab in new window, subsequent tabs use Cmd+T - -on run argv - set reuseWindow to item 1 of argv - set projectDir to item 2 of argv - - -- Try to save current clipboard (may fail for some content types) - set originalClipboard to missing value - try - set originalClipboard to the clipboard - end try - - -- Parse commands (items 3 onward) - set commands to {} - if (count of argv) > 2 then - repeat with i from 3 to (count of argv) - set end of commands to item i of argv - end repeat - end if - - set numTabs to count of commands - - -- Activate Ghostty - try - tell application "Ghostty" to activate - on error errMsg - error "Cannot activate Ghostty: " & errMsg - end try - - delay 0.3 - - try - tell application "System Events" - if not (exists process "Ghostty") then - error "Ghostty process not found. Is Ghostty running?" - end if - - tell process "Ghostty" - -- New window (unless reusing current) - if reuseWindow is "false" then - keystroke "n" using {command down} - delay 1.0 -- Increased for more reliable window creation - end if - - -- Ensure focused before starting setup - set frontmost to true - delay 0.5 - - -- Process each tab - repeat with tabIdx from 1 to numTabs - -- For tabs after the first, create a new tab - if tabIdx > 1 then - keystroke "t" using {command down} - delay 0.4 - end if - - -- cd to project directory - set the clipboard to "cd " & (quoted form of projectDir) & " && clear" - keystroke "v" using {command down} - key code 36 -- Enter - delay 0.5 - - -- Run command if provided - if tabIdx <= (count of commands) then - set cmd to item tabIdx of commands - if cmd is not "" then - set the clipboard to cmd - keystroke "v" using {command down} - key code 36 -- Enter - delay 0.4 - end if - end if - end repeat - - -- Go back to first tab (Cmd+1) - delay 0.2 - keystroke "1" using {command down} - end tell - end tell - - -- Success: Restore original clipboard if we saved it - if originalClipboard is not missing value then - try - set the clipboard to originalClipboard - end try - end if - - on error errMsg - -- Error: Restore clipboard before re-throwing - if originalClipboard is not missing value then - try - set the clipboard to originalClipboard - end try - end if - error errMsg - end try -end run From 5d87579781b20a1a102f47c5928340854a242d21 Mon Sep 17 00:00:00 2001 From: Jun Peng <61768526+minorole@users.noreply.github.com> Date: Wed, 7 Jan 2026 12:55:39 -0500 Subject: [PATCH 2/2] docs: update website for v0.3.0 tabs feature MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Version badge: v0.2.7 → v0.3.0 - Hero: mention tabs in feature list - Feature 05: describe tabs capability --- docs/index.html | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/index.html b/docs/index.html index d53fa64..6a269bb 100644 --- a/docs/index.html +++ b/docs/index.html @@ -1006,7 +1006,7 @@ Features Layouts Install - v0.2.7 + v0.3.0 @@ -1029,8 +1029,8 @@

One command.
Infinite focus.

Stop splitting panes manually. Open multiple projects in parallel — - Claude in one window, Aider in another, each project's layout and - commands configured exactly how you like. + Claude in one window, Aider in another, each project's layout, tabs, + and commands configured exactly how you like.

@@ -1095,7 +1095,7 @@

Multi-Project Launch

05

Dynamic Layouts

-

2-10 panes in any configuration. Presets like quad and dashboard, or custom layouts like 2-2-5-1.

+

2-10 sections per window, optionally across multiple tabs. Presets like quad and dashboard, or custom layouts like 2-2-5-1.