From 3f4e729a2b5880f85e0c0994357f8cb7888dc7b0 Mon Sep 17 00:00:00 2001 From: Blaine Motsinger Date: Mon, 21 Sep 2026 10:14:04 -0400 Subject: [PATCH 1/6] GH-10: extract resolve_template_path() helper --- bin/tmux-ws | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/bin/tmux-ws b/bin/tmux-ws index bebeb54..4a1aa0f 100755 --- a/bin/tmux-ws +++ b/bin/tmux-ws @@ -99,6 +99,16 @@ validate_dir() { } # --- Config Loading --- +resolve_template_path() { + local template_name="$1" + local template_file="$CONFIG_DIR/_templates/${template_name}.conf" + if [[ ! -f "$template_file" ]]; then + error "Template '$template_name' not found at $template_file" + exit 1 + fi + echo "$template_file" +} + load_config() { local name="$1" local config_dir="$CONFIG_DIR/$name" @@ -127,7 +137,8 @@ load_config() { # Inherit from base template if _BASE is set if [[ -n "$_BASE" ]]; then - local base_file="$CONFIG_DIR/_templates/${_BASE}.conf" + local base_file + base_file=$(resolve_template_path "$_BASE") if [[ -f "$base_file" ]]; then # Save workspace values local ws_windows=("${WINDOWS[@]+"${WINDOWS[@]}"}") @@ -153,10 +164,6 @@ load_config() { if [[ "$DEFAULT_DIR" == "$HOME" && "$tmpl_default" != "$HOME" ]]; then DEFAULT_DIR="$tmpl_default" fi - else - error "Base template '$_BASE' not found at $base_file" - error " Create it with: $SCRIPT_NAME init" - exit 1 fi fi From b3c78a5b9b5319d4654dcd65d8315eaee6ae295d Mon Sep 17 00:00:00 2001 From: Blaine Motsinger Date: Mon, 21 Sep 2026 10:22:37 -0400 Subject: [PATCH 2/6] GH-10: add 'add' command to scaffold workspace configs from template --- bin/tmux-ws | 79 +++++++++++++++++++++++++++++++++++------ tests/test_tmux_ws.bats | 60 +++++++++++++++++++++++++++++++ 2 files changed, 129 insertions(+), 10 deletions(-) diff --git a/bin/tmux-ws b/bin/tmux-ws index 4a1aa0f..b3ff48c 100755 --- a/bin/tmux-ws +++ b/bin/tmux-ws @@ -20,18 +20,19 @@ usage() { echo -e "${BOLD}$SCRIPT_NAME${NC} — tmux workspace manager" echo -e "" echo -e "${BOLD}Usage:${NC}" - echo -e " $SCRIPT_NAME create [--detach|-d] Create and attach to a workspace" - echo -e " $SCRIPT_NAME attach Attach to a running workspace" - echo -e " $SCRIPT_NAME list List available and running workspaces" - echo -e " $SCRIPT_NAME kill Kill a workspace session" - echo -e " $SCRIPT_NAME edit Open workspace config in \$EDITOR" - echo -e " $SCRIPT_NAME init Create config directory structure" - echo -e " $SCRIPT_NAME version Show version" - echo -e " $SCRIPT_NAME help Show this help" + echo -e " $SCRIPT_NAME add [dir] [--template ] Scaffold workspace config" + echo -e " $SCRIPT_NAME launch [--detach|-d] Create and attach to workspace" + echo -e " $SCRIPT_NAME attach Attach to running workspace" + echo -e " $SCRIPT_NAME list List available and running workspaces" + echo -e " $SCRIPT_NAME kill Kill workspace session" + echo -e " $SCRIPT_NAME edit Open workspace config in \$EDITOR" + echo -e " $SCRIPT_NAME init Create config directory structure" + echo -e " $SCRIPT_NAME version Show version" + echo -e " $SCRIPT_NAME help Show this help" echo -e "" echo -e "${BOLD}Options:${NC}" - echo -e " --detach, -d Create workspace without attaching" - echo -e " --force, -f Kill existing session before creating" + echo -e " --detach, -d Launch workspace without attaching" + echo -e " --template Use a specific template (default: default)" echo -e "" echo -e "${BOLD}Config:${NC}" echo -e " $CONFIG_DIR//workspace.conf" @@ -294,6 +295,63 @@ cmd_create() { fi } +cmd_add() { + local name="" + local dir="" + local template="default" + + # Parse args + while [[ $# -gt 0 ]]; do + case "$1" in + --template) template="$2"; shift 2 ;; + -*) error "Unknown option: $1"; usage; exit 1 ;; + *) if [[ -z "$name" ]]; then name="$1"; else dir="$1"; fi; shift ;; + esac + done + + if [[ -z "$name" ]]; then + error "Workspace name required" + usage + exit 1 + fi + + local config_file="$CONFIG_DIR/$name/workspace.conf" + if [[ -f "$config_file" ]]; then + error "Workspace '$name' already exists at $config_file" + exit 1 + fi + + # Resolve and validate template + local template_file + template_file=$(resolve_template_path "$template") + + # Create workspace directory + mkdir -p "$CONFIG_DIR/$name/hooks" + + # Copy template and substitute DEFAULT_DIR if dir arg given + if [[ -n "$dir" ]]; then + sed "s|^DEFAULT_DIR=.*|DEFAULT_DIR=\"$dir\"|" "$template_file" > "$config_file" + validate_dir "$dir" "Workspace directory" + else + cp "$template_file" "$config_file" + fi + + # Create hook stubs + cat > "$CONFIG_DIR/$name/hooks/pre-create.sh" <<'STUB' +#!/bin/bash +# Runs before the tmux session is created. +# Use this to start services, check prerequisites, etc. +STUB + + cat > "$CONFIG_DIR/$name/hooks/post-create.sh" <<'STUB' +#!/bin/bash +# Runs after the tmux session is created. +# Use this to wait for services, open URLs, send notifications, etc. +STUB + + info "Workspace '$name' scaffolded at $CONFIG_DIR/$name/" +} + cmd_list() { header "Available workspaces" local count=0 @@ -438,6 +496,7 @@ main() { shift case "$command" in + add) cmd_add "$@" ;; create) cmd_create "$@" ;; attach) cmd_attach "$@" ;; list) cmd_list ;; diff --git a/tests/test_tmux_ws.bats b/tests/test_tmux_ws.bats index 5030f6b..3a38658 100644 --- a/tests/test_tmux_ws.bats +++ b/tests/test_tmux_ws.bats @@ -20,6 +20,14 @@ setup() { mkdir -p "$TEST_CONFIG_DIR/bad-format" mkdir -p "$TEST_CONFIG_DIR/no-template/hooks" mkdir -p "$TEST_CONFIG_DIR/fail-hook/hooks" + cat > "$TEST_CONFIG_DIR/_templates/node.conf" < "$TEST_CONFIG_DIR/_templates/default.conf" < Date: Mon, 21 Sep 2026 10:34:26 -0400 Subject: [PATCH 3/6] GH-10: restore create command in usage text until launch rename --- bin/tmux-ws | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bin/tmux-ws b/bin/tmux-ws index b3ff48c..6de48a3 100755 --- a/bin/tmux-ws +++ b/bin/tmux-ws @@ -21,7 +21,7 @@ usage() { echo -e "" echo -e "${BOLD}Usage:${NC}" echo -e " $SCRIPT_NAME add [dir] [--template ] Scaffold workspace config" - echo -e " $SCRIPT_NAME launch [--detach|-d] Create and attach to workspace" + echo -e " $SCRIPT_NAME create [--detach|-d] Create and attach to workspace" echo -e " $SCRIPT_NAME attach Attach to running workspace" echo -e " $SCRIPT_NAME list List available and running workspaces" echo -e " $SCRIPT_NAME kill Kill workspace session" @@ -32,6 +32,7 @@ usage() { echo -e "" echo -e "${BOLD}Options:${NC}" echo -e " --detach, -d Launch workspace without attaching" + echo -e " --force, -f Kill existing session before creating" echo -e " --template Use a specific template (default: default)" echo -e "" echo -e "${BOLD}Config:${NC}" From 1ea6bfa3953de8bef98724118178847e5e76932e Mon Sep 17 00:00:00 2001 From: Blaine Motsinger Date: Mon, 21 Sep 2026 10:42:09 -0400 Subject: [PATCH 4/6] GH-10: rename create to launch, remove --force flag --- bin/tmux-ws | 26 ++++++++-------------- tests/test_tmux_ws.bats | 49 ++++++++++++++++++----------------------- 2 files changed, 30 insertions(+), 45 deletions(-) diff --git a/bin/tmux-ws b/bin/tmux-ws index 6de48a3..a3c9961 100755 --- a/bin/tmux-ws +++ b/bin/tmux-ws @@ -21,7 +21,7 @@ usage() { echo -e "" echo -e "${BOLD}Usage:${NC}" echo -e " $SCRIPT_NAME add [dir] [--template ] Scaffold workspace config" - echo -e " $SCRIPT_NAME create [--detach|-d] Create and attach to workspace" + echo -e " $SCRIPT_NAME launch [--detach|-d] Launch and attach to workspace" echo -e " $SCRIPT_NAME attach Attach to running workspace" echo -e " $SCRIPT_NAME list List available and running workspaces" echo -e " $SCRIPT_NAME kill Kill workspace session" @@ -32,7 +32,6 @@ usage() { echo -e "" echo -e "${BOLD}Options:${NC}" echo -e " --detach, -d Launch workspace without attaching" - echo -e " --force, -f Kill existing session before creating" echo -e " --template Use a specific template (default: default)" echo -e "" echo -e "${BOLD}Config:${NC}" @@ -216,16 +215,14 @@ parse_window_entry() { } # --- Core Commands --- -cmd_create() { +cmd_launch() { local name="" local detach=false - local force=false # Parse args while [[ $# -gt 0 ]]; do case "$1" in --detach|-d) detach=true; shift ;; - --force|-f) force=true; shift ;; -*) error "Unknown option: $1"; usage; exit 1 ;; *) name="$1"; shift ;; esac @@ -242,18 +239,13 @@ cmd_create() { # Check if session already exists if tmux has-session -t "$name" 2>/dev/null; then - if [[ "$force" == true ]]; then - warn "Killing existing session '$name'" - tmux kill-session -t "$name" + info "Session '$name' already exists" + if [[ "$detach" == true ]]; then + echo " Attach with: tmux attach -t $name" else - info "Session '$name' already exists" - if [[ "$detach" == true ]]; then - echo " Attach with: tmux attach -t $name" - else - tmux attach -t "$name" - fi - return + tmux attach -t "$name" fi + return fi # Run pre-create hooks @@ -420,7 +412,7 @@ cmd_attach() { if ! tmux has-session -t "$name" 2>/dev/null; then error "Session '$name' is not running" - echo " Create it with: $SCRIPT_NAME create $name" + echo " Create it with: $SCRIPT_NAME launch $name" exit 1 fi @@ -498,7 +490,7 @@ main() { case "$command" in add) cmd_add "$@" ;; - create) cmd_create "$@" ;; + launch) cmd_launch "$@" ;; attach) cmd_attach "$@" ;; list) cmd_list ;; kill) cmd_kill "$@" ;; diff --git a/tests/test_tmux_ws.bats b/tests/test_tmux_ws.bats index 3a38658..146174f 100644 --- a/tests/test_tmux_ws.bats +++ b/tests/test_tmux_ws.bats @@ -174,22 +174,22 @@ teardown() { [[ "${output}" == *"Running sessions"* ]] } -# --- Create --- +# --- Launch --- -@test "create requires a name" { - run tmux-ws create +@test "launch requires a name" { + run tmux-ws launch [ "$status" -eq 1 ] [[ "${output}" == *"Workspace name required"* ]] } -@test "create fails for nonexistent workspace" { - run tmux-ws create nonexistent --detach +@test "launch fails for nonexistent workspace" { + run tmux-ws launch nonexistent --detach [ "$status" -eq 1 ] [[ "${output}" == *"not found"* ]] } -@test "create builds workspace from config" { - run tmux-ws create test-workspace --detach +@test "launch builds workspace from config" { + run tmux-ws launch test-workspace --detach [ "$status" -eq 0 ] [[ "${output}" == *"created with 3 windows"* ]] @@ -202,20 +202,20 @@ teardown() { [[ "${output}" == *"shell"* ]] } -@test "create runs pre-create hook" { - run tmux-ws create test-workspace --detach +@test "launch runs pre-create hook" { + run tmux-ws launch test-workspace --detach [ "$status" -eq 0 ] [ -f "$TEST_CONFIG_DIR/_hook_ran" ] } -@test "create runs post-create hook" { - run tmux-ws create test-workspace --detach +@test "launch runs post-create hook" { + run tmux-ws launch test-workspace --detach [ "$status" -eq 0 ] [ -f "$TEST_CONFIG_DIR/_hook_ran_post" ] } -@test "create with inheritance" { - run tmux-ws create myapp --detach +@test "launch with inheritance" { + run tmux-ws launch myapp --detach [ "$status" -eq 0 ] [[ "${output}" == *"created with 2 windows"* ]] @@ -224,16 +224,9 @@ teardown() { [[ "${output}" == *"terminal"* ]] } -@test "create --force kills existing session" { +@test "launch reattaches to existing session" { tmux new-session -d -s test-workspace - run tmux-ws create test-workspace --detach --force - [ "$status" -eq 0 ] - [[ "${output}" == *"Killing existing session"* ]] -} - -@test "create reattaches to existing session without --force" { - tmux new-session -d -s test-workspace - run tmux-ws create test-workspace --detach + run tmux-ws launch test-workspace --detach [ "$status" -eq 0 ] [[ "${output}" == *"already exists"* ]] } @@ -292,20 +285,20 @@ teardown() { # --- Validation --- -@test "create fails for malformed window entry" { - run tmux-ws create bad-format --detach +@test "launch fails for malformed window entry" { + run tmux-ws launch bad-format --detach [ "$status" -eq 1 ] [[ "${output}" == *"invalid format"* ]] } -@test "create fails for missing _BASE template" { - run tmux-ws create no-template --detach +@test "launch fails for missing _BASE template" { + run tmux-ws launch no-template --detach [ "$status" -eq 1 ] [[ "${output}" == *"not found"* ]] } -@test "create fails when hook fails" { - run tmux-ws create fail-hook --detach +@test "launch fails when hook fails" { + run tmux-ws launch fail-hook --detach [ "$status" -eq 1 ] [[ "${output}" == *"hook failed"* ]] } From 2a343ac649e0a0c01d49b564853d44f48bd0b8ec Mon Sep 17 00:00:00 2001 From: Blaine Motsinger Date: Mon, 21 Sep 2026 10:45:44 -0400 Subject: [PATCH 5/6] GH-10: update README for add/launch commands --- README.md | 36 ++++++++++++++++++++++-------------- 1 file changed, 22 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 22e9089..5df9f87 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,8 @@ source ~/.bashrc # or source ~/.zshrc ## Usage ``` -tmux-ws create [--detach|-d] [--force|-f] Create and attach to a workspace +tmux-ws add [dir] [--template ] Scaffold workspace config +tmux-ws launch [--detach|-d] Create and attach to a workspace tmux-ws attach Attach to a running workspace tmux-ws list List available and running workspaces tmux-ws kill Kill a workspace session @@ -34,11 +35,17 @@ tmux-ws help Show help ### Examples ```bash -# Create a workspace and attach -tmux-ws create project +# Scaffold a new workspace config from template +tmux-ws add myproject + +# Scaffold with a custom project directory +tmux-ws add myproject ~/projects/myapp + +# Create workspace and attach +tmux-ws launch project # Create in background, stay in current shell -tmux-ws create project --detach +tmux-ws launch project --detach # Attach to a running workspace tmux-ws attach project @@ -145,20 +152,21 @@ Hooks run in a subshell, so they cannot modify the script's state (WINDOWS, DEFA ## Creating a New Workspace -Copy an existing workspace or the example: +Use `add` to scaffold a workspace config from a template: -```bash -# From the example -cp -r ~/.config/tmux-ws/_examples ~/.config/tmux-ws/myproject + # Scaffold with defaults + tmux-ws add myproject -# From an existing workspace -cp -r ~/.config/tmux-ws/project ~/.config/tmux-ws/myproject + # Scaffold with a specific project directory + tmux-ws add myproject ~/projects/myapp -# Edit it -tmux-ws edit myproject -``` + # Edit the config + tmux-ws edit myproject + +Or copy an existing workspace manually: -Directories starting with `_` (like `_templates`, `_examples`) are hidden from `tmux-ws list` but still usable with `tmux-ws create`. + cp -r ~/.config/tmux-ws/project ~/.config/tmux-ws/myproject + tmux-ws edit myproject ## Testing From de356078e9afdf3db2b1208a2621af8de528b4b1 Mon Sep 17 00:00:00 2001 From: Blaine Motsinger Date: Mon, 21 Sep 2026 10:51:24 -0400 Subject: [PATCH 6/6] GH-10: fix stale create reference in test coverage line --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 5df9f87..e960edc 100644 --- a/README.md +++ b/README.md @@ -174,7 +174,7 @@ Or copy an existing workspace manually: make test ``` -Requires [bats](https://github.com/bats-core/bats-core). Tests cover: help, version, list, create, inheritance, hooks, validation, kill, attach, edit, and init. +Requires [bats](https://github.com/bats-core/bats-core). Tests cover: help, version, list, add, launch, inheritance, hooks, validation, kill, attach, edit, and init. ## tmux Configuration