From 082aacd65b83ff50c5ab0d8f679c5e5bdaba7493 Mon Sep 17 00:00:00 2001 From: Jeffrey Leon Date: Fri, 3 Jul 2026 12:00:40 -0400 Subject: [PATCH] feat(props): friendly colour words and when/active_when presets MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Colour props (_color, _active_color, per-slot overrides, raw channel pins, window/pane accents) now accept a theme role word (accent, notice, ...) or a palette word (peach, mauve, surface_0, ...) resolved through @thm_role_ then @thm_ — the theme wins over tmux native names, so red is the palette red. Raw tmux values (hex, colour255, formats) pass through untouched. Visibility and escalation conditions gain two presets: always and never (alias off), normalized in shared render_style.sh helpers and consumed by layout.sh (powerline splice + group visibility), module_render.sh (the standalone-pill gate and chan's active switch). Everything else passes through raw — a bare unknown word was already false (tmux reads it as a format variable), the presets make the two useful constants sayable. git.conf and gitmux.conf set their in-repo when gate with -ogq (was -gq, which clobbered any user value at source time), matching zoom.conf. --- docs/reference/configuration.md | 41 ++++++++++++++ docs/reference/status-line.md | 5 ++ modules/git.conf | 4 +- modules/gitmux.conf | 4 +- run_tests.sh | 1 + tests/prop_grammar.sh | 98 +++++++++++++++++++++++++++++++++ tests/prop_grammar_expected.txt | 13 +++++ utils/layout.sh | 8 ++- utils/module_render.sh | 14 +++++ utils/pane_render.sh | 13 +++-- utils/render_style.sh | 47 ++++++++++++++++ utils/window_render.sh | 10 ++-- 12 files changed, 243 insertions(+), 15 deletions(-) create mode 100755 tests/prop_grammar.sh create mode 100644 tests/prop_grammar_expected.txt diff --git a/docs/reference/configuration.md b/docs/reference/configuration.md index 7547868..3086262 100644 --- a/docs/reference/configuration.md +++ b/docs/reference/configuration.md @@ -74,6 +74,32 @@ module's `_color` (and related `_active_color` / `_context_color` / raw `#{E:@thm_}` — set `@themux__color` (or the per-slot variant) as usual to override a single module without touching its role. +### Colour values + +Every colour prop (`_color`, `_active_color`, the per-slot +`_{leading,text}_color` overrides, the raw channel pins +`_{icon,text}_{fg,bg}`, and the window/pane accents) accepts three spellings: + +- **A role word** — `accent`, `info`, `notice`, `muted`, `alert`, `error`: + resolves through `@thm_role_`. +- **A palette word** — any `@thm_*` name the current theme defines (`peach`, + `mauve`, `surface_0`, ...): resolves through `@thm_`. The theme wins + over tmux's native colour names, so `red` is the palette red, not the + terminal's. +- **Raw tmux** — a hex (`#fab387`), a `colour255` name, `default`, or a + format (`#{E:@thm_peach}`, a draw-time `#{?...}` conditional): passes + through untouched. + +Words resolve at render time, so they track a theme switch just like the +role/palette references. An unknown word passes through raw — tmux ignores an +invalid colour silently, so a typo shows up as a missing colour, not an error. + +```sh +set -g @themux_git_color "mauve" # palette word +set -g @themux_session_color "accent" # role word +set -g @themux_time_color "#89dceb" # raw hex, same as always +``` + > [!WARNING] > **Breaking change:** module accent defaults changed in this release — see the > full list below. @@ -482,6 +508,21 @@ it there for you. Some modules ship their own per-module default: `git` sets whose variant displays the accent, and `cpu`/`ram` do the same for their live threshold colour (see below). +Visibility and escalation conditions share one grammar. A conditional +module's `@themux__when` (git/gitmux's in-repo gate, zoom's +`window_zoomed_flag`) and a stateful module's `@themux__active_when` +accept two preset words — `always` and `never` (alias `off`) — or any raw +tmux condition: a bare format variable (`client_prefix`), a `#{...}` +conditional, or a `#(...)` probe. tmux reads an unknown bare word as a format +variable that expands empty (false), so anything that is neither a preset nor +a real variable hides the module / never escalates. + +```sh +set -g @themux_git_when "always" # render even outside a repo +set -g @themux_gitmux_when "never" # hide entirely +set -g @themux_git_active_when "never" # keep the resting accent on dirty trees +``` + The `cpu`/`ram` threshold modules carry tmux-cpu's live level colour `#{_bg_color}` as their accent — green at rest, warming yellow → red as the value climbs. The default variants place it on both slots: the **icon** is `solid` diff --git a/docs/reference/status-line.md b/docs/reference/status-line.md index 50b929c..e1f0992 100644 --- a/docs/reference/status-line.md +++ b/docs/reference/status-line.md @@ -230,6 +230,11 @@ set -g @themux_git_color "#{E:@thm_role_accent}" # resting (clean) set -g @themux_git_active_color "#{E:@thm_role_notice}" # dirty ``` +**Visibility:** the module renders only inside a git work tree by default. +`@themux_git_when` accepts `always` (render everywhere), `never` (hide), or a +raw tmux condition — see +[colour values & conditions](./configuration.md#colour-values). + **Symbols:** each symbol has a glyph default and a plain-ASCII default, picked through the module's [leading content](./configuration.md#status-modules) cascade (`@themux_git_leading_show` > `@themux_module_leading_show` > diff --git a/modules/git.conf b/modules/git.conf index 07d8289..5f95f08 100644 --- a/modules/git.conf +++ b/modules/git.conf @@ -71,8 +71,10 @@ set -ogq "@themux_${MODULE_NAME}_active_when" "#{E:@_tmx_${MODULE_NAME}_dirty}" set -ogq "@themux_${MODULE_NAME}_state_target" "both" # Only render inside a git work tree (a cheap rev-parse, evaluated per refresh). +# -ogq so the gate is a user option: the when grammar (always/never presets or +# a raw tmux condition) can replace the in-repo default. set -gq "@_tmx_${MODULE_NAME}_in_repo" '#(git -C "#{pane_current_path}" rev-parse --is-inside-work-tree 2>/dev/null)' -set -gq "@themux_${MODULE_NAME}_when" "#{E:@_tmx_${MODULE_NAME}_in_repo}" +set -ogq "@themux_${MODULE_NAME}_when" "#{E:@_tmx_${MODULE_NAME}_in_repo}" # module_block.conf matches modules by substring over the joined status lines, # so the token "git" also matches inside "gitmux": a status line configured diff --git a/modules/gitmux.conf b/modules/gitmux.conf index efd8203..2214301 100644 --- a/modules/gitmux.conf +++ b/modules/gitmux.conf @@ -21,6 +21,8 @@ set -ogq "@themux_${MODULE_NAME}_text_bg" "#{E:@thm_crust}" # whole segment when outside a repo and, in a powerline run, lets neighbours # connect straight through instead of around an empty gitmux. set -gq "@_tmx_${MODULE_NAME}_in_repo" '#(git -C "#{pane_current_path}" rev-parse --is-inside-work-tree 2>/dev/null)' -set -gq "@themux_${MODULE_NAME}_when" "#{E:@_tmx_${MODULE_NAME}_in_repo}" +# -ogq so the gate is a user option (when grammar: always/never or a raw +# tmux condition), same as git.conf. +set -ogq "@themux_${MODULE_NAME}_when" "#{E:@_tmx_${MODULE_NAME}_in_repo}" source -F "#{d:current_file}/../utils/module_block.conf" diff --git a/run_tests.sh b/run_tests.sh index f949410..000c7df 100755 --- a/run_tests.sh +++ b/run_tests.sh @@ -22,3 +22,4 @@ script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" &>/dev/null && pwd -P) "${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 "$@" +"${script_dir}"/tests/harness.sh --test "${script_dir}"/tests/prop_grammar.sh --expected "${script_dir}"/tests/prop_grammar_expected.txt "$@" diff --git a/tests/prop_grammar.sh b/tests/prop_grammar.sh new file mode 100755 index 0000000..1bc27b9 --- /dev/null +++ b/tests/prop_grammar.sh @@ -0,0 +1,98 @@ +#!/usr/bin/env bash + +script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" &>/dev/null && pwd -P) +# shellcheck disable=SC1091 +source "${script_dir}/helpers.sh" + +src() { + tmux source "${script_dir}/../themux_options.conf" + tmux source "${script_dir}/../themux.conf" +} + +# The friendly prop grammar (utils/render_style.sh): colour props accept a +# theme role or palette word, when/active_when accept always/never presets — +# and every prop still accepts raw tmux semantics untouched. Assertions read +# the baked core/seg (deterministic under the default catppuccin_mocha +# palette: accent #a6e3a1, notice/peach #fab387, mauve #cba6f7, red #f38ba8). +tmux set -g @themux_status_line_1 "git" + +core_bg1() { tmux show -gqv @_tmx_module_git_core | grep -o 'bg=[^]]*' | head -1; } + +# A palette word resolves through @thm_. +tmux set -g @themux_git_color "mauve" +src +printf 'colour_palette_word [%s]\n' "$(core_bg1)" + +# A role word resolves through @thm_role_ (checked before the palette). +tmux set -g @themux_git_color "accent" +src +printf 'colour_role_word [%s]\n' "$(core_bg1)" + +# The theme wins over tmux's native colour names: red is @thm_red, not the +# terminal's red. +tmux set -g @themux_git_color "red" +src +printf 'colour_theme_wins [%s]\n' "$(core_bg1)" + +# Raw tmux values pass through untouched: a colour255 name and a hex. +tmux set -g @themux_git_color "colour123" +src +printf 'colour_raw_name [%s]\n' "$(core_bg1)" +tmux set -g @themux_git_color "#94e2d5" +src +printf 'colour_raw_hex [%s]\n' "$(core_bg1)" +tmux set -gu @themux_git_color + +# The active accent takes the same words. +tmux set -g @themux_git_active_color "mauve" +src +printf 'colour_active_word [%s]\n' "$(core_bg1)" +tmux set -gu @themux_git_active_color + +# when presets gate the standalone pill (@themux_module_git): `always` drops +# the gate entirely, `never` bakes a constant-false one, and any other value +# passes through raw (zoom's bare window_zoomed_flag idiom keeps working). +# git.conf sets its in-repo default with -ogq, so a user value set before the +# source survives. +tmux set -g @themux_git_when "always" +src +printf 'when_always_ungated ' +{ [ -z "$(tmux show -gqv @themux_module_git | { grep -oF '#{?#{E:@_tmx_git_in_repo},' || true; })" ]; } && printf "Y\n" || printf "n\n" + +tmux set -g @themux_git_when "never" +src +printf 'when_never_const_false ' +tmux show -gqv @themux_module_git | { grep -coF '#{?0,' || true; } + +tmux set -g @themux_git_when "window_zoomed_flag" +src +printf 'when_raw_variable ' +tmux show -gqv @themux_module_git | { grep -coF '#{?window_zoomed_flag,' || true; } + +tmux set -gu @themux_git_when +src +printf 'when_default_in_repo ' +tmux show -gqv @themux_module_git | { grep -coF '#{?#{E:@_tmx_git_in_repo},' || true; } + +# active_when presets: `never` pins the resting accent (the collapsed core +# carries no dirty switch), `always` pins the active one behind a +# constant-true format (a bare 1 would be read as an unknown format variable +# and stay false). +tmux set -g @themux_git_active_when "never" +src +printf 'active_never_resting [%s]\n' "$(core_bg1)" + +tmux set -g @themux_git_active_when "always" +src +printf 'active_always_pinned [%s]\n' "$(core_bg1)" +tmux set -gu @themux_git_active_when +src + +# The window renderer routes its accents through the same helper (one line, +# so grep -c is a presence flag: 1 resolved, 0 not). +tmux set -g @themux_window_leading_color "mauve" +src +printf 'window_colour_word ' +tmux show -gqv window-status-format | { grep -coF '#cba6f7' || true; } +tmux set -gu @themux_window_leading_color +src diff --git a/tests/prop_grammar_expected.txt b/tests/prop_grammar_expected.txt new file mode 100644 index 0000000..abac7ee --- /dev/null +++ b/tests/prop_grammar_expected.txt @@ -0,0 +1,13 @@ +colour_palette_word [bg=#{?#{E:@_tmx_git_dirty},#fab387,#cba6f7}] +colour_role_word [bg=#{?#{E:@_tmx_git_dirty},#fab387,#a6e3a1}] +colour_theme_wins [bg=#{?#{E:@_tmx_git_dirty},#fab387,#f38ba8}] +colour_raw_name [bg=#{?#{E:@_tmx_git_dirty},#fab387,colour123}] +colour_raw_hex [bg=#{?#{E:@_tmx_git_dirty},#fab387,#94e2d5}] +colour_active_word [bg=#{?#{E:@_tmx_git_dirty},#cba6f7,#a6e3a1}] +when_always_ungated Y +when_never_const_false 1 +when_raw_variable 1 +when_default_in_repo 1 +active_never_resting [bg=#a6e3a1] +active_always_pinned [bg=#{?#{==:1,1},#fab387,#a6e3a1}] +window_colour_word 1 diff --git a/utils/layout.sh b/utils/layout.sh index 7adc09d..209474a 100755 --- a/utils/layout.sh +++ b/utils/layout.sh @@ -38,6 +38,10 @@ interp() { # $1 module, $2 string -> the string with the module's plugin literal "$fn" "$2" } +# Shared prop grammar (themux_when_cond for the visibility presets). +# shellcheck source=render_style.sh +. "$(dirname "$0")/render_style.sh" + windows_block=$(tmux show -gqv @_tmx_fmt_windows) # Control-byte marker for a zone-aware notch (auto), matching utils/module_render.sh @@ -197,7 +201,7 @@ powerline_run() { local shown="0" shown_fixed=1 # shown: anything visible yet? (fixed while constant) for i in "${!mods[@]}"; do m="${mods[i]}"; c="${conns[i]}" - cond=$(tmux show -gqv "@themux_${m}_when") + cond=$(themux_when_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" "$align") @@ -298,7 +302,7 @@ expand_zone() { G) read -ra mm <<<"${it_mods[$1]}" for m in "${mm[@]}"; do - cond=$(tmux show -gqv "@themux_${m}_when") + cond=$(themux_when_cond "$(tmux show -gqv "@themux_${m}_when")") [ -z "$cond" ] && { printf '1'; return; } if [ -z "$vis" ]; then vis="$cond"; else vis="#{||:${vis},${cond}}"; fi done diff --git a/utils/module_render.sh b/utils/module_render.sh index 93bbfe1..9224e95 100755 --- a/utils/module_render.sh +++ b/utils/module_render.sh @@ -84,6 +84,20 @@ text_acc=$(bake "$text_acc" "$text_acc_E") lead_aacc=$(bake "$lead_aacc" "$lead_aacc_E") text_aacc=$(bake "$text_aacc" "$text_aacc_E") +# Friendly prop grammar (render_style.sh): accents and raw channel pins accept +# a theme role/palette word (notice, peach), active_when accepts always/never. +# Raw tmux values — hex, colour255, formats, bare variables — pass through. +lead_acc=$(themux_colour "$lead_acc") +text_acc=$(themux_colour "$text_acc") +lead_aacc=$(themux_colour "$lead_aacc") +text_aacc=$(themux_colour "$text_aacc") +icon_bg_ov=$(themux_colour "$icon_bg_ov") icon_fg_ov=$(themux_colour "$icon_fg_ov") +text_bg_ov=$(themux_colour "$text_bg_ov") text_fg_ov=$(themux_colour "$text_fg_ov") +active_when=$(themux_active_cond "$active_when") +# The standalone-pill gate below and layout.sh's powerline splice both read +# the same normalized vocabulary (always -> unconditional, never -> hidden). +when=$(themux_when_cond "$when") + # Select leading content before routing state. `auto` state needs to know if the # leading slot exists: visible leading keeps state on leading; hidden leading # moves state to text. Invalid leading values intentionally fall back to icons. diff --git a/utils/pane_render.sh b/utils/pane_render.sh index df7a0f0..c29703d 100755 --- a/utils/pane_render.sh +++ b/utils/pane_render.sh @@ -43,12 +43,13 @@ case "$shape" in esac # Per-part accents: a resting accent (inactive pane) and an active accent (active -# pane). surface/crust/fg are static. -lead_base=$(expand "#{E:@themux_pane_leading_color}") -lead_act=$(expand "#{E:@themux_pane_leading_active_color}") -txt_base=$(expand "#{E:@themux_pane_text_color}") -txt_act=$(expand "#{E:@themux_pane_text_active_color}") -surface=$(expand "#{E:@themux_pane_background_color}") +# pane). surface/crust/fg are static. themux_colour resolves a friendly theme +# role/palette word (notice, peach); raw tmux values pass through. +lead_base=$(themux_colour "$(expand "#{E:@themux_pane_leading_color}")") +lead_act=$(themux_colour "$(expand "#{E:@themux_pane_leading_active_color}")") +txt_base=$(themux_colour "$(expand "#{E:@themux_pane_text_color}")") +txt_act=$(themux_colour "$(expand "#{E:@themux_pane_text_active_color}")") +surface=$(themux_colour "$(expand "#{E:@themux_pane_background_color}")") crust=$(expand "#{@thm_crust}") fg=$(expand "#{@thm_fg}") diff --git a/utils/render_style.sh b/utils/render_style.sh index 0b9d73b..853f4ae 100755 --- a/utils/render_style.sh +++ b/utils/render_style.sh @@ -40,6 +40,53 @@ themux_notch_mode() { # $1 raw -> gt | lt | auto | off case "$1" in '>') echo gt ;; '<') echo lt ;; auto|on) echo auto ;; *) echo off ;; esac } +# Map a friendly colour word to the current theme. A bare lowercase word +# resolves through @thm_role_ (accent, notice, ...) then @thm_ +# (peach, surface_0, ...) — the theme wins over tmux's native colour names, +# so `red` is the palette red, not the terminal's. Anything else — a hex, a +# colour255 name, `default`, a #{...} format, an unknown word — passes +# through untouched, keeping raw tmux semantics assignable from outside. +# One display round-trip per call, at render time only (bake tracks theme +# switches because a theme change re-renders). +themux_colour() { # $1 raw colour value -> theme colour or the value itself + case "$1" in + '' | *[!a-z0-9_]*) printf '%s' "$1"; return ;; + esac + local v + v=$(tmux display -p "#{?#{!=:#{@thm_role_$1},},#{E:@thm_role_$1},#{?#{!=:#{@thm_$1},},#{E:@thm_$1},}}") + printf '%s' "${v:-$1}" +} + +# Normalize a visibility condition (@themux__when). Two preset words: +# `always` joins the module unconditionally (empty condition) and `never` +# (alias `off`) hides it (a constant-false condition). Anything else passes +# through raw — a bare format variable (window_zoomed_flag), a #{...} +# conditional, or a #(...) probe. tmux looks a bare unknown word up as a +# format variable (empty -> false), so an unmapped word silently hides the +# module; the presets make the two useful constants sayable without knowing +# that. +themux_when_cond() { # $1 raw when value -> normalized condition + case "$1" in + always) printf '' ;; + never | off) printf '0' ;; + *) printf '%s' "$1" ;; + esac +} + +# Normalize an escalation condition (@themux__active_when): `always` +# pins the active appearance (a constant-true format — a bare 1 would be +# looked up as a format variable and read false), `never` (alias `off`) pins +# the resting one (the empty condition collapses the per-channel switch in +# module_render.sh's chan). Anything else passes through raw, e.g. session's +# bare `client_prefix`. +themux_active_cond() { # $1 raw active_when value -> normalized condition + case "$1" in + always) printf '#{==:1,1}' ;; + never | off) printf '' ;; + *) printf '%s' "$1" ;; + esac +} + # Parse a @themux_*_padding value into four side pads: # "N" -> "N N|N N" # " [leading-right]| [text-right]" diff --git a/utils/window_render.sh b/utils/window_render.sh index 5e2207e..ddc61a4 100755 --- a/utils/window_render.sh +++ b/utils/window_render.sh @@ -33,7 +33,7 @@ read -r pleft pright tleft tright <<<"$(pad_parse "$(themux_prop window padding) crust=$(expand "#{@thm_crust}") fg=$(expand "#{@thm_fg}") flags=$(expand "#{@_tmx_w_flags}") -surface=$(expand "#{E:@themux_window_background_color}") +surface=$(themux_colour "$(expand "#{E:@themux_window_background_color}")") # Shape glyphs (octal UTF-8). squared has none — the block padding is its edge. case "$shape" in @@ -105,12 +105,12 @@ render_side() { 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}") - txt_acc=$(expand "#{E:@themux_window_text_color}") + lead_acc=$(themux_colour "$(expand "#{E:@themux_window_leading_color}")") + txt_acc=$(themux_colour "$(expand "#{E:@themux_window_text_color}")") else lvar="$leading_active" tvar="$name_active" - lead_acc=$(expand "#{E:@themux_window_leading_active_color}") - txt_acc=$(expand "#{E:@themux_window_text_active_color}") + lead_acc=$(themux_colour "$(expand "#{E:@themux_window_leading_active_color}")") + txt_acc=$(themux_colour "$(expand "#{E:@themux_window_text_active_color}")") fi resolve_style "$lvar" "$lead_acc" "$surface" "$crust" "$fg" ibg="$RS_BG" ifg="$RS_FG"