From 24ef7f4ce75b5f09198f2d012d1bb42684568d13 Mon Sep 17 00:00:00 2001 From: Jeffrey Leon Date: Wed, 1 Jul 2026 20:34:19 -0400 Subject: [PATCH 1/8] feat(modules)!: directional + zone-aware auto notch --- tests/application_module.sh | 6 ++-- utils/layout.sh | 60 +++++++++++++++++++++++++++++-------- utils/module_render.sh | 60 ++++++++++++++++++++++++++++++------- utils/render_style.sh | 9 ++++++ 4 files changed, 110 insertions(+), 25 deletions(-) diff --git a/tests/application_module.sh b/tests/application_module.sh index 69b588f..78c31bd 100644 --- a/tests/application_module.sh +++ b/tests/application_module.sh @@ -15,9 +15,11 @@ print_option @themux_module_application | grep -q "@thm_" && print_option @themux_module_application | sed -E 's/(bash|fish|zsh)//' # Notch: the icon<->text seam takes the shape's right cap instead of the plain -# middle separator (the module color tapering into the text background). +# middle separator (the module color tapering into the text background). ">" is +# explicit (not zone-aware), so it bakes directly here without utils/layout.sh +# splicing in the seam — "auto"/"on" cannot resolve outside a real status line. tmux set -g @themux_module_shape "rounded" -tmux set -g @themux_module_notch "on" +tmux set -g @themux_module_notch ">" tmux source "${script_dir}/../themux_options.conf" tmux source "${script_dir}/../themux.conf" print_option @themux_module_application | sed -E 's/(bash|fish|zsh)//' diff --git a/utils/layout.sh b/utils/layout.sh index 7ebdc20..631bb94 100755 --- a/utils/layout.sh +++ b/utils/layout.sh @@ -40,6 +40,14 @@ interp() { # $1 module, $2 string -> the string with the module's plugin literal windows_block=$(tmux show -gqv @_tmx_fmt_windows) +# Control-byte marker for a zone-aware notch (auto), matching utils/module_render.sh +# (both hardcode the same literal byte — they run as separate processes, so there is +# no shared variable to pass it through). mmid is the centre/off replacement: a +# module's own middle separator, resolved once here since it is a flat option (not +# per-module cascaded). +NOTCH_MARK=$(printf '\036') +mmid=$(tmux show -gqv @themux_module_middle_separator) + # Cap glyphs for the module shape. Empty only for unstyled; squared uses a full # block cap, so `=` and edge flush still work for square-cornered modules. mshape=$(tmux display -p "#{?#{@themux_module_shape},#{@themux_module_shape},#{@themux_all_shape}}") @@ -73,18 +81,45 @@ mod_field() { # $1 module, $2 edge option (lcol|rcol|lbg) -> a usable colour tmux show -gqv "@_tmx_module_${1}_${2}" fi } -mod_core() { # $1 module -> its bare core (a ref normally; inlined for tmux-cpu) +# Resolve the raw seam text for one module occurrence, by zone: left -> the +# module's baked gt seam, right -> its baked lt seam, centre -> the shared middle +# separator (auto's "off" fallback, same replacement notch=off itself bakes). +# Fetched RAW with `show -gqv` — NOT through mod_field: a seam carries no plugin +# #{var} literal, and mod_field's eager #{E:} for _expand modules would prematurely +# flatten a chan() draw-time conditional baked into the seam colour. Empty means +# the module baked its notch directly (gt/lt/off, no marker in the core) — the +# caller then skips the #{s///} splice entirely (safe no-op, but a needless one). +mod_notch_seam() { # $1 module, $2 align (left|centre|right) -> raw seam text or "" + case "$2" in + left) tmux show -gqv "@_tmx_module_${1}_seam_gt" ;; + right) tmux show -gqv "@_tmx_module_${1}_seam_lt" ;; + *) printf '%s' "$mmid" ;; + esac +} +mod_core() { # $1 module, $2 align -> its bare core (a ref normally; inlined for tmux-cpu) + local seam val + seam=$(mod_notch_seam "$1" "$2") if [ "$(tmux show -gqv "@themux_${1}_expand")" = "yes" ]; then tmux set -gF @_tmx_layout_tmp "#{E:@_tmx_module_${1}_core}" - interp "$1" "$(tmux show -gv @_tmx_layout_tmp)" + val=$(interp "$1" "$(tmux show -gv @_tmx_layout_tmp)") + [ -n "$seam" ] && val="${val//$NOTCH_MARK/$seam}" + printf '%s' "$val" + elif [ -n "$seam" ]; then + printf '%s' "#{s/${NOTCH_MARK}/${seam}/:#{E:@_tmx_module_${1}_core}}" else printf '%s' "#{E:@_tmx_module_${1}_core}" fi } -full_pill() { # $1 module -> its own pill, caps included (inlined for tmux-cpu) +full_pill() { # $1 module, $2 align -> its own pill, caps included (inlined for tmux-cpu) + local seam val + seam=$(mod_notch_seam "$1" "$2") if [ "$(tmux show -gqv "@themux_${1}_expand")" = "yes" ]; then tmux set -gF @_tmx_layout_tmp "#{E:@themux_module_${1}}" - interp "$1" "$(tmux show -gv @_tmx_layout_tmp)" + val=$(interp "$1" "$(tmux show -gv @_tmx_layout_tmp)") + [ -n "$seam" ] && val="${val//$NOTCH_MARK/$seam}" + printf '%s' "$val" + elif [ -n "$seam" ]; then + printf '%s' "#{s/${NOTCH_MARK}/${seam}/:#{E:@themux_module_${1}}}" else printf '%s' "#{E:@themux_module_${1}}" fi @@ -114,6 +149,7 @@ powerline_run() { read -ra mods <<<"$1" read -ra conns <<<"$2" local fl="${3:-0}" fr="${4:-0}" # flush the left/right outer cap to the edge + local align="${5:-left}" # this occurrence's zone, for the notch seam local i m c cond lcol lbg rcol rbg core head conn seg local out2="" prc="" prb="" # prev visible module's right colour / right bg @@ -123,7 +159,7 @@ powerline_run() { cond=$(tmux show -gqv "@themux_${m}_when") lcol=$(mod_field "$m" lcol); lbg=$(mod_field "$m" lbg) rcol=$(mod_field "$m" rcol); rbg=$(mod_field "$m" rbg) - core=$(mod_core "$m") + core=$(mod_core "$m" "$align") # flush_left drops the opening cap so the first block fills the terminal edge. if [ "$fl" = 1 ]; then head="${core}"; else head="#[fg=${lcol},bg=default]${mpll}${core}"; fi # The seam glyph follows this module's incoming connector token. @@ -230,8 +266,8 @@ expand_zone() { *) printf '1' ;; esac } - render_item() { # $1 item index, $2 flush-left, $3 flush-right - local idx="$1" fl2="$2" fr2="$3" wedge2 wfmt2 + render_item() { # $1 item index, $2 flush-left, $3 flush-right, $4 align (this zone) + local idx="$1" fl2="$2" fr2="$3" align2="$4" wedge2 wfmt2 local -a mm2 case "${it_type[idx]}" in W) @@ -248,9 +284,9 @@ expand_zone() { G) read -ra mm2 <<<"${it_mods[idx]}" if [ ${#mm2[@]} -eq 1 ] && [ "$fl2" = 0 ] && [ "$fr2" = 0 ]; then - full_pill "${mm2[0]}" + full_pill "${mm2[0]}" "$align2" else - powerline_run "${it_mods[idx]}" "${it_conns[idx]}" "$fl2" "$fr2" + powerline_run "${it_mods[idx]}" "${it_conns[idx]}" "$fl2" "$fr2" "$align2" fi ;; esac @@ -337,13 +373,13 @@ expand_zone() { [ "$right_join" = 1 ] && fr=1 if [ "$right_join" = 1 ] && [ "$next_vis" != 1 ]; then - drop_right=$(render_item "$i" "$fl" 1) - keep_right=$(render_item "$i" "$fl" 0) + drop_right=$(render_item "$i" "$fl" 1 "$align") + keep_right=$(render_item "$i" "$fl" 0 "$align") tmux set -g "@_tmx_item_drop_${i}" "$drop_right" tmux set -g "@_tmx_item_keep_${i}" "$keep_right" out+="#{?${next_vis},#{E:@_tmx_item_drop_${i}},#{E:@_tmx_item_keep_${i}}}" else - out+="$(render_item "$i" "$fl" "$fr")" + out+="$(render_item "$i" "$fl" "$fr" "$align")" fi case "${it_type[i]}" in diff --git a/utils/module_render.sh b/utils/module_render.sh index 2f7ffec..fe421a5 100755 --- a/utils/module_render.sh +++ b/utils/module_render.sh @@ -34,6 +34,12 @@ name="$1" . "$(dirname "$0")/render_style.sh" US=$(printf '\037') +# Control-byte placeholder for a zone-aware notch (auto): distinct from the US +# field delimiter above. A literal printable placeholder is unsafe — tmux applies +# strftime %-expansion to anything passing through #{E:}, so any %-bearing text +# would get mangled. utils/layout.sh splices the real seam in at draw time, once +# it knows which zone this occurrence sits in. +NOTCH_MARK=$(printf '\036') # A property cascade: @themux__ > @themux_module_ > # internal module default > @themux_all_. Empty-check (not truthy) so a @@ -206,19 +212,49 @@ else first="$iblock" fbg="$ibg" facc="$accent" second="$tblock" sbg="$tbg" sacc="$(chan "$text_aacc" "$text_acc")" fi -# Seam between the two blocks. notch=on (with differing backgrounds) takes the -# shape's right cap — the icon colour tapering into the text background. Otherwise -# the blocks abut through the plain middle separator (@themux_module_middle_separator, -# empty by default), so the icon and text backgrounds are themselves the divider -# (catppuccin-style) and the module stays compact — no extra full-block cell. +# Seam between the two blocks. notch normalizes to gt/lt/auto/off (themux_notch_mode, +# render_style.sh): off (or the two blocks sharing one background) lets them abut +# through the plain middle separator (@themux_module_middle_separator, empty by +# default), so the icon and text backgrounds are themselves the divider +# (catppuccin-style) and the module stays compact — no extra full-block cell. gt +# takes the shape's right cap (the first block's colour tapering into the second — +# today's former notch=on); lt mirrors it, the second block's colour tapering into +# the first (colours swapped). auto is zone-aware and cannot resolve here — only +# utils/layout.sh knows which zone this occurrence sits in — so it bakes BOTH +# concrete seams into hidden per-direction options and leaves the control-byte +# marker (NOTCH_MARK) in the core/pill; layout splices in the right one at draw +# time via tmux's #{s///}. +mode=$(themux_notch_mode "$notch") +seam_gt="" seam_lt="" if [ -z "$second" ]; then seam="" -elif [ "$fbg" != "$sbg" ] && [ "$notch" = on ]; then - seamcol="$fbg" - [ "$fbg" = default ] && seamcol="$facc" - seam="#[fg=$seamcol,bg=$sbg]$rglyph" -else +elif [ "$fbg" = "$sbg" ]; then seam="$midsep" +else + case "$mode" in + gt) + seamcol="$fbg" + [ "$fbg" = default ] && seamcol="$facc" + seam="#[fg=$seamcol,bg=$sbg]$rglyph" + ;; + lt) + ltcol="$sbg" + [ "$sbg" = default ] && ltcol="$sacc" + seam="#[fg=$ltcol,bg=$fbg]$lglyph" + ;; + auto) + seamcol="$fbg" + [ "$fbg" = default ] && seamcol="$facc" + seam_gt="#[fg=$seamcol,bg=$sbg]$rglyph" + ltcol="$sbg" + [ "$sbg" = default ] && ltcol="$sacc" + seam_lt="#[fg=$ltcol,bg=$fbg]$lglyph" + seam="$NOTCH_MARK" + ;; + *) + seam="$midsep" + ;; + esac fi # Expose the bare core (blocks + seam, no outer caps) and the edge cap colours @@ -238,7 +274,9 @@ set_args=(set -g "@_tmx_module_${name}_core" "${first}${seam}${second}" ';' set -g "@_tmx_module_${name}_lcol" "$lcol" ';' set -g "@_tmx_module_${name}_rcol" "$rcol" ';' set -g "@_tmx_module_${name}_lbg" "$fbg" - ';' set -g "@_tmx_module_${name}_rbg" "$sbg") + ';' set -g "@_tmx_module_${name}_rbg" "$sbg" + ';' set -g "@_tmx_module_${name}_seam_gt" "$seam_gt" + ';' set -g "@_tmx_module_${name}_seam_lt" "$seam_lt") if [ -n "$when" ]; then prefix="" [ "$text_style" = naked ] && prefix="#{@_tmx_module_divider}" diff --git a/utils/render_style.sh b/utils/render_style.sh index 1d5bbfc..0b9d73b 100755 --- a/utils/render_style.sh +++ b/utils/render_style.sh @@ -31,6 +31,15 @@ themux_prop() { # $1 item, $2 prop tmux display -p "#{?#{==:#{@themux_$1_$2},},#{@themux_all_$2},#{@themux_$1_$2}}" } +# Normalize a raw @themux_*_notch value to its direction: gt (>, the left block's +# colour penetrates right), lt (<, the right block penetrates left), auto (zone- +# aware — on is its alias) or off (default; unknown values degrade here, same +# convention as state_target). Single home for the on->auto alias, used by all +# three renderers (module_render.sh, window_render.sh, pane_render.sh). +themux_notch_mode() { # $1 raw -> gt | lt | auto | off + case "$1" in '>') echo gt ;; '<') echo lt ;; auto|on) echo auto ;; *) echo off ;; esac +} + # Parse a @themux_*_padding value into four side pads: # "N" -> "N N|N N" # " [leading-right]| [text-right]" From 1f755cb2c1b7503a1949a1451a78a3b84d50c26d Mon Sep 17 00:00:00 2001 From: Jeffrey Leon Date: Wed, 1 Jul 2026 20:37:10 -0400 Subject: [PATCH 2/8] feat(panes)!: directional notch and right-position seam --- utils/pane_render.sh | 55 +++++++++++++++++++++++++++++++++----------- 1 file changed, 41 insertions(+), 14 deletions(-) diff --git a/utils/pane_render.sh b/utils/pane_render.sh index ad5c4e7..df7a0f0 100755 --- a/utils/pane_render.sh +++ b/utils/pane_render.sh @@ -76,16 +76,29 @@ rtcap=$rtbg; [ "$rtbg" = default ] && rtcap=$txt_base atcap=$atbg; [ "$atbg" = default ] && atcap=$txt_act txt_cap=$(sw "$atcap" "$rtcap") -# The notch seam glyph mirrors the shape's right cap (octal UTF-8): slanted E0BC, -# rounded E0B4, powerline E0B0, squared █ block. -seam_glyph="" -[ "$notch" = on ] && case "$shape" in - slanted) seam_glyph=$(printf '\356\202\274') ;; - rounded) seam_glyph=$(printf '\356\202\264') ;; - powerline) seam_glyph=$(printf '\356\202\260') ;; - squared) seam_glyph=$(printf '\342\226\210') ;; +# Seam glyphs, direction-aware (octal UTF-8, mirrors utils/module_render.sh): gt +# tapers the visually-first block's colour into the second via the shape's right +# cap; lt mirrors it, the second block's colour into the first via the left cap. +# Squared has no outer cap (see left_glyph/right_glyph above), but the seam still +# draws a full block — same glyph both directions, only its colours differ. +case "$shape" in + rounded) seam_lglyph=$(printf '\356\202\266'); seam_rglyph=$(printf '\356\202\264') ;; + slanted) seam_lglyph=$(printf '\356\202\272'); seam_rglyph=$(printf '\356\202\274') ;; + powerline) seam_lglyph=$(printf '\356\202\262'); seam_rglyph=$(printf '\356\202\260') ;; + squared) seam_lglyph=$(printf '\342\226\210'); seam_rglyph=$(printf '\342\226\210') ;; + *) seam_lglyph=""; seam_rglyph="" ;; # unstyled: no seam esac +# auto is position-aware: panes have no bar zone to key off (unlike modules and +# windows), so the direction instead follows leading_position — the leading +# block's colour penetrates rightward when it sits on the left (mirrors the +# pre-existing on behaviour), leftward when it sits on the right. Explicit gt/lt +# ignore position, the same way explicit modules ignore their zone. +mode=$(themux_notch_mode "$notch") +if [ "$mode" = auto ]; then + [ "$position" = right ] && mode=lt || mode=gt +fi + # Padding owns all four inner sides, including the number<->text seam. lead_block="#[fg=$lead_fg,bg=$lead_bg]$(spaces "$pleft")$index$(spaces "$pright")" txt_block="#[fg=$txt_fg,bg=$txt_bg]$(spaces "$tleft")$text$(spaces "$tright")" @@ -94,16 +107,30 @@ txt_block="#[fg=$txt_fg,bg=$txt_bg]$(spaces "$tleft")$text$(spaces "$tright")" # (its cap colour). Empty glyph (squared) -> no cap, the block fills its own edge. cap() { [ -n "$1" ] && printf '#[fg=%s,bg=default]%s' "$2" "$1"; } -# Notch seam: the leading's right cap tapering into the text bg, only on the -# number-on-the-left layout (matching windows) and only when the two blocks -# differ — same bg would render an invisible phantom cell. +# Notch seam: the block that is visually first tapers into the second (gt) or +# vice versa (lt), gated on the two blocks actually differing — same bg would +# render an invisible phantom cell. Block order follows leading_position, like +# utils/module_render.sh: left -> leading first, text second; right -> reversed. +# fg uses the cap-fallback colour ($lead_cap/$txt_cap, not the raw $lead_bg/ +# $txt_bg) so a naked block (bg=default) still shows a coloured seam instead of +# an invisible fg=default one; bg stays the raw block colour since bg=default +# there is itself legitimate — a naked block's cap over the bare border. seam() { - [ -n "$seam_glyph" ] && [ "$lead_bg" != "$txt_bg" ] && - printf '#[fg=%s,bg=%s]%s' "$lead_bg" "$txt_bg" "$seam_glyph" + local fbg sbg fcap scap glyph + if [ "$position" = right ]; then + fbg="$txt_bg" fcap="$txt_cap" sbg="$lead_bg" scap="$lead_cap" + else + fbg="$lead_bg" fcap="$lead_cap" sbg="$txt_bg" scap="$txt_cap" + fi + [ "$fbg" = "$sbg" ] && return + case "$mode" in + gt) glyph="$seam_rglyph"; [ -n "$glyph" ] && printf '#[fg=%s,bg=%s]%s' "$fcap" "$sbg" "$glyph" ;; + lt) glyph="$seam_lglyph"; [ -n "$glyph" ] && printf '#[fg=%s,bg=%s]%s' "$scap" "$fbg" "$glyph" ;; + esac } if [ "$position" = right ]; then - fmt="$(cap "$left_glyph" "$txt_cap")$txt_block$lead_block$(cap "$right_glyph" "$lead_cap")" + fmt="$(cap "$left_glyph" "$txt_cap")$txt_block$(seam)$lead_block$(cap "$right_glyph" "$lead_cap")" else fmt="$(cap "$left_glyph" "$lead_cap")$lead_block$(seam)$txt_block$(cap "$right_glyph" "$txt_cap")" fi From 24cf57435a677a4e5ee1ea4887ff873e6bb99b9e Mon Sep 17 00:00:00 2001 From: Jeffrey Leon Date: Wed, 1 Jul 2026 20:42:40 -0400 Subject: [PATCH 3/8] feat(windows)!: directional notch and zone-aware auto for the window list --- utils/layout.sh | 10 ++++++++ utils/window_render.sh | 58 ++++++++++++++++++++++++++++++++---------- 2 files changed, 55 insertions(+), 13 deletions(-) diff --git a/utils/layout.sh b/utils/layout.sh index 631bb94..b7a9552 100755 --- a/utils/layout.sh +++ b/utils/layout.sh @@ -271,6 +271,16 @@ expand_zone() { local -a mm2 case "${it_type[idx]}" in W) + # window-status-format is ONE shared tmux option (not per-occurrence like + # a module core), so an auto notch cannot resolve through a splice; it + # dispatches at draw time on this hidden global instead (utils/ + # window_render.sh). Last write wins if "windows" sits in more than one + # zone of the same row (or across rows) — documented, not fixed here. + case "$align2" in + left) tmux set -g @_tmx_window_notch_dir gt ;; + right) tmux set -g @_tmx_window_notch_dir lt ;; + *) tmux set -g @_tmx_window_notch_dir off ;; + esac case "$fl2:$fr2" in 1:1) wedge2=both ;; 1:0) wedge2=left ;; diff --git a/utils/window_render.sh b/utils/window_render.sh index 50f37c1..0062a71 100755 --- a/utils/window_render.sh +++ b/utils/window_render.sh @@ -59,14 +59,20 @@ connected=0 # First window index (for the ribbon's opening left cap). With contiguous indices # it is base-index; baked as a literal so the draw-time test stays cheap. base=$(tmux show -gwv base-index 2>/dev/null); [ -n "$base" ] || base=0 -# The notch seam mirrors the shape's right cap; left layout only (like panes). -seam_glyph="" -[ "$notch" = on ] && case "$shape" in - slanted) seam_glyph=$(printf '\356\202\274') ;; - rounded) seam_glyph=$(printf '\356\202\264') ;; - powerline) seam_glyph=$(printf '\356\202\260') ;; - squared) seam_glyph=$(printf '\342\226\210') ;; +# Seam glyphs, direction-aware (octal UTF-8, mirrors utils/module_render.sh and +# utils/pane_render.sh): gt tapers the visually-first block's colour into the +# second via the shape's right cap; lt mirrors it, the second block's colour into +# the first via the left cap. Squared has no outer cap (lglyph/rglyph above are +# empty there), but the seam still draws a full block — same glyph both +# directions, only its colours differ. +case "$shape" in + rounded) seam_lglyph=$(printf '\356\202\266'); seam_rglyph=$(printf '\356\202\264') ;; + slanted) seam_lglyph=$(printf '\356\202\272'); seam_rglyph=$(printf '\356\202\274') ;; + powerline) seam_lglyph=$(printf '\356\202\262'); seam_rglyph=$(printf '\356\202\260') ;; + squared) seam_lglyph=$(printf '\342\226\210'); seam_rglyph=$(printf '\342\226\210') ;; + *) seam_lglyph=""; seam_rglyph="" ;; esac +mode=$(themux_notch_mode "$notch") # A cap. With a glyph (rounded/slanted) it is the shape glyph over the bare bar # in the cap colour $2 ($2 falls back to the accent when a block is naked). With @@ -96,6 +102,7 @@ render_side() { local p="$1" o="$2" opt="$3" prefix="$4" local lvar tvar lead_acc txt_acc ibg ifg tbg tfg icap tcap number text seam nblock nameblock local first_cap last_cap raw_lcap raw_rcap flags_out none_out left_out right_out both_out + local fbg sbg fcap scap gt_txt lt_txt if [ "$p" = w ]; then lvar="$leading" tvar="$name_style" lead_acc=$(expand "#{E:@themux_window_leading_color}") @@ -114,6 +121,33 @@ render_side() { number=$(expand "#{@themux_window_${o}number}") text="#{E:@_tmx_${p}_text}" # draw-time; "" when the name is hidden + # Notch seam: the block that is visually first tapers into the second (gt) or + # vice versa (lt), gated on the two blocks actually differing — same bg would + # render an invisible phantom cell. Block order follows position, like + # utils/module_render.sh: left -> number first, name second; right -> reversed. + # Style directives stay as two independent #[...] escapes (no comma) so the + # seam is safe wherever it lands inside a #{?name,...,} conditional (a literal + # comma there would be read as a branch separator, not a style attribute). + # gt/lt bake directly; auto dispatches at draw time on the hidden global + # @_tmx_window_notch_dir that utils/layout.sh sets per occurrence (left->gt, + # right->lt) — window-status-format is ONE shared option, so this cannot + # resolve per zone the way a module's marker splice does; last write wins if + # "windows" appears in more than one zone (documented in the options/docs). + seam="" + if [ -n "$seam_rglyph" ]; then + if [ "$position" = right ]; then fbg="$tbg" fcap="$tcap" sbg="$ibg" scap="$icap" + else fbg="$ibg" fcap="$icap" sbg="$tbg" scap="$tcap"; fi + if [ "$fbg" != "$sbg" ]; then + gt_txt="#[fg=$fcap]#[bg=$sbg]$seam_rglyph" + lt_txt="#[fg=$scap]#[bg=$fbg]$seam_lglyph" + case "$mode" in + gt) seam="$gt_txt" ;; + lt) seam="$lt_txt" ;; + auto) seam="#{?#{==:#{@_tmx_window_notch_dir},gt},${gt_txt},#{?#{==:#{@_tmx_window_notch_dir},lt},${lt_txt},}}" ;; + esac + fi + fi + # Cap colours are stashed for the connected separator (the bg channel is the # block colour, or the accent the block uses when transparent/naked). if [ "$p" = w ]; then @@ -128,10 +162,6 @@ render_side() { # number block, then the name block (only when the window has a name). The # right cap follows the rightmost block: the name bg when a name shows, the # number bg when it does not. - seam="" - # Styles inside the #{?name,...} conditional must not carry a literal comma - # (#{?} splits on commas), so set fg and bg as separate #[...] directives. - [ -n "$seam_glyph" ] && [ "$ibg" != "$tbg" ] && seam="#[fg=$icap]#[bg=$tbg]$seam_glyph" nameblock="#{?${text},${seam}#[fg=$tfg]#[bg=$tbg]$(spaces "$tleft")${text}$(spaces "$tright"),}" if [ "$connected" = 1 ]; then # Inner seams come from the separator. The first window opens the ribbon @@ -147,13 +177,15 @@ render_side() { fi flags_out="${nblock}${nameblock}${flags}" elif [ -n "$(tmux show -gqv "@_tmx_${p}_text")" ]; then - # number on the right: name block first, then the number block. + # number on the right: name block first, then the number block. The seam + # lands immediately after the name content, before the flags, so the state + # icons stay adjacent to the number block. raw_lcap=$(cap "$lglyph" "$tcap" "$tbg") nameblock="#[fg=$tfg,bg=$tbg]${text} " raw_rcap=$(cap "$rglyph" "$icap" "$ibg") first_cap="$raw_lcap" last_cap="$raw_rcap" - flags_out="${nameblock}${flags}${nblock}" + flags_out="${nameblock}${seam}${flags}${nblock}" else # number on the right, no name: just the number block. raw_lcap=$(cap "$lglyph" "$icap" "$ibg") From 501e17a94e9ef347e2edf46494163cf9b185869e Mon Sep 17 00:00:00 2001 From: Jeffrey Leon Date: Wed, 1 Jul 2026 20:46:40 -0400 Subject: [PATCH 4/8] docs: document the directional notch grammar --- README.md | 9 +++++-- docs/explanation/design.md | 6 ++++- docs/reference/configuration.md | 43 +++++++++++++++++++++++++++------ themux_options.conf | 27 ++++++++++++++++++--- 4 files changed, 71 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 74987d2..a74dcbc 100644 --- a/README.md +++ b/README.md @@ -149,8 +149,13 @@ set -g @themux_module_notch "off" accent text) or `naked` (transparent, accent text — pair with `@themux_status_background "none"`). A naked block keeps the shape's caps as an outline, so `rounded` leading + `naked` text reads as a capsule. -- **notch** — `on` makes the leading↔text seam inherit the shape's cap instead - of a flat edge. +- **notch** — the leading↔text seam. `off` (default) is flat; `>` bakes the + shape's cap with leading's colour tapering into text; `<` mirrors it, + colours reversed; `auto` (`on` is an alias) picks a direction per placement: + zone-aware for modules/windows (left zone `>`, right zone `<`, centre off), + position-aware for panes (`leading_position left` → `>`, `right` → `<`). A + different axis from the connectors below — notch shapes the seam *inside* + one item, connectors join *between* items. The text-block **variant** prop is `@themux__text_variant` for all three items; on windows the name *content* lives in `@themux_window_name` (the bare diff --git a/docs/explanation/design.md b/docs/explanation/design.md index 7726ee3..63c9b1a 100644 --- a/docs/explanation/design.md +++ b/docs/explanation/design.md @@ -32,7 +32,7 @@ vocabulary. | **shape** | `squared` / `rounded` / `slanted` / `powerline` | Block with square / round / slant / arrow (powerline chevron) caps. | | | `unstyled` | themux leaves the item alone so you can build it by hand. | | **leading** / **text** | `solid` / `soft` / `subtle` / `naked` | The icon-or-number block and the text block each pick a style: accent block / grey block / grey block with accent text / transparent. | -| **notch** | `on` / `off` | The leading↔text seam inherits the shape's cap. | +| **notch** | `off` / `>` / `<` / `auto` (`on` alias) | The leading↔text seam direction; `auto` resolves one per placement. | The props are orthogonal: each item's `shape`, `leading`, `text` and `notch` are chosen independently, and any combination is valid — a colored chip with a @@ -40,6 +40,10 @@ transparent label, a rounded outline capsule, a plain grey pill. A common renderer resolves the two block styles, so a new shape is a few cap glyphs, not a flag that interacts with every other flag. +One prop bends this locality rule: `notch auto` is the only one whose resolved +value depends on where the item sits (its status-line zone, or a pane's +`leading_position`) rather than solely on the item's own options. + `unstyled` is the escape hatch: it makes the law of orthogonality hold even at the edges. Anything themux's props do not cover, you build yourself with the exposed palette, and themux stays out of the way. diff --git a/docs/reference/configuration.md b/docs/reference/configuration.md index 5ca5724..ae0e599 100644 --- a/docs/reference/configuration.md +++ b/docs/reference/configuration.md @@ -7,7 +7,7 @@ load, so changing an option and reloading is enough — no `kill-server`. The three UI items — **status modules**, **windows**, **panes** — are each a *component* styled through a few independent props: a **shape**, a -**leading** style, a **text** style, a **notch** flag, and **padding**. +**leading** style, a **text** style, a **notch** direction, and **padding**. ### Anatomy of a component @@ -32,8 +32,8 @@ its text block `subtle` (grey fill, accent text), so the two halves stand apart. / `powerline` / `unstyled`). - **leading** — the icon block on a module, the number block on a window or pane. - **text** — the value on a module, the window or pane name. -- **notch** — when `on`, the seam between leading and text inherits the shape's - cap instead of a flat edge. +- **notch** — the seam between leading and text: a direction (`>` / `<`), `off` + (flat), or the zone-aware `auto` default. See below. - **padding** — the spacing inside each block. --- @@ -62,7 +62,7 @@ set -g @themux_module_notch "off" | `@themux__shape` | `squared` · `rounded` · `slanted` · `powerline` · `unstyled` | `squared` | | `@themux__leading_variant` | `solid` · `soft` · `subtle` · `naked` | `solid` | | `@themux__text_variant` † | `solid` · `soft` · `subtle` · `naked` | `soft` | -| `@themux__notch` | `on` · `off` | `off` | +| `@themux__notch` | `off` · `>` · `<` · `auto` · `on` (alias of `auto`) | `off` | | `@themux__leading_position` | `left` · `right` | `left` | | `@themux__padding` | `"N"` or `" [LR]\| [TR]"` cells (see below) | `1` | | `@themux__leading_active_variant` | `solid` · `soft` · `subtle` · `naked` | resting variant | @@ -129,9 +129,38 @@ A `naked` block keeps the shape's caps as an outline, so a `rounded` leading + `naked` text reads as a capsule. Pair naked styles with `@themux_status_background "none"` for a fully bare bar. -**notch** — `on` makes the leading↔text seam inherit the shape's cap (the -leading color tapering into the text background) instead of a flat boundary; -it collapses to nothing when the two blocks share a background. +**notch** — the seam between the leading and text blocks. `off` (default) keeps +a flat boundary (a module falls back to `@themux_module_middle_separator` +there); `>` bakes the shape's cap with the leading (visually first) block's +colour tapering into the second; `<` mirrors it, the second block's colour +tapering back into the first, cap and colours reversed. Either direction +collapses to nothing when the two blocks share one background — no phantom +cell. + +`auto` (`on` is an alias) resolves a direction per placement instead of a fixed +one: + +| Item | Resolves by | first / left | last / right | centre | +| --- | --- | --- | --- | --- | +| module, window | status-line zone | `>` | `<` | `off` | +| pane | `@themux__leading_position` | `>` (`left`) | `<` (`right`) | — | + +A module referenced in more than one zone gets one seam per zone — each +occurrence resolves independently. The window list is the one exception: +`window-status-format` is a single shared tmux option, so if the `windows` +token appears in more than one zone (or row), the last one themux processes +wins for all of them. + +This is a different axis from the module [connectors](#status-layout) (`>` / +`<` between adjacent items in a zone): notch shapes the seam *inside* one +item, between its own leading and text blocks; connectors join *between* +items. They share glyphs by design — the same taper reads the same way either +side of the axis — but are configured and resolved independently. + +> **Breaking**: the old `on` always drew the `>` seam. As an alias of `auto` +> it can now draw `>`, `<`, or nothing depending on where the item sits — pin +> `>` explicitly (`@themux__notch ">"`) if you relied on the old fixed +> direction. For windows and panes, each part has a resting and an active accent: the resting one (`@themux_window_leading_color`, `@themux_pane_leading_color`, and the diff --git a/themux_options.conf b/themux_options.conf index 4c7fe1b..1da31ef 100644 --- a/themux_options.conf +++ b/themux_options.conf @@ -60,11 +60,17 @@ set -ogq @themux_all_padding "1" # leading_variant : solid | soft | subtle | naked (the number block) # text_variant : solid | soft | subtle | naked (the name block; the # window name content lives in @themux_window_name) -# notch : on | off (number<->name seam) +# notch : off | > | < | auto | on(alias) (number<->name seam) # padding : " [LR]| [TR]" cells — leading/text sides # Variants: solid = accent block, soft = grey block, subtle = grey block + accent # text, naked = transparent + accent text. Defaults come from @themux_all_*; set # @themux_window_{shape,leading_variant,text_variant,notch} to override windows. +# notch: > tapers the number block's colour into the name (right cap); < mirrors +# it (left cap, colours swapped); auto (on is an alias) picks a direction by +# status-line zone — left zone > , right zone < , centre off. window-status-format +# is ONE shared tmux option, so if "windows" appears in more than one zone the +# last one resolved wins for all of them. Either direction collapses to nothing +# when the two blocks share a background. # How adjacent windows meet in the list, mirroring the module connectors (symbols # only): | separate pills (default, like a space between modules) | <> raised # (active window over its neighbours) | > right | < left | = flat. Anything but | @@ -95,7 +101,7 @@ set -ogq @themux_status_line_5 "" # shape : squared | rounded | slanted | powerline | unstyled # leading_variant : solid | soft | subtle | naked (the icon block) # text_variant : solid | soft | subtle | naked (the text block) -# notch : on | off (icon<->text seam) +# notch : off | > | < | auto | on(alias) (icon<->text seam) # padding : " [LR]| [TR]" cells — leading/text sides # Variants: solid = accent block, soft = grey block, subtle = grey block + accent # text, naked = transparent + accent text. Defaults come from @themux_all_*; set @@ -103,6 +109,13 @@ set -ogq @themux_status_line_5 "" # or @themux__{leading_variant,text_variant,...} for ONE module (e.g. # @themux_cpu_text_variant subtle). Precedence: > module > built-in # internal defaults > all. +# notch: > tapers the icon block's colour into the text (right cap); < mirrors it +# (left cap, colours swapped); auto (on is an alias) picks a direction by +# status-line zone — left zone > , right zone < , centre off (falls back to +# @themux_module_middle_separator, like off itself). Every occurrence of the same +# module resolves independently (unlike windows), since each is its own status- +# format reference. Either direction collapses to nothing when the two blocks +# share a background. # A module's colour rides in its accent (@themux__color, or per-slot # @themux___color); an alert/state module makes that accent a # draw-time ref (cpu/ram threshold, session prefix) and the variant decides how it @@ -140,10 +153,15 @@ set -ogq @themux_menu_selected_style "fg=#{@thm_fg},bold,bg=#{@thm_overlay_0}" # shape : squared | rounded | slanted | powerline | unstyled # leading_variant : solid | soft | subtle | naked (the pane number block) # text_variant : solid | soft | subtle | naked (the label block) -# notch : on | off (leading<->text seam) +# notch : off | > | < | auto | on(alias) (leading<->text seam) # Variants: solid = accent block, soft = grey block, subtle = grey block + accent # text, naked = transparent + accent text. Defaults come from @themux_all_*; set # @themux_pane_{shape,leading_variant,text_variant,notch} to override just panes. +# notch: > tapers the leading block's colour into the text (right cap); < mirrors +# it (left cap, colours swapped); auto (on is an alias) picks a direction from +# leading_position instead of a bar zone — panes have none — left -> > , right -> +# < . Either direction collapses to nothing when the two blocks share a +# background. # Active appearance: the active pane is rendered with each part's _active_variant # + _active_color (below). Default _active_variant = the resting variant, so only # the colour swaps to the active accent. @@ -207,6 +225,7 @@ set -ogq @themux_window_flags_icon_format "##{?window_activity_flag,#{E:@themux_ # Status line options (DO NOT USE -o IN YOUR OWN CONFIGURATION) # The module caps come from the shape (drawn in utils/module_render.sh); the # middle separator is the gap between a module's icon and text (replaced by the -# shape's cap seam when @themux_module_notch is on). +# shape's cap seam wherever @themux_module_notch resolves to a direction — off, +# and a zone-off auto, still use this separator). set -ogq @themux_module_middle_separator "" set -ogq @themux_module_connect_separator "no" # yes, no From 9f129da8774cd6abc17eb463459ede52a23c3800 Mon Sep 17 00:00:00 2001 From: Jeffrey Leon Date: Wed, 1 Jul 2026 21:03:44 -0400 Subject: [PATCH 5/8] test: cover directional and zone-aware auto notch --- run_tests.sh | 1 + tests/notch_direction.sh | 253 +++++++++++++++++++++++++++++ tests/notch_direction_expected.txt | 32 ++++ 3 files changed, 286 insertions(+) create mode 100644 tests/notch_direction.sh create mode 100644 tests/notch_direction_expected.txt diff --git a/run_tests.sh b/run_tests.sh index d212989..df8383e 100755 --- a/run_tests.sh +++ b/run_tests.sh @@ -19,3 +19,4 @@ script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" &>/dev/null && pwd -P) "${script_dir}"/tests/harness.sh --test "${script_dir}"/tests/panes_variant.sh --expected "${script_dir}"/tests/panes_variant_expected.txt "$@" "${script_dir}"/tests/harness.sh --test "${script_dir}"/tests/ram_module.sh --expected "${script_dir}"/tests/ram_module_expected.txt "$@" "${script_dir}"/tests/harness.sh --test "${script_dir}"/tests/module_variant.sh --expected "${script_dir}"/tests/module_variant_expected.txt "$@" +"${script_dir}"/tests/harness.sh --test "${script_dir}"/tests/notch_direction.sh --expected "${script_dir}"/tests/notch_direction_expected.txt "$@" diff --git a/tests/notch_direction.sh b/tests/notch_direction.sh new file mode 100644 index 0000000..8d24fb1 --- /dev/null +++ b/tests/notch_direction.sh @@ -0,0 +1,253 @@ +#!/usr/bin/env bash + +script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" &>/dev/null && pwd -P) +# shellcheck disable=SC1091 +source "${script_dir}/helpers.sh" + +# Directional + zone-aware auto notch (@themux_*_notch: off | > | < | auto | on +# alias). Conventions mirror tests/module_variant.sh and tests/windows_line.sh: +# pre-layout assertions read the raw or expanded module core (core/core_e, the +# #{E:} idiom); zone-resolution assertions run utils/layout.sh and grep the RAW +# status-format[N] text, since an auto-resolved seam is spliced in literally +# there (a full module pill stays a hidden #{E:@themux_module_} reference +# otherwise, so its own outer caps never leak into that raw text). `|| true` +# guards every possibly-zero-match grep — the harness ERR-trap kills the tmux +# server on a bare non-match. +core() { tmux show -gqv "@_tmx_module_$1_core"; } +core_e() { tmux display -p "#{E:@_tmx_module_$1_core}"; } +run_layout() { tmux run-shell "${script_dir}/../utils/layout.sh"; } +src() { + tmux source "${script_dir}/../themux_options.conf" + tmux source "${script_dir}/../themux.conf" +} +count() { # $1 haystack, $2 needle -> occurrence count (0-match safe) + printf '%s' "$1" | { grep -o "$2" || true; } | wc -l | tr -d ' ' +} + +mpll=$(printf '\356\202\266') # rounded LEFT cap (E0B6) — the lt seam glyph +mprr=$(printf '\356\202\264') # rounded RIGHT cap (E0B4) — the gt seam glyph +block=$(printf '\342\226\210') # squared block (U+2588) — both directions there + +# 1-2. Explicit ">"/"<" are NOT zone-dependent: they bake directly at module_ +# render.sh time, so layout's per-zone splice never runs (its raw seam_gt/ +# seam_lt fetch comes back empty — the micro-opt skips the #{s///} wrap +# entirely). Placing the module in the OPPOSITE zone from its natural auto +# resolution and confirming the explicit direction still wins is the point. +# A rounded module's outer caps always draw one mpll + one mprr regardless of +# notch, so the seam's own contribution is counted (2 = outer + seam, not +# just present). +tmux set -g @themux_module_shape "rounded" +tmux set -g @themux_status_line_1 " / / host" +tmux set -g @themux_host_notch ">" +src +run_layout +resolved=$(tmux display -p "#{E:@themux_module_host}") +printf "\nexplicit_gt_right_zone_extra_mprr " +{ [ "$(count "$resolved" "$mprr")" -eq 2 ]; } && printf "Y" || printf "n" +printf "\nexplicit_gt_right_zone_no_extra_mpll " +{ [ "$(count "$resolved" "$mpll")" -eq 1 ]; } && printf "Y" || printf "n" + +tmux set -g @themux_status_line_1 "host / / " +tmux set -g @themux_host_notch "<" +src +run_layout +resolved=$(tmux display -p "#{E:@themux_module_host}") +printf "\nexplicit_lt_left_zone_extra_mpll " +{ [ "$(count "$resolved" "$mpll")" -eq 2 ]; } && printf "Y" || printf "n" +printf "\nexplicit_lt_left_zone_no_extra_mprr " +{ [ "$(count "$resolved" "$mprr")" -eq 1 ]; } && printf "Y" || printf "n" +tmux set -gu @themux_host_notch + +# 3-5. auto: left zone -> gt, right zone -> lt, centre zone -> off (falls back +# to the module middle separator, default empty AND a custom value). A single +# module alone in its zone renders through the full_pill fast path (a hidden +# @themux_module_ reference), so the raw status-format text is clean: +# only a spliced seam's own bytes are ever inlined there. +tmux set -g @themux_all_notch "auto" +tmux set -g @themux_status_line_1 "host / / " +src +run_layout +printf "\nauto_left_zone_gt " +{ tmux show -gv 'status-format[0]' | grep -qF "$mprr"; } && printf "Y" || printf "n" + +tmux set -g @themux_status_line_1 " / / host" +src +run_layout +printf "\nauto_right_zone_lt " +{ tmux show -gv 'status-format[0]' | grep -qF "$mpll"; } && printf "Y" || printf "n" + +tmux set -g @themux_status_line_1 " / host / " +src +run_layout +printf "\nauto_centre_zone_no_gt " +{ tmux show -gv 'status-format[0]' | grep -qF "$mprr"; } && printf "n" || printf "Y" +printf "\nauto_centre_zone_no_lt " +{ tmux show -gv 'status-format[0]' | grep -qF "$mpll"; } && printf "n" || printf "Y" + +tmux set -g @themux_module_middle_separator "MIDSEP" +src +run_layout +printf "\nauto_centre_zone_uses_custom_midsep " +{ tmux show -gv 'status-format[0]' | grep -qF "MIDSEP"; } && printf "Y" || printf "n" +tmux set -gu @themux_module_middle_separator + +# 6. "on" is a plain alias of "auto": same three zone outcomes. +tmux set -g @themux_all_notch "on" +tmux set -g @themux_status_line_1 "host / / " +src +run_layout +printf "\non_alias_left_zone_gt " +{ tmux show -gv 'status-format[0]' | grep -qF "$mprr"; } && printf "Y" || printf "n" + +tmux set -g @themux_status_line_1 " / / host" +src +run_layout +printf "\non_alias_right_zone_lt " +{ tmux show -gv 'status-format[0]' | grep -qF "$mpll"; } && printf "Y" || printf "n" + +tmux set -g @themux_status_line_1 " / host / " +src +run_layout +printf "\non_alias_centre_zone_off " +{ tmux show -gv 'status-format[0]' | grep -qF "$mprr"; } && printf "n" || printf "Y" + +# 7. Cascade override: @themux_all_notch auto + @themux_cpu_notch "<" fixes cpu +# to lt regardless of zone (checked pre-layout, like the explicit tests above — +# cpu is an _expand module, so mod_core/full_pill inline its whole core rather +# than referencing it, but the same "explicit ignores zone" rule still governs +# what module_render.sh baked). Its sibling (host, plain, same zone) still +# resolves through the zone (left -> gt). core_e reads the BARE core (no outer +# caps at all, unlike tests 1-2's full pill), so a plain presence check is +# clean here — nothing else in it can produce a false mpll/mprr match. +tmux set -g @themux_status_line_1 "cpu host" +tmux set -g @themux_all_notch "auto" +tmux set -g @themux_cpu_notch "<" +src +run_layout +cpu_core=$(core_e cpu) +printf "\ncascade_override_cpu_fixed_lt " +{ printf '%s' "$cpu_core" | grep -qF "$mpll"; } && printf "Y" || printf "n" +printf "\ncascade_override_cpu_no_gt_seam " +{ printf '%s' "$cpu_core" | grep -qF "$mprr"; } && printf "n" || printf "Y" +printf "\ncascade_override_sibling_zone_resolved_gt " +{ tmux show -gv 'status-format[0]' | grep -qF "$mprr"; } && printf "Y" || printf "n" +tmux set -gu @themux_cpu_notch + +# 8. Multi-placement: the SAME module referenced in both the left and right +# zone of one row resolves independently per occurrence — one snippet gets gt, +# the other lt, in the same status-format[0] string. +tmux set -g @themux_status_line_1 "host / / host" +src +run_layout +fmt0=$(tmux show -gv 'status-format[0]') +printf "\nmulti_placement_one_gt " +{ [ "$(count "$fmt0" "$mprr")" -eq 1 ]; } && printf "Y" || printf "n" +printf "\nmulti_placement_one_lt " +{ [ "$(count "$fmt0" "$mpll")" -eq 1 ]; } && printf "Y" || printf "n" + +# 9. Panes: no bar zone, so auto follows leading_position instead (left -> gt, +# right -> lt). Also regression-tests the newly added position=right seam path +# (there was none before this feature). +tmux set -g @themux_pane_shape "rounded" +tmux set -g @themux_pane_leading_variant "solid" +tmux set -g @themux_pane_text_variant "soft" +tmux set -g @themux_pane_notch "auto" +tmux set -g @themux_pane_status "top" +tmux set -g @themux_pane_leading_position "left" +src +printf "\npane_auto_left_position_gt " +{ tmux show -gv pane-border-format | grep -qF "$mprr"; } && printf "Y" || printf "n" +tmux set -g @themux_pane_leading_position "right" +src +printf "\npane_auto_right_position_lt " +{ tmux show -gv pane-border-format | grep -qF "$mpll"; } && printf "Y" || printf "n" +tmux set -gu @themux_pane_leading_position +tmux set -gu @themux_pane_notch +tmux set -gu @themux_pane_leading_variant +tmux set -gu @themux_pane_text_variant +tmux set -gu @themux_pane_status + +# 10. Windows: layout sets the hidden @_tmx_window_notch_dir global per +# occurrence (left -> gt, right -> lt, centre -> off); window-status-format +# carries the draw-time #{?#{==:...}} dispatch that reads it (ONE shared tmux +# option, unlike a module's per-occurrence splice). +tmux set -g @themux_window_shape "rounded" +tmux set -g @themux_window_notch "auto" +tmux set -g @themux_status_line_1 "windows" +src +run_layout +printf "\nwindow_dispatch_present " +{ tmux show -gv window-status-format | grep -qF '@_tmx_window_notch_dir'; } && printf "Y" || printf "n" +printf "\nwindow_notch_dir_left_zone " +tmux show -gqv @_tmx_window_notch_dir + +tmux set -g @themux_status_line_1 " / / windows" +src +run_layout +printf "window_notch_dir_right_zone " +tmux show -gqv @_tmx_window_notch_dir + +tmux set -g @themux_status_line_1 " / windows / " +src +run_layout +printf "window_notch_dir_centre_zone " +tmux show -gqv @_tmx_window_notch_dir +tmux set -gu @themux_window_notch +tmux set -gu @themux_window_shape + +# 11. An invalid value degrades to off, same convention as state_target. +tmux set -g @themux_status_line_1 "host" +tmux set -g @themux_module_shape "rounded" +tmux set -g @themux_host_notch "off" +src +off_core=$(core_e host) +tmux set -g @themux_host_notch "nonsense" +src +invalid_core=$(core_e host) +printf "\ninvalid_notch_falls_back_to_off " +{ [ "$off_core" = "$invalid_core" ]; } && printf "Y" || printf "n" + +# 12. Squared shape: gt and lt draw the SAME glyph (U+2588, direction-agnostic) +# but with swapped colours. Squared's outer caps ALSO draw a block glyph (unlike +# windows/panes, a module's cap() only skips squared on a naked/default bg), so +# presence alone proves nothing — compare against the off baseline (one more +# block = the seam), and that the two directions add the identical glyph COUNT +# while their full cores still differ (the colours). +tmux set -g @themux_module_shape "squared" +tmux set -g @themux_host_notch "off" +src +squared_off=$(core_e host) +tmux set -g @themux_host_notch ">" +src +squared_gt=$(core_e host) +tmux set -g @themux_host_notch "<" +src +squared_lt=$(core_e host) +printf "\nsquared_gt_adds_block " +{ [ "$(count "$squared_gt" "$block")" -gt "$(count "$squared_off" "$block")" ]; } && printf "Y" || printf "n" +printf "\nsquared_lt_adds_block " +{ [ "$(count "$squared_lt" "$block")" -gt "$(count "$squared_off" "$block")" ]; } && printf "Y" || printf "n" +printf "\nsquared_gt_lt_same_glyph_count " +{ [ "$(count "$squared_gt" "$block")" -eq "$(count "$squared_lt" "$block")" ]; } && printf "Y" || printf "n" +printf "\nsquared_gt_lt_different_colours " +{ [ "$squared_gt" != "$squared_lt" ]; } && printf "Y" || printf "n" +tmux set -gu @themux_host_notch +tmux set -g @themux_module_shape "rounded" + +# 13. Active-state seam: session carries active_when (client_prefix), so its +# resting/active leading colours differ and module_render.sh's chan() wraps the +# leading block's bg in a draw-time #{?client_prefix,...} conditional. That +# block bg feeds directly into the seam colour formula, so the conditional ends +# up nested inside the seam's own #[fg=...] attribute — which utils/layout.sh +# then splices whole into status-format via #{s///:...}. This is +# the mechanism's main risk (the plan's own test 13): confirms tmux resolves +# the nested #{?} correctly instead of mangling it. +tmux set -g @themux_status_line_1 "session" +tmux set -g @themux_session_notch "auto" +src +run_layout +printf "\nactive_seam_nested_conditional_survives " +{ tmux show -gv 'status-format[0]' | grep -qF 'client_prefix'; } && printf "Y" || printf "n" +printf "\nactive_seam_has_gt_glyph " +{ tmux show -gv 'status-format[0]' | grep -qF "$mprr"; } && printf "Y" || printf "n" +tmux set -gu @themux_session_notch diff --git a/tests/notch_direction_expected.txt b/tests/notch_direction_expected.txt new file mode 100644 index 0000000..09a3e95 --- /dev/null +++ b/tests/notch_direction_expected.txt @@ -0,0 +1,32 @@ + +explicit_gt_right_zone_extra_mprr Y +explicit_gt_right_zone_no_extra_mpll Y +explicit_lt_left_zone_extra_mpll Y +explicit_lt_left_zone_no_extra_mprr Y +auto_left_zone_gt Y +auto_right_zone_lt Y +auto_centre_zone_no_gt Y +auto_centre_zone_no_lt Y +auto_centre_zone_uses_custom_midsep Y +on_alias_left_zone_gt Y +on_alias_right_zone_lt Y +on_alias_centre_zone_off Y +cascade_override_cpu_fixed_lt Y +cascade_override_cpu_no_gt_seam Y +cascade_override_sibling_zone_resolved_gt Y +multi_placement_one_gt Y +multi_placement_one_lt Y +pane_auto_left_position_gt Y +pane_auto_right_position_lt Y +window_dispatch_present Y +window_notch_dir_left_zone gt +window_notch_dir_right_zone lt +window_notch_dir_centre_zone off + +invalid_notch_falls_back_to_off Y +squared_gt_adds_block Y +squared_lt_adds_block Y +squared_gt_lt_same_glyph_count Y +squared_gt_lt_different_colours Y +active_seam_nested_conditional_survives Y +active_seam_has_gt_glyph Y From 3f05579199bb4a7282af9b4ecdd3bc01ef111b8c Mon Sep 17 00:00:00 2001 From: Jeffrey Leon Date: Wed, 1 Jul 2026 22:43:31 -0400 Subject: [PATCH 6/8] fix(modules): route auto notch seams through the expand pipeline MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit _expand modules (cpu/ram) baked a live #{l:...} colour ref into their notch seam, but the seam was bash-spliced into the core AFTER the core's own #{E:} expansion + interp pass, so the seam's #{l:} wrapper was never peeled and its fg stayed a dead literal — the dark notch triangle on cpu/ram. Route the seam through the same #{E:} expansion the core gets, splice, then interp both together. Also fix a related leak: the splice was skipped whenever the replacement text was empty (e.g. centre zone's default middle separator), but auto mode always bakes the marker regardless of the replacement — skip the splice only when no marker was baked at all, never merely because the replacement is empty, or the raw NOTCH_MARK control byte leaks into the drawn status line. --- tests/notch_direction.sh | 48 ++++++++++++++++++++ tests/notch_direction_expected.txt | 5 +++ utils/layout.sh | 71 +++++++++++++++++++++++------- 3 files changed, 109 insertions(+), 15 deletions(-) diff --git a/tests/notch_direction.sh b/tests/notch_direction.sh index 8d24fb1..26bc1fe 100644 --- a/tests/notch_direction.sh +++ b/tests/notch_direction.sh @@ -83,12 +83,26 @@ printf "\nauto_centre_zone_no_gt " { tmux show -gv 'status-format[0]' | grep -qF "$mprr"; } && printf "n" || printf "Y" printf "\nauto_centre_zone_no_lt " { tmux show -gv 'status-format[0]' | grep -qF "$mpll"; } && printf "n" || printf "Y" +# FIX 2 regression: the marker IS baked into the core in auto mode regardless of +# the centre replacement text, and the default middle separator is itself empty +# — splicing must be decided by marker PRESENCE (mod_has_marker), not by +# whether the replacement text is empty, or the marker leaks into the rendered +# line. For host (a plain ref module), that splice is tmux's own draw-time +# #{s///:...}, so the RAW stored status-format[0] legitimately still +# carries the marker byte as the substitution's pattern argument — that is the +# mechanism working as designed, not a leak. What must never happen is the +# marker surviving into the EXPANDED (drawn) text, so these two check +# #{E:status-format[0]} instead of the raw option. +printf "\nauto_centre_zone_default_empty_midsep_no_raw_marker " +{ tmux display -p "#{E:status-format[0]}" | grep -qF "$(printf '\036')"; } && printf "n" || printf "Y" tmux set -g @themux_module_middle_separator "MIDSEP" src run_layout printf "\nauto_centre_zone_uses_custom_midsep " { tmux show -gv 'status-format[0]' | grep -qF "MIDSEP"; } && printf "Y" || printf "n" +printf "\nauto_centre_zone_custom_midsep_no_raw_marker " +{ tmux display -p "#{E:status-format[0]}" | grep -qF "$(printf '\036')"; } && printf "n" || printf "Y" tmux set -gu @themux_module_middle_separator # 6. "on" is a plain alias of "auto": same three zone outcomes. @@ -251,3 +265,37 @@ printf "\nactive_seam_nested_conditional_survives " printf "\nactive_seam_has_gt_glyph " { tmux show -gv 'status-format[0]' | grep -qF "$mprr"; } && printf "Y" || printf "n" tmux set -gu @themux_session_notch + +# 14. FIX 1 regression (the user-confirmed dark notch triangle): cpu is an +# _expand module whose accent is a LIVE #{l:#{cpu_bg_color}} plugin literal. +# mod_core's _expand branch must run the seam through the SAME #{E:} expansion +# the core itself gets, BEFORE interp — same pipeline the icon backgrounds +# already ride. A dead (unpeeled) seam stays literally double-wrapped +# `#{l:#{cpu_bg_color}...}` in the baked status-format text (an invalid fg -> +# the dark glyph); once expanded exactly once it reads as a bare +# `#{cpu_bg_color}` literal (interp is a no-op here — no tmux-cpu plugin in the +# test harness — so it is not rewritten further, but the peel already +# happened, which is what mattered). Also re-checks FIX 2 across every +# status-format row, not just the centre case above. +tmux set -g @themux_status_line_1 "cpu / / " +tmux set -g @themux_all_notch "auto" +tmux set -g @themux_module_shape "rounded" +src +run_layout +fmt0=$(tmux show -gv 'status-format[0]') +printf "\ncpu_seam_not_double_wrapped " +{ printf '%s' "$fmt0" | grep -qF '#{l:#{cpu'; } && printf "n" || printf "Y" +printf "\ncpu_seam_live_colour_present " +{ printf '%s' "$fmt0" | grep -qF 'cpu_bg_color'; } && printf "Y" || printf "n" +# Unlike the centre-zone/ref-module check above, an _expand module's splice +# happens entirely at bash-string level (mod_core's _expand branch), before the +# result is ever stored — so the RAW status-format text is the right thing to +# check here; there is no draw-time #{s///} pattern argument to account for. +printf "\nno_raw_marker_in_any_status_format_row " +leak=0 +for row_i in 0 1 2 3 4; do + row=$(tmux show -gqv "status-format[$row_i]" 2>/dev/null) || true + { printf '%s' "$row" | grep -qF "$(printf '\036')"; } && leak=1 +done +{ [ "$leak" -eq 0 ]; } && printf "Y" || printf "n" +tmux set -gu @themux_all_notch diff --git a/tests/notch_direction_expected.txt b/tests/notch_direction_expected.txt index 09a3e95..4c87aad 100644 --- a/tests/notch_direction_expected.txt +++ b/tests/notch_direction_expected.txt @@ -7,7 +7,9 @@ auto_left_zone_gt Y auto_right_zone_lt Y auto_centre_zone_no_gt Y auto_centre_zone_no_lt Y +auto_centre_zone_default_empty_midsep_no_raw_marker Y auto_centre_zone_uses_custom_midsep Y +auto_centre_zone_custom_midsep_no_raw_marker Y on_alias_left_zone_gt Y on_alias_right_zone_lt Y on_alias_centre_zone_off Y @@ -30,3 +32,6 @@ squared_gt_lt_same_glyph_count Y squared_gt_lt_different_colours Y active_seam_nested_conditional_survives Y active_seam_has_gt_glyph Y +cpu_seam_not_double_wrapped Y +cpu_seam_live_colour_present Y +no_raw_marker_in_any_status_format_row Y diff --git a/utils/layout.sh b/utils/layout.sh index b7a9552..7adc09d 100755 --- a/utils/layout.sh +++ b/utils/layout.sh @@ -81,44 +81,85 @@ mod_field() { # $1 module, $2 edge option (lcol|rcol|lbg) -> a usable colour tmux show -gqv "@_tmx_module_${1}_${2}" fi } -# Resolve the raw seam text for one module occurrence, by zone: left -> the +# Resolve the raw seam TEXT for one module occurrence, by zone: left -> the # module's baked gt seam, right -> its baked lt seam, centre -> the shared middle # separator (auto's "off" fallback, same replacement notch=off itself bakes). # Fetched RAW with `show -gqv` — NOT through mod_field: a seam carries no plugin # #{var} literal, and mod_field's eager #{E:} for _expand modules would prematurely -# flatten a chan() draw-time conditional baked into the seam colour. Empty means -# the module baked its notch directly (gt/lt/off, no marker in the core) — the -# caller then skips the #{s///} splice entirely (safe no-op, but a needless one). -mod_notch_seam() { # $1 module, $2 align (left|centre|right) -> raw seam text or "" +# flatten a chan() draw-time conditional baked into the seam colour. This text can +# be legitimately EMPTY even while the marker is present — centre's default +# middle separator is "" — so it must never be used on its own to decide whether +# to splice; see mod_has_marker below for that decision. +mod_notch_seam() { # $1 module, $2 align (left|centre|right) -> raw seam text (may be empty) case "$2" in left) tmux show -gqv "@_tmx_module_${1}_seam_gt" ;; right) tmux show -gqv "@_tmx_module_${1}_seam_lt" ;; *) printf '%s' "$mmid" ;; esac } +# Whether this module's core/pill carries the NOTCH_MARK control byte at all — +# decoupled from mod_notch_seam's replacement TEXT above. module_render.sh only +# ever bakes @_tmx_module__seam_gt (together with _seam_lt) in auto mode; +# explicit gt/lt/off resolve the seam directly and leave both empty. So seam_gt's +# presence alone is a reliable marker indicator, independent of which zone this +# occurrence resolves to (including centre, whose own replacement is usually +# empty and would otherwise look identical to "no marker" — the FIX behind this +# helper). When there is no marker the caller keeps today's unwrapped fast path. +mod_has_marker() { # $1 module -> non-empty (truthy) iff the module baked the marker + tmux show -gqv "@_tmx_module_${1}_seam_gt" +} +# Expand a gt/lt seam through the same #{E:} pass an _expand module's core/pill +# gets (mod_core/full_pill's _expand branch below), so its #{l:} colour wrappers +# are peeled exactly once, just like the core's — this is what makes a live +# metric colour (cpu/ram) resolve instead of staying a dead literal. Only called +# for left/right: centre's replacement ($mmid) is already a plain string with no +# plugin literals or #{l:} wrappers, so callers splice it raw, unexpanded. +mod_notch_seam_expand() { # $1 module, $2 align (left|right) -> #{E:}-expanded seam text + local opt="seam_gt"; [ "$2" = right ] && opt="seam_lt" + tmux set -gF @_tmx_layout_tmp "#{E:@_tmx_module_${1}_${opt}}" + tmux show -gv @_tmx_layout_tmp +} mod_core() { # $1 module, $2 align -> its bare core (a ref normally; inlined for tmux-cpu) - local seam val + local seam has val seam=$(mod_notch_seam "$1" "$2") + case "$2" in + left|right) has="$seam" ;; + *) has=$(mod_has_marker "$1") ;; + esac if [ "$(tmux show -gqv "@themux_${1}_expand")" = "yes" ]; then tmux set -gF @_tmx_layout_tmp "#{E:@_tmx_module_${1}_core}" - val=$(interp "$1" "$(tmux show -gv @_tmx_layout_tmp)") - [ -n "$seam" ] && val="${val//$NOTCH_MARK/$seam}" - printf '%s' "$val" - elif [ -n "$seam" ]; then + val=$(tmux show -gv @_tmx_layout_tmp) + if [ -n "$has" ]; then + case "$2" in + left|right) seam=$(mod_notch_seam_expand "$1" "$2") ;; + esac + val="${val//$NOTCH_MARK/$seam}" + fi + printf '%s' "$(interp "$1" "$val")" + elif [ -n "$has" ]; then printf '%s' "#{s/${NOTCH_MARK}/${seam}/:#{E:@_tmx_module_${1}_core}}" else printf '%s' "#{E:@_tmx_module_${1}_core}" fi } full_pill() { # $1 module, $2 align -> its own pill, caps included (inlined for tmux-cpu) - local seam val + local seam has val seam=$(mod_notch_seam "$1" "$2") + case "$2" in + left|right) has="$seam" ;; + *) has=$(mod_has_marker "$1") ;; + esac if [ "$(tmux show -gqv "@themux_${1}_expand")" = "yes" ]; then tmux set -gF @_tmx_layout_tmp "#{E:@themux_module_${1}}" - val=$(interp "$1" "$(tmux show -gv @_tmx_layout_tmp)") - [ -n "$seam" ] && val="${val//$NOTCH_MARK/$seam}" - printf '%s' "$val" - elif [ -n "$seam" ]; then + val=$(tmux show -gv @_tmx_layout_tmp) + if [ -n "$has" ]; then + case "$2" in + left|right) seam=$(mod_notch_seam_expand "$1" "$2") ;; + esac + val="${val//$NOTCH_MARK/$seam}" + fi + printf '%s' "$(interp "$1" "$val")" + elif [ -n "$has" ]; then printf '%s' "#{s/${NOTCH_MARK}/${seam}/:#{E:@themux_module_${1}}}" else printf '%s' "#{E:@themux_module_${1}}" From 8f57935b11e6db05e555846dd5b4a5cca55f5597 Mon Sep 17 00:00:00 2001 From: Jeffrey Leon Date: Wed, 1 Jul 2026 22:45:11 -0400 Subject: [PATCH 7/8] fix(windows): gate right-position notch seam on name visibility position=left already wrapped its name block (and notch seam) inside the #{?${text},...,} name-visibility conditional, but the newly added position=right path emitted its seam unconditionally after the name content. A window whose name resolves empty at draw time then showed a floating seam taper with nothing to taper into. Wrap the position=right name content and seam in the same conditional. window_status_styling_expected.txt is updated for the same reason: it captured the previously-unconditional position=right output. --- tests/notch_direction.sh | 28 ++++++++++++++++++++++++ tests/notch_direction_expected.txt | 3 +++ tests/window_status_styling_expected.txt | 4 ++-- utils/window_render.sh | 9 +++++--- 4 files changed, 39 insertions(+), 5 deletions(-) diff --git a/tests/notch_direction.sh b/tests/notch_direction.sh index 26bc1fe..1a871a6 100644 --- a/tests/notch_direction.sh +++ b/tests/notch_direction.sh @@ -299,3 +299,31 @@ for row_i in 0 1 2 3 4; do done { [ "$leak" -eq 0 ]; } && printf "Y" || printf "n" tmux set -gu @themux_all_notch + +# 15. FIX 3 regression: window leading_position=right + notch auto. Before the +# fix the seam was emitted unconditionally right after the name block, so a +# window whose name resolves empty at draw time left a floating taper with +# nothing to taper into. The fix wraps the name content AND the seam inside the +# SAME #{?${text},…,} name-visibility conditional the position=left path +# already used. The exact adjacency "#{?#{E:@_tmx_w_text},#[fg=" only exists +# once the style/name content sits inside that conditional (old position=right +# code had no such wrapper at all), and the notch dispatch +# (@_tmx_window_notch_dir) must appear textually after that opening, inside the +# same conditional's branch. +tmux set -g @themux_status_line_1 "windows" +tmux set -g @themux_window_shape "rounded" +tmux set -g @themux_window_leading_position "right" +tmux set -g @themux_window_notch "auto" +src +run_layout +wfmt=$(tmux show -gv window-status-format) +printf "\nwindow_right_seam_inside_text_conditional " +{ printf '%s' "$wfmt" | grep -qF '#{?#{E:@_tmx_w_text},#[fg='; } && printf "Y" || printf "n" +printf "\nwindow_right_notch_dispatch_present " +{ printf '%s' "$wfmt" | grep -qF '@_tmx_window_notch_dir'; } && printf "Y" || printf "n" +printf "\nwindow_right_notch_dispatch_after_conditional_open " +before=${wfmt%%@_tmx_window_notch_dir*} +{ printf '%s' "$before" | grep -qF '#{?#{E:@_tmx_w_text},#[fg='; } && printf "Y" || printf "n" +tmux set -gu @themux_window_leading_position +tmux set -gu @themux_window_notch +tmux set -gu @themux_window_shape diff --git a/tests/notch_direction_expected.txt b/tests/notch_direction_expected.txt index 4c87aad..c580a58 100644 --- a/tests/notch_direction_expected.txt +++ b/tests/notch_direction_expected.txt @@ -35,3 +35,6 @@ active_seam_has_gt_glyph Y cpu_seam_not_double_wrapped Y cpu_seam_live_colour_present Y no_raw_marker_in_any_status_format_row Y +window_right_seam_inside_text_conditional Y +window_right_notch_dispatch_present Y +window_right_notch_dispatch_after_conditional_open Y diff --git a/tests/window_status_styling_expected.txt b/tests/window_status_styling_expected.txt index d214c84..8719155 100644 --- a/tests/window_status_styling_expected.txt +++ b/tests/window_status_styling_expected.txt @@ -7,9 +7,9 @@ window-status-format #[fg=#9399b2,bg=default]#[fg=#11111b,bg=#9399b2] #I #{?# window-status-current-format #[fg=#cba6f7,bg=default]#[fg=#11111b,bg=#cba6f7] #I #{?#{E:@_tmx_cw_text},#[fg=#cdd6f4]#[bg=#313244] #{E:@_tmx_cw_text} ,}#[fg=#{?#{E:@_tmx_cw_text},#313244,#cba6f7},bg=default] -window-status-format #[bg=#313244] #[fg=#cdd6f4,bg=#313244]#{E:@_tmx_w_text} #[fg=#11111b,bg=#9399b2] #I #[bg=#9399b2] +window-status-format #[bg=#313244] #{?#{E:@_tmx_w_text},#[fg=#cdd6f4,bg=#313244]#{E:@_tmx_w_text} ,}#[fg=#11111b,bg=#9399b2] #I #[bg=#9399b2] -window-status-current-format #[bg=#313244] #[fg=#cdd6f4,bg=#313244]#{E:@_tmx_cw_text} #[fg=#11111b,bg=#cba6f7] #I #[bg=#cba6f7] +window-status-current-format #[bg=#313244] #{?#{E:@_tmx_cw_text},#[fg=#cdd6f4,bg=#313244]#{E:@_tmx_cw_text} ,}#[fg=#11111b,bg=#cba6f7] #I #[bg=#cba6f7] window-status-format #[bg=#9399b2] #[fg=#11111b,bg=#9399b2] #I #{?#{E:@_tmx_w_text},#[fg=#cdd6f4]#[bg=#313244] #{E:@_tmx_w_text} ,}#[bg=#{?#{E:@_tmx_w_text},#313244,#9399b2}] diff --git a/utils/window_render.sh b/utils/window_render.sh index 0062a71..5e2207e 100755 --- a/utils/window_render.sh +++ b/utils/window_render.sh @@ -179,13 +179,16 @@ render_side() { elif [ -n "$(tmux show -gqv "@_tmx_${p}_text")" ]; then # number on the right: name block first, then the number block. The seam # lands immediately after the name content, before the flags, so the state - # icons stay adjacent to the number block. + # icons stay adjacent to the number block. Wrapped in the same #{?${text},…,} + # name-visibility conditional as the position=left nameblock above, so a + # window whose name resolves empty at draw time doesn't leave a floating + # seam taper with nothing to taper into. raw_lcap=$(cap "$lglyph" "$tcap" "$tbg") - nameblock="#[fg=$tfg,bg=$tbg]${text} " + nameblock="#{?${text},#[fg=$tfg,bg=$tbg]${text} ${seam},}" raw_rcap=$(cap "$rglyph" "$icap" "$ibg") first_cap="$raw_lcap" last_cap="$raw_rcap" - flags_out="${nameblock}${seam}${flags}${nblock}" + flags_out="${nameblock}${flags}${nblock}" else # number on the right, no name: just the number block. raw_lcap=$(cap "$lglyph" "$icap" "$ibg") From 362d20976592078c63bd57e288cd0849f5591ff4 Mon Sep 17 00:00:00 2001 From: Jeffrey Leon Date: Wed, 1 Jul 2026 22:45:21 -0400 Subject: [PATCH 8/8] docs: squared notch colours and direct-consumption caveat MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Document that the squared shape draws the same full-block glyph for both notch directions (only the colour placement differs), and that auto/on only resolve through the layout grammar — a module consumed directly (a user's own tmux format, or _prepend/_append) never gets its marker spliced. tmux's status-line renderer silently drops that byte at draw time, so the seam is simply absent, as if notch were off. Also update tests/module_variant.sh's stale notch comment: under the current design its length assertion only measures the RAW core's marker-vs-empty byte, not a rendered glyph's width; real glyph coverage lives in tests/notch_direction.sh. --- README.md | 14 +++++++++----- docs/reference/configuration.md | 15 ++++++++++++++- tests/module_variant.sh | 8 +++++--- 3 files changed, 28 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index a74dcbc..df39e68 100644 --- a/README.md +++ b/README.md @@ -151,11 +151,15 @@ set -g @themux_module_notch "off" outline, so `rounded` leading + `naked` text reads as a capsule. - **notch** — the leading↔text seam. `off` (default) is flat; `>` bakes the shape's cap with leading's colour tapering into text; `<` mirrors it, - colours reversed; `auto` (`on` is an alias) picks a direction per placement: - zone-aware for modules/windows (left zone `>`, right zone `<`, centre off), - position-aware for panes (`leading_position left` → `>`, `right` → `<`). A - different axis from the connectors below — notch shapes the seam *inside* - one item, connectors join *between* items. + colours reversed (with the `squared` shape both draw the same full-block + glyph — only the colour placement differs); `auto` (`on` is an alias) picks + a direction per placement: zone-aware for modules/windows (left zone `>`, + right zone `<`, centre off), position-aware for panes (`leading_position + left` → `>`, `right` → `<`). A different axis from the connectors below — + notch shapes the seam *inside* one item, connectors join *between* items. + `auto`/`on` only resolve through the layout grammar; a module consumed + directly (your own format, `_prepend`/`_append`) keeps no seam at all — + see the [Configuration Reference](./docs/reference/configuration.md) for why. The text-block **variant** prop is `@themux__text_variant` for all three items; on windows the name *content* lives in `@themux_window_name` (the bare diff --git a/docs/reference/configuration.md b/docs/reference/configuration.md index ae0e599..5a11f35 100644 --- a/docs/reference/configuration.md +++ b/docs/reference/configuration.md @@ -135,7 +135,9 @@ there); `>` bakes the shape's cap with the leading (visually first) block's colour tapering into the second; `<` mirrors it, the second block's colour tapering back into the first, cap and colours reversed. Either direction collapses to nothing when the two blocks share one background — no phantom -cell. +cell. With the `squared` shape both directions draw the same full-block glyph +(there is no taper to mirror), so `>` vs `<` differ only in which side the +colour lands on, not in the glyph itself. `auto` (`on` is an alias) resolves a direction per placement instead of a fixed one: @@ -157,6 +159,17 @@ item, between its own leading and text blocks; connectors join *between* items. They share glyphs by design — the same taper reads the same way either side of the axis — but are configured and resolved independently. +`auto`/`on` only resolve when a module is read through the layout grammar (a +token in `@themux_status_line_N`, or a pane's `leading_position`) — that is +what tells themux which zone/side this occurrence sits in. If you consume +`@themux_module_` directly instead — your own tmux formats, or a +`_prepend`/`_append` value — there is no zone to resolve against, so the +internal placeholder byte themux uses to defer that choice is never replaced. +tmux's status-line renderer silently drops that byte at draw time (it has no +visible glyph), so the leading and text blocks simply abut with no seam, as if +notch were `off`. Pin an explicit `>` or `<` on any module you consume +directly this way. + > **Breaking**: the old `on` always drew the `>` seam. As an alias of `auto` > it can now draw `>`, `<`, or nothing depending on where the item sits — pin > `>` explicitly (`@themux__notch ">"`) if you relied on the old fixed diff --git a/tests/module_variant.sh b/tests/module_variant.sh index 8da0c33..9c2cb98 100644 --- a/tests/module_variant.sh +++ b/tests/module_variant.sh @@ -92,9 +92,11 @@ printf "\nposition_swaps_edges " { [ "$ll" = "$rr" ] && [ "$lr" = "$rl" ] && [ "$ll" != "$lr" ]; } && printf "Y" || printf "n" # notch=off lets the icon and text blocks abut (compact, catppuccin-style — no seam -# cell). notch=on draws the shape's tapered cap as the seam between them, so the -# notched module is wider. The metric's solid icon over its subtle text differs in -# bg like any module, so the seam is drawn there too. +# cell). notch=on (an alias of auto) leaves a one-byte NOTCH_MARK control-byte +# placeholder in this RAW core instead of a baked glyph — utils/layout.sh splices +# the real per-zone seam in later — so this length assertion only measures +# marker-vs-empty, not the rendered glyph's width. Real glyph coverage (colour, +# direction, zone resolution) lives in tests/notch_direction.sh. tmux set -gu @themux_module_leading_position tmux set -g @themux_status_line_1 "host cpu" tmux set -g @themux_all_shape "rounded"