From 19063a3e020c8d8b92f871d4fd7bdc643afd4726 Mon Sep 17 00:00:00 2001 From: Gabriel Brown Date: Tue, 18 Aug 2026 07:47:51 -0400 Subject: [PATCH] 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 --- .gitignore | 3 + config/dot/kitty/kitty.conf | 34 +------ config/dot/kitty/themes/tokyonight-day.conf | 38 ++++++++ config/dot/kitty/themes/tokyonight-moon.conf | 42 +++++++++ config/dot/nvim/lua/config/autocmds.lua | 3 + config/dot/nvim/lua/config/panama.lua | 88 +++++++++++++++++++ config/dot/nvim/lua/plugins/colorscheme.lua | 87 ++++++++++++------ .../dot/quickshell/scripts/panama-theme-apps | 57 ++++++++++++ .../dot/quickshell/services/ColorScheme.qml | 8 ++ setup/scripts/link-dotfiles | 21 +++++ 10 files changed, 324 insertions(+), 57 deletions(-) create mode 100644 config/dot/kitty/themes/tokyonight-day.conf create mode 100644 config/dot/kitty/themes/tokyonight-moon.conf create mode 100644 config/dot/nvim/lua/config/panama.lua create mode 100755 config/dot/quickshell/scripts/panama-theme-apps diff --git a/.gitignore b/.gitignore index 6db0dac..860119d 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,6 @@ # Python helper bytecode is local runtime state. __pycache__/ *.py[cod] + +# Generated from the colour scheme; machine state, not configuration. +/config/dot/kitty/current-theme.conf diff --git a/config/dot/kitty/kitty.conf b/config/dot/kitty/kitty.conf index 05116ec..f28b611 100644 --- a/config/dot/kitty/kitty.conf +++ b/config/dot/kitty/kitty.conf @@ -183,7 +183,6 @@ font_size 14.0 #: Cursor customization {{{ -cursor #c8d3f5 #: Default cursor color. If set to the special value none the cursor #: will be rendered with a "reverse video" effect. It's color will be @@ -193,7 +192,6 @@ cursor #c8d3f5 #: precedence. Also, the cursor colors are modified if the cell #: background and foreground colors have very low contrast. -cursor_text_color #111325 #: The color of text under the cursor. If you want it rendered with #: the background color of the cell underneath instead, use the @@ -312,7 +310,6 @@ cursor_shape block #: robustly with the ever-changing sea of bugs that is Cocoa is too #: much effort. -url_color #4fd6be # url_style curly #: The color and style for highlighting URLs on mouse-over. url_style @@ -734,12 +731,10 @@ window_border_width 1pt #: placed centrally. A value of top-left means the padding will be #: only at the bottom and right edges. -active_border_color #82aaff #: The color for the border of the active window. Set this to none to #: not draw borders around the active window. -inactive_border_color #2f334d #: The color for the border of inactive windows. @@ -960,16 +955,11 @@ tab_powerline_style round #: Template to use for active tabs. If not specified falls back to #: tab_title_template. -active_tab_foreground #1e2030 -active_tab_background #82aaff # active_tab_font_style bold-italic -inactive_tab_foreground #c8d3f5 -inactive_tab_background #42465a # inactive_tab_font_style normal #: Tab bar colors and styles. -tab_bar_background #222436 #: Background color for the tab bar. Defaults to using the terminal #: background color. @@ -983,8 +973,8 @@ tab_bar_margin_color none #: Color scheme {{{ -foreground #c8d3f5 -background #222436 + +include current-theme.conf #: The foreground and background colors. @@ -1043,8 +1033,6 @@ dim_opacity 1 #: How much to dim text that has the DIM/FAINT attribute set. One #: means no dimming and zero means fully dimmed (i.e. invisible). -selection_foreground #2d3f76 -selection_background #c8d3f5 #: The foreground and background colors for text selected with the #: mouse. Setting both of these to none will cause a "reverse video" @@ -1060,47 +1048,29 @@ selection_background #c8d3f5 #: dull and bright version, for the first 16 colors. You can set the #: remaining 240 colors as color16 to color255. -color0 #1b1d2b -color8 #444a73 #: black -color1 #ff757f -color9 #ff757f #: red -color2 #a5e8b5 -color10 #a5e8b5 #: green -color3 #ffc777 -color11 #ffc777 #: yellow -color4 #82aaff -color12 #82aaff #: blue -color5 #c099ff -color13 #c099ff #: magenta -color6 #86e1fc -color14 #86e1fc #: cyan -color7 #828bb8 -color15 #c8d3f5 #: white -color16 #ff966c -color17 #c53b53 # mark1_foreground black diff --git a/config/dot/kitty/themes/tokyonight-day.conf b/config/dot/kitty/themes/tokyonight-day.conf new file mode 100644 index 0000000..532a7f4 --- /dev/null +++ b/config/dot/kitty/themes/tokyonight-day.conf @@ -0,0 +1,38 @@ +# Tokyo Night Day — the palette's own light variant. +# +# The same hues as Moon at a different lightness, so a terminal in light mode +# still belongs to this desktop rather than looking like a different machine. +# Kept in the same order as tokyonight-moon.conf so the two can be diffed. + +cursor #3760bf +cursor_text_color #e1e2e7 +url_color #118c74 +active_border_color #2e7de9 +inactive_border_color #c4c8da +active_tab_foreground #e1e2e7 +active_tab_background #2e7de9 +inactive_tab_foreground #3760bf +inactive_tab_background #c4c8da +tab_bar_background #e1e2e7 +foreground #3760bf +background #e1e2e7 +selection_foreground #e1e2e7 +selection_background #99a7df +color0 #e9e9ed +color8 #a1a6c5 +color1 #f52a65 +color9 #f52a65 +color2 #587539 +color10 #587539 +color3 #8c6c3e +color11 #8c6c3e +color4 #2e7de9 +color12 #2e7de9 +color5 #9854f1 +color13 #9854f1 +color6 #007197 +color14 #007197 +color7 #6172b0 +color15 #3760bf +color16 #b15c00 +color17 #c64343 diff --git a/config/dot/kitty/themes/tokyonight-moon.conf b/config/dot/kitty/themes/tokyonight-moon.conf new file mode 100644 index 0000000..ca70424 --- /dev/null +++ b/config/dot/kitty/themes/tokyonight-moon.conf @@ -0,0 +1,42 @@ +# Tokyo Night Moon — the dark theme Panama ships. +# +# Extracted from kitty.conf so the two schemes can be swapped. kitty.conf +# includes current-theme.conf, which Panama generates from the colour scheme +# setting; that generated file is gitignored because it is machine state. +# +# Live changes go through `kitty @ set-colors`, which is why kitty.conf enables +# remote control. Without it a scheme change would only reach terminals opened +# afterwards. + +cursor #c8d3f5 +cursor_text_color #111325 +url_color #4fd6be +active_border_color #82aaff +inactive_border_color #2f334d +active_tab_foreground #1e2030 +active_tab_background #82aaff +inactive_tab_foreground #c8d3f5 +inactive_tab_background #42465a +tab_bar_background #222436 +foreground #c8d3f5 +background #222436 +selection_foreground #2d3f76 +selection_background #c8d3f5 +color0 #1b1d2b +color8 #444a73 +color1 #ff757f +color9 #ff757f +color2 #a5e8b5 +color10 #a5e8b5 +color3 #ffc777 +color11 #ffc777 +color4 #82aaff +color12 #82aaff +color5 #c099ff +color13 #c099ff +color6 #86e1fc +color14 #86e1fc +color7 #828bb8 +color15 #c8d3f5 +color16 #ff966c +color17 #c53b53 diff --git a/config/dot/nvim/lua/config/autocmds.lua b/config/dot/nvim/lua/config/autocmds.lua index 5cf7dbc..037718f 100644 --- a/config/dot/nvim/lua/config/autocmds.lua +++ b/config/dot/nvim/lua/config/autocmds.lua @@ -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() diff --git a/config/dot/nvim/lua/config/panama.lua b/config/dot/nvim/lua/config/panama.lua new file mode 100644 index 0000000..bd43fb0 --- /dev/null +++ b/config/dot/nvim/lua/config/panama.lua @@ -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 diff --git a/config/dot/nvim/lua/plugins/colorscheme.lua b/config/dot/nvim/lua/plugins/colorscheme.lua index 976220c..236ee5b 100644 --- a/config/dot/nvim/lua/plugins/colorscheme.lua +++ b/config/dot/nvim/lua/plugins/colorscheme.lua @@ -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", diff --git a/config/dot/quickshell/scripts/panama-theme-apps b/config/dot/quickshell/scripts/panama-theme-apps new file mode 100755 index 0000000..8b2708e --- /dev/null +++ b/config/dot/quickshell/scripts/panama-theme-apps @@ -0,0 +1,57 @@ +#!/usr/bin/env bash + +# Propagates the colour scheme to applications that do not read the desktop +# portal. +# +# Most modern applications DO follow org.freedesktop.portal.Settings and need +# nothing from us: GTK4/libadwaita, Qt6, Chromium and Electron all read +# org.freedesktop.appearance color-scheme, which xdg-desktop-portal-gtk serves +# from gsettings. Panama sets that, so those follow automatically. +# +# Terminals are the notable exception -- they predate the standard and carry +# their own palettes. kitty is handled here. +# +# panama-theme-apps dark|light +# +# kitty gets it twice: the generated include file so terminals opened later +# start correct, and a live `set-colors` over its control socket so terminals +# already open change now. Without the second, a scheme change appears to do +# nothing until you open a new window. + +set -euo pipefail + +scheme="${1:-dark}" +case "$scheme" in + dark|light) ;; + *) printf 'usage: panama-theme-apps [dark|light]\n' >&2; exit 2 ;; +esac + +kitty_dir="${XDG_CONFIG_HOME:-$HOME/.config}/kitty" +theme_file="$kitty_dir/themes/tokyonight-moon.conf" +[[ "$scheme" == "light" ]] && theme_file="$kitty_dir/themes/tokyonight-day.conf" + +status_kitty="skipped" +if [[ -r "$theme_file" ]]; then + # Written atomically: kitty may read this while a new window is starting. + if cp "$theme_file" "$kitty_dir/current-theme.conf.tmp" 2>/dev/null \ + && mv "$kitty_dir/current-theme.conf.tmp" "$kitty_dir/current-theme.conf" 2>/dev/null; then + status_kitty="written" + fi + + # Live-apply to running terminals. kitty appends its PID to the socket name + # from listen_on, so there is one socket per instance -- "unix:@mykitty" + # alone reaches nothing, which is exactly how this looked like remote + # control being disabled when it was not. + if command -v kitty >/dev/null 2>&1 && command -v ss >/dev/null 2>&1; then + applied=0 + while read -r socket; do + [[ -n "$socket" ]] || continue + if kitty @ --to "unix:@${socket#@}" set-colors --all --configured "$theme_file" >/dev/null 2>&1; then + applied=$((applied + 1)) + fi + done < <(ss -xl 2>/dev/null | grep -oE '@mykitty[^[:space:]]*' | sort -u) + (( applied > 0 )) && status_kitty="applied to $applied" + fi +fi + +printf '{"scheme":"%s","kitty":"%s"}\n' "$scheme" "$status_kitty" diff --git a/config/dot/quickshell/services/ColorScheme.qml b/config/dot/quickshell/services/ColorScheme.qml index 4353142..4f09838 100644 --- a/config/dot/quickshell/services/ColorScheme.qml +++ b/config/dot/quickshell/services/ColorScheme.qml @@ -21,6 +21,8 @@ import qs.config Singleton { id: root + readonly property string appThemePath: Quickshell.shellDir + "/scripts/panama-theme-apps" + readonly property bool dark: DesktopPreferences.get("colorScheme") !== "light" property string lastError: "" @@ -92,6 +94,12 @@ Singleton { commands.push(["hyprctl", "eval", `hl.config({ general = { col = { inactive_border = "${inactive}" } } })`]); + // Applications that predate org.freedesktop.appearance and carry their + // own palettes -- terminals, chiefly. Everything that reads the portal + // (GTK4, Qt6, Chromium, Electron) is already handled by the gsettings + // write above and needs nothing here. + commands.push([root.appThemePath, root.dark ? "dark" : "light"]); + root.enqueue(commands); } } diff --git a/setup/scripts/link-dotfiles b/setup/scripts/link-dotfiles index e3eb5a3..3ab8fd6 100755 --- a/setup/scripts/link-dotfiles +++ b/setup/scripts/link-dotfiles @@ -70,6 +70,27 @@ for dir in "${dirs[@]}"; do log "Linked $PANAMA_DOT/$dir → $CONFIG/$dir" done +# kitty.conf ends with `include current-theme.conf`, and that file is generated +# from the desktop colour scheme rather than committed -- it is machine state. +# A fresh checkout therefore has no such file, and kitty starts by complaining +# about a missing include and falling back to its stock colours. Seed it from +# the scheme in settings.json (dark unless the user has chosen otherwise) so a +# first launch is themed; ColorScheme.qml overwrites it on every change after. +KITTY_THEME="$PANAMA_DOT/kitty/current-theme.conf" +if [ -e "$KITTY_THEME" ]; then + log "Keeping existing kitty theme at $KITTY_THEME" +else + scheme="dark" + settings="${XDG_CONFIG_HOME:-$HOME/.config}/panama/settings.json" + if [ -r "$settings" ]; then + stored="$(jq -r '.colorScheme // "dark"' "$settings" 2>/dev/null || echo dark)" + [ "$stored" = "light" ] && scheme="light" + fi + [ "$scheme" = "light" ] && theme="tokyonight-day" || theme="tokyonight-moon" + cp "$PANAMA_DOT/kitty/themes/$theme.conf" "$KITTY_THEME" + log "Seeded kitty $scheme theme ($theme) → $KITTY_THEME" +fi + # Vicinae 0.26 discovers user themes from its XDG data directory rather than # from ~/.config/vicinae. Keep the authored theme in Panama with the rest of # the launcher config and expose only that file at Vicinae's runtime path.