|
#!/bin/bash |
|
# Claude Code status line (two lines). |
|
# TOP (env/aux): 🕐 clock · 📁 dir · git · env-flag · ☁ az · 🧊 tf · 🐘 php · 🐳 docker · ⇄ mcp |
|
# BOT (session): [model] · 📌 note · ⚙ effort · 🤖 subagents · 🧠 ctx · +/- lines · ⏱ time · 💲 cost |
|
# |
|
# Requires: jq. Optional: git, gh, az, docker, php, python3 (python3 only for right-aligning the note). |
|
# Terminal hyperlinks (shift-click) use OSC8 — works in Ghostty, iTerm2, WezTerm, kitty, etc. |
|
|
|
input=$(cat) |
|
|
|
# ---- colors ---- |
|
RST=$'\033[0m'; DIM=$'\033[90m'; BOLD=$'\033[1m' |
|
YEL=$'\033[33m'; MAG=$'\033[35m'; GRN=$'\033[32m'; RED=$'\033[31m' |
|
BLU=$'\033[34m'; CYN=$'\033[36m' |
|
OSC=$'\033]8;;'; ST=$'\033\\' # OSC8 hyperlink: ${OSC}URL${ST}TEXT${OSC}${ST} |
|
# link <url> <text> -> emits a shift-clickable hyperlink (OSC8) |
|
link() { printf '%s%s%s%s%s%s' "$OSC" "$1" "$ST" "$2" "$OSC" "$ST"; } |
|
|
|
# repo_web_url <dir> -> browsable https URL for the origin remote (GitHub or Azure DevOps), else "" |
|
repo_web_url() { |
|
local r; r=$(git -C "$1" remote get-url origin 2>/dev/null) || return |
|
case "$r" in |
|
git@github.com:*) r="https://github.com/${r#git@github.com:}"; echo "${r%.git}" ;; |
|
ssh://git@github.com/*) r="https://github.com/${r#ssh://git@github.com/}"; echo "${r%.git}" ;; |
|
git@github-*:*) r="https://github.com/${r#git@github-*:}"; echo "${r%.git}" ;; # ssh host-alias (2nd account) |
|
https://github.com/*) echo "${r%.git}" ;; |
|
# ADO ssh: git@ssh.dev.azure.com:v3/ORG/PROJECT/REPO -> https://dev.azure.com/ORG/PROJECT/_git/REPO |
|
git@ssh.dev.azure.com:v3/*) local p="${r#git@ssh.dev.azure.com:v3/}"; local o="${p%%/*}"; local rest="${p#*/}"; local pr="${rest%%/*}"; local rp="${rest#*/}"; echo "https://dev.azure.com/${o}/${pr}/_git/${rp}" ;; |
|
# ADO https: https://ORG@dev.azure.com/ORG/PROJECT/_git/REPO -> strip creds |
|
https://*@dev.azure.com/*) echo "https://dev.azure.com/${r#*@dev.azure.com/}" ;; |
|
https://dev.azure.com/*) echo "$r" ;; |
|
# legacy visualstudio.com (https already browsable; strip embedded creds if any) |
|
https://*@*.visualstudio.com/*) local h="${r#https://*@}"; echo "https://${h}" ;; |
|
https://*.visualstudio.com/*) echo "$r" ;; |
|
# legacy vs-ssh: USER@vs-ssh.visualstudio.com:v3/ORG/PROJECT/REPO |
|
*@vs-ssh.visualstudio.com:v3/*) local p="${r#*vs-ssh.visualstudio.com:v3/}"; local o="${p%%/*}"; local rest="${p#*/}"; local pr="${rest%%/*}"; local rp="${rest#*/}"; echo "https://${o}.visualstudio.com/${pr}/_git/${rp}" ;; |
|
*) echo "" ;; |
|
esac |
|
} |
|
|
|
# pr_info <dir> <branch> <weburl> -> "icon<TAB>#number<TAB>prurl" for current branch's PR, else "" |
|
# cached 90s per repo+branch (network calls). Icon by state: 🟢 open · 📝 draft · 🟣 merged · 🔴 closed |
|
pr_info() { |
|
local d="$1" br="$2" web="$3" |
|
[ -n "$br" ] || return |
|
local key; key=$(echo "${d}@${br}" | tr -c 'A-Za-z0-9' '_') |
|
local pf="$cache/pr_${key}" |
|
if [ -z "$(find "$pf" -mtime -90s 2>/dev/null)" ]; then |
|
local line="" |
|
case "$web" in |
|
*github.com*) |
|
line=$( cd "$d" 2>/dev/null && gh pr status --json number,state,isDraft,url -q \ |
|
'.currentBranch | select(.number) | "\(.number)\t\(.state)\t\(.isDraft)\t\(.url)"' 2>/dev/null) ;; |
|
*dev.azure.com*|*visualstudio.com*) |
|
# web = https://ORG.visualstudio.com/PROJECT/_git/REPO OR https://dev.azure.com/ORG/PROJECT/_git/REPO |
|
local org proj repo |
|
repo="${web##*/_git/}"; proj="${web%/_git/*}"; proj="${proj##*/}" |
|
case "$web" in |
|
*dev.azure.com*) org="https://dev.azure.com/$(echo "${web#https://dev.azure.com/}" | cut -d/ -f1)" ;; |
|
*) org="$(echo "$web" | sed -E 's#(https://[^/]+).*#\1#')" ;; |
|
esac |
|
line=$(az repos pr list --org "$org" --project "$proj" --repository "$repo" \ |
|
--source-branch "$br" --status all --top 1 \ |
|
--query "[0].{n:pullRequestId,s:status,d:isDraft}" -o tsv 2>/dev/null \ |
|
| awk -F'\t' 'NF{print $1"\t"$2"\t"$3"\t""'"${web}"'/pullrequest/"$1}') ;; |
|
esac |
|
printf '%s' "$line" >"$pf" |
|
fi |
|
local n st draft url; IFS=$'\t' read -r n st draft url <"$pf" 2>/dev/null |
|
[ -n "$n" ] || return |
|
local ic |
|
case "$st" in |
|
OPEN|active) ic="🟢" ;; |
|
MERGED|completed) ic="🟣" ;; |
|
CLOSED|abandoned) ic="🔴" ;; |
|
*) ic="🔵" ;; |
|
esac |
|
case "$draft" in true|True) ic="📝" ;; esac |
|
printf '%s\t#%s\t%s' "$ic" "$n" "$url" |
|
} |
|
|
|
# ---- pull fields from JSON (single jq pass; \x1f-joined so empty fields don't collapse) ---- |
|
IFS=$'\x1f' read -r dir model tpath dur_ms api_ms added removed cost sid cols ctxtok ctxpct < <( |
|
echo "$input" | jq -j '[ |
|
.workspace.current_dir // .cwd // "", |
|
.model.display_name // "?", |
|
.transcript_path // "", |
|
(.cost.total_duration_ms // 0 | tostring), |
|
(.cost.total_api_duration_ms // 0 | tostring), |
|
(.cost.total_lines_added // 0 | tostring), |
|
(.cost.total_lines_removed // 0 | tostring), |
|
(.cost.total_cost_usd // 0 | tostring), |
|
(.session_id // ""), |
|
((.width // .cols // .columns // .terminal.width // .terminal.cols // 0) | tostring), |
|
((.context_window.total_input_tokens // 0) | tostring), |
|
((.context_window.used_percentage // 0) | tostring) |
|
] | join("")' |
|
) |
|
|
|
cache="$HOME/.claude/.cache"; mkdir -p "$cache" |
|
top="" # session info |
|
bot="" # environment / aux |
|
|
|
# ===================================================================== |
|
# TOP LINE (rendered second) — session |
|
# ===================================================================== |
|
|
|
# ---- [model] (icon by family) ---- |
|
# CUSTOMIZE: model icons. Add cases for other models you use. |
|
case "$model" in |
|
*Opus*|*opus*) micon="🔮" ;; |
|
*Sonnet*|*sonnet*) micon="✍️" ;; |
|
*Haiku*|*haiku*) micon="🍃" ;; |
|
*) micon="🤖" ;; |
|
esac |
|
top+=" ${DIM}${micon} [${model}]${RST}" |
|
|
|
# ---- 📌 model/agent-driven note (per-session scratch file) ---- |
|
# Format: "<expiry_epoch> <text>" OR "<expiry_epoch> <url> <text>" (url makes 📌 shift-clickable) |
|
# echo "0 ⏳ deploying" > ~/.claude/.cache/statusline_note_$CLAUDE_CODE_SESSION_ID |
|
# echo "0 https://example.com/run/123 🚀 build" > ...same file |
|
# expiry 0 = persist until changed/cleared; future epoch = auto-hide + self-delete. |
|
note="" |
|
notefile="$cache/statusline_note_$sid" # STRICT per-session: no global fallback (avoids cross-session bleed) |
|
if [ -n "$sid" ] && [ -f "$notefile" ]; then |
|
read -r nexp nrest < "$notefile" 2>/dev/null |
|
now=$(date +%s) |
|
if [ -n "$nrest" ] && { [ -z "$nexp" ] || [ "$nexp" = "0" ] || { [ "$nexp" -gt "$now" ] 2>/dev/null; }; }; then |
|
nurl=""; ntext="$nrest" |
|
case "$nrest" in |
|
http://*|https://*) nurl="${nrest%% *}"; ntext="${nrest#* }" ;; |
|
esac |
|
if [ -n "$nurl" ] && [ "$nurl" != "$ntext" ]; then |
|
note="${BOLD}${YEL}$(link "$nurl" "📌 ${ntext}")${RST}" |
|
else |
|
note="${BOLD}${YEL}📌 ${ntext}${RST}" |
|
fi |
|
else |
|
rm -f "$notefile" # expired -> clean up |
|
fi |
|
fi |
|
|
|
# ---- ⚙ effort level (from status JSON, else settings.json) ---- |
|
eff=$(echo "$input" | jq -r '.effort.level // .effortLevel // empty' 2>/dev/null) |
|
[ -z "$eff" ] && eff=$(jq -r '.effortLevel // empty' "$HOME/.claude/settings.json" 2>/dev/null) |
|
if [ -n "$eff" ]; then |
|
case "$eff" in |
|
high|max) ec="$RED" ;; medium) ec="$YEL" ;; *) ec="$GRN" ;; |
|
esac |
|
top+=" ${ec}⚙ ${eff}${RST}" |
|
fi |
|
|
|
# ---- 🤖 subagents spawned this session (count of agent-*.jsonl; instant) ---- |
|
# Subagents run in their own context (NOT counted in 🧠) but DO bill to 💲 cost. |
|
if [ -n "$tpath" ]; then |
|
sadir="${tpath%.jsonl}/subagents" |
|
if [ -d "$sadir" ]; then |
|
nsa=$(ls "$sadir"/agent-*.jsonl 2>/dev/null | grep -c .) |
|
[ "$nsa" -gt 0 ] 2>/dev/null && top+=" ${MAG}🤖 ${nsa}${RST}" |
|
fi |
|
fi |
|
|
|
# CUSTOMIZE: context % thresholds (60/85) and color live in the block just below. |
|
# ---- 🧠 context window usage (prefer JSON context_window; fall back to transcript) ---- |
|
ctx=0 |
|
[ -n "$ctxtok" ] && [ "$ctxtok" -gt 0 ] 2>/dev/null && ctx="$ctxtok" |
|
if [ "$ctx" = "0" ] && [ -n "$tpath" ] && [ -f "$tpath" ]; then |
|
ctx=$(grep '"usage"' "$tpath" 2>/dev/null | tail -1 | jq -r ' |
|
(.message.usage // {}) | |
|
((.input_tokens // 0) + (.cache_read_input_tokens // 0) + (.cache_creation_input_tokens // 0)) |
|
' 2>/dev/null) |
|
fi |
|
if [ -n "$ctx" ] && [ "$ctx" -gt 0 ] 2>/dev/null; then |
|
k=$((ctx / 1000)) |
|
# color by exact % when provided, else by token thresholds (scaled up for 1M-context models) |
|
if [ -n "$ctxpct" ] && [ "$ctxpct" -gt 0 ] 2>/dev/null; then |
|
if [ "$ctxpct" -ge 85 ]; then c="$RED" |
|
elif [ "$ctxpct" -ge 60 ]; then c="$YEL" |
|
else c="$GRN"; fi |
|
top+=" ${c}🧠 ${k}k ${ctxpct}%${RST}" |
|
else |
|
case "$model" in *1[Mm]*|*"[1m]"*) hi=800; mid=500 ;; *) hi=160; mid=120 ;; esac |
|
if [ "$k" -ge "$hi" ]; then c="$RED" |
|
elif [ "$k" -ge "$mid" ]; then c="$YEL" |
|
else c="$GRN"; fi |
|
top+=" ${c}🧠 ${k}k${RST}" |
|
fi |
|
fi |
|
|
|
# fmt_dur <ms> -> "1d 4h" / "3h12m" / "48m" / "30s" |
|
fmt_dur() { |
|
local s=$(( $1 / 1000 )) dd hh mm |
|
dd=$(( s / 86400 )); hh=$(( (s % 86400) / 3600 )); mm=$(( (s % 3600) / 60 )) |
|
if [ "$dd" -gt 0 ]; then printf '%dd %dh' "$dd" "$hh" |
|
elif [ "$hh" -gt 0 ]; then printf '%dh%dm' "$hh" "$mm" |
|
elif [ "$mm" -gt 0 ]; then printf '%dm' "$mm" |
|
else printf '%ds' "$s"; fi |
|
} |
|
|
|
# ---- +added / -removed lines ---- |
|
if [ "$added" -gt 0 ] 2>/dev/null || [ "$removed" -gt 0 ] 2>/dev/null; then |
|
top+=" ${GRN}+${added}${RST}${DIM}/${RST}${RED}-${removed}${RST}" |
|
fi |
|
|
|
# ---- ⏱ duration (W=wall, A=api/active) and 💲 cost, grouped at end of session line ---- |
|
if [ "$dur_ms" -gt 0 ] 2>/dev/null; then |
|
top+=" ${DIM}⏱W $(fmt_dur "$dur_ms")${RST}" |
|
[ "$api_ms" -gt 0 ] 2>/dev/null && top+=" ${DIM}⚡A $(fmt_dur "$api_ms")${RST}" |
|
fi |
|
if [ "$(echo "$cost > 0" | bc -l 2>/dev/null)" = "1" ]; then |
|
top+=" ${DIM}💲$(printf '%.2f' "$cost")${RST}" |
|
fi |
|
|
|
# ===================================================================== |
|
# BOTTOM LINE (rendered first) — environment / aux |
|
# ===================================================================== |
|
|
|
# ---- 🕐 clock: 12h A/P, three zones slashed PST/CST/EST, times only ---- |
|
# CUSTOMIZE: change/add/remove timezones here (any tz database name). One time per zone. |
|
clk="" |
|
for tz in America/Los_Angeles America/Chicago America/New_York; do |
|
t=$(TZ="$tz" date '+%-I:%M%p') # e.g. 11:05PM |
|
ap="${t: -2}"; t="${t%??}" # ap=PM, t=11:05 |
|
clk+="${t}${ap:0:1}/" # 11:05P/ |
|
done |
|
bot+=" ${DIM}🕐 ${clk%/}${RST}" |
|
|
|
# ---- 📁 directory: smart icon (name family > language > host > location); full leaf ---- |
|
# shift-click opens the repo's web page (GitHub/ADO), or the folder in Finder if not a repo. |
|
# CUSTOMIZE the name-family cases below for your own repo collection. |
|
leaf="${dir##*/}" |
|
dweb=$(repo_web_url "$dir") |
|
case "$dir" in |
|
"$HOME") dicon="🏠"; leaf="~" ;; |
|
"$HOME"/Desktop/Github/*) |
|
# inside the repo collection — pick by name family, then language, then host |
|
case "$leaf" in |
|
Work.*|*-work) dicon="💼" ;; # e.g. work / org repos |
|
platform-*) dicon="🏗️" ;; # e.g. a platform family |
|
*mcp*|*MCP*) dicon="🔌" ;; # MCP servers |
|
*deploy*|*-iac|*iac-*) dicon="🧱" ;; # deploy / IaC |
|
*.com) dicon="🌐" ;; # personal site |
|
*) |
|
if [ -f "$dir/artisan" ]; then dicon="🐘" # Laravel |
|
elif [ -f "$dir/go.mod" ]; then dicon="🐹" # Go |
|
elif [ -f "$dir/package.json" ]; then dicon="📦" # Node |
|
elif [ -n "$dweb" ]; then |
|
case "$dweb" in *dev.azure.com*|*visualstudio.com*) dicon="🔷" ;; *) dicon="🐙" ;; esac |
|
else dicon="📂"; fi ;; |
|
esac ;; |
|
"$HOME"/Desktop/*) dicon="🖥️" ;; |
|
"$HOME"/*) dicon="🏠" ;; |
|
*) dicon="📁" ;; |
|
esac |
|
dtarget="${dweb:-file://$dir}" |
|
bot+=" ${YEL}${dicon} $(link "$dtarget" "$leaf")${RST}" |
|
|
|
# ---- git: <icon> branch ●dirty ↑ahead ↓behind ---- |
|
if branch=$(git -C "$dir" symbolic-ref --short HEAD 2>/dev/null); then |
|
# type icon REPLACES the text prefix |
|
short_branch="$branch" |
|
case "$branch" in |
|
main|master) bicon="🌳" ;; |
|
feature/*) bicon="✨"; short_branch="${branch#feature/}" ;; # CUSTOMIZE: branch-type icons |
|
bugfix/*) bicon="🐛"; short_branch="${branch#bugfix/}" ;; |
|
hotfix/*) bicon="🔥"; short_branch="${branch#hotfix/}" ;; |
|
release/*) bicon="🚀"; short_branch="${branch#release/}" ;; |
|
chore/*) bicon="🧹"; short_branch="${branch#chore/}" ;; |
|
*) bicon="🌿" ;; |
|
esac |
|
# drop a leading "first.last/" user-name segment -> keeps the meaningful tail |
|
short_branch=$(echo "$short_branch" | sed -E 's#^[a-z]+\.[a-z]+/##') |
|
# middle-truncate if too long (keeps BOTH ends identifiable) |
|
if [ "${#short_branch}" -gt 30 ]; then |
|
short_branch="${short_branch:0:14}…${short_branch: -14}" |
|
fi |
|
# branch shift-clickable -> branch web page (GitHub /tree/, ADO ?version=GB) |
|
if [ -n "$dweb" ]; then |
|
case "$dweb" in |
|
*dev.azure.com*|*visualstudio.com*) burl="${dweb}?version=GB${branch}" ;; |
|
*) burl="${dweb}/tree/${branch}" ;; |
|
esac |
|
blabel=$(link "$burl" "$short_branch") |
|
else |
|
blabel="$short_branch" |
|
fi |
|
git_str="${bicon} ${blabel}" |
|
dirty=$(git -C "$dir" status --porcelain --no-optional-locks 2>/dev/null | grep -c .) |
|
[ "$dirty" -gt 0 ] && git_str+=" ●${dirty}" |
|
upstream=$(git -C "$dir" rev-list --left-right --count @{u}...HEAD 2>/dev/null) |
|
if [ -n "$upstream" ]; then |
|
behind=$(echo "$upstream" | cut -f1); ahead=$(echo "$upstream" | cut -f2) |
|
[ "$ahead" -gt 0 ] 2>/dev/null && git_str+=" ↑${ahead}" |
|
[ "$behind" -gt 0 ] 2>/dev/null && git_str+=" ↓${behind}" |
|
fi |
|
bot+=" ${MAG}(${git_str})${RST}" |
|
elif sha=$(git -C "$dir" rev-parse --short HEAD 2>/dev/null); then |
|
bot+=" ${MAG}(⚠️ ${sha})${RST}" # detached HEAD |
|
fi |
|
|
|
# ---- PR for current branch (GitHub + Azure DevOps, cached); shift-click -> PR page ---- |
|
if [ -n "$branch" ] && [ -n "$dweb" ]; then |
|
IFS=$'\t' read -r pric prnum prurl < <(pr_info "$dir" "$branch" "$dweb") |
|
if [ -n "$prnum" ]; then |
|
if [ -n "$prurl" ]; then |
|
bot+=" ${CYN}${pric} $(link "$prurl" "PR ${prnum}")${RST}" |
|
else |
|
bot+=" ${CYN}${pric} PR ${prnum}${RST}" |
|
fi |
|
fi |
|
fi |
|
|
|
# ---- ☁ azure subscription (cached 45s) + env flag derived from its name ---- |
|
# name shift-clickable -> Portal subscription overview. Requires the `az` CLI + a login. |
|
subfile="$cache/az_sub" |
|
if [ -z "$(find "$subfile" -mtime -45s 2>/dev/null)" ]; then |
|
az account show --query '[name, id, tenantId]' -o tsv 2>/dev/null | tr '\n' '\t' >"$subfile" || : >"$subfile" |
|
fi |
|
IFS=$'\t' read -r sub subid subtenant _ < "$subfile" 2>/dev/null |
|
if [ -n "$sub" ]; then |
|
lc=$(echo "$sub" | tr '[:upper:]' '[:lower:]') |
|
# CUSTOMIZE: env detection — match your subscription naming to color prod red vs dev green. |
|
case "$lc" in |
|
*prod*|*prd*) bot+=" ${BOLD}${RED}🔴 PROD${RST}" ;; |
|
*dev*|*test*|*sandbox*|*nonprod*) bot+=" ${GRN}🟢 dev${RST}" ;; |
|
*) bot+=" ${YEL}🟡 ?${RST}" ;; |
|
esac |
|
if [ -n "$subid" ] && [ -n "$subtenant" ]; then |
|
suburl="https://portal.azure.com/#@${subtenant}/resource/subscriptions/${subid}/overview" |
|
bot+=" ${BLU}☁ $(link "$suburl" "$sub")${RST}" |
|
else |
|
bot+=" ${BLU}☁ ${sub}${RST}" |
|
fi |
|
fi |
|
|
|
# ---- 🧊 terraform workspace (instant: read .terraform/environment) ---- |
|
if [ -f "$dir/.terraform/environment" ]; then |
|
ws=$(cat "$dir/.terraform/environment" 2>/dev/null) |
|
[ -n "$ws" ] && bot+=" ${CYN}🧊 ${ws}${RST}" |
|
fi |
|
|
|
# ---- 🐘 php version (only inside a PHP/Laravel project; cached once) ---- |
|
if [ -f "$dir/composer.json" ] || [ -f "$dir/artisan" ] || ls "$dir"/*.php >/dev/null 2>&1; then |
|
pvfile="$cache/php_ver" |
|
[ -f "$pvfile" ] || php -r 'echo PHP_MAJOR_VERSION.".".PHP_MINOR_VERSION;' >"$pvfile" 2>/dev/null |
|
pv=$(cat "$pvfile" 2>/dev/null) |
|
[ -n "$pv" ] && bot+=" ${MAG}🐘 ${pv}${RST}" |
|
fi |
|
|
|
# ---- 🐳 docker running container count (cached 45s) ---- |
|
dfile="$cache/docker_n" |
|
if [ -z "$(find "$dfile" -mtime -45s 2>/dev/null)" ]; then |
|
docker ps -q 2>/dev/null | grep -c . >"$dfile" 2>/dev/null || echo 0 >"$dfile" |
|
fi |
|
dn=$(cat "$dfile" 2>/dev/null) |
|
[ -n "$dn" ] && [ "$dn" -gt 0 ] 2>/dev/null && bot+=" ${BLU}🐳 ${dn}${RST}" |
|
|
|
# ---- ⇄ MCP server count (global + this project, from ~/.claude.json) ---- |
|
cfg="$HOME/.claude.json" |
|
if [ -f "$cfg" ]; then |
|
mcpn=$(jq --arg d "$dir" -r ' |
|
((.mcpServers // {}) | length) |
|
+ (((.projects[$d].mcpServers) // {}) | length) |
|
' "$cfg" 2>/dev/null) |
|
[ -n "$mcpn" ] && [ "$mcpn" -gt 0 ] 2>/dev/null && bot+=" ${CYN}⇄ ${mcpn}${RST}" |
|
fi |
|
|
|
# ---- emit: environment line on top, session line below ---- |
|
# The note (📌) renders right-aligned at the END of the session line when width is known. |
|
sline="${top# }" |
|
if [ -n "$note" ]; then |
|
{ [ -z "$cols" ] || [ "$cols" -lt 20 ] 2>/dev/null; } && cols="$COLUMNS" # Claude Code sets COLUMNS for the script |
|
if [ -n "$cols" ] && [ "$cols" -gt 20 ] 2>/dev/null && command -v python3 >/dev/null 2>&1; then |
|
pad=$(SLINE="$sline" NOTE="$note" COLS="$cols" python3 - <<'PY' |
|
import os,re,unicodedata |
|
def w(s): |
|
s=re.sub(r'\x1b\[[0-9;]*m','',s) # strip SGR color |
|
s=re.sub(r'\x1b\]8;;.*?\x1b\\','',s) # strip OSC8 hyperlink wrappers |
|
n=0 |
|
for ch in s: |
|
if unicodedata.combining(ch): continue |
|
ea=unicodedata.east_asian_width(ch) |
|
n += 2 if ea in ('W','F') or ord(ch) >= 0x1F000 else 1 |
|
return n |
|
cols=int(os.environ['COLS']); sl=w(os.environ['SLINE']); nt=w(os.environ['NOTE']) |
|
gap=cols - sl - nt |
|
print(gap if gap > 1 else 1) |
|
PY |
|
) |
|
printf '%s\n%s%*s%s' "${bot# }" "$sline" "$pad" "" "$note" |
|
else |
|
printf '%s\n%s %s' "${bot# }" "$sline" "$note" |
|
fi |
|
else |
|
printf '%s\n%s' "${bot# }" "$sline" |
|
fi |