Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
25 changes: 25 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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

Expand Down
53 changes: 50 additions & 3 deletions statusline.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -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) ---
Expand Down Expand Up @@ -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}"
Expand Down
52 changes: 50 additions & 2 deletions statusline.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
# =========================

# ---------------------------------------------------------------------------
Expand Down Expand Up @@ -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
# ---------------------------------------------------------------------------
Expand All @@ -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+=" "
Expand Down
132 changes: 132 additions & 0 deletions test/test-workdir.sh
Original file line number Diff line number Diff line change
@@ -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 <<JSON
{
"model": {"display_name": "Opus", "id": "claude-opus-4-6"},
"cwd": "$dir",
"workspace": {"current_dir": "$dir", "project_dir": "$dir"},
"context_window": {"used_percentage": 10, "context_window_size": 200000, "current_usage": {"input_tokens": 100, "output_tokens": 10, "cache_read_input_tokens": 0}},
"cost": {"total_cost_usd": 0, "total_duration_ms": 1000, "total_lines_added": 0, "total_lines_removed": 0},
"session_id": "abcd1234"
}
JSON
}

test_full_style() {
local script output
script=$(make_script full)
output=$(bash "$script" < <(mk_json '/home/user/projects/my-app') 2>/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
Loading