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/**
|
**/node_modules/**
|
||||||
**/*.db
|
**/*.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": {
|
"nixpkgs-unstable": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1765186076,
|
"lastModified": 1769018530,
|
||||||
"narHash": "sha256-hM20uyap1a0M9d344I692r+ik4gTMyj60cQWO+hAYP8=",
|
"narHash": "sha256-MJ27Cy2NtBEV5tsK+YraYr2g851f3Fl1LpNHDzDX15c=",
|
||||||
"owner": "nixos",
|
"owner": "nixos",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "addf7cf5f383a3101ecfba091b98d0a1263dc9b8",
|
"rev": "88d3861acdd3d2f0e361767018218e51810df8a1",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@ -110,11 +110,11 @@
|
|||||||
},
|
},
|
||||||
"nixpkgs_2": {
|
"nixpkgs_2": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1765311797,
|
"lastModified": 1769089682,
|
||||||
"narHash": "sha256-mSD5Ob7a+T2RNjvPvOA1dkJHGVrNVl8ZOrAwBjKBDQo=",
|
"narHash": "sha256-9yA/LIuAVQq0lXelrZPjLuLVuZdm03p8tfmHhnDIkms=",
|
||||||
"owner": "nixos",
|
"owner": "nixos",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "09eb77e94fa25202af8f3e81ddc7353d9970ac1b",
|
"rev": "078d69f03934859a181e81ba987c2bb033eebfc5",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|||||||
@ -1,13 +1,13 @@
|
|||||||
{ config, lib, inputs, outputs, pkgs, timeZone, system, ... }:
|
{ config, lib, inputs, outputs, pkgs, timeZone, system, ... }:
|
||||||
let
|
let
|
||||||
supportedDesktops = [ "sway" "hyprland" ];
|
supportedDesktops = [ "sway" "hyprland" "niri" ];
|
||||||
supportedDesktopsStr = lib.strings.concatStringsSep ", " supportedDesktops;
|
supportedDesktopsStr = lib.strings.concatStringsSep ", " supportedDesktops;
|
||||||
deskCfg = config.deskCfg;
|
deskCfg = config.deskCfg;
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
options.deskCfg = {
|
options.deskCfg = {
|
||||||
de = lib.mkOption {
|
de = lib.mkOption {
|
||||||
default = "sway";
|
default = "niri";
|
||||||
type = lib.types.str;
|
type = lib.types.str;
|
||||||
description = "Desktop Environment";
|
description = "Desktop Environment";
|
||||||
};
|
};
|
||||||
@ -30,8 +30,10 @@ in
|
|||||||
modules/user/main_user.nix
|
modules/user/main_user.nix
|
||||||
modules/sway/sway_conf.nix
|
modules/sway/sway_conf.nix
|
||||||
modules/hypr/hyprland.nix
|
modules/hypr/hyprland.nix
|
||||||
|
modules/niri/niri_conf.nix
|
||||||
../shared/modules/system/power_manager.nix
|
../shared/modules/system/power_manager.nix
|
||||||
../shared/modules/services/motu-m4-combined.nix
|
../shared/modules/services/motu-m4-combined.nix
|
||||||
|
# ../shared/modules/services/theme_switcher/default.nix
|
||||||
# inputs.nur.hmModules.nur
|
# inputs.nur.hmModules.nur
|
||||||
];
|
];
|
||||||
|
|
||||||
@ -88,7 +90,16 @@ in
|
|||||||
};
|
};
|
||||||
|
|
||||||
hypr = {
|
hypr = {
|
||||||
|
enable = false;
|
||||||
|
user = deskCfg.userName;
|
||||||
|
systemPackages = with pkgs; [
|
||||||
|
libreoffice
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
niriwm = {
|
||||||
enable = true;
|
enable = true;
|
||||||
|
useNonFree = true;
|
||||||
user = deskCfg.userName;
|
user = deskCfg.userName;
|
||||||
systemPackages = with pkgs; [
|
systemPackages = with pkgs; [
|
||||||
libreoffice
|
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
|
||||||
decoration {
|
decoration {
|
||||||
rounding = 12
|
rounding = 12
|
||||||
inactive_opacity = 0.9
|
active_opacity = 0.98
|
||||||
|
inactive_opacity = 0.95
|
||||||
dim_inactive = true
|
dim_inactive = true
|
||||||
dim_strength = 0.1
|
dim_strength = 0.03
|
||||||
|
|
||||||
blur {
|
blur {
|
||||||
enabled = true
|
enabled = true
|
||||||
@ -67,7 +68,7 @@ decoration {
|
|||||||
}
|
}
|
||||||
|
|
||||||
shadow {
|
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 = [
|
imports = [
|
||||||
../../../shared/modules/apps/firefox/firefox.nix
|
../../../shared/modules/apps/firefox/firefox.nix
|
||||||
../hypr/hypr_home.nix
|
../niri/niri_home.nix
|
||||||
../vpn-proxy/vpn-proxy.nix
|
../vpn-proxy/vpn-proxy.nix
|
||||||
];
|
];
|
||||||
|
|
||||||
@ -41,7 +41,7 @@
|
|||||||
# Enable VPN proxy script
|
# Enable VPN proxy script
|
||||||
vpnProxy.enable = true;
|
vpnProxy.enable = true;
|
||||||
|
|
||||||
hyprhome = {
|
nirihome = {
|
||||||
enable = true;
|
enable = true;
|
||||||
homePackages = with pkgs; [
|
homePackages = with pkgs; [
|
||||||
#
|
#
|
||||||
@ -57,6 +57,7 @@
|
|||||||
cmake
|
cmake
|
||||||
gcc
|
gcc
|
||||||
oxker # docker desktop tui <3
|
oxker # docker desktop tui <3
|
||||||
|
python3
|
||||||
## nodejs frontend
|
## nodejs frontend
|
||||||
nodejs_24
|
nodejs_24
|
||||||
husky
|
husky
|
||||||
@ -329,6 +330,7 @@
|
|||||||
xdg.configFile = {
|
xdg.configFile = {
|
||||||
# Active linked dotfiles
|
# Active linked dotfiles
|
||||||
"hypr".source = config.lib.file.mkOutOfStoreSymlink "/home/nate/nixos/nate-work/linked-dotfiles/hypr";
|
"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";
|
"waybar".source = config.lib.file.mkOutOfStoreSymlink "/home/nate/nixos/nate-work/linked-dotfiles/waybar";
|
||||||
# Shared
|
# Shared
|
||||||
"helix".source = config.lib.file.mkOutOfStoreSymlink "/home/nate/nixos/shared/linked-dotfiles/helix";
|
"helix".source = config.lib.file.mkOutOfStoreSymlink "/home/nate/nixos/shared/linked-dotfiles/helix";
|
||||||
|
|||||||
@ -237,6 +237,11 @@ in
|
|||||||
# package = unstable.mesa.drivers;
|
# package = unstable.mesa.drivers;
|
||||||
enable32Bit = true;
|
enable32Bit = true;
|
||||||
# package32 = unstable.pkgsi686Linux.mesa.drivers;
|
# package32 = unstable.pkgsi686Linux.mesa.drivers;
|
||||||
|
extraPackages = with pkgs; [
|
||||||
|
intel-vaapi-driver
|
||||||
|
intel-media-driver
|
||||||
|
vpl-gpu-rt
|
||||||
|
];
|
||||||
};
|
};
|
||||||
nvidia = {
|
nvidia = {
|
||||||
# Modesetting is required.
|
# Modesetting is required.
|
||||||
@ -275,42 +280,68 @@ in
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
# Create special on the go boot entry
|
# Create special on the go boot entry for battery saving
|
||||||
#
|
# This disables NVIDIA GPU completely and uses Intel integrated graphics only
|
||||||
## Commenting out for now as it doesn't really work with the laptop well...
|
specialisation = {
|
||||||
#
|
on-the-go.configuration = {
|
||||||
# specialisation = {
|
system.nixos.tags = [ "on-the-go" ];
|
||||||
# on-the-go.configuration = {
|
|
||||||
# system.nixos.tags = [ "on-the-go" ];
|
|
||||||
|
|
||||||
# boot.extraModprobeConfig = ''
|
# Blacklist all NVIDIA kernel modules
|
||||||
# blacklist nouveau
|
boot.blacklistedKernelModules = [ "nouveau" "nvidia" "nvidia_drm" "nvidia_modeset" "nvidia_uvm" ];
|
||||||
# options nouveau modeset=0
|
|
||||||
# '';
|
|
||||||
|
|
||||||
# services.udev.extraRules = ''
|
# Force Intel i915 driver for 13th gen (Raptor Lake) integrated graphics
|
||||||
# # Remove NVIDIA USB xHCI Host Controller devices, if present
|
# Device IDs: a7a0 or a7a8 for i7-13700H
|
||||||
# ACTION=="add", SUBSYSTEM=="pci", ATTR{vendor}=="0x10de", ATTR{class}=="0x0c0330", ATTR{power/control}="auto", ATTR{remove}="1"
|
boot.kernelParams = [
|
||||||
# # Remove NVIDIA USB Type-C UCSI devices, if present
|
"i915.force_probe=a7a0"
|
||||||
# ACTION=="add", SUBSYSTEM=="pci", ATTR{vendor}=="0x10de", ATTR{class}=="0x0c8000", ATTR{power/control}="auto", ATTR{remove}="1"
|
"module_blacklist=nvidia,nvidia_drm,nvidia_modeset,nvidia_uvm"
|
||||||
# # 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
|
boot.extraModprobeConfig = ''
|
||||||
# ACTION=="add", SUBSYSTEM=="pci", ATTR{vendor}=="0x10de", ATTR{class}=="0x03[0-9]*", ATTR{power/control}="auto", ATTR{remove}="1"
|
blacklist nouveau
|
||||||
# '';
|
blacklist nvidia
|
||||||
# hardware.nvidia = {
|
blacklist nvidia_drm
|
||||||
# prime.offload.enable = lib.mkForce false;
|
blacklist nvidia_modeset
|
||||||
# prime.offload.enableOffloadCmd = lib.mkForce false;
|
blacklist nvidia_uvm
|
||||||
# powerManagement.finegrained = lib.mkForce false;
|
options nouveau modeset=0
|
||||||
# prime.sync.enable = lib.mkForce false;
|
'';
|
||||||
# };
|
|
||||||
# boot.blacklistedKernelModules = [ "nouveau" "nvidia" "nvidia_drm" "nvidia_modeset" ];
|
# Remove NVIDIA devices from PCI bus to save power
|
||||||
# # Hint to kernel for integrated graphics, ID from command `$ nix-shell -p pciutils --run "lspci -nn | grep VGA"`
|
services.udev.extraRules = ''
|
||||||
# boot.kernelParams = [ "i915.force_probe=a7a0" "acpi_backlight=native" ];
|
# Remove NVIDIA USB xHCI Host Controller devices, if present
|
||||||
# # Default drivers
|
ACTION=="add", SUBSYSTEM=="pci", ATTR{vendor}=="0x10de", ATTR{class}=="0x0c0330", ATTR{power/control}="auto", ATTR{remove}="1"
|
||||||
# services.xserver.videoDrivers = [ "modesetting" "fbdev" ];
|
# 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;
|
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
|
# sdkmanager
|
||||||
unstable.opencode
|
unstable.opencode
|
||||||
unstable.claude-code
|
unstable.claude-code
|
||||||
|
usbutils
|
||||||
|
openscad
|
||||||
|
|
||||||
#
|
#
|
||||||
# Gaming
|
# Gaming
|
||||||
@ -85,6 +87,7 @@
|
|||||||
#
|
#
|
||||||
bat
|
bat
|
||||||
duf
|
duf
|
||||||
|
dust
|
||||||
fd
|
fd
|
||||||
fzf
|
fzf
|
||||||
lsd
|
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