{ pkgs, config, lib, ... }: let cfg = config.stylix.targets.kde; inherit (config.lib.stylix) colors mkEnableTarget mkEnableWallpaper; inherit (config.stylix) image; mergeWithImage = default: withImage: let satisfies = check: (check default) && (check withImage); in if image == null || !cfg.useWallpaper then default else if satisfies lib.isString then default + withImage else if satisfies lib.isAttrs then default // withImage else if satisfies lib.isList then default ++ withImage else throw "unreachable (image merge in stylix KDE module)"; formatValue = value: if lib.isBool value then if value then "true" else "false" else toString value; formatSection = path: data: let header = lib.concatStrings (map (p: "[${p}]") path); formatChild = name: formatLines (path ++ [ name ]); children = lib.mapAttrsToList formatChild data; partitioned = lib.partition lib.isString children; directChildren = partitioned.right; indirectChildren = partitioned.wrong; in lib.optional (directChildren != [ ]) header ++ directChildren ++ lib.flatten indirectChildren; formatLines = path: data: if lib.isAttrs data then if data ? _immutable then if lib.isAttrs data.value then formatSection (path ++ [ "$i" ]) data.value else "${lib.last path}[$i]=${formatValue data.value}" else formatSection path data else "${lib.last path}=${formatValue data}"; formatConfig = data: lib.concatStringsSep "\n" (formatLines [ ] data); # Marking a setting as immutable should prevent it being overwritten # through the system settings menu. makeImmutable = value: { _immutable = true; inherit value; }; # PascalCase is the standard naming for color scheme files. Schemes named # in kebab-case will load when selected manually, but don't work with a # look and feel package. colorschemeSlug = lib.concatStrings ( lib.filter lib.isString (builtins.split "[^a-zA-Z]" colors.scheme) ); colorEffect = { ColorEffect = 0; ColorAmount = 0; ContrastEffect = 1; ContrastAmount = 0.5; IntensityEffect = 0; IntensityAmount = 0; }; mkColorTriple = name: lib.concatStringsSep "," ( map (color: colors."${name}-rgb-${color}") [ "r" "g" "b" ] ); mkColorMapping = num: let hex = "base0${lib.toHexString num}"; in { name = hex; value = mkColorTriple hex; }; colors' = builtins.listToAttrs (map mkColorMapping (lib.range 0 15)); kdecolors = with colors'; { BackgroundNormal = base00; BackgroundAlternate = base01; DecorationFocus = base0D; DecorationHover = base0D; ForegroundNormal = base05; ForegroundActive = base05; ForegroundInactive = base05; ForegroundLink = base05; ForegroundVisited = base05; ForegroundNegative = base08; ForegroundNeutral = base0D; ForegroundPositive = base0B; }; colorscheme = { General = { ColorScheme = colorschemeSlug; Name = colors.scheme; }; "ColorEffects:Disabled" = colorEffect; "ColorEffects:Inactive" = colorEffect; "Colors:Window" = kdecolors; "Colors:View" = kdecolors; "Colors:Button" = kdecolors; "Colors:Tooltip" = kdecolors; "Colors:Complementary" = kdecolors; "Colors:Selection" = kdecolors // (with colors'; { BackgroundNormal = base0D; BackgroundAlternate = base0D; ForegroundNormal = base00; ForegroundActive = base00; ForegroundInactive = base00; ForegroundLink = base00; ForegroundVisited = base00; }); WM = with colors'; { activeBlend = base0A; activeBackground = base00; activeForeground = base05; inactiveBlend = base03; inactiveBackground = base00; inactiveForeground = base05; }; }; Id = "stylix"; Name = "Stylix"; wallpaperMetadata = { KPlugin = { inherit Id Name; }; }; lookAndFeelMetadata = { KPlugin = { inherit Id Name; Description = "Generated from your Home Manager configuration"; ServiceTypes = [ "Plasma/LookAndFeel" ]; Website = "https://github.com/nix-community/stylix"; }; KPackageStructure = "Plasma/LookAndFeel"; }; lookAndFeelDefaults = mergeWithImage { kwinrc."org.kde.kdecoration2".library = cfg.decorations; plasmarc.Theme.name = "default"; kdeglobals = { KDE.widgetStyle = "Breeze"; General.ColorScheme = colorschemeSlug; }; } { # This only takes effect on the first login. Wallpaper.Image = Id; }; # Contains an optional wallpaper package, a colorscheme file, and a look and feel # package which depends on both. themePackage = pkgs.runCommandLocal "stylix-kde-theme" (mergeWithImage { colorscheme = formatConfig colorscheme; lookAndFeelMetadata = builtins.toJSON lookAndFeelMetadata; lookAndFeelDefaults = formatConfig lookAndFeelDefaults; } { wallpaperMetadata = builtins.toJSON wallpaperMetadata; wallpaperImage = image; } ) ( mergeWithImage '' write_text() { mkdir --parents "$(dirname "$2")" printf '%s\n' "$1" >"$2" } wallpaper="$out/share/wallpapers/${Id}" look_and_feel="$out/share/plasma/look-and-feel/${Id}" colorschemePath="$out/share/color-schemes/${colorschemeSlug}.colors" write_text "$colorscheme" "$colorschemePath" write_text "$lookAndFeelMetadata" "$look_and_feel/metadata.json" write_text "$lookAndFeelDefaults" "$look_and_feel/contents/defaults" '' '' PATH="${lib.getBin pkgs.imagemagick}/bin:$PATH" mkdir --parents "$wallpaper/contents/images" magick \ "$wallpaperImage" \ -thumbnail 400x250 \ "$wallpaper/contents/screenshot.png" dimensions="$(identify -ping -format '%wx%h' "$wallpaperImage")" magick "$wallpaperImage" "$wallpaper/contents/images/$dimensions.png" write_text "$wallpaperMetadata" "$wallpaper/metadata.json" '' ); # The cursor theme can be configured through a look and feel package, # however its size cannot. kcminputrc = { Mouse = { cursorSize = makeImmutable (toString config.stylix.cursor.size); cursorTheme = makeImmutable config.stylix.cursor.name; }; }; kded5rc = { # The gtkconfig module copies settings from KDE to the GTK configuration. # This blocks Home Manager activation because the same files are already # managed by Stylix. Module-gtkconfig = makeImmutable { autoload = false; }; }; kdeglobals = rec { KDE.LookAndFeelPackage = makeImmutable Id; General = with config.stylix.fonts; rec { font = makeImmutable "${sansSerif.name},${toString sizes.applications},-1,5,50,0,0,0,0,0"; fixed = makeImmutable "${monospace.name},${toString sizes.terminal},-1,5,50,0,0,0,0,0"; desktopFont = makeImmutable "${sansSerif.name},${toString sizes.desktop},-1,5,50,0,0,0,0,0"; menuFont = desktopFont; taskbarFont = desktopFont; toolBarFont = desktopFont; smallestReadableFont = desktopFont; }; WM.activeFont = General.desktopFont; UiSettings.ColorScheme = colorschemeSlug; }; configPackage = pkgs.runCommandLocal "stylix-kde-config" ( { kded5rc = formatConfig kded5rc; kdeglobals = formatConfig kdeglobals; } // (lib.optionalAttrs (config.stylix.cursor != null) { kcminputrc = formatConfig kcminputrc; }) ) ( '' mkdir "$out" printf '%s\n' "$kded5rc" >"$out/kded5rc" printf '%s\n' "$kdeglobals" >"$out/kdeglobals" '' + (lib.optionalString ( config.stylix.cursor != null ) ''printf '%s\n' "$kcminputrc" >"$out/kcminputrc"'') ); # plasma-apply-wallpaperimage is necessary to change the wallpaper # after the first login. # # plasma-apply-lookandfeel is only here to trigger a hot reload, the theme # would still be applied without it if you logged out and back in. # # Home Manager clears $PATH before running the activation script, but we # want to avoid installing these tools explicitly because that would pull # in large dependencies for people who aren't actually using KDE. # The workaround used is to assume a list of common paths where the tools # might be installed, and look there. The ideal solution would require # changes to KDE to make it possible to update the wallpaper through # config files alone. activatorPackage = pkgs.writeShellApplication { name = "stylix-kde-apply-plasma-theme"; text = mergeWithImage '' get_exe() { for directory in /run/current-system/sw/bin /usr/bin /bin; do if [[ -f "$directory/$1" ]]; then printf '%s\n' "$directory/$1" return 0 fi done echo "Skipping '$1': command not found" return 1 } if look_and_feel="$(get_exe plasma-apply-lookandfeel)"; then "$look_and_feel" --apply "${Id}" || echo "Failed plasma-apply-lookandfeel, ignoring error." fi '' '' if wallpaper_image="$(get_exe plasma-apply-wallpaperimage)"; then "$wallpaper_image" "${themePackage}/share/wallpapers/${Id}" || echo "Failed plasma-apply-wallpaperimage, ignoring error." fi ''; runtimeEnv.QT_QPA_PLATFORM = "minimal"; }; activator = lib.getExe activatorPackage; in { options.stylix.targets.kde = { enable = mkEnableTarget "KDE" true; useWallpaper = mkEnableWallpaper "KDE" true; decorations = lib.mkOption { type = lib.types.str; default = "org.kde.breeze"; description = '' The library for the window decorations theme. Decorations other than default `org.kde.breeze` may not be compatible with stylix. To list all available decorations, see the `library` key in the `org.kde.kdecoration2` section of `$HOME/.config/kwinrc` after imperatively applying the window decoration via the System Settings app. ''; }; }; config = lib.mkIf (config.stylix.enable && cfg.enable && pkgs.stdenv.hostPlatform.isLinux) { home = { packages = [ themePackage ]; # This activation entry will run the theme activator when the homeConfiguration is activated activation.stylixLookAndFeel = lib.hm.dag.entryAfter [ "writeBoundary" ] '' run ${activator} || verboseEcho \ "Stylix KDE theme setting failed. This only works in a running Plasma session." ''; }; xdg = { systemDirs.config = [ "${configPackage}" ]; # This desktop entry will run the theme activator when a new Plasma session is started # Note: This doesn't run again if a new homeConfiguration is activated from a running Plasma session configFile."autostart/stylix-activate-kde.desktop".text = '' [Desktop Entry] Type=Application Exec=${activator} Name=Stylix: activate KDE theme X-KDE-AutostartScript=true ''; }; }; }