diff --git a/modules/desktop/default.nix b/modules/desktop/default.nix new file mode 100644 index 0000000..39af47e --- /dev/null +++ b/modules/desktop/default.nix @@ -0,0 +1,14 @@ +{ self, inputs, ... }: { + flake.nixosModules.desktop = { + imports = [ + self.nixosModules.desktopShellDmsOptions + self.nixosModules.desktopHyprland + self.nixosModules.desktopNiri + self.nixosModules.desktopPlasma + self.nixosModules.desktopOptions + self.nixosModules.desktopGui + self.nixosModules.desktopWallpapers + self.nixosModules.desktopWaydroid + ]; + }; +} diff --git a/modules/desktop/gui.nix b/modules/desktop/gui.nix new file mode 100644 index 0000000..0a0ccbb --- /dev/null +++ b/modules/desktop/gui.nix @@ -0,0 +1,128 @@ +{ ... }: { + flake.nixosModules.desktopGui = + { config, lib, pkgs, self, inputs, options, ... }: + let + pi5Greeter = self.lib.pi5NiriKdl; + cfg = config.chiasson.desktop; + guiEnabled = cfg.hyprland.enable || cfg.niri.enable || cfg.plasma.enable; + hmAvailable = lib.hasAttrByPath [ "home-manager" "sharedModules" ] options; + dm = cfg.displayManager; + variant = dm.variant or "sddm"; + useGreeter = variant == "dankgreeter"; + sddmTheme = + if dm.sddm.theme.package == null then + "breeze" + else + "${dm.sddm.theme.package}/share/sddm/themes/${dm.sddm.theme.id}"; + effectiveDefaultSession = + if cfg.defaultSession != null then + cfg.defaultSession + else if cfg.hyprland.enable then + "hyprland" + else if cfg.niri.enable then + "niri" + else + "plasma"; + greeterCompositor = + if effectiveDefaultSession == "niri" then + "niri" + else if effectiveDefaultSession == "hyprland" then + "hyprland" + else + "niri"; + enabledUsers = config.chiasson.users.enabled or [ ]; + greeterConfigHome = + if dm.greeter.configHome != null then + dm.greeter.configHome + else if enabledUsers != [ ] then + "/home/${builtins.head enabledUsers}" + else + null; + in + { + # Unconditional imports only — conditional imports that peek at `config` recurse during merge. + imports = [ + inputs.dms.nixosModules.greeter + ]; + + config = lib.mkMerge [ + (lib.mkIf guiEnabled { + services.xserver.enable = true; + + # Chromium/Electron (Edge, Vesktop, etc.) only add native Wayland + IME flags when this is + # set; nixpkgs wrappers gate on NIXOS_OZONE_WL and WAYLAND_DISPLAY. Helps PipeWire/desktop + # portal screen capture on wlroots-like sessions. Harmless on X11 (flags stay off). + environment.sessionVariables.NIXOS_OZONE_WL = lib.mkDefault "1"; + + xdg.portal.enable = true; + xdg.portal.extraPortals = + [ pkgs.xdg-desktop-portal-gtk ] + ++ lib.optionals cfg.plasma.enable [ pkgs.kdePackages.xdg-desktop-portal-kde ]; + }) + (lib.mkIf (guiEnabled && !useGreeter) { + services.displayManager.sddm = { + enable = true; + wayland.enable = dm.sddm.wayland.enable; + theme = sddmTheme; + inherit (dm.sddm) enableHidpi settings; + extraPackages = lib.optionals (dm.sddm.theme.package != null) ( + with pkgs.kdePackages; [ + qtdeclarative + qtsvg + ] + ); + }; + services.displayManager.defaultSession = effectiveDefaultSession; + }) + (lib.mkIf (guiEnabled && useGreeter) { + assertions = [ + { + assertion = greeterConfigHome != null; + message = "DankGreeter needs chiasson.desktop.displayManager.greeter.configHome or a non-empty chiasson.users.enabled list."; + } + ]; + + services.displayManager.defaultSession = effectiveDefaultSession; + + programs.dank-material-shell.greeter = { + enable = true; + compositor = { + name = greeterCompositor; + } + // lib.optionalAttrs (cfg.niri.raspberryPi5DrmWorkaround && greeterCompositor == "niri") { + customConfig = lib.mkDefault pi5Greeter.dankGreeterCompositorConfig; + }; + configHome = greeterConfigHome; + }; + }) + (lib.mkIf (cfg.defaultPackages.enabled && guiEnabled) { + environment.systemPackages = cfg.defaultPackages.packages; + }) + (lib.mkIf (cfg.extraPackages != [ ] && guiEnabled) { + environment.systemPackages = cfg.extraPackages; + }) + (lib.mkIf (guiEnabled && cfg.keyring.enable) { + services.gnome.gnome-keyring.enable = true; + security.pam.services.login.enableGnomeKeyring = true; + services.xserver.updateDbusEnvironment = lib.mkDefault true; + # Electron apps (Element, Slack, etc.) use libsecret via keytar; without it they report + # "unsupported keyring" even if gnome-keyring is enabled. + environment.systemPackages = [ pkgs.libsecret ]; + }) + (lib.mkIf (guiEnabled && cfg.keyring.enable && !useGreeter) { + security.pam.services.sddm.enableGnomeKeyring = true; + }) + (lib.mkIf (guiEnabled && cfg.keyring.enable && useGreeter) { + security.pam.services.greetd.enableGnomeKeyring = true; + }) + (lib.mkIf (guiEnabled && cfg.keyring.enable && hmAvailable) { + "home-manager".sharedModules = [ + ({ lib, pkgs, ... }: { + services.gnome-keyring.enable = lib.mkDefault true; + home.packages = [ pkgs.gcr ]; + }) + ]; + }) + ]; + }; +} diff --git a/modules/desktop/hyprland/default.nix b/modules/desktop/hyprland/default.nix new file mode 100644 index 0000000..cbb6adb --- /dev/null +++ b/modules/desktop/hyprland/default.nix @@ -0,0 +1,213 @@ +{ self, ... }: { + flake.nixosModules.desktopHyprland = + { config, options, lib, pkgs, ... }: + let + cfg = config.chiasson.desktop; + hmAvailable = lib.hasAttrByPath [ "home-manager" "sharedModules" ] options; + in + { + options.chiasson.desktop.hyprland = { + enable = lib.mkOption { + type = lib.types.bool; + default = false; + description = "Hyprland session + HM wiring."; + }; + settings = lib.mkOption { + type = lib.types.attrs; + default = { }; + description = "Extra `wayland.windowManager.hyprland.settings` merged with defaults."; + }; + }; + + config = lib.mkMerge [ + (lib.mkIf cfg.hyprland.enable { + programs.hyprland.enable = true; + }) + (lib.mkIf (cfg.hyprland.enable && hmAvailable) { + "home-manager".sharedModules = [ self.homeManagerModules.desktopHyprland ]; + }) + ]; + }; + + flake.homeManagerModules.desktopHyprland = { + config, + lib, + osConfig ? { }, + pkgs, + ... + }: + let + hyprlandEnabled = osConfig.chiasson.desktop.hyprland.enable or false; + # nixpkgs hyprland-plugins pin is stale for current Hyprland — override to a known-good rev. + hyprbarsPatched = + let + hyprlandPluginsSrc = pkgs.fetchFromGitHub { + owner = "hyprwm"; + repo = "hyprland-plugins"; + rev = "b85a56b9531013c79f2f3846fd6ee2ff014b8960"; + hash = "sha256-xwNa+1D8WPsDnJtUofDrtyDCZKZotbUymzV/R5s+M0I="; + }; + in + pkgs.hyprlandPlugins.hyprbars.overrideAttrs (_: { + version = "unstable-2026-02-23"; + src = "${hyprlandPluginsSrc}/hyprbars"; + }); + in + { + config = lib.mkIf hyprlandEnabled { + wayland.windowManager.hyprland = { + enable = true; + # null = use NixOS Hyprland/xdg-desktop-portal-hyprland (same versions as the session). + package = null; + portalPackage = null; + plugins = [ hyprbarsPatched ]; + extraConfig = '' + source = ~/.config/hypr/colors.conf + ''; + + settings = lib.mkMerge [ + { + monitor = [ ",preferred,auto,auto" ]; + general = { + gaps_in = 8; + gaps_out = 4; + border_size = 2; + allow_tearing = false; + }; + cursor.no_hardware_cursors = true; + env = [ + "XCURSOR_THEME,phinger-cursors-dark" + "XCURSOR_SIZE,32" + ]; + input = { + kb_layout = "ca"; + kb_variant = ""; + numlock_by_default = true; + }; + binds.scroll_event_delay = 50; + exec-once = [ + "nm-applet --indicator &" + "sleep 1 && hyprctl reload" + ]; + + # Default keybinds + bind = + [ + "SUPER,T,exec,kitty -e fish" + "ControlSuper,T,exec,konsole" + "SUPER,D,exec,rofi -show drun" + "ControlSuper,D,exec,rofi -show window" + "SUPER,E,exec,dolphin" + "Super, Q, killactive" + "ControlSuper, Q, exec, hyprctl kill" + "Super, F, fullscreen, 0" + "Super, G, fullscreen, 1" + "ShiftSuper, F, fullscreenstate, 0 3" + "Super, Minus, splitratio, -0.1" + "Super, Equal, splitratio, 0.1" + "AltSuper, Space, togglefloating" + "Super, P, pin" + "Super, Space, exec, dms ipc call spotlight toggle" + "Super, I, exec, dms ipc call settings focusOrToggle" + "Super, N, exec, dms ipc call notepad toggle" + "ShiftSuper, N, exec, dms ipc call notifications toggle" + "Super, M, exec, dms ipc call processlist focusOrToggle" + "Super, L, exec, dms ipc call lock lock" + "ShiftSuper, V, exec, dms ipc call clipboard toggle" + "Super, Tab, cyclenext" + "Super, Tab, bringactivetotop" + "Super, left, movefocus, l" + "Super, right, movefocus, r" + "Super, up, movefocus, u" + "Super, down, movefocus, d" + "ShiftSuper, left, movewindow, l" + "ShiftSuper, right, movewindow, r" + "ShiftSuper, up, movewindow, u" + "ShiftSuper, down, movewindow, d" + ] + ++ (builtins.map (i: "Super, ${toString i}, workspace, ${toString i}") (builtins.genList (n: n + 1) 9)) + ++ (builtins.map (i: "ControlSuper, ${toString i}, focusworkspaceoncurrentmonitor, ${toString i}") (builtins.genList (n: n + 1) 9)) + ++ (builtins.map (i: "ShiftSuper, ${toString i}, movetoworkspacesilent, ${toString i}") (builtins.genList (n: n + 1) 9)); + + bindm = [ + " , mouse:282, movewindow" + "Super, mouse:272, movewindow" + "Super, mouse:273, resizewindow" + ]; + + bindl = [ + "Super, mouse_up, splitratio, -0.1" + "Super, mouse_down, splitratio, 0.1" + " , XF86AudioPlay, exec, playerctl play-pause" + " , XF86AudioPrev, exec, playerctl previous" + " , XF86AudioNext, exec, playerctl next" + " , XF86AudioMute, exec, wpctl set-mute @DEFAULT_AUDIO_SINK@ toggle" + " , XF86MonBrightnessDown, exec, brightnessctl s 10%-" + " , XF86MonBrightnessUp, exec, brightnessctl s +10%" + ]; + + bindel = [ + " , XF86AudioRaiseVolume, exec, wpctl set-volume @DEFAULT_AUDIO_SINK@ 5%+" + " , XF86AudioLowerVolume, exec, wpctl set-volume @DEFAULT_AUDIO_SINK@ 5%-" + ]; + + # Decoration / blur + decoration = { + rounding = 10; + active_opacity = 0.95; + inactive_opacity = 0.85; + shadow = { + enabled = true; + range = 5; + render_power = 8; + }; + blur = { + enabled = true; + new_optimizations = true; + xray = false; + size = 2; + passes = 4; + vibrancy = 10; + }; + }; + misc = { + disable_hyprland_logo = true; + disable_splash_rendering = true; + }; + plugin.hyprbars = { + enabled = true; + bar_height = 30; + bar_blur = true; + bar_padding = 10; + bar_button_padding = 7; + bar_precedence_over_border = true; + bar_part_of_window = true; + bar_title_enabled = true; + bar_text_size = 10; + bar_text_font = "Sans"; + bar_text_align = "center"; + bar_buttons_alignment = "left"; + icon_on_hover = true; + "hyprbars-button" = [ + "rgb(ed6a5f), 12, 󰖭, hyprctl dispatch killactive, rgb(460804)" + "rgb(f6be50), 12, 󰖰, hyprctl dispatch movetoworkspacesilent special:minimized, rgb(90591d)" + "rgb(61c555), 12, 󰘖, hyprctl dispatch fullscreen 1, rgb(2a6218)" + ]; + }; + + windowrule = [ + "sync_fullscreen 0, match:class ^(?i)microsoft-edge|Spotify|org.kde.gwenview|zen-beta$" + "opacity 1.0 override 0.95 override, match:class ^(?i)microsoft-edge$" + "opacity 1.0 override 1.00 override, match:class ^(?i)com.stremio.stremio$" + "opacity 1.0 override 0.85 override, match:class ^(?i)zen-beta$" + "no_screen_share on, match:class ^(?i)(microsoft-edge|zen-beta)$, match:title ^(?i).*(scotiabank|paypal).*" + "no_screen_share on, match:class ^(?i)microsoft-edge$, match:initial_title ^(?i).*(?i)personal 2.*edge.*$" + "hyprbars:no_bar on, match:class ^(?i)(microsoft-edge|Cursor|Flow|looking-glass-client|localsend_app)$" + ]; + } + (osConfig.chiasson.desktop.hyprland.settings or { }) + ]; + }; + }; + }; +} diff --git a/modules/desktop/niri/default.nix b/modules/desktop/niri/default.nix new file mode 100644 index 0000000..5504565 --- /dev/null +++ b/modules/desktop/niri/default.nix @@ -0,0 +1,207 @@ +{ self, inputs, ... }: +let + # Keep defaults in this let — a bare niri-settings.nix next to this file would get picked up by import-tree. + niriBaseSettings = + pkgs: + { + input.keyboard = { + xkb.layout = "ca"; + xkb.variant = ""; + }; + input."focus-follows-mouse" = _: { + props."max-scroll-amount" = "45%"; + content = { }; + }; + input."warp-mouse-to-focus" = _: { }; + layout.gaps = 5; + + window-rules = [ + { + matches = [ + { + app-id = "^$"; + title = "^$"; + } + ]; + open-floating = true; + open-focused = false; + } + ]; +#TODO[epic=Binds] Go over binds again + binds = { + "Mod+T"."spawn-sh" = "${pkgs.lib.getExe pkgs.kitty} -e fish"; #TODO[epic=Binds] This should only be set if having kitty + "Mod+Control+T"."spawn-sh" = "konsole"; #TODO[epic=Binds] This should only be set if having konsole + "Mod+D"."spawn-sh" = "rofi -show drun"; #TODO[epic=Binds] This should only be set if having rofi + "Mod+Space"."spawn-sh" = "dms ipc call spotlight toggle"; #TODO[epic=Binds] This should only be set if having dms + "Mod+E"."spawn-sh" = "dolphin"; #TODO[epic=Binds] This should only be set if having dolphin + "Mod+Control+O"."spawn-sh" = "rofi -show window"; #TODO[epic=Binds] This should only be set if having rofi +"Mod+V"."spawn-sh" = "dms ipc call clipboard toggle"; #TODO[epic=Binds] This should only be set if having dms +"Mod+N"."spawn-sh" = "dms ipc call notepad toggle"; #TODO[epic=Binds] This should only be set if having dms +"Mod+Shift+N"."spawn-sh" = "dms ipc call notifications toggle"; #TODO[epic=Binds] This should only be set if having dms +"Mod+M"."spawn-sh" = "dms ipc call processlist toggle"; #TODO[epic=Binds] This should only be set if having dms +"Mod+L"."spawn-sh" = "dms ipc call lock lock"; #TODO[epic=Binds] This should only be set if having dms + + "Mod+Q"."close-window" = _: { }; + "Mod+F"."maximize-column" = _: { }; + "Mod+Shift+F"."fullscreen-window" = _: { }; + "Mod+O"."toggle-overview" = _: { }; + "Mod+Shift+NumberSign"."show-hotkey-overlay" = _: { }; + "Mod+Shift+E".quit = _: { }; + + "Mod+Left"."focus-column-or-monitor-left" = _: { }; + "Mod+Down"."focus-window-or-monitor-down" = _: { }; + "Mod+Up"."focus-window-or-monitor-up" = _: { }; + "Mod+Right"."focus-column-or-monitor-right" = _: { }; + + "Mod+Shift+WheelScrollDown"."focus-workspace-down" = _: { }; + "Mod+Shift+WheelScrollUp"."focus-workspace-up" = _: { }; + + "Mod+WheelScrollDown"."focus-column-or-monitor-right" = _: { }; + "Mod+WheelScrollUp"."focus-column-or-monitor-left" = _: { }; + + "Mod+Shift+Left"."move-column-left-or-to-monitor-left" = _: { }; + "Mod+Shift+Down"."move-window-down" = _: { }; + "Mod+Shift+Up"."move-window-up" = _: { }; + "Mod+Shift+Right"."move-column-right-or-to-monitor-right" = _: { }; + "Mod+Page_Up"."focus-workspace-up" = _: { }; + "Mod+Page_Down"."focus-workspace-down" = _: { }; + "Mod+Shift+Page_Up"."move-column-to-workspace-up" = _: { }; + "Mod+Shift+Page_Down"."move-column-to-workspace-down" = _: { }; + "Mod+R"."switch-preset-column-width" = _: { }; + "Mod+BracketLeft"."consume-or-expel-window-left" = _: { }; + "Mod+BracketRight"."consume-or-expel-window-right" = _: { }; + "Mod+Comma"."consume-window-into-column" = _: { }; + "Mod+Period"."expel-window-from-column" = _: { }; + "Mod+Alt+Space"."toggle-window-floating" = _: { }; + #"Mod+Shift+V"."switch-focus-between-floating-and-tiling" = _: { }; #TODO[epic=Binds] Find free bind that is not in the way and not overcomplicated to remember + + "Mod+1"."focus-workspace" = 1; + "Mod+2"."focus-workspace" = 2; + "Mod+3"."focus-workspace" = 3; + "Mod+4"."focus-workspace" = 4; + "Mod+5"."focus-workspace" = 5; + "Mod+6"."focus-workspace" = 6; + "Mod+7"."focus-workspace" = 7; + "Mod+8"."focus-workspace" = 8; + "Mod+9"."focus-workspace" = 9; + "Mod+Ctrl+1"."move-column-to-workspace" = 1; + "Mod+Ctrl+2"."move-column-to-workspace" = 2; + "Mod+Ctrl+3"."move-column-to-workspace" = 3; + "Mod+Ctrl+4"."move-column-to-workspace" = 4; + "Mod+Ctrl+5"."move-column-to-workspace" = 5; + "Mod+Ctrl+6"."move-column-to-workspace" = 6; + "Mod+Ctrl+7"."move-column-to-workspace" = 7; + "Mod+Ctrl+8"."move-column-to-workspace" = 8; + "Mod+Ctrl+9"."move-column-to-workspace" = 9; + + "XF86AudioRaiseVolume".spawn = [ + "wpctl" + "set-volume" + "@DEFAULT_AUDIO_SINK@" + "0.05+" + ]; + "XF86AudioLowerVolume".spawn = [ + "wpctl" + "set-volume" + "@DEFAULT_AUDIO_SINK@" + "0.05-" + ]; + "XF86AudioMute".spawn = [ + "wpctl" + "set-mute" + "@DEFAULT_AUDIO_SINK@" + "toggle" + ]; + + Print.screenshot = _: { }; + "Ctrl+Print"."screenshot-screen" = _: { }; + "Alt+Print"."screenshot-window" = _: { }; + }; + }; + + mergeNiriSettings = + pkgs: niriCfg: + let + lib = pkgs.lib; + pi5 = self.lib.pi5NiriKdl; + rpi5Extra = lib.optionalString (niriCfg.raspberryPi5DrmWorkaround or false) pi5.drmExtraConfig; + userExtra = niriCfg.extraSettings or { }; + extraConfigMerged = rpi5Extra + (userExtra.extraConfig or ""); + in + lib.recursiveUpdate (niriBaseSettings pkgs) ( + userExtra + // lib.optionalAttrs (rpi5Extra != "" || (userExtra.extraConfig or "") != "") { + extraConfig = extraConfigMerged; + } + ); +in +{ + flake.homeManagerModules.desktopNiri = + { lib, pkgs, osConfig ? { }, ... }: + let + niriOs = osConfig.chiasson.desktop.niri or { }; + niriEnabled = osConfig.chiasson.desktop.niri.enable or false; + mergedSettings = mergeNiriSettings pkgs niriOs; + niriConfigPkg = inputs.wrapper-modules.wrappers.niri.wrap { + inherit pkgs; + settings = mergedSettings; + }; + in + { + config = lib.mkIf niriEnabled { + xdg.configFile."niri/config.kdl".source = "${niriConfigPkg}/niri-config.kdl"; + }; + }; + + flake.nixosModules.desktopNiri = + { config, options, lib, pkgs, self, ... }: + let + cfg = config.chiasson.desktop; + hmAvailable = lib.hasAttrByPath [ "home-manager" "sharedModules" ] options; + in + { + options.chiasson.desktop.niri = { + enable = lib.mkOption { + type = lib.types.bool; + default = false; + description = "Niri compositor session + NixOS packages."; + }; + + raspberryPi5DrmWorkaround = lib.mkEnableOption '' + Pi 5 + RP1 DSI: Niri DRM debug rules (renderD128, ignore card1/2) + matching DankGreeter niri config. + Opt-in only — HDMI-only / other Pi setups do not need this. + ''; + + extraSettings = lib.mkOption { + type = lib.types.attrs; + default = { }; + description = '' + Merged into shared defaults → `config.kdl` via wrapper-modules. Put raw KDL in `extraConfig`. + ''; + }; + }; + + config = lib.mkMerge [ + { + assertions = [ + { + assertion = !cfg.niri.raspberryPi5DrmWorkaround || cfg.niri.enable; + message = "chiasson.desktop.niri.raspberryPi5DrmWorkaround requires chiasson.desktop.niri.enable."; + } + ]; + } + (lib.mkIf cfg.niri.enable { + programs.niri.enable = true; + programs.niri.package = pkgs.niri; + programs.xwayland.enable = true; + xdg.portal.extraPortals = [ pkgs.xdg-desktop-portal-gnome ]; + # Niri resolves `xwayland-satellite` from PATH to provide XWayland + `$DISPLAY` for X11 + # clients (Steam, etc.). See https://github.com/YaLTeR/niri/issues/452 + environment.systemPackages = [ pkgs.xwayland-satellite ]; + }) + (lib.mkIf (cfg.niri.enable && hmAvailable) { + "home-manager".sharedModules = [ self.homeManagerModules.desktopNiri ]; + }) + ]; + }; +} diff --git a/modules/desktop/options.nix b/modules/desktop/options.nix new file mode 100644 index 0000000..9ca4803 --- /dev/null +++ b/modules/desktop/options.nix @@ -0,0 +1,218 @@ +{ ... }: { + flake.nixosModules.desktopOptions = + { config, options, lib, pkgs, self, inputs, ... }: + let + cfg = config.chiasson.desktop; + hmAvailable = lib.hasAttrByPath [ "home-manager" "sharedModules" ] options; + guiEnabled = cfg.hyprland.enable || cfg.niri.enable || cfg.plasma.enable; + dmsEnabled = cfg.shell == "dms"; + sddmIni = pkgs.formats.ini { }; + # Pixie SDDM theme — Qt6 main; upstream has a qt5 branch if you need it. + pixieSddm = pkgs.stdenvNoCC.mkDerivation { + pname = "pixie-sddm"; + version = "0-unstable-2026-03-29"; + src = pkgs.fetchFromGitHub { + owner = "xCaptaiN09"; + repo = "pixie-sddm"; + rev = "12a5f459ebd6d699be42c188c10976c8bb7076d7"; + hash = "sha256-lmE/49ySuAZDh5xLochWqfSw9qWrIV+fYaK5T2Ckck8="; + }; + dontConfigure = true; + dontBuild = true; + installPhase = '' + mkdir -p "$out/share/sddm/themes/pixie" + cp -r "$src"/. "$out/share/sddm/themes/pixie/" + ''; + meta = { + description = "Pixel / Material 3 inspired SDDM theme (Qt6)"; + homepage = "https://github.com/xCaptaiN09/pixie-sddm"; + license = lib.licenses.mit; + platforms = lib.platforms.linux; + }; + }; + in + { + options.chiasson.desktop = { + defaultSession = lib.mkOption { + type = lib.types.nullOr (lib.types.enum [ + "hyprland" + "niri" + "plasma" + ]); + default = null; + example = "niri"; + description = '' + DM session preference; `null` picks from which compositor flags are enabled. Turn on only + the compositor you use so SDDM does not drag in extras. + ''; + }; + + shell = lib.mkOption { + type = lib.types.nullOr (lib.types.enum [ "dms" ]); + default = null; + example = "dms"; + description = "Desktop shell overlay (e.g. DMS). Extend the enum when you add another."; + }; + + displayManager = { + variant = lib.mkOption { + type = lib.types.nullOr (lib.types.enum [ + "sddm" + "dankgreeter" + ]); + default = null; + description = '' + SDDM vs DankGreeter (greetd + DMS — [docs](https://danklinux.com/docs/dankgreeter/nixos-flake)). + `null`: Plasma-only → SDDM; Hyprland/Niri + `desktop.shell = "dms"` → DankGreeter; else SDDM. + ''; + }; + + greeter = { + configHome = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + example = "/home/olivier"; + description = '' + Whose DMS files to mirror into the greeter cache. `null` → first entry in `chiasson.users.enabled`. + ''; + }; + }; + }; + + displayManager.sddm = { + wayland.enable = lib.mkEnableOption '' + SDDM greeter on Wayland (nicer on HiDPI; turn off if the greeter glitches on your GPU). + '' // { + default = true; + }; + + enableHidpi = lib.mkOption { + type = lib.types.bool; + default = true; + description = "Passed through to `services.displayManager.sddm.enableHidpi`."; + }; + + theme = { + package = lib.mkOption { + type = lib.types.nullOr lib.types.package; + default = pixieSddm; + description = '' + Package providing `share/sddm/themes/`. Default: bundled [Pixie](https://github.com/xCaptaiN09/pixie-sddm). + `null` → Breeze. Other nixpkgs examples: `catppuccin-sddm`, `sddm-sugar-dark`. + ''; + }; + + id = lib.mkOption { + type = lib.types.str; + default = "pixie"; + description = '' + Subdir under `share/sddm/themes/` (default `pixie`). Match your theme package (e.g. catppuccin ids). + ''; + }; + }; + + settings = lib.mkOption { + type = sddmIni.type; + default = { }; + description = "Extra `services.displayManager.sddm.settings` (INI)."; + }; + }; + + plasma.enable = lib.mkOption { + type = lib.types.bool; + default = false; + description = "Plasma 6 session bits for this flake."; + }; + + defaultPackages = { + enabled = lib.mkOption { + type = lib.types.bool; + default = true; + description = "Shared desktop utility packages (ntfs3g, cifs-utils, …)."; + }; + packages = lib.mkOption { + type = lib.types.listOf lib.types.package; + default = with pkgs; [ + ntfs3g + cifs-utils + usbutils + xhost + ]; + description = "Packages merged when `defaultPackages.enabled` is true."; + }; + }; + + extraPackages = lib.mkOption { + type = lib.types.listOf lib.types.package; + default = [ ]; + description = "Extra packages on GUI hosts."; + }; + + homeManager = { + bundleWisdom = lib.mkOption { + type = lib.types.bool; + default = true; + description = '' + Add `wisdom` (baseline + bash) to HM `sharedModules`. Other slices still go in per-user `extraModules`. + ''; + }; + }; + + keyring = { + enable = lib.mkEnableOption '' + gnome-keyring + pam (login + sddm or greetd) + HM user service + `gcr` + `services.xserver.updateDbusEnvironment`. + niri/hyprland: `dbus-update-activation-environment` at compositor start so libsecret/Electron see `WAYLAND_DISPLAY`. + '' // { + default = true; + }; + }; + }; + + config = lib.mkMerge [ + { + assertions = [ + { + assertion = cfg.defaultSession != "hyprland" || cfg.hyprland.enable; + message = "chiasson.desktop.defaultSession = \"hyprland\" requires chiasson.desktop.hyprland.enable = true."; + } + { + assertion = cfg.defaultSession != "niri" || cfg.niri.enable; + message = "chiasson.desktop.defaultSession = \"niri\" requires chiasson.desktop.niri.enable = true."; + } + { + assertion = cfg.defaultSession != "plasma" || cfg.plasma.enable; + message = "chiasson.desktop.defaultSession = \"plasma\" requires chiasson.desktop.plasma.enable = true."; + } + { + assertion = + cfg.displayManager.variant != "dankgreeter" || cfg.hyprland.enable || cfg.niri.enable; + message = "chiasson.desktop.displayManager.variant = \"dankgreeter\" requires chiasson.desktop.hyprland or chiasson.desktop.niri."; + } + { + assertion = cfg.displayManager.variant != "dankgreeter" || cfg.shell == "dms"; + message = "DankGreeter expects chiasson.desktop.shell = \"dms\" for DMS/matugen sync; use SDDM or enable DMS."; + } + ]; + } + (lib.mkIf guiEnabled { + chiasson.desktop.displayManager.variant = lib.mkDefault ( + if cfg.plasma.enable && !(cfg.hyprland.enable || cfg.niri.enable) then + "sddm" + else if (cfg.hyprland.enable || cfg.niri.enable) && cfg.shell == "dms" then + "dankgreeter" + else + "sddm" + ); + }) + (lib.mkIf (guiEnabled && hmAvailable && cfg.homeManager.bundleWisdom) { + "home-manager".sharedModules = [ self.homeManagerModules.wisdom ]; + }) + (lib.mkIf (dmsEnabled && hmAvailable) { + "home-manager".sharedModules = [ self.homeManagerModules.desktopShellDms ]; + }) + (lib.mkIf (hmAvailable && (dmsEnabled || cfg.niri.enable)) { + "home-manager".extraSpecialArgs = { inherit inputs; }; + }) + ]; + }; +} diff --git a/modules/desktop/plasma/default.nix b/modules/desktop/plasma/default.nix new file mode 100644 index 0000000..e413b2f --- /dev/null +++ b/modules/desktop/plasma/default.nix @@ -0,0 +1,14 @@ +{ ... }: { + flake.nixosModules.desktopPlasma = + { config, lib, pkgs, ... }: + let + cfg = config.chiasson.desktop; + in + { + config = lib.mkIf cfg.plasma.enable { + services.desktopManager.plasma6.enable = true; + environment.etc."xdg/menus/applications.menu".text = + builtins.readFile "${pkgs.kdePackages.plasma-workspace}/etc/xdg/menus/plasma-applications.menu"; + }; + }; +} diff --git a/modules/desktop/shells/dms/default.nix b/modules/desktop/shells/dms/default.nix new file mode 100644 index 0000000..9c4e826 --- /dev/null +++ b/modules/desktop/shells/dms/default.nix @@ -0,0 +1,90 @@ +{ inputs, ... }: { + flake.nixosModules.desktopShellDmsOptions = { lib, ... }: { + options.chiasson.desktop.shells.dms = { + enableGpuTemp = lib.mkOption { + type = lib.types.bool; + default = true; + description = "GPU temp in DMS bar."; + }; + obsidianSnippetsDir = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + description = "Legacy single Obsidian snippets dir for matugen."; + }; + obsidianConfigDirs = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ ]; + description = "Vault `.obsidian/` paths for matugen."; + }; + obsidianSnippetsDirs = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ ]; + description = "Explicit `.obsidian/snippets` paths."; + }; + extraRightBarWidgets = lib.mkOption { + type = lib.types.listOf lib.types.attrs; + default = [ ]; + description = "Extra right-bar widgets (prepended)."; + }; + enableWvkbdToggle = lib.mkEnableOption '' + wvkbd DMS plugin + bar toggle (touch / uConsole). + ''; + enableRbwLockToggle = lib.mkEnableOption '' + rbw vault lock/unlock button in the DMS bar (Bitwarden CLI via rbw). + ''; + rebuildCommand = lib.mkOption { + type = lib.types.nullOr (lib.types.listOf lib.types.str); + default = null; + example = [ "sudo" "nixos-rebuild" "switch" "--flake" ".#14900k" ]; + description = "Command used by DMS nix-monitor widget for rebuild actions."; + }; + }; + }; + + flake.homeManagerModules.desktopShellDms = { + lib, + osConfig ? { }, + ... + }: + let + cfg = lib.attrByPath [ "chiasson" "desktop" "shells" "dms" ] { } osConfig; + selectedShell = lib.attrByPath [ "chiasson" "desktop" "shell" ] null osConfig; + dmsEnabled = selectedShell == "dms"; + hostName = lib.attrByPath [ "networking" "hostName" ] "nixos" osConfig; + rebuildCommand = + if (cfg.rebuildCommand or null) != null then + cfg.rebuildCommand + else + [ "sudo" "nixos-rebuild" "switch" "--flake" ".#${hostName}" ]; + in + { + imports = [ + ./home-manager/default.nix + ]; + + config = lib.mkIf dmsEnabled { + dms.enable = true; + dms.enableGpuTemp = cfg.enableGpuTemp or true; + dms.obsidianSnippetsDir = cfg.obsidianSnippetsDir or null; + dms.obsidianConfigDirs = cfg.obsidianConfigDirs or [ ]; + dms.obsidianSnippetsDirs = cfg.obsidianSnippetsDirs or [ ]; + dms.enableWvkbdToggle = cfg.enableWvkbdToggle or false; + dms.enableRbwLockToggle = cfg.enableRbwLockToggle or false; + dms.extraRightBarWidgets = + (lib.optionals (cfg.enableWvkbdToggle or false) [ + { + id = "wvkbdToggle"; + enabled = true; + } + ]) + ++ (lib.optionals (cfg.enableRbwLockToggle or false) [ + { + id = "rbwLockToggle"; + enabled = true; + } + ]) + ++ (cfg.extraRightBarWidgets or [ ]); + programs.nix-monitor.rebuildCommand = rebuildCommand; + }; + }; +} diff --git a/modules/desktop/shells/dms/home-manager/default.nix b/modules/desktop/shells/dms/home-manager/default.nix new file mode 100644 index 0000000..9a74e97 --- /dev/null +++ b/modules/desktop/shells/dms/home-manager/default.nix @@ -0,0 +1,1110 @@ +{ + inputs, + config, + pkgs, + lib, + ... +}: let + home = config.home.homeDirectory; + cfg = config.dms; + gpuTempEnabled = cfg.enableGpuTemp; + obsidianSnippetsDir = cfg.obsidianSnippetsDir; # legacy single path + obsidianSnippetDirs = + let + snippetsFromConfigDirs = map (d: "${d}/snippets") cfg.obsidianConfigDirs; + explicitSnippetsDirs = + cfg.obsidianSnippetsDirs + ++ lib.optionals (obsidianSnippetsDir != null) [obsidianSnippetsDir]; + in + lib.unique (snippetsFromConfigDirs ++ explicitSnippetsDirs); + + # Clears Quickshell QML bytecode cache so bar plugins reload (stale *.qmlc keeps old UI). + dmsRestartOnceScript = pkgs.writeShellScript "dms-rbw-plugin-restart-once" '' + set -euo pipefail + rt="''${XDG_RUNTIME_DIR:-/run/user/$(id -u)}" + exec 9>"$rt/dms-rbw-plugin-restart.lock" + ${pkgs.util-linux}/bin/flock -n 9 || exit 0 + cache="''${XDG_CACHE_HOME:-''$HOME/.cache}/quickshell/qmlcache" + rm -rf "$cache" + ${pkgs.systemd}/bin/systemctl --user try-restart dms.service 2>/dev/null || true + ''; + +in { + options = { + dms.enable = lib.mkOption { + type = lib.types.bool; + default = false; + description = "DMS Home Manager config from this flake."; + }; + + dms.enableGpuTemp = lib.mkOption { + type = lib.types.bool; + default = true; + description = "GPU temp widget in the bar."; + }; + + # /.obsidian/snippets — unset skips Obsidian matugen output + dms.obsidianSnippetsDir = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + example = "/home/olivier/Documents/Notes/.obsidian/snippets"; + description = "Snippet dir for `matugen.css` (legacy; prefer `obsidianConfigDirs`)."; + }; + + # Each path is a vault’s `.obsidian/` — we write `/snippets/matugen.css`. + dms.obsidianConfigDirs = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = []; + example = [ + "/mnt/zimaos/Obsidian/Home/.obsidian" + ]; + description = "Vault `.obsidian` dirs for matugen CSS."; + }; + + # Or pass `.obsidian/snippets` paths yourself. + dms.obsidianSnippetsDirs = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = []; + example = [ + "/mnt/zimaos/Obsidian/Home/.obsidian/snippets" + ]; + description = "Explicit snippet directories for matugen CSS."; + }; + + # Prepended to the bar’s rightWidgets (shows first). + dms.extraRightBarWidgets = lib.mkOption { + type = lib.types.listOf lib.types.attrs; + default = []; + example = [ + { id = "wvkbdToggle"; enabled = true; } + { id = "rbwLockToggle"; enabled = true; } + ]; + description = "Extra `{ id, enabled, … }` widgets, merged before defaults."; + }; + + dms.enableWvkbdToggle = lib.mkEnableOption '' + Ship wvkbd-toggle plugin into `~/.config/DankMaterialShell/plugins/` (toggle from NixOS `desktop.shells.dms`). + ''; + + dms.enableRbwLockToggle = lib.mkEnableOption '' + Ship rbw-lock-toggle into `~/.config/DankMaterialShell/plugins/rbwLockToggle/` (directory name matches plugin id; Bitwarden vault lock/unlock in the bar). + ''; + }; + + imports = [ + inputs.dms.homeModules.dank-material-shell + inputs.nix-monitor.homeManagerModules.default + ]; + + config = lib.mkIf cfg.enable (lib.mkMerge [ + (lib.mkIf cfg.enableWvkbdToggle { + xdg.configFile."DankMaterialShell/plugins/wvkbd-toggle/plugin.json".source = + ../plugins/wvkbd-toggle/plugin.json; + xdg.configFile."DankMaterialShell/plugins/wvkbd-toggle/WvkbdToggle.qml".source = + ../plugins/wvkbd-toggle/WvkbdToggle.qml; + xdg.configFile."DankMaterialShell/plugins/wvkbd-toggle/WvkbdToggleSettings.qml".source = + ../plugins/wvkbd-toggle/WvkbdToggleSettings.qml; + }) + (lib.mkIf cfg.enableRbwLockToggle { + # Directory name matches plugin id (same idea as NixMonitor / ideapad wvkbdToggle). + xdg.configFile."DankMaterialShell/plugins/rbwLockToggle/plugin.json" = { + source = ../plugins/rbw-lock-toggle/plugin.json; + onChange = "${dmsRestartOnceScript}"; + }; + xdg.configFile."DankMaterialShell/plugins/rbwLockToggle/RbwLockToggleSettings.qml" = { + source = ../plugins/rbw-lock-toggle/RbwLockToggleSettings.qml; + onChange = "${dmsRestartOnceScript}"; + }; + xdg.configFile."DankMaterialShell/plugins/rbwLockToggle/RbwLockToggle.qml" = { + source = ../plugins/rbw-lock-toggle/RbwLockToggle.qml; + onChange = "${dmsRestartOnceScript}"; + }; + }) + { + + # Nix rebuild widget used by DankMaterialShell (via nix-monitor). + # Per-host `rebuildCommand` is set in `hosts/clients//home.nix`. + programs.nix-monitor = { + enable = true; + generationsCommand = [ + "bash" + "-c" + "readlink /nix/var/nix/profiles/system | cut -d- -f2" + ]; + }; + + home.packages = with pkgs; [ + kdePackages.qtdeclarative + kdePackages.kirigami.unwrapped + kdePackages.qtmultimedia + kdePackages.sonnet + ]; + programs.dank-material-shell = { + enable = true; + + systemd = { + enable = true; # Systemd service for auto-start + restartIfChanged = true; # Auto-restart dms.service when dankMaterialShell changes + }; + + # Core features + enableSystemMonitoring = true; # System monitoring widgets (dgop) + dgop.package = inputs.dgop.packages.${pkgs.system}.default; + enableVPN = true; # VPN management widget + enableDynamicTheming = true; # Wallpaper-based theming (matugen) + enableAudioWavelength = true; # Audio visualizer (cava) + enableCalendarEvents = false; # Disable calendar integration (khal/ikhal) + + # Leave `session` at {} — non-empty makes HM rewrite session.json every switch and nukes wallpaper/session state. + settings = { + theme = "dark"; + dynamicTheming = true; + + currentThemeName = "dynamic"; + customThemeFile = ""; + matugenScheme = "scheme-tonal-spot"; + runUserMatugenTemplates = true; + # Stock DMS vesktop matugen output conflicts with our `templates/dank-discord.css` + config.toml entry. + matugenTemplateVesktop = false; + matugenTargetMonitor = ""; + popupTransparency = 1; + dockTransparency = 0.85; + widgetBackgroundColor = "sch"; + widgetColorMode = "default"; + cornerRadius = 10; + use24HourClock = false; + showSeconds = true; + useFahrenheit = false; + nightModeEnabled = false; + animationSpeed = 1; + customAnimationDuration = 500; + wallpaperFillMode = "Fill"; + blurredWallpaperLayer = false; + blurWallpaperOnOverview = false; + + showLauncherButton = true; + showWorkspaceSwitcher = true; + showFocusedWindow = true; + showWeather = true; + showMusic = true; + showClipboard = true; + showCpuUsage = true; + showMemUsage = true; + showCpuTemp = true; + showGpuTemp = gpuTempEnabled; + showSystemTray = true; + } + // lib.optionalAttrs gpuTempEnabled { + selectedGpuIndex = 0; + enabledGpuPciIds = []; + } + // { + showClock = true; + showNotificationButton = true; + showBattery = true; + showControlCenterButton = true; + showCapsLockIndicator = true; + + controlCenterShowNetworkIcon = true; + controlCenterShowBluetoothIcon = true; + controlCenterShowAudioIcon = true; + controlCenterShowVpnIcon = true; + controlCenterShowBrightnessIcon = false; + controlCenterShowMicIcon = false; + controlCenterShowBatteryIcon = false; + controlCenterShowPrinterIcon = false; + + showPrivacyButton = true; + privacyShowMicIcon = false; + privacyShowCameraIcon = false; + privacyShowScreenShareIcon = false; + + controlCenterWidgets = [ + { + id = "volumeSlider"; + enabled = true; + width = 50; + } + { + id = "brightnessSlider"; + enabled = true; + width = 50; + } + { + id = "wifi"; + enabled = true; + width = 50; + } + { + id = "bluetooth"; + enabled = true; + width = 50; + } + { + id = "audioOutput"; + enabled = true; + width = 50; + } + { + id = "audioInput"; + enabled = true; + width = 50; + } + { + id = "nightMode"; + enabled = true; + width = 50; + } + { + id = "darkMode"; + enabled = true; + width = 50; + } + ]; + + showWorkspaceIndex = true; + showWorkspacePadding = false; + workspaceScrolling = false; + showWorkspaceApps = true; + maxWorkspaceIcons = 10; + workspaceAppIconSizeOffset = 1; + workspacesPerMonitor = true; + showOccupiedWorkspacesOnly = false; + dwlShowAllTags = false; + workspaceNameIcons = {}; + + waveProgressEnabled = true; + scrollTitleEnabled = true; + clockCompactMode = false; + focusedWindowCompactMode = false; + runningAppsCompactMode = true; + keyboardLayoutNameCompactMode = false; + runningAppsCurrentWorkspace = false; + runningAppsGroupByApp = true; + centeringMode = "geometric"; + + clockDateFormat = "yyyy-MM-dd"; + lockDateFormat = ""; + mediaSize = 1; + + appLauncherViewMode = "list"; + spotlightModalViewMode = "list"; + sortAppsAlphabetically = false; + appLauncherGridColumns = 4; + spotlightCloseNiriOverview = true; + niriOverviewOverlayEnabled = true; + useAutoLocation = true; + weatherEnabled = true; + + networkPreference = "auto"; + vpnLastConnected = ""; + iconTheme = "System Default"; + + launcherLogoMode = "os"; + launcherLogoCustomPath = ""; + launcherLogoColorOverride = ""; + launcherLogoColorInvertOnMode = false; + launcherLogoBrightness = 0.5; + launcherLogoContrast = 1; + launcherLogoSizeOffset = 0; + + fontFamily = "Inter Variable"; + monoFontFamily = "Fira Code"; + fontWeight = 400; + fontScale = 1; + + notepadUseMonospace = true; + notepadFontFamily = ""; + notepadFontSize = 14; + notepadShowLineNumbers = false; + notepadTransparencyOverride = -1; + notepadLastCustomTransparency = 0.7; + + soundsEnabled = true; + useSystemSoundTheme = false; + soundNewNotification = true; + soundVolumeChanged = true; + soundPluggedIn = true; + + acMonitorTimeout = 0; + acLockTimeout = 0; + acSuspendTimeout = 0; + acSuspendBehavior = 0; + acProfileName = ""; + + batteryMonitorTimeout = 0; + batteryLockTimeout = 0; + batterySuspendTimeout = 0; + batterySuspendBehavior = 0; + batteryProfileName = ""; + + lockBeforeSuspend = false; + loginctlLockIntegration = true; + fadeToLockEnabled = false; + fadeToLockGracePeriod = 5; + launchPrefix = ""; + + brightnessDevicePins = {}; + wifiNetworkPins = {}; + bluetoothDevicePins = {}; + audioInputDevicePins = {}; + audioOutputDevicePins = {}; + + gtkThemingEnabled = false; + qtThemingEnabled = false; + syncModeWithPortal = true; + terminalsAlwaysDark = true; + + showDock = true; + dockAutoHide = true; + dockGroupByApp = true; + dockOpenOnOverview = false; + dockPosition = 1; + dockSpacing = 3; + dockBottomGap = -16; + dockMargin = 2; + dockIconSize = 45; + dockIndicatorStyle = "circle"; + dockBorderEnabled = true; + dockBorderColor = "primary"; + dockBorderOpacity = 0.2; + dockBorderThickness = 2; + + notificationOverlayEnabled = false; + modalDarkenBackground = true; + lockScreenShowPowerActions = true; + enableFprint = false; + maxFprintTries = 15; + lockScreenActiveMonitor = "all"; + lockScreenInactiveColor = "#000000"; + hideBrightnessSlider = false; + + notificationTimeoutLow = 5000; + notificationTimeoutNormal = 5000; + notificationTimeoutCritical = 0; + notificationPopupPosition = 0; + + osdAlwaysShowValue = false; + osdPosition = 5; + osdVolumeEnabled = true; + osdMediaVolumeEnabled = true; + osdBrightnessEnabled = true; + osdIdleInhibitorEnabled = true; + osdMicMuteEnabled = true; + osdCapsLockEnabled = true; + osdPowerProfileEnabled = false; + osdAudioOutputEnabled = true; + + powerActionConfirm = true; + powerActionHoldDuration = 0.5; + powerMenuActions = ["reboot" "logout" "poweroff" "lock" "suspend" "restart"]; + powerMenuDefaultAction = "logout"; + powerMenuGridLayout = false; + + customPowerActionLock = ""; + customPowerActionLogout = ""; + customPowerActionSuspend = ""; + customPowerActionHibernate = ""; + customPowerActionReboot = ""; + customPowerActionPowerOff = ""; + + updaterUseCustomCommand = false; + updaterCustomCommand = ""; + updaterTerminalAdditionalParams = ""; + + displayNameMode = "model"; + + showOnLastDisplay = { + dock = true; + }; + + barConfigs = [ + { + id = "default"; + name = "Main Bar"; + enabled = true; + position = 0; + screenPreferences = ["all"]; + showOnLastDisplay = true; + + leftWidgets = [ + { + id = "launcherButton"; + enabled = true; + } + { + id = "nixMonitor"; + enabled = true; + } + { + id = "cpuUsage"; + enabled = true; + } + { + id = "cpuTemp"; + enabled = true; + } + ] + ++ lib.optionals gpuTempEnabled [ + { + id = "gpuTemp"; + enabled = true; + selectedGpuIndex = 0; + pciId = "10de:1f07"; + } + ] + ++ [ + { + id = "memUsage"; + enabled = true; + } + { + id = "music"; + enabled = false; + } + { + id = "focusedWindow"; + enabled = false; + } + ]; + + centerWidgets = [ + { + id = "privacyIndicator"; + enabled = false; + } + { + id = "workspaceSwitcher"; + enabled = true; + } + { + id = "privacyIndicator"; + enabled = true; + } + ]; + + rightWidgets = cfg.extraRightBarWidgets + ++ [ + { + id = "systemTray"; + enabled = true; + } + { + id = "notificationButton"; + enabled = true; + } + { + id = "battery"; + enabled = true; + } + { + id = "controlCenterButton"; + enabled = true; + } + { + id = "clock"; + enabled = true; + clockCompactMode = false; + } + ]; + + spacing = 0; + innerPadding = 4; + bottomGap = -4; + transparency = 0; + widgetTransparency = 0.45; + squareCorners = true; + noBackground = false; + gothCornersEnabled = true; + gothCornerRadiusOverride = false; + gothCornerRadiusValue = 12; + borderEnabled = false; + borderColor = "primary"; + borderOpacity = 1; + borderThickness = 1; + widgetOutlineEnabled = true; + widgetOutlineColor = "secondary"; + widgetOutlineOpacity = 0.25; + widgetOutlineThickness = 2; + fontScale = 1; + autoHide = false; + autoHideDelay = 250; + openOnOverview = false; + visible = true; + popupGapsAuto = true; + popupGapsManual = 22; + maximizeDetection = true; + } + ]; + + configVersion = 2; + }; + }; + + # DMS / matugen custom templates. + # + # DMS docs (custom templates): https://danklinux.com/docs/dankmaterialshell/application-themes#custom-matugen-templates + xdg.configFile."matugen/config.toml".text = + '' + [config] + + [templates.hyprland] + input_path = '${home}/.config/matugen/templates/hyprland-colors.conf' + output_path = '${home}/.config/hypr/colors.conf' + + [templates.kdeglobals] + input_path = '${home}/.config/matugen/templates/kdeglobals.conf' + output_path = '${home}/.config/kdeglobals' + + [templates.ohmyposh] + input_path = '${home}/.config/matugen/templates/ohmyposh-theme.omp.json' + output_path = '${home}/.config/oh-my-posh/theme.omp.json' + + [templates.dank-discord] + input_path = '${home}/.config/matugen/templates/dank-discord.css' + output_path = '${home}/.config/vesktop/themes/dank-discord.css' + '' + + lib.optionalString (obsidianSnippetDirs != []) ( + lib.concatStringsSep "\n" ( + lib.lists.imap0 (i: snippetDir: '' + + [templates.Obsidian${toString i}] + input_path = '${home}/.config/matugen/templates/obsidian-minimal-matugen-colors.css' + output_path = '${snippetDir}/matugen.css' + '') obsidianSnippetDirs + ) + ) + ; + + # Obsidian snippet dirs for matugen: mkdir here instead of tmpfiles (no chmod fights on weird mounts). + home.activation.dmsEnsureObsidianSnippetDirs = lib.hm.dag.entryAfter ["writeBoundary"] ( + let + snippetDirs = lib.unique obsidianSnippetDirs; + escapedSnippetDirs = map lib.escapeShellArg snippetDirs; + in + '' + for snippetDir in ${lib.concatStringsSep " " escapedSnippetDirs}; do + parentDir="$(dirname "$snippetDir")" + + if [ -d "$snippetDir" ]; then + continue + fi + + if [ ! -d "$parentDir" ] || [ ! -w "$parentDir" ]; then + echo "dms: skipping Obsidian snippet dir '$snippetDir' (parent missing or not writable)" >&2 + continue + fi + + mkdir -p "$snippetDir" + done + '' + ); + + xdg.configFile."matugen/templates/dank-discord.css".source = ../templates/dank-discord.css; + + # matugen template → Hyprland colors file sourced at runtime. + # Your Hyprland module includes: `source = ~/.config/hypr/colors.conf` + # (see `modules/desktop/hyprland/home-manager/appearance.nix`). + xdg.configFile."matugen/templates/hyprland-colors.conf".text = '' + # Hyprland colors generated by DMS/matugen + # Keep `.default` so DMS can swap to `.dark` when "Always use Dark Theme" is enabled. + + general { + col.active_border = rgb({{colors.primary.default.hex_stripped}}) rgb({{colors.secondary.default.hex_stripped}}) rgb({{colors.tertiary.default.hex_stripped}}) + col.inactive_border = rgba({{colors.surface_variant.default.hex_stripped}}ee) + } + + decoration { + shadow { + color = rgba({{colors.shadow.default.hex_stripped}}cc) + } + } + + # Hyprbars colors + plugin { + hyprbars { + bar_color = rgba({{colors.surface.default.hex_stripped}}ff) + col.text = rgba({{colors.on_surface.default.hex_stripped}}ff) + } + } + ''; + + # matugen template → KDE globals file used by KDE/Qt apps (e.g. Dolphin). + # Uses DMS-provided matugen color variables; keep `.default` for mode-aware colors. + xdg.configFile."matugen/templates/kdeglobals.conf".text = '' + [General] + ColorScheme=DMSDynamic + TerminalApplication=kitty + TerminalService=kitty.desktop + + # Keep Qt/KDE app icons on WhiteSur (this is what Dolphin reads via KDE config) + [Icons] + Theme=WhiteSur-dark + + [Colors:Window] + BackgroundNormal=#{{colors.background.default.hex_stripped}} + BackgroundAlternate=#{{colors.surface.default.hex_stripped}} + DecorationFocus=#{{colors.primary.default.hex_stripped}} + DecorationHover=#{{colors.secondary.default.hex_stripped}} + ForegroundNormal=#{{colors.on_surface.default.hex_stripped}} + ForegroundInactive=#{{colors.on_surface_variant.default.hex_stripped}} + ForegroundActive=#{{colors.on_surface.default.hex_stripped}} + ForegroundLink=#{{colors.primary.default.hex_stripped}} + ForegroundVisited=#{{colors.tertiary.default.hex_stripped}} + ForegroundNegative=#{{colors.error.default.hex_stripped}} + ForegroundNeutral=#{{colors.secondary.default.hex_stripped}} + ForegroundPositive=#{{colors.primary.default.hex_stripped}} + + [Colors:View] + BackgroundNormal=#{{colors.background.default.hex_stripped}} + BackgroundAlternate=#{{colors.surface.default.hex_stripped}} + DecorationFocus=#{{colors.primary.default.hex_stripped}} + DecorationHover=#{{colors.secondary.default.hex_stripped}} + ForegroundNormal=#{{colors.on_surface.default.hex_stripped}} + ForegroundInactive=#{{colors.on_surface_variant.default.hex_stripped}} + ForegroundActive=#{{colors.on_surface.default.hex_stripped}} + ForegroundLink=#{{colors.primary.default.hex_stripped}} + ForegroundVisited=#{{colors.tertiary.default.hex_stripped}} + ForegroundNegative=#{{colors.error.default.hex_stripped}} + ForegroundNeutral=#{{colors.secondary.default.hex_stripped}} + ForegroundPositive=#{{colors.primary.default.hex_stripped}} + + [Colors:Button] + BackgroundNormal=#{{colors.surface.default.hex_stripped}} + BackgroundAlternate=#{{colors.surface_variant.default.hex_stripped}} + DecorationFocus=#{{colors.primary.default.hex_stripped}} + DecorationHover=#{{colors.secondary.default.hex_stripped}} + ForegroundNormal=#{{colors.on_surface.default.hex_stripped}} + ForegroundInactive=#{{colors.on_surface_variant.default.hex_stripped}} + ForegroundActive=#{{colors.on_surface.default.hex_stripped}} + ForegroundLink=#{{colors.primary.default.hex_stripped}} + ForegroundVisited=#{{colors.tertiary.default.hex_stripped}} + ForegroundNegative=#{{colors.error.default.hex_stripped}} + ForegroundNeutral=#{{colors.secondary.default.hex_stripped}} + ForegroundPositive=#{{colors.primary.default.hex_stripped}} + + [Colors:Selection] + BackgroundNormal=#{{colors.primary.default.hex_stripped}} + BackgroundAlternate=#{{colors.primary_container.default.hex_stripped}} + DecorationFocus=#{{colors.primary.default.hex_stripped}} + DecorationHover=#{{colors.secondary.default.hex_stripped}} + ForegroundNormal=#{{colors.on_primary.default.hex_stripped}} + ForegroundInactive=#{{colors.on_primary.default.hex_stripped}} + ForegroundActive=#{{colors.on_primary.default.hex_stripped}} + ForegroundLink=#{{colors.on_primary.default.hex_stripped}} + ForegroundVisited=#{{colors.on_primary.default.hex_stripped}} + ForegroundNegative=#{{colors.on_primary.default.hex_stripped}} + ForegroundNeutral=#{{colors.on_primary.default.hex_stripped}} + ForegroundPositive=#{{colors.on_primary.default.hex_stripped}} + + [Colors:Tooltip] + BackgroundNormal=#{{colors.surface_variant.default.hex_stripped}} + BackgroundAlternate=#{{colors.surface.default.hex_stripped}} + DecorationFocus=#{{colors.primary.default.hex_stripped}} + DecorationHover=#{{colors.secondary.default.hex_stripped}} + ForegroundNormal=#{{colors.on_surface.default.hex_stripped}} + ForegroundInactive=#{{colors.on_surface_variant.default.hex_stripped}} + ForegroundActive=#{{colors.on_surface.default.hex_stripped}} + ForegroundLink=#{{colors.primary.default.hex_stripped}} + ForegroundVisited=#{{colors.tertiary.default.hex_stripped}} + ForegroundNegative=#{{colors.error.default.hex_stripped}} + ForegroundNeutral=#{{colors.secondary.default.hex_stripped}} + ForegroundPositive=#{{colors.primary.default.hex_stripped}} + + [WM] + activeBackground=#{{colors.background.default.hex_stripped}} + activeBlend=#{{colors.background.default.hex_stripped}} + activeForeground=#{{colors.on_surface.default.hex_stripped}} + inactiveBackground=#{{colors.background.default.hex_stripped}} + inactiveBlend=#{{colors.background.default.hex_stripped}} + inactiveForeground=#{{colors.on_surface_variant.default.hex_stripped}} + ''; + + # matugen → Oh My Posh JSON; hex in the base theme swapped for matugen tokens. + xdg.configFile."matugen/templates/ohmyposh-theme.omp.json".text = '' + { + "$schema": "https://raw.githubusercontent.com/JanDeDobbeleer/oh-my-posh/main/themes/schema.json", + "blocks": [ + { + "alignment": "left", + "segments": [ + { + "background": "#3A456E", + "foreground": "#ffbebc", + "leading_diamond": "\ue0b6", + "style": "diamond", + "template": "\u007B\u007B .UserName \u007D\u007D@\u007B\u007B .HostName \u007D\u007D <#ffffff>on", + "type": "session" + }, + { + "type": "os", + "style": "diamond", + "background": "#3A456E", + "foreground": "#ffffff", + "trailing_diamond": "\ue0b4", + "template": " \u007B\u007B .Icon \u007D\u007D " + }, + {"type": "session", + "style": "diamond", + "background": "#3A456E", + "foreground": "#ffbebc", + "template": "\u007B\u007B if .SSHSession \u007D\u007Dvia SSH\u007B\u007B end \u007D\u007D", + "trailing_diamond": "\ue0b4", + "type": "session" + }, + { + "background": "#3A456E", + "foreground": "#bc93ff", + "leading_diamond": "\ue0b6", + "properties": { + "time_format": "Monday <#ffffff>at 3:04 PM" + }, + "style": "diamond", + "trailing_diamond": "\ue0b4", + "template": "\u007B\u007B .CurrentDate | date .Format \u007D\u007D", + "type": "time" + }, + + + { + "background": "#3A456E", + "foreground": "#43CCEA", + "foreground_templates": [ + "\u007B\u007B if or (.Working.Changed) (.Staging.Changed) \u007D\u007D#FF9248\u007B\u007B end \u007D\u007D", + "\u007B\u007B if and (gt .Ahead 0) (gt .Behind 0) \u007D\u007D#ff4500\u007B\u007B end \u007D\u007D", + "\u007B\u007B if gt .Ahead 0 \u007D\u007D#B388FF\u007B\u007B end \u007D\u007D", + "\u007B\u007B if gt .Behind 0 \u007D\u007D#B388FF\u007B\u007B end \u007D\u007D" + ], + "leading_diamond": " \ue0b6", + "options": { + "branch_template": "\u007B\u007B trunc 25 .Branch \u007D\u007D", + "fetch_status": true, + "fetch_upstream_icon": true + }, + "style": "diamond", + "template": " \u007B\u007B .UpstreamIcon \u007D\u007D\u007B\u007B .HEAD \u007D\u007D\u007B\u007Bif .BranchStatus \u007D\u007D \u007B\u007B .BranchStatus \u007D\u007D\u007B\u007B end \u007D\u007D\u007B\u007B if .Working.Changed \u007D\u007D \uf044 \u007B\u007B .Working.String \u007D\u007D\u007B\u007B end \u007D\u007D\u007B\u007B if and (.Working.Changed) (.Staging.Changed) \u007D\u007D |\u007B\u007B end \u007D\u007D\u007B\u007B if .Staging.Changed \u007D\u007D \uf046 \u007B\u007B .Staging.String \u007D\u007D\u007B\u007B end \u007D\u007D\u007B\u007B if gt .StashCount 0 \u007D\u007D \ueb4b \u007B\u007B .StashCount \u007D\u007D\u007B\u007B end \u007D\u007D ", + "trailing_diamond": "\ue0b4", + "type": "git" + }, + { + "background": "#3A456E", + "foreground": "#E4F34A", + "leading_diamond": " \ue0b6", + "options": { + "fetch_version": false + }, + "style": "diamond", + "template": "\ue235 \u007B\u007B if .Error \u007D\u007D\u007B\u007B .Error \u007D\u007D\u007B\u007B else \u007D\u007D\u007B\u007B if .Venv \u007D\u007D\u007B\u007B .Venv \u007D\u007D \u007B\u007B end \u007D\u007D\u007B\u007B .Full \u007D\u007D\u007B\u007B end \u007D\u007D", + "trailing_diamond": "\ue0b4", + "type": "python" + }, + { + "background": "#3A456E", + "foreground": "#7FD5EA", + "leading_diamond": " \ue0b6", + "options": { + "fetch_version": false + }, + "style": "diamond", + "template": "\ue626\u007B\u007B if .Error \u007D\u007D\u007B\u007B .Error \u007D\u007D\u007B\u007B else \u007D\u007D\u007B\u007B .Full \u007D\u007D\u007B\u007B end \u007D\u007D", + "trailing_diamond": "\ue0b4", + "type": "go" + }, + { + "background": "#3A456E", + "foreground": "#42E66C", + "leading_diamond": " \ue0b6", + "options": { + "fetch_version": false + }, + "style": "diamond", + "template": "\ue718\u007B\u007B if .PackageManagerIcon \u007D\u007D\u007B\u007B .PackageManagerIcon \u007D\u007D \u007B\u007B end \u007D\u007D\u007B\u007B .Full \u007D\u007D", + "trailing_diamond": "\ue0b4", + "type": "node" + }, + { + "background": "#3A456E", + "foreground": "#E64747", + "leading_diamond": " \ue0b6", + "options": { + "fetch_version": false + }, + "style": "diamond", + "template": "\ue791\u007B\u007B if .Error \u007D\u007D\u007B\u007B .Error \u007D\u007D\u007B\u007B else \u007D\u007D\u007B\u007B .Full \u007D\u007D\u007B\u007B end \u007D\u007D", + "trailing_diamond": "\ue0b4", + "type": "ruby" + }, + { + "background": "#3A456E", + "foreground": "#E64747", + "leading_diamond": " \ue0b6", + "options": { + "fetch_version": false + }, + "style": "diamond", + "template": "\ue738\u007B\u007B if .Error \u007D\u007D\u007B\u007B .Error \u007D\u007D\u007B\u007B else \u007D\u007D\u007B\u007B .Full \u007D\u007D\u007B\u007B end \u007D\u007D", + "trailing_diamond": "\ue0b4", + "type": "java" + }, + { + "background": "#3A456E", + "foreground": "#9B6BDF", + "leading_diamond": " \ue0b6", + "options": { + "fetch_version": false + }, + "style": "diamond", + "template": "\ue624\u007B\u007B if .Error \u007D\u007D\u007B\u007B .Error \u007D\u007D\u007B\u007B else \u007D\u007D\u007B\u007B .Full \u007D\u007D\u007B\u007B end \u007D\u007D ", + "trailing_diamond": "\ue0b4", + "type": "julia" + }, + { + "type": "php", + "style": "diamond", + "foreground": "#ffffff", + "background": "#4063D8", + "leading_diamond": " \ue0b6", + "options": { + "fetch_version": false + }, + "template": "\ue73d \u007B\u007B .Full \u007D\u007D ", + "trailing_diamond": "\ue0b4" + }, + { + "background": "#3A456E", + "foreground": "#9B6BDF", + "foreground_templates": [ + "\u007B\u007Bif eq \"Charging\" .State.String\u007D\u007D#40c4ff\u007B\u007Bend\u007D\u007D", + "\u007B\u007Bif eq \"Discharging\" .State.String\u007D\u007D#ff5722\u007B\u007Bend\u007D\u007D", + "\u007B\u007Bif eq \"Full\" .State.String\u007D\u007D#4caf50\u007B\u007Bend\u007D\u007D" + ], + "leading_diamond": " \ue0b6", + "options": { + "charged_icon": "• ", + "charging_icon": "\u21e1 ", + "discharging_icon": "\u21e3 " + }, + "style": "diamond", + "template": "\u007B\u007B if not .Error \u007D\u007D\u007B\u007B .Icon \u007D\u007D\u007B\u007B .Percentage \u007D\u007D\u007B\u007B end \u007D\u007D\u007B\u007B .Error \u007D\u007D", + "trailing_diamond": "\ue0b4", + "type": "battery" + } + ], + "type": "prompt" + }, + { + "alignment": "right", + "segments": [ + { + "background": "#3A456E", + "foreground": "#AEA4BF", + "leading_diamond": "\ue0b6", + "options": { + "style": "austin", + "threshold": 150 + }, + "style": "diamond", + "template": "\u007B\u007B .FormattedMs \u007D\u007D", + "trailing_diamond": "\ue0b4 ", + "type": "executiontime" + } + ], + "type": "prompt" + }, + { + "alignment": "left", + "newline": true, + "segments": [ + { + "background": "#3A456E", + "foreground": "#3EC669", + "leading_diamond": "\ue0b6", + "properties": { + "style": "agnoster_full", + "cycle": [ + "#3EC669,#3A456E", + "#43CCEA,#3A456E", + "#E4F34A,#3A456E", + "#9B6BDF,#3A456E" + ], + "cycle_folder_separator": true + }, + "style": "diamond", + "template": "\ue5ff \u007B\u007B .Path \u007D\u007D", + "trailing_diamond": "\ue0b4", + "type": "path" + }, + { + "background": "#3A456E", + "foreground": "#ffbebc", + "leading_diamond": "\ue0b6", + "style": "diamond", + "template": "\ue602", + "trailing_diamond": "\ue0b4", + "type": "text" + } + ], + "type": "prompt" + } + ], + "final_space": true, + "version": 4 + } + ''; + + # matugen template → Obsidian CSS snippet (Minimal-compatible variables). + # + # Upstream template: + # https://raw.githubusercontent.com/Simorg2002/obsidian-matugen-template/refs/heads/main/obsidian-minimal-matugen-colors.css + xdg.configFile."matugen/templates/obsidian-minimal-matugen-colors.css".text = '' + .theme-dark { + --accent-h:calc({{colors.primary.dark.hue}}/255*360); + --accent-s:calc({{colors.primary.dark.saturation}}/255*100%); + --accent-l:calc({{colors.primary.dark.lightness}}/255*100%); + + --text-normal:{{colors.on_surface.dark.hex}}; + --text-muted:{{colors.on_surface_variant.dark.hex}}; + --text-faint:{{colors.outline.dark.hex}}; + + --background-primary:{{colors.surface.dark.hex}}; /* editor and right ribbon */ + --background-secondary:{{colors.surface_dim.dark.hex}}; /* left ribbon */ + --background-modifier-hover:{{colors.secondary_container.dark.hex}}; + --background-modifier-active-hover:{{colors.secondary.dark.hex}}; + --background-modifier-message:{{colors.surface_dim.dark.hex}}; + --background-modifier-form-field:{{colors.primary_container.dark.hex}}; + + --ribbon-background:{{colors.surface.dark.hex}}; + --divider-color:{{colors.secondary_container.dark.hex}}; + --scrollbar-thumb-bg:{{colors.on_secondary_container.dark.hex}}; + --status-bar-border-color:{{colors.secondary_container.dark.hex}}; + --status-bar-background:{{colors.surface.dark.hex}}; + --titlebar-background:{{colors.surface.dark.hex}}; + --titlebar-text-color:{{colors.on_surface.dark.hex}}; + + --nav-item-color:{{colors.on_surface_variant.dark.hex}}; + --nav-item-color-active:{{colors.on_primary_container.dark.hex}}; + --nav-item-background-active:{{colors.primary_container.dark.hex}}; + --nav-item-color-hover:{{colors.on_secondary_container.dark.hex}}; + --nav-item-background-hover:{{colors.secondary_container.dark.hex}}; + + --tab-background-active:{{colors.primary_container.dark.hex}}; + --tab-text-color-active:{{colors.on_primary_container.dark.hex}}; + --tab-text-color:{{colors.on_surface_variant.dark.hex}}; + + --icon-color:{{colors.on_surface.dark.hex}}; + + --h1-color:{{colors.primary.dark.hex}}; + --h2-color: color-mix(in srgb, {{colors.primary.dark.hex}}, {{colors.tertiary.dark.hex}}); + --h3-color:{{colors.tertiary.dark.hex}}; + --h4-color: color-mix(in srgb, {{colors.primary.dark.hex}}, {{colors.secondary.dark.hex}}); + --h5-color:{{colors.secondary.dark.hex}}; + --h6-color: color-mix(in srgb, {{colors.tertiary.dark.hex}}, {{colors.secondary.dark.hex}}); + + + } + + + /* add parts not accessible without specific selector?*/ + .theme-dark .sidebar-toggle-button.mod-left { + color:{{colors.on_surface.dark.hex}}; + background:{{colors.surface.dark.hex}}; + } + .theme-dark .sidebar-toggle-button.mod-right { + color:{{colors.on_surface.dark.hex}}; + background:{{colors.surface.dark.hex}}; + } + + .theme-dark .workspace-tab-header-container{ + background:{{colors.surface.dark.hex}}; + } + + + /* copy for light mode */ + .theme-light { + --accent-h:calc({{colors.primary.light.hue}}/255*360); + --accent-s:calc({{colors.primary.light.saturation}}/255*100%); + --accent-l:calc({{colors.primary.light.lightness}}/255*100%); + + --text-normal:{{colors.on_surface.light.hex}}; + --text-muted:{{colors.on_surface_variant.light.hex}}; + --text-faint:{{colors.outline.light.hex}}; + + --background-primary:{{colors.surface.light.hex}}; /* editor and right ribbon */ + --background-secondary:{{colors.surface_dim.light.hex}}; /* left ribbon */ + --background-modifier-hover:{{colors.secondary_container.light.hex}}; + --background-modifier-active-hover:{{colors.secondary.light.hex}}; + --background-modifier-message:{{colors.surface_dim.light.hex}}; + --background-modifier-form-field:{{colors.primary_container.light.hex}}; + + --ribbon-background:{{colors.surface.light.hex}}; + --divider-color:{{colors.secondary_container.light.hex}}; + --scrollbar-thumb-bg:{{colors.on_secondary_container.light.hex}}; + --status-bar-border-color:{{colors.secondary_container.light.hex}}; + --status-bar-background:{{colors.surface.light.hex}}; + --titlebar-background:{{colors.surface.light.hex}}; + --titlebar-text-color:{{colors.on_surface.light.hex}}; + + --nav-item-color:{{colors.on_surface_variant.light.hex}}; + --nav-item-color-active:{{colors.on_primary_container.light.hex}}; + --nav-item-background-active:{{colors.primary_container.light.hex}}; + --nav-item-color-hover:{{colors.on_secondary_container.light.hex}}; + --nav-item-background-hover:{{colors.secondary_container.light.hex}}; + + --tab-background-active:{{colors.primary_container.light.hex}}; + --tab-text-color-active:{{colors.on_primary_container.light.hex}}; + --tab-text-color:{{colors.on_surface_variant.light.hex}}; + + --icon-color:{{colors.on_surface.light.hex}}; + + --h1-color:{{colors.primary.light.hex}}; + --h2-color: color-mix(in srgb, {{colors.primary.light.hex}}, {{colors.tertiary.light.hex}}); + --h3-color:{{colors.tertiary.light.hex}}; + --h4-color: color-mix(in srgb, {{colors.primary.light.hex}}, {{colors.secondary.light.hex}}); + --h5-color:{{colors.secondary.light.hex}}; + --h6-color: color-mix(in srgb, {{colors.tertiary.light.hex}}, {{colors.secondary.light.hex}}); + + + } + + + /* add parts not accessible without specific selector?*/ + .theme-light .sidebar-toggle-button.mod-left { + color:{{colors.on_surface.light.hex}}; + background:{{colors.surface.light.hex}}; + } + .theme-light .sidebar-toggle-button.mod-right { + color:{{colors.on_surface.light.hex}}; + background:{{colors.surface.light.hex}}; + } + + .theme-light .workspace-tab-header-container{ + background:{{colors.surface.light.hex}}; + } + + /* Hide window controls (frameless titlebar buttons) */ + div[aria-label="Close window"], + div[aria-label="Minimize"], + div[aria-label="Restore down"], + div[aria-label="Maximize"]{ + display: none !important; + } + + /* Remove the extra right-side spacing reserved for those buttons */ + .is-hidden-frameless:not(.is-fullscreen) .workspace-tabs.mod-top-right-space .workspace-tab-header-container{ + padding-right: 0px !important; + } + + .is-hidden-frameless:not(.is-fullscreen) .workspace-tabs.mod-top-right-space .workspace-tab-header-container:after{ + display: none !important; + } + ''; + } + (lib.mkIf (lib.attrByPath [ "chiasson" "home" "shell" "ohMyPosh" "enable" ] false config) { + # Same path as matugen `[templates.ohmyposh]` output in `programs.dank-material-shell` config. + programs.oh-my-posh = { + useTheme = lib.mkForce null; + configFile = lib.mkForce "${config.xdg.configHome}/oh-my-posh/theme.omp.json"; + }; + }) + ]); +} diff --git a/modules/desktop/shells/dms/plugins/rbw-lock-toggle/RbwLockToggle.qml b/modules/desktop/shells/dms/plugins/rbw-lock-toggle/RbwLockToggle.qml new file mode 100644 index 0000000..c36bc2d --- /dev/null +++ b/modules/desktop/shells/dms/plugins/rbw-lock-toggle/RbwLockToggle.qml @@ -0,0 +1,80 @@ +import QtQuick +import Quickshell +import Quickshell.Io +import qs.Common +import qs.Services +import qs.Widgets +import qs.Modules.Plugins + +PluginComponent { + id: root + + property bool vaultUnlocked: false + + function refreshLockState() { + if (!statusProcess.running) + statusProcess.exec(["rbw", "unlocked"]); + } + + function toggleLock() { + if (root.vaultUnlocked) { + Quickshell.execDetached(["rbw", "lock"]); + } else { + Quickshell.execDetached(["rbw", "unlock"]); + } + } + + pillClickAction: () => root.toggleLock() + + Component.onCompleted: refreshLockState() + + Timer { + interval: 2500 + repeat: true + running: true + onTriggered: root.refreshLockState() + } + + Process { + id: statusProcess + command: ["rbw", "unlocked"] + onExited: (exitCode, _exitStatus) => { + root.vaultUnlocked = exitCode === 0; + } + } + + horizontalBarPill: Component { + MouseArea { + implicitWidth: iconH.implicitWidth + implicitHeight: iconH.implicitHeight + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: root.toggleLock() + + DankIcon { + id: iconH + name: root.vaultUnlocked ? "lock_open_right" : "lock" + size: root.iconSize + color: parent.containsMouse ? Theme.primary : Theme.surfaceText + } + } + } + + verticalBarPill: Component { + MouseArea { + implicitWidth: iconV.implicitWidth + implicitHeight: iconV.implicitHeight + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: root.toggleLock() + + DankIcon { + id: iconV + name: root.vaultUnlocked ? "lock_open_right" : "lock" + size: root.iconSize + color: parent.containsMouse ? Theme.primary : Theme.surfaceText + anchors.horizontalCenter: parent.horizontalCenter + } + } + } +} diff --git a/modules/desktop/shells/dms/plugins/rbw-lock-toggle/RbwLockToggleSettings.qml b/modules/desktop/shells/dms/plugins/rbw-lock-toggle/RbwLockToggleSettings.qml new file mode 100644 index 0000000..6e879e0 --- /dev/null +++ b/modules/desktop/shells/dms/plugins/rbw-lock-toggle/RbwLockToggleSettings.qml @@ -0,0 +1,16 @@ +import QtQuick +import qs.Common +import qs.Modules.Plugins + +PluginSettings { + id: root + pluginId: "rbwLockToggle" + + StyledText { + width: parent.width + text: "Shows rbw vault state with a closed lock (locked) or open lock (unlocked). Click to run `rbw unlock` (pinentry) or `rbw lock`. Requires `rbw` on PATH in the DMS session and a working pinentry." + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + wrapMode: Text.WordWrap + } +} diff --git a/modules/desktop/shells/dms/plugins/rbw-lock-toggle/plugin.json b/modules/desktop/shells/dms/plugins/rbw-lock-toggle/plugin.json new file mode 100644 index 0000000..b0a1a85 --- /dev/null +++ b/modules/desktop/shells/dms/plugins/rbw-lock-toggle/plugin.json @@ -0,0 +1,13 @@ +{ + "id": "rbwLockToggle", + "name": "Bitwarden (rbw) lock", + "description": "Bar control for rbw vault: locked/unlocked padlock; click to unlock or lock", + "version": "1.0.0", + "author": "NixOS-V2", + "type": "widget", + "capabilities": ["dankbar-widget"], + "component": "./RbwLockToggle.qml", + "settings": "./RbwLockToggleSettings.qml", + "icon": "lock_open", + "permissions": ["settings_read"] +} diff --git a/modules/desktop/shells/dms/plugins/wvkbd-toggle/WvkbdToggle.qml b/modules/desktop/shells/dms/plugins/wvkbd-toggle/WvkbdToggle.qml new file mode 100644 index 0000000..4986df5 --- /dev/null +++ b/modules/desktop/shells/dms/plugins/wvkbd-toggle/WvkbdToggle.qml @@ -0,0 +1,51 @@ +import QtQuick +import Quickshell +import qs.Common +import qs.Services +import qs.Widgets +import qs.Modules.Plugins + +PluginComponent { + id: root + + function toggleKeyboard() { + Quickshell.execDetached(["sh", "-c", "pkill -SIGRTMIN -x wvkbd-mobintl"]); + } + + pillClickAction: () => root.toggleKeyboard() + + horizontalBarPill: Component { + MouseArea { + implicitWidth: icon.implicitWidth + implicitHeight: icon.implicitHeight + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: root.toggleKeyboard() + + DankIcon { + id: icon + name: "keyboard" + size: Theme.iconSize + color: parent.containsMouse ? Theme.primary : Theme.surfaceText + } + } + } + + verticalBarPill: Component { + MouseArea { + implicitWidth: iconV.implicitWidth + implicitHeight: iconV.implicitHeight + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: root.toggleKeyboard() + + DankIcon { + id: iconV + name: "keyboard" + size: Theme.iconSize + color: parent.containsMouse ? Theme.primary : Theme.surfaceText + anchors.horizontalCenter: parent.horizontalCenter + } + } + } +} diff --git a/modules/desktop/shells/dms/plugins/wvkbd-toggle/WvkbdToggleSettings.qml b/modules/desktop/shells/dms/plugins/wvkbd-toggle/WvkbdToggleSettings.qml new file mode 100644 index 0000000..d7a2fed --- /dev/null +++ b/modules/desktop/shells/dms/plugins/wvkbd-toggle/WvkbdToggleSettings.qml @@ -0,0 +1,16 @@ +import QtQuick +import qs.Common +import qs.Modules.Plugins + +PluginSettings { + id: root + pluginId: "wvkbdToggle" + + StyledText { + width: parent.width + text: "Click the keyboard icon in the bar to show or hide the on-screen keyboard (wvkbd)." + font.pixelSize: Theme.fontSizeSmall + color: Theme.surfaceVariantText + wrapMode: Text.WordWrap + } +} diff --git a/modules/desktop/shells/dms/plugins/wvkbd-toggle/plugin.json b/modules/desktop/shells/dms/plugins/wvkbd-toggle/plugin.json new file mode 100644 index 0000000..e99fe2d --- /dev/null +++ b/modules/desktop/shells/dms/plugins/wvkbd-toggle/plugin.json @@ -0,0 +1,13 @@ +{ + "id": "wvkbdToggle", + "name": "Virtual Keyboard Toggle", + "description": "Bar button to show/hide the wvkbd on-screen keyboard (for touch/tablet)", + "version": "1.0.0", + "author": "NixOS-V2", + "type": "widget", + "capabilities": ["dankbar-widget"], + "component": "./WvkbdToggle.qml", + "settings": "./WvkbdToggleSettings.qml", + "icon": "keyboard", + "permissions": ["settings_read"] +} diff --git a/modules/desktop/shells/dms/templates/dank-discord.css b/modules/desktop/shells/dms/templates/dank-discord.css new file mode 100644 index 0000000..b9ad27f --- /dev/null +++ b/modules/desktop/shells/dms/templates/dank-discord.css @@ -0,0 +1,119 @@ +/** + * Vesktop / Vencord theme — matugen + DMS wallpaper colors. + * Derived from pywal-vencord-style mapping (NixOS-New templates/colors-discord.css). + */ + +* { + /* Surfaces */ + --background-primary: {{colors.background.default.hex}}; + --background-secondary: {{colors.surface.default.hex}}; + --background-tertiary: {{colors.surface_dim.default.hex}}; + + --background-primary-alt: {{colors.background.default.hex}}; + --background-secondary-alt: {{colors.surface.default.hex}}; + --background-tertiary-alt: {{colors.surface_dim.default.hex}}; + + --channeltextarea-background: {{colors.surface.default.hex}}; + --custom-channel-members-bg: {{colors.surface.default.hex}}; + + --profile-gradient-primary-color: {{colors.surface.default.hex}}; + --profile-gradient-secondary-color: {{colors.surface_variant.default.hex}}; + + --__header-bar-background: {{colors.surface.default.hex}} !important; + + --bg-base-tertiary: {{colors.background.default.hex}}; + + --card-primary-bg: color-mix(in srgb, {{colors.surface.default.hex}}, black 20%); + --input-background: color-mix(in srgb, {{colors.surface.default.hex}}, black 20%); + --autocomplete-bg: color-mix(in srgb, {{colors.surface.default.hex}}, black 20%); + --background-nested-floating: color-mix(in srgb, {{colors.surface.default.hex}}, black 20%); + --background-floating: color-mix(in srgb, {{colors.surface.default.hex}}, black 20%); + --scrollbar-auto-track: color-mix(in srgb, {{colors.surface.default.hex}}, black 20%); + --scrollbar-thin-track: color-mix(in srgb, {{colors.surface.default.hex}}, black 20%); + + --border-subtle: color-mix(in srgb, {{colors.surface.default.hex}}, black 20%); + --background-base-lowest: color-mix(in srgb, {{colors.surface.default.hex}}, black 20%); + --background-surface-high: color-mix(in srgb, {{colors.surface.default.hex}}, black 20%); + + --button-secondary-background: color-mix(in srgb, {{colors.surface.default.hex}}, black 30%); + --background-surface-higher: color-mix(in srgb, {{colors.surface.default.hex}}, black 30%); + --background-base-lower: color-mix(in srgb, {{colors.surface.default.hex}}, black 35%); + + --background-message-hover: color-mix(in srgb, {{colors.surface.default.hex}}, black 40%); + --button-secondary-background-hover: color-mix(in srgb, {{colors.surface.default.hex}}, black 40%); + --background-base-low: color-mix(in srgb, {{colors.surface.default.hex}}, black 40%); + --background-surface-highest: color-mix(in srgb, {{colors.surface.default.hex}}, black 40%); + --chat-background-default: color-mix(in srgb, {{colors.surface.default.hex}}, black 45%); + + --button-secondary-background-active: color-mix(in srgb, {{colors.surface.default.hex}}, black 60%); + + --primary-630: {{colors.surface_variant.default.hex}}; + + /* Muted / secondary chrome */ + --scrollbar-auto-thumb: {{colors.on_surface_variant.default.hex}}; + --scrollbar-thin-thumb: {{colors.on_surface_variant.default.hex}}; + --interactive-muted: {{colors.on_surface_variant.default.hex}}; + --text-muted: {{colors.on_surface_variant.default.hex}}; + --background-modifier-hover: color-mix(in srgb, {{colors.on_surface_variant.default.hex}}, black 40%); + --background-modifier-active: color-mix(in srgb, {{colors.on_surface_variant.default.hex}}, black 20%); + --background-modifier-accent: {{colors.secondary_container.default.hex}}; + --background-accent: {{colors.secondary.default.hex}}; + + --input-border: {{colors.outline.default.hex}}; + --border-normal: {{colors.outline.default.hex}}; + --icon-secondary: {{colors.on_surface_variant.default.hex}}; + --icon-tertiary: {{colors.on_surface_variant.default.hex}}; + --channel-icon: {{colors.on_surface_variant.default.hex}}; + --channels-default: {{colors.on_surface_variant.default.hex}}; + --header-primary: {{colors.on_surface.default.hex}}; + --__lottieIconColor: {{colors.on_surface_variant.default.hex}}; + --interactive-normal: {{colors.on_surface_variant.default.hex}}; + + /* Selection / highlights */ + --red-400: {{colors.error.default.hex}}; + --background-modifier-selected: {{colors.secondary_container.default.hex}}; + + --notice-background-positive: color-mix(in srgb, {{colors.tertiary.default.hex}}, black 75%); + --notice-text-positive: {{colors.tertiary.default.hex}}; + + /* Danger */ + --status-danger: {{colors.error.default.hex}}; + --button-outline-danger-border: {{colors.error.default.hex}}; + --button-outline-danger-text: {{colors.error.default.hex}}; + --button-danger-background: {{colors.error.default.hex}}; + + --yellow-300: {{colors.error.default.hex}}; + + /* Brand / accents */ + --brand-experiment: {{colors.primary.default.hex}}; + --brand-experiment-360: {{colors.primary.default.hex}}; + --brand-experiment-500: {{colors.primary.default.hex}}; + --profile-gradient-button-color: {{colors.primary.default.hex}}; + + --green-360: {{colors.primary.default.hex}}; + + /* Foreground */ + --text-normal: {{colors.on_surface.default.hex}}; + --interactive-active: {{colors.on_surface.default.hex}}; +} + +/* Status indicators (legacy Discord hex fills) */ +rect[fill="#d83a41"] { + fill: {{colors.error.default.hex}} !important; +} + +rect[fill="#cc954c"] { + fill: {{colors.secondary.default.hex}} !important; +} + +rect[fill="#40a258"] { + fill: {{colors.primary.default.hex}} !important; +} + +.wrapper_ef3116 { + background: {{colors.background.default.hex}} !important; +} + +.bar_c38106 { + background: {{colors.background.default.hex}} !important; +} diff --git a/modules/desktop/wallpapers.nix b/modules/desktop/wallpapers.nix new file mode 100644 index 0000000..09214ec --- /dev/null +++ b/modules/desktop/wallpapers.nix @@ -0,0 +1,52 @@ +{ inputs, ... }: { + flake.nixosModules.desktopWallpapers = + { config, lib, pkgs, ... }: + let + cfg = config.chiasson.desktop.wallpapers; + d = config.chiasson.desktop; + guiEnabled = d.hyprland.enable || d.niri.enable || d.plasma.enable; + + wallpaperDir = pkgs.stdenvNoCC.mkDerivation { + pname = "nixos-v2-wallpapers"; + version = "0.1"; + src = builtins.path { + path = cfg.source; + name = "wallpapers-src"; + }; + dontConfigure = true; + dontBuild = true; + installPhase = '' + mkdir -p "$out/share/wallpapers" + find "$src" -mindepth 1 -maxdepth 1 ! -name .git -exec cp -a {} "$out/share/wallpapers/" \; + ''; + }; + + installPath = "${wallpaperDir}/share/wallpapers"; + in + { + options.chiasson.desktop.wallpapers = { + enable = lib.mkEnableOption '' + Copy `source` into the store; sets `NIXOS_V2_WALLPAPERS` and `/etc/wallpapers`. + '' // { + default = true; + }; + + source = lib.mkOption { + type = lib.types.path; + default = inputs.wallpapers; + description = '' + Directory copied into the store. Default: `inputs.wallpapers` + (`git+https://git.chiasson.cloud/Olivier/wallpapers`, pinned in `flake.lock`). + ''; + }; + }; + + config = lib.mkIf (guiEnabled && cfg.enable) { + environment = { + variables.NIXOS_V2_WALLPAPERS = installPath; + sessionVariables.NIXOS_V2_WALLPAPERS = installPath; + etc."wallpapers".source = installPath; + }; + }; + }; +} diff --git a/modules/desktop/waydroid.nix b/modules/desktop/waydroid.nix new file mode 100644 index 0000000..5897cfe --- /dev/null +++ b/modules/desktop/waydroid.nix @@ -0,0 +1,127 @@ +{ ... }: { + flake.nixosModules.desktopWaydroid = + { config, lib, pkgs, ... }: + # `virtualisation.waydroid.package` defaults to `waydroid-nftables` when `networking.nftables.enable` + # is true — required on recent kernels where legacy `ip_tables` / `waydroid-net.sh` (iptables) fails + # with `waydroid session start` (nixpkgs#459520). + let + cfg = config.chiasson.desktop.waydroid; + in + { + options.chiasson.desktop.waydroid = { + enable = lib.mkEnableOption '' + Waydroid + synced base props / nav mode. Needs Wayland; desktop hosts only. This module also + sets `networking.nftables.enable` (default) so NixOS uses `waydroid-nftables` — needed on newer + kernels where `waydroid-net.sh` / iptables fails. For **Google Play** apps (e.g. Hot Wheels + Showcase), run `sudo waydroid init -s GAPPS -f` once after the first `nixos-rebuild` (or reset + the container if you already initialized without GAPPS); then sign in and install from the Play + Store. If `dnsmasq` reports port 53 in use, free that port (Waydroid needs it). + ''; + + width = lib.mkOption { + type = lib.types.int; + default = 1920; + description = "Waydroid rendering width (`persist.waydroid.width`)."; + }; + + height = lib.mkOption { + type = lib.types.int; + default = 1080; + description = "Waydroid rendering height (`persist.waydroid.height`)."; + }; + + multiWindows = lib.mkOption { + type = lib.types.bool; + default = false; + description = "Enable Waydroid multi-window integration."; + }; + + navigationMode = lib.mkOption { + type = lib.types.enum [ "3button" "gestures" ]; + default = "gestures"; + description = "Maps to Waydroid `navigation_mode` secure prop (3button=0, gestures=2)."; + }; + + extraBaseProperties = lib.mkOption { + type = lib.types.attrsOf lib.types.str; + default = { }; + description = '' + Extra `key = value` pairs written into `/var/lib/waydroid/waydroid_base.prop` on activation + (same mechanism as width/height). Use for upstream tweaks — e.g. NVIDIA hosts often need + `ro.hardware.gralloc=default` and `ro.hardware.egl=swiftshader`; Linux 5.18+ may need + `sys.use_memfd=true` ([NixOS Waydroid wiki](https://wiki.nixos.org/wiki/Waydroid)). + ''; + }; + }; + + config = lib.mkIf cfg.enable { + networking.nftables.enable = lib.mkDefault true; + + virtualisation.waydroid.enable = true; + + system.activationScripts.waydroidProps = { + text = '' + PROP_FILE="/var/lib/waydroid/waydroid_base.prop" + + if [ ! -f "$PROP_FILE" ]; then + echo "waydroid: $PROP_FILE not found yet, skipping prop sync." + echo "waydroid: run 'sudo waydroid init' once, then rebuild." + exit 0 + fi + + set_prop() { + key="$1" + value="$2" + + if ${lib.getExe pkgs.gnugrep} -q "^''${key}=" "$PROP_FILE"; then + ${pkgs.gnused}/bin/sed -i "s|^''${key}=.*|''${key}=''${value}|" "$PROP_FILE" + else + printf "%s=%s\n" "$key" "$value" >> "$PROP_FILE" + fi + } + + set_prop "persist.waydroid.multi_windows" "${if cfg.multiWindows then "true" else "false"}" + set_prop "persist.waydroid.width" "${toString cfg.width}" + set_prop "persist.waydroid.height" "${toString cfg.height}" + ${lib.concatStringsSep "\n" ( + lib.mapAttrsToList (k: v: '' + set_prop ${lib.escapeShellArg k} ${lib.escapeShellArg v}'') cfg.extraBaseProperties + )} + ''; + }; + + systemd.services.waydroid-navigation-mode = { + description = "Set Waydroid Android navigation mode"; + wantedBy = [ "multi-user.target" ]; + after = [ "waydroid-container.service" ]; + wants = [ "waydroid-container.service" ]; + path = [ config.virtualisation.waydroid.package ]; + + serviceConfig = { + Type = "oneshot"; + RemainAfterExit = true; + }; + + script = '' + set -eu + + nav_mode="${if cfg.navigationMode == "gestures" then "2" else "0"}" + + if ! systemctl -q is-active waydroid-container.service; then + exit 0 + fi + + for _ in $(seq 1 30); do + if waydroid shell settings put secure navigation_mode "$nav_mode" >/dev/null 2>&1; then + exit 0 + fi + sleep 1 + done + + echo "waydroid: warning: could not set navigation_mode=$nav_mode (container not ready?)" >&2 + exit 0 + ''; + }; + }; + }; +} diff --git a/modules/hosts/14900k/_private/jellyfin-nfs-export.nix b/modules/hosts/14900k/_private/jellyfin-nfs-export.nix new file mode 100644 index 0000000..b4f8c33 --- /dev/null +++ b/modules/hosts/14900k/_private/jellyfin-nfs-export.nix @@ -0,0 +1,48 @@ +# Export large Jellyfin media trees to nix-server. Local path must already exist +# (e.g. /mnt/test/jellyfin/{movies,tv}). On nix-server this is mounted at /mnt/nixdesk-jellyfin. +# +# After deploy: ensure Jellyfin can read files over NFS — typical fix: +# chmod -R a+rX /mnt/test/jellyfin +{ ... }: +{ + # Avoid UID/GID mismatches across machines: map all NFS writes from nix-server to a single + # local system user/group on this server. + users.groups.nfsmedia = { gid = 990; }; + users.users.nfsmedia = { + isSystemUser = true; + uid = 990; + group = "nfsmedia"; + }; + + systemd.tmpfiles.settings."14900k-jellyfin-media-dirs" = { + "/mnt/test/jellyfin"."d" = { mode = "2775"; user = "nfsmedia"; group = "nfsmedia"; }; + "/mnt/test/jellyfin/movies"."d" = { mode = "2775"; user = "nfsmedia"; group = "nfsmedia"; }; + "/mnt/test/jellyfin/tv"."d" = { mode = "2775"; user = "nfsmedia"; group = "nfsmedia"; }; + }; + + # Fixed ports so the firewall can allow NFS v3 helpers (see networking.firewall below). + services.nfs.server = { + enable = true; + mountdPort = 4000; + lockdPort = 4001; + statdPort = 4002; + exports = '' + /mnt/test/jellyfin 192.168.2.238(rw,sync,no_subtree_check,crossmnt,root_squash,all_squash,anonuid=990,anongid=990) + ''; + }; + + networking.firewall.allowedTCPPorts = [ + 111 # portmapper + 2049 + 4000 + 4001 + 4002 + ]; + networking.firewall.allowedUDPPorts = [ + 111 + 2049 + 4000 + 4001 + 4002 + ]; +} diff --git a/modules/hosts/14900k/configuration.nix b/modules/hosts/14900k/configuration.nix index f861969..9857ba9 100644 --- a/modules/hosts/14900k/configuration.nix +++ b/modules/hosts/14900k/configuration.nix @@ -17,6 +17,7 @@ ./_private/peripherals.nix # ./_private/printing-epson.nix ./_private/displays.nix + ./_private/jellyfin-nfs-export.nix ]; sops = { @@ -37,7 +38,24 @@ group = "users"; mode = "0400"; }; +services.cloudflare-warp.enable = true; + # Intel iGPU video acceleration (VA-API / QSV via oneVPL). + # This fixes common NixOS issues like `vaInitialize failed` and missing QSV encoders in apps. + hardware.graphics = { + enable = true; + extraPackages = with pkgs; [ + intel-media-driver # iHD (Gen8+) + vpl-gpu-rt # oneVPL runtime (QSV) + libvdpau-va-gl + ]; + }; + + environment.sessionVariables = { + LIBVA_DRIVER_NAME = "iHD"; + # Force VA-API to use the Intel iGPU render node (otherwise libva may pick NVIDIA and iHD fails). + LIBVA_DRM_DEVICE = "/dev/dri/renderD128"; + }; chiasson.system.caching.attic = { enable = true; @@ -86,7 +104,20 @@ palera1n.enable = true; uconsoleKernelBuilder.enable = true; - extraPackages = [ pkgs.sops pkgs.nodejs_22 ]; + extraPackages = with pkgs; [ + sops + nodejs_22 + ffmpeg + bento4 + yt-dlp + + # Native install (avoid flatpak sandbox issues for QSV/VAAPI). + handbrake + + # Diagnostics + libva-utils # vainfo + ]; + networking = { hostName = "nixdesk"; @@ -102,6 +133,7 @@ self.homeManagerModules.wisdomTerminalsKitty self.homeManagerModules.wisdomBrowsersEdge self.homeManagerModules.wisdomBrowsersFlow + self.homeManagerModules.wisdomBrowsersOrion self.homeManagerModules.wisdomEditorsCursor self.homeManagerModules.wisdomEditorsObsidian self.homeManagerModules.wisdomShellYazi @@ -135,6 +167,7 @@ browsers.edge.enable = true; browsers.flow.enable = false; + browsers.orion.enable = true; editors.cursor.enable = true; editors.obsidian.enable = true; diff --git a/modules/system/services/immich.nix b/modules/system/services/immich.nix new file mode 100644 index 0000000..dc7ecb6 --- /dev/null +++ b/modules/system/services/immich.nix @@ -0,0 +1,213 @@ +{ ... }: { + flake.nixosModules.systemServiceImmich = + { config, lib, pkgs, ... }: + let + cfg = config.chiasson.system.services.immich; + in + { + options.chiasson.system.services.immich = with lib; { + enable = mkEnableOption "Immich stack (server + machine-learning + redis + postgres)."; + + version = mkOption { + type = types.str; + default = "release"; + description = "Immich image tag to deploy."; + }; + + host = mkOption { + type = types.str; + default = "0.0.0.0"; + description = "Host interface to bind Immich server."; + }; + + port = mkOption { + type = types.int; + default = 2283; + description = "Host TCP port mapped to Immich server port 2283."; + }; + + openFirewall = mkOption { + type = types.bool; + default = true; + description = "Open firewall for Immich server port."; + }; + + timezone = mkOption { + type = types.str; + default = "UTC"; + description = "Timezone passed to Immich services via TZ."; + }; + + uploadLocation = mkOption { + type = types.str; + default = "/var/lib/immich/library"; + description = "Host path used for Immich uploads/library."; + }; + + environmentFiles = mkOption { + type = types.listOf types.path; + default = [ ]; + description = '' + Docker `--env-file` paths for **immich-database** and **immich-server** (after inline `-e`). + Use a sops template with `POSTGRES_PASSWORD` and `DB_PASSWORD` (same value) when using + `sops.placeholder` for the DB secret; then set `postgres.password` to empty. + ''; + }; + + postgres = { + image = mkOption { + type = types.str; + default = "ghcr.io/immich-app/postgres:14-vectorchord0.4.3-pgvectors0.2.0@sha256:bcf63357191b76a916ae5eb93464d65c07511da41e3bf7a8416db519b40b1c23"; + description = "Immich recommended Postgres image with vector extensions."; + }; + + user = mkOption { + type = types.str; + default = "postgres"; + description = "Immich Postgres username."; + }; + + password = mkOption { + type = types.str; + default = ""; + description = '' + Immich Postgres password (inline `POSTGRES_PASSWORD` / `DB_PASSWORD`). + Leave empty when using `environmentFiles` with those keys from a sops template. + ''; + }; + + database = mkOption { + type = types.str; + default = "immich"; + description = "Immich Postgres database name."; + }; + }; + }; + + config = lib.mkIf cfg.enable { + assertions = [ + { + assertion = + cfg.postgres.password != "" || cfg.environmentFiles != [ ]; + message = + "chiasson.system.services.immich: set postgres.password or environmentFiles (e.g. sops-rendered POSTGRES_PASSWORD + DB_PASSWORD)."; + } + ]; + + virtualisation = { + docker.enable = true; + oci-containers = { + backend = "docker"; + containers = { + immich-redis = { + image = "docker.io/valkey/valkey:9@sha256:3b55fbaa0cd93cf0d9d961f405e4dfcc70efe325e2d84da207a0a8e6d8fde4f9"; + extraOptions = [ "--network=immich-network" ]; + }; + + immich-database = { + image = cfg.postgres.image; + environment = + { + POSTGRES_USER = cfg.postgres.user; + POSTGRES_DB = cfg.postgres.database; + POSTGRES_INITDB_ARGS = "--data-checksums"; + } + // lib.optionalAttrs (cfg.postgres.password != "") { + POSTGRES_PASSWORD = cfg.postgres.password; + }; + environmentFiles = cfg.environmentFiles; + volumes = [ "immich-postgres:/var/lib/postgresql/data" ]; + extraOptions = [ + "--network=immich-network" + "--shm-size=128mb" + ]; + }; + + immich-machine-learning = { + image = "ghcr.io/immich-app/immich-machine-learning:${cfg.version}"; + environment = { + TZ = cfg.timezone; + }; + volumes = [ "immich-model-cache:/cache" ]; + extraOptions = [ "--network=immich-network" ]; + }; + + immich-server = { + image = "ghcr.io/immich-app/immich-server:${cfg.version}"; + dependsOn = [ + "immich-redis" + "immich-database" + "immich-machine-learning" + ]; + ports = [ "${cfg.host}:${toString cfg.port}:2283" ]; + environment = + { + TZ = cfg.timezone; + DB_HOSTNAME = "immich-database"; + DB_USERNAME = cfg.postgres.user; + DB_DATABASE_NAME = cfg.postgres.database; + REDIS_HOSTNAME = "immich-redis"; + IMMICH_MACHINE_LEARNING_URL = "http://immich-machine-learning:3003"; + UPLOAD_LOCATION = "/data"; + } + // lib.optionalAttrs (cfg.postgres.password != "") { DB_PASSWORD = cfg.postgres.password; }; + environmentFiles = cfg.environmentFiles; + volumes = [ + "${cfg.uploadLocation}:/data" + "/etc/localtime:/etc/localtime:ro" + ]; + extraOptions = [ + "--network=immich-network" + "--pull=always" + ]; + }; + }; + }; + }; + + systemd.services.immich-network = { + description = "Create Docker network for Immich"; + after = [ "docker.service" ]; + requires = [ "docker.service" ]; + wantedBy = [ "multi-user.target" ]; + serviceConfig.Type = "oneshot"; + script = '' + ${pkgs.docker}/bin/docker network inspect immich-network >/dev/null 2>&1 || \ + ${pkgs.docker}/bin/docker network create immich-network + ''; + }; + + systemd.services."docker-immich-redis" = { + after = [ "immich-network.service" ]; + requires = [ "immich-network.service" ]; + }; + + systemd.services."docker-immich-database" = { + after = [ "immich-network.service" ]; + requires = [ "immich-network.service" ]; + }; + + systemd.services."docker-immich-machine-learning" = { + after = [ "immich-network.service" ]; + requires = [ "immich-network.service" ]; + }; + + systemd.services."docker-immich-server" = { + after = [ + "immich-network.service" + "docker-immich-redis.service" + "docker-immich-database.service" + "docker-immich-machine-learning.service" + ]; + requires = [ + "immich-network.service" + "docker-immich-redis.service" + "docker-immich-database.service" + "docker-immich-machine-learning.service" + ]; + }; + + networking.firewall.allowedTCPPorts = lib.optionals cfg.openFirewall [ cfg.port ]; + }; + }; +} diff --git a/modules/wisdom/browsers/chrome.nix b/modules/wisdom/browsers/chrome.nix new file mode 100644 index 0000000..7ea487c --- /dev/null +++ b/modules/wisdom/browsers/chrome.nix @@ -0,0 +1,19 @@ +{ ... }: { + flake.homeManagerModules.wisdomBrowsersChrome = + { config, lib, pkgs, ... }: + let + root = config.chiasson.home; + cfg = config.chiasson.home.browsers.chrome; + in + { + options.chiasson.home.browsers.chrome.enable = lib.mkEnableOption '' + Chrome (unfree, needs `allowUnfree`); skipped if nixpkgs has no build for this platform. + ''; + + config = lib.mkIf (root.enable && cfg.enable) { + home.packages = lib.optional ( + lib.meta.availableOn pkgs.stdenv.hostPlatform pkgs.google-chrome + ) pkgs.google-chrome; + }; + }; +} diff --git a/modules/wisdom/browsers/edge.nix b/modules/wisdom/browsers/edge.nix new file mode 100644 index 0000000..637b6e6 --- /dev/null +++ b/modules/wisdom/browsers/edge.nix @@ -0,0 +1,17 @@ +{ ... }: { + flake.homeManagerModules.wisdomBrowsersEdge = + { config, lib, pkgs, ... }: + let + root = config.chiasson.home; + cfg = config.chiasson.home.browsers.edge; + in + { + options.chiasson.home.browsers.edge.enable = lib.mkEnableOption "Edge (unfree); skipped if unavailable on this platform."; + + config = lib.mkIf (root.enable && cfg.enable) { + home.packages = lib.optional ( + lib.meta.availableOn pkgs.stdenv.hostPlatform pkgs.microsoft-edge + ) pkgs.microsoft-edge; + }; + }; +} diff --git a/modules/wisdom/browsers/flow.nix b/modules/wisdom/browsers/flow.nix new file mode 100644 index 0000000..e9c0670 --- /dev/null +++ b/modules/wisdom/browsers/flow.nix @@ -0,0 +1,67 @@ +{ ... }: { + flake.homeManagerModules.wisdomBrowsersFlow = + { config, lib, pkgs, ... }: + let + root = config.chiasson.home; + cfg = config.chiasson.home.browsers.flow; + + flow-browser = + let + pname = "flow-browser"; + version = "0.11.0"; + + suffix = if pkgs.stdenv.hostPlatform.isAarch64 then "arm64" else "x86_64"; + + hash = + if pkgs.stdenv.hostPlatform.isAarch64 then + "sha256-rTRKbNyVRJAw7ZyDR6kx+XJ4rWmErZqA0b6LP9t5eOA=" + else + "sha256-/Tca4uUBfgbZQEeXdYkCz6CWxqvCl40CQpACFry1k9s="; + + src = pkgs.fetchurl { + url = "https://github.com/MultiboxLabs/flow-browser/releases/download/v${version}/flow-browser-${version}-${suffix}.AppImage"; + inherit hash; + }; + + appimageContents = pkgs.appimageTools.extractType2 { inherit pname version src; }; + in + pkgs.appimageTools.wrapType2 { + inherit pname version src; + + nativeBuildInputs = [ pkgs.makeWrapper ]; + + extraInstallCommands = '' + wrapProgram $out/bin/${pname} \ + --add-flags "\''${NIXOS_OZONE_WL:+\''${WAYLAND_DISPLAY:+--ozone-platform-hint=auto --enable-features=WaylandWindowDecorations --enable-wayland-ime=true}}" + + install -m 444 -D ${appimageContents}/flow-browser.desktop -t $out/share/applications + substituteInPlace $out/share/applications/flow-browser.desktop \ + --replace-fail 'Exec=AppRun --ozone-platform-hint=auto' 'Exec=${pname} --ozone-platform-hint=auto' + + install -m 444 -D ${appimageContents}/usr/share/icons/hicolor/512x512/apps/flow-browser.png \ + $out/share/icons/hicolor/512x512/apps/flow-browser.png + ''; + + meta = { + description = "Chromium-based browser (upstream AppImage)"; + homepage = "https://github.com/MultiboxLabs/flow-browser"; + license = lib.licenses.gpl3Plus; + sourceProvenance = with lib.sourceTypes; [ binaryNativeCode ]; + platforms = [ "x86_64-linux" "aarch64-linux" ]; + mainProgram = pname; + maintainers = [ ]; + }; + }; + in + { + options.chiasson.home.browsers.flow.enable = lib.mkEnableOption '' + [Flow](https://github.com/MultiboxLabs/flow-browser) — upstream AppImage wrapped for NixOS. + ''; + + config = lib.mkIf (root.enable && cfg.enable) { + home.packages = lib.optional ( + lib.meta.availableOn pkgs.stdenv.hostPlatform flow-browser + ) flow-browser; + }; + }; +} diff --git a/modules/wisdom/browsers/orion.nix b/modules/wisdom/browsers/orion.nix new file mode 100644 index 0000000..860b498 --- /dev/null +++ b/modules/wisdom/browsers/orion.nix @@ -0,0 +1,59 @@ +{ ... }: { + flake.homeManagerModules.wisdomBrowsersOrion = + { config, lib, pkgs, ... }: + let + root = config.chiasson.home; + cfg = config.chiasson.home.browsers.orion; + + pname = "oriongtk"; + version = "0.3.0"; + flatpakBundle = pkgs.fetchurl { + url = "https://cdn.kagi.com/downloads/oriongtk.${version}.flatpak"; + hash = "sha256-0NOWPS2Yv5NpnTxqsiMvshHFyTyDotPi964/2og/bCw="; + }; + + appId = "com.kagi.OrionGtk"; + + oriongtk = pkgs.runCommand "oriongtk-${version}" + { + nativeBuildInputs = [ pkgs.makeWrapper ]; + passthru = { + inherit pname version; + }; + } + '' + mkdir -p "$out/bin" + makeWrapper ${pkgs.flatpak}/bin/flatpak "$out/bin/${pname}" \ + --add-flags "run" \ + --add-flags ${lib.escapeShellArg appId} + ''; + in + { + options.chiasson.home.browsers.orion.enable = lib.mkEnableOption '' + [Orion](https://orionbrowser.com/) (Kagi) — installs the upstream Flatpak bundle and provides `oriongtk`. + ''; + + config = lib.mkIf (root.enable && cfg.enable) { + home.packages = [ oriongtk ]; + + home.activation.oriongtkFlatpak = lib.hm.dag.entryAfter [ "writeBoundary" ] '' + set -eu + + if [ ! -x "${pkgs.flatpak}/bin/flatpak" ]; then + echo "oriongtk: flatpak missing; enable Flatpak (e.g. services.flatpak on NixOS)." >&2 + exit 1 + fi + + echo "oriongtk: ensuring ${appId} from ${flatpakBundle} (user)" + # `--or-update` still exits non-zero when the same ref is already installed from this bundle; + # `--reinstall` is idempotent for HM switches (uninstall first only if present). + ${pkgs.flatpak}/bin/flatpak --user install \ + --assumeyes \ + --noninteractive \ + --reinstall \ + --bundle \ + ${lib.escapeShellArg (builtins.toString flatpakBundle)} + ''; + }; + }; +} diff --git a/modules/wisdom/browsers/zen.nix b/modules/wisdom/browsers/zen.nix new file mode 100644 index 0000000..2af2e74 --- /dev/null +++ b/modules/wisdom/browsers/zen.nix @@ -0,0 +1,85 @@ +{ inputs, ... }: { + flake.homeManagerModules.wisdomBrowsersZen = + { config, lib, pkgs, ... }: + let + root = config.chiasson.home; + cfg = config.chiasson.home.browsers.zen; + in + { + imports = [ inputs.zen-browser.homeModules.beta ]; + + options.chiasson.home.browsers.zen.enable = lib.mkEnableOption "Zen Browser + locked-down policies / extensions."; + + config = lib.mkIf (root.enable && cfg.enable) { + programs.zen-browser = { + enable = true; + policies = { + PasswordManagerEnabled = false; + AutofillCreditCardEnabled = false; + AutofillAddressEnabled = false; + DisableAppUpdate = true; + DisableFeedbackCommands = true; + DisableFirefoxStudies = true; + DisablePocket = true; + DisableTelemetry = true; + OfferToSaveLogins = false; + EnableTrackingProtection = { + Value = true; + Locked = true; + Cryptomining = true; + Fingerprinting = true; + }; + ExtensionSettings = { + "{446900e4-71c2-419f-a6a7-df9c091e268b}" = { + install_url = "https://addons.mozilla.org/firefox/downloads/latest/bitwarden-password-manager/latest.xpi"; + installation_mode = "normal_installed"; + }; + "uBlock0@raymondhill.net" = { + install_url = "https://addons.mozilla.org/firefox/downloads/latest/ublock-origin/latest.xpi"; + installation_mode = "normal_installed"; + }; + "{762f9885-5a13-4abd-9c77-433dcd38b8fd}" = { + install_url = "https://addons.mozilla.org/firefox/downloads/latest/return-youtube-dislikes/latest.xpi"; + installation_mode = "normal_installed"; + }; + }; + }; + }; + + home.packages = [ + (pkgs.writeShellApplication { + name = "extract-firefox-extension"; + runtimeInputs = with pkgs; [ + wget + unzip + jq + ]; + text = '' + if [ -z "$1" ]; then + echo "usage: $0 " + exit 1 + fi + + PLUGIN_URL="$1" + TEMP_DIR="extension-id-$(date +%s)" + mkdir "$TEMP_DIR" || exit 1 + cd "$TEMP_DIR" || exit 1 + + DOWNLOAD_URL=$(echo "$PLUGIN_URL" \ + | sed -E 's|https://addons.mozilla.org/firefox/downloads/file/[0-9]+/([^/]+)-[^/]+\.xpi|\1|' \ + | tr '_' '-' \ + | awk '{print "https://addons.mozilla.org/firefox/downloads/latest/" $1 "/latest.xpi"}') + + wget -q "$DOWNLOAD_URL" -O latest.xpi || { cd ..; rm -rf "$TEMP_DIR"; exit 1; } + unzip -q latest.xpi -d unpacked || { cd ..; rm -rf "$TEMP_DIR"; exit 1; } + + jq -r '.browser_specific_settings.gecko.id' unpacked/manifest.json || { cd ..; rm -rf "$TEMP_DIR"; exit 1; } + + cd .. + rm -rf "$TEMP_DIR" + ''; + }) + ]; + }; + }; +} diff --git a/modules/wisdom/desktop/gtk-qt-theming.nix b/modules/wisdom/desktop/gtk-qt-theming.nix new file mode 100644 index 0000000..e89f6e6 --- /dev/null +++ b/modules/wisdom/desktop/gtk-qt-theming.nix @@ -0,0 +1,128 @@ +{ ... }: { + flake.homeManagerModules.wisdomDesktopGtkQtTheming = + { config, lib, pkgs, ... }: + let + root = config.chiasson.home; + cfg = config.chiasson.home.desktop.theming; + in + { + options.chiasson.home.desktop.theming = { + enable = lib.mkEnableOption '' + WhiteSur GTK + icon themes, Phinger cursor, and Qt via the KDE platform theme — same idea as + the old `home-shared.nix` stack for Hyprland/Niri (no full Plasma session required). Optional + `dank-colors.css` import for DMS/matugen GTK accents. + ''; + + matugenGtkColors = lib.mkOption { + type = lib.types.bool; + default = true; + description = '' + Add `@import url("dank-colors.css");` to gtk3/gtk4 extraCss. DMS/matugen writes + `~/.config/gtk-3.0` / `gtk-4.0` `dank-colors.css` when dynamic theming is on — disable if + you use this module without DMS. + ''; + }; + + gtkTheme = { + name = lib.mkOption { + type = lib.types.str; + default = "WhiteSur-Dark"; + }; + package = lib.mkOption { + type = lib.types.package; + default = pkgs.whitesur-gtk-theme; + }; + }; + + iconTheme = { + name = lib.mkOption { + type = lib.types.str; + default = "WhiteSur-dark"; + }; + package = lib.mkOption { + type = lib.types.package; + default = pkgs.whitesur-icon-theme; + }; + }; + + cursor = { + name = lib.mkOption { + type = lib.types.str; + default = "phinger-cursors-dark"; + }; + package = lib.mkOption { + type = lib.types.package; + default = pkgs.phinger-cursors; + }; + size = lib.mkOption { + type = lib.types.int; + default = 32; + }; + }; + + qt = { + platformTheme = lib.mkOption { + type = lib.types.str; + default = "kde"; + description = '' + `QT_QPA_PLATFORMTHEME` (e.g. `kde` for Qt/KDE integration, matches previous flake). + ''; + }; + extraPackages = lib.mkOption { + type = lib.types.listOf lib.types.package; + default = [ pkgs.whitesur-kde pkgs.qt6Packages.qt6ct ]; + description = "Extra packages (KDE look-and-feel + qt6ct GUI)."; + }; + }; + }; + + config = lib.mkIf (root.enable && cfg.enable) (lib.mkMerge [ + { + home.packages = cfg.qt.extraPackages; + + home.sessionVariables = { + QT_QPA_PLATFORMTHEME = cfg.qt.platformTheme; + }; + + home.pointerCursor = { + name = cfg.cursor.name; + package = cfg.cursor.package; + size = cfg.cursor.size; + gtk.enable = true; + x11.enable = true; + }; + + gtk = { + enable = true; + theme = { + name = cfg.gtkTheme.name; + package = cfg.gtkTheme.package; + }; + iconTheme = { + name = cfg.iconTheme.name; + package = cfg.iconTheme.package; + }; + cursorTheme = { + name = cfg.cursor.name; + package = cfg.cursor.package; + size = cfg.cursor.size; + }; + gtk3.extraConfig = { + gtk-application-prefer-dark-theme = 1; + }; + gtk4.extraConfig = { + gtk-application-prefer-dark-theme = 1; + }; + }; + } + (lib.mkIf cfg.matugenGtkColors { + gtk.gtk3.extraCss = '' + @import url("dank-colors.css"); + ''; + gtk.gtk4.extraCss = '' + @import url("dank-colors.css"); + ''; + }) + ]); + }; +} diff --git a/modules/wisdom/desktop/screenshot.nix b/modules/wisdom/desktop/screenshot.nix new file mode 100644 index 0000000..99e28fe --- /dev/null +++ b/modules/wisdom/desktop/screenshot.nix @@ -0,0 +1,219 @@ +{ ... }: { + flake.homeManagerModules.wisdomDesktopScreenshot = + { config, lib, pkgs, ... }: + let + root = config.chiasson.home; + cfg = config.chiasson.home.desktop.screenshot; + hyprlandHm = lib.attrByPath [ "wayland" "windowManager" "hyprland" ] { } config; + hyprlandHmEnabled = hyprlandHm.enable or false; + keyOk = cfg.swiftshareApiKeyFile != null && cfg.swiftshareApiKeyFile != ""; + in + { + options.chiasson.home.desktop.screenshot.enable = lib.mkEnableOption '' + grim/slurp/swappy + SwiftShare helpers; Hyprland binds if HM Hyprland is on. + ''; + + options.chiasson.home.desktop.screenshot.swiftshareApiKeyFile = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + description = '' + File with SwiftShare API key (sops path is fine). Required when screenshot module is on. + ''; + }; + + config = lib.mkMerge [ + (lib.mkIf (root.enable && cfg.enable) { + assertions = [ + { + assertion = keyOk; + message = "chiasson.home.desktop.screenshot: set chiasson.home.desktop.screenshot.swiftshareApiKeyFile to your SwiftShare API key file path."; + } + ]; + }) + (lib.mkIf (root.enable && cfg.enable && keyOk) ( + let + apiKeyFile = cfg.swiftshareApiKeyFile; + in + lib.mkMerge [ + { + home.packages = with pkgs; [ + grim + slurp + swappy + wl-clipboard + libnotify + (writeShellScriptBin "swiftshare-upload" '' + #!${pkgs.bash}/bin/bash + set -euo pipefail + + COPY_URL=0 + if [ "$#" -ge 1 ] && [ "$1" = "--copy-url" ]; then + COPY_URL=1 + shift + fi + + APP_NAME="" + if [ "$#" -ge 2 ] && [ "$1" = "--app-name" ]; then + APP_NAME="$2" + shift 2 + fi + + API_KEY_FILE=${lib.escapeShellArg apiKeyFile} + if [ -r "$API_KEY_FILE" ]; then + SWIFTSHARE_API_KEY="$(tr -d '\n' < "$API_KEY_FILE")" + fi + + if [ -z "''${SWIFTSHARE_API_KEY:-}" ]; then + ${pkgs.libnotify}/bin/notify-send "SwiftShare upload" "SwiftShare API key missing (expected readable: $API_KEY_FILE)" + echo "Error: SwiftShare API key missing (expected readable: $API_KEY_FILE)" >&2 + exit 1 + fi + + IMAGE_FILE="" + RESPONSE_FILE="" + cleanup() { + if [ -n "$RESPONSE_FILE" ] && [ -f "$RESPONSE_FILE" ]; then + rm -f "$RESPONSE_FILE" + fi + } + trap cleanup EXIT + + if [ "$#" -ge 1 ] && [ "$1" != "-" ]; then + IMAGE_FILE="$1" + if [ "''${IMAGE_FILE#'/'}" = "''${IMAGE_FILE}" ]; then + IMAGE_FILE="$(${pkgs.coreutils}/bin/readlink -f "''${IMAGE_FILE}")" + fi + if [ ! -f "$IMAGE_FILE" ]; then + echo "Error: file not found: $IMAGE_FILE" >&2 + exit 1 + fi + else + APP_NAME="''${APP_NAME:-screenshot}" + APP_NAME="''${APP_NAME%% *}" + APP_NAME="''${APP_NAME//[^A-Za-z0-9]/}" + APP_NAME="''${APP_NAME,,}" + if [ -z "$APP_NAME" ]; then + APP_NAME="screenshot" + fi + + IMAGE_FILE="$(${pkgs.coreutils}/bin/mktemp --suffix=.png "''${TMPDIR:-/tmp}/''${APP_NAME}_XXXXXX")" + cat > "$IMAGE_FILE" + fi + + if [ ! -s "$IMAGE_FILE" ]; then + ${pkgs.libnotify}/bin/notify-send "SwiftShare" "Empty capture (maybe canceled) – not uploading" + echo "Empty image file, not uploading." >&2 + exit 0 + fi + + RESPONSE_FILE="$(mktemp)" + set +e + HTTP_STATUS="$(${pkgs.curl}/bin/curl -sS -o "''${RESPONSE_FILE}" -w '%{http_code}' \ + -X POST "https://swiftshare.cloud/api/upload/sharex" \ + -F "upload=@''${IMAGE_FILE}" \ + -F "apiKey=''${SWIFTSHARE_API_KEY}")" + CURL_EXIT=$? + set -e + + RESPONSE="$(cat "''${RESPONSE_FILE}")" + + if [ "''${CURL_EXIT}" -ne 0 ]; then + ${pkgs.libnotify}/bin/notify-send "SwiftShare upload failed" "Network or HTTP error (curl exit ''${CURL_EXIT})" + echo "SwiftShare upload failed (curl exit ''${CURL_EXIT})." >&2 + echo "Response body:" >&2 + echo "''${RESPONSE}" >&2 + exit 1 + fi + + if ! echo "''${HTTP_STATUS}" | grep -qE '^2[0-9][0-9]$'; then + ERROR_MSG="$(${pkgs.jq}/bin/jq -r '.error // empty' <<< "''${RESPONSE}")" + if [ -z "''${ERROR_MSG}" ] || [ "''${ERROR_MSG}" = "null" ]; then + ERROR_MSG="Failed to upload file" + fi + ${pkgs.libnotify}/bin/notify-send "SwiftShare upload failed (''${HTTP_STATUS})" "''${ERROR_MSG}" + echo "SwiftShare upload failed (HTTP ''${HTTP_STATUS}): ''${ERROR_MSG}" >&2 + exit 1 + fi + + URL="$(${pkgs.jq}/bin/jq -r '.url // empty' <<< "''${RESPONSE}")" + THUMBNAIL="$(${pkgs.jq}/bin/jq -r '.thumbnail // empty' <<< "''${RESPONSE}")" + + if [ -z "$URL" ] || [ "$URL" = "null" ]; then + ${pkgs.libnotify}/bin/notify-send "SwiftShare upload failed" "Could not parse URL from response" + echo "Upload failed. Raw response:" >&2 + echo "''${RESPONSE}" >&2 + exit 1 + fi + + echo "$URL" + if [ -n "$THUMBNAIL" ] && [ "$THUMBNAIL" != "null" ]; then + echo "$THUMBNAIL" + fi + + if [ "$COPY_URL" = "1" ]; then + ${pkgs.wl-clipboard}/bin/wl-copy <<< "$URL" + fi + + if [ -n "$IMAGE_FILE" ] && [ -f "$IMAGE_FILE" ]; then + ${pkgs.libnotify}/bin/notify-send \ + -a "SwiftShare" \ + -i "$IMAGE_FILE" \ + -h string:image-path:"$IMAGE_FILE" \ + "SwiftShare upload" "Uploaded image: $URL" + else + ${pkgs.libnotify}/bin/notify-send "SwiftShare upload" "Uploaded image: $URL" + fi + '') + + (writeShellScriptBin "swiftshare-screenshot" '' + #!${pkgs.bash}/bin/bash + set -euo pipefail + + LOG_DIR="$HOME/.local/state/swiftshare" + mkdir -p "$LOG_DIR" + LOG_FILE="$LOG_DIR/screenshot.log" + + APP_CLASS="$(${pkgs.hyprland}/bin/hyprctl activewindow -j 2>/dev/null | ${pkgs.jq}/bin/jq -r '.class // .initialClass // empty' 2>/dev/null || true)" + APP_CLASS="''${APP_CLASS%% *}" + APP_CLASS="''${APP_CLASS//[^A-Za-z0-9]/}" + APP_CLASS="''${APP_CLASS,,}" + if [ -z "$APP_CLASS" ]; then + APP_CLASS="screenshot" + fi + + GEOM="$(${pkgs.slurp}/bin/slurp)" + SLURP_EXIT=$? + + if [ "$SLURP_EXIT" -ne 0 ] || [ -z "$GEOM" ]; then + ${pkgs.libnotify}/bin/notify-send "SwiftShare" "Capture canceled" + { + echo "==== $(date) ==== capture canceled (slurp exit $SLURP_EXIT, geom='$GEOM')" + } >>"$LOG_FILE" 2>&1 + exit 0 + fi + + { + echo "==== $(date) ====" + echo "Geometry: $GEOM" + ${pkgs.grim}/bin/grim -g "$GEOM" - | ${pkgs.swappy}/bin/swappy -f - -o - | swiftshare-upload --copy-url --app-name "$APP_CLASS" + } >>"$LOG_FILE" 2>&1 + '') + ]; + } + (lib.mkIf hyprlandHmEnabled { + wayland.windowManager.hyprland.settings = { + bind = [ + ", Print, exec, grim -g \"$(slurp)\" - | wl-copy" + "Control, Print, exec, grim -g \"$(slurp)\" - | swappy -f -" + "SUPER, Print, exec, swiftshare-screenshot" + ]; + windowrule = [ + "float on, opacity 1.0 override, match:class ^(swappy)$" + ]; + }; + }) + ] + )) + ]; + }; +} diff --git a/modules/wisdom/editors/cursor.nix b/modules/wisdom/editors/cursor.nix new file mode 100644 index 0000000..0786c27 --- /dev/null +++ b/modules/wisdom/editors/cursor.nix @@ -0,0 +1,60 @@ +{ inputs, ... }: { + flake.homeManagerModules.wisdomEditorsCursor = + { config, lib, pkgs, ... }: + let + root = config.chiasson.home; + cfg = config.chiasson.home.editors.cursor; + cursorPkgs = inputs.cursor.packages.${pkgs.stdenv.hostPlatform.system} or { }; + cursorPkg = + if cursorPkgs ? cursor then + cursorPkgs.cursor + else if cursorPkgs ? default then + cursorPkgs.default + else + null; + # NixOS-New `home-shared.nix`: `cursor-cli` alongside the AppImage. nixpkgs now names this + # `cursor-agent`; keep both for compatibility across pins. + defaultAgentPkg = + if pkgs ? cursor-agent then + pkgs.cursor-agent + else if pkgs ? cursor-cli then + pkgs.cursor-cli + else + null; + in + { + options.chiasson.home.editors.cursor = { + enable = lib.mkEnableOption "Cursor editor from the `cursor` flake input."; + setAsDefaultEditor = lib.mkOption { + type = lib.types.bool; + default = true; + description = "`EDITOR` / `VISUAL` → `cursor --wait`."; + }; + agent = { + enable = lib.mkEnableOption '' + Cursor Agent CLI (`cursor-agent` in current nixpkgs; older pins used `cursor-cli`). + '' // { + default = true; + }; + package = lib.mkOption { + type = with lib.types; nullOr package; + default = defaultAgentPkg; + defaultText = "pkgs.cursor-agent or pkgs.cursor-cli or null"; + description = '' + Package providing the `cursor-agent` CLI. Set to `null` to omit the CLI while keeping the GUI app. + ''; + }; + }; + }; + + config = lib.mkIf (root.enable && cfg.enable && cursorPkg != null) { + home.packages = + [ cursorPkg ] + ++ lib.optionals (cfg.agent.enable && cfg.agent.package != null) [ cfg.agent.package ]; + home.sessionVariables = lib.mkIf cfg.setAsDefaultEditor { + EDITOR = "cursor --wait"; + VISUAL = "cursor --wait"; + }; + }; + }; +} diff --git a/modules/wisdom/filebrowsers/dolphin.nix b/modules/wisdom/filebrowsers/dolphin.nix new file mode 100644 index 0000000..cdba40a --- /dev/null +++ b/modules/wisdom/filebrowsers/dolphin.nix @@ -0,0 +1,76 @@ +{ ... }: { + flake.homeManagerModules.wisdomFilebrowsersDolphin = + { config, lib, pkgs, ... }: + let + root = config.chiasson.home; + cfg = root.filebrowsers.dolphin; + in + { + options.chiasson.home.filebrowsers.dolphin.enable = lib.mkEnableOption "Dolphin + declarative dolphinrc."; + + config = lib.mkIf (root.enable && cfg.enable) { + home.packages = [ pkgs.kdePackages.dolphin ]; + + xdg.configFile."dolphinrc".text = '' + [ContentDisplay] + UsePermissionsFormat=CombinedFormat + + [General] + StartupPath=~ + DoubleClickViewAction=show_hidden_files + ShowFullPath=true + ShowFullPathInTitlebar=true + ShowStatusBar=FullWidth + UseTabForSwitchingSplitView=true + Version=202 + ViewPropsTimestamp=2025,11,17,23,21,57.762 + + [KFileDialog Settings] + Places Icons Auto-resize=false + Places Icons Static Size=22 + + [MainWindow] + MenuBar=Disabled + ToolBarsMovable=Disabled + + [PreviewSettings] + Plugins=appimagethumbnail,audiothumbnail,blenderthumbnail,comicbookthumbnail,cursorthumbnail,djvuthumbnail,ebookthumbnail,exrthumbnail,directorythumbnail,fontthumbnail,imagethumbnail,jpegthumbnail,kraorathumbnail,windowsexethumbnail,windowsimagethumbnail,mobithumbnail,opendocumentthumbnail,gsthumbnail,rawthumbnail,svgthumbnail,ffmpegthumbs + + [IconsMode] + IconSize=48 + PreviewSize=48 + TextLines=2 + UseThumbnails=true + + [DetailsMode] + FontWeight=50 + HighlightEntireRow=true + + [ViewProperties] + Mode=1 + ColumnWidths=50,50,50,50,50,50,50,50,50,50 + SortColumn=0 + SortOrder=0 + SortFoldersFirst=true + SortHiddenLast=false + SortCaseSensitively=false + ShowPreviews=true + ShowInGroups=false + ShowFoldersFirst=true + ShowHiddenFilesLast=false + NaturalSorting=true + + [ContextMenu] + ShowCopyToMenu=true + ShowMoveToMenu=true + + [Search] + Location=Everywhere + + [SettingsWindow] + SidebarWidth=180 + SplitterState=AAAA/wAAAAD9AAAAAAAAAAAAAAABAAAAAQAAAAEAAAAAQAAAAEAAAAA= + ''; + }; + }; +} diff --git a/modules/wisdom/hardware/uconsole-gamepad.nix b/modules/wisdom/hardware/uconsole-gamepad.nix new file mode 100644 index 0000000..01f03b1 --- /dev/null +++ b/modules/wisdom/hardware/uconsole-gamepad.nix @@ -0,0 +1,61 @@ +{ ... }: { + flake.homeManagerModules.wisdomHardwareUconsoleGamepad = + { config, lib, pkgs, ... }: + let + root = config.chiasson.home; + cfg = config.chiasson.home.hardware.uconsoleGamepad; + in + { + options.chiasson.home.hardware.uconsoleGamepad.enable = lib.mkEnableOption '' + uConsole gamepad antimicrox profile + Hyprland exec-once when HM Hyprland is on. + ''; + + config = lib.mkIf (root.enable && cfg.enable) (lib.mkMerge [ + { + home.packages = [ pkgs.antimicrox ]; + home.file.".config/antimicrox/uconsole.gamecontroller.amgp".text = '' + + + Clockwork Pi DevTerm + 030000fdaf1e00002400000010010000785536 + + + + + + + + + + Stick 2 + Stick 1 + + + + + positivehalf + + + positivehalf + + + + + + ''; + } + (lib.mkIf (config.wayland.windowManager.hyprland.enable or false) { + wayland.windowManager.hyprland.settings.exec-once = lib.mkAfter [ + "antimicrox --hidden --no-tray --profile ${config.home.homeDirectory}/.config/antimicrox/uconsole.gamecontroller.amgp &" + ]; + }) + ]); + }; +}