diff --git a/CHANGELOG.md b/CHANGELOG.md index 69c5c95..f238aaf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,16 @@ # Changelog +## Unreleased + +### Features +- Add optional workdir display on line 1 of the statusline + (`CFG_SHOW_WORKDIR`, `CFG_WORKDIR_STYLE`, `CFG_WORKDIR_MAX_LEN`). Four + styles: `full`, `relative`, `basename`, `worktree` (default). The + `worktree` style renders the cwd basename only when it differs from the + repo name, disambiguating sibling git worktrees of the same repo. Long + paths are left-truncated with `…` so the worktree-specific tail is + preserved. + ## v1.0.1 — 2026-04-13 ### Security diff --git a/README.md b/README.md index c26c1c6..9d888ce 100644 --- a/README.md +++ b/README.md @@ -126,6 +126,7 @@ Edit the `CONFIGURATION` block at the top of the statusline script. No external | `CFG_SHOW_COST` | `true` | Session cost + burn rate + duration | | `CFG_SHOW_LINES` | `true` | Lines added/removed in session | | `CFG_SHOW_SESSION` | `true` | Session ID (first 8 chars) | +| `CFG_SHOW_WORKDIR` | `true` | Working directory displayed after the repo name | ### Settings @@ -137,6 +138,30 @@ Edit the `CONFIGURATION` block at the top of the statusline script. No external | `CFG_PREFIX` | `" . "` | Prefix for continuation lines | | `CFG_SEPARATOR` | `" \| "` | Separator between sections | | `CFG_BAR_WIDTH` | `15` | Width of the context progress bar | +| `CFG_WORKDIR_STYLE` | `"worktree"` | `"full"`, `"relative"`, `"basename"`, or `"worktree"` (see [Show workdir](#show-workdir)) | +| `CFG_WORKDIR_MAX_LEN` | `40` | Max workdir width. Longer paths are left-truncated with `…`. | + +### Show workdir + +When you run many git worktrees off the same repo, the repo name on line 1 is +identical across them and tells you nothing about which worktree you are in. +`CFG_SHOW_WORKDIR` appends a dim path fragment after the repo name to +disambiguate — rendered as `repo ▸ workdir`. + +`CFG_WORKDIR_STYLE` controls what gets shown: + +| Value | Renders | +|-------|---------| +| `"full"` | Absolute path (`/home/user/projects/my-app`) | +| `"relative"` | `$HOME`-relative path (`~/projects/my-app`) | +| `"basename"` | Just the cwd leaf (`my-app`) | +| `"worktree"` *(default)* | The cwd leaf, **only** when it differs from the repo name. Users with a single worktree see nothing; users with `code-koala-worktrees/p01-numbered-diffs` see `code-koala ▸ p01-numbered-diffs`. | + +Paths longer than `CFG_WORKDIR_MAX_LEN` (default `40`) are **left-truncated** +with `…` so the worktree-specific tail is always visible. Example: +`…/code-koala-worktrees/p01-numbered-diffs`. + +Set `CFG_SHOW_WORKDIR=false` to disable the suffix entirely. ### Minimal mode diff --git a/statusline.ps1 b/statusline.ps1 index a3cbd32..c199832 100644 --- a/statusline.ps1 +++ b/statusline.ps1 @@ -15,14 +15,17 @@ $CFG_SHOW_TOKENS = $true $CFG_SHOW_COST = $true $CFG_SHOW_LINES = $true $CFG_SHOW_SESSION = $true +$CFG_SHOW_WORKDIR = $true $CFG_WEATHER_UNIT = "C" # "C" for Celsius, "F" for Fahrenheit $CFG_CACHE_GIT_SEC = 5 $CFG_CACHE_WEATHER_SEC = 1800 # 30 minutes $CFG_PREFIX = " . " $CFG_SEPARATOR = " | " $CFG_BAR_WIDTH = 15 -$CFG_ACCENT_COLOR = "" # Hex color for accent line. Empty = rainbow gradient. -$CFG_STYLE = "banner" # "banner" (v2) or "classic" (v1 look) +$CFG_ACCENT_COLOR = "" # Hex color for accent line. Empty = rainbow gradient. +$CFG_STYLE = "banner" # "banner" (v2) or "classic" (v1 look) +$CFG_WORKDIR_STYLE = "worktree" # "full", "relative", "basename", "worktree" +$CFG_WORKDIR_MAX_LEN = 40 # Left-truncate workdir past this width # ========================= # --- Encoding setup (required for emoji / Unicode output) --- @@ -274,8 +277,52 @@ if ($gitRepo) { $locationName = Split-Path $cwd -Leaf } +# Workdir suffix: optional path fragment rendered after location_name. +# Disambiguates multiple git worktrees of the same repo (which share repo_name). +$workdirSuffix = "" +if ($CFG_SHOW_WORKDIR -and $cwd) { + $rawDir = $cwd + $wdBase = Split-Path $rawDir -Leaf + + # ~-relative form — match bash behavior, normalize to forward slashes so + # the $HOME prefix match is insensitive to slash direction. + $homeDir = if ($env:USERPROFILE) { $env:USERPROFILE } else { $HOME } + $rawFwd = $rawDir -replace '\\', '/' + $homeFwd = if ($homeDir) { $homeDir -replace '\\', '/' } else { '' } + $relDir = $rawFwd + if ($homeFwd -and $rawFwd.StartsWith($homeFwd, [StringComparison]::OrdinalIgnoreCase)) { + $relDir = '~' + $rawFwd.Substring($homeFwd.Length) + } + + switch ($CFG_WORKDIR_STYLE) { + "full" { $workdirSuffix = ($rawDir -replace '\\', '/') } + "relative" { $workdirSuffix = $relDir } + "basename" { $workdirSuffix = $wdBase } + default { + # "worktree": show basename only when it differs from the location name + if ($locationName -and $wdBase -and $wdBase -ne $locationName) { + $workdirSuffix = $wdBase + } + } + } + + # Left-truncate (preserve worktree-specific tail) if longer than max + if ($workdirSuffix -and $workdirSuffix.Length -gt $CFG_WORKDIR_MAX_LEN) { + $keep = [Math]::Max(1, $CFG_WORKDIR_MAX_LEN - 1) + $workdirSuffix = [char]0x2026 + $workdirSuffix.Substring($workdirSuffix.Length - $keep) + } +} + $line1Parts = @() -if ($locationName) { $line1Parts += "${BOLD}${locationName}${RESET}" } +if ($locationName) { + $loc = "${BOLD}${locationName}${RESET}" + if ($workdirSuffix) { + $loc += "${DIM} $([char]0x25B8) ${workdirSuffix}${RESET}" + } + $line1Parts += $loc +} elseif ($workdirSuffix) { + $line1Parts += "${DIM}${workdirSuffix}${RESET}" +} $line1Parts += "${BOLD}${CYAN}[$model]${RESET}" if ($gitStr) { $line1Parts += $gitStr } $line1Parts += "${barColor}[${bar}]${RESET} ${barColor}${pct}%${RESET}/${ctxLabel}" diff --git a/statusline.sh b/statusline.sh index ca5ed22..d80e867 100644 --- a/statusline.sh +++ b/statusline.sh @@ -18,14 +18,17 @@ CFG_SHOW_TOKENS=true CFG_SHOW_COST=true CFG_SHOW_LINES=true CFG_SHOW_SESSION=true +CFG_SHOW_WORKDIR=true CFG_WEATHER_UNIT="C" # "C" for Celsius, "F" for Fahrenheit CFG_CACHE_GIT_SEC=5 CFG_CACHE_WEATHER_SEC=1800 # 30 minutes CFG_PREFIX=" . " CFG_SEPARATOR=" | " CFG_BAR_WIDTH=15 -CFG_ACCENT_COLOR="" # Hex color for accent line. Empty = rainbow gradient. -CFG_STYLE="banner" # "banner" (v2) or "classic" (v1 look) +CFG_ACCENT_COLOR="" # Hex color for accent line. Empty = rainbow gradient. +CFG_STYLE="banner" # "banner" (v2) or "classic" (v1 look) +CFG_WORKDIR_STYLE="worktree" # "full", "relative", "basename", "worktree" +CFG_WORKDIR_MAX_LEN=40 # Left-truncate workdir past this width # ========================= # --------------------------------------------------------------------------- @@ -365,6 +368,42 @@ if [[ -z "$location_name" && -n "$display_dir" ]]; then location_name="$(basename "$display_dir" 2>/dev/null)" fi +# --------------------------------------------------------------------------- +# Workdir suffix: optional path fragment rendered after location_name on line 1. +# Disambiguates multiple git worktrees of the same repo (which share repo_name). +# --------------------------------------------------------------------------- +workdir_suffix="" +if [[ "$CFG_SHOW_WORKDIR" == true && -n "$display_dir" ]]; then + # Resolve raw absolute path (display_dir has already been ~-shortened) + raw_dir="${work_dir:-${project_dir:-$cwd}}" + wd_base="$(basename "$raw_dir" 2>/dev/null)" + + case "$CFG_WORKDIR_STYLE" in + full) + workdir_suffix="$raw_dir" + ;; + relative) + workdir_suffix="$display_dir" + ;; + basename) + workdir_suffix="$wd_base" + ;; + worktree|*) + # Show basename only when it differs from repo name + if [[ -n "$location_name" && "$wd_base" != "$location_name" ]]; then + workdir_suffix="$wd_base" + fi + ;; + esac + + # Left-truncate (preserve worktree-specific tail) if longer than max + if [[ -n "$workdir_suffix" ]] && (( ${#workdir_suffix} > CFG_WORKDIR_MAX_LEN )); then + keep=$(( CFG_WORKDIR_MAX_LEN - 1 )) + (( keep < 1 )) && keep=1 + workdir_suffix="…${workdir_suffix: -$keep}" + fi +fi + # --------------------------------------------------------------------------- # LINE 1: Folder/Repo | Model | Path | Session | Agent/Worktree # --------------------------------------------------------------------------- @@ -375,6 +414,15 @@ if [[ -n "$location_name" ]]; then line1="${C_BOLD}${location_name}${C_RESET}" fi +# Workdir suffix (dim, separator glyph between repo and path) +if [[ -n "$workdir_suffix" ]]; then + if [[ -n "$line1" ]]; then + line1+="${C_DIM} ▸ ${workdir_suffix}${C_RESET}" + else + line1+="${C_DIM}${workdir_suffix}${C_RESET}" + fi +fi + # Model name in cyan if [[ -n "$display_model" ]]; then [[ -n "$line1" ]] && line1+=" " diff --git a/test/test-workdir.sh b/test/test-workdir.sh new file mode 100644 index 0000000..237f91e --- /dev/null +++ b/test/test-workdir.sh @@ -0,0 +1,132 @@ +#!/usr/bin/env bash +# Tests for CFG_SHOW_WORKDIR / CFG_WORKDIR_STYLE / CFG_WORKDIR_MAX_LEN. +set -euo pipefail +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +source "$SCRIPT_DIR/helpers.sh" + +REPO_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" +export COLUMNS=120 + +# Build a temp script with git/weather disabled and specific workdir config. +# Also disable mission / cost / session noise so assertions are narrow. +make_script() { + local style="${1:-worktree}" show="${2:-true}" maxlen="${3:-40}" + local tmp + tmp=$(mktemp) + sed -e 's/CFG_SHOW_WEATHER=true/CFG_SHOW_WEATHER=false/' \ + -e 's/CFG_SHOW_GIT=true/CFG_SHOW_GIT=false/' \ + -e "s/CFG_SHOW_WORKDIR=true/CFG_SHOW_WORKDIR=$show/" \ + -e "s/CFG_WORKDIR_STYLE=\"worktree\"/CFG_WORKDIR_STYLE=\"$style\"/" \ + -e "s/CFG_WORKDIR_MAX_LEN=40/CFG_WORKDIR_MAX_LEN=$maxlen/" \ + "$REPO_DIR/statusline.sh" > "$tmp" + echo "$tmp" +} + +mk_json() { + local dir="$1" + cat </dev/null) + rm -f "$script" + assert_contains "full style shows absolute path" "$output" "/home/user/projects/my-app" +} + +test_relative_style() { + # display_dir replaces $HOME with ~ + local script output d + d="$HOME/myproj-x" + script=$(make_script relative) + output=$(bash "$script" < <(mk_json "$d") 2>/dev/null) + rm -f "$script" + assert_contains "relative style shows ~-prefixed path" "$output" "~/myproj-x" +} + +test_basename_style() { + local script output + script=$(make_script basename) + output=$(bash "$script" < <(mk_json '/home/user/projects/my-app') 2>/dev/null) + rm -f "$script" + assert_contains "basename style shows only cwd leaf" "$output" "my-app" + assert_not_contains "basename style hides parent dirs" "$output" "projects" +} + +test_worktree_hides_when_match() { + # Git off → location_name = cwd basename → equals workdir basename → + # worktree style renders nothing, so no separator glyph should appear. + local script output + script=$(make_script worktree) + output=$(bash "$script" < <(mk_json '/home/user/projects/my-app') 2>/dev/null) + rm -f "$script" + assert_not_contains "worktree hides when basename matches location_name" "$output" "▸" +} + +test_worktree_shows_in_subdir_of_repo() { + # Real repo, cwd is a SUBDIRECTORY. Git is enabled so location_name comes + # from the repo toplevel basename, while cwd basename is the subdir name. + # The two differ — worktree style should surface the subdir. + local base repo sub script output + base=$(mktemp -d) + repo="$base/my-repo" + mkdir -p "$repo/src/api" + git -C "$repo" init -q + git -C "$repo" config user.email t@t >/dev/null + git -C "$repo" config user.name t >/dev/null + echo x > "$repo/x" + git -C "$repo" add x >/dev/null + git -C "$repo" commit -qm init + sub="$repo/src/api" + + # Build a script with git ENABLED (so location_name is "my-repo") + script=$(mktemp) + sed -e 's/CFG_SHOW_WEATHER=true/CFG_SHOW_WEATHER=false/' \ + -e 's/CFG_SHOW_COST=true/CFG_SHOW_COST=false/' \ + "$REPO_DIR/statusline.sh" > "$script" + output=$(bash "$script" < <(mk_json "$sub") 2>/dev/null) + rm -f "$script" + rm -rf "$base" + + assert_contains "worktree style surfaces subdir when basename differs" "$output" "▸" + assert_contains "worktree style shows the subdir leaf" "$output" "api" +} + +test_disabled_shows_nothing() { + local script output + script=$(make_script worktree false) + output=$(bash "$script" < <(mk_json '/home/user/projects/differs-basename') 2>/dev/null) + rm -f "$script" + assert_not_contains "disabled flag emits no glyph" "$output" "▸" +} + +test_long_path_left_truncated() { + local script output longpath + script=$(make_script full 'true' 20) + longpath='/home/user/very/deeply/nested/directory/structure/myproj-leaf' + output=$(bash "$script" < <(mk_json "$longpath") 2>/dev/null) + rm -f "$script" + assert_contains "long path starts with ellipsis" "$output" "…" + assert_contains "long path preserves tail leaf" "$output" "myproj-leaf" + assert_not_contains "long path drops head segments" "$output" "/home/user/very/deeply/nested" +} + +test_full_style +test_relative_style +test_basename_style +test_worktree_hides_when_match +test_worktree_shows_in_subdir_of_repo +test_disabled_shows_nothing +test_long_path_left_truncated + +print_summary