From 43fcacca5b019d35313995fd3a482bc5e821637c Mon Sep 17 00:00:00 2001 From: suri-codes Date: Tue, 11 Aug 2026 09:53:48 -0700 Subject: [PATCH] A new Epoch. --- flake.lock | 12 +- hosts/Styx/configuration.nix | 2 + modules/home/term/ghostty/ghostty/config | 2 +- modules/home/tools/default.nix | 1 + modules/home/wm/hyprland/default.nix | 20 +- modules/home/wm/hyprland/hyprland.lua | 358 +++++++++++++++++++++++ modules/home/wm/niri/config.kdl | 2 +- users/suri/Styx.nix | 1 + 8 files changed, 389 insertions(+), 9 deletions(-) create mode 100644 modules/home/wm/hyprland/hyprland.lua diff --git a/flake.lock b/flake.lock index 4532e3e..ced1886 100644 --- a/flake.lock +++ b/flake.lock @@ -236,11 +236,11 @@ }, "nixpkgs_4": { "locked": { - "lastModified": 1786247143, - "narHash": "sha256-8S3Kcxs7D4UtxJxSJZz0m14CGhuW0MxfrIwJxeGWGnQ=", + "lastModified": 1786384358, + "narHash": "sha256-RzPPiWeUtuvymnpuEWsdtzli5w4kjZs49FqEs3/1u+I=", "owner": "nixos", "repo": "nixpkgs", - "rev": "279b4a8275f032c566576b3f181fa0f27197f588", + "rev": "2fcb964de67fcf60b43471c55d5d99e61a9ccb5a", "type": "github" }, "original": { @@ -270,11 +270,11 @@ ] }, "locked": { - "lastModified": 1786429008, - "narHash": "sha256-DGJqGezkI3gxxHvK7Nns8juWgv1o4POW+3kRR8utWzg=", + "lastModified": 1786462142, + "narHash": "sha256-ESBm32DJg1yLiPnXw88n5h8RcrdL2adc7TuLhtPzeow=", "owner": "noctalia-dev", "repo": "noctalia", - "rev": "2c0bb1d341193845576da39057c49d2a507afc41", + "rev": "b62efdd0dc163bf593358ada70b4efa76fec3d27", "type": "github" }, "original": { diff --git a/hosts/Styx/configuration.nix b/hosts/Styx/configuration.nix index 6d615dd..c694bb6 100644 --- a/hosts/Styx/configuration.nix +++ b/hosts/Styx/configuration.nix @@ -49,6 +49,8 @@ package = inputs.niri.packages.${pkgs.system}.niri-unstable; }; + programs.hyprland.enable = true; + # xdg.portal = { # enable = true; # extraPortals = [ diff --git a/modules/home/term/ghostty/ghostty/config b/modules/home/term/ghostty/ghostty/config index c38cd9d..8a143b7 100644 --- a/modules/home/term/ghostty/ghostty/config +++ b/modules/home/term/ghostty/ghostty/config @@ -100,5 +100,5 @@ theme = Monokai Pro window-decoration = none -# background-opacity = 1 +background-opacity = 0.75 background-blur = false diff --git a/modules/home/tools/default.nix b/modules/home/tools/default.nix index 9b490ce..b8c280f 100644 --- a/modules/home/tools/default.nix +++ b/modules/home/tools/default.nix @@ -55,6 +55,7 @@ timr-tui claude-code + kitty # libqalculate (pkgs.writeShellApplication { diff --git a/modules/home/wm/hyprland/default.nix b/modules/home/wm/hyprland/default.nix index c915eb0..e3e4dd1 100644 --- a/modules/home/wm/hyprland/default.nix +++ b/modules/home/wm/hyprland/default.nix @@ -1 +1,19 @@ -{ ... }: { } +{ + lib, + pkgs, + config, + ... +}: +with lib; +let + cfg = config.nyx.hyprland; +in +{ + options.nyx.hyprland = { + enable = mkEnableOption "Hyprland Window Manager"; + }; + config = mkIf (cfg.enable && pkgs.stdenv.isLinux) { + home.file.".config/hypr/hyprland.lua".source = + config.lib.file.mkOutOfStoreSymlink "${config.home.homeDirectory}/nyx/modules/home/wm/hyprland/hyprland.lua"; + }; +} diff --git a/modules/home/wm/hyprland/hyprland.lua b/modules/home/wm/hyprland/hyprland.lua new file mode 100644 index 0000000..0944a30 --- /dev/null +++ b/modules/home/wm/hyprland/hyprland.lua @@ -0,0 +1,358 @@ +-- Hyprland config (Lua format, Hyprland >= 0.55). +-- Mirrors modules/home/wm/niri/config.kdl as closely as Hyprland's model allows, +-- using the "scrolling" layout (Hyprland's niri-style column layout). +-- +-- A few niri actions have no Hyprland equivalent and are intentionally left +-- unbound rather than faked: toggle-keyboard-shortcuts-inhibit, move-workspace +-- (reordering a workspace among others), move-column-to-first/last, and +-- per-window height presets. Direction args use Hyprland's long form +-- (left/right/up/down) for window/focus dispatchers and short form (l/r/u/d) +-- for monitor and scrolling-layout column commands, matching the official +-- example config and wiki respectively. + +local mainMod = "SUPER" +local terminal = "ghostty" + +------------------ +---- MONITORS ---- +------------------ + +hl.monitor({ + output = "", + mode = "preferred", + position = "auto", + scale = "auto", +}) + +----------------------- +---- LOOK AND FEEL ---- +----------------------- + +hl.config({ + general = { + layout = "scrolling", + + -- niri: layout { gaps 12 } + gaps_in = 12, + gaps_out = 12, + + -- niri: layout { focus-ring { width 3; active-color; inactive-color } } + border_size = 3, + col = { + active_border = "#cccccc", + inactive_border = "#505050", + }, + }, + + decoration = { + -- niri: window-rule { geometry-corner-radius 20; clip-to-geometry true } (applied globally) + rounding = 20, + + -- niri: layout { shadow { on; softness 30; spread 5; offset x=0 y=5 } } + shadow = { + enabled = true, + range = 30, + render_power = 3, + offset = { 0, 5 }, + }, + + -- niri: window-rule { background-effect { blur true; xray false } } (global) + blur {} + blur = { + enabled = true, + xray = false, + passes = 2, + size = 3, + noise = 0.05, + vibrancy = 1.0, -- closest analog to niri's blur.saturation; scales differently + }, + }, + + input = { + -- niri: input { keyboard { repeat-delay 350; repeat-rate 45; numlock } } + numlock_by_default = true, + repeat_delay = 350, + repeat_rate = 45, + + -- niri: input { mouse { accel-profile "flat" } } + accel_profile = "flat", + + -- niri: input { focus-follows-mouse } + follow_mouse = 1, + }, + + scrolling = { + -- niri: layout { default-column-width { proportion 0.5 } } + column_width = 0.5, + + -- niri: layout { preset-column-widths { proportion 0.33333; 0.5; 0.66667 } } + explicit_column_widths = "0.33333, 0.5, 0.66667", + + -- niri: layout { center-focused-column "on-overflow" } (closest available mode) + focus_fit_method = 1, -- 0 = center, 1 = fit + + fullscreen_on_one_column = true, + }, + + binds = { + -- niri: binds Mod+WheelScroll* use cooldown-ms=150 + scroll_event_delay = 150, + }, +}) + +-- niri: cursor { xcursor-theme "Bibata-Modern-Ice"; xcursor-size 22 } +-- Hyprland (hyprcursor) reads cursor theme/size from the environment. +hl.env("XCURSOR_THEME", "Bibata-Modern-Ice") +hl.env("XCURSOR_SIZE", "22") +hl.env("HYPRCURSOR_THEME", "Bibata-Modern-Ice") +hl.env("HYPRCURSOR_SIZE", "22") + +----------------------- +---- WORKSPACES ------ +----------------------- + +-- niri: workspace "browser"/"work"/"messaging"/"inbox"/"tunes" +hl.workspace_rule({ workspace = "name:browser", persistent = true }) +hl.workspace_rule({ workspace = "name:work", persistent = true }) +hl.workspace_rule({ workspace = "name:messaging", persistent = true }) +hl.workspace_rule({ workspace = "name:inbox", persistent = true }) +hl.workspace_rule({ workspace = "name:tunes", persistent = true }) + +------------------- +---- AUTOSTART ---- +------------------- + +-- niri: spawn-at-startup ... +hl.on("hyprland.start", function() + hl.exec_cmd("openrgb --client 127.0.0.1:6742 --mode direct --color DDDDDD") + hl.exec_cmd("noctalia") + hl.exec_cmd("zeditor") + hl.exec_cmd("zen") + hl.exec_cmd("zulip") + hl.exec_cmd("spotify") + hl.exec_cmd("vesktop") + hl.exec_cmd("mailspring --password-store=gnome-libsecret") +end) + +--------------------- +---- KEYBINDINGS ---- +--------------------- + +-- Core Noctalia binds +hl.bind(mainMod .. " + Space", hl.dsp.exec_cmd("noctalia msg panel-toggle launcher")) +hl.bind(mainMod .. " + S", hl.dsp.exec_cmd("noctalia msg panel-toggle session")) +hl.bind(mainMod .. " + SHIFT + S", hl.dsp.exec_cmd("noctalia msg panel-toggle control-center")) +hl.bind(mainMod .. " + comma", hl.dsp.exec_cmd("noctalia msg settings-toggle")) + +-- Audio & Brightness +hl.bind("XF86AudioRaiseVolume", hl.dsp.exec_cmd("noctalia msg volume-up"), { repeating = true }) +hl.bind("XF86AudioLowerVolume", hl.dsp.exec_cmd("noctalia msg volume-down"), { repeating = true }) +hl.bind("XF86AudioMute", hl.dsp.exec_cmd("noctalia msg volume-mute")) +hl.bind("XF86MonBrightnessUp", hl.dsp.exec_cmd("noctalia msg brightness-up"), { repeating = true }) +hl.bind("XF86MonBrightnessDown", hl.dsp.exec_cmd("noctalia msg brightness-down"), { repeating = true }) + +-- My specific keybinds +hl.bind(mainMod .. " + SHIFT + Return", hl.dsp.exec_cmd(terminal .. " -e zellij -l welcome"), { description = "Open Ghostty (zellij)" }) +hl.bind(mainMod .. " + Return", hl.dsp.exec_cmd(terminal), { description = "Open Ghostty" }) + +-- niri: toggle-overview -> closest Noctalia equivalent is the window switcher overlay +hl.bind(mainMod .. " + O", hl.dsp.exec_cmd("noctalia msg window-switcher"), { repeating = false }) +hl.bind(mainMod .. " + Q", hl.dsp.window.close(), { repeating = false }) + +-- Column focus / movement (scrolling layout) +hl.bind(mainMod .. " + left", hl.dsp.layout("focus l")) +hl.bind(mainMod .. " + right", hl.dsp.layout("focus r")) +hl.bind(mainMod .. " + H", hl.dsp.layout("focus l")) +hl.bind(mainMod .. " + L", hl.dsp.layout("focus r")) + +-- Window focus within a column +hl.bind(mainMod .. " + down", hl.dsp.focus({ direction = "down" })) +hl.bind(mainMod .. " + up", hl.dsp.focus({ direction = "up" })) + +hl.bind(mainMod .. " + CTRL + left", hl.dsp.layout("swapcol l")) +hl.bind(mainMod .. " + CTRL + right", hl.dsp.layout("swapcol r")) +hl.bind(mainMod .. " + CTRL + H", hl.dsp.layout("swapcol l")) +hl.bind(mainMod .. " + CTRL + L", hl.dsp.layout("swapcol r")) + +hl.bind(mainMod .. " + CTRL + down", hl.dsp.window.move({ direction = "down" })) +hl.bind(mainMod .. " + CTRL + up", hl.dsp.window.move({ direction = "up" })) + +-- niri's "focus/move-window-or-workspace" fallback variants: Hyprland has no +-- auto-fallback-to-workspace, so these alias to the plain window focus/move. +hl.bind(mainMod .. " + J", hl.dsp.focus({ direction = "down" })) +hl.bind(mainMod .. " + K", hl.dsp.focus({ direction = "up" })) +hl.bind(mainMod .. " + CTRL + J", hl.dsp.window.move({ direction = "down" })) +hl.bind(mainMod .. " + CTRL + K", hl.dsp.window.move({ direction = "up" })) + +-- niri: focus/move-column-first/last -> approximated with a full view fit +hl.bind(mainMod .. " + Home", hl.dsp.layout("fit tobeg")) +hl.bind(mainMod .. " + End", hl.dsp.layout("fit toend")) + +-- Monitor focus / move column to monitor +hl.bind(mainMod .. " + SHIFT + left", hl.dsp.focus({ monitor = "l" })) +hl.bind(mainMod .. " + SHIFT + down", hl.dsp.focus({ monitor = "d" })) +hl.bind(mainMod .. " + SHIFT + up", hl.dsp.focus({ monitor = "u" })) +hl.bind(mainMod .. " + SHIFT + right", hl.dsp.focus({ monitor = "r" })) +hl.bind(mainMod .. " + SHIFT + H", hl.dsp.focus({ monitor = "l" })) +hl.bind(mainMod .. " + SHIFT + J", hl.dsp.focus({ monitor = "d" })) +hl.bind(mainMod .. " + SHIFT + K", hl.dsp.focus({ monitor = "u" })) +hl.bind(mainMod .. " + SHIFT + L", hl.dsp.focus({ monitor = "r" })) + +hl.bind(mainMod .. " + SHIFT + CTRL + left", hl.dsp.window.move({ monitor = "l", follow = true })) +hl.bind(mainMod .. " + SHIFT + CTRL + down", hl.dsp.window.move({ monitor = "d", follow = true })) +hl.bind(mainMod .. " + SHIFT + CTRL + up", hl.dsp.window.move({ monitor = "u", follow = true })) +hl.bind(mainMod .. " + SHIFT + CTRL + right", hl.dsp.window.move({ monitor = "r", follow = true })) +hl.bind(mainMod .. " + SHIFT + CTRL + H", hl.dsp.window.move({ monitor = "l", follow = true })) +hl.bind(mainMod .. " + SHIFT + CTRL + J", hl.dsp.window.move({ monitor = "d", follow = true })) +hl.bind(mainMod .. " + SHIFT + CTRL + K", hl.dsp.window.move({ monitor = "u", follow = true })) +hl.bind(mainMod .. " + SHIFT + CTRL + L", hl.dsp.window.move({ monitor = "r", follow = true })) + +-- Workspaces (relative) +hl.bind(mainMod .. " + Page_Down", hl.dsp.focus({ workspace = "+1" })) +hl.bind(mainMod .. " + Page_Up", hl.dsp.focus({ workspace = "-1" })) + +hl.bind(mainMod .. " + CTRL + Page_Down", hl.dsp.window.move({ workspace = "+1", follow = true })) +hl.bind(mainMod .. " + CTRL + Page_Up", hl.dsp.window.move({ workspace = "-1", follow = true })) + +hl.bind(mainMod .. " + mouse_down", hl.dsp.focus({ workspace = "+1" })) +hl.bind(mainMod .. " + mouse_up", hl.dsp.focus({ workspace = "-1" })) +hl.bind(mainMod .. " + CTRL + mouse_down", hl.dsp.window.move({ workspace = "+1", follow = true })) +hl.bind(mainMod .. " + CTRL + mouse_up", hl.dsp.window.move({ workspace = "-1", follow = true })) + +hl.bind(mainMod .. " + mouse_right", hl.dsp.layout("focus r")) +hl.bind(mainMod .. " + mouse_left", hl.dsp.layout("focus l")) +hl.bind(mainMod .. " + CTRL + mouse_right", hl.dsp.layout("swapcol r")) +hl.bind(mainMod .. " + CTRL + mouse_left", hl.dsp.layout("swapcol l")) + +hl.bind(mainMod .. " + SHIFT + mouse_down", hl.dsp.layout("focus r")) +hl.bind(mainMod .. " + SHIFT + mouse_up", hl.dsp.layout("focus l")) +hl.bind(mainMod .. " + CTRL + SHIFT + mouse_down", hl.dsp.layout("swapcol r")) +hl.bind(mainMod .. " + CTRL + SHIFT + mouse_up", hl.dsp.layout("swapcol l")) + +-- Workspaces (absolute) +for i = 1, 9 do + hl.bind(mainMod .. " + " .. i, hl.dsp.focus({ workspace = i })) + hl.bind(mainMod .. " + CTRL + " .. i, hl.dsp.window.move({ workspace = i, follow = true })) +end + +-- Column width presets +hl.bind(mainMod .. " + R", hl.dsp.layout("colresize +conf")) +hl.bind(mainMod .. " + SHIFT + R", hl.dsp.layout("colresize -conf")) + +hl.bind(mainMod .. " + F", hl.dsp.window.fullscreen({ mode = "maximized" }), { description = "Maximize column" }) +hl.bind(mainMod .. " + SHIFT + F", hl.dsp.window.fullscreen({ mode = "fullscreen" })) + +hl.bind(mainMod .. " + C", hl.dsp.window.center()) +hl.bind(mainMod .. " + CTRL + C", hl.dsp.layout("fit all")) + +hl.bind(mainMod .. " + minus", hl.dsp.layout("colresize -0.1")) +hl.bind(mainMod .. " + equal", hl.dsp.layout("colresize +0.1")) + +-- Floating +hl.bind(mainMod .. " + V", hl.dsp.window.float({ action = "toggle" })) + +-- Screenshots (via Noctalia's compositor-agnostic screenshot service) +hl.bind("Print", hl.dsp.exec_cmd("noctalia msg screenshot-region")) +hl.bind("CTRL + Print", hl.dsp.exec_cmd("noctalia msg screenshot-fullscreen")) +hl.bind(mainMod .. " + SHIFT + P", hl.dsp.exec_cmd("noctalia msg screenshot-region")) + +-- Quit +hl.bind(mainMod .. " + SHIFT + E", hl.dsp.exit()) +hl.bind("CTRL + ALT + Delete", hl.dsp.exit()) + +-- Named workspaces +hl.bind(mainMod .. " + B", hl.dsp.focus({ workspace = "name:browser" })) +hl.bind(mainMod .. " + W", hl.dsp.focus({ workspace = "name:work" })) +hl.bind(mainMod .. " + D", hl.dsp.focus({ workspace = "name:messaging" })) +hl.bind(mainMod .. " + M", hl.dsp.focus({ workspace = "name:inbox" })) +hl.bind(mainMod .. " + T", hl.dsp.focus({ workspace = "name:tunes" })) + +-------------------------------- +---- WINDOWS AND WORKSPACES ---- +-------------------------------- + +hl.window_rule({ + name = "zen-browser-workspace", + match = { initial_class = "zen" }, + workspace = "name:browser", + scrolling_width = 1.0, +}) + +-- `float` is a static effect, evaluated once at window creation, but the PiP +-- title only appears after the window opens -- match on it via an event +-- listener instead (per the Hyprland docs' documented workaround). +hl.on("window.title", function(w) + if w ~= nil and w.class == "zen" and w.title == "Picture-in-Picture" then + hl.dispatch(hl.dsp.window.float({ action = "set" })) + end +end) + +hl.window_rule({ + name = "zed-work-workspace", + match = { initial_class = "dev.zed.Zed" }, + workspace = "name:work", + scrolling_width = 1.0, +}) + +hl.window_rule({ + name = "mailspring-inbox-workspace", + match = { initial_class = "Mailspring" }, + workspace = "name:inbox", + scrolling_width = 1.0, +}) + +hl.window_rule({ + name = "spotify-tunes-workspace", + match = { initial_class = "spotify" }, + workspace = "name:tunes", + scrolling_width = 1.0, +}) + +hl.window_rule({ + name = "vesktop-messaging-workspace", + match = { initial_class = "vesktop" }, + workspace = "name:messaging", + scrolling_width = 0.66, +}) + +hl.window_rule({ + name = "zulip-messaging-workspace", + match = { initial_class = "zulip" }, + workspace = "name:messaging", + scrolling_width = 0.66, +}) + +-- niri: window-rule { match app-id="dev.noctalia.Noctalia"; open-floating true; ... } +hl.window_rule({ + match = { class = "dev.noctalia.Noctalia" }, + float = true, + size = { 1080, 920 }, +}) + +------------------------ +---- LAYER RULES ------- +------------------------ + +hl.layer_rule({ + match = { namespace = "^noctalia-(bar-[^\"]+|notification|dock|panel|attached-panel|osd)$" }, + blur = true, + xray = false, +}) + +hl.layer_rule({ + match = { namespace = "noctalia-window-switcher" }, + blur = true, + xray = false, +}) + +------------------------------------ +---- NOCTALIA COLOR TEMPLATES ------- +------------------------------------ + +-- For Noctalia Color templates +-- Guarded: noctalia.lua doesn't exist until Noctalia has applied its Hyprland +-- template at least once (`noctalia config apply` / first shell launch), so a +-- bare require() here would error out the whole config on a fresh system. +local ok, noctaliaTheme = pcall(require, "noctalia") +if ok then + noctaliaTheme.apply_theme() +end diff --git a/modules/home/wm/niri/config.kdl b/modules/home/wm/niri/config.kdl index 7274b2b..356872d 100644 --- a/modules/home/wm/niri/config.kdl +++ b/modules/home/wm/niri/config.kdl @@ -340,7 +340,7 @@ debug { window-rule { background-effect { // disabling blur because my shit is chopped - blur false + blur true xray false } } diff --git a/users/suri/Styx.nix b/users/suri/Styx.nix index 6bad6dd..eb9047e 100644 --- a/users/suri/Styx.nix +++ b/users/suri/Styx.nix @@ -11,6 +11,7 @@ # desktop nyx.niri.enable = true; + nyx.hyprland.enable = true; nyx.noctalia.enable = true; # apps -- 2.51.2