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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .github/workflows/install.yml
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,13 @@ jobs:
- name: Run test suite (minimal)
run: su -s /bin/bash -c 'HOME=/home/testuser; export HOME; export TERM=xterm-256color; cd ~/dotfiles && bash test.sh minimal' testuser

- name: Readable colours on a terminal without truecolor (#53)
run: |
# The probe needs a PTY, so `script` must exist or the test would skip
# on every cell and prove nothing while still reporting a pass.
command -v script >/dev/null || { echo "script(1) missing - the colour test cannot run"; exit 1; }
su -s /bin/bash -c 'HOME=/home/testuser; export HOME; cd ~/dotfiles && bash tests/nvim-colour-fallback.sh' testuser

git-partial-clone-gate:
name: "git without partial clone @ Ubuntu 16.04"
runs-on: ubuntu-latest
Expand Down
9 changes: 9 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,15 @@ 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.

**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
option value aborts the rest of init.lua. Parser installs are gated on nvim 0.12,
Expand Down
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@ 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.8] - 2026-09-18

### Fixed
- nvim rendered near-black on near-black on terminals without truecolor (#53). Such terminals now get `habamax` and `notermguicolors`; 256-colour terminals are unchanged. Force the old behaviour in `~/.config/nvim/local.lua`.

## [1.11.7] - 2026-09-17

### Added
Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
1.11.7
1.11.8
26 changes: 24 additions & 2 deletions nvim/.config/nvim/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,19 @@ end
-- telescope key was dead on an nvim 0.11+ host with no build tools. Telescope's own
-- sorter is the fallback.
local fzf_ok = vim.fn.executable('make') == 1 and vim.fn.executable(cc) == 1
-- nightfly, like most modern schemes, defines 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` leaves nothing readable behind, which is
-- the near-black-on-near-black report (#53).
-- This gate only fires on POSITIVE evidence of a low-colour terminal, so every host
-- 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
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 }
Expand Down Expand Up @@ -66,7 +79,16 @@ require('lazy').setup({
lazy = false,
priority = 1000,
config = function()
vim.cmd.colorscheme('nightfly')
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
end,
},
-- Installed (available via <leader>cs - all load at VeryLazy):
Expand Down Expand Up @@ -672,7 +694,7 @@ opt.undofile = true
opt.undodir = undodir
opt.visualbell = true
opt.wildmode = 'list:longest'
opt.termguicolors = true
opt.termguicolors = truecolor_ok -- see the truecolor_ok gate at the top of this file

opt.matchpairs:append('<:>')
-- linematch realigns hunks for cleaner inline diffs. inline:word (changed words
Expand Down
7 changes: 7 additions & 0 deletions nvim/.config/nvim/local.lua.example
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,13 @@
-- 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.
-- 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({})

Expand Down
98 changes: 98 additions & 0 deletions tests/nvim-colour-fallback.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
#!/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.
#
# 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
set -euo pipefail

DOTFILES_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
INIT="$DOTFILES_DIR/nvim/.config/nvim/init.lua"

_fail=0
_ran=0
_err() { printf ' ERROR: %s\n' "$1"; _fail=1; _ran=$(( _ran + 1 )); }
_ok() { printf ' OK: %s\n' "$1"; _ran=$(( _ran + 1 )); }

if ! command -v nvim >/dev/null 2>&1; then
echo "RESULT: SKIPPED and not verified - nvim is not installed"
exit 0
fi

tmp=$(mktemp -d)
# shellcheck disable=SC2064
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),
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
local out="$tmp/out.$1"
NVCOLOUR_OUT="$out" TERM="$1" COLORTERM='' 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=?"
}

echo "arm 1: 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" ;;
esac

echo "arm 2: 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)"
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
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' \
"$_verdict" "$_ran" "$_skipped"
else
printf 'RESULT: %s (%d checks ran, none skipped)\n' "$_verdict" "$_ran"
fi
exit "$_fail"
Loading