Add Qt/GTK2 theming, default apps, LibreOffice; fix display.conf race

- Add qt-theme-set: a working Qt theming pipeline via qt6ct (the
  QT_QPA_PLATFORMTHEME=qt6ct env var was pointing at a platform theme
  plugin that wasn't even installed, so every Qt app fell back to its
  own default style). Also regenerates kdeglobals' KColorScheme groups
  and sets QT_QUICK_CONTROLS_STYLE=org.kde.desktop for Kirigami-based
  KDE apps, whose QML-drawn chrome ignores QPalette/qt6ct entirely.
  Gwenview's dialogs/content now follow the theme; its toolbar/statusbar
  chrome is a known remaining gap tied to Kirigami's platform-theme
  detection outside a full Plasma session
- Add GTK2 compat (~/.gtkrc-2.0: icon theme + font follow the theme;
  no dark variant exists for GTK2's Adwaita engine)
- Add modules/default-apps.nix: Gwenview/Ark as system-default image
  viewer/archive manager via /etc/xdg/mimeapps.list, LibreOffice
  (Qt6 build), and a working Thunar "Open Terminal Here" (it shipped
  calling exo-open, which isn't installed and never resolved)
- Fix display-set.sh: mktemp defaulted to /tmp, a different filesystem
  than ~/.config/hypr here, making the mv into display.conf a
  non-atomic copy+unlink. Hyprland's file watcher could catch it
  mid-replace and throw "source= globbing error: found no match" on
  any monitor mode/orientation change. Stress-tested with 60 concurrent
  write+reload cycles after the fix with zero errors
This commit is contained in:
Nexoniarz 2026-08-30 16:51:39 +02:00
commit d1ff0a11e0
9 changed files with 224 additions and 2 deletions

65
modules/default-apps.nix Normal file
View file

@ -0,0 +1,65 @@
{ config, pkgs, lib, ... }:
{
environment.systemPackages = with pkgs; [
libreoffice-qt6 # Qt6-styled build — fits the Qt theming work in theme.nix
# better than the default GTK3 build would.
];
# System-level fallback defaults (XDG spec: only consulted for a
# mimetype the user's own ~/.config/mimeapps.list doesn't already
# claim), so a future "Open With" choice always wins over this without
# needing to touch it. Mimetype lists taken directly from Gwenview's
# and Ark's own .desktop files, not guessed.
#
# One mimetype=desktopfile pair per line: the semicolon only separates
# multiple desktop-file candidates for the SAME key, it doesn't chain
# separate mimetype=value pairs on one line — mimeapps.list parsers
# (verified against xdg-utils' own xdg-mime script) match by scanning
# for a line that STARTS WITH "mimetype=", so cramming several pairs
# onto one semicolon-joined line silently hides every key after the
# first from ever being found.
environment.etc."xdg/mimeapps.list".text =
let
gwenviewMimes = [
"inode/directory" "image/avif" "image/gif" "image/heif" "image/jpeg"
"image/jxl" "image/png" "image/bmp" "image/x-eps" "image/x-icns"
"image/x-ico" "image/x-portable-bitmap" "image/x-portable-graymap"
"image/x-portable-pixmap" "image/x-xbitmap" "image/x-xpixmap"
"image/tiff" "image/x-psd" "image/x-webp" "image/webp" "image/x-tga"
"image/x-xcf" "image/openraster" "image/svg+xml" "image/svg+xml-compressed"
];
arkMimes = [
"application/x-cd-image" "application/x-bcpio" "application/x-cpio"
"application/x-cpio-compressed" "application/x-rpm" "application/x-compress"
"application/gzip" "application/x-bzip" "application/x-bzip2"
"application/x-lzma" "application/x-xz" "application/zstd" "application/x-lz4"
"application/x-source-rpm" "application/vnd.debian.binary-package"
"application/vnd.ms-cab-compressed" "application/x-archive" "application/x-tar"
"application/x-compressed-tar" "application/x-bzip-compressed-tar"
"application/x-bzip2-compressed-tar" "application/x-tarz"
"application/x-xz-compressed-tar" "application/x-lzma-compressed-tar"
"application/x-7z-compressed" "application/vnd.rar" "application/zip"
"application/x-java-archive" "application/x-arj"
];
line = desktop: mime: "${mime}=${desktop}";
in
''
[Default Applications]
${lib.concatMapStringsSep "\n" (line "org.kde.gwenview.desktop") gwenviewMimes}
${lib.concatMapStringsSep "\n" (line "org.kde.ark.desktop") arkMimes}
'';
# Thunar's own "Open Terminal Here" custom action (Edit > Configure
# Custom Actions) shipped pointing at `exo-open --launch
# TerminalEmulator` — exo isn't installed on this system at all (no
# XFCE session, so its helpers.rc mechanism has nothing to resolve
# against), so the action silently did nothing. `f`, not `w`: only
# creates this baseline for a fresh account — once it exists, further
# edits made through Thunar's own UI (adding more custom actions) are
# never overwritten by a rebuild.
systemd.tmpfiles.rules = [
"d /home/nexoniarz/.config/Thunar 0755 nexoniarz users -"
''f /home/nexoniarz/.config/Thunar/uca.xml 0644 nexoniarz users - <?xml version="1.0" encoding="UTF-8"?><actions><action><icon>utilities-terminal</icon><name>Open Terminal Here</name><submenu></submenu><unique-id>1788014825097143-1</unique-id><command>kitty --working-directory %f</command><description>Open a terminal in this folder</description><range></range><patterns>*</patterns><startup-notify/><directories/></action></actions>''
];
}

View file

@ -12,6 +12,7 @@ let
displaySet = pkgs.writeShellScriptBin "display-set" (builtins.readFile ./scripts/display-set.sh);
hyprThemeSet = pkgs.writeShellScriptBin "hypr-theme-set" (builtins.readFile ./scripts/hypr-theme-set.sh);
gtkThemeSet = pkgs.writeShellScriptBin "gtk-theme-set" (builtins.readFile ./scripts/gtk-theme-set.sh);
qtThemeSet = pkgs.writeShellScriptBin "qt-theme-set" (builtins.readFile ./scripts/qt-theme-set.sh);
kittyThemeSet = pkgs.writeShellScriptBin "kitty-theme-set" (builtins.readFile ./scripts/kitty-theme-set.sh);
rofiThemeSet = pkgs.writeShellScriptBin "rofi-theme-set" (builtins.readFile ./scripts/rofi-theme-set.sh);
listCursorThemes = pkgs.writeShellScriptBin "list-cursor-themes" (builtins.readFile ./scripts/list-cursor-themes.sh);
@ -36,6 +37,7 @@ in
displaySet
hyprThemeSet
gtkThemeSet
qtThemeSet
kittyThemeSet
rofiThemeSet
listCursorThemes

View file

@ -25,7 +25,15 @@ touch "$conf"
line="monitor = $descriptor"
if grep -q "^monitor = $mon," "$conf" 2>/dev/null; then
tmp=$(mktemp)
# mktemp in the same directory as $conf, not the default /tmp: /tmp is
# a different filesystem here, which makes `mv` below a non-atomic
# copy+unlink instead of a rename — Hyprland's file watcher (it
# watches every `source=`d file, including this one) could catch the
# destination mid-replace and its `source=` glob would find nothing,
# throwing "globbing error: found no match" (observed live after an
# orientation change, which always hits this branch since DP-1
# already has an existing line by then).
tmp=$(mktemp "$conf.XXXXXX")
awk -v prefix="monitor = $mon," -v newline="$line" '
index($0, prefix) == 1 { print newline; next } { print }
' "$conf" > "$tmp"

View file

@ -117,3 +117,16 @@ if command -v dconf >/dev/null 2>&1; then
dconf write /org/gnome/desktop/interface/icon-theme "'$icon_theme'"
dconf write /org/gnome/desktop/interface/color-scheme "'$color_scheme'"
fi
# GTK2 compat: separate config format (~/.gtkrc-2.0, not settings.ini),
# and no live theme-switching mechanism at all — GTK2 apps only read this
# at their own startup. Only icon theme and font actually follow the
# current theme; gnome-themes-extra's bundled GTK2 Adwaita engine has no
# dark variant to switch to (GTK2 predates that concept), so gtk-theme-
# name here is always the one real option, not a broken attempt at dark
# mode.
cat > "$HOME/.gtkrc-2.0" <<EOF
gtk-theme-name="Adwaita"
gtk-icon-theme-name="$icon_theme"
gtk-font-name="Noto Sans 10"
EOF

View file

@ -0,0 +1,105 @@
#!/usr/bin/env bash
# Regenerates qt6ct's config + color scheme to match the current Quickshell
# theme, the Qt-side equivalent of gtk-theme-set. Qt apps (Gwenview, Ark,
# Kate, ...) had no working platform theme plugin at all before this — the
# QT_QPA_PLATFORMTHEME=qt6ct env var (hyprland.conf) was pointing at a
# plugin that wasn't even installed, so every Qt app fell back to
# whatever default style/palette it shipped with, ignoring the rest of
# the desktop entirely. style=Fusion (not a KDE/Breeze style) matters
# specifically: Fusion paints straight from QPalette, so a custom color
# scheme here is guaranteed to actually show — Breeze-family styles
# prefer reading colors from kdeglobals instead, which is a separate,
# static, unrelated leftover file on this machine.
set -euo pipefail
accent="${1:?usage: qt-theme-set <accent> <bg> <bgAlt> <fg> <fgDim> <border> <dark:1|0>}"
bg="${2:?bg required}"
bg_alt="${3:?bgAlt required}"
fg="${4:?fg required}"
fg_dim="${5:?fgDim required}"
border="${6:?border required}"
dark="${7:?dark required}"
dir="$HOME/.config/qt6ct"
mkdir -p "$dir/colors"
# qt6ct color scheme files store QPalette::ColorRole values as a fixed-
# order, comma-separated #aarrggbb list — order and format verified
# against the example schemes qt6ct itself ships
# (share/qt6ct/colors/darker.conf etc.), not guessed: WindowText, Button,
# Light, Midlight, Dark, Mid, Text, BrightText, ButtonText, Base, Window,
# Shadow, Highlight, HighlightedText, Link, LinkVisited, AlternateBase,
# NoRole, ToolTipBase, ToolTipText, PlaceholderText.
a() { printf '#ff%s' "${1#\#}"; } # opaque
t() { printf '#80%s' "${1#\#}"; } # translucent, for PlaceholderText
active="$(a "$fg"), $(a "$bg_alt"), $(a "$border"), $(a "$bg_alt"), $(a "$bg"), $(a "$border"), $(a "$fg"), $(a "$fg"), $(a "$fg"), $(a "$bg"), $(a "$bg"), $(a "$border"), $(a "$accent"), $(a "$bg"), $(a "$accent"), $(a "$accent"), $(a "$bg_alt"), $(a "$bg"), $(a "$bg_alt"), $(a "$fg"), $(t "$fg_dim")"
disabled="$(a "$fg_dim"), $(a "$bg_alt"), $(a "$border"), $(a "$bg_alt"), $(a "$bg"), $(a "$border"), $(a "$fg_dim"), $(a "$fg_dim"), $(a "$fg_dim"), $(a "$bg"), $(a "$bg"), $(a "$border"), $(a "$accent"), $(a "$fg_dim"), $(a "$accent"), $(a "$accent"), $(a "$bg_alt"), $(a "$bg"), $(a "$bg_alt"), $(a "$fg_dim"), $(t "$fg_dim")"
color_file="$dir/colors/quickshell-theme.conf"
cat > "$color_file" <<EOF
[ColorScheme]
active_colors=$active
disabled_colors=$disabled
inactive_colors=$active
EOF
icon_theme="Gruvbox-Plus-Light"
[ "$dark" = "1" ] && icon_theme="Gruvbox-Plus-Dark"
cat > "$dir/qt6ct.conf" <<EOF
[Appearance]
style=Fusion
icon_theme=$icon_theme
custom_palette=true
color_scheme_path=$color_file
standard_dialogs=default
EOF
# qt6ct's palette only reaches classic QWidget UI. Modern KDE apps
# (confirmed live: Gwenview) draw their toolbar/chrome with Kirigami/QML
# instead, which ignores QPalette entirely and reads kdeglobals' color
# groups directly via KColorScheme — so without this, qt6ct alone left
# exactly that chrome stuck on whatever this machine's stale leftover
# kdeglobals had (unrelated purple KDE colors from some earlier install),
# while everything else (dialogs, the actual image viewport, ...)
# correctly followed the theme.
rgb() {
local hex="${1#\#}"
printf '%d,%d,%d' "0x${hex:0:2}" "0x${hex:2:2}" "0x${hex:4:2}"
}
cat > "$HOME/.config/kdeglobals" <<EOF
[General]
ColorScheme=QuickshellTheme
widgetStyle=Fusion
[KDE]
widgetStyle=Fusion
[Colors:Window]
BackgroundNormal=$(rgb "$bg_alt")
ForegroundNormal=$(rgb "$fg")
[Colors:View]
BackgroundNormal=$(rgb "$bg")
ForegroundNormal=$(rgb "$fg")
[Colors:Button]
BackgroundNormal=$(rgb "$bg_alt")
ForegroundNormal=$(rgb "$fg")
[Colors:Selection]
BackgroundNormal=$(rgb "$accent")
ForegroundNormal=$(rgb "$bg")
DecorationFocus=$(rgb "$accent")
DecorationHover=$(rgb "$accent")
[Colors:Tooltip]
BackgroundNormal=$(rgb "$bg_alt")
ForegroundNormal=$(rgb "$fg")
[Colors:Header]
BackgroundNormal=$(rgb "$bg_alt")
ForegroundNormal=$(rgb "$fg")
EOF

View file

@ -13,6 +13,16 @@
adwaita-icon-theme
gruvbox-plus-icons
dconf
# Qt's side of the same live-theming pipeline as GTK above — see
# qt-theme-set (modules/scripts/qt-theme-set.sh). Without this
# installed, QT_QPA_PLATFORMTHEME=qt6ct (hyprland.conf) pointed at a
# platform theme plugin that didn't exist, so every Qt app (Gwenview,
# Ark, Kate, ...) silently fell back to its own default style/palette
# instead of anything resembling the rest of the desktop.
qt6Packages.qt6ct
kdePackages.qqc2-desktop-style # QQC2 style needed for QT_QUICK_CONTROLS_STYLE=
# org.kde.desktop (hyprland.conf) to actually
# theme Kirigami-based apps' chrome
];
# No environment.sessionVariables.GTK_THEME here on purpose: GTK_THEME
@ -25,11 +35,16 @@
systemd.tmpfiles.rules = [
"d /home/nexoniarz/.config/gtk-3.0 0755 nexoniarz users -"
"d /home/nexoniarz/.config/gtk-4.0 0755 nexoniarz users -"
"d /home/nexoniarz/.config/qt6ct 0755 nexoniarz users -"
"d /home/nexoniarz/.config/qt6ct/colors 0755 nexoniarz users -"
# `f`, not `L+`: created empty only if missing, never overwritten on
# rebuild — gtk-theme-set owns the actual content from here on.
# rebuild — gtk-theme-set/qt-theme-set own the actual content from
# here on.
"f /home/nexoniarz/.config/gtk-3.0/gtk.css 0644 nexoniarz users - -"
"f /home/nexoniarz/.config/gtk-4.0/gtk.css 0644 nexoniarz users - -"
"f /home/nexoniarz/.config/gtk-3.0/settings.ini 0644 nexoniarz users - -"
"f /home/nexoniarz/.config/gtk-4.0/settings.ini 0644 nexoniarz users - -"
"f /home/nexoniarz/.gtkrc-2.0 0644 nexoniarz users - -"
"f /home/nexoniarz/.config/qt6ct/qt6ct.conf 0644 nexoniarz users - -"
];
}