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.