Carry the colour scheme into terminals and the editor

The scheme switch already reached everything that reads
org.freedesktop.appearance -- GTK4, Qt6, Chromium, Electron -- because
ColorScheme.qml writes the gsettings key those all watch. Applications
carrying their own palettes did not follow, so choosing light mode left
the two windows actually used all day, kitty and neovim, still dark.

kitty: the 32 colours move out of kitty.conf into themes/, and kitty.conf
ends with `include current-theme.conf`. The generated file is machine
state rather than configuration, so it is gitignored and link-dotfiles
seeds it on install -- otherwise a fresh checkout starts by complaining
about a missing include. Running terminals are re-coloured in place over
their control sockets; a restart is not needed.

neovim: reads settings.json directly, since it neither watches the portal
nor keeps a socket open. Tokyo Night ships Day in the same family as
Moon, so light mode keeps the editor's identity instead of turning it
into a different-looking application. The existing readability overrides
were written against Moon and are now dark-only -- applied to Day they
would have put light grey on a light background, the same problem they
exist to fix, inverted. Light mode gets one override of its own:
tokyonight's shipped comment colour measures 2.54:1 against Day's
background, under the 3:1 floor for secondary text, so it is replaced
with 3.25:1 -- readable, still dimmer than Normal's 4.52:1.

An editor already open when the scheme flips re-applies on FocusGained,
which is cheap and fires exactly when the mismatch would be noticed.

Verified both directions: kitty re-coloured 4 live terminals, and neovim
starts as tokyonight-day with background=light and tokyonight-moon with
background=dark.

Claude-Session: https://claude.ai/code/session_01BRvzt4H8XXLPVH5MyYdk9L
This commit is contained in:
Gabriel Brown
2026-08-18 07:47:51 -04:00
parent 5e6dc9e533
commit 19063a3e02
10 changed files with 324 additions and 57 deletions
+3
View File
@@ -10,3 +10,6 @@ vim.api.nvim_create_autocmd("BufWritePre", {
vim.fn.winrestview(view)
end,
})
-- Follow the desktop's light/dark setting while running, not only at startup.
require("config.panama").watch()
+88
View File
@@ -0,0 +1,88 @@
-- Panama desktop integration.
--
-- Neovim is the one application here that neither reads the desktop portal nor
-- has a control socket open by default, so it reads the shared settings file
-- directly -- the same ~/.config/panama/settings.json that the shell and the
-- Hyprland config read.
--
-- Nothing here may raise. A missing or malformed settings file must cost the
-- user their colour scheme preference and nothing else; editing text is more
-- important than matching the desktop.
local M = {}
local function settings_path()
local config_home = os.getenv("XDG_CONFIG_HOME")
if config_home == nil or config_home == "" then
local home = os.getenv("HOME")
if home == nil or home == "" then
return nil
end
config_home = home .. "/.config"
end
return config_home .. "/panama/settings.json"
end
-- "dark" or "light". Defaults to dark, which is what Panama ships.
function M.color_scheme()
local path = settings_path()
if not path then
return "dark"
end
local ok, contents = pcall(function()
local file = io.open(path, "r")
if not file then
return nil
end
local text = file:read("*a")
file:close()
return text
end)
if not ok or not contents or contents == "" then
return "dark"
end
local decoded_ok, decoded = pcall(vim.json.decode, contents)
if not decoded_ok or type(decoded) ~= "table" then
return "dark"
end
return decoded.colorScheme == "light" and "light" or "dark"
end
function M.is_light()
return M.color_scheme() == "light"
end
-- tokyonight ships a light variant in the same family, so light mode stays the
-- same identity rather than becoming a different editor theme.
function M.tokyonight_style()
return M.is_light() and "day" or "moon"
end
-- Re-apply the scheme when the window regains focus.
--
-- Neovim reads the setting once at startup and has no control socket open by
-- default, so an editor already running when the desktop scheme flips would
-- otherwise stay on the old palette until it was restarted. FocusGained is
-- cheap, happens exactly when you would notice the mismatch, and does nothing
-- at all unless the scheme actually changed.
function M.watch()
local applied = M.color_scheme()
vim.api.nvim_create_autocmd("FocusGained", {
group = vim.api.nvim_create_augroup("PanamaColorScheme", { clear = true }),
callback = function()
local current = M.color_scheme()
if current == applied then
return
end
applied = current
vim.o.background = current
pcall(vim.cmd.colorscheme, current == "light" and "tokyonight-day" or "tokyonight-moon")
end,
})
end
return M
+62 -25
View File
@@ -1,31 +1,68 @@
-- Tokyo Night, following the desktop's colour scheme.
--
-- Moon when Panama is dark, Day when it is light. Same theme family either way,
-- so the editor keeps the identity the rest of the desktop has rather than
-- becoming a different-looking application when the scheme flips.
--
-- The readability fixes below are deliberately dark-only. They were written
-- against Moon's palette -- a pale comment colour, a mid-grey gutter -- and
-- applying them to Day would put light grey text on a light background, which
-- is exactly the legibility problem they exist to solve, inverted.
local panama = require("config.panama")
return {
{
"folke/tokyonight.nvim",
opts = {
style = "moon",
transparent = true,
on_colors = function(colors)
colors.comment = "#a0a7c5"
colors.fg_gutter = "#787f93"
colors.terminal_black = "#828bb8"
end,
on_highlights = function(highlights, colors)
-- Fix inline code visibility in markdown
highlights["@markup.raw.markdown_inline"] = {
bg = colors.terminal_black,
fg = colors.fg,
}
highlights["RenderMarkdownCodeInline"] = {
bg = colors.terminal_black,
fg = colors.fg,
}
-- Fix LspReference* readability: DiagnosticUnnecessary dims fg for unused
-- imports, making text nearly invisible against LspReferenceText's background
highlights["LspReferenceText"] = { bg = colors.fg_gutter, fg = colors.fg }
highlights["LspReferenceRead"] = { bg = colors.fg_gutter, fg = colors.fg }
highlights["LspReferenceWrite"] = { bg = colors.fg_gutter, fg = colors.fg }
end,
},
opts = function()
local light = panama.is_light()
return {
style = panama.tokyonight_style(),
light_style = "day",
transparent = true,
on_colors = function(colors)
if light then
-- Day's defaults are tuned for a light ground and mostly need no
-- help. Comments are the exception: the shipped #848cb5 measures
-- 2.54:1 against the #e1e2e7 background, well under the 3:1 floor
-- for secondary text. This is 3.25:1 -- readable, and still clearly
-- dimmer than Normal's 4.52:1 so it does not compete with code.
colors.comment = "#7079a8"
return
end
colors.comment = "#a0a7c5"
colors.fg_gutter = "#787f93"
colors.terminal_black = "#828bb8"
end,
on_highlights = function(highlights, colors)
-- Inline code in markdown is invisible at both lightnesses without
-- an explicit background, because the theme leaves it unset.
highlights["@markup.raw.markdown_inline"] = {
bg = light and colors.bg_highlight or colors.terminal_black,
fg = colors.fg,
}
highlights["RenderMarkdownCodeInline"] = {
bg = light and colors.bg_highlight or colors.terminal_black,
fg = colors.fg,
}
-- LspReference* readability: DiagnosticUnnecessary dims fg for unused
-- imports, making text nearly invisible against LspReferenceText's
-- background.
local reference = {
bg = light and colors.bg_visual or colors.fg_gutter,
fg = colors.fg,
}
highlights["LspReferenceText"] = reference
highlights["LspReferenceRead"] = reference
highlights["LspReferenceWrite"] = reference
end,
}
end,
},
{
"LazyVim/LazyVim",