summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorThomas Vanbesien <tvanbesi@proton.me>2026-05-19 12:00:21 +0200
committerThomas Vanbesien <tvanbesi@proton.me>2026-05-19 15:06:47 +0200
commit53563e59b9d20922c0f37c99cf20e2be8f703a74 (patch)
tree4b8a5d0bbdddae9a15e522695fb35b755b8a653a
parentf1a519f6c5d78238b42be77701c9ff98d9f5b805 (diff)
downloaddotfiles-53563e59b9d20922c0f37c99cf20e2be8f703a74.tar.gz
dotfiles-53563e59b9d20922c0f37c99cf20e2be8f703a74.zip
feat(nvim): focus-aware cursor line via per-window highlight namespaces
Split 50-highlight.lua into 40-colors.lua (colorscheme, custom highlights, per-window namespaces, focus tracking) and 50-extmarks.lua (TODO scanner reading the namespace contract from vim.g.dotfiles). The 40- prefix guarantees colors load before extmarks. Focused vs unfocused windows now differ by cursor-line color and a CursorLineNr badge in the focus hue (green on dark, magenta on light); the number column stays visible on closed folds where CursorLine cannot.
-rw-r--r--.config/nvim/init.lua1
-rw-r--r--.config/nvim/plugin/40-colors.lua179
-rw-r--r--.config/nvim/plugin/50-colors.lua85
-rw-r--r--.config/nvim/plugin/50-extmarks.lua (renamed from .config/nvim/plugin/50-highlight.lua)17
4 files changed, 191 insertions, 91 deletions
diff --git a/.config/nvim/init.lua b/.config/nvim/init.lua
index 49057b9..e78cb27 100644
--- a/.config/nvim/init.lua
+++ b/.config/nvim/init.lua
@@ -151,6 +151,7 @@ vim.opt.scrolloff = 2 -- Keep this many screen lines above/below the cursor
vim.opt.ignorecase = true -- Search ignores case by default
vim.opt.smartcase = true -- Search is case-sensitive if searching for uppercase characters
vim.opt.conceallevel = 2 -- Hide text with the "conceal" syntax attribute
+vim.opt.showtabline = 2 -- Always show tabline
vim.opt.list = true -- Display <Tab> and other non-printables
vim.opt.listchars = { -- Characters used by 'list'
tab = "> ", -- Tab
diff --git a/.config/nvim/plugin/40-colors.lua b/.config/nvim/plugin/40-colors.lua
new file mode 100644
index 0000000..21c93e7
--- /dev/null
+++ b/.config/nvim/plugin/40-colors.lua
@@ -0,0 +1,179 @@
+--
+-- Color scheme, custom highlights & per-window highlight namespaces
+--
+vim.pack.add({ "https://github.com/maxmx03/solarized.nvim" }) -- Solarized color scheme
+
+local solarized_colors = {
+ base03 = "#002b36",
+ base02 = "#073642",
+ base01 = "#586e75",
+ base00 = "#657b83",
+ base0 = "#839496",
+ base1 = "#93a1a1",
+ base2 = "#eee8d5",
+ base3 = "#fdf6e3",
+ yellow = "#b58900",
+ orange = "#cb4b16",
+ red = "#dc322f",
+ magenta = "#d33682",
+ violet = "#6c71c4",
+ blue = "#268bd2",
+ cyan = "#2aa198",
+ green = "#859900",
+ -- solarized.nvim "mix" tints: subtle highlight backgrounds, theme-specific
+ mix_base1 = "#2c4e56", -- dark (active cursor line)
+ mix_blue_light = "#e7ebe1", -- light (active cursor line)
+ mix_green = "#274c25", -- dark (inactive cursor line: muted green)
+ mix_magenta_light = "#f8e2d9", -- light (inactive cursor line: muted pink)
+}
+
+-- Per-window highlight namespaces: one for the focused window, one for the
+-- unfocused windows. A window's active namespace decides its CursorLine color,
+-- which is how the focused window is made obvious. Both namespaces must define
+-- `dotfiles.Todo`: an extmark's hl_group is resolved against the window's
+-- active namespace and does NOT fall back to the global namespace, so the TODO
+-- highlight (placed in 50-extmarks.lua) only renders if the group exists in
+-- whichever namespace the window currently uses.
+local ns_active = vim.api.nvim_create_namespace("extmarks")
+local ns_inactive = vim.api.nvim_create_namespace("extmarks.inactive")
+local todo_hl = { bg = "Yellow", fg = "Black", bold = true }
+vim.api.nvim_set_hl(ns_active, "dotfiles.Todo", todo_hl)
+vim.api.nvim_set_hl(ns_inactive, "dotfiles.Todo", todo_hl)
+
+-- Publish the extmark storage namespace and TODO group name for 50-extmarks.lua.
+-- This file is named 40- (not 50-) so it is sourced before 50-extmarks.lua and
+-- the contract below exists when that file reads it. Preserve the existing
+-- `dotfiles` table (augroup).
+local dotfiles = vim.g.dotfiles
+dotfiles.todo_ns = ns_active
+dotfiles.todo_hl = "dotfiles.Todo"
+vim.g.dotfiles = dotfiles
+
+-- Custom highlights in the global namespace, independent of the per-window ones
+local function adjust_highlight()
+ if vim.g.colors_name == "solarized" then
+ vim.api.nvim_set_hl(0, "EndOfBuffer", { fg = solarized_colors.base0, update = true })
+ vim.api.nvim_set_hl(0, "MatchParen", { link = "CurSearch" })
+ if vim.o.background == "light" then
+ vim.api.nvim_set_hl(0, "TabLineSel", { bg = "#edffd5" })
+ vim.api.nvim_set_hl(0, "Cursorline", { bg = "#edffd5" })
+ vim.api.nvim_set_hl(0, "NormalFloat", { bg = solarized_colors.base2 })
+ elseif vim.o.background == "dark" then
+ vim.api.nvim_set_hl(0, "TabLineSel", { bg = "#043624" })
+ vim.api.nvim_set_hl(0, "Cursorline", { bg = "#043624" })
+ vim.api.nvim_set_hl(0, "NormalFloat", { bg = solarized_colors.base02 })
+ end
+ end
+end
+
+-- Theme-aware cursor line, one focus hue (green on dark, magenta/pink on light)
+-- at two intensities. CursorLineNr stays visible on closed folds (the number
+-- column is NOT painted by Folded), so it carries the focus cue: active window
+-- a muted "mix" of the hue, unfocused windows a bright bold badge of the same
+-- hue. The cursor-line background backs this up: active a subtle neutral "mix"
+-- tint (light enough to edit on, distinct from Folded), unfocused the muted
+-- hue wash.
+local function set_cursorline_hl()
+ local active, muted, badge
+ if vim.o.background == "light" then
+ active, muted = solarized_colors.mix_blue_light, solarized_colors.mix_magenta_light
+ badge = { bg = solarized_colors.magenta, fg = solarized_colors.base3, bold = true }
+ else
+ active, muted = solarized_colors.mix_base1, solarized_colors.mix_green
+ badge = { bg = solarized_colors.green, fg = solarized_colors.base03, bold = true }
+ end
+ vim.api.nvim_set_hl(ns_active, "CursorLine", { bg = active })
+ vim.api.nvim_set_hl(ns_active, "CursorLineNr", { bg = muted })
+ vim.api.nvim_set_hl(ns_inactive, "CursorLine", { bg = muted })
+ vim.api.nvim_set_hl(ns_inactive, "CursorLineNr", badge)
+end
+
+local function apply_highlights()
+ adjust_highlight()
+ set_cursorline_hl()
+end
+
+-- Single owner of every window's highlight namespace: the focused window gets
+-- `ns_active`, all others `ns_inactive`. Reapplied to every window on each
+-- focus change because WinLeave alone misses windows that existed at startup
+-- or were restored from a session.
+local function is_normal_win(win)
+ return vim.api.nvim_win_is_valid(win) and vim.api.nvim_win_get_config(win).relative == ""
+end
+
+local function update_focus_ns()
+ local cur = vim.api.nvim_get_current_win()
+ -- Focus in a floating window (completion, hover, ...) must not recolor the
+ -- split underneath; leave every window as it is.
+ if not is_normal_win(cur) then
+ return
+ end
+ for _, win in ipairs(vim.api.nvim_list_wins()) do
+ if is_normal_win(win) then
+ vim.api.nvim_win_set_hl_ns(win, (win == cur) and ns_active or ns_inactive)
+ end
+ end
+end
+
+-- Note: a closed fold's screen line is always drawn with `Folded` (see
+-- `:help fold.txt` / hl-Folded); Neovim has no per-line override, so the
+-- unfocused cursor line is not visible when it sits on a closed fold. This is
+-- standard behavior (cursorline never renders on closed folds in any window)
+-- and is accepted as-is.
+
+local function next_colorscheme()
+ local colorschemes = vim.fn.getcompletion("", "color")
+ vim.g.colors_name = vim.g.colors_name or "default"
+ for i = 1, #colorschemes do
+ if colorschemes[i] == vim.g.colors_name then
+ vim.cmd.colorscheme(colorschemes[(i % #colorschemes) + 1])
+ vim.notify("Color scheme set to " .. vim.g.colors_name)
+ return
+ end
+ end
+end
+
+local function random_colorscheme()
+ local colorschemes = vim.fn.getcompletion("", "color")
+ vim.cmd.colorscheme(colorschemes[math.random(#colorschemes)])
+ vim.notify("Color scheme set to " .. vim.g.colors_name)
+end
+
+local function toggle_theme()
+ vim.opt.background = vim.o.background == "light" and "dark" or "light"
+ vim.notify("Color theme set to " .. vim.o.background)
+end
+
+-- Reapply custom highlights after the colorscheme loads and on theme change
+vim.api.nvim_create_autocmd("ColorScheme", {
+ desc = "Reapply custom highlights",
+ group = vim.g.dotfiles.augroup,
+ callback = apply_highlights,
+})
+vim.api.nvim_create_autocmd("OptionSet", {
+ desc = "Reapply custom highlights on theme change",
+ pattern = "background",
+ group = vim.g.dotfiles.augroup,
+ callback = apply_highlights,
+})
+
+-- Keep the focused/unfocused cursor line correct as windows and focus change
+vim.api.nvim_create_autocmd(
+ { "VimEnter", "WinEnter", "WinNew", "WinClosed", "TabEnter", "SessionLoadPost", "FileType" },
+ {
+ desc = "Track the focused window for the cursor line",
+ group = vim.g.dotfiles.augroup,
+ callback = function()
+ -- Defer so the window list and current window have settled.
+ vim.schedule(update_focus_ns)
+ end,
+ }
+)
+
+vim.cmd.colorscheme("solarized") -- Default colorscheme
+vim.opt.background = "dark" -- Default theme
+apply_highlights()
+
+vim.api.nvim_create_user_command("ColorsNext", next_colorscheme, { desc = "Load next color scheme" })
+vim.api.nvim_create_user_command("ColorsRandom", random_colorscheme, { desc = "Load random color scheme" })
+vim.api.nvim_create_user_command("ColorsThemeToggle", toggle_theme, { desc = "Toggle light/dark theme" })
diff --git a/.config/nvim/plugin/50-colors.lua b/.config/nvim/plugin/50-colors.lua
deleted file mode 100644
index 4d9774e..0000000
--- a/.config/nvim/plugin/50-colors.lua
+++ /dev/null
@@ -1,85 +0,0 @@
---
--- Color scheme plugin
---
-vim.pack.add({ "https://github.com/maxmx03/solarized.nvim" }) -- Solarized color scheme
-
-local solarized_colors = {
- base03 = "#002b36",
- base02 = "#073642",
- base01 = "#586e75",
- base00 = "#657b83",
- base0 = "#839496",
- base1 = "#93a1a1",
- base2 = "#eee8d5",
- base3 = "#fdf6e3",
- yellow = "#b58900",
- orange = "#cb4b16",
- red = "#dc322f",
- magenta = "#d33682",
- violet = "#6c71c4",
- blue = "#268bd2",
- cyan = "#2aa198",
- green = "#859900",
-}
-
-local function adjust_highlight()
- if vim.g.colors_name == "solarized" then
- -- General highlight
- vim.api.nvim_set_hl(0, "EndOfBuffer", { fg = solarized_colors.base0, update = true })
- vim.api.nvim_set_hl(0, "MatchParen", { link = "CurSearch" })
- if vim.o.background == "light" then
- vim.api.nvim_set_hl(0, "TabLineSel", { bg = "#edffd5" })
- vim.api.nvim_set_hl(0, "Cursorline", { bg = "#edffd5" })
- vim.api.nvim_set_hl(0, "NormalFloat", { bg = solarized_colors.base2 })
- elseif vim.o.background == "dark" then
- vim.api.nvim_set_hl(0, "TabLineSel", { bg = "#043624" })
- vim.api.nvim_set_hl(0, "Cursorline", { bg = "#043624" })
- vim.api.nvim_set_hl(0, "NormalFloat", { bg = solarized_colors.base02 })
- end
- end
-end
-
-local function next_colorscheme()
- local colorschemes = vim.fn.getcompletion("", "color")
- vim.g.colors_name = vim.g.colors_name or "default"
- for i = 1, #colorschemes do
- if colorschemes[i] == vim.g.colors_name then
- vim.cmd.colorscheme(colorschemes[(i % #colorschemes) + 1])
- vim.notify("Color scheme set to " .. vim.g.colors_name)
- return
- end
- end
-end
-
-local function random_colorscheme()
- local colorschemes = vim.fn.getcompletion("", "color")
- vim.cmd.colorscheme(colorschemes[math.random(#colorschemes)])
- vim.notify("Color scheme set to " .. vim.g.colors_name)
-end
-
-local function toggle_theme()
- vim.opt.background = vim.o.background == "light" and "dark" or "light"
- vim.notify("Color theme set to " .. vim.o.background)
-end
-
--- Adjust colors after the color scheme has loaded
-vim.api.nvim_create_autocmd("ColorScheme", {
- desc = "Adjust color scheme",
- group = vim.g.dotfiles.augroup,
- callback = adjust_highlight,
-})
-
--- Adjust colors when the theme (light/dark) changes
-vim.api.nvim_create_autocmd("OptionSet", {
- desc = "Adjust color scheme",
- pattern = "background",
- group = vim.g.dotfiles.augroup,
- callback = adjust_highlight,
-})
-
-vim.cmd.colorscheme("solarized") -- Default colorscheme
-vim.opt.background = "dark" -- Default theme
-
-vim.api.nvim_create_user_command("ColorsNext", next_colorscheme, { desc = "Load next color scheme" })
-vim.api.nvim_create_user_command("ColorsRandom", random_colorscheme, { desc = "Load random color scheme" })
-vim.api.nvim_create_user_command("ColorsThemeToggle", toggle_theme, { desc = "Toggle light/dark theme" })
diff --git a/.config/nvim/plugin/50-highlight.lua b/.config/nvim/plugin/50-extmarks.lua
index a509a86..c11d10c 100644
--- a/.config/nvim/plugin/50-highlight.lua
+++ b/.config/nvim/plugin/50-extmarks.lua
@@ -1,6 +1,11 @@
--
--- Custom highlighting plugin
+-- TODO highlighting (treesitter-aware extmarks)
--
+-- The `dotfiles.Todo` highlight group and the storage namespace are owned by
+-- 40-colors.lua, because the same per-window namespaces also drive the
+-- focused/unfocused cursor line. That file is numbered 40- so it is sourced
+-- before this one and `vim.g.dotfiles.todo_ns` / `.todo_hl` exist below. Here
+-- we only place the extmarks.
-- Returns an iterator over the ascendants of `node`, starting at the root node
local function node_ascendants(node)
@@ -83,17 +88,17 @@ local function hl_todo_predicate(row, col)
end
local patterns = { todo = "TODO" }
-local hl_ns = vim.api.nvim_create_namespace("extmarks")
-
-vim.api.nvim_set_hl(hl_ns, "dotfiles.Todo", { bg = "Yellow", fg = "Black", bold = true })
-local extmarks_todo_opts = { ns = hl_ns, hl_group = "dotfiles.Todo", predicate = hl_todo_predicate }
+local extmarks_todo_opts = {
+ ns = vim.g.dotfiles.todo_ns,
+ hl_group = vim.g.dotfiles.todo_hl,
+ predicate = hl_todo_predicate,
+}
-- Initialize extmarks
vim.api.nvim_create_autocmd("FileType", {
desc = "Initialize extmarks",
group = vim.g.dotfiles.augroup,
callback = function()
- vim.api.nvim_win_set_hl_ns(vim.api.nvim_get_current_win(), hl_ns)
local lines = vim.api.nvim_buf_get_lines(0, 0, -1, false)
set_extmarks(pmatches(lines, patterns.todo), extmarks_todo_opts)
end,