diff --git a/home/arrakis/default.nix b/home/arrakis/default.nix index 1b71de5..f028153 100644 --- a/home/arrakis/default.nix +++ b/home/arrakis/default.nix @@ -26,24 +26,20 @@ # RetroArch (bare — cores/ROMs added by hand, WebDAV configured in-app). custom.gaming.retroarch = true; - wayland.windowManager.hyprland.settings = { - exec-once = [ - ]; + wayland.windowManager.hyprland.extraConfig = '' + hl.workspace_rule({ workspace = "1", monitor = "DP-3" }) + hl.workspace_rule({ workspace = "2", monitor = "eDP-1" }) + hl.workspace_rule({ workspace = "3", monitor = "eDP-1" }) + hl.workspace_rule({ workspace = "4", monitor = "eDP-1" }) - workspace = [ - "1, monitor:DP-3" - "2, monitor:eDP-1" - "3, monitor:eDP-1" - "4, monitor:eDP-1" - ]; - - windowrule = [ - "no_initial_focus on, match:class bevy-.*" - "float on, match:class bevy-.*" - "size 800 600, match:class bevy-.*" - "move 100%-800 100%-600, match:class bevy-.*" - ]; - }; + hl.window_rule({ + match = { class = "bevy-.*" }, + no_initial_focus = true, + float = true, + size = "800 600", + move = "100%-800 100%-600", + }) + ''; # v4 `bar.density` has no v5 equivalent; spacing is tuned via # bar.main.padding / widget_spacing / thickness if needed. diff --git a/home/arrakis/monitors.nix b/home/arrakis/monitors.nix index 2488121..9258e4d 100644 --- a/home/arrakis/monitors.nix +++ b/home/arrakis/monitors.nix @@ -1,12 +1,11 @@ _: { - wayland.windowManager.hyprland.settings = { - monitor = [ - "eDP-1, preferred, 0x0, 1, bitdepth, 8" - "DP-1, preferred, auto-up, 1" - "DP-2, preferred, auto-up, 1" - "DP-3, preferred, auto-up, 1" - "DP-4, preferred, auto-up, 1" - ]; - }; + # Hyprland 0.55+ is Lua-only (see home/common/programs/hyprland). + wayland.windowManager.hyprland.extraConfig = '' + hl.monitor({ output = "eDP-1", mode = "preferred", position = "0x0", scale = 1, bitdepth = 8 }) + hl.monitor({ output = "DP-1", mode = "preferred", position = "auto-up", scale = 1 }) + hl.monitor({ output = "DP-2", mode = "preferred", position = "auto-up", scale = 1 }) + hl.monitor({ output = "DP-3", mode = "preferred", position = "auto-up", scale = 1 }) + hl.monitor({ output = "DP-4", mode = "preferred", position = "auto-up", scale = 1 }) + ''; } diff --git a/home/common/programs/hyprland/default.nix b/home/common/programs/hyprland/default.nix index c235491..475f33c 100644 --- a/home/common/programs/hyprland/default.nix +++ b/home/common/programs/hyprland/default.nix @@ -66,260 +66,214 @@ in enable = true; package = null; portalPackage = null; - # Keep the hyprlang config format (the new default is "lua"); our - # settings are written as hyprlang. - configType = "hyprlang"; + # Hyprland 0.55+ removed the hyprlang parser entirely — the config format + # is now Lua only (see home/common/programs/hyprland — the whole config + # below is raw Lua using the hl.* API). home-manager still emits its own + # systemd-session start hook, so we don't duplicate that here. + configType = "lua"; plugins = [ ]; - settings = { - plugin = { - overview = { - autoDrag = true; - exitOnClick = true; - exitOnSwitch = true; - showNewWorkspace = false; - showEmptyWorkspace = false; - }; - }; - input = { - kb_options = "ctrl:nocaps"; - repeat_delay = 150; + extraConfig = '' + local mod = "SUPER" - touchpad = { - natural_scroll = "yes"; - }; - numlock_by_default = true; - }; + -- look & feel / behaviour + hl.config({ + general = { + border_size = 1, + gaps_in = 0, + gaps_out = 0, + snap = { + enabled = true, + border_overlap = true, + }, + col = { + inactive_border = "0x99999999", + active_border = "0x99999999", + }, + }, - cursor = { - no_hardware_cursors = "yes"; - # Auto-hide the pointer after 3s of no movement so a parked cursor - # doesn't sit on top of a game (visible in the stream too). It - # reappears the instant the mouse moves. - inactive_timeout = 3; - }; + input = { + kb_options = "ctrl:nocaps", + repeat_delay = 150, + touchpad = { + natural_scroll = true, + }, + numlock_by_default = true, + }, - general = { - border_size = 1; - gaps_in = 0; - gaps_out = 0; - snap = { - enabled = true; - border_overlap = true; - }; + cursor = { + no_hardware_cursors = true, + -- Auto-hide the pointer after 3s of no movement so a parked cursor + -- doesn't sit on top of a game (visible in the stream too). It + -- reappears the instant the mouse moves. + inactive_timeout = 3, + }, - "col.inactive_border" = "0x99999999"; - "col.active_border" = "0x99999999"; - }; + misc = { + disable_hyprland_logo = true, + disable_splash_rendering = true, + on_focus_under_fullscreen = 2, + }, - misc = { - disable_hyprland_logo = true; - disable_splash_rendering = true; - on_focus_under_fullscreen = 2; - }; + ecosystem = { + no_update_news = true, + }, - ecosystem = { - no_update_news = true; - }; + xwayland = { + force_zero_scaling = true, + }, - xwayland = { - force_zero_scaling = true; - }; + decoration = { + blur = { + enabled = true, + popups = false, + }, + shadow = { + enabled = false, + }, + }, + }) - decoration = { - blur = { - enabled = true; - popups = false; - }; - shadow = { - enabled = false; - }; - }; + -- keep default animations, but speed them all up + hl.animation({ leaf = "global", enabled = true, speed = 2, bezier = "default" }) - # keep default animations, but speed them all up - animations = { - animation = [ - "global, 1, 2, default" - ]; - }; + -- environment + hl.env("GDK_SCALE", "2.0") + hl.env("XCURSOR_SIZE", "${toString cfg.cursorSize}") + hl.env("HYPRCURSOR_THEME", "rose-pine-hyprcursor") + hl.env("HYPRCURSOR_SIZE", "${toString cfg.cursorSize}") + -- Route Qt6 apps (incl. the xdph screen-share picker) through qt6ct so + -- the noctalia-generated color scheme actually applies. Without this + -- var the qt6ct.conf is never read. + hl.env("QT_QPA_PLATFORMTHEME", "qt6ct") - env = [ - "GDK_SCALE, 2.0" - "XCURSOR_SIZE, ${toString cfg.cursorSize}" - "HYPRCURSOR_THEME, rose-pine-hyprcursor" - "HYPRCURSOR_SIZE, ${toString cfg.cursorSize}" - # Route Qt6 apps (incl. the xdph screen-share picker) through qt6ct so - # the noctalia-generated color scheme actually applies. Without this - # var the qt6ct.conf is never read. - "QT_QPA_PLATFORMTHEME, qt6ct" - ]; + -- autostart + hl.on("hyprland.start", function() + -- Import the Qt theme var too, so the dbus/systemd-activated + -- xdg-desktop-portal-hyprland (and its share-picker) inherit it. + hl.exec_cmd("dbus-update-activation-environment --systemd WAYLAND_DISPLAY XDG_CURRENT_DESKTOP QT_QPA_PLATFORMTHEME") + hl.exec_cmd("hyprctl setcursor Nordzy-cursors ${toString cfg.cursorSize}") + hl.exec_cmd("hyprsunset") + hl.exec_cmd("noctalia") + hl.exec_cmd("yogurt --tray") + end) - "exec-once" = [ - # Import the Qt theme var too, so the dbus/systemd-activated - # xdg-desktop-portal-hyprland (and its share-picker) inherit it. - "dbus-update-activation-environment --systemd WAYLAND_DISPLAY XDG_CURRENT_DESKTOP QT_QPA_PLATFORMTHEME" - "hyprctl setcursor Nordzy-cursors ${toString cfg.cursorSize}" - "hyprsunset" - "noctalia" - "yogurt --tray" - ]; + -- layer rules + hl.layer_rule({ match = { namespace = "noctalia-wallpaper" }, blur = true, ignore_alpha = 0.5 }) - layerrule = [ - "blur 1, match:namespace noctalia-wallpaper" - "ignore_alpha 0.5, match:namespace noctalia-wallpaper" - ]; - workspace = [ - # no gaps when only window - "w[t1], gapsout:0, gapsin:0" - "w[tg1], gapsout:0, gapsin:0" - "f[1], gapsout:0, gapsin:0" + -- workspace rules + hl.workspace_rule({ workspace = "w[t1]", gaps_out = 0, gaps_in = 0 }) -- no gaps when only window + hl.workspace_rule({ workspace = "w[tg1]", gaps_out = 0, gaps_in = 0 }) + hl.workspace_rule({ workspace = "f[1]", gaps_out = 0, gaps_in = 0 }) + hl.workspace_rule({ workspace = "special:terminal", on_created_empty = "[float; size 1400 1000; center 1] kitty", persistent = false }) + hl.workspace_rule({ workspace = "special:yazi", on_created_empty = "[float; size ${cfg.yaziSize}; center 1] kitty --session sessions/yazi", persistent = false }) - "special:terminal, on-created-empty:[float; size 1400 1000; center 1] kitty, persistent:false" - "special:yazi, on-created-empty:[float; size ${cfg.yaziSize}; center 1] kitty --session sessions/yazi, persistent:false" - ]; + -- window rules + hl.window_rule({ match = { class = "com.gabm.satty" }, float = true }) - windowrule = [ - # satty - "float on, match:class com.gabm.satty" + hl.window_rule({ match = { class = "imv" }, float = true, size = "1920 1080", move = "(cursor_x-(window_w*0.5)) (cursor_y-(window_h*0.5))" }) + hl.window_rule({ match = { class = "mpv" }, float = true, size = "1920 1080", move = "(cursor_x-(window_w*0.5)) (cursor_y-(window_h*0.5))" }) - # imv - "float on, match:class imv" - "size 1920 1080, match:class imv" - "move (cursor_x-(window_w*0.5)) (cursor_y-(window_h*0.5)), match:class imv" + -- webcam overlay (`webcam` command) — small, pinned, bottom-right. + -- Declared after the mpv class rule so its title match wins. + hl.window_rule({ + match = { title = "webcam-overlay" }, + float = true, + size = "360 240", + move = "100%-380 100%-260", + pin = true, + no_initial_focus = true, + border_size = 0, + }) - # mpv - "float on, match:class mpv" - "size 1920 1080, match:class mpv" - "move (cursor_x-(window_w*0.5)) (cursor_y-(window_h*0.5)), match:class mpv" + hl.window_rule({ match = { class = "thunar" }, float = true, size = "1800 1200", move = "(cursor_x-(window_w*0.5)) (cursor_y-(window_h*0.5))" }) + hl.window_rule({ match = { class = [[\.blueman-manager-wrapped]] }, float = true, size = "1200 800", move = "(cursor_x-(window_w*0.5)) (cursor_y-(window_h*0.5))" }) + hl.window_rule({ match = { class = [[org\.pulseaudio\.pavucontrol]] }, float = true, size = "1200 1200", move = "(cursor_x-(window_w*0.5)) (cursor_y-(window_h*0.5))" }) - # webcam overlay (`webcam` command) — small, pinned, bottom-right. - # Title rules come after the mpv class rules so they win. - "float on, match:title webcam-overlay" - "size 360 240, match:title webcam-overlay" - "move 100%-380 100%-260, match:title webcam-overlay" - "pin on, match:title webcam-overlay" - "no_initial_focus on, match:title webcam-overlay" - "border_size 0, match:title webcam-overlay" + -- wine / game installers (Inno Setup temp windows, e.g. Heroic/GOG). + -- Their class is the random "setup_*.tmp" filename, so match by suffix. + hl.window_rule({ match = { class = [[.*\.tmp]] }, float = true, center = true }) - # thunar - "float on, match:class thunar" - "size 1800 1200, match:class thunar" - "move (cursor_x-(window_w*0.5)) (cursor_y-(window_h*0.5)), match:class thunar" + -- metamask + hl.window_rule({ match = { class = [[chrome-nkbihfbeogaeaoehlefnkodbefgpgknn-.*]] }, float = true }) + -- bitwarden, chrome + hl.window_rule({ match = { class = [[chrome-nngceckbapebfimnlniiiahkandclblb-.*]] }, float = true }) + -- claude for chrome + hl.window_rule({ match = { class = [[chrome-fcoeoabgfenejglbffodgkkbkcdhcgfn-.*]] }, float = true, move = "(cursor_x-(window_w*0.5)) (cursor_y-(window_h*0.5))" }) - # bluetooth - "float on, match:class \\.blueman-manager-wrapped" - "size 1200 800, match:class \\.blueman-manager-wrapped" - "move (cursor_x-(window_w*0.5)) (cursor_y-(window_h*0.5)), match:class \\.blueman-manager-wrapped" + -- no gaps when only window + hl.window_rule({ match = { float = false, workspace = "w[t1]" }, border_size = 0, rounding = 0 }) + hl.window_rule({ match = { float = false, workspace = "w[tg1]" }, border_size = 0, rounding = 0 }) + hl.window_rule({ match = { float = false, workspace = "f[1]" }, border_size = 0, rounding = 0 }) - # pavucontrol - "float on, match:class org\\.pulseaudio\\.pavucontrol" - "size 1200 1200, match:class org\\.pulseaudio\\.pavucontrol" - "move (cursor_x-(window_w*0.5)) (cursor_y-(window_h*0.5)), match:class org\\.pulseaudio\\.pavucontrol" + hl.window_rule({ match = { workspace = "special:terminal" }, center = true }) + hl.window_rule({ match = { workspace = "special:yazi" }, center = true }) - # wine / game installers (Inno Setup temp windows, e.g. Heroic/GOG). - # Their class is the random "setup_*.tmp" filename, so match by suffix. - "float on, match:class .*\\.tmp" - "center 1, match:class .*\\.tmp" + -- keybinds + hl.bind(mod .. " + N", hl.dsp.exec_cmd("noctalia msg panel-toggle control-center")) + hl.bind(mod .. " + T", hl.dsp.exec_cmd("kitty")) + hl.bind(mod .. " + V", hl.dsp.window.float({ action = "toggle" })) + hl.bind(mod .. " + Q", hl.dsp.window.close()) + hl.bind(mod .. " + F", hl.dsp.window.fullscreen({ mode = "fullscreen" })) + hl.bind(mod .. " + SHIFT + F", hl.dsp.window.fullscreen({ mode = "maximized" })) - # metamask - "float on, match:class chrome-nkbihfbeogaeaoehlefnkodbefgpgknn-.*" + -- lock (routes through logind -> hypridle lock_cmd -> hyprlock) + hl.bind(mod .. " + CTRL + L", hl.dsp.exec_cmd("loginctl lock-session")) - # bitwarden, chrome - "float on, match:class chrome-nngceckbapebfimnlniiiahkandclblb-.*" + hl.bind(mod .. " + space", hl.dsp.exec_cmd("noctalia msg panel-toggle launcher")) - # claude for chrome - "float on, match:class chrome-fcoeoabgfenejglbffodgkkbkcdhcgfn-.*" - "move (cursor_x-(window_w*0.5)) (cursor_y-(window_h*0.5)), match:class chrome-fcoeoabgfenejglbffodgkkbkcdhcgfn-.*" + -- printscreen + hl.bind("Print", hl.dsp.exec_cmd("hyprshot -m region --raw | satty --filename - --output-filename ~/downloads/screenshots/$(date +%Y-%m-%d_%H-%M-%S).png")) + hl.bind("SHIFT + Print", hl.dsp.exec_cmd("hyprshot -m window --raw | satty --filename - --output-filename ~/downloads/screenshots/$(date +%Y-%m-%d_%H-%M-%S).png")) - # no gaps when only window - "border_size 0, match:float 0, match:workspace w[t1]" - "rounding 0, match:float 0, match:workspace w[t1]" - "border_size 0, match:float 0, match:workspace w[tg1]" - "rounding 0, match:float 0, match:workspace w[tg1]" - "border_size 0, match:float 0, match:workspace f[1]" - "rounding 0, match:float 0, match:workspace f[1]" + -- move focus with mod + hjkl + hl.bind(mod .. " + H", hl.dsp.focus({ direction = "left" })) + hl.bind(mod .. " + J", hl.dsp.focus({ direction = "down" })) + hl.bind(mod .. " + K", hl.dsp.focus({ direction = "up" })) + hl.bind(mod .. " + L", hl.dsp.focus({ direction = "right" })) - "center 1, match:workspace special:terminal" - "center 1, match:workspace special:yazi" - ]; + -- switch / move-to workspaces 1-6 + for i = 1, 6 do + hl.bind(mod .. " + " .. i, hl.dsp.focus({ workspace = i })) + hl.bind(mod .. " + SHIFT + " .. i, hl.dsp.window.move({ workspace = i })) + end - "$mod" = "SUPER"; + -- move active window inside workspace + hl.bind(mod .. " + SHIFT + H", hl.dsp.window.move({ direction = "left" })) + hl.bind(mod .. " + SHIFT + J", hl.dsp.window.move({ direction = "down" })) + hl.bind(mod .. " + SHIFT + K", hl.dsp.window.move({ direction = "up" })) + hl.bind(mod .. " + SHIFT + L", hl.dsp.window.move({ direction = "right" })) - bind = [ - "$mod, n, exec, noctalia msg panel-toggle control-center" - "$mod, t, exec, kitty" - "$mod, v, togglefloating" - "$mod, q, killactive" - "$mod, f, fullscreen, 0" - "$mod SHIFT, f, fullscreen, 1" + -- scroll through workspaces with mod + scroll + hl.bind(mod .. " + mouse_down", hl.dsp.focus({ workspace = "e+1" })) + hl.bind(mod .. " + mouse_up", hl.dsp.focus({ workspace = "e-1" })) - # lock (routes through logind -> hypridle lock_cmd -> hyprlock) - "$mod CTRL, l, exec, loginctl lock-session" + -- special workspaces (toggle + recenter in one handler) + hl.bind(mod .. " + X", function() + hl.dispatch(hl.dsp.workspace.toggle_special("terminal")) + hl.dispatch(hl.dsp.window.center()) + end) + hl.bind(mod .. " + E", function() + hl.dispatch(hl.dsp.workspace.toggle_special("yazi")) + hl.dispatch(hl.dsp.window.center()) + end) - "$mod, space, exec, noctalia msg panel-toggle launcher" + -- volume / brightness (locked so they work on the lock screen; repeat on hold) + hl.bind("XF86AudioRaiseVolume", hl.dsp.exec_cmd("wpctl set-volume @DEFAULT_AUDIO_SINK@ 5%+"), { locked = true, repeating = true }) + hl.bind("XF86AudioLowerVolume", hl.dsp.exec_cmd("wpctl set-volume @DEFAULT_AUDIO_SINK@ 5%-"), { locked = true, repeating = true }) + hl.bind("XF86MonBrightnessUp", hl.dsp.exec_cmd("brightnessctl set 5%+"), { locked = true, repeating = true }) + hl.bind("XF86MonBrightnessDown", hl.dsp.exec_cmd("brightnessctl set 5%-"), { locked = true, repeating = true }) - # printscreen - ", Print, exec, hyprshot -m region --raw | satty --filename - --output-filename ~/downloads/screenshots/$(date +%Y-%m-%d_%H-%M-%S).png" - "SHIFT, Print, exec, hyprshot -m window --raw | satty --filename - --output-filename ~/downloads/screenshots/$(date +%Y-%m-%d_%H-%M-%S).png" + -- media keys (locked) + hl.bind("XF86AudioMute", hl.dsp.exec_cmd("pactl set-sink-mute @DEFAULT_SINK@ toggle"), { locked = true }) + hl.bind("XF86AudioMicMute", hl.dsp.exec_cmd("volumectl -m toggle-mute"), { locked = true }) + hl.bind("XF86AudioNext", hl.dsp.exec_cmd("playerctl next"), { locked = true }) + hl.bind("XF86AudioPrev", hl.dsp.exec_cmd("playerctl previous"), { locked = true }) + hl.bind("XF86AudioPlay", hl.dsp.exec_cmd("playerctl play-pause"), { locked = true }) - # move focus with mod + arrows - "$mod, h, movefocus, l" - "$mod, j, movefocus, d" - "$mod, k, movefocus, u" - "$mod, l, movefocus, r" - - # Switch workspaces with mod + [0-9] - "$mod, 1, workspace, 1" - "$mod, 2, workspace, 2" - "$mod, 3, workspace, 3" - "$mod, 4, workspace, 4" - "$mod, 5, workspace, 5" - "$mod, 6, workspace, 6" - - # Move active window to a workspace with mod + SHIFT + [0-9] - "$mod SHIFT, 1, movetoworkspace, 1" - "$mod SHIFT, 2, movetoworkspace, 2" - "$mod SHIFT, 3, movetoworkspace, 3" - "$mod SHIFT, 4, movetoworkspace, 4" - "$mod SHIFT, 5, movetoworkspace, 5" - "$mod SHIFT, 6, movetoworkspace, 6" - - # Move active window inside workspace - "$mod SHIFT, h, movewindow, l" - "$mod SHIFT, j, movewindow, d" - "$mod SHIFT, k, movewindow, u" - "$mod SHIFT, l, movewindow, r" - - # Scroll through existing workspaces with mod + scroll - "$mod, mouse_down, workspace, e+1" - "$mod, mouse_up, workspace, e-1" - - # special workspaces - "$mod, x, togglespecialworkspace, terminal" - "$mod, x, centerwindow" - "$mod, e, togglespecialworkspace, yazi" - "$mod, e, centerwindow" - ]; - - bindel = [ - ", XF86AudioRaiseVolume, exec, wpctl set-volume @DEFAULT_AUDIO_SINK@ 5%+" - ", XF86AudioLowerVolume, exec, wpctl set-volume @DEFAULT_AUDIO_SINK@ 5%-" - ", XF86MonBrightnessUp, exec, brightnessctl set 5%+" - ", XF86MonBrightnessDown, exec, brightnessctl set 5%-" - ]; - bindl = [ - ", XF86AudioMute, exec, pactl set-sink-mute @DEFAULT_SINK@ toggle" - ", XF86AudioMicMute, exec, volumectl -m toggle-mute" - ", XF86AudioNext, exec, playerctl next" - ", XF86AudioPrev, exec, playerctl previous" - ", XF86AudioPlay, exec, playerctl play-pause" - ]; - - bindm = [ - # Move/resize windows with mod + LMB/RMB and dragging - "$mod, mouse:272, movewindow" - "$mod, mouse:273, resizewindow" - ]; - }; + -- move/resize windows with mod + LMB/RMB drag + hl.bind(mod .. " + mouse:272", hl.dsp.window.drag(), { mouse = true }) + hl.bind(mod .. " + mouse:273", hl.dsp.window.resize(), { mouse = true }) + ''; }; services.hyprpaper.enable = false; diff --git a/home/common/programs/hyprland/noctalia/default.nix b/home/common/programs/hyprland/noctalia/default.nix index 9edab0c..4a8ee5a 100644 --- a/home/common/programs/hyprland/noctalia/default.nix +++ b/home/common/programs/hyprland/noctalia/default.nix @@ -90,19 +90,15 @@ in home.file.".face".source = avatar; # force overwrite files that noctalia may have turned into regular files - xdg.configFile."hypr/hyprland.conf".force = true; + # (Hyprland 0.55+ is Lua-only, so the managed file is now hyprland.lua) + xdg.configFile."hypr/hyprland.lua".force = true; xdg.configFile."gtk-3.0/gtk.css".force = true; - # source noctalia-generated color theme in hyprland (v5 path). - # Hyprland globs every source path, so a missing file errors as - # "globbing error: found no match" — pre-create an empty placeholder - # so the first boot doesn't fail before noctalia has run. - home.activation.ensureNoctaliaHyprConf = lib.hm.dag.entryAfter [ "writeBoundary" ] '' - run mkdir -p "$HOME/.config/hypr" - run touch "$HOME/.config/hypr/noctalia.conf" - ''; - - wayland.windowManager.hyprland.settings.source = [ - "~/.config/hypr/noctalia.conf" - ]; + # NOTE: noctalia still writes its color theme to ~/.config/hypr/noctalia.conf + # in *hyprlang* syntax ($primary = rgb(...), general { col.active_border = ... }). + # Hyprland 0.55+ dropped the hyprlang parser entirely, so that file can no + # longer be `source`d — there is no hl.source, and the lua config can't parse + # hyprlang. Hyprland therefore uses the static border colors from + # ../default.nix instead of noctalia's themed ones. To restore live theming, + # noctalia would need to emit a Lua snippet we can require() here. } diff --git a/home/konishi/default.nix b/home/konishi/default.nix index 4380546..4d1a118 100644 --- a/home/konishi/default.nix +++ b/home/konishi/default.nix @@ -38,84 +38,74 @@ # 4K@1x monitors render the Electron AI apps tiny; scale their UI up. Tune to taste. custom.aiApps.deviceScaleFactor = "1.5"; - wayland.windowManager.hyprland.settings = { - exec-once = [ - # boot-into-lock: paired with SDDM autologin (host config), lock the - # session the moment Hyprland starts so a cold boot lands on hyprlock, not - # an open desktop. A brief flash before it paints is possible. - "hyprlock" - "$HOME/.local/bin/hyprpaper-rotate" - ]; + wayland.windowManager.hyprland.extraConfig = '' + hl.on("hyprland.start", function() + -- boot-into-lock: paired with SDDM autologin (host config), lock the + -- session the moment Hyprland starts so a cold boot lands on hyprlock, not + -- an open desktop. A brief flash before it paints is possible. + hl.exec_cmd("hyprlock") + hl.exec_cmd("$HOME/.local/bin/hyprpaper-rotate") + end) - windowrule = [ - # ethui-dev - "workspace 2, match:title ethui-dev.*" - "no_initial_focus on, match:title ethui-dev.*" - "float on, match:title ethui-dev - dialog.*" + -- ethui-dev + hl.window_rule({ match = { title = "ethui-dev.*" }, workspace = "2", no_initial_focus = true }) + hl.window_rule({ match = { title = "ethui-dev - dialog.*" }, float = true }) - "workspace 1, match:title ethui.*" - "no_initial_focus on, match:title ethui.*" - "float on, match:title ethui - dialog.*" + hl.window_rule({ match = { title = "ethui.*" }, workspace = "1", no_initial_focus = true }) + hl.window_rule({ match = { title = "ethui - dialog.*" }, float = true }) - "workspace 1, match:title ^\\[bevy\\].*" + hl.window_rule({ match = { title = [[^\[bevy\].*]] }, workspace = "1" }) - "workspace 3 silent, match:class bevy-.*" - "no_initial_focus on, match:class bevy-.*" - "fullscreen on, match:class bevy-.*" + hl.window_rule({ match = { class = "bevy-.*" }, workspace = "3 silent", no_initial_focus = true, fullscreen = true }) - "workspace 1 silent, match:title egui-.*" - "border_size 0, match:title egui-.*" - "float on, match:title egui-.*" - "no_blur on, match:title egui-.*" - "move 100%-w-20 100%-h-20, match:title egui-.*" + hl.window_rule({ + match = { title = "egui-.*" }, + workspace = "1 silent", + border_size = 0, + float = true, + no_blur = true, + move = "100%-w-20 100%-h-20", + }) - # gaming: Steam/Proton titles run under XWayland with WM_CLASS - # steam_app_. Send them fullscreen to workspace 3 (DP-1) — the same - # streamed workspace as Big Picture below — instead of opening tiled on - # whatever monitor Steam is on. - # Native (non-Proton) games have their own class — grab it with - # `hyprctl clients | grep -iE "class|title"` while the game runs and add a - # matching line here. - "workspace 3, match:class steam_app_.*" - "fullscreen on, match:class steam_app_.*" + -- gaming: Steam/Proton titles run under XWayland with WM_CLASS + -- steam_app_. Send them fullscreen to workspace 3 (DP-1) — the same + -- streamed workspace as Big Picture below — instead of opening tiled on + -- whatever monitor Steam is on. + -- Native (non-Proton) games have their own class — grab it with + -- `hyprctl clients | grep -iE "class|title"` while the game runs and add a + -- matching rule here. + hl.window_rule({ match = { class = "steam_app_.*" }, workspace = "3", fullscreen = true }) - # Steam Big Picture shares the plain `steam` WM_CLASS with the normal - # client, so match its title instead. Without this it opens on whatever - # monitor the cursor is on, not the streamed one. Pin it to ws3 — DP-1's - # normal persistent workspace, which is what the stream captures. `.` - # stands in for the literal spaces to keep the rule string unambiguous. - "workspace 3, match:title Steam.Big.Picture.Mode" - "fullscreen on, match:title Steam.Big.Picture.Mode" + -- Steam Big Picture shares the plain `steam` WM_CLASS with the normal + -- client, so match its title instead. Without this it opens on whatever + -- monitor the cursor is on, not the streamed one. Pin it to ws3 — DP-1's + -- normal persistent workspace, which is what the stream captures. `.` + -- stands in for the literal spaces to keep the rule string unambiguous. + hl.window_rule({ match = { title = "Steam.Big.Picture.Mode" }, workspace = "3", fullscreen = true }) - # Heroic (GOG/Epic launcher) streamed via Moonlight — `heroic --console`. - # Tile it on ws3 (DP-1, the captured screen) rather than letting it float - # or land on whatever monitor the cursor is on. Games it launches are - # Proton titles → caught by the steam_app_ rules above; native ones need - # their own class added here. - "workspace 3, match:class heroic" - "tile on, match:class heroic" + -- Heroic (GOG/Epic launcher) streamed via Moonlight — `heroic --console`. + -- Tile it on ws3 (DP-1, the captured screen) rather than letting it float + -- or land on whatever monitor the cursor is on. Games it launches are + -- Proton titles → caught by the steam_app_ rules above; native ones need + -- their own class added here. + hl.window_rule({ match = { class = "heroic" }, workspace = "3", tile = true }) - # RetroArch streamed via Moonlight (also registered as a Sunshine app). - # Native Wayland app_id is `com.libretro.RetroArch` (set at map time) — the - # exact string is needed so the rule applies on spawn; a loose `retroarch` - # match misses at map and the window flashes onto the active workspace - # first. `silent` sends it to ws3 (DP-1, the captured screen) without - # yanking focus off whatever workspace you're driving from. - "workspace 3 silent, match:class com.libretro.RetroArch" - "fullscreen on, match:class com.libretro.RetroArch" - ]; + -- RetroArch streamed via Moonlight (also registered as a Sunshine app). + -- Native Wayland app_id is `com.libretro.RetroArch` (set at map time) — the + -- exact string is needed so the rule applies on spawn; a loose `retroarch` + -- match misses at map and the window flashes onto the active workspace + -- first. `silent` sends it to ws3 (DP-1, the captured screen) without + -- yanking focus off whatever workspace you're driving from. + hl.window_rule({ match = { class = "com.libretro.RetroArch" }, workspace = "3 silent", fullscreen = true }) - render = { - # Experiment: was `true`. With a fullscreen game direct-scanning-out, - # Sunshine starting a capture forces the compositor off the direct path, - # and that surface/context churn is a suspected trigger for Chromium's - # `EGL_CONTEXT_LOST` crash (Big Picture drops to software rendering mid- - # stream). Disabled to test whether the crashes stop. If it makes no - # difference, flip back to `true` for the local fullscreen latency win. - direct_scanout = false; - }; - - }; + -- Experiment: was `true`. With a fullscreen game direct-scanning-out, + -- Sunshine starting a capture forces the compositor off the direct path, + -- and that surface/context churn is a suspected trigger for Chromium's + -- `EGL_CONTEXT_LOST` crash (Big Picture drops to software rendering mid- + -- stream). Disabled to test whether the crashes stop. If it makes no + -- difference, flip back to `true` for the local fullscreen latency win. + hl.config({ render = { direct_scanout = false } }) + ''; programs.noctalia.settings = { shell.ui_scale = 1.25; diff --git a/home/konishi/monitors.nix b/home/konishi/monitors.nix index 04af1ed..ddf2af1 100644 --- a/home/konishi/monitors.nix +++ b/home/konishi/monitors.nix @@ -1,20 +1,19 @@ _: { - wayland.windowManager.hyprland.settings = { - monitor = [ - # List HDMI-A-1 first so it becomes the primary monitor (ID 0) - "HDMI-A-1, 3840x2160, 0x2160, 1" - # Then the other monitors - "DP-2, 3840x2160, 3840x180, 1, transform, 1" - "DP-1, 3840x2160, 0x0, 1" - ]; - workspace = [ - "1, monitor:HDMI-A-1, default:true, persistent:true" # bottom-left (primary) - "2, monitor:DP-2, persistent:true" # right (vertical) - "3, monitor:DP-1, persistent:true" # top-left - "4, monitor:HDMI-A-1" # bottom-left - "5, monitor:DP-2" # right (vertical) - "6, monitor:DP-1" # top-left - ]; - }; + # Hyprland 0.55+ is Lua-only (see home/common/programs/hyprland); monitors and + # workspace rules are expressed with the hl.* API in raw Lua. + wayland.windowManager.hyprland.extraConfig = '' + -- monitors (HDMI-A-1 listed first so it becomes the primary, ID 0) + hl.monitor({ output = "HDMI-A-1", mode = "3840x2160", position = "0x2160", scale = 1 }) + hl.monitor({ output = "DP-2", mode = "3840x2160", position = "3840x180", scale = 1, transform = 1 }) + hl.monitor({ output = "DP-1", mode = "3840x2160", position = "0x0", scale = 1 }) + + -- workspace -> monitor bindings + hl.workspace_rule({ workspace = "1", monitor = "HDMI-A-1", default = true, persistent = true }) -- bottom-left (primary) + hl.workspace_rule({ workspace = "2", monitor = "DP-2", persistent = true }) -- right (vertical) + hl.workspace_rule({ workspace = "3", monitor = "DP-1", persistent = true }) -- top-left + hl.workspace_rule({ workspace = "4", monitor = "HDMI-A-1" }) + hl.workspace_rule({ workspace = "5", monitor = "DP-2" }) + hl.workspace_rule({ workspace = "6", monitor = "DP-1" }) + ''; } diff --git a/hosts/common/features/bluetooth.nix b/hosts/common/features/bluetooth.nix index 80ecb48..dd73a98 100644 --- a/hosts/common/features/bluetooth.nix +++ b/hosts/common/features/bluetooth.nix @@ -6,6 +6,11 @@ settings = { General = { Enable = "Source,Sink,Media,Socket"; + # Required for LE Audio (BAP): ISO sockets / CIS live behind + # BlueZ's experimental gate. Without this the XM6 falls back to + # classic A2DP, and mic use forces the HSP/HFP downgrade. + Experimental = true; + KernelExperimental = true; }; }; }; diff --git a/hosts/common/features/display/wayland.nix b/hosts/common/features/display/wayland.nix index e4fa0d0..d65070b 100644 --- a/hosts/common/features/display/wayland.nix +++ b/hosts/common/features/display/wayland.nix @@ -13,6 +13,14 @@ # GTK/GNOME services still useful on Wayland programs.dconf.enable = true; + # Route the Secret portal (org.freedesktop.portal.Secret) to gnome-keyring. + # Hyprland's portal doesn't implement Secret, and the preferred backend list + # (hyprland;gtk) has no Secret provider — so without this, the portal exposes + # no Secret interface at all. Electron apps (e.g. Claude Desktop) fetch their + # token-encryption key through this portal; when it's missing, os_crypt init + # fails (prev_init_success:false) and they can't persist a login across boots. + xdg.portal.config.common."org.freedesktop.impl.portal.Secret" = "gnome-keyring"; + services = { # thumbnail support for images tumbler.enable = true; diff --git a/hosts/common/features/pipewire.nix b/hosts/common/features/pipewire.nix index b32c7cf..6042951 100644 --- a/hosts/common/features/pipewire.nix +++ b/hosts/common/features/pipewire.nix @@ -7,5 +7,30 @@ alsa.enable = true; alsa.support32Bit = true; pulse.enable = true; + + # Disable the LC3-SWB HFP codec. BlueZ 5.86's experimental LC3 super-wideband + # headset path (lit up by hardware.bluetooth Experimental=true) is broken for + # the Sony WF-1000XM6: when a call opens the mic, WirePlumber auto-switches to + # the highest-priority headset profile (LC3-SWB), the audio link fails to set + # up ("Hands-Free Voice gateway: Transport endpoint is not connected"), and the + # buds reset with no call audio/mic. Omitting lc3_swb from the codec allow-list + # makes autoswitch fall back to mSBC, which is stable wideband voice. Everything + # else (A2DP LDAC/AAC/SBC, and lc3 for any future LE Audio) stays enabled. + wireplumber.extraConfig."51-disable-lc3swb-hfp" = { + "monitor.bluez.properties" = { + "bluez5.codecs" = [ + "sbc" + "sbc_xq" + "aac" + "aptx" + "aptx_hd" + "aptx_ll" + "ldac" + "lc3" + "cvsd" + "msbc" + ]; + }; + }; }; }