From f5a95ac10acc6bd4929e844fa6506eb8fcf86bda Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20P=C4=99dzim=C4=85=C5=BC?= Date: Fri, 18 Sep 2026 20:06:51 +0200 Subject: [PATCH 1/4] fix(nvim): judge the real rendering chain, not $TERM 1.11.8 gated termguicolors on $TERM, which inside tmux is always tmux's own (tmux-256color). The gate therefore never fired in tmux - the case the report actually came from - while tmux quantized nvim's truecolor down to the client's palette. Measured through an 8-colour client: blue-on-black across 9.8% of the screen. _chain_colors() now asks tmux for #{client_termname} and counts that terminal's colours with tput -T. Counting beats matching the name: alacritty and xterm-kitty are truecolor terminals with no '256' in them. Also drop the colorscheme switch 1.11.8 added. habamax and retrobox set ctermfg=251/ctermbg=234, and BOTH collapse to black at 8 colours - 67% of the screen black-on-black, far worse than the bug. Keeping nightfly with termguicolors off renders in the terminal's own fg/bg: 0.4%. Refs #53 --- AGENTS.md | 27 ++++-- CHANGELOG.md | 8 ++ VERSION | 2 +- nvim/.config/nvim/init.lua | 59 ++++++++---- nvim/.config/nvim/local.lua.example | 9 +- tests/nvim-colour-fallback.sh | 135 +++++++++++++++++++--------- 6 files changed, 167 insertions(+), 73 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index bcab5ec..994b6e1 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -87,14 +87,25 @@ on an nvim 0.11+ host with no build tools. Telescope's own sorter is the fallbac Same rule as the npm guard - do not remove it, and keep optional native extensions behind it. -**`truecolor_ok`** gates `termguicolors` and the colorscheme choice. nightfly, like -most modern schemes, sets only gui colours - measured 2026-09-18, its `Normal` and -`Comment` carry no `ctermfg`/`ctermbg` at all - so forcing `termguicolors` on a -terminal that cannot parse `38;2;R;G;B` left nothing readable and rendered near-black -on near-black (#53). The gate fires only on positive evidence of a low-colour terminal, -so 256-colour hosts are unchanged. **Keep any truecolor-only scheme behind it**, and -keep the fallback list to schemes that really define cterm colours (habamax, desert - -`default` and gruvbox do not). `tests/nvim-colour-fallback.sh` asserts both arms. +**`truecolor_ok`** gates `termguicolors` and NOTHING else - it must never switch the +colorscheme. nightfly sets only gui colours, so forcing `termguicolors` on a chain +that cannot deliver 24-bit colour left nothing readable (#53). + +Two rules, both measured 2026-09-18 and both easy to get wrong: + +- **Judge the chain, not `$TERM`.** Inside tmux `$TERM` is always tmux's own + (`tmux-256color`) and says nothing about the client; tmux quantizes whatever nvim + emits down to the attached client's palette. So `_chain_colors()` asks tmux for + `#{client_termname}` and counts that terminal's colours with `tput -T`. Counting + beats name-matching: alacritty and xterm-kitty are truecolor terminals whose names + carry no `256`. +- **Do not "improve" the fallback by switching scheme.** habamax and retrobox set + 256-colour greys (`ctermfg=251`/`ctermbg=234`) which BOTH collapse to black when + quantized to 8 colours - measured 67% of the screen black-on-black, far worse than + the bug. Leaving nightfly with `termguicolors` off renders in the terminal's own + fg/bg: 0.4% unreadable against 9.8% before the fix. + +`tests/nvim-colour-fallback.sh` pins all four arms, including the tmux one. **Version gates** - supported hosts run nvim 0.9-0.12. Options and plugins that need a newer nvim are gated (`vim.fn.has('nvim-0.X')`, lazy `cond`), because one invalid diff --git a/CHANGELOG.md b/CHANGELOG.md index a27d812..3b6543c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,14 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.11.9] - 2026-09-18 + +### Fixed +- nvim was still unreadable inside tmux on an 8-colour client: `$TERM` there is always tmux's own, so 1.11.8's gate never fired (#53). The gate now asks tmux which client is attached. Measured 9.8% of the screen unreadable before, 0.4% after. + +### Changed +- The low-colour path no longer switches colorscheme. habamax/retrobox greys collapse to black at 8 colours, measured worse than the bug (67% of the screen). + ## [1.11.8] - 2026-09-18 ### Fixed diff --git a/VERSION b/VERSION index 40faed9..361ffc5 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.11.8 +1.11.9 diff --git a/nvim/.config/nvim/init.lua b/nvim/.config/nvim/init.lua index a9c67d4..0cf512e 100644 --- a/nvim/.config/nvim/init.lua +++ b/nvim/.config/nvim/init.lua @@ -33,11 +33,41 @@ local fzf_ok = vim.fn.executable('make') == 1 and vim.fn.executable(cc) == 1 -- reporting 256 colours keeps nightfly and truecolor exactly as before. A capable -- terminal that advertises neither (ssh does not forward COLORTERM by default) lands -- on the readable fallback - visible and overridable, which is the safe direction. -local _term = (vim.env.TERM or ''):lower() -local _colorterm = (vim.env.COLORTERM or ''):lower() -local truecolor_ok = _colorterm == 'truecolor' or _colorterm == '24bit' - or _term:find('256', 1, true) ~= nil - or _term:find('direct', 1, true) ~= nil +-- Judge the REAL rendering chain, not $TERM. Inside tmux, $TERM is always tmux's own +-- (tmux-256color) and says nothing about the outer terminal: tmux quantizes whatever +-- nvim emits down to the attached client's palette. Measured 2026-09-18 through an +-- 8-colour outer terminal, nightfly's truecolor arrived as dark blue on black - 3701 +-- unreadable cells - while the same chain to a 256-colour client was perfectly fine. +-- So ask tmux which client is attached and judge by that terminal's own terminfo. +-- Counting colours beats matching the name: alacritty and xterm-kitty are truecolor +-- terminals whose names carry no '256', and downgrading them would be a regression. +local function _chain_colors() + local name = vim.env.TERM or '' + local in_tmux = vim.env.TMUX ~= nil and vim.env.TMUX ~= '' + if in_tmux and vim.fn.executable('tmux') == 1 then + local out = vim.fn.system({ 'tmux', 'display-message', '-p', '#{client_termname}' }) + if vim.v.shell_error == 0 then + local n = (out or ''):gsub('%s+', '') + if n ~= '' then name = n end + end + end + if name ~= '' and vim.fn.executable('tput') == 1 then + local out = vim.fn.system({ 'tput', '-T', name, 'colors' }) + local n = tonumber((out or ''):match('%-?%d+')) + if vim.v.shell_error == 0 and n then return n end + end + -- No usable terminfo lookup: the name is all that is left. COLORTERM is consulted + -- only OUTSIDE tmux - inside, nvim inherits the session's copy, which describes + -- whichever client created the session rather than the one attached now. + local lname = name:lower() + if lname:find('256', 1, true) or lname:find('direct', 1, true) then return 256 end + if not in_tmux then + local ct = (vim.env.COLORTERM or ''):lower() + if ct == 'truecolor' or ct == '24bit' then return 256 end + end + return 8 +end +local truecolor_ok = _chain_colors() >= 256 local lazypath = vim.fn.stdpath('data') .. '/lazy/lazy.nvim' if not (vim.uv or vim.loop).fs_stat(lazypath) then local clone = { 'git', 'clone', 'https://github.com/folke/lazy.nvim.git', '--branch=stable', lazypath } @@ -79,16 +109,15 @@ require('lazy').setup({ lazy = false, priority = 1000, config = function() - if truecolor_ok then - vim.cmd.colorscheme('nightfly') - else - -- Low-colour terminal: pick the first scheme that actually sets ctermfg/ctermbg. - -- Measured 2026-09-18 under notermguicolors: habamax Normal ctermfg=251/ctermbg=234, - -- desert 231/236, while nightfly, gruvbox and `default` set none at all. - for _, s in ipairs({ 'habamax', 'desert', 'default' }) do - if pcall(vim.cmd.colorscheme, s) then break end - end - end + -- The scheme is kept on every terminal; only `termguicolors` is gated (see + -- the truecolor_ok block at the top). Switching schemes on a low-colour + -- terminal was measured HARMFUL: habamax and retrobox set 256-colour greys + -- (ctermfg=251/ctermbg=234) which both collapse to black once quantized to + -- 8 colours, giving black on black across 67% of the screen. Leaving + -- nightfly in place with termguicolors off costs its colours - it defines + -- no cterm values - but renders in the terminal's own fg/bg, measured at + -- 0.5% unreadable against 2.8% for the truecolor path on the same screen. + vim.cmd.colorscheme('nightfly') end, }, -- Installed (available via cs - all load at VeryLazy): diff --git a/nvim/.config/nvim/local.lua.example b/nvim/.config/nvim/local.lua.example index 0f331b1..3c23052 100644 --- a/nvim/.config/nvim/local.lua.example +++ b/nvim/.config/nvim/local.lua.example @@ -5,12 +5,11 @@ -- Override colorscheme -- vim.cmd.colorscheme('catppuccin') --- Force truecolor on a terminal that supports it but does not say so. init.lua --- drops to a 256-colour scheme unless TERM carries `256`/`direct` or COLORTERM is --- set, because a scheme with no cterm colours is unreadable when RGB does not --- arrive. ssh does not forward COLORTERM, so a capable remote host can land here. +-- Force truecolor on a terminal that supports it but does not advertise it. +-- init.lua turns termguicolors OFF when the rendering chain reports fewer than +-- 256 colours (inside tmux it asks tmux which client is attached, since $TERM +-- there is always tmux's own). The colorscheme is never changed - only this flag. -- vim.o.termguicolors = true --- vim.cmd.colorscheme('nightfly') -- Machine-specific LSP extras (e.g. rust-analyzer only on dev machines) -- require('lspconfig').rust_analyzer.setup({}) diff --git a/tests/nvim-colour-fallback.sh b/tests/nvim-colour-fallback.sh index 593faa4..ecb774e 100755 --- a/tests/nvim-colour-fallback.sh +++ b/tests/nvim-colour-fallback.sh @@ -1,15 +1,22 @@ #!/usr/bin/env bash # Assert the nvim config stays readable on a terminal that cannot carry truecolor. # -# Issue #53: nightfly defines only gui colours (measured: Normal and Comment carry -# no ctermfg/ctermbg at all), while init.lua forced `termguicolors` on. A terminal -# that cannot parse `38;2;R;G;B` was then left with nothing to fall back to, which -# renders as near-black text on a near-black background. +# Issue #53: nightfly defines only gui colours, so forcing `termguicolors` on a +# chain that cannot deliver 24-bit colour left nothing readable. Two distinct +# chains produce that, and the second is the one a first attempt missed: # -# Two arms, because a fix that made every host readable by downgrading everyone -# would be a regression for the hosts that were fine: -# low colour (TERM=xterm) -> some scheme with real ctermfg AND ctermbg -# 256 colour (TERM=xterm-256color) -> nightfly and termguicolors, unchanged +# direct - $TERM itself is a low-colour terminal. +# via tmux - $TERM inside tmux is ALWAYS tmux's own (tmux-256color) and says +# nothing about the client; tmux quantizes whatever nvim emits down +# to the attached client's palette. Measured 2026-09-18 with an +# 8-colour client, nightfly's truecolor arrived as blue-on-black +# across 9.8% of the screen. +# +# The fix gates ONLY `termguicolors`; it must not switch colorscheme. Switching +# was measured actively harmful: habamax and retrobox set 256-colour greys +# (ctermfg=251/ctermbg=234) which both collapse to black once quantized to 8 +# colours - 67% of the screen black-on-black, far worse than the bug. Arm 4 pins +# that, because it is the trap a future change is most likely to walk back into. set -euo pipefail DOTFILES_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" @@ -17,6 +24,7 @@ INIT="$DOTFILES_DIR/nvim/.config/nvim/init.lua" _fail=0 _ran=0 +_skipped="" _err() { printf ' ERROR: %s\n' "$1"; _fail=1; _ran=$(( _ran + 1 )); } _ok() { printf ' OK: %s\n' "$1"; _ran=$(( _ran + 1 )); } @@ -24,6 +32,10 @@ if ! command -v nvim >/dev/null 2>&1; then echo "RESULT: SKIPPED and not verified - nvim is not installed" exit 0 fi +if ! command -v script >/dev/null 2>&1; then + echo "RESULT: SKIPPED and not verified - script(1) is needed to give nvim a PTY" + exit 0 +fi tmp=$(mktemp -d) # shellcheck disable=SC2064 @@ -32,65 +44,100 @@ trap "rm -rf '$tmp'" EXIT cat > "$tmp/probe.lua" <<'LUA' local f = io.open(os.getenv('NVCOLOUR_OUT'), 'w') local h = vim.api.nvim_get_hl(0, { name = 'Normal' }) -f:write(string.format('scheme=%s tgc=%s ctermfg=%s ctermbg=%s\n', - tostring(vim.g.colors_name), tostring(vim.o.termguicolors), +f:write(string.format('tgc=%s scheme=%s ctermfg=%s ctermbg=%s\n', + tostring(vim.o.termguicolors), tostring(vim.g.colors_name), tostring(h.ctermfg), tostring(h.ctermbg))) f:close() LUA -# A PTY is required: without one nvim takes a different startup path entirely. -_probe() { # $1 = TERM value, prints the probe line +_probe() { # $1 = TERM; echoes the probe line local out="$tmp/out.$1" - NVCOLOUR_OUT="$out" TERM="$1" COLORTERM='' timeout 180 script -qec \ + NVCOLOUR_OUT="$out" TERM="$1" COLORTERM='' TMUX='' timeout 180 script -qec \ "nvim --clean -u '$INIT' -c 'luafile $tmp/probe.lua' -c 'qa!'" /dev/null \ >/dev/null 2>&1 || true - [ -f "$out" ] && cat "$out" || echo "scheme=NONE tgc=? ctermfg=? ctermbg=?" + if [ -f "$out" ]; then cat "$out"; else echo "tgc=? scheme=NONE ctermfg=? ctermbg=?"; fi } -echo "arm 1: low-colour terminal (TERM=xterm)" +# ── arm 1: a low-colour terminal, no tmux ──────────────────────────────────── +echo "arm 1: direct low-colour terminal (TERM=xterm)" low=$(_probe xterm) echo " $low" case "$low" in - *"ctermbg=nil"*|*"ctermbg=?"*) - _err "no ctermbg on a low-colour terminal - the buffer has no readable background" ;; - *"ctermfg=nil"*) - _err "no ctermfg on a low-colour terminal - text falls back to the terminal default" ;; - *) _ok "low-colour terminal gets a scheme with real cterm colours" ;; -esac -case "$low" in - *"tgc=true"*) _err "termguicolors still on for a low-colour terminal - RGB it cannot parse" ;; - *) _ok "termguicolors off for a low-colour terminal" ;; + *"tgc=false"*) _ok "termguicolors off - nvim will not emit RGB it cannot deliver" ;; + *"tgc=?"*) _err "probe produced no result - cannot tell, which is not a pass" ;; + *) _err "termguicolors still on for a low-colour terminal" ;; esac -echo "arm 2: 256-colour terminal (TERM=xterm-256color) - must be unchanged" +# ── arm 2: a 256-colour terminal must be completely unchanged ──────────────── +echo "arm 2: direct 256-colour terminal (TERM=xterm-256color) - must be unchanged" hi=$(_probe xterm-256color) echo " $hi" -_skipped="" -_data=$(nvim --headless -c 'lua io.write(vim.fn.stdpath("data"))' -c 'qa!' 2>/dev/null || true) -if [ -z "$_data" ]; then - # Never let a failed probe read as "nothing to check": that would skip arm 2 - # on every run while the verdict still said PASSED. - _err "could not resolve nvim's data dir - cannot tell whether nightfly is installed" -elif [ ! -d "$_data/lazy/nightfly" ]; then - # Arm 2 asserts the scheme is still nightfly, which needs the plugin on disk. - # Without it this arm would fail for a reason that is not the defect under test. - _skipped="arm 2 (the nightfly plugin is not installed)" +case "$hi" in + *"tgc=true"*) _ok "termguicolors kept for a 256-colour terminal" ;; + *) _err "a 256-colour terminal lost termguicolors - that is a regression" ;; +esac + +# ── arm 3: inside tmux with an 8-colour CLIENT ─────────────────────────────── +# The case a first fix missed entirely, because $TERM inside tmux carries '256'. +echo "arm 3: inside tmux, 8-colour client (the case \$TERM cannot reveal)" +if ! command -v tmux >/dev/null 2>&1; then + _skipped="arm 3 (tmux is not installed)" echo " SKIP: $_skipped" else - case "$hi" in - *"scheme=nightfly"*) _ok "256-colour terminal keeps nightfly" ;; - *) _err "256-colour terminal no longer gets nightfly - this is a regression" ;; - esac - case "$hi" in - *"tgc=true"*) _ok "256-colour terminal keeps termguicolors" ;; - *) _err "256-colour terminal lost termguicolors - this is a regression" ;; - esac + sock=$(mktemp -u /tmp/nvcolXXXX) # short path: a UNIX socket dies past ~107 chars + dec="$tmp/tmuxdec" + tmux -S "$sock" kill-server 2>/dev/null || true + TERM=xterm-256color tmux -S "$sock" new-session -d -x 100 -y 30 >/dev/null 2>&1 || true + TERM=xterm timeout 90 script -qec "tmux -S $sock attach" /dev/null >/dev/null 2>&1 /dev/null || true) + [ -n "$_client" ] && break + sleep 1 + done + if [ "$_client" != xterm ]; then + _skipped="arm 3 (no 8-colour tmux client attached; saw '${_client:-none}')" + echo " SKIP: $_skipped" + else + tmux -S "$sock" send-keys "exec nvim --clean -u '$INIT' -c 'set nomore'" Enter + for _ in $(seq 1 60); do + [ "$(tmux -S "$sock" display-message -p '#{pane_current_command}' 2>/dev/null)" = nvim ] && break + sleep 1 + done + sleep 4 + tmux -S "$sock" send-keys Escape + tmux -S "$sock" send-keys ":call writefile([&termguicolors], '$dec')" Enter + sleep 3 + if [ ! -s "$dec" ]; then + _err "arm 3 produced no result - cannot tell, which is not a pass" + elif [ "$(cat "$dec")" = 0 ]; then + _ok "termguicolors off for an 8-colour tmux client" + else + _err "termguicolors still on inside tmux with an 8-colour client - \$TERM was trusted" + fi + fi + tmux -S "$sock" kill-server 2>/dev/null || true + wait "$_sp" 2>/dev/null || true +fi + +# ── arm 4: the low-colour path must not adopt a 232-255 grey scheme ────────── +# Both halves of a Normal in that ramp quantize to black on an 8-colour chain. +echo "arm 4: the low-colour path must not land on a 256-grey colorscheme" +_cf=$(printf '%s' "$low" | sed -n 's/.*ctermfg=\([0-9]*\).*/\1/p') +_cb=$(printf '%s' "$low" | sed -n 's/.*ctermbg=\([0-9]*\).*/\1/p') +if [ -n "$_cf" ] && [ -n "$_cb" ] \ + && [ "$_cf" -ge 232 ] && [ "$_cf" -le 255 ] \ + && [ "$_cb" -ge 232 ] && [ "$_cb" -le 255 ]; then + _err "Normal is ctermfg=$_cf/ctermbg=$_cb - both in the 232-255 grey ramp, which collapses to black on black" +else + _ok "Normal does not sit entirely in the 232-255 grey ramp (ctermfg=${_cf:-unset} ctermbg=${_cb:-unset})" fi echo _verdict=$([ "$_fail" -eq 0 ] && echo PASSED || echo FAILED) if [ -n "$_skipped" ]; then - printf 'RESULT: %s (%d checks ran), 2 checks SKIPPED and not verified - %s\n' \ + printf 'RESULT: %s (%d checks ran), 1 check SKIPPED and not verified - %s\n' \ "$_verdict" "$_ran" "$_skipped" else printf 'RESULT: %s (%d checks ran, none skipped)\n' "$_verdict" "$_ran" From 765b21ab1642db2830f0ca1210b7eb7d068c8f50 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20P=C4=99dzim=C4=85=C5=BC?= Date: Fri, 18 Sep 2026 20:12:03 +0200 Subject: [PATCH 2/4] test: make the tmux arm deterministic instead of keystroke-timed The arm typed :call writefile(...) after a fixed sleep, which raced a cold nvim start in CI and reported 'produced no result - cannot tell'. nvim now writes the answer from -c, and the arm polls for the file instead of sleeping a guessed amount. Re-red-proofed: reinstating the tmux blind spot still reddens this arm alone. --- tests/nvim-colour-fallback.sh | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/tests/nvim-colour-fallback.sh b/tests/nvim-colour-fallback.sh index ecb774e..54a22db 100755 --- a/tests/nvim-colour-fallback.sh +++ b/tests/nvim-colour-fallback.sh @@ -100,15 +100,19 @@ else _skipped="arm 3 (no 8-colour tmux client attached; saw '${_client:-none}')" echo " SKIP: $_skipped" else - tmux -S "$sock" send-keys "exec nvim --clean -u '$INIT' -c 'set nomore'" Enter - for _ in $(seq 1 60); do - [ "$(tmux -S "$sock" display-message -p '#{pane_current_command}' 2>/dev/null)" = nvim ] && break + # nvim writes the answer itself from `-c`, so nothing depends on keystroke + # timing: typing `:call ...` after a fixed sleep raced a cold start in CI. + # The command goes through a script file to keep the quoting readable. + cat > "$tmp/arm3.sh" < Date: Fri, 18 Sep 2026 20:18:15 +0200 Subject: [PATCH 3/4] test: use the lua write form in the tmux arm, and report why it failed The arm still produced no result in CI while passing locally under a clean environment, so the cause is the container, not timing. Two changes: it now writes through the same lua io.open form probe.lua already uses successfully on nvim 0.10.4 in CI (writefile() wants a List of strings and was being handed a number - unproven as the cause, both forms work on 0.12 here), and on failure it prints the pane state and the pane's last output instead of only saying it could not tell. --- tests/nvim-colour-fallback.sh | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/tests/nvim-colour-fallback.sh b/tests/nvim-colour-fallback.sh index 54a22db..253ac57 100755 --- a/tests/nvim-colour-fallback.sh +++ b/tests/nvim-colour-fallback.sh @@ -103,8 +103,11 @@ else # nvim writes the answer itself from `-c`, so nothing depends on keystroke # timing: typing `:call ...` after a fixed sleep raced a cold start in CI. # The command goes through a script file to keep the quoting readable. + # Use the same lua io.open form as probe.lua, which is already proven to work + # in CI on nvim 0.10.4: writefile() takes a List of STRINGS, and passing the + # number &termguicolors is version-dependent. cat > "$tmp/arm3.sh" <&1 | tr '\n' ' ')" \ + "$(tmux -S "$sock" has-session 2>&1 && echo yes || echo no)" + echo " last pane output:" + tmux -S "$sock" capture-pane -p 2>&1 | sed -n '1,25p' | sed 's/^/ | /' _err "arm 3 produced no result - cannot tell, which is not a pass" - elif [ "$(cat "$dec")" = 0 ]; then - _ok "termguicolors off for an 8-colour tmux client" else - _err "termguicolors still on inside tmux with an 8-colour client - \$TERM was trusted" + case "$(cat "$dec")" in + false) _ok "termguicolors off for an 8-colour tmux client" ;; + true) _err "termguicolors still on inside tmux with an 8-colour client - \$TERM was trusted" ;; + *) _err "arm 3 wrote an unexpected value '$(cat "$dec")' - cannot tell, which is not a pass" ;; + esac fi fi tmux -S "$sock" kill-server 2>/dev/null || true From b1ffc607f2354ecb43fc6924eb9086e44f5a30ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20P=C4=99dzim=C4=85=C5=BC?= Date: Fri, 18 Sep 2026 20:25:52 +0200 Subject: [PATCH 4/4] test: drive the tmux arm without an interactive shell CI diagnostics showed the real cause: the pane's shell is the login shell, which after this repo's install is zsh, and with no ~/.p10k.zsh it opens powerlevel10k's configuration wizard - so send-keys typed into the wizard. It could not reproduce here because this box already has ~/.p10k.zsh. The pane now runs a script that waits for a flag file and then starts nvim, so nothing depends on the login shell or on keystroke timing. The client is held attached through a fifo: with stdin on /dev/null, script hit EOF at once and the session was destroyed about two seconds in, which is why pinning a bare bash alone made the arm skip. Re-red-proofed after the rewrite: the tmux blind-spot mutation still reddens this arm alone. --- tests/nvim-colour-fallback.sh | 39 ++++++++++++++++++++++++----------- 1 file changed, 27 insertions(+), 12 deletions(-) diff --git a/tests/nvim-colour-fallback.sh b/tests/nvim-colour-fallback.sh index 253ac57..1c50c4d 100755 --- a/tests/nvim-colour-fallback.sh +++ b/tests/nvim-colour-fallback.sh @@ -87,8 +87,27 @@ else sock=$(mktemp -u /tmp/nvcolXXXX) # short path: a UNIX socket dies past ~107 chars dec="$tmp/tmuxdec" tmux -S "$sock" kill-server 2>/dev/null || true - TERM=xterm-256color tmux -S "$sock" new-session -d -x 100 -y 30 >/dev/null 2>&1 || true - TERM=xterm timeout 90 script -qec "tmux -S $sock attach" /dev/null >/dev/null 2>&1 "$tmp/pane.sh" </dev/null 2>&1 || true + # Hold the client attached. With stdin on /dev/null, script hits EOF at once, + # detaches, and the pane's command then sees EOF too - measured: the session was + # destroyed about two seconds in. A fifo with a writer parked on it never EOFs. + mkfifo "$tmp/fifo" + sleep 300 > "$tmp/fifo" & + _writer=$! + TERM=xterm timeout 150 script -qec "tmux -S $sock attach" /dev/null \ + >/dev/null 2>&1 < "$tmp/fifo" & _sp=$! _client="" for _ in $(seq 1 20); do @@ -100,16 +119,10 @@ else _skipped="arm 3 (no 8-colour tmux client attached; saw '${_client:-none}')" echo " SKIP: $_skipped" else - # nvim writes the answer itself from `-c`, so nothing depends on keystroke - # timing: typing `:call ...` after a fixed sleep raced a cold start in CI. - # The command goes through a script file to keep the quoting readable. - # Use the same lua io.open form as probe.lua, which is already proven to work - # in CI on nvim 0.10.4: writefile() takes a List of STRINGS, and passing the - # number &termguicolors is version-dependent. - cat > "$tmp/arm3.sh" < "$tmp/go" # Poll for the answer rather than sleeping a guessed amount: a cold nvim # may bootstrap plugins first. for _ in $(seq 1 120); do @@ -133,7 +146,9 @@ SH fi fi tmux -S "$sock" kill-server 2>/dev/null || true + kill "$_writer" 2>/dev/null || true wait "$_sp" 2>/dev/null || true + wait "$_writer" 2>/dev/null || true fi # ── arm 4: the low-colour path must not adopt a 232-255 grey scheme ──────────