diff --git a/home/zephrynis.nix b/home/zephrynis.nix index 63bcbd1..e7e5cec 100644 --- a/home/zephrynis.nix +++ b/home/zephrynis.nix @@ -1,7 +1,27 @@ -{ inputs, pkgs, ... }: +{ inputs, lib, pkgs, ... }: +let + share-picker = inputs.hyprland-preview-share-picker.packages.${pkgs.stdenv.hostPlatform.system}.default; + # Substitutes the current wallpaper palette (matugen's colors.json) into the + # picker stylesheet template on every launch, so the picker re-themes with + # the wallpaper, then hands off to the real picker binary. + share-picker-themed = pkgs.writeShellScript "share-picker-themed" '' + dir="$HOME/.config/hyprland-preview-share-picker" + colors="$HOME/.local/state/quickshell/user/generated/colors.json" + if [ -f "$colors" ]; then + ${pkgs.jq}/bin/jq -r 'to_entries[] | "s|@\(.key)@|\(.value)|g"' "$colors" \ + | ${pkgs.gnused}/bin/sed -f - "$dir/style.css.in" > "$dir/generated.css" || true + else + cp -f "$dir/style.css.in" "$dir/generated.css" + fi + exec ${share-picker}/bin/hyprland-preview-share-picker "$@" + ''; +in { - imports = [ inputs.illogical-flake.homeManagerModules.default ]; + imports = [ + inputs.illogical-flake.homeManagerModules.default + inputs.spicetify-nix.homeManagerModules.spicetify + ]; home.username = "zephrynis"; home.homeDirectory = "/home/zephrynis"; @@ -19,12 +39,366 @@ # hyprland.plugins = [ ]; # listOf package, loaded via hyprland's plugin mechanism }; + # illogical-flake wipes and recopies ~/.config/hypr on every switch, so the + # UK layout override must be re-appended after its copy step each time. + # custom/general.lua is sourced after hyprland/general.lua (kb_layout = "us"). + home.activation.ukKeyboardLayout = lib.hm.dag.entryAfter [ "copyIllogicalImpulseConfigs" ] '' + hyprCustomGeneral="$HOME/.config/hypr/custom/general.lua" + if [ -f "$hyprCustomGeneral" ] && ! grep -q 'kb_layout = "gb"' "$hyprCustomGeneral"; then + cat >> "$hyprCustomGeneral" << 'EOF' + + -- UK keyboard layout (appended by nix-flake, overrides hyprland/general.lua) + hl.config({ input = { kb_layout = "gb" } }) + EOF + echo "Appended UK keyboard layout to hypr/custom/general.lua" + fi + ''; + + # Suppress the Hyprland update news popup (option lives under ecosystem + # since Hyprland 0.46); re-appended after every recopy like the layout. + home.activation.hyprlandNoUpdateNews = lib.hm.dag.entryAfter [ "ukKeyboardLayout" ] '' + hyprCustomGeneral="$HOME/.config/hypr/custom/general.lua" + if [ -f "$hyprCustomGeneral" ] && ! grep -q 'no_update_news' "$hyprCustomGeneral"; then + cat >> "$hyprCustomGeneral" << 'EOF' + + -- Suppress update news popup (appended by nix-flake) + hl.config({ ecosystem = { no_update_news = true } }) + EOF + echo "Appended no_update_news to hypr/custom/general.lua" + fi + ''; + + # Monitor layout (captured from nwg-displays output, 2026-07-18): AOC + # CQ27G2S at 165Hz on the left, 4K TV at 1.5x scale to its right. + # Overrides the mode=preferred/position=auto catch-all in hyprland/general.lua. + home.activation.hyprlandMonitorLayout = lib.hm.dag.entryAfter [ "hyprlandNoUpdateNews" ] '' + hyprCustomGeneral="$HOME/.config/hypr/custom/general.lua" + if [ -f "$hyprCustomGeneral" ] && ! grep -q 'hl.monitor' "$hyprCustomGeneral"; then + cat >> "$hyprCustomGeneral" << 'EOF' + + -- Monitor layout (appended by nix-flake, from nwg-displays 2026-07-18) + hl.monitor({ output = "DP-2", mode = "2560x1440@165.0", position = "0x0", scale = "1" }) + hl.monitor({ output = "DP-1", mode = "3840x2160@59.98", position = "2560x678", scale = "1.5" }) + + -- Workspace 1 stays on the 4K TV; cursor starts on the 165Hz AOC + hl.workspace_rule({ workspace = "1", monitor = "DP-1", default = true }) + hl.on("hyprland.start", function () + hl.dispatch(hl.dsp.focus({ monitor = "DP-2" })) + hl.dispatch(hl.dsp.cursor.move({ x = 1280, y = 720 })) + end) + EOF + echo "Appended monitor layout to hypr/custom/general.lua" + fi + ''; + + # Tailscale tray icon in the bar's systray (daemon + package are system-side + # in modules/tailscale.nix). custom/execs.lua is the dots' intended autostart + # hook, but it's wiped and recopied every switch like the rest of ~/.config/hypr. + home.activation.trayscaleAutostart = lib.hm.dag.entryAfter [ "copyIllogicalImpulseConfigs" ] '' + hyprCustomExecs="$HOME/.config/hypr/custom/execs.lua" + if [ -f "$hyprCustomExecs" ] && ! grep -q 'trayscale' "$hyprCustomExecs"; then + cat >> "$hyprCustomExecs" << 'EOF' + + -- Tailscale tray icon (appended by nix-flake) + hl.on("hyprland.start", function () + hl.exec_cmd("trayscale --hide-window") + end) + EOF + echo "Appended trayscale autostart to hypr/custom/execs.lua" + fi + ''; + + # illogical-flake's generated custom/env.lua forces QT_QPA_PLATFORMTHEME=qt6ct + # (its workaround for plasma-integration not being installed). desktop.nix now + # installs plasma-integration + Darkly, so restore upstream's "kde" — that + # applies the matugen-generated kdeglobals (Material You colors, Darkly style) + # to Qt apps like Prism Launcher. Appended after the regeneration each switch; + # later hl.env calls win. + home.activation.qtPlatformThemeKde = lib.hm.dag.entryAfter [ "copyIllogicalImpulseConfigs" ] '' + hyprCustomEnv="$HOME/.config/hypr/custom/env.lua" + if [ -f "$hyprCustomEnv" ] && ! grep -q 'kde-platform-theme' "$hyprCustomEnv"; then + cat >> "$hyprCustomEnv" << 'EOF' + + -- kde-platform-theme: restore upstream platform theme (appended by nix-flake); + -- plasma-integration + Darkly are installed system-side (desktop.nix) + hl.env("QT_QPA_PLATFORMTHEME", "kde") + EOF + echo "Restored QT_QPA_PLATFORMTHEME=kde in hypr/custom/env.lua" + fi + ''; + + # Same override for non-Hyprland entry points (illogical-flake's environment.nix + # sets this to qt6ct) + home.sessionVariables.QT_QPA_PLATFORMTHEME = lib.mkForce "kde"; + + # The illogical-flake copy step deletes/recreates ~/.config/hypr while + # Hyprland is running; its mid-copy reload fails ("cannot open hyprland.lua") + # and the error banner sticks. Reload once the configs are back in place. + home.activation.reloadHyprland = lib.hm.dag.entryAfter [ "hyprlandMonitorLayout" ] '' + for instance in /run/user/$(id -u)/hypr/*/; do + [ -d "$instance" ] || continue + HYPRLAND_INSTANCE_SIGNATURE="$(basename "$instance")" \ + /run/current-system/sw/bin/hyprctl reload >/dev/null 2>&1 || true + done + ''; + + # Disable the desktop background clock widget. config.json is only copied on + # first install (user edits persist), so this mainly matters for fresh + # installs; the guard keeps later manual edits to other keys untouched. + home.activation.disableBackgroundClock = lib.hm.dag.entryAfter [ "copyIllogicalImpulseConfigs" ] '' + iiConfig="$HOME/.config/illogical-impulse/config.json" + if [ -f "$iiConfig" ] && [ "$(${pkgs.jq}/bin/jq '.background.widgets.clock.enable' "$iiConfig")" != "false" ]; then + ${pkgs.jq}/bin/jq '.background.widgets.clock.enable = false' "$iiConfig" > "$iiConfig.tmp" \ + && mv "$iiConfig.tmp" "$iiConfig" + echo "Disabled background clock widget in illogical-impulse config.json" + fi + ''; + + # The dots swap terminal copy/interrupt conventions: foot binds Ctrl+C to + # clipboard-copy and moves SIGINT (\x03) to Ctrl+Shift+C; kitty maps Ctrl+C + # to copy_or_interrupt (copies whenever text is selected); foot also puts + # paste on Ctrl+V. Restore standard behavior — Ctrl+C interrupts, copy/paste + # on Ctrl+Shift+C/V — re-applied after every recopy like the hypr overrides. + home.activation.footCtrlC = lib.hm.dag.entryAfter [ "copyIllogicalImpulseConfigs" ] '' + footIni="$HOME/.config/foot/foot.ini" + if [ -f "$footIni" ] && grep -q '^clipboard-copy=Control+c$' "$footIni"; then + ${pkgs.gnused}/bin/sed -i \ + -e 's/^clipboard-copy=Control+c$/clipboard-copy=Control+Shift+c/' \ + -e 's/^clipboard-paste=Control+v$/clipboard-paste=Control+Shift+v/' \ + -e '/^\\x03=Control+Shift+c$/d' \ + "$footIni" + echo "Restored default Ctrl+C/Ctrl+V in foot.ini" + fi + ''; + + home.activation.kittyCtrlC = lib.hm.dag.entryAfter [ "copyIllogicalImpulseConfigs" ] '' + kittyConf="$HOME/.config/kitty/kitty.conf" + if [ -f "$kittyConf" ] && ! grep -q 'Restore default Ctrl+C' "$kittyConf"; then + cat >> "$kittyConf" << 'EOF' + +# Restore default Ctrl+C (appended by nix-flake); copy is Ctrl+Shift+C +map ctrl+c +EOF + echo "Unmapped Ctrl+C in kitty.conf" + fi + ''; + + # Point xdg-desktop-portal-hyprland at the preview share picker. xdph.conf + # lives in ~/.config/hypr, which the recopy wipes, so recreate it each switch. + home.activation.xdphSharePicker = lib.hm.dag.entryAfter [ "copyIllogicalImpulseConfigs" ] '' + cat > "$HOME/.config/hypr/xdph.conf" << 'EOF' + # Written by nix-flake (recreated every switch; the dots wipe this dir) + screencopy { + custom_picker_binary = ${share-picker-themed} + } + EOF + ''; + + # GTK apps decide light/dark from this key (via the settings portal). The + # dots' Arch installer sets it imperatively; the flake port never does, so + # GTK apps came up light while the shell rendered dark. + dconf.settings."org/gnome/desktop/interface".color-scheme = "prefer-dark"; + + # Share picker: single-click selection and Material-You styling. The colors + # reference GTK named colors from ~/.config/gtk-4.0/gtk.css, which matugen + # regenerates from the wallpaper — so the picker re-themes automatically. + # (This dir isn't shipped by the dots, so plain xdg.configFile works.) + xdg.configFile."hyprland-preview-share-picker/config.yaml".text = '' + stylesheets: + - generated.css + windows: + clicks: 1 + outputs: + clicks: 1 + ''; + # Template for the wrapper above: @token@ placeholders are replaced with hex + # values from matugen's colors.json (Material tokens, current light/dark set). + xdg.configFile."hyprland-preview-share-picker/style.css.in".text = '' + .window { + background-color: @surface@; + color: @on_surface@; + border: 1px solid @outline_variant@; + border-radius: 24px; + padding: 10px; + font-family: "Google Sans Flex", "Rubik", sans-serif; + font-size: 15px; + } + .page, + .notebook, + .notebook stack { + background: transparent; + } + .notebook header { + background: transparent; + border: none; + margin-bottom: 6px; + } + .notebook header tab { + border-radius: 9999px; + padding: 6px 18px; + margin: 4px 3px; + background: transparent; + color: @on_surface_variant@; + transition: background-color 150ms ease; + outline: none; + box-shadow: none; + } + .notebook header tab:hover { + background-color: @surface_container_high@; + } + .notebook header tab:checked { + background-color: @primary_container@; + color: @on_primary_container@; + } + .card { + background-color: @surface_container@; + color: @on_surface@; + border: 2px solid transparent; + border-radius: 18px; + padding: 6px; + transition: background-color 150ms ease, border-color 150ms ease; + } + .card:hover { + background-color: @surface_container_high@; + border-color: @primary@; + } + .card-loading { + background-color: @surface_container@; + border-radius: 18px; + } + .image { + border-radius: 12px; + background: transparent; + } + .image-label { + color: @on_surface_variant@; + font-size: 0.92em; + margin-top: 4px; + } + /* The outputs view wraps each preview card in a classless GtkButton whose + stock background shows as a white matte — neutralize it and let the + inner .card carry the visuals. */ + .page button:not(.region-button) { + background: none; + border: none; + box-shadow: none; + padding: 0; + outline: none; + } + .page button:not(.region-button):hover, + .page button:not(.region-button):active { + background: none; + } + /* `background` shorthand, not background-color: the theme's + background-image gradient must be reset too. */ + .region-button { + background: @primary_container@; + color: @on_primary_container@; + border: none; + border-radius: 9999px; + padding: 14px 32px; + font-size: 1.1em; + outline: none; + box-shadow: none; + } + .region-button:hover { + background: @secondary_container@; + } + .restore-button { + background-color: @surface_container@; + color: @on_surface_variant@; + border: none; + border-radius: 9999px; + padding: 6px 14px; + margin-top: 8px; + } + .restore-button check { + background-color: @surface_container_highest@; + border: 1px solid @outline@; + border-radius: 6px; + } + .restore-button check:checked { + background-color: @primary@; + color: @on_primary@; + border-color: @primary@; + } + ''; + + # The illogical-flake fish module generates its own config.fish, dropping the + # dots' `set fish_greeting` — so the "Welcome to fish" banner came back. + programs.fish.interactiveShellInit = "set -g fish_greeting"; + programs.git = { enable = true; userName = "zephrynis"; userEmail = "zephrynis.yt@gmail.com"; }; + # The dots run `hyprctl setcursor Bibata-Modern-Classic 24` but don't install + # the theme (an assumed system dependency on Arch) + home.pointerCursor = { + name = "Bibata-Modern-Classic"; + package = pkgs.bibata-cursors; + size = 24; + gtk.enable = true; + x11.enable = true; + }; + + + # Two profiles: personal (default, purple) and work (blue). Firefox has no + # native per-profile accent, so each profile tints its toolbars via + # userChrome.css. Launch work with `firefox -P work` (desktop entry below). + programs.firefox = { + enable = true; + profiles = + let + addons = pkgs.callPackage "${inputs.firefox-addons}/pkgs/firefox-addons" { + # Same wiring as the upstream flake's overlay, but against the system + # pkgs so nixpkgs.config (allowUnfree, for 1Password) applies + buildMozillaXpiAddon = + (import "${inputs.firefox-addons}/lib/mozilla.nix" { inherit lib; }) + .mkBuildMozillaXpiAddon { inherit (pkgs) fetchurl stdenv; }; + }; + settings = { + # Load userChrome.css + "toolkit.legacyUserProfileCustomizations.stylesheets" = true; + # Auto-enable the declaratively installed extensions + "extensions.autoDisableScopes" = 0; + }; + accent = color: '' + #navigator-toolbox { + background-color: ${color} !important; + color: #f4f2fa !important; + } + ''; + in + { + personal = { + id = 0; + isDefault = true; + inherit settings; + extensions.packages = [ addons.ublock-origin addons.proton-pass ]; + userChrome = accent "#45256e"; + }; + work = { + id = 1; + inherit settings; + extensions.packages = [ addons.ublock-origin addons.onepassword-password-manager ]; + userChrome = accent "#1e3a6e"; + }; + }; + }; + + # Launcher for the work profile; plain `firefox` opens personal (the default) + xdg.desktopEntries.firefox-work = { + name = "Firefox (Work)"; + genericName = "Web Browser"; + exec = "firefox -P work --name firefox-work %U"; + icon = "firefox"; + terminal = false; + categories = [ "Network" "WebBrowser" ]; + }; + programs.direnv = { enable = true; nix-direnv.enable = true; @@ -32,7 +406,39 @@ programs.vscode.enable = true; + # Spotify + Spicetify. The module installs its own wrapped Spotify, so + # pkgs.spotify is intentionally absent from home.packages. spicyLyrics is a + # first-class packaged extension (word-by-word karaoke lyrics view). + programs.spicetify = + let + spicePkgs = inputs.spicetify-nix.legacyPackages.${pkgs.stdenv.hostPlatform.system}; + in + { + enable = true; + enabledExtensions = with spicePkgs.extensions; [ + spicyLyrics + ]; + }; + + # File manager; gvfs/udisks2 backends are enabled system-side in desktop.nix. + # xdg-open and apps use this for directories. The claude-cli handler was + # registered imperatively by claude-code — kept here since HM now owns + # mimeapps.list. + xdg.mimeApps = { + enable = true; + defaultApplications = { + "inode/directory" = "org.gnome.Nautilus.desktop"; + "x-scheme-handler/claude-cli" = "claude-code-url-handler.desktop"; + }; + }; + home.packages = with pkgs; [ + # As a plain package, not programs.gh — the HM module symlinks a read-only + # config.yml into the store, which breaks `gh auth login`'s first-run write. + gh + nautilus + slack + claude-code ripgrep fd fzf