Add niri to work machine
This commit is contained in:
parent
67331242df
commit
b22d24f619
7
.gitignore
vendored
7
.gitignore
vendored
@ -1,2 +1,9 @@
|
||||
**/node_modules/**
|
||||
**/*.db
|
||||
|
||||
# Purchased wallpapers - not for distribution
|
||||
shared/modules/services/wallpapers/**/*.jpg
|
||||
shared/modules/services/wallpapers/**/*.png
|
||||
shared/modules/services/wallpapers/**/*.jpeg
|
||||
shared/modules/services/wallpapers/**/*.webp
|
||||
shared/modules/services/wallpapers/**/*.avif
|
||||
|
||||
12
flake.lock
generated
12
flake.lock
generated
@ -94,11 +94,11 @@
|
||||
},
|
||||
"nixpkgs-unstable": {
|
||||
"locked": {
|
||||
"lastModified": 1765186076,
|
||||
"narHash": "sha256-hM20uyap1a0M9d344I692r+ik4gTMyj60cQWO+hAYP8=",
|
||||
"lastModified": 1769018530,
|
||||
"narHash": "sha256-MJ27Cy2NtBEV5tsK+YraYr2g851f3Fl1LpNHDzDX15c=",
|
||||
"owner": "nixos",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "addf7cf5f383a3101ecfba091b98d0a1263dc9b8",
|
||||
"rev": "88d3861acdd3d2f0e361767018218e51810df8a1",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@ -110,11 +110,11 @@
|
||||
},
|
||||
"nixpkgs_2": {
|
||||
"locked": {
|
||||
"lastModified": 1765311797,
|
||||
"narHash": "sha256-mSD5Ob7a+T2RNjvPvOA1dkJHGVrNVl8ZOrAwBjKBDQo=",
|
||||
"lastModified": 1769089682,
|
||||
"narHash": "sha256-9yA/LIuAVQq0lXelrZPjLuLVuZdm03p8tfmHhnDIkms=",
|
||||
"owner": "nixos",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "09eb77e94fa25202af8f3e81ddc7353d9970ac1b",
|
||||
"rev": "078d69f03934859a181e81ba987c2bb033eebfc5",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
||||
@ -1,13 +1,13 @@
|
||||
{ config, lib, inputs, outputs, pkgs, timeZone, system, ... }:
|
||||
let
|
||||
supportedDesktops = [ "sway" "hyprland" ];
|
||||
supportedDesktops = [ "sway" "hyprland" "niri" ];
|
||||
supportedDesktopsStr = lib.strings.concatStringsSep ", " supportedDesktops;
|
||||
deskCfg = config.deskCfg;
|
||||
in
|
||||
{
|
||||
options.deskCfg = {
|
||||
de = lib.mkOption {
|
||||
default = "sway";
|
||||
default = "niri";
|
||||
type = lib.types.str;
|
||||
description = "Desktop Environment";
|
||||
};
|
||||
@ -30,8 +30,10 @@ in
|
||||
modules/user/main_user.nix
|
||||
modules/sway/sway_conf.nix
|
||||
modules/hypr/hyprland.nix
|
||||
modules/niri/niri_conf.nix
|
||||
../shared/modules/system/power_manager.nix
|
||||
../shared/modules/services/motu-m4-combined.nix
|
||||
# ../shared/modules/services/theme_switcher/default.nix
|
||||
# inputs.nur.hmModules.nur
|
||||
];
|
||||
|
||||
@ -88,7 +90,16 @@ in
|
||||
};
|
||||
|
||||
hypr = {
|
||||
enable = false;
|
||||
user = deskCfg.userName;
|
||||
systemPackages = with pkgs; [
|
||||
libreoffice
|
||||
];
|
||||
};
|
||||
|
||||
niriwm = {
|
||||
enable = true;
|
||||
useNonFree = true;
|
||||
user = deskCfg.userName;
|
||||
systemPackages = with pkgs; [
|
||||
libreoffice
|
||||
|
||||
@ -1,342 +0,0 @@
|
||||
* {
|
||||
all: unset;
|
||||
font-size: 14px;
|
||||
font-family: "Ubuntu Nerd Font";
|
||||
transition: 200ms;
|
||||
}
|
||||
|
||||
trough highlight {
|
||||
background: #cad3f5;
|
||||
}
|
||||
|
||||
scale trough {
|
||||
margin: 0rem 1rem;
|
||||
background-color: #363a4f;
|
||||
min-height: 8px;
|
||||
min-width: 70px;
|
||||
}
|
||||
|
||||
slider {
|
||||
background-color: #8aadf4;
|
||||
}
|
||||
|
||||
.floating-notifications.background .notification-row .notification-background {
|
||||
box-shadow: 0 0 8px 0 rgba(0, 0, 0, 0.8), inset 0 0 0 1px #363a4f;
|
||||
border-radius: 12.6px;
|
||||
margin: 18px;
|
||||
background-color: #24273a;
|
||||
color: #cad3f5;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.floating-notifications.background .notification-row .notification-background .notification {
|
||||
padding: 7px;
|
||||
border-radius: 12.6px;
|
||||
}
|
||||
|
||||
.floating-notifications.background .notification-row .notification-background .notification.critical {
|
||||
box-shadow: inset 0 0 7px 0 #ed8796;
|
||||
}
|
||||
|
||||
.floating-notifications.background .notification-row .notification-background .notification .notification-content {
|
||||
margin: 7px;
|
||||
}
|
||||
|
||||
.floating-notifications.background .notification-row .notification-background .notification .notification-content .summary {
|
||||
color: #cad3f5;
|
||||
}
|
||||
|
||||
.floating-notifications.background .notification-row .notification-background .notification .notification-content .time {
|
||||
color: #a5adcb;
|
||||
}
|
||||
|
||||
.floating-notifications.background .notification-row .notification-background .notification .notification-content .body {
|
||||
color: #cad3f5;
|
||||
}
|
||||
|
||||
.floating-notifications.background .notification-row .notification-background .notification > *:last-child > * {
|
||||
min-height: 3.4em;
|
||||
}
|
||||
|
||||
.floating-notifications.background .notification-row .notification-background .notification > *:last-child > * .notification-action {
|
||||
border-radius: 7px;
|
||||
color: #cad3f5;
|
||||
background-color: #363a4f;
|
||||
box-shadow: inset 0 0 0 1px #494d64;
|
||||
margin: 7px;
|
||||
}
|
||||
|
||||
.floating-notifications.background .notification-row .notification-background .notification > *:last-child > * .notification-action:hover {
|
||||
box-shadow: inset 0 0 0 1px #494d64;
|
||||
background-color: #363a4f;
|
||||
color: #cad3f5;
|
||||
}
|
||||
|
||||
.floating-notifications.background .notification-row .notification-background .notification > *:last-child > * .notification-action:active {
|
||||
box-shadow: inset 0 0 0 1px #494d64;
|
||||
background-color: #7dc4e4;
|
||||
color: #cad3f5;
|
||||
}
|
||||
|
||||
.floating-notifications.background .notification-row .notification-background .close-button {
|
||||
margin: 7px;
|
||||
padding: 2px;
|
||||
border-radius: 6.3px;
|
||||
color: #24273a;
|
||||
background-color: #ed8796;
|
||||
}
|
||||
|
||||
.floating-notifications.background .notification-row .notification-background .close-button:hover {
|
||||
background-color: #ee99a0;
|
||||
color: #24273a;
|
||||
}
|
||||
|
||||
.floating-notifications.background .notification-row .notification-background .close-button:active {
|
||||
background-color: #ed8796;
|
||||
color: #24273a;
|
||||
}
|
||||
|
||||
.control-center {
|
||||
box-shadow: 0 0 8px 0 rgba(0, 0, 0, 0.8), inset 0 0 0 1px #363a4f;
|
||||
border-radius: 12.6px;
|
||||
margin: 18px;
|
||||
background-color: #24273a;
|
||||
color: #cad3f5;
|
||||
padding: 14px;
|
||||
}
|
||||
|
||||
.control-center .widget-title > label {
|
||||
color: #cad3f5;
|
||||
font-size: 1.3em;
|
||||
}
|
||||
|
||||
.control-center .widget-title button {
|
||||
border-radius: 7px;
|
||||
color: #cad3f5;
|
||||
background-color: #363a4f;
|
||||
box-shadow: inset 0 0 0 1px #494d64;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.control-center .widget-title button:hover {
|
||||
box-shadow: inset 0 0 0 1px #494d64;
|
||||
background-color: #5b6078;
|
||||
color: #cad3f5;
|
||||
}
|
||||
|
||||
.control-center .widget-title button:active {
|
||||
box-shadow: inset 0 0 0 1px #494d64;
|
||||
background-color: #7dc4e4;
|
||||
color: #24273a;
|
||||
}
|
||||
|
||||
.control-center .notification-row .notification-background {
|
||||
border-radius: 7px;
|
||||
color: #cad3f5;
|
||||
background-color: #363a4f;
|
||||
box-shadow: inset 0 0 0 1px #494d64;
|
||||
margin-top: 14px;
|
||||
}
|
||||
|
||||
.control-center .notification-row .notification-background .notification {
|
||||
padding: 7px;
|
||||
border-radius: 7px;
|
||||
}
|
||||
|
||||
.control-center .notification-row .notification-background .notification.critical {
|
||||
box-shadow: inset 0 0 7px 0 #ed8796;
|
||||
}
|
||||
|
||||
.control-center .notification-row .notification-background .notification .notification-content {
|
||||
margin: 7px;
|
||||
}
|
||||
|
||||
.control-center .notification-row .notification-background .notification .notification-content .summary {
|
||||
color: #cad3f5;
|
||||
}
|
||||
|
||||
.control-center .notification-row .notification-background .notification .notification-content .time {
|
||||
color: #a5adcb;
|
||||
}
|
||||
|
||||
.control-center .notification-row .notification-background .notification .notification-content .body {
|
||||
color: #cad3f5;
|
||||
}
|
||||
|
||||
.control-center .notification-row .notification-background .notification > *:last-child > * {
|
||||
min-height: 3.4em;
|
||||
}
|
||||
|
||||
.control-center .notification-row .notification-background .notification > *:last-child > * .notification-action {
|
||||
border-radius: 7px;
|
||||
color: #cad3f5;
|
||||
background-color: #181926;
|
||||
box-shadow: inset 0 0 0 1px #494d64;
|
||||
margin: 7px;
|
||||
}
|
||||
|
||||
.control-center .notification-row .notification-background .notification > *:last-child > * .notification-action:hover {
|
||||
box-shadow: inset 0 0 0 1px #494d64;
|
||||
background-color: #363a4f;
|
||||
color: #cad3f5;
|
||||
}
|
||||
|
||||
.control-center .notification-row .notification-background .notification > *:last-child > * .notification-action:active {
|
||||
box-shadow: inset 0 0 0 1px #494d64;
|
||||
background-color: #7dc4e4;
|
||||
color: #cad3f5;
|
||||
}
|
||||
|
||||
.control-center .notification-row .notification-background .close-button {
|
||||
margin: 7px;
|
||||
padding: 2px;
|
||||
border-radius: 6.3px;
|
||||
color: #24273a;
|
||||
background-color: #ee99a0;
|
||||
}
|
||||
|
||||
.close-button {
|
||||
border-radius: 6.3px;
|
||||
}
|
||||
|
||||
.control-center .notification-row .notification-background .close-button:hover {
|
||||
background-color: #ed8796;
|
||||
color: #24273a;
|
||||
}
|
||||
|
||||
.control-center .notification-row .notification-background .close-button:active {
|
||||
background-color: #ed8796;
|
||||
color: #24273a;
|
||||
}
|
||||
|
||||
.control-center .notification-row .notification-background:hover {
|
||||
box-shadow: inset 0 0 0 1px #494d64;
|
||||
background-color: #8087a2;
|
||||
color: #cad3f5;
|
||||
}
|
||||
|
||||
.control-center .notification-row .notification-background:active {
|
||||
box-shadow: inset 0 0 0 1px #494d64;
|
||||
background-color: #7dc4e4;
|
||||
color: #cad3f5;
|
||||
}
|
||||
|
||||
.notification.critical progress {
|
||||
background-color: #ed8796;
|
||||
}
|
||||
|
||||
.notification.low progress,
|
||||
.notification.normal progress {
|
||||
background-color: #8aadf4;
|
||||
}
|
||||
|
||||
.control-center-dnd {
|
||||
margin-top: 5px;
|
||||
border-radius: 8px;
|
||||
background: #363a4f;
|
||||
border: 1px solid #494d64;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.control-center-dnd:checked {
|
||||
background: #363a4f;
|
||||
}
|
||||
|
||||
.control-center-dnd slider {
|
||||
background: #494d64;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.widget-dnd {
|
||||
margin: 0px;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.widget-dnd > switch {
|
||||
font-size: initial;
|
||||
border-radius: 8px;
|
||||
background: #363a4f;
|
||||
border: 1px solid #494d64;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.widget-dnd > switch:checked {
|
||||
background: #363a4f;
|
||||
}
|
||||
|
||||
.widget-dnd > switch slider {
|
||||
background: #494d64;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #6e738d;
|
||||
}
|
||||
|
||||
.widget-mpris .widget-mpris-player {
|
||||
background: #363a4f;
|
||||
padding: 7px;
|
||||
}
|
||||
|
||||
.widget-mpris .widget-mpris-title {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.widget-mpris .widget-mpris-subtitle {
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.widget-menubar > box > .menu-button-bar > button > label {
|
||||
font-size: 3rem;
|
||||
padding: 0.5rem 2rem;
|
||||
}
|
||||
|
||||
.widget-menubar > box > .menu-button-bar > :last-child {
|
||||
color: #ed8796;
|
||||
}
|
||||
|
||||
.power-buttons button:hover,
|
||||
.powermode-buttons button:hover,
|
||||
.screenshot-buttons button:hover {
|
||||
background: #363a4f;
|
||||
}
|
||||
|
||||
.control-center .widget-label > label {
|
||||
color: #cad3f5;
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.widget-buttons-grid {
|
||||
padding-top: 1rem;
|
||||
}
|
||||
|
||||
.widget-buttons-grid > flowbox > flowboxchild > button label {
|
||||
font-size: 2.5rem;
|
||||
}
|
||||
|
||||
.widget-volume {
|
||||
padding-top: 1rem;
|
||||
}
|
||||
|
||||
.widget-volume label {
|
||||
font-size: 1.5rem;
|
||||
color: #7dc4e4;
|
||||
}
|
||||
|
||||
.widget-volume trough highlight {
|
||||
background: #7dc4e4;
|
||||
}
|
||||
|
||||
.widget-backlight trough highlight {
|
||||
background: #eed49f;
|
||||
}
|
||||
|
||||
.widget-backlight label {
|
||||
font-size: 1.5rem;
|
||||
color: #eed49f;
|
||||
}
|
||||
|
||||
.widget-backlight .KB {
|
||||
padding-bottom: 1rem;
|
||||
}
|
||||
|
||||
.image {
|
||||
padding-right: 0.5rem;
|
||||
}
|
||||
@ -56,9 +56,10 @@ general {
|
||||
# Decoration
|
||||
decoration {
|
||||
rounding = 12
|
||||
inactive_opacity = 0.9
|
||||
active_opacity = 0.98
|
||||
inactive_opacity = 0.95
|
||||
dim_inactive = true
|
||||
dim_strength = 0.1
|
||||
dim_strength = 0.03
|
||||
|
||||
blur {
|
||||
enabled = true
|
||||
@ -67,7 +68,7 @@ decoration {
|
||||
}
|
||||
|
||||
shadow {
|
||||
enabled = false
|
||||
enabled = true
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
374
nate-work/linked-dotfiles/niri/config.kdl
Normal file
374
nate-work/linked-dotfiles/niri/config.kdl
Normal file
@ -0,0 +1,374 @@
|
||||
//
|
||||
// MISCELLANEOUS
|
||||
//
|
||||
|
||||
// gui startup
|
||||
spawn-at-startup "waybar"
|
||||
spawn-at-startup "keepassxc"
|
||||
spawn-at-startup "flatpak" "run" "org.signal.Signal"
|
||||
// shell startup
|
||||
spawn-sh-at-startup "kanshi"
|
||||
spawn-sh-at-startup "sleep 5 && nm-applet --indicator"
|
||||
spawn-sh-at-startup "sleep 5 && syncthingtray --wait"
|
||||
spawn-sh-at-startup "sleep 5 && swaync"
|
||||
|
||||
screenshot-path null // save screenshots just to clipboard
|
||||
prefer-no-csd // (Client Side Decorations) ask clients to not add their own decorations
|
||||
|
||||
hotkey-overlay {
|
||||
skip-at-startup
|
||||
}
|
||||
|
||||
//
|
||||
// OUTPUTS
|
||||
//
|
||||
output "HDMI-A-1" {
|
||||
scale 1.0
|
||||
variable-refresh-rate on-demand=true
|
||||
transform "normal"
|
||||
}
|
||||
|
||||
workspace "chat" {
|
||||
layout {
|
||||
always-center-single-column
|
||||
}
|
||||
}
|
||||
workspace "net"
|
||||
workspace "term"
|
||||
workspace "scratch"
|
||||
|
||||
//
|
||||
// INPUTS
|
||||
//
|
||||
input {
|
||||
keyboard {
|
||||
xkb {
|
||||
layout "us"
|
||||
}
|
||||
repeat-delay 175
|
||||
repeat-rate 50
|
||||
}
|
||||
|
||||
touchpad {
|
||||
tap
|
||||
dwt
|
||||
dwtp
|
||||
natural-scroll
|
||||
accel-speed 0.2
|
||||
accel-profile "adaptive"
|
||||
}
|
||||
|
||||
mouse {
|
||||
accel-speed 0.2
|
||||
accel-profile "adaptive"
|
||||
}
|
||||
|
||||
trackpoint {
|
||||
accel-speed 0.2
|
||||
accel-profile "adaptive"
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
//
|
||||
// LAYOUT
|
||||
//
|
||||
layout {
|
||||
gaps 2
|
||||
|
||||
center-focused-column "on-overflow"
|
||||
always-center-single-column
|
||||
default-column-display "normal"
|
||||
|
||||
tab-indicator {
|
||||
hide-when-single-tab
|
||||
}
|
||||
|
||||
preset-column-widths {
|
||||
proportion 0.33333
|
||||
proportion 0.5
|
||||
proportion 0.66667
|
||||
}
|
||||
|
||||
default-column-width {
|
||||
proportion 0.5;
|
||||
}
|
||||
|
||||
focus-ring {
|
||||
width 0
|
||||
}
|
||||
|
||||
border {
|
||||
active-gradient from="#eaa4a8" to="#cbeaa6" angle=45 in="oklch longer hue" relative-to="workspace-view"
|
||||
inactive-gradient from="#886D59" to="#517B65" angle=45 in="oklch longer hue" relative-to="workspace-view"
|
||||
}
|
||||
|
||||
shadow {
|
||||
softness 30
|
||||
spread 5
|
||||
offset x=8 y=8
|
||||
draw-behind-window true
|
||||
color "#00444444"
|
||||
}
|
||||
|
||||
struts {
|
||||
left 10
|
||||
right 10
|
||||
top 10
|
||||
bottom 10
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//
|
||||
// WINDOW RULES
|
||||
//
|
||||
|
||||
// All windows - corner radius, opacity
|
||||
window-rule {
|
||||
geometry-corner-radius 14 14 0 14
|
||||
clip-to-geometry true
|
||||
}
|
||||
|
||||
window-rule {
|
||||
match is-active=true
|
||||
opacity 0.99
|
||||
}
|
||||
|
||||
window-rule {
|
||||
match is-floating=true
|
||||
opacity 0.92
|
||||
}
|
||||
|
||||
// Term windows
|
||||
window-rule {
|
||||
match app-id="com.mitchellh.ghostty"
|
||||
|
||||
default-column-width { proportion 0.5; }
|
||||
open-on-workspace "term"
|
||||
}
|
||||
|
||||
// Net windows
|
||||
window-rule {
|
||||
match app-id="firefox"
|
||||
|
||||
default-column-width { proportion 1.0; }
|
||||
open-on-workspace "net"
|
||||
}
|
||||
|
||||
// Scratch windows
|
||||
window-rule {
|
||||
match app-id=r#"^org\.keepassxc\.KeePassXC$"#
|
||||
match app-id=r#"^org\.signal\.Signal$"#
|
||||
|
||||
block-out-from "screencast"
|
||||
open-on-workspace "scratch"
|
||||
}
|
||||
|
||||
// Bright border on screen-shared windows
|
||||
window-rule {
|
||||
match is-window-cast-target=true
|
||||
|
||||
focus-ring {
|
||||
active-color "#f38ba8"
|
||||
inactive-color "#7d0d2d"
|
||||
}
|
||||
|
||||
border {
|
||||
inactive-color "#7d0d2d"
|
||||
}
|
||||
|
||||
shadow {
|
||||
color "#7d0d2d70"
|
||||
}
|
||||
|
||||
tab-indicator {
|
||||
active-color "#f38ba8"
|
||||
inactive-color "#7d0d2d"
|
||||
}
|
||||
}
|
||||
|
||||
// Block out notifications from screencasts.
|
||||
layer-rule {
|
||||
match namespace="^notifications$"
|
||||
|
||||
block-out-from "screencast"
|
||||
}
|
||||
|
||||
// App specific window rules
|
||||
window-rule {
|
||||
match app-id=r#"^com.slack.Slack$"#
|
||||
|
||||
open-on-workspace "chat"
|
||||
}
|
||||
|
||||
//
|
||||
// BINDS
|
||||
//
|
||||
|
||||
binds {
|
||||
Mod+Shift+Slash { show-hotkey-overlay; }
|
||||
|
||||
// Terminal - consistent with sway/hyprland
|
||||
Mod+Return { spawn "ghostty"; }
|
||||
|
||||
// Application launcher - consistent with sway/hyprland
|
||||
Mod+D { spawn "wofi" "--show" "drun"; }
|
||||
|
||||
// File manager - consistent with sway/hyprland
|
||||
Mod+T { spawn "nautilus"; }
|
||||
|
||||
// Browser - consistent with sway/hyprland
|
||||
Mod+W { spawn "firefox"; }
|
||||
|
||||
// Close window - consistent with sway/hyprland
|
||||
Mod+Q { close-window; }
|
||||
|
||||
// Arrow keys for focus movement
|
||||
Mod+Left { focus-column-left; }
|
||||
Mod+Down { focus-window-down; }
|
||||
Mod+Up { focus-window-up; }
|
||||
Mod+Right { focus-column-right; }
|
||||
|
||||
// Colemak-DH keys
|
||||
Mod+N { focus-column-left; }
|
||||
Mod+I { focus-window-down; }
|
||||
Mod+E { focus-window-up; }
|
||||
Mod+O { focus-column-right; }
|
||||
|
||||
// Arrow keys for window movement
|
||||
Mod+Shift+Left { move-column-left; }
|
||||
Mod+Shift+Down { move-window-down; }
|
||||
Mod+Shift+Up { move-window-up; }
|
||||
Mod+Shift+Right { move-column-right; }
|
||||
|
||||
// Colemak-DH keys for window movement
|
||||
Mod+Shift+N { move-column-left; }
|
||||
Mod+Shift+I { move-window-down; }
|
||||
Mod+Shift+E { move-window-up; }
|
||||
Mod+Shift+O { move-column-right; }
|
||||
|
||||
Mod+Home { focus-column-first; }
|
||||
Mod+End { focus-column-last; }
|
||||
Mod+Ctrl+Home { move-column-to-first; }
|
||||
Mod+Ctrl+End { move-column-to-last; }
|
||||
|
||||
// Monitor focus (keeping existing Shift pattern since this conflicts with window movement)
|
||||
Mod+Ctrl+Left { focus-monitor-left; }
|
||||
Mod+Ctrl+Down { focus-monitor-down; }
|
||||
Mod+Ctrl+Up { focus-monitor-up; }
|
||||
Mod+Ctrl+Right { focus-monitor-right; }
|
||||
|
||||
// Colemak-DH keys for monitor focus
|
||||
Mod+Ctrl+N { focus-monitor-left; }
|
||||
Mod+Ctrl+I { focus-monitor-down; }
|
||||
Mod+Ctrl+E { focus-monitor-up; }
|
||||
Mod+Ctrl+O { focus-monitor-right; }
|
||||
|
||||
// Move column to monitor
|
||||
Mod+Shift+Ctrl+Left { move-column-to-monitor-left; }
|
||||
Mod+Shift+Ctrl+Down { move-column-to-monitor-down; }
|
||||
Mod+Shift+Ctrl+Up { move-column-to-monitor-up; }
|
||||
Mod+Shift+Ctrl+Right { move-column-to-monitor-right; }
|
||||
|
||||
// Colemak-DH keys for moving column to monitor
|
||||
Mod+Shift+Ctrl+N { move-column-to-monitor-left; }
|
||||
Mod+Shift+Ctrl+I { move-column-to-monitor-down; }
|
||||
Mod+Shift+Ctrl+E { move-column-to-monitor-up; }
|
||||
Mod+Shift+Ctrl+O { move-column-to-monitor-right; }
|
||||
|
||||
// Workspace navigation (using Page keys to avoid conflicts with Colemak-DH movement)
|
||||
Mod+Page_Down { focus-workspace-down; }
|
||||
Mod+Page_Up { focus-workspace-up; }
|
||||
Mod+Ctrl+Page_Down { move-column-to-workspace-down; }
|
||||
Mod+Ctrl+Page_Up { move-column-to-workspace-up; }
|
||||
|
||||
Mod+Shift+Page_Down { move-workspace-down; }
|
||||
Mod+Shift+Page_Up { move-workspace-up; }
|
||||
|
||||
Mod+Minus { focus-workspace "scratch"; }
|
||||
Mod+1 { focus-workspace "term"; }
|
||||
Mod+2 { focus-workspace "net"; }
|
||||
Mod+3 { focus-workspace "chat"; }
|
||||
Mod+4 { focus-workspace 1; }
|
||||
Mod+5 { focus-workspace 2; }
|
||||
Mod+6 { focus-workspace 3; }
|
||||
Mod+7 { focus-workspace 4; }
|
||||
Mod+8 { focus-workspace 5; }
|
||||
Mod+9 { focus-workspace 6; }
|
||||
Mod+Shift+Minus { move-column-to-workspace "scratch"; }
|
||||
Mod+Shift+1 { move-column-to-workspace "term"; }
|
||||
Mod+Shift+2 { move-column-to-workspace "net"; }
|
||||
Mod+Shift+3 { move-column-to-workspace "chat"; }
|
||||
Mod+Shift+4 { move-column-to-workspace 1; }
|
||||
Mod+Shift+5 { move-column-to-workspace 2; }
|
||||
Mod+Shift+6 { move-column-to-workspace 3; }
|
||||
Mod+Shift+7 { move-column-to-workspace 4; }
|
||||
Mod+Shift+8 { move-column-to-workspace 5; }
|
||||
Mod+Shift+9 { move-column-to-workspace 6; }
|
||||
|
||||
Mod+Comma { consume-window-into-column; }
|
||||
Mod+Period { expel-window-from-column; }
|
||||
|
||||
Mod+R { switch-preset-column-width; }
|
||||
Mod+Shift+R { reset-window-height; }
|
||||
// Fullscreen - consistent with sway/hyprland
|
||||
Mod+F { fullscreen-window; }
|
||||
Mod+Shift+F { maximize-column; }
|
||||
|
||||
// Floating toggle
|
||||
Mod+Space { toggle-window-floating; }
|
||||
Mod+Shift+Space { switch-focus-between-floating-and-tiling; }
|
||||
|
||||
Mod+C { center-column; }
|
||||
|
||||
Mod+bracketleft { set-column-width "-10%"; }
|
||||
Mod+bracketright { set-column-width "+10%"; }
|
||||
|
||||
Mod+Shift+bracketleft { set-window-height "-10%"; }
|
||||
Mod+Shift+bracketright { set-window-height "+10%"; }
|
||||
|
||||
//
|
||||
// Utilities
|
||||
//
|
||||
|
||||
// Notifications
|
||||
Mod+Shift+C { spawn-sh "swaync-client --toggle-panel"; }
|
||||
|
||||
// Screenshots - consistent with sway/hyprland
|
||||
Mod+P { screenshot; }
|
||||
Mod+Shift+P { screenshot-screen; }
|
||||
|
||||
// Traditional screenshot keys
|
||||
Print { screenshot; }
|
||||
Ctrl+Print { screenshot-screen; }
|
||||
Alt+Print { screenshot-window; }
|
||||
|
||||
// Volume control
|
||||
XF86AudioRaiseVolume { spawn "pactl" "set-sink-volume" "@DEFAULT_SINK@" "+5%"; }
|
||||
XF86AudioLowerVolume { spawn "pactl" "set-sink-volume" "@DEFAULT_SINK@" "-5%"; }
|
||||
XF86AudioMute { spawn "pactl" "set-sink-mute" "@DEFAULT_SINK@" "toggle"; }
|
||||
XF86AudioMicMute { spawn "pactl" "set-source-mute" "@DEFAULT_SOURCE@" "toggle"; }
|
||||
|
||||
// Media control
|
||||
XF86AudioPlay { spawn "playerctl" "play-pause"; }
|
||||
XF86AudioPause { spawn "playerctl" "pause"; }
|
||||
XF86AudioNext { spawn "playerctl" "next"; }
|
||||
XF86AudioPrev { spawn "playerctl" "previous"; }
|
||||
XF86AudioStop { spawn "playerctl" "stop"; }
|
||||
|
||||
// Brightness control
|
||||
XF86MonBrightnessUp { spawn "brightnessctl" "set" "+5%"; }
|
||||
XF86MonBrightnessDown { spawn "brightnessctl" "set" "5%-"; }
|
||||
|
||||
Mod+Shift+Q { quit; }
|
||||
Mod+Shift+Ctrl+P { power-off-monitors; }
|
||||
|
||||
Mod+Shift+Ctrl+T { toggle-debug-tint; }
|
||||
}
|
||||
|
||||
switch-events {
|
||||
// lid-close { spawn "systemctl" "suspend"; }
|
||||
// lid-open { spawn "notify-send" "The laptop lid is open!"; }
|
||||
}
|
||||
|
||||
@ -12,7 +12,7 @@
|
||||
|
||||
imports = [
|
||||
../../../shared/modules/apps/firefox/firefox.nix
|
||||
../hypr/hypr_home.nix
|
||||
../niri/niri_home.nix
|
||||
../vpn-proxy/vpn-proxy.nix
|
||||
];
|
||||
|
||||
@ -41,7 +41,7 @@
|
||||
# Enable VPN proxy script
|
||||
vpnProxy.enable = true;
|
||||
|
||||
hyprhome = {
|
||||
nirihome = {
|
||||
enable = true;
|
||||
homePackages = with pkgs; [
|
||||
#
|
||||
@ -57,6 +57,7 @@
|
||||
cmake
|
||||
gcc
|
||||
oxker # docker desktop tui <3
|
||||
python3
|
||||
## nodejs frontend
|
||||
nodejs_24
|
||||
husky
|
||||
@ -329,6 +330,7 @@
|
||||
xdg.configFile = {
|
||||
# Active linked dotfiles
|
||||
"hypr".source = config.lib.file.mkOutOfStoreSymlink "/home/nate/nixos/nate-work/linked-dotfiles/hypr";
|
||||
"niri".source = config.lib.file.mkOutOfStoreSymlink "/home/nate/nixos/nate-work/linked-dotfiles/niri";
|
||||
"waybar".source = config.lib.file.mkOutOfStoreSymlink "/home/nate/nixos/nate-work/linked-dotfiles/waybar";
|
||||
# Shared
|
||||
"helix".source = config.lib.file.mkOutOfStoreSymlink "/home/nate/nixos/shared/linked-dotfiles/helix";
|
||||
|
||||
@ -237,6 +237,11 @@ in
|
||||
# package = unstable.mesa.drivers;
|
||||
enable32Bit = true;
|
||||
# package32 = unstable.pkgsi686Linux.mesa.drivers;
|
||||
extraPackages = with pkgs; [
|
||||
intel-vaapi-driver
|
||||
intel-media-driver
|
||||
vpl-gpu-rt
|
||||
];
|
||||
};
|
||||
nvidia = {
|
||||
# Modesetting is required.
|
||||
@ -275,42 +280,68 @@ in
|
||||
};
|
||||
};
|
||||
};
|
||||
# Create special on the go boot entry
|
||||
#
|
||||
## Commenting out for now as it doesn't really work with the laptop well...
|
||||
#
|
||||
# specialisation = {
|
||||
# on-the-go.configuration = {
|
||||
# system.nixos.tags = [ "on-the-go" ];
|
||||
# Create special on the go boot entry for battery saving
|
||||
# This disables NVIDIA GPU completely and uses Intel integrated graphics only
|
||||
specialisation = {
|
||||
on-the-go.configuration = {
|
||||
system.nixos.tags = [ "on-the-go" ];
|
||||
|
||||
# boot.extraModprobeConfig = ''
|
||||
# blacklist nouveau
|
||||
# options nouveau modeset=0
|
||||
# '';
|
||||
# Blacklist all NVIDIA kernel modules
|
||||
boot.blacklistedKernelModules = [ "nouveau" "nvidia" "nvidia_drm" "nvidia_modeset" "nvidia_uvm" ];
|
||||
|
||||
# Force Intel i915 driver for 13th gen (Raptor Lake) integrated graphics
|
||||
# Device IDs: a7a0 or a7a8 for i7-13700H
|
||||
boot.kernelParams = [
|
||||
"i915.force_probe=a7a0"
|
||||
"module_blacklist=nvidia,nvidia_drm,nvidia_modeset,nvidia_uvm"
|
||||
];
|
||||
|
||||
boot.extraModprobeConfig = ''
|
||||
blacklist nouveau
|
||||
blacklist nvidia
|
||||
blacklist nvidia_drm
|
||||
blacklist nvidia_modeset
|
||||
blacklist nvidia_uvm
|
||||
options nouveau modeset=0
|
||||
'';
|
||||
|
||||
# services.udev.extraRules = ''
|
||||
# # Remove NVIDIA USB xHCI Host Controller devices, if present
|
||||
# ACTION=="add", SUBSYSTEM=="pci", ATTR{vendor}=="0x10de", ATTR{class}=="0x0c0330", ATTR{power/control}="auto", ATTR{remove}="1"
|
||||
# # Remove NVIDIA USB Type-C UCSI devices, if present
|
||||
# ACTION=="add", SUBSYSTEM=="pci", ATTR{vendor}=="0x10de", ATTR{class}=="0x0c8000", ATTR{power/control}="auto", ATTR{remove}="1"
|
||||
# # Remove NVIDIA Audio devices, if present
|
||||
# ACTION=="add", SUBSYSTEM=="pci", ATTR{vendor}=="0x10de", ATTR{class}=="0x040300", ATTR{power/control}="auto", ATTR{remove}="1"
|
||||
# # Remove NVIDIA VGA/3D controller devices
|
||||
# ACTION=="add", SUBSYSTEM=="pci", ATTR{vendor}=="0x10de", ATTR{class}=="0x03[0-9]*", ATTR{power/control}="auto", ATTR{remove}="1"
|
||||
# '';
|
||||
# hardware.nvidia = {
|
||||
# prime.offload.enable = lib.mkForce false;
|
||||
# prime.offload.enableOffloadCmd = lib.mkForce false;
|
||||
# powerManagement.finegrained = lib.mkForce false;
|
||||
# prime.sync.enable = lib.mkForce false;
|
||||
# };
|
||||
# boot.blacklistedKernelModules = [ "nouveau" "nvidia" "nvidia_drm" "nvidia_modeset" ];
|
||||
# # Hint to kernel for integrated graphics, ID from command `$ nix-shell -p pciutils --run "lspci -nn | grep VGA"`
|
||||
# boot.kernelParams = [ "i915.force_probe=a7a0" "acpi_backlight=native" ];
|
||||
# # Default drivers
|
||||
# services.xserver.videoDrivers = [ "modesetting" "fbdev" ];
|
||||
# };
|
||||
# };
|
||||
# Remove NVIDIA devices from PCI bus to save power
|
||||
services.udev.extraRules = ''
|
||||
# Remove NVIDIA USB xHCI Host Controller devices, if present
|
||||
ACTION=="add", SUBSYSTEM=="pci", ATTR{vendor}=="0x10de", ATTR{class}=="0x0c0330", ATTR{power/control}="auto", ATTR{remove}="1"
|
||||
# Remove NVIDIA USB Type-C UCSI devices, if present
|
||||
ACTION=="add", SUBSYSTEM=="pci", ATTR{vendor}=="0x10de", ATTR{class}=="0x0c8000", ATTR{power/control}="auto", ATTR{remove}="1"
|
||||
# Remove NVIDIA Audio devices, if present
|
||||
ACTION=="add", SUBSYSTEM=="pci", ATTR{vendor}=="0x10de", ATTR{class}=="0x040300", ATTR{power/control}="auto", ATTR{remove}="1"
|
||||
# Remove NVIDIA VGA/3D controller devices
|
||||
ACTION=="add", SUBSYSTEM=="pci", ATTR{vendor}=="0x10de", ATTR{class}=="0x03[0-9]*", ATTR{power/control}="auto", ATTR{remove}="1"
|
||||
'';
|
||||
|
||||
# Use Intel modesetting driver only
|
||||
services.xserver.videoDrivers = lib.mkForce [ "modesetting" ];
|
||||
|
||||
# Disable all NVIDIA hardware configurations
|
||||
hardware.nvidia = {
|
||||
prime.offload.enable = lib.mkForce false;
|
||||
prime.offload.enableOffloadCmd = lib.mkForce false;
|
||||
powerManagement.finegrained = lib.mkForce false;
|
||||
prime.sync.enable = lib.mkForce false;
|
||||
};
|
||||
|
||||
# Ensure Intel graphics packages are available
|
||||
hardware.graphics.extraPackages = lib.mkForce (with pkgs; [
|
||||
intel-vaapi-driver
|
||||
intel-media-driver
|
||||
vpl-gpu-rt
|
||||
]);
|
||||
|
||||
# Clear NVIDIA-specific environment variables
|
||||
environment.sessionVariables = {
|
||||
GBM_BACKEND = lib.mkForce "";
|
||||
__GLX_VENDOR_LIBRARY_NAME = lib.mkForce "";
|
||||
};
|
||||
};
|
||||
};
|
||||
nixpkgs.config.allowUnfree = true;
|
||||
};
|
||||
}
|
||||
|
||||
386
nate-work/modules/niri/niri_conf.nix
Normal file
386
nate-work/modules/niri/niri_conf.nix
Normal file
@ -0,0 +1,386 @@
|
||||
{ inputs, lib, config, pkgs, userName, ... }:
|
||||
let
|
||||
unstable = import inputs.nixpkgs-unstable { system = "x86_64-linux"; config.allowUnfree = true; };
|
||||
isOnTheGo = builtins.elem "on-the-go" config.system.nixos.tags;
|
||||
in
|
||||
{
|
||||
options.niriwm = {
|
||||
enable = lib.mkEnableOption "Enable niri window manager.";
|
||||
useNonFree = lib.mkOption {
|
||||
default = false;
|
||||
example = true;
|
||||
description = "Whether to enable non-free software in the niri config";
|
||||
};
|
||||
systemPackages = lib.mkOption {
|
||||
default = [];
|
||||
description = "Add any additional packages desired. Merged with niri defaults.";
|
||||
};
|
||||
user = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
###
|
||||
## Configuration
|
||||
###
|
||||
config = lib.mkIf config.niriwm.enable {
|
||||
|
||||
nixpkgs.config.allowUnfree = config.niriwm.useNonFree;
|
||||
|
||||
###
|
||||
## XDG portal setup
|
||||
###
|
||||
xdg.portal = {
|
||||
config = {
|
||||
common = {
|
||||
default = [
|
||||
"wlr"
|
||||
];
|
||||
};
|
||||
};
|
||||
extraPortals = with pkgs; [
|
||||
xdg-desktop-portal-gnome
|
||||
];
|
||||
wlr.enable = true;
|
||||
enable = true;
|
||||
};
|
||||
xdg.sounds.enable = true;
|
||||
|
||||
###
|
||||
## System Packages
|
||||
###
|
||||
environment.systemPackages = with pkgs; lib.lists.flatten [
|
||||
[
|
||||
bash
|
||||
egl-wayland
|
||||
foot
|
||||
unstable.ghostty
|
||||
git
|
||||
glib # gsettings
|
||||
grim
|
||||
libnotify
|
||||
man-pages
|
||||
man-pages-posix
|
||||
nbfc-linux
|
||||
nautilus
|
||||
networkmanagerapplet
|
||||
pavucontrol
|
||||
slurp
|
||||
syncthingtray
|
||||
swaylock
|
||||
wl-clipboard
|
||||
waybar
|
||||
wdisplays
|
||||
wofi
|
||||
xdg-utils
|
||||
zsh
|
||||
lxqt.lxqt-policykit
|
||||
unstable.xwayland-satellite
|
||||
kanshi
|
||||
# Fonts
|
||||
]
|
||||
config.niriwm.systemPackages
|
||||
];
|
||||
environment.variables.QT_STYLE_OVERRIDE = "kvantum";
|
||||
environment.sessionVariables = {
|
||||
# use wayland
|
||||
MOZ_ENABLE_WAYLAND = "1";
|
||||
T_QPA_PLATFORM = "wayland";
|
||||
GDK_BACKEND = "wayland";
|
||||
WLR_NO_HARDWARE_CURSORS = "1";
|
||||
ELECTRON_OZONE_PLATFORM_HINT = "auto";
|
||||
NIXOS_OZONE_WL = "1";
|
||||
# For NVIDIA - only enable if not using on-the-go
|
||||
GBM_BACKEND = if isOnTheGo then "" else "nvidia-drm";
|
||||
__GLX_VENDOR_LIBRARY_NAME = if isOnTheGo then "" else "nvidia";
|
||||
};
|
||||
|
||||
# adds additional man pages
|
||||
documentation.dev.enable = true;
|
||||
|
||||
programs.steam = {
|
||||
enable = true;
|
||||
gamescopeSession.enable = true;
|
||||
};
|
||||
programs.gamemode = {
|
||||
enable = true;
|
||||
settings = {
|
||||
general = {
|
||||
reaper_freq = 5;
|
||||
desiredgov = "performance";
|
||||
softrealtime = "auto";
|
||||
};
|
||||
};
|
||||
};
|
||||
programs.kdeconnect.enable = true;
|
||||
programs.niri.enable = true;
|
||||
programs.virt-manager.enable = true;
|
||||
programs.xfconf.enable = true;
|
||||
programs.regreet.enable = true;
|
||||
programs.zsh.enable = true;
|
||||
programs.ssh.startAgent = false; # Using GNOME Keyring's gcr-ssh-agent instead
|
||||
|
||||
# For nautilus
|
||||
services.gnome.sushi.enable = true;
|
||||
programs.nautilus-open-any-terminal = {
|
||||
enable = true;
|
||||
terminal = "ghostty";
|
||||
};
|
||||
|
||||
services.syncthing = {
|
||||
enable = true;
|
||||
dataDir = "/home/${config.niriwm.user}/.syncthing";
|
||||
openDefaultPorts = true;
|
||||
user = config.niriwm.user;
|
||||
};
|
||||
systemd.services.syncthing.environment.STNODEFAULTFOLDER = "true"; # Don't create default ~/Sync folder
|
||||
|
||||
# Set zsh as the default shell system-wide
|
||||
users.defaultUserShell = pkgs.zsh;
|
||||
environment.shells = with pkgs; [ zsh bash ];
|
||||
|
||||
###
|
||||
## Services
|
||||
###
|
||||
virtualisation = {
|
||||
docker = {
|
||||
enable = true;
|
||||
enableOnBoot = true;
|
||||
package = unstable.docker_25;
|
||||
};
|
||||
libvirtd = {
|
||||
enable = true;
|
||||
qemu = {
|
||||
swtpm.enable = true;
|
||||
};
|
||||
};
|
||||
spiceUSBRedirection.enable = true;
|
||||
};
|
||||
boot.initrd.supportedFilesystems = { nfs = true; };
|
||||
users.groups.libvirtd.members = ["nate"];
|
||||
|
||||
services.blueman.enable = true;
|
||||
services.gvfs.enable = true; # file manager mount, trash, etc
|
||||
services.tumbler.enable = true; # thunar thumbnails
|
||||
services.openssh.enable = true;
|
||||
services.dbus.enable = true;
|
||||
services.gnome.gnome-keyring.enable = true;
|
||||
services.flatpak.enable = true;
|
||||
services.usbmuxd.enable = true;
|
||||
|
||||
# For yubioath desktop
|
||||
services.pcscd.enable = true;
|
||||
|
||||
# Printing
|
||||
services.printing = {
|
||||
enable = true;
|
||||
browsing = true;
|
||||
drivers = [ pkgs.brlaser ];
|
||||
};
|
||||
|
||||
services.avahi = {
|
||||
enable = true;
|
||||
nssmdns4 = true;
|
||||
openFirewall = true;
|
||||
};
|
||||
|
||||
services.fprintd.enable = true;
|
||||
services.greetd = {
|
||||
enable = true;
|
||||
settings = rec {
|
||||
initial_session = {
|
||||
command = "${pkgs.niri}/bin/niri-session";
|
||||
user = config.niriwm.user;
|
||||
};
|
||||
default_session = initial_session;
|
||||
};
|
||||
};
|
||||
# disable lid switch sleep when plugged into power, laptop docked
|
||||
services.logind.settings.Login.HandleLidSwitchExternalPower = "ignore";
|
||||
|
||||
# Keyring setup
|
||||
security.pam.services.greetd.enableGnomeKeyring = true;
|
||||
security.pam.services.login.enableGnomeKeyring = true;
|
||||
|
||||
# Audio - Modern PipeWire setup for Framework laptop
|
||||
# Disable PulseAudio in favor of PipeWire
|
||||
services.pulseaudio.enable = false;
|
||||
security.rtkit.enable = true;
|
||||
services.pipewire = {
|
||||
enable = true;
|
||||
audio.enable = true;
|
||||
alsa.enable = true;
|
||||
alsa.support32Bit = true;
|
||||
pulse.enable = true;
|
||||
wireplumber.enable = true;
|
||||
wireplumber.extraConfig = {
|
||||
"wireplumber.settings" = {
|
||||
bluetooth.autoswitch-to-headset-profile = false;
|
||||
};
|
||||
bluetoothEnhancements = {
|
||||
"monitor.bluez.properties" = {
|
||||
"bluez5.enable-sbc-xq" = true;
|
||||
"bluez5.enable-msbc" = true;
|
||||
"bluez5.enable-hw-volume" = true;
|
||||
# Default roles: https://pipewire.pages.freedesktop.org/wireplumber/daemon/configuration/bluetooth.html#monitor-properties
|
||||
"bluez5.roles" = [ "a2dp_sink" "a2dp_source" "bap_sink" "bap_source" "hfp_hf" "hfp_ag" ];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
###
|
||||
## Misc
|
||||
###
|
||||
# Necessary for home-manager niri setup
|
||||
security.polkit.enable = true;
|
||||
|
||||
hardware.bluetooth = {
|
||||
enable = true;
|
||||
powerOnBoot = true; # powers up the default Bluetooth controller on boot
|
||||
settings = {
|
||||
General = {
|
||||
Name = "Nate-Vasion";
|
||||
ControllerMode = "dual";
|
||||
FastConnectable = "true";
|
||||
Experimental = "true";
|
||||
};
|
||||
Policy = { AutoEnable = "true"; };
|
||||
LE = { EnableAdvMonInterleaveScan = 1; };
|
||||
};
|
||||
};
|
||||
|
||||
#
|
||||
# Hardware scanning support
|
||||
#
|
||||
hardware.sane = {
|
||||
enable = true;
|
||||
brscan5.enable = true;
|
||||
};
|
||||
|
||||
#
|
||||
# Nvidia Setup
|
||||
#
|
||||
|
||||
services.udev.extraRules = ''
|
||||
# For betaflight configurator
|
||||
# DFU (Internal bootloader for STM32 and AT32 MCUs)
|
||||
SUBSYSTEM=="usb", ATTRS{idVendor}=="2e3c", ATTRS{idProduct}=="df11", MODE="0664", GROUP="dialout"
|
||||
SUBSYSTEM=="usb", ATTRS{idVendor}=="0483", ATTRS{idProduct}=="df11", MODE="0664", GROUP="dialout"
|
||||
# For ddcutil monitor controls
|
||||
KERNEL=="i2c-[0-9]*", GROUP="i2c", MODE="0660"
|
||||
'';
|
||||
|
||||
services.xserver.videoDrivers = [ "nvidia" ];
|
||||
hardware = {
|
||||
graphics = {
|
||||
enable = true;
|
||||
enable32Bit = true;
|
||||
extraPackages = with pkgs; [
|
||||
intel-vaapi-driver
|
||||
intel-media-driver
|
||||
vpl-gpu-rt
|
||||
];
|
||||
};
|
||||
nvidia = {
|
||||
# Modesetting is required.
|
||||
modesetting.enable = true;
|
||||
# Nvidia power management. Experimental, and can cause sleep/suspend to fail.
|
||||
# Enable this if you have graphical corruption issues or application crashes after waking
|
||||
# up from sleep. This fixes it by saving the entire VRAM memory to /tmp/ instead
|
||||
# of just the bare essentials.
|
||||
powerManagement.enable = false;
|
||||
|
||||
# Fine-grained power management. Turns off GPU when not in use.
|
||||
# Experimental and only works on modern Nvidia GPUs (Turing or newer).
|
||||
powerManagement.finegrained = true;
|
||||
|
||||
# Use the NVidia open source kernel module (not to be confused with the
|
||||
# independent third-party "nouveau" open source driver).
|
||||
# Support is limited to the Turing and later architectures. Full list of
|
||||
# supported GPUs is at:
|
||||
# https://github.com/NVIDIA/open-gpu-kernel-modules#compatible-gpus
|
||||
# Only available from driver 515.43.04+
|
||||
# Currently alpha-quality/buggy, so false is currently the recommended setting.
|
||||
open = true;
|
||||
|
||||
# Enable the Nvidia settings menu,
|
||||
# accessible via `nvidia-settings`.
|
||||
nvidiaSettings = true;
|
||||
|
||||
# Optionally, you may need to select the appropriate driver version for your specific GPU.
|
||||
package = config.boot.kernelPackages.nvidiaPackages.stable;
|
||||
prime = {
|
||||
# sync.enable = true;
|
||||
offload.enable = true;
|
||||
offload.enableOffloadCmd = true; # adds `nvidia-offload` command to env
|
||||
intelBusId = "PCI:0:2:0";
|
||||
nvidiaBusId = "PCI:1:0:0";
|
||||
};
|
||||
};
|
||||
};
|
||||
# Create special on the go boot entry for battery saving
|
||||
# This disables NVIDIA GPU completely and uses Intel integrated graphics only
|
||||
specialisation = {
|
||||
on-the-go.configuration = {
|
||||
system.nixos.tags = [ "on-the-go" ];
|
||||
|
||||
# Blacklist all NVIDIA kernel modules
|
||||
boot.blacklistedKernelModules = [ "nouveau" "nvidia" "nvidia_drm" "nvidia_modeset" "nvidia_uvm" ];
|
||||
|
||||
# Force Intel i915 driver for 13th gen (Raptor Lake) integrated graphics
|
||||
# Device IDs: a7a0 or a7a8 for i7-13700H
|
||||
boot.kernelParams = [
|
||||
"i915.force_probe=a7a0"
|
||||
"module_blacklist=nvidia,nvidia_drm,nvidia_modeset,nvidia_uvm"
|
||||
];
|
||||
|
||||
boot.extraModprobeConfig = ''
|
||||
blacklist nouveau
|
||||
blacklist nvidia
|
||||
blacklist nvidia_drm
|
||||
blacklist nvidia_modeset
|
||||
blacklist nvidia_uvm
|
||||
options nouveau modeset=0
|
||||
'';
|
||||
|
||||
# Remove NVIDIA devices from PCI bus to save power
|
||||
services.udev.extraRules = ''
|
||||
# Remove NVIDIA USB xHCI Host Controller devices, if present
|
||||
ACTION=="add", SUBSYSTEM=="pci", ATTR{vendor}=="0x10de", ATTR{class}=="0x0c0330", ATTR{power/control}="auto", ATTR{remove}="1"
|
||||
# Remove NVIDIA USB Type-C UCSI devices, if present
|
||||
ACTION=="add", SUBSYSTEM=="pci", ATTR{vendor}=="0x10de", ATTR{class}=="0x0c8000", ATTR{power/control}="auto", ATTR{remove}="1"
|
||||
# Remove NVIDIA Audio devices, if present
|
||||
ACTION=="add", SUBSYSTEM=="pci", ATTR{vendor}=="0x10de", ATTR{class}=="0x040300", ATTR{power/control}="auto", ATTR{remove}="1"
|
||||
# Remove NVIDIA VGA/3D controller devices
|
||||
ACTION=="add", SUBSYSTEM=="pci", ATTR{vendor}=="0x10de", ATTR{class}=="0x03[0-9]*", ATTR{power/control}="auto", ATTR{remove}="1"
|
||||
'';
|
||||
|
||||
# Use Intel modesetting driver only
|
||||
services.xserver.videoDrivers = lib.mkForce [ "modesetting" ];
|
||||
|
||||
# Disable all NVIDIA hardware configurations
|
||||
hardware.nvidia = {
|
||||
prime.offload.enable = lib.mkForce false;
|
||||
prime.offload.enableOffloadCmd = lib.mkForce false;
|
||||
powerManagement.finegrained = lib.mkForce false;
|
||||
prime.sync.enable = lib.mkForce false;
|
||||
};
|
||||
|
||||
# Ensure Intel graphics packages are available
|
||||
hardware.graphics.extraPackages = lib.mkForce (with pkgs; [
|
||||
intel-vaapi-driver
|
||||
intel-media-driver
|
||||
vpl-gpu-rt
|
||||
]);
|
||||
|
||||
# Clear NVIDIA-specific environment variables
|
||||
environment.sessionVariables = {
|
||||
GBM_BACKEND = lib.mkForce "";
|
||||
__GLX_VENDOR_LIBRARY_NAME = lib.mkForce "";
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
64
nate-work/modules/niri/niri_home.nix
Normal file
64
nate-work/modules/niri/niri_home.nix
Normal file
@ -0,0 +1,64 @@
|
||||
{ inputs, lib, config, pkgs, ... }:
|
||||
{
|
||||
options.nirihome = {
|
||||
enable = lib.mkEnableOption "Enable niri home config";
|
||||
homePackages = lib.mkOption {
|
||||
default = [];
|
||||
description = "Add any additional packages desired. Merged with niri defaults.";
|
||||
};
|
||||
};
|
||||
|
||||
config = lib.mkIf config.nirihome.enable {
|
||||
# Note: We don't use wayland.windowManager.niri in home-manager
|
||||
# because we manage the niri config through dotfiles.
|
||||
# The system-level module enables niri via programs.niri.enable
|
||||
|
||||
# Import systemd variables for niri
|
||||
systemd.user.sessionVariables = {
|
||||
WAYLAND_DISPLAY = "wayland-1";
|
||||
XDG_CURRENT_DESKTOP = "niri";
|
||||
};
|
||||
|
||||
home.pointerCursor = {
|
||||
gtk.enable = true;
|
||||
x11.enable = true;
|
||||
name = "Bibata-Modern-Classic";
|
||||
package = pkgs.bibata-cursors;
|
||||
size = 32;
|
||||
};
|
||||
|
||||
home.packages = with pkgs; lib.lists.flatten [
|
||||
[
|
||||
### niri packages
|
||||
swaybg
|
||||
swaylock-effects
|
||||
waybar
|
||||
wofi
|
||||
# Etc
|
||||
gopsuinfo # For system stats in panel
|
||||
wl-clipboard # System clipboard
|
||||
brightnessctl
|
||||
wev
|
||||
wdisplays
|
||||
# Notifs
|
||||
libnotify
|
||||
swaynotificationcenter
|
||||
# Tray Applets
|
||||
networkmanagerapplet
|
||||
pavucontrol
|
||||
syncthingtray
|
||||
tailscale-systray
|
||||
# include portals here for flatpak
|
||||
xdg-desktop-portal-gnome
|
||||
xdg-desktop-portal-gtk
|
||||
]
|
||||
config.nirihome.homePackages
|
||||
];
|
||||
programs.cava = {
|
||||
enable = true;
|
||||
settings = {
|
||||
smoothing.noise_reduction = 55;
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
@ -72,6 +72,8 @@
|
||||
# sdkmanager
|
||||
unstable.opencode
|
||||
unstable.claude-code
|
||||
usbutils
|
||||
openscad
|
||||
|
||||
#
|
||||
# Gaming
|
||||
@ -85,6 +87,7 @@
|
||||
#
|
||||
bat
|
||||
duf
|
||||
dust
|
||||
fd
|
||||
fzf
|
||||
lsd
|
||||
|
||||
74
shared/modules/services/theme_switcher/EXAMPLE_CONFIG.nix
Normal file
74
shared/modules/services/theme_switcher/EXAMPLE_CONFIG.nix
Normal file
@ -0,0 +1,74 @@
|
||||
# Example configuration for enabling the theme switcher module
|
||||
# Add this to your host's desktop-configuration.nix or similar file
|
||||
|
||||
{
|
||||
# Import the module (if not already in your imports list)
|
||||
imports = [
|
||||
../../shared/modules/services/theme_switcher
|
||||
];
|
||||
|
||||
# Enable and configure the theme switcher
|
||||
services.themeSwitcher = {
|
||||
enable = true;
|
||||
user = "nate"; # Change to your username
|
||||
|
||||
# Theme generation settings
|
||||
backend = "haishoku"; # Recommended color extraction backend
|
||||
lightTime = "06:00:00"; # Switch to light theme at 6 AM
|
||||
darkTime = "18:00:00"; # Switch to dark theme at 6 PM
|
||||
enableAutoSwitch = true; # Enable automatic time-based switching
|
||||
|
||||
# Palette generation strategy
|
||||
paletteGeneration = "continuous"; # Options: continuous, light-dark
|
||||
|
||||
# Wallpaper rotation settings
|
||||
rotation = {
|
||||
interval = "5m"; # Rotate wallpaper every 5 minutes
|
||||
sorting = "random"; # Currently only random is supported
|
||||
};
|
||||
|
||||
# Wpaperd wallpaper daemon settings
|
||||
wpaperd = {
|
||||
enable = true;
|
||||
mode = "center"; # Options: center, fit, fit-border-color, stretch, tile
|
||||
transition = {
|
||||
effect = "fade"; # Transition effect (fade, simple, etc.)
|
||||
duration = 300; # Transition duration in milliseconds
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
# IMPORTANT: Disable the old wallpaper-rotator module if you were using it
|
||||
# services.wallpaperRotator.enable = false; # Comment out or remove
|
||||
|
||||
# Note: You may want to comment out or remove static theme configurations
|
||||
# when enabling the dynamic theme switcher. For example:
|
||||
|
||||
# BEFORE (static theme) - COMMENT THESE OUT:
|
||||
# home-manager.users.nate = {
|
||||
# gtk.theme = {
|
||||
# name = "catppuccin-macchiato-lavender-compact+rimless";
|
||||
# package = pkgs.catppuccin-gtk.override { ... };
|
||||
# };
|
||||
#
|
||||
# xdg.configFile."Kvantum/kvantum.kvconfig".source = ...;
|
||||
# };
|
||||
|
||||
# AFTER (dynamic theme):
|
||||
# The theme switcher module handles GTK/Qt theming automatically
|
||||
# You can still keep icon and cursor themes:
|
||||
# home-manager.users.nate = {
|
||||
# gtk.iconTheme = {
|
||||
# name = "Papirus-Dark";
|
||||
# package = pkgs.catppuccin-papirus-folders;
|
||||
# };
|
||||
# gtk.cursorTheme = {
|
||||
# name = "Bibata-Modern-Classic";
|
||||
# package = pkgs.bibata-cursors;
|
||||
# };
|
||||
# };
|
||||
}
|
||||
|
||||
# For manual testing before enabling auto-switch:
|
||||
# services.themeSwitcher.enableAutoSwitch = false;
|
||||
# Then use: ~/.local/bin/apply-theme.sh --light (or --dark)
|
||||
399
shared/modules/services/theme_switcher/OUTLINE.md
Normal file
399
shared/modules/services/theme_switcher/OUTLINE.md
Normal file
@ -0,0 +1,399 @@
|
||||
# NixOS Dynamic Theme Switcher - Implementation Instructions
|
||||
|
||||
## Objective
|
||||
Build a time-based dynamic theme switching system on NixOS that:
|
||||
1. Extracts color palettes from wallpapers using pywal16 with haishoku backend
|
||||
2. Generates and applies themes for GTK, Qt/Kvantum, Ghostty, and other applications
|
||||
3. Switches between light/dark themes based on time of day
|
||||
4. Integrates with existing wpaperd wallpaper rotation service
|
||||
|
||||
## Current System State
|
||||
|
||||
### Existing Theming Configuration
|
||||
**GTK Configuration:**
|
||||
- Current theme: `catppuccin-macchiato-lavender-compact+rimless`
|
||||
- Settings location: `~/.config/gtk-3.0/settings.ini`, `~/.config/gtk-4.0/settings.ini`
|
||||
- Icon theme: `Papirus-Dark`
|
||||
- Cursor: `Bibata-Modern-Classic`
|
||||
- Files are symlinked from Nix store via home-manager
|
||||
|
||||
**Qt/Kvantum Configuration:**
|
||||
- Current theme: `catppuccin-macchiato-lavender`
|
||||
- Location: `~/.config/Kvantum/kvantum.kvconfig`
|
||||
- Style: kvantum (both Qt5 and Qt6)
|
||||
- Environment variable: `QT_STYLE_OVERRIDE=kvantum`
|
||||
|
||||
**Wallpaper Service:**
|
||||
- Using wpaperd with home-manager integration
|
||||
- Wallpapers in: `/home/nate/nixos/shared/modules/services/wallpapers/`
|
||||
- Organized into `Light/` and `Dark/` subdirectories
|
||||
- Service configured via `services.wallpaperRotator` module
|
||||
|
||||
**Actions Required:**
|
||||
1. Disable/modify static GTK theme configuration in home.nix
|
||||
2. Disable/modify static Kvantum theme configuration
|
||||
3. Integrate with wpaperd to trigger theme changes on wallpaper rotation
|
||||
4. Remove hardcoded Catppuccin theme packages when pywal themes are working
|
||||
|
||||
## Implementation Components
|
||||
|
||||
### 3. Install Required Packages
|
||||
|
||||
Add to module configuration:
|
||||
```nix
|
||||
home.packages = with pkgs; [
|
||||
pywal16 # Color scheme generator with 16-color support
|
||||
python3Packages.haishoku # Recommended backend for pywal16
|
||||
wpaperd # Already installed via wallpaper service
|
||||
];
|
||||
```
|
||||
|
||||
**Available pywal16 backends:** modern_colorthief, wal, fast_colorthief, haishoku, colorthief, colorz, okthief, schemer2
|
||||
|
||||
### 4. Color Extraction & Theme Generation
|
||||
|
||||
#### 4.1 Pywal16 Built-in Templates
|
||||
**Pywal16 already includes templates for:**
|
||||
- Ghostty: `ghostty.conf` template (tested and confirmed)
|
||||
- CSS variables: `colors.css` template
|
||||
- Waybar: `colors-waybar.css`
|
||||
- Hyprland: `colors-hyprland.conf`
|
||||
- Rofi: `colors-rofi-dark.rasi` and `colors-rofi-light.rasi`
|
||||
- Sway: `colors-sway`
|
||||
- Mako: `colors-mako`
|
||||
- Foot, Kitty, Alacritty terminal configs
|
||||
- Many others (47 templates total)
|
||||
|
||||
**Output location:** `~/.cache/wal/` after running `wal -i /path/to/wallpaper`
|
||||
|
||||
**Usage:**
|
||||
```bash
|
||||
wal -i /path/to/wallpaper --backend haishoku -n
|
||||
# -n flag skips setting wallpaper (wpaperd handles that)
|
||||
# --backend haishoku uses the haishoku color extraction
|
||||
# -l flag for light themes
|
||||
```
|
||||
|
||||
#### 4.2 GTK Theme Generation
|
||||
**Goal:** Create custom GTK 3.0 and GTK 4.0 CSS themes from pywal colors
|
||||
|
||||
**Note:** Pywal16 includes `colors.css` template with CSS variables, but NOT full GTK themes.
|
||||
|
||||
**Required files:**
|
||||
- `~/.config/gtk-3.0/gtk.css` - Custom CSS overlay
|
||||
- `~/.config/gtk-4.0/gtk.css` - Custom CSS overlay
|
||||
|
||||
**Approach:** Create pywal template to generate GTK CSS using colors.json output
|
||||
- Map pywal colors to GTK color definitions
|
||||
- Use @define-color for GTK theme variables
|
||||
- Apply to common widgets
|
||||
|
||||
#### 4.3 Qt/Kvantum Theme Generation
|
||||
**Goal:** Generate Kvantum theme from pywal colors
|
||||
|
||||
**Kvantum structure (.kvconfig file):**
|
||||
- `[General]` section with metadata
|
||||
- `[GeneralColors]` section with color definitions (window.color, base.color, button.color, etc.)
|
||||
- `[Hacks]` section with tweaks
|
||||
- Per-widget sections (PanelButtonCommand, etc.)
|
||||
|
||||
**Location:** `~/.config/Kvantum/PywalTheme/PywalTheme.kvconfig`
|
||||
|
||||
**Required:** Create pywal template that maps colors.json to .kvconfig format
|
||||
**Optional:** SVG file can be minimal or omitted for basic themes
|
||||
|
||||
**Application:** Use `kvantummanager --set PywalTheme` to activate
|
||||
|
||||
### 5. Theme Application Script
|
||||
|
||||
**Script: `apply-theme.sh`**
|
||||
|
||||
**Parameters:**
|
||||
- `--light` or `--dark` (theme mode)
|
||||
- Optional: `--wallpaper-path` (specific wallpaper, otherwise use wpaperd current)
|
||||
|
||||
**Script Logic:**
|
||||
|
||||
1. **Determine wallpaper source:**
|
||||
- If `--wallpaper-path` provided, use it
|
||||
- Otherwise, let wpaperd select based on mode (Light/ or Dark/ directory)
|
||||
- Use `wpaperctl` to interact with wpaperd if needed
|
||||
|
||||
2. **Generate color scheme:**
|
||||
```bash
|
||||
if [ "$MODE" = "light" ]; then
|
||||
wal -i "$WALLPAPER" --backend haishoku -l -n
|
||||
else
|
||||
wal -i "$WALLPAPER" --backend haishoku -n
|
||||
fi
|
||||
```
|
||||
|
||||
3. **Apply GTK theme:**
|
||||
```bash
|
||||
# pywal doesn't generate full GTK themes, use custom CSS
|
||||
ln -sf ~/.cache/wal/gtk-3.0.css ~/.config/gtk-3.0/gtk.css
|
||||
ln -sf ~/.cache/wal/gtk-4.0.css ~/.config/gtk-4.0/gtk.css
|
||||
|
||||
gsettings set org.gnome.desktop.interface color-scheme "prefer-$MODE"
|
||||
```
|
||||
|
||||
4. **Apply Qt/Kvantum theme:**
|
||||
```bash
|
||||
kvantummanager --set PywalTheme
|
||||
```
|
||||
|
||||
5. **Reload configurations:**
|
||||
- Ghostty: Auto-reloads config on file change (no action needed)
|
||||
- GTK apps: Automatically detect gsettings changes
|
||||
- Qt apps: May need restart for Kvantum changes
|
||||
- Hyprland/Sway: Reload via IPC if using pywal templates
|
||||
|
||||
6. **Optional: Update Flatpak:**
|
||||
```bash
|
||||
flatpak override --user --filesystem=~/.cache/wal:ro
|
||||
```
|
||||
|
||||
### 6. Wallpaper Integration & Time-Based Switching
|
||||
|
||||
**Option A: Integrate with wpaperd events (Recommended)**
|
||||
- Modify wallpaper-rotator module to support Light/Dark subdirectories
|
||||
- Change wallpaper directory based on time of day
|
||||
- Trigger theme generation on wallpaper change
|
||||
|
||||
**Option B: Independent systemd timer**
|
||||
|
||||
**Timer configuration:**
|
||||
```nix
|
||||
systemd.user.timers.theme-switcher = {
|
||||
Unit.Description = "Dynamic Theme Switcher Timer";
|
||||
Timer = {
|
||||
OnCalendar = [ "*-*-* 06:00:00" "*-*-* 18:00:00" ]; # 6am light, 6pm dark
|
||||
Persistent = true;
|
||||
};
|
||||
Install.WantedBy = [ "timers.target" ];
|
||||
};
|
||||
```
|
||||
|
||||
**Service configuration:**
|
||||
```nix
|
||||
systemd.user.services.theme-switcher = {
|
||||
Unit.Description = "Dynamic Theme Switcher";
|
||||
Service = {
|
||||
Type = "oneshot";
|
||||
ExecStart = "${pkgs.bash}/bin/bash ${./scripts/theme-switcher.sh}";
|
||||
Environment = "PATH=${lib.makeBinPath [ pkgs.pywal16 pkgs.coreutils ]}";
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
**Script determines light/dark based on current hour:**
|
||||
```bash
|
||||
hour=$(date +%H)
|
||||
if [ $hour -ge 6 ] && [ $hour -lt 18 ]; then
|
||||
MODE="light"
|
||||
WALLPAPER_DIR="Light"
|
||||
else
|
||||
MODE="dark"
|
||||
WALLPAPER_DIR="Dark"
|
||||
fi
|
||||
```
|
||||
|
||||
### 7. NixOS Module Structure
|
||||
|
||||
**Module: `theme_switcher/default.nix`**
|
||||
|
||||
```nix
|
||||
{ config, lib, pkgs, ... }:
|
||||
|
||||
let
|
||||
cfg = config.services.themeSwitcher;
|
||||
in
|
||||
{
|
||||
options.services.themeSwitcher = {
|
||||
enable = lib.mkEnableOption "dynamic theme switching based on time and wallpapers";
|
||||
|
||||
user = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
description = "Username for theme switching";
|
||||
};
|
||||
|
||||
wallpaperPath = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = "/home/${cfg.user}/nixos/shared/modules/services/wallpapers";
|
||||
description = "Path to wallpaper directories (Light/ and Dark/)";
|
||||
};
|
||||
|
||||
backend = lib.mkOption {
|
||||
type = lib.types.enum [ "haishoku" "modern_colorthief" "fast_colorthief" ];
|
||||
default = "haishoku";
|
||||
description = "Pywal color extraction backend";
|
||||
};
|
||||
|
||||
lightTime = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = "06:00:00";
|
||||
description = "Time to switch to light theme (HH:MM:SS)";
|
||||
};
|
||||
|
||||
darkTime = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = "18:00:00";
|
||||
description = "Time to switch to dark theme (HH:MM:SS)";
|
||||
};
|
||||
};
|
||||
|
||||
config = lib.mkIf cfg.enable {
|
||||
home-manager.users.${cfg.user} = {
|
||||
home.packages = with pkgs; [
|
||||
pywal16
|
||||
python3Packages.haishoku
|
||||
];
|
||||
|
||||
# Install theme application scripts
|
||||
home.file.".local/bin/apply-theme.sh" = {
|
||||
source = ./scripts/apply-theme.sh;
|
||||
executable = true;
|
||||
};
|
||||
|
||||
# Pywal custom templates for GTK and Kvantum
|
||||
xdg.configFile."wal/templates/gtk-3.0.css".source = ./templates/gtk-3.0.css;
|
||||
xdg.configFile."wal/templates/gtk-4.0.css".source = ./templates/gtk-4.0.css;
|
||||
xdg.configFile."wal/templates/kvantum.kvconfig".source = ./templates/kvantum.kvconfig;
|
||||
|
||||
# Systemd timer and service
|
||||
systemd.user.timers.theme-switcher = { /* ... */ };
|
||||
systemd.user.services.theme-switcher = { /* ... */ };
|
||||
|
||||
# Keep Qt/GTK enabled but don't set static themes
|
||||
qt = {
|
||||
enable = true;
|
||||
platformTheme.name = "kvantum";
|
||||
style.name = "kvantum";
|
||||
};
|
||||
|
||||
gtk.enable = true;
|
||||
};
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
**Template files needed:**
|
||||
- `templates/gtk-3.0.css` - GTK3 theme using pywal variables
|
||||
- `templates/gtk-4.0.css` - GTK4 theme using pywal variables
|
||||
- `templates/kvantum.kvconfig` - Kvantum config using pywal colors
|
||||
|
||||
## Testing & Validation
|
||||
|
||||
### 8. Test Plan
|
||||
|
||||
1. **Test pywal color extraction:**
|
||||
```bash
|
||||
# Test with light wallpaper
|
||||
wal -i ~/nixos/shared/modules/services/wallpapers/Light/IU-Light.jpg --backend haishoku -l -n
|
||||
cat ~/.cache/wal/colors.json # Verify colors extracted
|
||||
|
||||
# Test with dark wallpaper
|
||||
wal -i ~/nixos/shared/modules/services/wallpapers/Dark/IU-Dark.jpg --backend haishoku -n
|
||||
cat ~/.cache/wal/colors.json # Verify colors extracted
|
||||
```
|
||||
|
||||
2. **Verify pywal template output:**
|
||||
```bash
|
||||
ls -la ~/.cache/wal/
|
||||
# Should see: colors.json, ghostty.conf, colors.css, and custom templates
|
||||
cat ~/.cache/wal/ghostty.conf # Check ghostty colors
|
||||
```
|
||||
|
||||
3. **Manual theme application:**
|
||||
```bash
|
||||
~/.local/bin/apply-theme.sh --light
|
||||
# Check: GTK apps, Qt apps, Ghostty colors update
|
||||
|
||||
~/.local/bin/apply-theme.sh --dark
|
||||
# Check: Theme switches to dark
|
||||
```
|
||||
|
||||
4. **Verify theme files:**
|
||||
- `~/.cache/wal/colors.json` - Generated by pywal
|
||||
- `~/.cache/wal/gtk-3.0.css` - From custom template
|
||||
- `~/.cache/wal/gtk-4.0.css` - From custom template
|
||||
- `~/.cache/wal/kvantum.kvconfig` - From custom template
|
||||
- `~/.cache/wal/ghostty.conf` - From built-in template
|
||||
|
||||
5. **Test Kvantum theme:**
|
||||
```bash
|
||||
kvantummanager --set PywalTheme
|
||||
# Or check: cat ~/.config/Kvantum/kvantum.kvconfig
|
||||
```
|
||||
|
||||
6. **Test systemd automation:**
|
||||
```bash
|
||||
systemctl --user start theme-switcher.service
|
||||
systemctl --user status theme-switcher.timer
|
||||
systemctl --user list-timers | grep theme
|
||||
```
|
||||
|
||||
7. **Test with live applications:**
|
||||
- Open GTK app (nautilus, gnome-calculator) - verify colors
|
||||
- Open Qt app (if available) - verify Kvantum theme
|
||||
- Open Ghostty terminal - verify color palette matches wallpaper
|
||||
- Check waybar/rofi if using pywal templates for those
|
||||
|
||||
## Implementation Steps Summary
|
||||
|
||||
### Phase 1: Setup pywal with existing wallpapers
|
||||
1. Create theme_switcher module with pywal16 package
|
||||
2. Test color extraction on Light/ and Dark/ wallpapers
|
||||
3. Verify built-in templates (ghostty.conf, colors.css) generate correctly
|
||||
|
||||
### Phase 2: Create custom templates
|
||||
1. Create `templates/gtk-3.0.css` using pywal variables
|
||||
2. Create `templates/gtk-4.0.css` using pywal variables
|
||||
3. Create `templates/kvantum.kvconfig` mapping colors.json to Kvantum format
|
||||
4. Install templates to `~/.config/wal/templates/`
|
||||
|
||||
### Phase 3: Theme application script
|
||||
1. Write `apply-theme.sh` script that:
|
||||
- Runs pywal with appropriate flags (-l for light)
|
||||
- Symlinks generated CSS to GTK config directories
|
||||
- Copies Kvantum config to `~/.config/Kvantum/PywalTheme/`
|
||||
- Runs `kvantummanager --set PywalTheme`
|
||||
- Updates gsettings for color-scheme preference
|
||||
|
||||
### Phase 4: Time-based automation
|
||||
1. Create systemd timer for light/dark switching times
|
||||
2. Create systemd service that calls apply-theme.sh
|
||||
3. Script determines mode based on current time
|
||||
4. Selects random wallpaper from Light/ or Dark/ directory
|
||||
|
||||
### Phase 5: Integration and cleanup
|
||||
1. Modify existing home.nix files to disable static themes when module enabled
|
||||
2. Keep icon themes, cursor themes (not wallpaper-dependent)
|
||||
3. Test on one host before deploying to all
|
||||
4. Document wpaperd integration approach for future enhancement
|
||||
|
||||
## Key Technical Details
|
||||
|
||||
**Pywal template syntax:**
|
||||
```
|
||||
{background} -> #081212
|
||||
{foreground} -> #c1c3c3
|
||||
{color0} -> #081212
|
||||
...
|
||||
{color15} -> #c1c3c3
|
||||
{cursor} -> #c1c3c3
|
||||
{wallpaper} -> /path/to/wallpaper.jpg
|
||||
```
|
||||
|
||||
**Template location:** `~/.config/wal/templates/filename.ext`
|
||||
**Output location:** `~/.cache/wal/filename.ext`
|
||||
|
||||
**Ghostty:** Automatically reloads config when files change - no manual reload needed
|
||||
|
||||
**GTK:** Changes propagate via gsettings/dconf - running apps update automatically
|
||||
|
||||
**Kvantum:** May require app restart to see changes (Qt limitation)
|
||||
|
||||
**Wallpaper directories:**
|
||||
- Light: `/home/nate/nixos/shared/modules/services/wallpapers/Light/`
|
||||
- Dark: `/home/nate/nixos/shared/modules/services/wallpapers/Dark/`
|
||||
447
shared/modules/services/theme_switcher/README.md
Normal file
447
shared/modules/services/theme_switcher/README.md
Normal file
@ -0,0 +1,447 @@
|
||||
# Dynamic Theme Switcher Module
|
||||
|
||||
A NixOS module that automatically generates and applies color themes from wallpapers using pywal16, with integrated wpaperd wallpaper management and support for GTK, Qt/Kvantum, Ghostty, and other applications.
|
||||
|
||||
## Features
|
||||
|
||||
- **Integrated Wallpaper Management**: Built-in wpaperd integration with configurable rotation modes
|
||||
- **Automatic Color Extraction**: Uses pywal16 with configurable backends (haishoku, colorthief, etc.) to extract color palettes from wallpapers
|
||||
- **Multi-Application Support**: Generates themes for GTK 3.0, GTK 4.0, Qt/Kvantum, Ghostty terminal, and more
|
||||
- **Time-Based Switching**: Automatically switches between light and dark themes based on configured times
|
||||
- **Flexible Rotation Modes**: Choose between continuous rotation, switch-only, or hybrid modes
|
||||
- **Manual Control**: Provides scripts for manual theme switching
|
||||
- **Synchronized Wallpaper & Theme**: Wallpaper and color theme always match
|
||||
|
||||
## Directory Structure
|
||||
|
||||
```
|
||||
theme_switcher/
|
||||
├── default.nix # Main module configuration
|
||||
├── templates/
|
||||
│ ├── gtk-3.0.css # GTK 3.0 theme template
|
||||
│ ├── gtk-4.0.css # GTK 4.0 theme template
|
||||
│ └── PywalTheme.kvconfig # Kvantum Qt theme template
|
||||
├── OUTLINE.md # Implementation specification
|
||||
└── README.md # This file
|
||||
```
|
||||
|
||||
## Installation
|
||||
|
||||
### 1. Import the Module
|
||||
|
||||
Add the module to your host configuration:
|
||||
|
||||
```nix
|
||||
# In your host's configuration.nix or desktop-configuration.nix
|
||||
imports = [
|
||||
../../shared/modules/services/theme_switcher
|
||||
];
|
||||
```
|
||||
|
||||
### 2. Enable and Configure
|
||||
|
||||
```nix
|
||||
services.themeSwitcher = {
|
||||
enable = true;
|
||||
user = "yourUsername";
|
||||
|
||||
# Optional: Customize settings
|
||||
backend = "haishoku"; # Color extraction backend
|
||||
lightTime = "06:00:00"; # Switch to light theme at 6 AM
|
||||
darkTime = "18:00:00"; # Switch to dark theme at 6 PM
|
||||
enableAutoSwitch = true; # Enable automatic time-based switching
|
||||
|
||||
# Wallpaper rotation settings
|
||||
rotation = {
|
||||
mode = "continuous"; # Options: continuous, switch-only, hybrid
|
||||
interval = "5m"; # Rotation interval (for continuous mode)
|
||||
};
|
||||
|
||||
# Wpaperd configuration
|
||||
wpaperd = {
|
||||
enable = true;
|
||||
mode = "center"; # Options: center, fit, stretch, tile
|
||||
transition.effect = "fade";
|
||||
transition.duration = 300; # milliseconds
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
**Note:** If you were previously using the separate `wallpaper-rotator` module, you should disable it to avoid conflicts.
|
||||
|
||||
### 3. Wallpaper Directory Structure
|
||||
|
||||
Ensure your wallpaper directory has Light and Dark subdirectories:
|
||||
|
||||
```
|
||||
wallpapers/
|
||||
├── Light/
|
||||
│ ├── wallpaper1.jpg
|
||||
│ ├── wallpaper2.jpg
|
||||
│ └── ...
|
||||
└── Dark/
|
||||
├── wallpaper1.jpg
|
||||
├── wallpaper2.jpg
|
||||
└── ...
|
||||
```
|
||||
|
||||
### 4. Rebuild Your System
|
||||
|
||||
```bash
|
||||
sudo nixos-rebuild switch --flake .#hostname
|
||||
```
|
||||
|
||||
## Configuration Options
|
||||
|
||||
### Core Options
|
||||
|
||||
#### `services.themeSwitcher.enable`
|
||||
- **Type**: boolean
|
||||
- **Default**: false
|
||||
- **Description**: Enable the dynamic theme switcher with integrated wallpaper management
|
||||
|
||||
#### `services.themeSwitcher.user`
|
||||
- **Type**: string
|
||||
- **Required**: yes
|
||||
- **Description**: Username for which to apply theme switching
|
||||
|
||||
#### `services.themeSwitcher.wallpaperPath`
|
||||
- **Type**: string
|
||||
- **Default**: `${homeDirectory}/nixos/shared/modules/services/wallpapers`
|
||||
- **Description**: Path to wallpaper directories (must contain Light/ and Dark/ subdirectories)
|
||||
|
||||
#### `services.themeSwitcher.backend`
|
||||
- **Type**: enum
|
||||
- **Default**: `"haishoku"`
|
||||
- **Options**: `"haishoku"`, `"modern_colorthief"`, `"fast_colorthief"`, `"colorthief"`, `"colorz"`, `"wal"`
|
||||
- **Description**: Pywal color extraction backend algorithm
|
||||
|
||||
#### `services.themeSwitcher.lightTime`
|
||||
- **Type**: string
|
||||
- **Default**: `"06:00:00"`
|
||||
- **Description**: Time to switch to light theme (HH:MM:SS format)
|
||||
|
||||
#### `services.themeSwitcher.darkTime`
|
||||
- **Type**: string
|
||||
- **Default**: `"18:00:00"`
|
||||
- **Description**: Time to switch to dark theme (HH:MM:SS format)
|
||||
|
||||
#### `services.themeSwitcher.enableAutoSwitch`
|
||||
- **Type**: boolean
|
||||
- **Default**: true
|
||||
- **Description**: Enable automatic time-based theme switching via systemd timer
|
||||
|
||||
### Rotation Options
|
||||
|
||||
#### `services.themeSwitcher.rotation.mode`
|
||||
- **Type**: enum
|
||||
- **Default**: `"continuous"`
|
||||
- **Options**:
|
||||
- `"continuous"`: Rotate wallpapers every interval AND regenerate theme each time
|
||||
- `"switch-only"`: Only change wallpaper at light/dark switch times (6 AM/6 PM)
|
||||
- `"hybrid"`: Rotate wallpapers but only regenerate theme at switch times (not yet implemented)
|
||||
- **Description**: Wallpaper rotation and theme generation behavior
|
||||
|
||||
#### `services.themeSwitcher.rotation.interval`
|
||||
- **Type**: string
|
||||
- **Default**: `"5m"`
|
||||
- **Example**: `"30s"`, `"10m"`, `"1h"`
|
||||
- **Description**: How often to rotate wallpapers in continuous mode
|
||||
|
||||
#### `services.themeSwitcher.rotation.sorting`
|
||||
- **Type**: enum
|
||||
- **Default**: `"random"`
|
||||
- **Options**: `"random"`
|
||||
- **Description**: Wallpaper selection method (currently only random is supported)
|
||||
|
||||
### Wpaperd Integration Options
|
||||
|
||||
#### `services.themeSwitcher.wpaperd.enable`
|
||||
- **Type**: boolean
|
||||
- **Default**: true
|
||||
- **Description**: Enable wpaperd wallpaper daemon integration
|
||||
|
||||
#### `services.themeSwitcher.wpaperd.mode`
|
||||
- **Type**: enum
|
||||
- **Default**: `"center"`
|
||||
- **Options**: `"center"`, `"fit"`, `"fit-border-color"`, `"stretch"`, `"tile"`
|
||||
- **Description**: How to display wallpapers when size differs from display resolution
|
||||
|
||||
#### `services.themeSwitcher.wpaperd.transition.effect`
|
||||
- **Type**: string
|
||||
- **Default**: `"fade"`
|
||||
- **Example**: `"simple"`, `"fade"`
|
||||
- **Description**: Wallpaper transition effect name
|
||||
|
||||
#### `services.themeSwitcher.wpaperd.transition.duration`
|
||||
- **Type**: integer
|
||||
- **Default**: 300
|
||||
- **Description**: Transition duration in milliseconds
|
||||
|
||||
## Manual Usage
|
||||
|
||||
### Apply Theme Manually
|
||||
|
||||
The module installs `apply-theme.sh` to `~/.local/bin/`:
|
||||
|
||||
```bash
|
||||
# Apply light theme with random wallpaper from Light/ directory
|
||||
~/.local/bin/apply-theme.sh --light
|
||||
|
||||
# Apply dark theme with random wallpaper from Dark/ directory
|
||||
~/.local/bin/apply-theme.sh --dark
|
||||
|
||||
# Apply theme with specific wallpaper
|
||||
~/.local/bin/apply-theme.sh --light --wallpaper-path /path/to/wallpaper.jpg
|
||||
```
|
||||
|
||||
### Check Systemd Timer Status
|
||||
|
||||
```bash
|
||||
# View timer status
|
||||
systemctl --user status theme-switcher.timer
|
||||
|
||||
# List upcoming timer events
|
||||
systemctl --user list-timers | grep theme
|
||||
|
||||
# Manually trigger theme switch
|
||||
systemctl --user start theme-switcher.service
|
||||
|
||||
# View service logs
|
||||
journalctl --user -u theme-switcher.service
|
||||
```
|
||||
|
||||
### Test Pywal Color Extraction
|
||||
|
||||
```bash
|
||||
# Test light theme generation
|
||||
wal -i ~/nixos/shared/modules/services/wallpapers/Light/IU-Light.jpg --backend haishoku -l -n
|
||||
|
||||
# Test dark theme generation
|
||||
wal -i ~/nixos/shared/modules/services/wallpapers/Dark/IU-Dark.jpg --backend haishoku -n
|
||||
|
||||
# View generated colors
|
||||
cat ~/.cache/wal/colors.json
|
||||
|
||||
# View generated templates
|
||||
ls -la ~/.cache/wal/
|
||||
```
|
||||
|
||||
## Generated Files
|
||||
|
||||
After running the theme switcher, pywal generates the following files:
|
||||
|
||||
### Pywal Cache (`~/.cache/wal/`)
|
||||
- `colors.json` - Extracted color palette in JSON format
|
||||
- `colors.sh` - Shell script with color variables
|
||||
- `ghostty.conf` - Ghostty terminal theme (auto-reloads)
|
||||
- `gtk-3.0.css` - GTK 3.0 theme (from custom template)
|
||||
- `gtk-4.0.css` - GTK 4.0 theme (from custom template)
|
||||
- `PywalTheme.kvconfig` - Kvantum Qt theme (from custom template)
|
||||
|
||||
### User Configuration
|
||||
- `~/.config/gtk-3.0/gtk.css` - Symlink to pywal-generated GTK3 theme
|
||||
- `~/.config/gtk-4.0/gtk.css` - Symlink to pywal-generated GTK4 theme
|
||||
- `~/.config/Kvantum/PywalTheme/PywalTheme.kvconfig` - Kvantum theme copy
|
||||
|
||||
## Integration with Existing Themes
|
||||
|
||||
When enabling this module, you should adjust your existing theme configuration:
|
||||
|
||||
### Before (Static Catppuccin Theme)
|
||||
```nix
|
||||
gtk = {
|
||||
enable = true;
|
||||
theme = {
|
||||
name = "catppuccin-macchiato-lavender-compact+rimless";
|
||||
package = pkgs.catppuccin-gtk.override { ... };
|
||||
};
|
||||
};
|
||||
|
||||
qt = {
|
||||
enable = true;
|
||||
platformTheme.name = "kvantum";
|
||||
style.name = "kvantum";
|
||||
};
|
||||
|
||||
xdg.configFile."Kvantum/kvantum.kvconfig".source = ...;
|
||||
```
|
||||
|
||||
### After (Dynamic Pywal Theme)
|
||||
```nix
|
||||
# The theme switcher module automatically sets:
|
||||
# - gtk.enable = true
|
||||
# - qt.enable = true
|
||||
# - qt.platformTheme.name = "kvantum"
|
||||
# - qt.style.name = "kvantum"
|
||||
|
||||
# You can still keep static icon and cursor themes:
|
||||
gtk.iconTheme = {
|
||||
name = "Papirus-Dark";
|
||||
package = pkgs.catppuccin-papirus-folders;
|
||||
};
|
||||
|
||||
gtk.cursorTheme = {
|
||||
name = "Bibata-Modern-Classic";
|
||||
package = pkgs.bibata-cursors;
|
||||
};
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Theme Not Applying
|
||||
```bash
|
||||
# Check if pywal generated files
|
||||
ls -la ~/.cache/wal/
|
||||
|
||||
# Check systemd service status
|
||||
systemctl --user status theme-switcher.service
|
||||
|
||||
# View detailed logs
|
||||
journalctl --user -u theme-switcher.service -n 50
|
||||
|
||||
# Manually run apply-theme.sh with verbose output
|
||||
~/.local/bin/apply-theme.sh --light
|
||||
```
|
||||
|
||||
### Kvantum Theme Not Working
|
||||
```bash
|
||||
# Check if Kvantum theme was created
|
||||
ls -la ~/.config/Kvantum/PywalTheme/
|
||||
|
||||
# Manually set Kvantum theme
|
||||
kvantummanager --set PywalTheme
|
||||
|
||||
# Check if Qt applications are using Kvantum
|
||||
echo $QT_STYLE_OVERRIDE # Should be "kvantum"
|
||||
```
|
||||
|
||||
### Colors Don't Match Wallpaper
|
||||
Try a different pywal backend:
|
||||
```nix
|
||||
services.themeSwitcher.backend = "modern_colorthief"; # or "fast_colorthief", "colorthief", etc.
|
||||
```
|
||||
|
||||
### GTK Applications Not Updating
|
||||
```bash
|
||||
# Check if GTK CSS files exist
|
||||
ls -la ~/.config/gtk-3.0/gtk.css
|
||||
ls -la ~/.config/gtk-4.0/gtk.css
|
||||
|
||||
# Restart GTK applications for changes to take effect
|
||||
```
|
||||
|
||||
## Supported Applications
|
||||
|
||||
### Automatically Themed
|
||||
- **GTK 3 Applications**: Nautilus, GNOME Calculator, etc.
|
||||
- **GTK 4 Applications**: GNOME Text Editor, newer GNOME apps
|
||||
- **Qt Applications**: KDE apps when using Kvantum
|
||||
- **Ghostty Terminal**: Auto-reloads on config change
|
||||
- **Waybar**: If using pywal template (built-in)
|
||||
- **Rofi**: If using pywal template (built-in)
|
||||
- **Hyprland/Sway**: If using pywal template (built-in)
|
||||
|
||||
### Manual Integration Required
|
||||
- **Firefox**: Use pywal-firefox extension
|
||||
- **VSCode**: Use pywal theme extension
|
||||
- **Other Applications**: Check if pywal templates exist in `~/.cache/wal/`
|
||||
|
||||
## Advanced Usage
|
||||
|
||||
### Custom Pywal Templates
|
||||
|
||||
To add your own templates, create files in `~/.config/wal/templates/`:
|
||||
|
||||
```bash
|
||||
# Template file: ~/.config/wal/templates/myapp.conf
|
||||
# Use pywal template variables
|
||||
background = {background}
|
||||
foreground = {foreground}
|
||||
color0 = {color0}
|
||||
...
|
||||
color15 = {color15}
|
||||
```
|
||||
|
||||
After running `apply-theme.sh`, find your generated file in `~/.cache/wal/myapp.conf`.
|
||||
|
||||
### Disable Automatic Switching
|
||||
|
||||
To use manual control only:
|
||||
```nix
|
||||
services.themeSwitcher = {
|
||||
enable = true;
|
||||
user = "username";
|
||||
enableAutoSwitch = false; # Disable timer
|
||||
};
|
||||
```
|
||||
|
||||
Then use `~/.local/bin/apply-theme.sh` manually as needed.
|
||||
|
||||
### Rotation Modes Explained
|
||||
|
||||
#### Continuous Mode (Default)
|
||||
- Wallpaper rotates every interval (e.g., 5 minutes)
|
||||
- Theme is regenerated from each new wallpaper
|
||||
- Wallpaper and theme always match
|
||||
- **Resource usage**: Moderate (pywal runs every interval)
|
||||
- **Best for**: Users who want frequently changing, perfectly matched themes
|
||||
|
||||
#### Switch-Only Mode
|
||||
- Wallpaper changes only at light/dark switch times (6 AM / 6 PM)
|
||||
- Theme is generated once per switch
|
||||
- Wallpaper and theme always match
|
||||
- **Resource usage**: Minimal (pywal runs twice per day)
|
||||
- **Best for**: Users who prefer consistent themes throughout the day
|
||||
|
||||
#### Hybrid Mode (Future)
|
||||
- Wallpaper rotates every interval
|
||||
- Theme is generated only at switch times
|
||||
- Wallpaper and theme may not match mid-day
|
||||
- **Not yet implemented**
|
||||
|
||||
### Migration from wallpaper-rotator Module
|
||||
|
||||
If you were previously using the separate `wallpaper-rotator` module:
|
||||
|
||||
1. **Disable the old module** in your configuration:
|
||||
```nix
|
||||
# Comment out or remove:
|
||||
# services.wallpaperRotator.enable = true;
|
||||
```
|
||||
|
||||
2. **Enable themeSwitcher** with equivalent settings:
|
||||
```nix
|
||||
services.themeSwitcher = {
|
||||
enable = true;
|
||||
user = "yourUsername";
|
||||
rotation.interval = "5m"; # Same as old duration setting
|
||||
wpaperd.mode = "center"; # Same as old mode setting
|
||||
};
|
||||
```
|
||||
|
||||
3. **Rebuild** your system - wpaperd will now be managed by themeSwitcher
|
||||
|
||||
## Technical Details
|
||||
|
||||
- **Pywal Template Syntax**: `{background}`, `{foreground}`, `{color0}`-`{color15}`, `{cursor}`, `{wallpaper}`
|
||||
- **Template Location**: `~/.config/wal/templates/filename.ext`
|
||||
- **Output Location**: `~/.cache/wal/filename.ext`
|
||||
- **Color Extraction**: 16-color palette using pywal16
|
||||
- **GTK Reload**: Automatic via gsettings changes
|
||||
- **Kvantum Reload**: May require app restart
|
||||
- **Ghostty Reload**: Automatic on config file change
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
- Direct wpaperd integration (trigger theme change on wallpaper rotation)
|
||||
- Per-monitor theme configuration
|
||||
- Additional application templates (Firefox, VSCode, etc.)
|
||||
- Theme preview before applying
|
||||
- Fallback color schemes for monochrome wallpapers
|
||||
|
||||
## Credits
|
||||
|
||||
- Built for NixOS using home-manager
|
||||
- Uses [pywal16](https://github.com/eylles/pywal16) for color extraction
|
||||
- Based on [pywal](https://github.com/dylanaraps/pywal) original concept
|
||||
347
shared/modules/services/theme_switcher/TESTING_GUIDE.md
Normal file
347
shared/modules/services/theme_switcher/TESTING_GUIDE.md
Normal file
@ -0,0 +1,347 @@
|
||||
# Theme Switcher Testing Guide
|
||||
|
||||
This guide will help you test the theme switcher module on the nate-work host.
|
||||
|
||||
## Pre-Testing Checklist
|
||||
|
||||
### 1. Review Current Configuration
|
||||
|
||||
Check your current nate-work configuration:
|
||||
|
||||
```bash
|
||||
# View current wallpaper rotator settings
|
||||
grep -A 10 "wallpaperRotator" nate-work/desktop-configuration.nix
|
||||
|
||||
# View current GTK/Qt theme settings
|
||||
grep -A 10 "gtk\|kvantum" nate-work/modules/home-manager/home.nix
|
||||
```
|
||||
|
||||
### 2. Files to Modify
|
||||
|
||||
You'll need to modify these files:
|
||||
|
||||
1. **nate-work/desktop-configuration.nix** (or wherever you import modules)
|
||||
- Add themeSwitcher import
|
||||
- Disable wallpaperRotator if enabled
|
||||
- Configure themeSwitcher settings
|
||||
|
||||
2. **nate-work/modules/home-manager/home.nix**
|
||||
- Comment out static GTK theme configuration
|
||||
- Comment out static Kvantum configuration
|
||||
- Keep icon and cursor themes
|
||||
|
||||
## Testing Phase 1: Manual Mode
|
||||
|
||||
Start with manual mode to test theme generation before enabling automation.
|
||||
|
||||
### Step 1: Add Module Configuration
|
||||
|
||||
Edit `nate-work/desktop-configuration.nix`:
|
||||
|
||||
```nix
|
||||
{
|
||||
imports = [
|
||||
# ... existing imports ...
|
||||
../../shared/modules/services/theme_switcher
|
||||
];
|
||||
|
||||
# Disable old wallpaper rotator if present
|
||||
# services.wallpaperRotator.enable = false;
|
||||
|
||||
# Enable theme switcher in manual mode
|
||||
services.themeSwitcher = {
|
||||
enable = true;
|
||||
user = "nate";
|
||||
|
||||
# Start with manual mode (no auto-switching)
|
||||
enableAutoSwitch = false;
|
||||
|
||||
# Use switch-only mode for testing (no continuous rotation yet)
|
||||
rotation.mode = "switch-only";
|
||||
|
||||
# Configure wpaperd
|
||||
wpaperd = {
|
||||
enable = true;
|
||||
mode = "center";
|
||||
transition.effect = "fade";
|
||||
transition.duration = 300;
|
||||
};
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### Step 2: Comment Out Static Themes
|
||||
|
||||
Edit `nate-work/modules/home-manager/home.nix`:
|
||||
|
||||
```nix
|
||||
# Comment out these sections:
|
||||
# gtk.theme = { ... };
|
||||
# xdg.configFile."Kvantum/kvantum.kvconfig" = ...;
|
||||
# xdg.configFile."gtk-4.0/..." = ...;
|
||||
|
||||
# Keep these (icon and cursor themes are independent):
|
||||
gtk.iconTheme = { ... }; # Keep
|
||||
gtk.cursorTheme = { ... }; # Keep
|
||||
```
|
||||
|
||||
### Step 3: Rebuild System
|
||||
|
||||
```bash
|
||||
# From /home/nate/nixos directory
|
||||
sudo nixos-rebuild switch --flake .#nate-work
|
||||
```
|
||||
|
||||
This will:
|
||||
- Install pywal16, haishoku, wpaperd
|
||||
- Install apply-theme.sh to ~/.local/bin/
|
||||
- Install pywal templates
|
||||
- Start wpaperd (but with no auto-rotation)
|
||||
- NOT start any timers (manual mode)
|
||||
|
||||
### Step 4: Manual Testing
|
||||
|
||||
After rebuild, test the script manually:
|
||||
|
||||
```bash
|
||||
# Test light theme
|
||||
~/.local/bin/apply-theme.sh --light
|
||||
|
||||
# Verify:
|
||||
# 1. Wallpaper changed to one from Light/ directory
|
||||
# 2. Colors extracted: cat ~/.cache/wal/colors.json
|
||||
# 3. GTK theme generated: ls ~/.config/gtk-3.0/gtk.css
|
||||
# 4. Kvantum theme generated: ls ~/.config/Kvantum/PywalTheme/
|
||||
# 5. Open a GTK app (nautilus, calculator) - check colors
|
||||
```
|
||||
|
||||
```bash
|
||||
# Test dark theme
|
||||
~/.local/bin/apply-theme.sh --dark
|
||||
|
||||
# Verify same things with Dark wallpaper
|
||||
```
|
||||
|
||||
```bash
|
||||
# Test with specific wallpaper
|
||||
~/.local/bin/apply-theme.sh --light --wallpaper-path ~/nixos/shared/modules/services/wallpapers/Light/IU-Light.jpg
|
||||
```
|
||||
|
||||
### Step 5: Verify Wpaperd Integration
|
||||
|
||||
```bash
|
||||
# Check if wpaperd is running
|
||||
pgrep wpaperd
|
||||
|
||||
# Check wpaperd status
|
||||
systemctl --user status wpaperd
|
||||
|
||||
# Manually change wallpaper using wpaperctl
|
||||
wpaperctl wallpaper ~/nixos/shared/modules/services/wallpapers/Dark/IU-Dark.jpg
|
||||
```
|
||||
|
||||
## Testing Phase 2: Automatic Switching (Time-Based)
|
||||
|
||||
Once manual testing works, enable automatic time-based switching.
|
||||
|
||||
### Step 1: Enable Auto-Switch
|
||||
|
||||
Edit `nate-work/desktop-configuration.nix`:
|
||||
|
||||
```nix
|
||||
services.themeSwitcher = {
|
||||
enable = true;
|
||||
user = "nate";
|
||||
|
||||
# Enable automatic switching
|
||||
enableAutoSwitch = true;
|
||||
|
||||
# Keep switch-only mode for now
|
||||
rotation.mode = "switch-only";
|
||||
|
||||
# Optional: Adjust times for testing
|
||||
lightTime = "08:00:00"; # Your preferred time
|
||||
darkTime = "17:00:00"; # Your preferred time
|
||||
};
|
||||
```
|
||||
|
||||
### Step 2: Rebuild and Check Timer
|
||||
|
||||
```bash
|
||||
sudo nixos-rebuild switch --flake .#nate-work
|
||||
|
||||
# Verify timer is installed
|
||||
systemctl --user list-timers | grep theme
|
||||
|
||||
# Should show two entries (one for lightTime, one for darkTime)
|
||||
systemctl --user status theme-switcher.timer
|
||||
```
|
||||
|
||||
### Step 3: Test Timer Manually
|
||||
|
||||
```bash
|
||||
# Manually trigger the service
|
||||
systemctl --user start theme-switcher.service
|
||||
|
||||
# Check logs
|
||||
journalctl --user -u theme-switcher.service -n 50
|
||||
|
||||
# Should show theme being applied based on current time
|
||||
```
|
||||
|
||||
## Testing Phase 3: Continuous Rotation
|
||||
|
||||
Once automatic switching works, enable continuous wallpaper rotation.
|
||||
|
||||
### Step 1: Enable Continuous Mode
|
||||
|
||||
Edit `nate-work/desktop-configuration.nix`:
|
||||
|
||||
```nix
|
||||
services.themeSwitcher = {
|
||||
enable = true;
|
||||
user = "nate";
|
||||
enableAutoSwitch = true;
|
||||
|
||||
# Enable continuous rotation
|
||||
rotation = {
|
||||
mode = "continuous";
|
||||
interval = "5m"; # Rotate every 5 minutes
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
### Step 2: Rebuild and Monitor
|
||||
|
||||
```bash
|
||||
sudo nixos-rebuild switch --flake .#nate-work
|
||||
|
||||
# Check rotation timer
|
||||
systemctl --user list-timers | grep wallpaper
|
||||
systemctl --user status wallpaper-rotation.timer
|
||||
|
||||
# Watch logs in real-time
|
||||
journalctl --user -u wallpaper-rotation.service -f
|
||||
```
|
||||
|
||||
### Step 3: Verify Rotation
|
||||
|
||||
Wait 5 minutes and verify:
|
||||
- Wallpaper changes
|
||||
- Theme regenerates
|
||||
- Colors match new wallpaper
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Theme Not Applying
|
||||
|
||||
```bash
|
||||
# Check if pywal generated files
|
||||
ls -la ~/.cache/wal/
|
||||
|
||||
# Expected files:
|
||||
# - colors.json
|
||||
# - gtk-3.0.css
|
||||
# - gtk-4.0.css
|
||||
# - PywalTheme.kvconfig
|
||||
# - ghostty.conf
|
||||
```
|
||||
|
||||
### Wallpaper Not Changing
|
||||
|
||||
```bash
|
||||
# Check wpaperd status
|
||||
systemctl --user status wpaperd
|
||||
journalctl --user -u wpaperd -n 50
|
||||
|
||||
# Try manual change
|
||||
wpaperctl wallpaper ~/path/to/wallpaper.jpg
|
||||
```
|
||||
|
||||
### GTK Theme Not Loading
|
||||
|
||||
```bash
|
||||
# Check GTK config
|
||||
ls -la ~/.config/gtk-3.0/
|
||||
ls -la ~/.config/gtk-4.0/
|
||||
|
||||
# Check if files are symlinks
|
||||
readlink ~/.config/gtk-3.0/gtk.css
|
||||
# Should point to ~/.cache/wal/gtk-3.0.css
|
||||
```
|
||||
|
||||
### Kvantum Theme Not Loading
|
||||
|
||||
```bash
|
||||
# Check Kvantum directory
|
||||
ls -la ~/.config/Kvantum/PywalTheme/
|
||||
|
||||
# Manually set theme
|
||||
kvantummanager --set PywalTheme
|
||||
|
||||
# Restart Qt applications
|
||||
```
|
||||
|
||||
### Service Failures
|
||||
|
||||
```bash
|
||||
# Check service logs
|
||||
journalctl --user -u theme-switcher.service --since today
|
||||
journalctl --user -u wallpaper-rotation.service --since today
|
||||
|
||||
# Check environment
|
||||
systemctl --user show theme-switcher.service | grep PATH
|
||||
```
|
||||
|
||||
## Performance Monitoring
|
||||
|
||||
### Resource Usage (Continuous Mode)
|
||||
|
||||
```bash
|
||||
# Monitor theme regeneration
|
||||
watch -n 60 'ls -lh ~/.cache/wal/colors.json'
|
||||
|
||||
# Check pywal CPU usage
|
||||
top -b -n 1 | grep wal
|
||||
```
|
||||
|
||||
If continuous mode is too resource-intensive, switch to switch-only mode.
|
||||
|
||||
## Success Criteria
|
||||
|
||||
- ✓ Manual theme switching works (--light and --dark)
|
||||
- ✓ Wallpapers change via wpaperctl
|
||||
- ✓ GTK apps reflect new colors
|
||||
- ✓ Qt apps reflect new Kvantum theme
|
||||
- ✓ Ghostty terminal shows new colors (if installed)
|
||||
- ✓ Timers appear in systemctl --user list-timers
|
||||
- ✓ Automatic switching works at configured times
|
||||
- ✓ Continuous rotation works (if enabled)
|
||||
- ✓ No service failures in journalctl
|
||||
|
||||
## Next Steps After Testing
|
||||
|
||||
Once everything works on nate-work:
|
||||
|
||||
1. Consider applying to other hosts (frame12, nate, scrappy)
|
||||
2. Fine-tune rotation interval if needed
|
||||
3. Try different pywal backends (modern_colorthief, etc.)
|
||||
4. Customize wpaperd transition effects
|
||||
5. Add custom pywal templates for other apps
|
||||
|
||||
## Rollback Plan
|
||||
|
||||
If something goes wrong:
|
||||
|
||||
```bash
|
||||
# Disable the module
|
||||
# In nate-work/desktop-configuration.nix:
|
||||
services.themeSwitcher.enable = false;
|
||||
|
||||
# Re-enable old configuration
|
||||
# services.wallpaperRotator.enable = true;
|
||||
# Uncomment GTK/Qt theme settings in home.nix
|
||||
|
||||
# Rebuild
|
||||
sudo nixos-rebuild switch --flake .#nate-work
|
||||
```
|
||||
243
shared/modules/services/theme_switcher/apply_themes.py
Executable file
243
shared/modules/services/theme_switcher/apply_themes.py
Executable file
@ -0,0 +1,243 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Apply Themes Script - Applies GTK/Kvantum/Helix themes after pywal runs.
|
||||
|
||||
This script:
|
||||
1. Links GTK CSS files from pywal cache
|
||||
2. Sets GTK color scheme preference based on light/dark mode
|
||||
3. Copies and applies Kvantum theme
|
||||
4. Copies Helix theme to config directory
|
||||
5. Optionally runs color_mapper.py for semantic color mapping
|
||||
"""
|
||||
|
||||
import json
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
import logging
|
||||
|
||||
# Setup logging
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(levelname)s: %(message)s'
|
||||
)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ThemeApplicator:
|
||||
"""Applies themes after pywal has generated color palette."""
|
||||
|
||||
def __init__(self):
|
||||
self.home = Path.home()
|
||||
self.cache_dir = self.home / ".cache/wal"
|
||||
self.config_dir = self.home / ".config"
|
||||
|
||||
def is_light_mode(self) -> bool:
|
||||
"""
|
||||
Determine if current theme is light mode by checking pywal cache.
|
||||
|
||||
Returns:
|
||||
True if light mode, False if dark mode
|
||||
"""
|
||||
colors_json = self.cache_dir / "colors.json"
|
||||
|
||||
if not colors_json.exists():
|
||||
logger.warning("colors.json not found, assuming dark mode")
|
||||
return False
|
||||
|
||||
try:
|
||||
with open(colors_json) as f:
|
||||
data = json.load(f)
|
||||
return data.get("special", {}).get("light", False)
|
||||
except (json.JSONDecodeError, KeyError) as e:
|
||||
logger.warning(f"Failed to parse colors.json: {e}, assuming dark mode")
|
||||
return False
|
||||
|
||||
def setup_gtk_themes(self) -> None:
|
||||
"""Link GTK CSS files from pywal cache."""
|
||||
gtk3_dir = self.config_dir / "gtk-3.0"
|
||||
gtk4_dir = self.config_dir / "gtk-4.0"
|
||||
|
||||
# Ensure directories exist
|
||||
gtk3_dir.mkdir(parents=True, exist_ok=True)
|
||||
gtk4_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Link GTK 3.0 CSS
|
||||
gtk3_source = self.cache_dir / "gtk-3.0.css"
|
||||
gtk3_target = gtk3_dir / "gtk.css"
|
||||
|
||||
if gtk3_source.exists():
|
||||
# Remove old symlink/file
|
||||
if gtk3_target.exists() or gtk3_target.is_symlink():
|
||||
gtk3_target.unlink()
|
||||
gtk3_target.symlink_to(gtk3_source)
|
||||
logger.info(f"Linked GTK 3.0 theme: {gtk3_target}")
|
||||
else:
|
||||
logger.warning(f"GTK 3.0 CSS not found: {gtk3_source}")
|
||||
|
||||
# Link GTK 4.0 CSS
|
||||
gtk4_source = self.cache_dir / "gtk-4.0.css"
|
||||
gtk4_target = gtk4_dir / "gtk.css"
|
||||
|
||||
if gtk4_source.exists():
|
||||
# Remove old symlink/file
|
||||
if gtk4_target.exists() or gtk4_target.is_symlink():
|
||||
gtk4_target.unlink()
|
||||
gtk4_target.symlink_to(gtk4_source)
|
||||
logger.info(f"Linked GTK 4.0 theme: {gtk4_target}")
|
||||
else:
|
||||
logger.warning(f"GTK 4.0 CSS not found: {gtk4_source}")
|
||||
|
||||
def set_gtk_color_scheme(self, is_light: bool) -> None:
|
||||
"""
|
||||
Set GTK color scheme preference using gsettings.
|
||||
|
||||
Args:
|
||||
is_light: True for light mode, False for dark mode
|
||||
"""
|
||||
scheme = "prefer-light" if is_light else "prefer-dark"
|
||||
|
||||
try:
|
||||
subprocess.run(
|
||||
['gsettings', 'set', 'org.gnome.desktop.interface', 'color-scheme', scheme],
|
||||
check=True,
|
||||
timeout=5,
|
||||
capture_output=True
|
||||
)
|
||||
logger.info(f"Set GTK color scheme to: {scheme}")
|
||||
except (subprocess.CalledProcessError, subprocess.TimeoutExpired, FileNotFoundError) as e:
|
||||
logger.warning(f"Failed to set GTK color scheme (this is normal if not using GNOME): {e}")
|
||||
|
||||
def setup_kvantum_theme(self) -> None:
|
||||
"""Copy and apply Kvantum theme from pywal cache."""
|
||||
kvantum_dir = self.config_dir / "Kvantum/PywalTheme"
|
||||
kvantum_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Copy Kvantum config
|
||||
kvantum_source = self.cache_dir / "PywalTheme.kvconfig"
|
||||
kvantum_target = kvantum_dir / "PywalTheme.kvconfig"
|
||||
|
||||
if kvantum_source.exists():
|
||||
shutil.copy2(kvantum_source, kvantum_target)
|
||||
logger.info(f"Copied Kvantum theme: {kvantum_target}")
|
||||
|
||||
# Apply Kvantum theme
|
||||
try:
|
||||
subprocess.run(
|
||||
['kvantummanager', '--set', 'PywalTheme'],
|
||||
check=True,
|
||||
timeout=5,
|
||||
capture_output=True
|
||||
)
|
||||
logger.info("Applied Kvantum theme: PywalTheme")
|
||||
except FileNotFoundError:
|
||||
logger.warning("kvantummanager not found, skipping Kvantum theme application")
|
||||
except (subprocess.CalledProcessError, subprocess.TimeoutExpired) as e:
|
||||
logger.warning(f"Failed to apply Kvantum theme: {e}")
|
||||
else:
|
||||
logger.warning(f"Kvantum theme not found: {kvantum_source}")
|
||||
|
||||
def setup_helix_theme(self) -> None:
|
||||
"""Copy Helix theme from pywal cache to config directory."""
|
||||
helix_themes_dir = self.config_dir / "helix/themes"
|
||||
helix_themes_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Copy minimal Helix theme
|
||||
helix_source = self.cache_dir / "helix_minimal.toml"
|
||||
helix_target = helix_themes_dir / "pywal_minimal.toml"
|
||||
|
||||
if helix_source.exists():
|
||||
shutil.copy2(helix_source, helix_target)
|
||||
logger.info(f"Updated Helix theme: pywal_minimal")
|
||||
else:
|
||||
logger.warning(f"Helix theme not found: {helix_source}")
|
||||
|
||||
# Copy semantic Helix theme if it exists
|
||||
helix_semantic_source = self.cache_dir / "helix_semantic.toml"
|
||||
helix_semantic_target = helix_themes_dir / "pywal_semantic.toml"
|
||||
|
||||
if helix_semantic_source.exists():
|
||||
shutil.copy2(helix_semantic_source, helix_semantic_target)
|
||||
logger.info(f"Updated Helix semantic theme: pywal_semantic")
|
||||
|
||||
def run_color_mapper(self) -> None:
|
||||
"""
|
||||
Run color_mapper.py to generate semantic color mappings.
|
||||
|
||||
This is optional and will only run if the script exists.
|
||||
"""
|
||||
# Look for color_mapper.py in the same directory as this script
|
||||
script_dir = Path(__file__).parent
|
||||
color_mapper = script_dir / "color_mapper.py"
|
||||
|
||||
if not color_mapper.exists():
|
||||
logger.debug(f"color_mapper.py not found at {color_mapper}, skipping")
|
||||
return
|
||||
|
||||
try:
|
||||
result = subprocess.run(
|
||||
['python3', str(color_mapper)],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=10,
|
||||
check=True
|
||||
)
|
||||
logger.info("Applied semantic color mapping")
|
||||
# Log color mapper output for debugging
|
||||
if result.stdout:
|
||||
for line in result.stdout.strip().split('\n'):
|
||||
logger.debug(f"color_mapper: {line}")
|
||||
except FileNotFoundError:
|
||||
logger.warning("python3 not found, skipping color mapper")
|
||||
except (subprocess.CalledProcessError, subprocess.TimeoutExpired) as e:
|
||||
logger.warning(f"Failed to run color_mapper.py: {e}")
|
||||
|
||||
def run(self) -> int:
|
||||
"""
|
||||
Execute theme application process.
|
||||
|
||||
Returns:
|
||||
Exit code (0 for success, 1 for failure)
|
||||
"""
|
||||
try:
|
||||
# Check if pywal has been run
|
||||
if not self.cache_dir.exists():
|
||||
logger.error(f"Pywal cache not found: {self.cache_dir}")
|
||||
logger.error("Please run pywal first")
|
||||
return 1
|
||||
|
||||
# Determine mode
|
||||
is_light = self.is_light_mode()
|
||||
mode = "light" if is_light else "dark"
|
||||
logger.info(f"Applying {mode} mode themes")
|
||||
|
||||
# Setup GTK themes
|
||||
self.setup_gtk_themes()
|
||||
self.set_gtk_color_scheme(is_light)
|
||||
|
||||
# Setup Kvantum theme
|
||||
self.setup_kvantum_theme()
|
||||
|
||||
# Setup Helix theme
|
||||
self.setup_helix_theme()
|
||||
|
||||
# Run color mapper for semantic themes
|
||||
self.run_color_mapper()
|
||||
|
||||
logger.info("All themes applied successfully")
|
||||
return 0
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Theme application failed: {e}", exc_info=True)
|
||||
return 1
|
||||
|
||||
|
||||
def main():
|
||||
"""Main entry point."""
|
||||
applicator = ThemeApplicator()
|
||||
return applicator.run()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main())
|
||||
304
shared/modules/services/theme_switcher/color_mapper.py
Normal file
304
shared/modules/services/theme_switcher/color_mapper.py
Normal file
@ -0,0 +1,304 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Intelligent color mapper for pywal themes.
|
||||
|
||||
Analyzes generated pywal colors and maps them to semantic roles based on
|
||||
hue, saturation, and lightness. Creates a mapping file that can be used
|
||||
to apply consistent semantic meaning to colors regardless of wallpaper.
|
||||
"""
|
||||
|
||||
import json
|
||||
import colorsys
|
||||
from pathlib import Path
|
||||
from typing import Dict, Tuple, List
|
||||
|
||||
def hex_to_rgb(hex_color: str) -> Tuple[int, int, int]:
|
||||
"""Convert hex color to RGB tuple."""
|
||||
hex_color = hex_color.lstrip('#')
|
||||
return (int(hex_color[0:2], 16), int(hex_color[2:4], 16), int(hex_color[4:6], 16))
|
||||
|
||||
def rgb_to_hsl(rgb: Tuple[int, int, int]) -> Tuple[float, float, float]:
|
||||
"""Convert RGB to HSL."""
|
||||
r, g, b = [x / 255.0 for x in rgb]
|
||||
h, l, s = colorsys.rgb_to_hls(r, g, b)
|
||||
return (h * 360, s * 100, l * 100) # Convert to degrees and percentages
|
||||
|
||||
def analyze_color(hex_color: str) -> Dict:
|
||||
"""Analyze a color and return its properties."""
|
||||
rgb = hex_to_rgb(hex_color)
|
||||
h, s, l = rgb_to_hsl(rgb)
|
||||
|
||||
# Determine color category based on hue
|
||||
if s < 10:
|
||||
category = "neutral"
|
||||
elif 0 <= h < 30 or 330 <= h <= 360:
|
||||
category = "red"
|
||||
elif 30 <= h < 60:
|
||||
category = "orange"
|
||||
elif 60 <= h < 90:
|
||||
category = "yellow"
|
||||
elif 90 <= h < 150:
|
||||
category = "green"
|
||||
elif 150 <= h < 210:
|
||||
category = "cyan"
|
||||
elif 210 <= h < 270:
|
||||
category = "blue"
|
||||
elif 270 <= h < 330:
|
||||
category = "magenta"
|
||||
else:
|
||||
category = "neutral"
|
||||
|
||||
return {
|
||||
"hex": hex_color,
|
||||
"rgb": rgb,
|
||||
"hue": h,
|
||||
"saturation": s,
|
||||
"lightness": l,
|
||||
"category": category
|
||||
}
|
||||
|
||||
def calculate_contrast(color1_hsl: Tuple[float, float, float], color2_hsl: Tuple[float, float, float]) -> float:
|
||||
"""Calculate perceived contrast between two colors based on lightness difference."""
|
||||
return abs(color1_hsl[2] - color2_hsl[2])
|
||||
|
||||
def find_best_color_for_role(colors: Dict[str, Dict], role: str, is_light_mode: bool, bg_lightness: float = 50.0) -> str:
|
||||
"""
|
||||
Find the best color from the palette for a given semantic role.
|
||||
|
||||
Roles: error, warning, success, info, emphasis, secondary
|
||||
"""
|
||||
|
||||
# Define preferred characteristics for each role
|
||||
role_preferences = {
|
||||
"error": {
|
||||
"categories": ["red", "orange"],
|
||||
"min_saturation": 30,
|
||||
"prefer_vibrant": True,
|
||||
"prefer_contrast": True # Errors need high visibility
|
||||
},
|
||||
"warning": {
|
||||
"categories": ["orange", "yellow"],
|
||||
"min_saturation": 30,
|
||||
"prefer_vibrant": True,
|
||||
"prefer_contrast": True
|
||||
},
|
||||
"comment": {
|
||||
"categories": ["yellow", "orange", "green", "cyan"], # Flexible for high contrast
|
||||
"min_saturation": 25,
|
||||
"prefer_vibrant": False,
|
||||
"prefer_contrast": True # Comments should be very visible
|
||||
},
|
||||
"string": {
|
||||
"categories": ["green", "cyan"],
|
||||
"min_saturation": 25,
|
||||
"prefer_vibrant": False,
|
||||
"prefer_contrast": False
|
||||
},
|
||||
"keyword": {
|
||||
"categories": ["magenta", "blue"],
|
||||
"min_saturation": 20,
|
||||
"prefer_vibrant": False,
|
||||
"prefer_contrast": False
|
||||
},
|
||||
"number": {
|
||||
"categories": ["orange", "red", "magenta"],
|
||||
"min_saturation": 30,
|
||||
"prefer_vibrant": True,
|
||||
"prefer_contrast": False
|
||||
},
|
||||
"info": {
|
||||
"categories": ["blue", "cyan"],
|
||||
"min_saturation": 20,
|
||||
"prefer_vibrant": False,
|
||||
"prefer_contrast": False
|
||||
}
|
||||
}
|
||||
|
||||
prefs = role_preferences.get(role, {})
|
||||
preferred_cats = prefs.get("categories", [])
|
||||
min_sat = prefs.get("min_saturation", 0)
|
||||
prefer_vibrant = prefs.get("prefer_vibrant", False)
|
||||
prefer_contrast = prefs.get("prefer_contrast", False)
|
||||
|
||||
# Score each color
|
||||
best_score = -1
|
||||
best_color = None
|
||||
|
||||
for color_name, props in colors.items():
|
||||
if props["category"] == "neutral":
|
||||
continue
|
||||
|
||||
score = 0
|
||||
|
||||
# Category match (highest priority)
|
||||
if props["category"] in preferred_cats:
|
||||
score += 100
|
||||
|
||||
# Saturation requirements
|
||||
if props["saturation"] >= min_sat:
|
||||
score += 50
|
||||
|
||||
# Contrast preference (for comments and errors)
|
||||
if prefer_contrast:
|
||||
# Calculate contrast with background
|
||||
contrast = abs(props["lightness"] - bg_lightness)
|
||||
score += contrast * 0.5 # Add up to 50 points for maximum contrast
|
||||
|
||||
# Vibrancy preference
|
||||
if prefer_vibrant:
|
||||
# For vibrant colors, prefer higher saturation and medium lightness
|
||||
score += props["saturation"] / 2
|
||||
if 40 <= props["lightness"] <= 60:
|
||||
score += 20
|
||||
else:
|
||||
# For less vibrant, prefer moderate saturation
|
||||
if 30 <= props["saturation"] <= 70:
|
||||
score += 20
|
||||
|
||||
# Light mode adjustments - prefer colors with good contrast
|
||||
if is_light_mode:
|
||||
# In light mode, prefer darker, more saturated colors for visibility
|
||||
if props["lightness"] < 50:
|
||||
score += 10
|
||||
else:
|
||||
# In dark mode, prefer brighter colors
|
||||
if props["lightness"] > 40:
|
||||
score += 10
|
||||
|
||||
if score > best_score:
|
||||
best_score = score
|
||||
best_color = color_name
|
||||
|
||||
# Fallback to first color if no match found
|
||||
if best_color is None:
|
||||
best_color = list(colors.keys())[0]
|
||||
|
||||
return best_color
|
||||
|
||||
def create_semantic_mapping(colors_json_path: Path) -> Dict:
|
||||
"""Create semantic color mapping from pywal colors."""
|
||||
|
||||
with open(colors_json_path) as f:
|
||||
data = json.load(f)
|
||||
|
||||
# Determine if light mode and get background lightness
|
||||
bg_hex = data.get("special", {}).get("background", "#000000")
|
||||
bg_rgb = hex_to_rgb(bg_hex)
|
||||
bg_hsl = rgb_to_hsl(bg_rgb)
|
||||
bg_lightness = bg_hsl[2]
|
||||
is_light_mode = bg_lightness > 50
|
||||
|
||||
# Analyze all colors
|
||||
analyzed = {}
|
||||
for i in range(1, 8): # color1 through color7 (skip color0 which is usually bg)
|
||||
color_key = f"color{i}"
|
||||
hex_color = data["colors"][color_key]
|
||||
analyzed[color_key] = analyze_color(hex_color)
|
||||
|
||||
# Map semantic roles to best matching colors
|
||||
mapping = {
|
||||
"error": find_best_color_for_role(analyzed, "error", is_light_mode, bg_lightness),
|
||||
"warning": find_best_color_for_role(analyzed, "warning", is_light_mode, bg_lightness),
|
||||
"comment": find_best_color_for_role(analyzed, "comment", is_light_mode, bg_lightness),
|
||||
"string": find_best_color_for_role(analyzed, "string", is_light_mode, bg_lightness),
|
||||
"keyword": find_best_color_for_role(analyzed, "keyword", is_light_mode, bg_lightness),
|
||||
"number": find_best_color_for_role(analyzed, "number", is_light_mode, bg_lightness),
|
||||
"info": find_best_color_for_role(analyzed, "info", is_light_mode, bg_lightness),
|
||||
}
|
||||
|
||||
# Add the original color values for reference
|
||||
result = {
|
||||
"semantic_mapping": mapping,
|
||||
"is_light_mode": is_light_mode,
|
||||
"color_analysis": analyzed
|
||||
}
|
||||
|
||||
return result
|
||||
|
||||
def apply_semantic_mapping(template_path: Path, output_path: Path, mapping: Dict):
|
||||
"""Apply semantic mapping to Helix template."""
|
||||
|
||||
with open(template_path) as f:
|
||||
template = f.read()
|
||||
|
||||
# Read pywal colors
|
||||
colors_json = Path.home() / ".cache/wal/colors.json"
|
||||
with open(colors_json) as f:
|
||||
colors_data = json.load(f)
|
||||
|
||||
# Replace color references with semantically mapped ones
|
||||
semantic = mapping["semantic_mapping"]
|
||||
|
||||
# First, do standard pywal substitutions
|
||||
output = template
|
||||
|
||||
# Replace special colors
|
||||
for key, value in colors_data["special"].items():
|
||||
output = output.replace(f"{{{key}}}", value)
|
||||
|
||||
# Replace palette colors
|
||||
for key, value in colors_data["colors"].items():
|
||||
output = output.replace(f"{{{key}}}", value)
|
||||
|
||||
# Now apply semantic color replacements
|
||||
error_color = colors_data["colors"][semantic["error"]]
|
||||
warning_color = colors_data["colors"][semantic["warning"]]
|
||||
comment_color = colors_data["colors"][semantic["comment"]]
|
||||
string_color = colors_data["colors"][semantic["string"]]
|
||||
keyword_color = colors_data["colors"][semantic["keyword"]]
|
||||
number_color = colors_data["colors"][semantic["number"]]
|
||||
info_color = colors_data["colors"][semantic["info"]]
|
||||
|
||||
# Replace semantic placeholders
|
||||
output = output.replace("{{ERROR_COLOR}}", error_color)
|
||||
output = output.replace("{{WARNING_COLOR}}", warning_color)
|
||||
output = output.replace("{{COMMENT_COLOR}}", comment_color)
|
||||
output = output.replace("{{STRING_COLOR}}", string_color)
|
||||
output = output.replace("{{KEYWORD_COLOR}}", keyword_color)
|
||||
output = output.replace("{{NUMBER_COLOR}}", number_color)
|
||||
output = output.replace("{{INFO_COLOR}}", info_color)
|
||||
|
||||
with open(output_path, 'w') as f:
|
||||
f.write(output)
|
||||
|
||||
def main():
|
||||
"""Main entry point."""
|
||||
import sys
|
||||
|
||||
colors_json = Path.home() / ".cache/wal/colors.json"
|
||||
|
||||
if not colors_json.exists():
|
||||
print("Error: Pywal colors.json not found. Run pywal first.")
|
||||
return 1
|
||||
|
||||
# Analyze colors and create mapping
|
||||
mapping = create_semantic_mapping(colors_json)
|
||||
|
||||
# Save mapping for debugging/inspection
|
||||
mapping_output = Path.home() / ".cache/wal/semantic_mapping.json"
|
||||
with open(mapping_output, 'w') as f:
|
||||
json.dump(mapping, f, indent=2)
|
||||
|
||||
print("Semantic color mapping created:")
|
||||
for role, color in mapping["semantic_mapping"].items():
|
||||
props = mapping["color_analysis"][color]
|
||||
print(f" {role:12} -> {color} ({props['category']:8} {props['hex']})")
|
||||
|
||||
# Apply to helix template if it exists
|
||||
helix_template = Path.home() / ".config/wal/templates/helix_semantic.toml"
|
||||
if helix_template.exists():
|
||||
helix_output = Path.home() / ".cache/wal/helix_semantic.toml"
|
||||
apply_semantic_mapping(helix_template, helix_output, mapping)
|
||||
print(f"\nApplied semantic mapping to: {helix_output}")
|
||||
|
||||
# Also copy to helix themes directory
|
||||
helix_themes_dir = Path.home() / ".config/helix/themes"
|
||||
helix_themes_dir.mkdir(parents=True, exist_ok=True)
|
||||
final_output = helix_themes_dir / "pywal_semantic.toml"
|
||||
apply_semantic_mapping(helix_template, final_output, mapping)
|
||||
print(f"Copied to: {final_output}")
|
||||
|
||||
return 0
|
||||
|
||||
if __name__ == "__main__":
|
||||
exit(main())
|
||||
348
shared/modules/services/theme_switcher/default.nix
Normal file
348
shared/modules/services/theme_switcher/default.nix
Normal file
@ -0,0 +1,348 @@
|
||||
{ config, lib, pkgs, ... }:
|
||||
|
||||
# Theme Switcher Module - Architecture Overview
|
||||
#
|
||||
# This module integrates wpaperd for wallpaper management with pywal for dynamic theming.
|
||||
# wpaperd handles wallpaper rotation and transitions, while Python scripts handle palette
|
||||
# generation and intelligent semantic color mapping.
|
||||
#
|
||||
# Python Scripts:
|
||||
# - theme_switcher.py: Time-based light/dark theme switching
|
||||
# - wallpaper_rotation.py: Continuous wallpaper rotation with theme regeneration
|
||||
# - apply_themes.py: Applies GTK/Kvantum/Helix themes after pywal runs
|
||||
# - color_mapper.py: Intelligent semantic color mapping (reds for errors, high contrast for comments)
|
||||
#
|
||||
# Two modes of operation:
|
||||
#
|
||||
# 1. Light/Dark Theme Switching (theme-switcher service, triggered by timer at configured times):
|
||||
# - Updates wpaperd config to point to Light/ or Dark/ directory
|
||||
# - Restarts wpaperd (which picks a random wallpaper with transition)
|
||||
# - Generates pywal palette from the selected wallpaper
|
||||
# - Runs semantic color mapper to assign colors based on hue analysis
|
||||
# - Applies GTK/Kvantum/Helix themes with intelligent color assignments
|
||||
#
|
||||
# 2. Continuous Wallpaper Rotation (wallpaper-rotation service, triggered by timer every N minutes):
|
||||
# - Pauses wpaperd's auto-rotation
|
||||
# - Calls wpaperctl next-wallpaper (wpaperd handles the smooth transition)
|
||||
# - Gets current wallpaper from wpaperd
|
||||
# - Regenerates pywal palette from new wallpaper
|
||||
# - Runs semantic color mapper for intelligent color assignment
|
||||
# - Applies GTK/Kvantum/Helix themes
|
||||
# - Resumes wpaperd's auto-rotation
|
||||
#
|
||||
# This approach leverages wpaperd's built-in features (transitions, random selection, timing)
|
||||
# while keeping palette generation and theme application in sync with wallpaper changes.
|
||||
# The semantic color mapper ensures consistent meaning (e.g., reds for errors) regardless of wallpaper.
|
||||
|
||||
let
|
||||
cfg = config.services.themeSwitcher;
|
||||
|
||||
# Get home directory for the user
|
||||
userConfig = config.home-manager.users.${cfg.user};
|
||||
homeDir = userConfig.home.homeDirectory or "/home/${cfg.user}";
|
||||
|
||||
# Create a custom pywal16 with haishoku backend available
|
||||
pywal16WithBackends = pkgs.pywal16.overridePythonAttrs (old: {
|
||||
propagatedBuildInputs = (old.propagatedBuildInputs or []) ++ [
|
||||
pkgs.python313Packages.haishoku
|
||||
];
|
||||
});
|
||||
|
||||
# Python scripts for theme management
|
||||
themeSwitcherScript = pkgs.writeScript "theme-switcher" ''
|
||||
#!${pkgs.python3}/bin/python3
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
# Execute the theme switcher Python script
|
||||
result = subprocess.run([
|
||||
"${./theme_switcher.py}",
|
||||
"--wallpaper-path", "${cfg.wallpaperPath}",
|
||||
"--light-time", "${cfg.lightTime}",
|
||||
"--dark-time", "${cfg.darkTime}",
|
||||
"--backend", "${cfg.backend}",
|
||||
"--rotation-interval", "${cfg.rotation.interval}",
|
||||
"--wpaperd-mode", "${cfg.wpaperd.mode}",
|
||||
"--sorting", "${cfg.rotation.sorting}",
|
||||
"--apply-themes-script", "${applyThemesScript}"
|
||||
])
|
||||
sys.exit(result.returncode)
|
||||
'';
|
||||
|
||||
# Python script to apply GTK/Kvantum/Helix themes
|
||||
applyThemesScript = ./apply_themes.py;
|
||||
|
||||
# Python script for wallpaper rotation
|
||||
wallpaperRotationScript = pkgs.writeScript "wallpaper-rotation" ''
|
||||
#!${pkgs.python3}/bin/python3
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
# Execute the wallpaper rotation Python script
|
||||
result = subprocess.run([
|
||||
"${./wallpaper_rotation.py}",
|
||||
"--light-time", "${cfg.lightTime}",
|
||||
"--dark-time", "${cfg.darkTime}",
|
||||
"--backend", "${cfg.backend}",
|
||||
"--apply-themes-script", "${applyThemesScript}"
|
||||
])
|
||||
sys.exit(result.returncode)
|
||||
'';
|
||||
in
|
||||
{
|
||||
options.services.themeSwitcher = {
|
||||
enable = lib.mkEnableOption "dynamic theme switching based on time and wallpapers with integrated wallpaper management";
|
||||
|
||||
user = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
description = "Username for theme switching";
|
||||
};
|
||||
|
||||
wallpaperPath = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = "${homeDir}/nixos/shared/modules/services/wallpapers";
|
||||
defaultText = lib.literalExpression ''"''${config.home-manager.users.''${cfg.user}.home.homeDirectory}/nixos/shared/modules/services/wallpapers"'';
|
||||
description = "Path to wallpaper directories (Light/ and Dark/ subdirectories expected)";
|
||||
};
|
||||
|
||||
backend = lib.mkOption {
|
||||
type = lib.types.enum [ "haishoku" "modern_colorthief" "fast_colorthief" "colorthief" "colorz" "wal" ];
|
||||
default = "haishoku";
|
||||
description = "Pywal color extraction backend";
|
||||
};
|
||||
|
||||
lightTime = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = "09:00:00";
|
||||
description = "Time to switch to light theme (HH:MM:SS)";
|
||||
};
|
||||
|
||||
darkTime = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = "16:30:00";
|
||||
description = "Time to switch to dark theme (HH:MM:SS)";
|
||||
};
|
||||
|
||||
enableThemeSwitch = lib.mkOption {
|
||||
type = lib.types.bool;
|
||||
default = true;
|
||||
description = "Enable automatic time-based theme switching via systemd timer";
|
||||
};
|
||||
|
||||
paletteGeneration = lib.mkOption {
|
||||
type = lib.types.enum [ "light-dark" "continuous" ];
|
||||
default = "continuous";
|
||||
description = ''
|
||||
When to regenerate color palettes from wallpapers:
|
||||
- light-dark: Only regenerate at light/dark switch times (6 AM/6 PM)
|
||||
- continuous: Regenerate palette every time wallpaper rotates
|
||||
'';
|
||||
};
|
||||
|
||||
# Wallpaper rotation options
|
||||
rotation = {
|
||||
interval = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = "5m";
|
||||
example = "30s";
|
||||
description = "How often to rotate wallpapers (e.g., 30s, 5m, 1h)";
|
||||
};
|
||||
|
||||
sorting = lib.mkOption {
|
||||
type = lib.types.enum [ "random" ];
|
||||
default = "random";
|
||||
description = "Wallpaper selection method (currently only random is supported)";
|
||||
};
|
||||
};
|
||||
|
||||
# Wpaperd integration options
|
||||
wpaperd = {
|
||||
enable = lib.mkOption {
|
||||
type = lib.types.bool;
|
||||
default = true;
|
||||
description = "Enable wpaperd wallpaper daemon integration";
|
||||
};
|
||||
|
||||
mode = lib.mkOption {
|
||||
type = lib.types.enum [ "center" "fit" "fit-border-color" "stretch" "tile" ];
|
||||
default = "center";
|
||||
description = "How to display wallpapers when size differs from display resolution";
|
||||
};
|
||||
|
||||
transition = {
|
||||
effect = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = "fade";
|
||||
example = "simple";
|
||||
description = "Transition effect name (fade, simple, etc.)";
|
||||
};
|
||||
|
||||
duration = lib.mkOption {
|
||||
type = lib.types.int;
|
||||
default = 300;
|
||||
example = 2000;
|
||||
description = "Transition duration in milliseconds";
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
config = lib.mkIf cfg.enable {
|
||||
home-manager.users.${cfg.user} = {
|
||||
home.packages = with pkgs; [
|
||||
pywal16WithBackends # pywal16 with haishoku backend included
|
||||
imagemagick # Useful for image manipulation
|
||||
wpaperd # Wallpaper daemon and CLI tool
|
||||
];
|
||||
|
||||
# Install manual theme switching script to user's local bin
|
||||
home.file.".local/bin/switch-theme.sh" = {
|
||||
source = pkgs.writeShellScript "switch-theme.sh" ''
|
||||
# Manual theme switching script
|
||||
# Usage: switch-theme.sh [light|dark]
|
||||
|
||||
if [ $# -ne 1 ]; then
|
||||
echo "Usage: $0 [light|dark]"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
MODE="$1"
|
||||
|
||||
if [ "$MODE" != "light" ] && [ "$MODE" != "dark" ]; then
|
||||
echo "Error: Mode must be 'light' or 'dark'"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Trigger the theme switcher service which will handle everything
|
||||
${pkgs.systemd}/bin/systemctl --user start theme-switcher.service
|
||||
'';
|
||||
executable = true;
|
||||
};
|
||||
|
||||
# Pywal custom templates for GTK, Kvantum, and Helix
|
||||
xdg.configFile."wal/templates/gtk-3.0.css".source = ./templates/gtk-3.0.css;
|
||||
xdg.configFile."wal/templates/gtk-4.0.css".source = ./templates/gtk-4.0.css;
|
||||
xdg.configFile."wal/templates/PywalTheme.kvconfig".source = ./templates/PywalTheme.kvconfig;
|
||||
xdg.configFile."wal/templates/helix_minimal.toml".source = ./templates/helix_minimal.toml;
|
||||
xdg.configFile."wal/templates/helix_semantic.toml".source = ./templates/helix_semantic.toml;
|
||||
|
||||
# Start wpaperd daemon if enabled
|
||||
# Note: We don't use services.wpaperd from home-manager because we need to
|
||||
# dynamically update the config file, which would be read-only if managed by home-manager
|
||||
systemd.user.services.wpaperd = lib.mkIf cfg.wpaperd.enable {
|
||||
Unit = {
|
||||
Description = "Wallpaper daemon for Wayland";
|
||||
After = [ "graphical-session.target" ];
|
||||
PartOf = [ "graphical-session.target" ];
|
||||
};
|
||||
Service = {
|
||||
Type = "simple";
|
||||
# Create initial config before starting if it doesn't exist
|
||||
ExecStartPre = pkgs.writeShellScript "wpaperd-init.sh" ''
|
||||
mkdir -p ~/.config/wpaperd
|
||||
if [ ! -f ~/.config/wpaperd/config.toml ] || [ -L ~/.config/wpaperd/config.toml ]; then
|
||||
# Remove old symlink if exists and create initial config
|
||||
rm -f ~/.config/wpaperd/config.toml
|
||||
|
||||
# Determine initial directory based on current time
|
||||
hour=$(${pkgs.coreutils}/bin/date +%H)
|
||||
light_hour=$(echo "${cfg.lightTime}" | ${pkgs.coreutils}/bin/cut -d: -f1)
|
||||
dark_hour=$(echo "${cfg.darkTime}" | ${pkgs.coreutils}/bin/cut -d: -f1)
|
||||
|
||||
if [ "$hour" -ge "$light_hour" ] && [ "$hour" -lt "$dark_hour" ]; then
|
||||
WALLPAPER_DIR="${cfg.wallpaperPath}/Light"
|
||||
else
|
||||
WALLPAPER_DIR="${cfg.wallpaperPath}/Dark"
|
||||
fi
|
||||
|
||||
cat > ~/.config/wpaperd/config.toml << EOF
|
||||
[default]
|
||||
path = "$WALLPAPER_DIR"
|
||||
duration = "${cfg.rotation.interval}"
|
||||
mode = "${cfg.wpaperd.mode}"
|
||||
sorting = "${cfg.rotation.sorting}"
|
||||
EOF
|
||||
fi
|
||||
'';
|
||||
ExecStart = "${pkgs.wpaperd}/bin/wpaperd";
|
||||
Restart = "on-failure";
|
||||
};
|
||||
Install.WantedBy = [ "graphical-session.target" ];
|
||||
};
|
||||
|
||||
# Systemd timer and service for theme switching at light/dark times
|
||||
systemd.user.timers.theme-switcher = lib.mkIf cfg.enableThemeSwitch {
|
||||
Unit.Description = "Dynamic Theme Switcher Timer";
|
||||
Timer = {
|
||||
OnCalendar = [ "*-*-* ${cfg.lightTime}" "*-*-* ${cfg.darkTime}" ];
|
||||
Persistent = true;
|
||||
};
|
||||
Install.WantedBy = [ "timers.target" ];
|
||||
};
|
||||
|
||||
systemd.user.services.theme-switcher = lib.mkIf cfg.enableThemeSwitch {
|
||||
Unit.Description = "Dynamic Theme Switcher Service";
|
||||
Service = {
|
||||
Type = "oneshot";
|
||||
ExecStart = "${themeSwitcherScript}";
|
||||
# Use home-manager profile to get pywal with all backends
|
||||
Environment = [
|
||||
"PATH=${config.home-manager.users.${cfg.user}.home.profileDirectory}/bin:${lib.makeBinPath (with pkgs; [
|
||||
python3
|
||||
systemd
|
||||
coreutils
|
||||
findutils
|
||||
glib
|
||||
libsForQt5.qtstyleplugin-kvantum
|
||||
bash
|
||||
wpaperd
|
||||
procps # for pgrep
|
||||
])}"
|
||||
];
|
||||
};
|
||||
};
|
||||
|
||||
# Wallpaper rotation timer and service (for continuous palette generation)
|
||||
systemd.user.timers.wallpaper-rotation = lib.mkIf (cfg.paletteGeneration == "continuous") {
|
||||
Unit.Description = "Wallpaper Rotation Timer (with Theme Regeneration)";
|
||||
Timer = {
|
||||
OnBootSec = "1min"; # Start 1 minute after boot
|
||||
OnUnitActiveSec = cfg.rotation.interval;
|
||||
Persistent = false;
|
||||
};
|
||||
Install.WantedBy = [ "timers.target" ];
|
||||
};
|
||||
|
||||
systemd.user.services.wallpaper-rotation = lib.mkIf (cfg.paletteGeneration == "continuous") {
|
||||
Unit.Description = "Wallpaper Rotation Service (with Theme Regeneration)";
|
||||
Service = {
|
||||
Type = "oneshot";
|
||||
ExecStart = "${wallpaperRotationScript}";
|
||||
# Use home-manager profile to get pywal with all backends
|
||||
Environment = [
|
||||
"PATH=${config.home-manager.users.${cfg.user}.home.profileDirectory}/bin:${lib.makeBinPath (with pkgs; [
|
||||
python3
|
||||
systemd
|
||||
coreutils
|
||||
findutils
|
||||
glib
|
||||
libsForQt5.qtstyleplugin-kvantum
|
||||
bash
|
||||
wpaperd
|
||||
procps # for pgrep
|
||||
])}"
|
||||
];
|
||||
};
|
||||
};
|
||||
|
||||
# Keep Qt/GTK enabled but allow pywal to override themes
|
||||
qt = {
|
||||
enable = true;
|
||||
platformTheme.name = "kvantum";
|
||||
style.name = "kvantum";
|
||||
};
|
||||
|
||||
gtk.enable = true;
|
||||
};
|
||||
};
|
||||
}
|
||||
250
shared/modules/services/theme_switcher/helix_theme_tester.py
Executable file
250
shared/modules/services/theme_switcher/helix_theme_tester.py
Executable file
@ -0,0 +1,250 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Simple Helix Theme Tester - Generate Helix theme from image for testing.
|
||||
Usage:
|
||||
./helix_theme_tester.py test image.jpg
|
||||
./helix_theme_tester.py test image.jpg --light
|
||||
./helix_theme_tester.py test image.jpg --output custom_name.toml
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
import json
|
||||
# Import from existing color_mapper
|
||||
from color_mapper import create_semantic_mapping, apply_semantic_mapping
|
||||
def generate_palette_from_image(image_path: Path, is_light: bool = False) -> bool:
|
||||
"""Run pywal on image to generate colors.json"""
|
||||
|
||||
cmd = ['wal', '-i', str(image_path), '--backend', 'haishoku', '-n']
|
||||
if is_light:
|
||||
cmd.append('-l')
|
||||
|
||||
try:
|
||||
subprocess.run(cmd, check=True, timeout=30)
|
||||
print(f"✓ Generated palette from {image_path.name}")
|
||||
return True
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(f"✗ Failed to generate palette: {e}")
|
||||
return False
|
||||
except FileNotFoundError:
|
||||
print("✗ Error: 'wal' command not found. Is pywal16 installed?")
|
||||
return False
|
||||
def generate_helix_theme(output_name: str = "pywal_test") -> bool:
|
||||
"""Generate Helix theme using color_mapper logic"""
|
||||
|
||||
colors_json = Path.home() / ".cache/wal/colors.json"
|
||||
if not colors_json.exists():
|
||||
print(f"✗ colors.json not found at {colors_json}")
|
||||
return False
|
||||
|
||||
# Use existing color_mapper to create semantic mapping
|
||||
print("✓ Analyzing colors and creating semantic mapping...")
|
||||
mapping = create_semantic_mapping(colors_json)
|
||||
|
||||
# Print the mapping for visibility
|
||||
print("\nSemantic color mapping:")
|
||||
for role, color in mapping["semantic_mapping"].items():
|
||||
props = mapping["color_analysis"][color]
|
||||
print(f" {role:12} -> {color} ({props['category']:8} {props['hex']})")
|
||||
|
||||
# Check for contrast issues
|
||||
print("\nContrast analysis:")
|
||||
bg_lightness = mapping.get("color_analysis", {}).get("color0", {}).get("lightness", 50)
|
||||
for role, color_key in mapping["semantic_mapping"].items():
|
||||
color_props = mapping["color_analysis"][color_key]
|
||||
contrast = abs(color_props["lightness"] - bg_lightness)
|
||||
status = "✓" if contrast > 30 else "⚠"
|
||||
print(f" {status} {role:12}: contrast {contrast:.1f}")
|
||||
|
||||
# Apply to helix template
|
||||
helix_template = Path(__file__).parent / "templates/helix_semantic.toml"
|
||||
helix_output = Path.home() / f".config/helix/themes/{output_name}.toml"
|
||||
helix_output.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
apply_semantic_mapping(helix_template, helix_output, mapping)
|
||||
print(f"\n✓ Helix theme created: {helix_output}")
|
||||
print(f"\nTest in Helix:")
|
||||
print(f" hx some_file.py")
|
||||
print(f" :theme {output_name}")
|
||||
|
||||
return True
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description='Generate Helix theme from image for testing'
|
||||
)
|
||||
parser.add_argument('command', choices=['test'], help='Command to run')
|
||||
parser.add_argument('image', type=Path, help='Path to image file')
|
||||
parser.add_argument('--light', action='store_true', help='Generate light theme')
|
||||
parser.add_argument('--output', default='pywal_test', help='Output theme name')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if not args.image.exists():
|
||||
print(f"✗ Image not found: {args.image}")
|
||||
return 1
|
||||
|
||||
print(f"Testing with image: {args.image}")
|
||||
print(f"Mode: {'light' if args.light else 'dark'}\n")
|
||||
|
||||
# Step 1: Generate palette
|
||||
if not generate_palette_from_image(args.image, args.light):
|
||||
return 1
|
||||
|
||||
# Step 2: Generate Helix theme
|
||||
if not generate_helix_theme(args.output):
|
||||
return 1
|
||||
|
||||
print("\n✓ Done! Now test the theme in Helix and iterate.")
|
||||
return 0
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main())
|
||||
Enhanced color_mapper.py with Tweaking
|
||||
Let me show you what minimal enhancements to add for tweaking:
|
||||
# Add to color_mapper.py after existing imports
|
||||
def adjust_color_lightness(hex_color: str, factor: float) -> str:
|
||||
"""
|
||||
Adjust color lightness by factor.
|
||||
factor > 1.0 = lighter, factor < 1.0 = darker
|
||||
"""
|
||||
rgb = hex_to_rgb(hex_color)
|
||||
h, s, l = rgb_to_hsl(rgb)
|
||||
|
||||
# Adjust lightness
|
||||
l = max(0, min(100, l * factor))
|
||||
|
||||
# Convert back to RGB then hex
|
||||
r, g, b = colorsys.hls_to_rgb(h/360, l/100, s/100)
|
||||
return f"#{int(r*255):02x}{int(g*255):02x}{int(b*255):02x}"
|
||||
def adjust_color_saturation(hex_color: str, factor: float) -> str:
|
||||
"""
|
||||
Adjust color saturation by factor.
|
||||
factor > 1.0 = more saturated, factor < 1.0 = less saturated
|
||||
"""
|
||||
rgb = hex_to_rgb(hex_color)
|
||||
h, s, l = rgb_to_hsl(rgb)
|
||||
|
||||
# Adjust saturation
|
||||
s = max(0, min(100, s * factor))
|
||||
|
||||
# Convert back
|
||||
r, g, b = colorsys.hls_to_rgb(h/360, l/100, s/100)
|
||||
return f"#{int(r*255):02x}{int(g*255):02x}{int(b*255):02x}"
|
||||
def calculate_contrast_ratio(color1_hex: str, color2_hex: str) -> float:
|
||||
"""Calculate WCAG contrast ratio between two colors"""
|
||||
def relative_luminance(hex_color):
|
||||
rgb = hex_to_rgb(hex_color)
|
||||
r, g, b = [x / 255.0 for x in rgb]
|
||||
|
||||
# Apply gamma correction
|
||||
r = r / 12.92 if r <= 0.03928 else ((r + 0.055) / 1.055) ** 2.4
|
||||
g = g / 12.92 if g <= 0.03928 else ((g + 0.055) / 1.055) ** 2.4
|
||||
b = b / 12.92 if b <= 0.03928 else ((b + 0.055) / 1.055) ** 2.4
|
||||
|
||||
return 0.2126 * r + 0.7152 * g + 0.0722 * b
|
||||
|
||||
l1 = relative_luminance(color1_hex)
|
||||
l2 = relative_luminance(color2_hex)
|
||||
|
||||
lighter = max(l1, l2)
|
||||
darker = min(l1, l2)
|
||||
|
||||
return (lighter + 0.05) / (darker + 0.05)
|
||||
def enforce_minimum_contrast(fg_hex: str, bg_hex: str, min_ratio: float = 4.5) -> str:
|
||||
"""
|
||||
Adjust foreground color to meet minimum contrast ratio with background.
|
||||
Preserves hue and saturation, only adjusts lightness.
|
||||
"""
|
||||
current_ratio = calculate_contrast_ratio(fg_hex, bg_hex)
|
||||
|
||||
if current_ratio >= min_ratio:
|
||||
return fg_hex # Already meets requirement
|
||||
|
||||
# Determine if we need to lighten or darken
|
||||
bg_rgb = hex_to_rgb(bg_hex)
|
||||
bg_h, bg_s, bg_l = rgb_to_hsl(bg_rgb)
|
||||
|
||||
fg_rgb = hex_to_rgb(fg_hex)
|
||||
fg_h, fg_s, fg_l = rgb_to_hsl(fg_rgb)
|
||||
|
||||
# If background is dark, lighten foreground; if light, darken foreground
|
||||
step = 5 if bg_l < 50 else -5
|
||||
|
||||
# Iteratively adjust lightness
|
||||
attempts = 0
|
||||
while attempts < 20: # Prevent infinite loop
|
||||
fg_l += step
|
||||
fg_l = max(0, min(100, fg_l))
|
||||
|
||||
# Convert back to hex
|
||||
r, g, b = colorsys.hls_to_rgb(fg_h/360, fg_l/100, fg_s/100)
|
||||
adjusted_hex = f"#{int(r*255):02x}{int(g*255):02x}{int(b*255):02x}"
|
||||
|
||||
if calculate_contrast_ratio(adjusted_hex, bg_hex) >= min_ratio:
|
||||
return adjusted_hex
|
||||
|
||||
attempts += 1
|
||||
|
||||
# If we couldn't meet contrast, return best effort
|
||||
return adjusted_hex
|
||||
# Add parameters to apply_semantic_mapping
|
||||
def apply_semantic_mapping(template_path: Path, output_path: Path, mapping: Dict,
|
||||
adjustments: Dict = None):
|
||||
"""
|
||||
Apply semantic mapping to Helix template.
|
||||
|
||||
adjustments: Optional dict with tweaking parameters:
|
||||
{
|
||||
'comment': {'contrast': 4.5, 'saturation': 1.2},
|
||||
'error': {'saturation': 1.3, 'force_hue': (0, 30)},
|
||||
}
|
||||
"""
|
||||
|
||||
with open(template_path) as f:
|
||||
template = f.read()
|
||||
|
||||
# Read pywal colors
|
||||
colors_json = Path.home() / ".cache/wal/colors.json"
|
||||
with open(colors_json) as f:
|
||||
colors_data = json.load(f)
|
||||
|
||||
# Get background for contrast calculations
|
||||
bg_color = colors_data["special"]["background"]
|
||||
|
||||
# Replace standard pywal colors first
|
||||
output = template
|
||||
for key, value in colors_data["special"].items():
|
||||
output = output.replace(f"{{{key}}}", value)
|
||||
for key, value in colors_data["colors"].items():
|
||||
output = output.replace(f"{{{key}}}", value)
|
||||
|
||||
# Now apply semantic colors with optional adjustments
|
||||
semantic = mapping["semantic_mapping"]
|
||||
|
||||
for role in ["error", "warning", "comment", "string", "keyword", "number", "info"]:
|
||||
color_key = semantic[role]
|
||||
color_hex = colors_data["colors"][color_key]
|
||||
|
||||
# Apply adjustments if provided
|
||||
if adjustments and role in adjustments:
|
||||
adj = adjustments[role]
|
||||
|
||||
# Apply saturation adjustment
|
||||
if 'saturation' in adj:
|
||||
color_hex = adjust_color_saturation(color_hex, adj['saturation'])
|
||||
|
||||
# Apply lightness adjustment
|
||||
if 'lightness' in adj:
|
||||
color_hex = adjust_color_lightness(color_hex, adj['lightness'])
|
||||
|
||||
# Enforce minimum contrast
|
||||
if 'contrast' in adj:
|
||||
color_hex = enforce_minimum_contrast(color_hex, bg_color, adj['contrast'])
|
||||
|
||||
# Replace in template
|
||||
placeholder = f"{{{role.upper()}_COLOR}}"
|
||||
output = output.replace(placeholder, color_hex)
|
||||
|
||||
with open(output_path, 'w') as f:
|
||||
f.write(output)
|
||||
@ -0,0 +1,360 @@
|
||||
[%General]
|
||||
author=Pywal Theme Generator
|
||||
comment=A Kvantum theme generated from wallpaper colors
|
||||
x11drag=menubar_and_primary_toolbar
|
||||
alt_mnemonic=true
|
||||
left_tabs=false
|
||||
attach_active_tab=false
|
||||
mirror_doc_tabs=true
|
||||
group_toolbar_buttons=false
|
||||
spread_progressbar=true
|
||||
composite=true
|
||||
menu_shadow_depth=6
|
||||
tooltip_shadow_depth=4
|
||||
scroll_width=12
|
||||
scroll_arrows=false
|
||||
scroll_min_extent=40
|
||||
slider_width=6
|
||||
slider_handle_width=20
|
||||
slider_handle_length=20
|
||||
center_toolbar_handle=true
|
||||
check_size=16
|
||||
textless_progressbar=false
|
||||
progressbar_thickness=4
|
||||
menubar_mouse_tracking=true
|
||||
toolbutton_style=0
|
||||
double_click=false
|
||||
translucent_windows=false
|
||||
blurring=false
|
||||
popup_blurring=false
|
||||
opaque=kaffeine,kmplayer,subtitlecomposer,kdenlive,vlc,smplayer,smplayer2,avidemux,avidemux2_qt4,avidemux3_qt4,avidemux3_qt5,kamoso,QtCreator,VirtualBox,VirtualBoxVM,trojita,dragon,digikam,lyx,calligra,ink
|
||||
vertical_spin_indicators=false
|
||||
spin_button_width=16
|
||||
fill_rubberband=false
|
||||
merge_menubar_with_toolbar=true
|
||||
small_icon_size=16
|
||||
large_icon_size=32
|
||||
button_icon_size=16
|
||||
toolbar_icon_size=22
|
||||
combo_as_lineedit=true
|
||||
hide_combo_checkboxes=true
|
||||
combo_menu=true
|
||||
hide_spin_checkboxes=true
|
||||
button_contents_shift=false
|
||||
groupbox_top_label=true
|
||||
inline_spin_indicators=true
|
||||
joined_inactive_tabs=false
|
||||
|
||||
[GeneralColors]
|
||||
window.color={background}
|
||||
base.color={background}
|
||||
alt.base.color={color0}
|
||||
button.color={color0}
|
||||
light.color={color8}
|
||||
mid.light.color={color8}
|
||||
dark.color={color0}
|
||||
mid.color={color8}
|
||||
highlight.color={color4}
|
||||
inactive.highlight.color={color8}
|
||||
text.color={foreground}
|
||||
window.text.color={foreground}
|
||||
button.text.color={foreground}
|
||||
disabled.text.color={color8}
|
||||
tooltip.base.color={color0}
|
||||
tooltip.text.color={foreground}
|
||||
highlight.text.color={background}
|
||||
link.color={color4}
|
||||
link.visited.color={color5}
|
||||
progress.indicator.text.color={background}
|
||||
|
||||
[Hacks]
|
||||
transparent_ktitle_label=true
|
||||
transparent_dolphin_view=true
|
||||
transparent_pcmanfm_sidepane=true
|
||||
blur_translucent=false
|
||||
transparent_menutitle=true
|
||||
respect_darkness=true
|
||||
kcapacitybar_as_progressbar=true
|
||||
force_size_grip=true
|
||||
iconless_pushbutton=false
|
||||
iconless_menu=false
|
||||
disabled_icon_opacity=70
|
||||
lxqtmainmenu_iconsize=22
|
||||
normal_default_pushbutton=true
|
||||
single_top_toolbar=true
|
||||
tint_on_mouseover=0
|
||||
transparent_pcmanfm_view=false
|
||||
no_selection_tint=false
|
||||
|
||||
[PanelButtonCommand]
|
||||
frame=true
|
||||
frame.element=button
|
||||
frame.top=4
|
||||
frame.bottom=4
|
||||
frame.left=4
|
||||
frame.right=4
|
||||
interior=true
|
||||
interior.element=button
|
||||
indicator.size=10
|
||||
text.normal.color={foreground}
|
||||
text.focus.color={background}
|
||||
text.press.color={background}
|
||||
text.toggle.color={background}
|
||||
text.shadow=0
|
||||
text.margin=1
|
||||
text.iconspacing=4
|
||||
indicator.element=arrow
|
||||
text.margin.top=2
|
||||
text.margin.bottom=2
|
||||
text.margin.left=2
|
||||
text.margin.right=2
|
||||
min_width=+0.3font
|
||||
min_height=+0.3font
|
||||
frame.expansion=0
|
||||
|
||||
[PanelButtonTool]
|
||||
inherits=PanelButtonCommand
|
||||
|
||||
[Toolbar]
|
||||
inherits=PanelButtonCommand
|
||||
interior.element=none
|
||||
frame.element=none
|
||||
text.normal.color={foreground}
|
||||
text.focus.color={foreground}
|
||||
|
||||
[ToolbarButton]
|
||||
inherits=PanelButtonCommand
|
||||
text.normal.color={foreground}
|
||||
text.focus.color={background}
|
||||
text.press.color={background}
|
||||
|
||||
[DockTitle]
|
||||
inherits=PanelButtonCommand
|
||||
frame=false
|
||||
interior=false
|
||||
text.normal.color={foreground}
|
||||
text.focus.color={background}
|
||||
|
||||
[IndicatorSpinBox]
|
||||
inherits=PanelButtonCommand
|
||||
indicator.element=arrow
|
||||
indicator.size=10
|
||||
|
||||
[RadioButton]
|
||||
inherits=PanelButtonCommand
|
||||
frame=false
|
||||
interior.element=radio
|
||||
text.normal.color={foreground}
|
||||
text.focus.color={foreground}
|
||||
|
||||
[CheckBox]
|
||||
inherits=PanelButtonCommand
|
||||
frame=false
|
||||
interior.element=checkbox
|
||||
text.normal.color={foreground}
|
||||
text.focus.color={foreground}
|
||||
|
||||
[GenericFrame]
|
||||
inherits=PanelButtonCommand
|
||||
frame=true
|
||||
interior=false
|
||||
frame.element=common
|
||||
interior.element=common
|
||||
frame.top=1
|
||||
frame.bottom=1
|
||||
frame.left=1
|
||||
frame.right=1
|
||||
|
||||
[LineEdit]
|
||||
inherits=PanelButtonCommand
|
||||
frame.element=lineedit
|
||||
interior.element=lineedit
|
||||
text.normal.color={foreground}
|
||||
text.focus.color={foreground}
|
||||
|
||||
[DropDownButton]
|
||||
inherits=PanelButtonCommand
|
||||
indicator.element=arrow-down
|
||||
|
||||
[IndicatorArrow]
|
||||
indicator.element=arrow
|
||||
indicator.size=10
|
||||
|
||||
[ToolboxTab]
|
||||
inherits=PanelButtonCommand
|
||||
text.normal.color={foreground}
|
||||
text.press.color={background}
|
||||
text.focus.color={background}
|
||||
|
||||
[Tab]
|
||||
inherits=PanelButtonCommand
|
||||
interior.element=tab
|
||||
text.normal.color={foreground}
|
||||
text.focus.color={background}
|
||||
text.press.color={background}
|
||||
frame.element=tab
|
||||
indicator.element=tab
|
||||
indicator.size=10
|
||||
frame.top=4
|
||||
frame.bottom=4
|
||||
frame.left=4
|
||||
frame.right=4
|
||||
|
||||
[TabFrame]
|
||||
inherits=PanelButtonCommand
|
||||
frame.element=tabframe
|
||||
interior.element=tabframe
|
||||
frame.top=2
|
||||
frame.bottom=2
|
||||
frame.left=2
|
||||
frame.right=2
|
||||
|
||||
[TreeExpander]
|
||||
inherits=PanelButtonCommand
|
||||
indicator.size=10
|
||||
indicator.element=tree
|
||||
|
||||
[HeaderSection]
|
||||
inherits=PanelButtonCommand
|
||||
interior.element=header
|
||||
frame.element=header
|
||||
text.normal.color={foreground}
|
||||
text.focus.color={background}
|
||||
text.press.color={background}
|
||||
text.toggle.color={background}
|
||||
frame.top=1
|
||||
frame.bottom=1
|
||||
frame.left=1
|
||||
frame.right=1
|
||||
|
||||
[SizeGrip]
|
||||
indicator.element=resize-grip
|
||||
|
||||
[Scrollbar]
|
||||
inherits=PanelButtonCommand
|
||||
indicator.element=arrow
|
||||
indicator.size=10
|
||||
|
||||
[ScrollbarSlider]
|
||||
inherits=PanelButtonCommand
|
||||
frame.element=scrollbarslider
|
||||
interior=false
|
||||
frame.left=6
|
||||
frame.right=6
|
||||
frame.top=6
|
||||
frame.bottom=6
|
||||
indicator.element=grip
|
||||
indicator.size=13
|
||||
|
||||
[ScrollbarGroove]
|
||||
inherits=PanelButtonCommand
|
||||
interior=false
|
||||
frame=false
|
||||
|
||||
[Progressbar]
|
||||
inherits=PanelButtonCommand
|
||||
frame.element=progress
|
||||
interior.element=progress
|
||||
text.normal.color={foreground}
|
||||
text.focus.color={background}
|
||||
text.press.color={background}
|
||||
text.toggle.color={background}
|
||||
text.bold=false
|
||||
frame.expansion=8
|
||||
|
||||
[ProgressbarContents]
|
||||
inherits=PanelButtonCommand
|
||||
frame=true
|
||||
frame.element=progress-pattern
|
||||
interior.element=progress-pattern
|
||||
|
||||
[ItemView]
|
||||
inherits=PanelButtonCommand
|
||||
text.normal.color={foreground}
|
||||
text.focus.color={background}
|
||||
text.press.color={background}
|
||||
text.toggle.color={background}
|
||||
frame.element=itemview
|
||||
interior.element=itemview
|
||||
frame.top=2
|
||||
frame.bottom=2
|
||||
frame.left=2
|
||||
frame.right=2
|
||||
|
||||
[Splitter]
|
||||
indicator.size=48
|
||||
|
||||
[Menu]
|
||||
inherits=PanelButtonCommand
|
||||
frame.top=3
|
||||
frame.bottom=3
|
||||
frame.left=3
|
||||
frame.right=3
|
||||
frame.element=menu
|
||||
interior.element=menu
|
||||
text.normal.color={foreground}
|
||||
text.focus.color={background}
|
||||
text.press.color={background}
|
||||
|
||||
[MenuItem]
|
||||
inherits=PanelButtonCommand
|
||||
frame=true
|
||||
frame.element=menuitem
|
||||
interior.element=menuitem
|
||||
indicator.element=menuitem
|
||||
text.normal.color={foreground}
|
||||
text.focus.color={background}
|
||||
text.press.color={background}
|
||||
text.toggle.color={background}
|
||||
frame.top=3
|
||||
frame.bottom=3
|
||||
frame.left=3
|
||||
frame.right=3
|
||||
|
||||
[MenuBar]
|
||||
inherits=PanelButtonCommand
|
||||
frame.element=menubar
|
||||
interior.element=menubar
|
||||
frame.bottom=0
|
||||
|
||||
[MenuBarItem]
|
||||
inherits=PanelButtonCommand
|
||||
interior=true
|
||||
interior.element=menubaritem
|
||||
frame.element=menubaritem
|
||||
text.normal.color={foreground}
|
||||
text.focus.color={background}
|
||||
text.press.color={background}
|
||||
frame.top=2
|
||||
frame.bottom=2
|
||||
frame.left=2
|
||||
frame.right=2
|
||||
|
||||
[TitleBar]
|
||||
inherits=PanelButtonCommand
|
||||
frame=false
|
||||
interior.element=titlebar
|
||||
indicator.size=16
|
||||
indicator.element=mdi
|
||||
text.normal.color={foreground}
|
||||
text.focus.color={background}
|
||||
text.bold=true
|
||||
|
||||
[ComboBox]
|
||||
inherits=PanelButtonCommand
|
||||
interior.element=combo
|
||||
frame.element=combo
|
||||
indicator.element=carrow
|
||||
|
||||
[ToolTip]
|
||||
inherits=GenericFrame
|
||||
frame.top=3
|
||||
frame.bottom=3
|
||||
frame.left=3
|
||||
frame.right=3
|
||||
interior=true
|
||||
text.shadow=0
|
||||
text.margin=0
|
||||
interior.element=tooltip
|
||||
frame.element=tooltip
|
||||
frame.expansion=0
|
||||
255
shared/modules/services/theme_switcher/templates/gtk-3.0.css
Normal file
255
shared/modules/services/theme_switcher/templates/gtk-3.0.css
Normal file
@ -0,0 +1,255 @@
|
||||
/* GTK 3.0 Theme - Generated by pywal */
|
||||
/* This theme uses colors extracted from wallpapers */
|
||||
|
||||
/* Define color variables */
|
||||
@define-color theme_bg_color {background};
|
||||
@define-color theme_fg_color {foreground};
|
||||
@define-color theme_base_color {background};
|
||||
@define-color theme_text_color {foreground};
|
||||
@define-color theme_selected_bg_color {color4};
|
||||
@define-color theme_selected_fg_color {background};
|
||||
@define-color insensitive_bg_color shade({background}, 0.95);
|
||||
@define-color insensitive_fg_color shade({foreground}, 0.7);
|
||||
@define-color insensitive_base_color {background};
|
||||
@define-color theme_unfocused_bg_color {background};
|
||||
@define-color theme_unfocused_fg_color {foreground};
|
||||
@define-color theme_unfocused_base_color {background};
|
||||
@define-color theme_unfocused_text_color {foreground};
|
||||
@define-color theme_unfocused_selected_bg_color {color4};
|
||||
@define-color theme_unfocused_selected_fg_color {background};
|
||||
@define-color borders shade({background}, 0.8);
|
||||
@define-color unfocused_borders shade({background}, 0.85);
|
||||
|
||||
/* Additional accent colors */
|
||||
@define-color accent_bg_color {color4};
|
||||
@define-color accent_fg_color {background};
|
||||
@define-color accent_color {color4};
|
||||
@define-color destructive_bg_color {color1};
|
||||
@define-color destructive_fg_color {background};
|
||||
@define-color destructive_color {color1};
|
||||
@define-color success_bg_color {color2};
|
||||
@define-color success_fg_color {background};
|
||||
@define-color success_color {color2};
|
||||
@define-color warning_bg_color {color3};
|
||||
@define-color warning_fg_color {background};
|
||||
@define-color warning_color {color3};
|
||||
@define-color error_bg_color {color1};
|
||||
@define-color error_fg_color {background};
|
||||
@define-color error_color {color1};
|
||||
|
||||
/* Window styling */
|
||||
window {
|
||||
background-color: {background};
|
||||
color: {foreground};
|
||||
}
|
||||
|
||||
/* Widget styling */
|
||||
* {{
|
||||
background-color: {background};
|
||||
color: {foreground};
|
||||
}}
|
||||
|
||||
button {
|
||||
background-color: {color0};
|
||||
color: {foreground};
|
||||
border: 1px solid {color8};
|
||||
border-radius: 4px;
|
||||
padding: 6px 12px;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background-color: {color8};
|
||||
border-color: {color4};
|
||||
}
|
||||
|
||||
button:active,
|
||||
button:checked {
|
||||
background-color: {color4};
|
||||
color: {background};
|
||||
}
|
||||
|
||||
button:disabled {
|
||||
background-color: shade({background}, 0.95);
|
||||
color: shade({foreground}, 0.7);
|
||||
}
|
||||
|
||||
/* Entry/Input fields */
|
||||
entry {
|
||||
background-color: {color0};
|
||||
color: {foreground};
|
||||
border: 1px solid {color8};
|
||||
border-radius: 4px;
|
||||
padding: 6px;
|
||||
}
|
||||
|
||||
entry:focus {
|
||||
border-color: {color4};
|
||||
}
|
||||
|
||||
entry:disabled {
|
||||
background-color: shade({background}, 0.95);
|
||||
color: shade({foreground}, 0.7);
|
||||
}
|
||||
|
||||
/* Selections */
|
||||
selection,
|
||||
*:selected {
|
||||
background-color: {color4};
|
||||
color: {background};
|
||||
}
|
||||
|
||||
/* Headerbar */
|
||||
headerbar {
|
||||
background-color: {color0};
|
||||
color: {foreground};
|
||||
border-bottom: 1px solid {color8};
|
||||
}
|
||||
|
||||
headerbar button {
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
}
|
||||
|
||||
headerbar button:hover {
|
||||
background-color: {color8};
|
||||
}
|
||||
|
||||
/* Sidebar */
|
||||
.sidebar {
|
||||
background-color: {color0};
|
||||
border-right: 1px solid {color8};
|
||||
}
|
||||
|
||||
/* Notebook/Tabs */
|
||||
notebook > header {
|
||||
background-color: {color0};
|
||||
border-bottom: 1px solid {color8};
|
||||
}
|
||||
|
||||
notebook > header > tabs > tab {
|
||||
background-color: transparent;
|
||||
color: {foreground};
|
||||
padding: 6px 12px;
|
||||
}
|
||||
|
||||
notebook > header > tabs > tab:checked {
|
||||
background-color: {background};
|
||||
border-bottom: 2px solid {color4};
|
||||
}
|
||||
|
||||
/* Menu */
|
||||
menu,
|
||||
.menu {
|
||||
background-color: {color0};
|
||||
color: {foreground};
|
||||
border: 1px solid {color8};
|
||||
}
|
||||
|
||||
menuitem {
|
||||
padding: 6px 12px;
|
||||
}
|
||||
|
||||
menuitem:hover {
|
||||
background-color: {color4};
|
||||
color: {background};
|
||||
}
|
||||
|
||||
/* Scrollbar */
|
||||
scrollbar {
|
||||
background-color: {background};
|
||||
}
|
||||
|
||||
scrollbar slider {
|
||||
background-color: {color8};
|
||||
border-radius: 8px;
|
||||
min-width: 8px;
|
||||
min-height: 8px;
|
||||
}
|
||||
|
||||
scrollbar slider:hover {
|
||||
background-color: {color4};
|
||||
}
|
||||
|
||||
/* Progressbar */
|
||||
progressbar {
|
||||
background-color: {color0};
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
progressbar progress {
|
||||
background-color: {color4};
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* Switch */
|
||||
switch {
|
||||
background-color: {color0};
|
||||
border: 1px solid {color8};
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
switch:checked {
|
||||
background-color: {color4};
|
||||
}
|
||||
|
||||
switch slider {
|
||||
background-color: {foreground};
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
/* Checkbutton & Radiobutton */
|
||||
checkbutton check,
|
||||
radiobutton radio {
|
||||
background-color: {color0};
|
||||
border: 1px solid {color8};
|
||||
}
|
||||
|
||||
checkbutton check:checked,
|
||||
radiobutton radio:checked {
|
||||
background-color: {color4};
|
||||
border-color: {color4};
|
||||
}
|
||||
|
||||
/* Tooltip */
|
||||
tooltip {
|
||||
background-color: {color0};
|
||||
color: {foreground};
|
||||
border: 1px solid {color8};
|
||||
border-radius: 4px;
|
||||
padding: 6px;
|
||||
}
|
||||
|
||||
/* Popover */
|
||||
popover {
|
||||
background-color: {color0};
|
||||
color: {foreground};
|
||||
border: 1px solid {color8};
|
||||
border-radius: 8px;
|
||||
padding: 6px;
|
||||
}
|
||||
|
||||
/* Frame */
|
||||
frame {
|
||||
border: 1px solid {color8};
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* List */
|
||||
list,
|
||||
.view {
|
||||
background-color: {background};
|
||||
color: {foreground};
|
||||
}
|
||||
|
||||
list row {
|
||||
padding: 6px;
|
||||
}
|
||||
|
||||
list row:hover {
|
||||
background-color: {color8};
|
||||
}
|
||||
|
||||
list row:selected {
|
||||
background-color: {color4};
|
||||
color: {background};
|
||||
}
|
||||
397
shared/modules/services/theme_switcher/templates/gtk-4.0.css
Normal file
397
shared/modules/services/theme_switcher/templates/gtk-4.0.css
Normal file
@ -0,0 +1,397 @@
|
||||
/* GTK 4.0 Theme - Generated by pywal */
|
||||
/* This theme uses colors extracted from wallpapers */
|
||||
|
||||
/* Define color variables */
|
||||
@define-color window_bg_color {background};
|
||||
@define-color window_fg_color {foreground};
|
||||
@define-color view_bg_color {background};
|
||||
@define-color view_fg_color {foreground};
|
||||
@define-color accent_bg_color {color4};
|
||||
@define-color accent_fg_color {background};
|
||||
@define-color accent_color {color4};
|
||||
@define-color destructive_bg_color {color1};
|
||||
@define-color destructive_fg_color {background};
|
||||
@define-color destructive_color {color1};
|
||||
@define-color success_bg_color {color2};
|
||||
@define-color success_fg_color {background};
|
||||
@define-color success_color {color2};
|
||||
@define-color warning_bg_color {color3};
|
||||
@define-color warning_fg_color {background};
|
||||
@define-color warning_color {color3};
|
||||
@define-color error_bg_color {color1};
|
||||
@define-color error_fg_color {background};
|
||||
@define-color error_color {color1};
|
||||
@define-color headerbar_bg_color {color0};
|
||||
@define-color headerbar_fg_color {foreground};
|
||||
@define-color headerbar_border_color {color8};
|
||||
@define-color sidebar_bg_color {color0};
|
||||
@define-color sidebar_fg_color {foreground};
|
||||
@define-color card_bg_color {color0};
|
||||
@define-color card_fg_color {foreground};
|
||||
@define-color popover_bg_color {color0};
|
||||
@define-color popover_fg_color {foreground};
|
||||
@define-color dialog_bg_color {background};
|
||||
@define-color dialog_fg_color {foreground};
|
||||
|
||||
/* Legacy GTK3 compatibility colors */
|
||||
@define-color theme_bg_color {background};
|
||||
@define-color theme_fg_color {foreground};
|
||||
@define-color theme_base_color {background};
|
||||
@define-color theme_text_color {foreground};
|
||||
@define-color theme_selected_bg_color {color4};
|
||||
@define-color theme_selected_fg_color {background};
|
||||
@define-color insensitive_bg_color alpha({foreground}, 0.1);
|
||||
@define-color insensitive_fg_color alpha({foreground}, 0.5);
|
||||
@define-color borders alpha({foreground}, 0.2);
|
||||
|
||||
/* Window styling */
|
||||
window {
|
||||
background-color: @window_bg_color;
|
||||
color: @window_fg_color;
|
||||
}
|
||||
|
||||
/* Widget styling */
|
||||
* {
|
||||
background-color: @window_bg_color;
|
||||
color: @window_fg_color;
|
||||
}
|
||||
|
||||
/* Button */
|
||||
button {
|
||||
background-color: @card_bg_color;
|
||||
color: @window_fg_color;
|
||||
border: 1px solid @borders;
|
||||
border-radius: 6px;
|
||||
padding: 6px 12px;
|
||||
min-height: 24px;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background-color: alpha(@accent_bg_color, 0.1);
|
||||
border-color: @accent_color;
|
||||
}
|
||||
|
||||
button:active {
|
||||
background-color: @accent_bg_color;
|
||||
color: @accent_fg_color;
|
||||
}
|
||||
|
||||
button:disabled {
|
||||
background-color: @insensitive_bg_color;
|
||||
color: @insensitive_fg_color;
|
||||
}
|
||||
|
||||
button.suggested-action {
|
||||
background-color: @accent_bg_color;
|
||||
color: @accent_fg_color;
|
||||
}
|
||||
|
||||
button.destructive-action {
|
||||
background-color: @destructive_bg_color;
|
||||
color: @destructive_fg_color;
|
||||
}
|
||||
|
||||
/* Entry/Input fields */
|
||||
entry {
|
||||
background-color: @view_bg_color;
|
||||
color: @view_fg_color;
|
||||
border: 1px solid @borders;
|
||||
border-radius: 6px;
|
||||
padding: 6px;
|
||||
min-height: 32px;
|
||||
}
|
||||
|
||||
entry:focus {
|
||||
border-color: @accent_color;
|
||||
outline: 2px solid alpha(@accent_color, 0.3);
|
||||
outline-offset: -1px;
|
||||
}
|
||||
|
||||
entry:disabled {
|
||||
background-color: @insensitive_bg_color;
|
||||
color: @insensitive_fg_color;
|
||||
}
|
||||
|
||||
/* Selections */
|
||||
selection,
|
||||
*:selected {
|
||||
background-color: @accent_bg_color;
|
||||
color: @accent_fg_color;
|
||||
}
|
||||
|
||||
/* Headerbar */
|
||||
headerbar {
|
||||
background-color: @headerbar_bg_color;
|
||||
color: @headerbar_fg_color;
|
||||
border-bottom: 1px solid @headerbar_border_color;
|
||||
min-height: 46px;
|
||||
padding: 0 6px;
|
||||
}
|
||||
|
||||
headerbar button {
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
}
|
||||
|
||||
headerbar button:hover {
|
||||
background-color: alpha(@accent_bg_color, 0.1);
|
||||
}
|
||||
|
||||
headerbar button:active {
|
||||
background-color: alpha(@accent_bg_color, 0.2);
|
||||
}
|
||||
|
||||
/* Sidebar */
|
||||
.sidebar {
|
||||
background-color: @sidebar_bg_color;
|
||||
border-right: 1px solid @borders;
|
||||
}
|
||||
|
||||
.sidebar row {
|
||||
padding: 6px;
|
||||
}
|
||||
|
||||
.sidebar row:hover {
|
||||
background-color: alpha(@accent_bg_color, 0.1);
|
||||
}
|
||||
|
||||
.sidebar row:selected {
|
||||
background-color: @accent_bg_color;
|
||||
color: @accent_fg_color;
|
||||
}
|
||||
|
||||
/* Notebook/Tabs */
|
||||
notebook > header {
|
||||
background-color: @card_bg_color;
|
||||
border-bottom: 1px solid @borders;
|
||||
}
|
||||
|
||||
notebook > header > tabs > tab {
|
||||
background-color: transparent;
|
||||
color: @window_fg_color;
|
||||
padding: 6px 12px;
|
||||
min-height: 32px;
|
||||
}
|
||||
|
||||
notebook > header > tabs > tab:hover {
|
||||
background-color: alpha(@accent_bg_color, 0.1);
|
||||
}
|
||||
|
||||
notebook > header > tabs > tab:checked {
|
||||
background-color: @window_bg_color;
|
||||
border-bottom: 2px solid @accent_color;
|
||||
}
|
||||
|
||||
/* Menu */
|
||||
menubar {
|
||||
background-color: @headerbar_bg_color;
|
||||
color: @headerbar_fg_color;
|
||||
}
|
||||
|
||||
menu,
|
||||
.menu {
|
||||
background-color: @popover_bg_color;
|
||||
color: @popover_fg_color;
|
||||
border: 1px solid @borders;
|
||||
border-radius: 8px;
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
menuitem {
|
||||
padding: 6px 12px;
|
||||
border-radius: 4px;
|
||||
min-height: 28px;
|
||||
}
|
||||
|
||||
menuitem:hover {
|
||||
background-color: @accent_bg_color;
|
||||
color: @accent_fg_color;
|
||||
}
|
||||
|
||||
/* Scrollbar */
|
||||
scrollbar {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
scrollbar slider {
|
||||
background-color: alpha(@window_fg_color, 0.3);
|
||||
border-radius: 8px;
|
||||
min-width: 10px;
|
||||
min-height: 10px;
|
||||
}
|
||||
|
||||
scrollbar slider:hover {
|
||||
background-color: alpha(@window_fg_color, 0.5);
|
||||
}
|
||||
|
||||
scrollbar slider:active {
|
||||
background-color: @accent_color;
|
||||
}
|
||||
|
||||
/* Progressbar */
|
||||
progressbar {
|
||||
background-color: @view_bg_color;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
progressbar progress {
|
||||
background-color: @accent_bg_color;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
progressbar trough {
|
||||
background-color: alpha(@window_fg_color, 0.1);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* Switch */
|
||||
switch {
|
||||
background-color: alpha(@window_fg_color, 0.2);
|
||||
border-radius: 12px;
|
||||
min-width: 48px;
|
||||
min-height: 24px;
|
||||
}
|
||||
|
||||
switch:checked {
|
||||
background-color: @accent_bg_color;
|
||||
}
|
||||
|
||||
switch slider {
|
||||
background-color: @window_fg_color;
|
||||
border-radius: 50%;
|
||||
min-width: 20px;
|
||||
min-height: 20px;
|
||||
margin: 2px;
|
||||
}
|
||||
|
||||
/* Checkbutton & Radiobutton */
|
||||
checkbutton check,
|
||||
radiobutton radio {
|
||||
background-color: @view_bg_color;
|
||||
border: 1px solid @borders;
|
||||
min-width: 16px;
|
||||
min-height: 16px;
|
||||
}
|
||||
|
||||
checkbutton check {
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
radiobutton radio {
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
checkbutton check:checked,
|
||||
radiobutton radio:checked {
|
||||
background-color: @accent_bg_color;
|
||||
border-color: @accent_bg_color;
|
||||
color: @accent_fg_color;
|
||||
}
|
||||
|
||||
/* Tooltip */
|
||||
tooltip {
|
||||
background-color: @popover_bg_color;
|
||||
color: @popover_fg_color;
|
||||
border: 1px solid @borders;
|
||||
border-radius: 6px;
|
||||
padding: 6px 8px;
|
||||
}
|
||||
|
||||
tooltip label {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* Popover */
|
||||
popover {
|
||||
background-color: @popover_bg_color;
|
||||
color: @popover_fg_color;
|
||||
border: 1px solid @borders;
|
||||
border-radius: 12px;
|
||||
padding: 6px;
|
||||
}
|
||||
|
||||
popover > contents {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
/* Frame */
|
||||
frame {
|
||||
border: 1px solid @borders;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
/* List */
|
||||
list,
|
||||
listview,
|
||||
.view {
|
||||
background-color: @view_bg_color;
|
||||
color: @view_fg_color;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
list row,
|
||||
listview row {
|
||||
padding: 6px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
list row:hover,
|
||||
listview row:hover {
|
||||
background-color: alpha(@accent_bg_color, 0.1);
|
||||
}
|
||||
|
||||
list row:selected,
|
||||
listview row:selected {
|
||||
background-color: @accent_bg_color;
|
||||
color: @accent_fg_color;
|
||||
}
|
||||
|
||||
/* Card */
|
||||
.card {
|
||||
background-color: @card_bg_color;
|
||||
color: @card_fg_color;
|
||||
border: 1px solid @borders;
|
||||
border-radius: 12px;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
/* Dialog */
|
||||
dialog {
|
||||
background-color: @dialog_bg_color;
|
||||
color: @dialog_fg_color;
|
||||
}
|
||||
|
||||
/* Searchbar */
|
||||
searchbar {
|
||||
background-color: @headerbar_bg_color;
|
||||
border-bottom: 1px solid @borders;
|
||||
}
|
||||
|
||||
/* Toolbar */
|
||||
toolbar {
|
||||
background-color: @headerbar_bg_color;
|
||||
padding: 4px;
|
||||
border-bottom: 1px solid @borders;
|
||||
}
|
||||
|
||||
/* Infobar */
|
||||
.info {
|
||||
background-color: @accent_bg_color;
|
||||
color: @accent_fg_color;
|
||||
}
|
||||
|
||||
.warning {
|
||||
background-color: @warning_bg_color;
|
||||
color: @warning_fg_color;
|
||||
}
|
||||
|
||||
.error {
|
||||
background-color: @error_bg_color;
|
||||
color: @error_fg_color;
|
||||
}
|
||||
|
||||
.success,
|
||||
.question {
|
||||
background-color: @success_bg_color;
|
||||
color: @success_fg_color;
|
||||
}
|
||||
@ -0,0 +1,153 @@
|
||||
# Pywal Minimal - Judicious Syntax Highlighting
|
||||
# Uses only 4 colors for syntax
|
||||
# Functions and variables remain plain text for reduced visual noise
|
||||
# Colors dynamically generated from wallpaper via pywal
|
||||
|
||||
"ui.background" = {{ bg = "base" }}
|
||||
"ui.virtual" = {{ fg = "surface0" }}
|
||||
"ui.virtual.ruler" = {{ bg = "surface0" }}
|
||||
"ui.virtual.indent-guide" = {{ fg = "surface0" }}
|
||||
"ui.virtual.inlay-hint" = {{ fg = "overlay1", bg = "mantle", modifiers = ["italic"] }}
|
||||
"ui.virtual.jump-label" = {{ fg = "red", modifiers = ["bold"] }}
|
||||
|
||||
"ui.selection" = {{ fg = "text", bg = "surface1" }}
|
||||
"ui.selection.primary" = {{ fg = "text", bg = "surface2" }}
|
||||
|
||||
"ui.cursor" = {{ fg = "base", bg = "cursor" }}
|
||||
"ui.cursor.primary" = {{ fg = "base", bg = "cursor" }}
|
||||
"ui.cursor.match" = {{ fg = "color1", modifiers = ["bold"] }}
|
||||
"ui.cursorline.primary" = {{ bg = "surface0" }}
|
||||
"ui.cursorcolumn.primary" = {{ bg = "surface0" }}
|
||||
|
||||
"ui.linenr" = {{ fg = "surface1" }}
|
||||
"ui.linenr.selected" = {{ fg = "cursor", modifiers = ["bold"] }}
|
||||
|
||||
"ui.statusline" = {{ fg = "text", bg = "mantle" }}
|
||||
"ui.statusline.inactive" = {{ fg = "overlay0", bg = "mantle" }}
|
||||
"ui.statusline.normal" = {{ fg = "base", bg = "cursor", modifiers = ["bold"] }}
|
||||
"ui.statusline.insert" = {{ fg = "base", bg = "color2", modifiers = ["bold"] }}
|
||||
"ui.statusline.select" = {{ fg = "base", bg = "color5", modifiers = ["bold"] }}
|
||||
|
||||
"ui.bufferline" = {{ fg = "overlay0", bg = "mantle" }}
|
||||
"ui.bufferline.active" = {{ fg = "cursor", bg = "base", modifiers = ["bold"] }}
|
||||
|
||||
"ui.help" = {{ fg = "text", bg = "surface0" }}
|
||||
"ui.text" = "text"
|
||||
"ui.text.focus" = {{ fg = "text", bg = "surface0" }}
|
||||
"ui.text.inactive" = "overlay1"
|
||||
|
||||
"ui.menu" = {{ fg = "text", bg = "surface0" }}
|
||||
"ui.menu.selected" = {{ fg = "text", bg = "surface1", modifiers = ["bold"] }}
|
||||
"ui.menu.scroll" = {{ fg = "overlay0", bg = "surface0" }}
|
||||
|
||||
"ui.popup" = {{ fg = "text", bg = "surface0" }}
|
||||
"ui.window" = {{ fg = "base" }}
|
||||
|
||||
"diagnostic.error" = {{ underline = {{ color = "color1", style = "curl" }} }}
|
||||
"diagnostic.warning" = {{ underline = {{ color = "color3", style = "curl" }} }}
|
||||
"diagnostic.info" = {{ underline = {{ color = "color6", style = "curl" }} }}
|
||||
"diagnostic.hint" = {{ underline = {{ color = "color4", style = "curl" }} }}
|
||||
"diagnostic.unnecessary" = {{ modifiers = ["dim"] }}
|
||||
"diagnostic.deprecated" = {{ modifiers = ["crossed_out"] }}
|
||||
|
||||
"error" = "color1"
|
||||
"warning" = "color3"
|
||||
"info" = "color6"
|
||||
"hint" = "color4"
|
||||
|
||||
"diff.plus" = "color2"
|
||||
"diff.minus" = "color1"
|
||||
"diff.delta" = "color3"
|
||||
|
||||
"markup.heading" = {{ fg = "cursor", modifiers = ["bold"] }}
|
||||
"markup.list" = "color5"
|
||||
"markup.bold" = {{ modifiers = ["bold"] }}
|
||||
"markup.italic" = {{ modifiers = ["italic"] }}
|
||||
"markup.strikethrough" = {{ modifiers = ["crossed_out"] }}
|
||||
"markup.link.url" = {{ fg = "color4", modifiers = ["underlined"] }}
|
||||
"markup.link.text" = "color5"
|
||||
"markup.quote" = "color2"
|
||||
"markup.raw" = "color2"
|
||||
|
||||
# Minimal syntax highlighting - only 4 colors used
|
||||
"comment" = "color3" # Comments pop (yellow/bright color)
|
||||
|
||||
"keyword" = {{ fg = "color5", modifiers = ["italic"] }} # Keywords highlighted and italic (magenta)
|
||||
"keyword.control" = {{ fg = "color5", modifiers = ["italic"] }}
|
||||
"keyword.directive" = {{ fg = "color5", modifiers = ["italic"] }}
|
||||
"keyword.function" = {{ fg = "color5", modifiers = ["italic"] }}
|
||||
"keyword.operator" = {{ fg = "color5", modifiers = ["italic"] }}
|
||||
"keyword.return" = {{ fg = "color5", modifiers = ["italic"] }}
|
||||
"keyword.storage" = {{ fg = "color5", modifiers = ["italic"] }}
|
||||
|
||||
"string" = "color2" # Strings highlighted (green)
|
||||
"string.regexp" = "color1"
|
||||
"string.special" = "color2"
|
||||
|
||||
"constant.numeric" = "color1" # Numbers highlighted (orange/red)
|
||||
"constant.builtin" = "color1"
|
||||
"constant.character.escape" = "color1"
|
||||
|
||||
# Everything else remains plain text
|
||||
"function" = "text" # Functions are plain text
|
||||
"function.builtin" = "text"
|
||||
"function.method" = "text"
|
||||
"function.macro" = "text"
|
||||
|
||||
"variable" = "text" # Variables are plain text
|
||||
"variable.builtin" = "text"
|
||||
"variable.parameter" = "text"
|
||||
"variable.other.member" = "text"
|
||||
|
||||
"type" = {{ fg = "cursor", modifiers = ["italic"]}} # Types are slightly highlighted
|
||||
"type.builtin" = "cursor"
|
||||
|
||||
"constructor" = "text" # Constructors are plain text
|
||||
|
||||
"attribute" = "text"
|
||||
"label" = "text"
|
||||
"namespace" = "text"
|
||||
"tag" = "text"
|
||||
|
||||
# Top-level declarations get accent color
|
||||
"function.definition" = {{ fg = "color7", modifiers = ["bold"] }}
|
||||
"type.definition" = {{ fg = "color7", modifiers = ["bold"] }}
|
||||
|
||||
# Punctuation is slightly dimmed but still readable
|
||||
"punctuation" = "subtext0"
|
||||
"punctuation.bracket" = "subtext0"
|
||||
"punctuation.delimiter" = "subtext0"
|
||||
"punctuation.special" = "subtext0"
|
||||
|
||||
"operator" = "subtext0" # Operators slightly dimmed
|
||||
|
||||
[palette]
|
||||
# Pywal generated colors - background to foreground gradient
|
||||
base = "{background}"
|
||||
mantle = "{color0}"
|
||||
crust = "{color0}"
|
||||
|
||||
surface0 = "{color8}"
|
||||
surface1 = "{color8}"
|
||||
surface2 = "{color7}"
|
||||
|
||||
overlay0 = "{color8}"
|
||||
overlay1 = "{color7}"
|
||||
overlay2 = "{color7}"
|
||||
|
||||
subtext0 = "{color7}"
|
||||
subtext1 = "{color15}"
|
||||
text = "{foreground}"
|
||||
|
||||
# Core colors from pywal palette
|
||||
red = "{color1}"
|
||||
color1 = "{color1}" # Red/Orange - numbers, errors
|
||||
color2 = "{color2}" # Green - strings
|
||||
color3 = "{color3}" # Yellow - comments
|
||||
color4 = "{color4}" # Blue - info
|
||||
color5 = "{color5}" # Magenta - keywords
|
||||
color6 = "{color6}" # Cyan - hints
|
||||
color7 = "{color7}" # Light gray - definitions
|
||||
|
||||
color15 = "{color15}" # Bright white
|
||||
cursor = "{cursor}" # Cursor color
|
||||
@ -0,0 +1,164 @@
|
||||
# Pywal Semantic - Intelligent color mapping based on semantic meaning
|
||||
# Colors are automatically mapped based on hue analysis:
|
||||
# - Reds/oranges for errors and warnings
|
||||
# - High contrast colors for comments
|
||||
# - Appropriate semantic colors for syntax elements
|
||||
# Colors dynamically generated from wallpaper via pywal with semantic mapping
|
||||
|
||||
"ui.background" = {{ bg = "base" }}
|
||||
"ui.virtual" = {{ fg = "surface0" }}
|
||||
"ui.virtual.ruler" = {{ bg = "surface0" }}
|
||||
"ui.virtual.indent-guide" = {{ fg = "surface0" }}
|
||||
"ui.virtual.inlay-hint" = {{ fg = "overlay1", bg = "mantle", modifiers = ["italic"] }}
|
||||
"ui.virtual.jump-label" = {{ fg = "error_color", modifiers = ["bold"] }}
|
||||
|
||||
"ui.selection" = {{ fg = "text", bg = "surface1" }}
|
||||
"ui.selection.primary" = {{ fg = "text", bg = "surface2" }}
|
||||
|
||||
"ui.cursor" = {{ fg = "base", bg = "cursor" }}
|
||||
"ui.cursor.primary" = {{ fg = "base", bg = "cursor" }}
|
||||
"ui.cursor.match" = {{ fg = "keyword_color", modifiers = ["bold"] }}
|
||||
"ui.cursorline.primary" = {{ bg = "surface0" }}
|
||||
"ui.cursorcolumn.primary" = {{ bg = "surface0" }}
|
||||
|
||||
"ui.linenr" = {{ fg = "surface1" }}
|
||||
"ui.linenr.selected" = {{ fg = "cursor", modifiers = ["bold"] }}
|
||||
|
||||
"ui.statusline" = {{ fg = "text", bg = "mantle" }}
|
||||
"ui.statusline.inactive" = {{ fg = "overlay0", bg = "mantle" }}
|
||||
"ui.statusline.normal" = {{ fg = "base", bg = "cursor", modifiers = ["bold"] }}
|
||||
"ui.statusline.insert" = {{ fg = "base", bg = "string_color", modifiers = ["bold"] }}
|
||||
"ui.statusline.select" = {{ fg = "base", bg = "keyword_color", modifiers = ["bold"] }}
|
||||
|
||||
"ui.bufferline" = {{ fg = "overlay0", bg = "mantle" }}
|
||||
"ui.bufferline.active" = {{ fg = "cursor", bg = "base", modifiers = ["bold"] }}
|
||||
|
||||
"ui.help" = {{ fg = "text", bg = "surface0" }}
|
||||
"ui.text" = "text"
|
||||
"ui.text.focus" = {{ fg = "text", bg = "surface0" }}
|
||||
"ui.text.inactive" = "overlay1"
|
||||
|
||||
"ui.menu" = {{ fg = "text", bg = "surface0" }}
|
||||
"ui.menu.selected" = {{ fg = "text", bg = "surface1", modifiers = ["bold"] }}
|
||||
"ui.menu.scroll" = {{ fg = "overlay0", bg = "surface0" }}
|
||||
|
||||
"ui.popup" = {{ fg = "text", bg = "surface0" }}
|
||||
"ui.window" = {{ fg = "base" }}
|
||||
|
||||
"diagnostic.error" = {{ underline = {{ color = "error_color", style = "curl" }} }}
|
||||
"diagnostic.warning" = {{ underline = {{ color = "warning_color", style = "curl" }} }}
|
||||
"diagnostic.info" = {{ underline = {{ color = "info_color", style = "curl" }} }}
|
||||
"diagnostic.hint" = {{ underline = {{ color = "info_color", style = "curl" }} }}
|
||||
"diagnostic.unnecessary" = {{ modifiers = ["dim"] }}
|
||||
"diagnostic.deprecated" = {{ modifiers = ["crossed_out"] }}
|
||||
|
||||
"error" = "error_color"
|
||||
"warning" = "warning_color"
|
||||
"info" = "info_color"
|
||||
"hint" = "info_color"
|
||||
|
||||
"diff.plus" = "string_color"
|
||||
"diff.minus" = "error_color"
|
||||
"diff.delta" = "warning_color"
|
||||
|
||||
"markup.heading" = {{ fg = "cursor", modifiers = ["bold"] }}
|
||||
"markup.list" = "keyword_color"
|
||||
"markup.bold" = {{ modifiers = ["bold"] }}
|
||||
"markup.italic" = {{ modifiers = ["italic"] }}
|
||||
"markup.strikethrough" = {{ modifiers = ["crossed_out"] }}
|
||||
"markup.link.url" = {{ fg = "info_color", modifiers = ["underlined"] }}
|
||||
"markup.link.text" = "keyword_color"
|
||||
"markup.quote" = "string_color"
|
||||
"markup.raw" = "string_color"
|
||||
|
||||
# Semantic syntax highlighting
|
||||
"comment" = "comment_color" # Highest contrast color for visibility
|
||||
|
||||
"keyword" = {{ fg = "keyword_color", modifiers = ["italic"] }}
|
||||
"keyword.control" = {{ fg = "keyword_color", modifiers = ["italic"] }}
|
||||
"keyword.directive" = {{ fg = "keyword_color", modifiers = ["italic"] }}
|
||||
"keyword.function" = {{ fg = "keyword_color", modifiers = ["italic"] }}
|
||||
"keyword.operator" = {{ fg = "keyword_color", modifiers = ["italic"] }}
|
||||
"keyword.return" = {{ fg = "keyword_color", modifiers = ["italic"] }}
|
||||
"keyword.storage" = {{ fg = "keyword_color", modifiers = ["italic"] }}
|
||||
|
||||
"string" = "string_color"
|
||||
"string.regexp" = "error_color"
|
||||
"string.special" = "string_color"
|
||||
|
||||
"constant.numeric" = "number_color"
|
||||
"constant.builtin" = "number_color"
|
||||
"constant.character.escape" = "number_color"
|
||||
|
||||
# Everything else remains plain text
|
||||
"function" = "text"
|
||||
"function.builtin" = "text"
|
||||
"function.method" = "text"
|
||||
"function.macro" = "text"
|
||||
|
||||
"variable" = "text"
|
||||
"variable.builtin" = "text"
|
||||
"variable.parameter" = "text"
|
||||
"variable.other.member" = "text"
|
||||
|
||||
"type" = {{ fg = "cursor", modifiers = ["italic"]}}
|
||||
"type.builtin" = "cursor"
|
||||
|
||||
"constructor" = "text"
|
||||
|
||||
"attribute" = "text"
|
||||
"label" = "text"
|
||||
"namespace" = "text"
|
||||
"tag" = "text"
|
||||
|
||||
# Top-level declarations get accent color
|
||||
"function.definition" = {{ fg = "color7", modifiers = ["bold"] }}
|
||||
"type.definition" = {{ fg = "color7", modifiers = ["bold"] }}
|
||||
|
||||
# Punctuation is slightly dimmed but still readable
|
||||
"punctuation" = "subtext0"
|
||||
"punctuation.bracket" = "subtext0"
|
||||
"punctuation.delimiter" = "subtext0"
|
||||
"punctuation.special" = "subtext0"
|
||||
|
||||
"operator" = "subtext0"
|
||||
|
||||
[palette]
|
||||
# Pywal generated colors - background to foreground gradient
|
||||
base = "{background}"
|
||||
mantle = "{color0}"
|
||||
crust = "{color0}"
|
||||
|
||||
surface0 = "{color8}"
|
||||
surface1 = "{color8}"
|
||||
surface2 = "{color7}"
|
||||
|
||||
overlay0 = "{color8}"
|
||||
overlay1 = "{color7}"
|
||||
overlay2 = "{color7}"
|
||||
|
||||
subtext0 = "{color7}"
|
||||
subtext1 = "{color15}"
|
||||
text = "{foreground}"
|
||||
|
||||
# Core colors from pywal palette
|
||||
red = "{color1}"
|
||||
color1 = "{color1}"
|
||||
color2 = "{color2}"
|
||||
color3 = "{color3}"
|
||||
color4 = "{color4}"
|
||||
color5 = "{color5}"
|
||||
color6 = "{color6}"
|
||||
color7 = "{color7}"
|
||||
|
||||
color15 = "{color15}"
|
||||
cursor = "{cursor}"
|
||||
|
||||
# Semantic colors (will be replaced by color_mapper.py)
|
||||
error_color = "{{ERROR_COLOR}}"
|
||||
warning_color = "{{WARNING_COLOR}}"
|
||||
comment_color = "{{COMMENT_COLOR}}"
|
||||
string_color = "{{STRING_COLOR}}"
|
||||
keyword_color = "{{KEYWORD_COLOR}}"
|
||||
number_color = "{{NUMBER_COLOR}}"
|
||||
info_color = "{{INFO_COLOR}}"
|
||||
297
shared/modules/services/theme_switcher/theme_switcher.py
Executable file
297
shared/modules/services/theme_switcher/theme_switcher.py
Executable file
@ -0,0 +1,297 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Theme Switcher Script - Time-based light/dark theme switching.
|
||||
|
||||
This script:
|
||||
1. Determines current theme mode (light/dark) based on time
|
||||
2. Updates wpaperd config to use appropriate wallpaper directory
|
||||
3. Restarts wpaperd to load new config
|
||||
4. Gets the selected wallpaper from wpaperd
|
||||
5. Generates pywal color palette from the wallpaper
|
||||
6. Applies GTK/Kvantum/Helix themes
|
||||
"""
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
import shutil
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
import logging
|
||||
import argparse
|
||||
|
||||
# Setup logging
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(levelname)s: %(message)s'
|
||||
)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ThemeSwitcher:
|
||||
"""Handles time-based light/dark theme switching."""
|
||||
|
||||
def __init__(self, wallpaper_path: str, light_time: str, dark_time: str,
|
||||
backend: str, rotation_interval: str, wpaperd_mode: str,
|
||||
sorting: str, apply_themes_script: str):
|
||||
self.wallpaper_path = Path(wallpaper_path)
|
||||
self.light_hour = int(light_time.split(':')[0])
|
||||
self.dark_hour = int(dark_time.split(':')[0])
|
||||
self.backend = backend
|
||||
self.rotation_interval = rotation_interval
|
||||
self.wpaperd_mode = wpaperd_mode
|
||||
self.sorting = sorting
|
||||
self.apply_themes_script = apply_themes_script
|
||||
self.home = Path.home()
|
||||
|
||||
# Find command paths
|
||||
self.systemctl = self._find_command('systemctl')
|
||||
self.wpaperctl = self._find_command('wpaperctl')
|
||||
self.wal = self._find_command('wal')
|
||||
|
||||
def _find_command(self, cmd: str) -> str:
|
||||
"""Find full path to a command."""
|
||||
path = shutil.which(cmd)
|
||||
if path is None:
|
||||
# Try common NixOS locations
|
||||
common_paths = [
|
||||
f'/run/current-system/sw/bin/{cmd}',
|
||||
f'/etc/profiles/per-user/{os.environ.get("USER", "")}/bin/{cmd}',
|
||||
f'{self.home}/.nix-profile/bin/{cmd}',
|
||||
]
|
||||
for p in common_paths:
|
||||
if Path(p).exists():
|
||||
return p
|
||||
raise FileNotFoundError(f"Command not found: {cmd}")
|
||||
return path
|
||||
|
||||
def is_wpaperd_running(self) -> bool:
|
||||
"""Check if wpaperd process is running."""
|
||||
try:
|
||||
pgrep = shutil.which('pgrep') or 'pgrep'
|
||||
result = subprocess.run(
|
||||
[pgrep, '-x', 'wpaperd'],
|
||||
capture_output=True,
|
||||
timeout=5
|
||||
)
|
||||
return result.returncode == 0
|
||||
except (subprocess.TimeoutExpired, FileNotFoundError):
|
||||
return False
|
||||
|
||||
def determine_mode(self) -> tuple[str, Path]:
|
||||
"""
|
||||
Determine current theme mode based on time.
|
||||
|
||||
Returns:
|
||||
Tuple of (mode_name, wallpaper_directory)
|
||||
"""
|
||||
current_hour = datetime.now().hour
|
||||
|
||||
if self.light_hour <= current_hour < self.dark_hour:
|
||||
mode = "light"
|
||||
wallpaper_dir = self.wallpaper_path / "Light"
|
||||
else:
|
||||
mode = "dark"
|
||||
wallpaper_dir = self.wallpaper_path / "Dark"
|
||||
|
||||
return mode, wallpaper_dir
|
||||
|
||||
def update_wpaperd_config(self, wallpaper_dir: Path) -> None:
|
||||
"""
|
||||
Update wpaperd configuration file.
|
||||
|
||||
Args:
|
||||
wallpaper_dir: Path to wallpaper directory (Light or Dark)
|
||||
"""
|
||||
config_dir = self.home / ".config/wpaperd"
|
||||
config_file = config_dir / "config.toml"
|
||||
|
||||
# Ensure config directory exists
|
||||
config_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Remove old config file
|
||||
if config_file.exists():
|
||||
config_file.unlink()
|
||||
|
||||
# Create new config
|
||||
config_content = f"""[default]
|
||||
path = "{wallpaper_dir}"
|
||||
duration = "{self.rotation_interval}"
|
||||
mode = "{self.wpaperd_mode}"
|
||||
sorting = "{self.sorting}"
|
||||
"""
|
||||
|
||||
config_file.write_text(config_content)
|
||||
logger.info(f"Updated wpaperd config: {config_file}")
|
||||
|
||||
def restart_wpaperd(self) -> None:
|
||||
"""Restart wpaperd systemd service."""
|
||||
try:
|
||||
subprocess.run(
|
||||
[self.systemctl, '--user', 'restart', 'wpaperd.service'],
|
||||
check=True,
|
||||
timeout=10
|
||||
)
|
||||
logger.info("Restarted wpaperd service")
|
||||
# Give wpaperd time to start and load wallpaper
|
||||
time.sleep(2)
|
||||
except subprocess.CalledProcessError as e:
|
||||
logger.error(f"Failed to restart wpaperd: {e}")
|
||||
raise
|
||||
except subprocess.TimeoutExpired:
|
||||
logger.error("Timeout restarting wpaperd")
|
||||
raise
|
||||
|
||||
def get_current_wallpaper(self) -> Path:
|
||||
"""
|
||||
Get the current wallpaper path from wpaperd.
|
||||
|
||||
Returns:
|
||||
Path to current wallpaper file
|
||||
|
||||
Raises:
|
||||
RuntimeError: If unable to get wallpaper path
|
||||
"""
|
||||
wallpaper_state_dir = self.home / ".local/state/wpaperd/wallpapers"
|
||||
|
||||
if not wallpaper_state_dir.exists():
|
||||
raise RuntimeError(f"Wpaperd state directory not found: {wallpaper_state_dir}")
|
||||
|
||||
# Get first monitor directory
|
||||
try:
|
||||
monitor_dirs = list(wallpaper_state_dir.iterdir())
|
||||
if not monitor_dirs:
|
||||
raise RuntimeError("No monitor directories found in wpaperd state")
|
||||
|
||||
monitor_name = monitor_dirs[0].name
|
||||
|
||||
# Query wpaperd for current wallpaper
|
||||
result = subprocess.run(
|
||||
[self.wpaperctl, 'get-wallpaper', monitor_name],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True,
|
||||
timeout=5
|
||||
)
|
||||
|
||||
wallpaper_path = result.stdout.strip()
|
||||
if not wallpaper_path:
|
||||
raise RuntimeError("wpaperctl returned empty wallpaper path")
|
||||
|
||||
return Path(wallpaper_path)
|
||||
|
||||
except (subprocess.CalledProcessError, subprocess.TimeoutExpired, IndexError) as e:
|
||||
raise RuntimeError(f"Failed to get current wallpaper: {e}")
|
||||
|
||||
def generate_palette(self, wallpaper: Path, mode: str) -> None:
|
||||
"""
|
||||
Generate pywal color palette from wallpaper.
|
||||
|
||||
Args:
|
||||
wallpaper: Path to wallpaper image
|
||||
mode: Theme mode ('light' or 'dark')
|
||||
"""
|
||||
cmd = [self.wal, '-i', str(wallpaper), '--backend', self.backend, '-n']
|
||||
|
||||
if mode == 'light':
|
||||
cmd.append('-l')
|
||||
|
||||
try:
|
||||
subprocess.run(cmd, check=True, timeout=30)
|
||||
logger.info(f"Generated {mode} palette from: {wallpaper}")
|
||||
except subprocess.CalledProcessError as e:
|
||||
logger.error(f"Failed to generate palette: {e}")
|
||||
raise
|
||||
except subprocess.TimeoutExpired:
|
||||
logger.error("Timeout while generating palette")
|
||||
raise
|
||||
|
||||
def apply_themes(self) -> None:
|
||||
"""Apply GTK and Kvantum themes using helper script."""
|
||||
try:
|
||||
subprocess.run(
|
||||
['python3', self.apply_themes_script],
|
||||
check=True,
|
||||
timeout=30
|
||||
)
|
||||
logger.info("Applied themes successfully")
|
||||
except subprocess.CalledProcessError as e:
|
||||
logger.error(f"Failed to apply themes: {e}")
|
||||
raise
|
||||
except subprocess.TimeoutExpired:
|
||||
logger.error("Timeout while applying themes")
|
||||
raise
|
||||
|
||||
def run(self) -> int:
|
||||
"""
|
||||
Execute the theme switching process.
|
||||
|
||||
Returns:
|
||||
Exit code (0 for success, 1 for failure)
|
||||
"""
|
||||
try:
|
||||
# Determine mode and wallpaper directory
|
||||
mode, wallpaper_dir = self.determine_mode()
|
||||
logger.info(f"Switching to {mode} theme with wallpapers from {wallpaper_dir}")
|
||||
|
||||
# Check if wpaperd is running
|
||||
if not self.is_wpaperd_running():
|
||||
logger.error("wpaperd is not running")
|
||||
return 1
|
||||
|
||||
# Update wpaperd config
|
||||
self.update_wpaperd_config(wallpaper_dir)
|
||||
|
||||
# Restart wpaperd
|
||||
self.restart_wpaperd()
|
||||
|
||||
# Get current wallpaper
|
||||
current_wallpaper = self.get_current_wallpaper()
|
||||
logger.info(f"Current wallpaper: {current_wallpaper}")
|
||||
|
||||
# Generate palette
|
||||
self.generate_palette(current_wallpaper, mode)
|
||||
|
||||
# Apply themes
|
||||
self.apply_themes()
|
||||
|
||||
logger.info(f"Theme switched to {mode} mode successfully!")
|
||||
return 0
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Theme switching failed: {e}", exc_info=True)
|
||||
return 1
|
||||
|
||||
|
||||
def main():
|
||||
"""Main entry point."""
|
||||
parser = argparse.ArgumentParser(description='Time-based theme switcher')
|
||||
parser.add_argument('--wallpaper-path', required=True, help='Path to wallpaper directories')
|
||||
parser.add_argument('--light-time', default='09:00:00', help='Light theme start time (HH:MM:SS)')
|
||||
parser.add_argument('--dark-time', default='16:30:00', help='Dark theme start time (HH:MM:SS)')
|
||||
parser.add_argument('--backend', default='haishoku', help='Pywal color extraction backend')
|
||||
parser.add_argument('--rotation-interval', default='5m', help='Wallpaper rotation interval')
|
||||
parser.add_argument('--wpaperd-mode', default='center', help='Wpaperd display mode')
|
||||
parser.add_argument('--sorting', default='random', help='Wallpaper sorting method')
|
||||
parser.add_argument('--apply-themes-script', required=True, help='Path to apply-themes script')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
switcher = ThemeSwitcher(
|
||||
wallpaper_path=args.wallpaper_path,
|
||||
light_time=args.light_time,
|
||||
dark_time=args.dark_time,
|
||||
backend=args.backend,
|
||||
rotation_interval=args.rotation_interval,
|
||||
wpaperd_mode=args.wpaperd_mode,
|
||||
sorting=args.sorting,
|
||||
apply_themes_script=args.apply_themes_script
|
||||
)
|
||||
|
||||
return switcher.run()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main())
|
||||
306
shared/modules/services/theme_switcher/wallpaper_rotation.py
Executable file
306
shared/modules/services/theme_switcher/wallpaper_rotation.py
Executable file
@ -0,0 +1,306 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Wallpaper Rotation Script - Handles continuous wallpaper rotation with palette regeneration.
|
||||
|
||||
This script:
|
||||
1. Checks if wpaperd is running
|
||||
2. Determines current theme mode (light/dark) for correct pywal flags
|
||||
3. Pauses wpaperd's auto-rotation
|
||||
4. Advances to next wallpaper
|
||||
5. Gets the new wallpaper path
|
||||
6. Regenerates pywal palette
|
||||
7. Applies themes
|
||||
8. Resumes wpaperd's auto-rotation
|
||||
"""
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
import shutil
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
import logging
|
||||
import argparse
|
||||
|
||||
# Setup logging
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(levelname)s: %(message)s'
|
||||
)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class WallpaperRotator:
|
||||
"""Handles wallpaper rotation with theme regeneration."""
|
||||
|
||||
def __init__(self, light_time: str, dark_time: str, backend: str,
|
||||
apply_themes_script: str):
|
||||
self.light_hour = int(light_time.split(':')[0])
|
||||
self.dark_hour = int(dark_time.split(':')[0])
|
||||
self.backend = backend
|
||||
self.apply_themes_script = apply_themes_script
|
||||
self.home = Path.home()
|
||||
|
||||
# Find command paths
|
||||
self.wpaperctl = self._find_command('wpaperctl')
|
||||
self.wal = self._find_command('wal')
|
||||
|
||||
def _find_command(self, cmd: str) -> str:
|
||||
"""Find full path to a command."""
|
||||
path = shutil.which(cmd)
|
||||
if path is None:
|
||||
# Try common NixOS locations
|
||||
common_paths = [
|
||||
f'/run/current-system/sw/bin/{cmd}',
|
||||
f'/etc/profiles/per-user/{os.environ.get("USER", "")}/bin/{cmd}',
|
||||
f'{self.home}/.nix-profile/bin/{cmd}',
|
||||
]
|
||||
for p in common_paths:
|
||||
if Path(p).exists():
|
||||
return p
|
||||
raise FileNotFoundError(f"Command not found: {cmd}")
|
||||
return path
|
||||
|
||||
def is_wpaperd_running(self) -> bool:
|
||||
"""Check if wpaperd process is running."""
|
||||
try:
|
||||
pgrep = shutil.which('pgrep') or 'pgrep'
|
||||
result = subprocess.run(
|
||||
[pgrep, '-x', 'wpaperd'],
|
||||
capture_output=True,
|
||||
timeout=5
|
||||
)
|
||||
return result.returncode == 0
|
||||
except (subprocess.TimeoutExpired, FileNotFoundError):
|
||||
return False
|
||||
|
||||
def determine_mode(self) -> tuple[str, list[str]]:
|
||||
"""
|
||||
Determine current theme mode and pywal flags based on time.
|
||||
|
||||
Returns:
|
||||
Tuple of (mode_name, pywal_flags_list)
|
||||
"""
|
||||
current_hour = datetime.now().hour
|
||||
|
||||
if self.light_hour <= current_hour < self.dark_hour:
|
||||
mode = "light"
|
||||
pywal_flags = ['-l', '-n']
|
||||
else:
|
||||
mode = "dark"
|
||||
pywal_flags = ['-n']
|
||||
|
||||
return mode, pywal_flags
|
||||
|
||||
def pause_wpaperd(self) -> None:
|
||||
"""Pause wpaperd's automatic wallpaper rotation."""
|
||||
try:
|
||||
subprocess.run(
|
||||
[self.wpaperctl, 'pause-wallpaper'],
|
||||
check=True,
|
||||
timeout=5,
|
||||
capture_output=True
|
||||
)
|
||||
logger.debug("Paused wpaperd rotation")
|
||||
except subprocess.CalledProcessError as e:
|
||||
logger.error(f"Failed to pause wpaperd: {e}")
|
||||
raise
|
||||
except subprocess.TimeoutExpired:
|
||||
logger.error("Timeout while pausing wpaperd")
|
||||
raise
|
||||
except FileNotFoundError:
|
||||
logger.error("wpaperctl command not found")
|
||||
raise
|
||||
|
||||
def resume_wpaperd(self) -> None:
|
||||
"""Resume wpaperd's automatic wallpaper rotation."""
|
||||
try:
|
||||
subprocess.run(
|
||||
[self.wpaperctl, 'resume-wallpaper'],
|
||||
check=True,
|
||||
timeout=5,
|
||||
capture_output=True
|
||||
)
|
||||
logger.debug("Resumed wpaperd rotation")
|
||||
except subprocess.CalledProcessError as e:
|
||||
logger.warning(f"Failed to resume wpaperd: {e}")
|
||||
except subprocess.TimeoutExpired:
|
||||
logger.warning("Timeout while resuming wpaperd")
|
||||
except FileNotFoundError:
|
||||
logger.warning("wpaperctl command not found")
|
||||
|
||||
def advance_wallpaper(self) -> None:
|
||||
"""Advance to next wallpaper using wpaperctl."""
|
||||
try:
|
||||
subprocess.run(
|
||||
[self.wpaperctl, 'next-wallpaper'],
|
||||
check=True,
|
||||
timeout=5,
|
||||
capture_output=True
|
||||
)
|
||||
logger.info("Advanced to next wallpaper")
|
||||
# Give wpaperd time to complete transition
|
||||
time.sleep(1)
|
||||
except subprocess.CalledProcessError as e:
|
||||
logger.error(f"Failed to advance wallpaper: {e}")
|
||||
raise
|
||||
except subprocess.TimeoutExpired:
|
||||
logger.error("Timeout while advancing wallpaper")
|
||||
raise
|
||||
except FileNotFoundError:
|
||||
logger.error("wpaperctl command not found")
|
||||
raise
|
||||
|
||||
def get_current_wallpaper(self) -> Path:
|
||||
"""
|
||||
Get the current wallpaper path from wpaperd.
|
||||
|
||||
Returns:
|
||||
Path to current wallpaper file
|
||||
|
||||
Raises:
|
||||
RuntimeError: If unable to get wallpaper path
|
||||
"""
|
||||
wallpaper_state_dir = self.home / ".local/state/wpaperd/wallpapers"
|
||||
|
||||
if not wallpaper_state_dir.exists():
|
||||
raise RuntimeError(f"Wpaperd state directory not found: {wallpaper_state_dir}")
|
||||
|
||||
# Get first monitor directory
|
||||
try:
|
||||
monitor_dirs = list(wallpaper_state_dir.iterdir())
|
||||
if not monitor_dirs:
|
||||
raise RuntimeError("No monitor directories found in wpaperd state")
|
||||
|
||||
monitor_name = monitor_dirs[0].name
|
||||
|
||||
# Query wpaperd for current wallpaper
|
||||
result = subprocess.run(
|
||||
[self.wpaperctl, 'get-wallpaper', monitor_name],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True,
|
||||
timeout=5
|
||||
)
|
||||
|
||||
wallpaper_path = result.stdout.strip()
|
||||
if not wallpaper_path:
|
||||
raise RuntimeError("wpaperctl returned empty wallpaper path")
|
||||
|
||||
return Path(wallpaper_path)
|
||||
|
||||
except (subprocess.CalledProcessError, subprocess.TimeoutExpired, IndexError) as e:
|
||||
raise RuntimeError(f"Failed to get current wallpaper: {e}")
|
||||
|
||||
def generate_palette(self, wallpaper: Path, pywal_flags: list[str]) -> None:
|
||||
"""
|
||||
Generate pywal color palette from wallpaper.
|
||||
|
||||
Args:
|
||||
wallpaper: Path to wallpaper image
|
||||
pywal_flags: List of additional pywal flags
|
||||
"""
|
||||
cmd = [self.wal, '-i', str(wallpaper), '--backend', self.backend] + pywal_flags
|
||||
|
||||
try:
|
||||
subprocess.run(cmd, check=True, timeout=30, capture_output=True)
|
||||
logger.info(f"Generated palette from: {wallpaper.name}")
|
||||
except subprocess.CalledProcessError as e:
|
||||
logger.error(f"Failed to generate palette: {e}")
|
||||
raise
|
||||
except subprocess.TimeoutExpired:
|
||||
logger.error("Timeout while generating palette")
|
||||
raise
|
||||
except FileNotFoundError:
|
||||
logger.error("wal command not found")
|
||||
raise
|
||||
|
||||
def apply_themes(self) -> None:
|
||||
"""Apply GTK and Kvantum themes using helper script."""
|
||||
try:
|
||||
subprocess.run(
|
||||
['python3', self.apply_themes_script],
|
||||
check=True,
|
||||
timeout=30,
|
||||
capture_output=True
|
||||
)
|
||||
logger.debug("Applied themes successfully")
|
||||
except subprocess.CalledProcessError as e:
|
||||
logger.error(f"Failed to apply themes: {e}")
|
||||
raise
|
||||
except subprocess.TimeoutExpired:
|
||||
logger.error("Timeout while applying themes")
|
||||
raise
|
||||
except FileNotFoundError as e:
|
||||
logger.error(f"Theme application script not found: {e}")
|
||||
raise
|
||||
|
||||
def run(self) -> int:
|
||||
"""
|
||||
Execute the wallpaper rotation and theme regeneration process.
|
||||
|
||||
Returns:
|
||||
Exit code (0 for success, 1 for failure)
|
||||
"""
|
||||
# Check if wpaperd is running
|
||||
if not self.is_wpaperd_running():
|
||||
logger.info("wpaperd is not running, skipping rotation")
|
||||
return 0
|
||||
|
||||
try:
|
||||
# Determine mode
|
||||
mode, pywal_flags = self.determine_mode()
|
||||
logger.info(f"Rotating wallpaper and regenerating {mode} palette...")
|
||||
|
||||
# Pause wpaperd
|
||||
self.pause_wpaperd()
|
||||
|
||||
try:
|
||||
# Advance to next wallpaper
|
||||
self.advance_wallpaper()
|
||||
|
||||
# Get new wallpaper path
|
||||
current_wallpaper = self.get_current_wallpaper()
|
||||
logger.info(f"New wallpaper: {current_wallpaper.name}")
|
||||
|
||||
# Generate palette
|
||||
self.generate_palette(current_wallpaper, pywal_flags)
|
||||
|
||||
# Apply themes
|
||||
self.apply_themes()
|
||||
|
||||
logger.info("Wallpaper rotated and palette regenerated successfully!")
|
||||
return 0
|
||||
|
||||
finally:
|
||||
# Always resume wpaperd, even if something fails
|
||||
self.resume_wpaperd()
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Wallpaper rotation failed: {e}", exc_info=True)
|
||||
return 1
|
||||
|
||||
|
||||
def main():
|
||||
"""Main entry point."""
|
||||
parser = argparse.ArgumentParser(description='Wallpaper rotation with theme regeneration')
|
||||
parser.add_argument('--light-time', default='09:00:00', help='Light theme start time (HH:MM:SS)')
|
||||
parser.add_argument('--dark-time', default='16:30:00', help='Dark theme start time (HH:MM:SS)')
|
||||
parser.add_argument('--backend', default='haishoku', help='Pywal color extraction backend')
|
||||
parser.add_argument('--apply-themes-script', required=True, help='Path to apply-themes script')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
rotator = WallpaperRotator(
|
||||
light_time=args.light_time,
|
||||
dark_time=args.dark_time,
|
||||
backend=args.backend,
|
||||
apply_themes_script=args.apply_themes_script
|
||||
)
|
||||
|
||||
return rotator.run()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main())
|
||||
100
shared/modules/services/wallpaper-rotator.nix
Normal file
100
shared/modules/services/wallpaper-rotator.nix
Normal file
@ -0,0 +1,100 @@
|
||||
{ config, lib, pkgs, ... }:
|
||||
|
||||
let
|
||||
cfg = config.services.wallpaperRotator;
|
||||
in
|
||||
{
|
||||
options.services.wallpaperRotator = {
|
||||
enable = lib.mkEnableOption "wallpaper rotation service using wpaperd";
|
||||
|
||||
user = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
description = "Username for the wallpaper service";
|
||||
};
|
||||
|
||||
wallpaperPath = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = "/home/${cfg.user}/nixos/shared/modules/services/wallpapers";
|
||||
description = "Path to the wallpaper directory";
|
||||
};
|
||||
|
||||
duration = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = "5m";
|
||||
example = "30s";
|
||||
description = "How long to display each wallpaper (e.g., 30s, 5m, 1h)";
|
||||
};
|
||||
|
||||
sorting = lib.mkOption {
|
||||
type = lib.types.enum [ "random" "ascending" "descending" ];
|
||||
default = "random";
|
||||
description = "Order in which to display wallpapers";
|
||||
};
|
||||
|
||||
mode = lib.mkOption {
|
||||
type = lib.types.enum [ "center" "fit" "fit-border-color" "stretch" "tile" ];
|
||||
default = "center";
|
||||
description = "How to display wallpapers when size differs from display resolution";
|
||||
};
|
||||
|
||||
transition = {
|
||||
effect = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = "fade";
|
||||
example = "hexagonalize";
|
||||
description = ''
|
||||
Transition effect name. Available effects include:
|
||||
fade, hexagonalize, simple-zoom, swirl, circle, wipe, slide, etc.
|
||||
See wpaperd documentation for full list.
|
||||
'';
|
||||
};
|
||||
|
||||
duration = lib.mkOption {
|
||||
type = lib.types.int;
|
||||
default = 300;
|
||||
example = 2000;
|
||||
description = "Transition duration in milliseconds";
|
||||
};
|
||||
};
|
||||
|
||||
queueSize = lib.mkOption {
|
||||
type = lib.types.int;
|
||||
default = 10;
|
||||
description = "Size of wallpaper history queue for random mode";
|
||||
};
|
||||
|
||||
initialTransition = lib.mkOption {
|
||||
type = lib.types.bool;
|
||||
default = true;
|
||||
description = "Enable transition effect at wpaperd startup";
|
||||
};
|
||||
};
|
||||
|
||||
config = lib.mkIf cfg.enable {
|
||||
home-manager.users.${cfg.user} = {
|
||||
services.wpaperd = {
|
||||
enable = true;
|
||||
settings = {
|
||||
# Default configuration for all displays
|
||||
default = {
|
||||
path = cfg.wallpaperPath;
|
||||
duration = cfg.duration;
|
||||
sorting = cfg.sorting;
|
||||
mode = cfg.mode;
|
||||
transition-time = cfg.transition.duration;
|
||||
queue-size = cfg.queueSize;
|
||||
initial-transition = cfg.initialTransition;
|
||||
};
|
||||
|
||||
# Apply transition effect
|
||||
"default.transition.${cfg.transition.effect}" = {};
|
||||
};
|
||||
};
|
||||
|
||||
# Add wpaperd CLI tool to user packages for manual control
|
||||
home.packages = with pkgs; [
|
||||
wpaperd
|
||||
];
|
||||
};
|
||||
};
|
||||
}
|
||||
26
shared/modules/services/wallpapers/README.md
Normal file
26
shared/modules/services/wallpapers/README.md
Normal file
@ -0,0 +1,26 @@
|
||||
# Wallpapers
|
||||
|
||||
This directory contains purchased wallpapers that are licensed for personal use only.
|
||||
|
||||
## License
|
||||
|
||||
These images are purchased and copyrighted by their respective creators.
|
||||
They are **not included in this git repository** and are licensed for personal use only.
|
||||
|
||||
No reproduction, distribution, or commercial use is permitted.
|
||||
|
||||
## Usage
|
||||
|
||||
Place your wallpaper images in this directory. The wallpaper rotation service
|
||||
will automatically detect and rotate through them.
|
||||
|
||||
Supported formats:
|
||||
- JPEG (.jpg, .jpeg)
|
||||
- PNG (.png)
|
||||
- WebP (.webp)
|
||||
- AVIF (.avif)
|
||||
|
||||
## Configuration
|
||||
|
||||
The wallpaper rotation service is configured via the `services.wallpaperRotator`
|
||||
option in your host configuration. See the module documentation for available options.
|
||||
Loading…
Reference in New Issue
Block a user