{ inputs, pkgs, lib, config, ... }: let enabled = config.mod.hyprland.enable; mod = "SUPER"; monitorScript = pkgs.writeShellScript "hyprland-monitor-handler" '' INTERNAL="eDP-1" EXTERNAL_MONITORS="HDMI-A-1 DP-3" HYPRCTL="${pkgs.hyprland}/bin/hyprctl" JQ="${pkgs.jq}/bin/jq" get_active_external() { # Return the first connected external monitor for mon in $EXTERNAL_MONITORS; do if $HYPRCTL monitors -j | $JQ -e ".[] | select(.name == \"$mon\")" > /dev/null 2>&1; then echo "$mon" return 0 fi done return 1 } bind_workspaces() { local external batch="" if external=$(get_active_external); then # External monitor connected: move workspaces 1-5 to external, 6-10 to internal for ws in 1 2 3 4 5; do batch="$batch dispatch hl.dsp.workspace.move({ monitor = \"$external\", workspace = $ws });" done for ws in 6 7 8 9 10; do batch="$batch dispatch hl.dsp.workspace.move({ monitor = \"$INTERNAL\", workspace = $ws });" done else # No external monitor: move all workspaces to internal for ws in 1 2 3 4 5 6 7 8 9 10; do batch="$batch dispatch hl.dsp.workspace.move({ monitor = \"$INTERNAL\", workspace = $ws });" done fi $HYPRCTL --batch "$batch" } handle_event() { case $1 in monitoradded*|monitorremoved*) sleep 0.5 bind_workspaces ;; esac } # Start the event listener first so monitoradded events emitted during # session startup are not lost in the gap before we begin reading them. ${pkgs.socat}/bin/socat -U - UNIX-CONNECT:"$XDG_RUNTIME_DIR/hypr/$HYPRLAND_INSTANCE_SIGNATURE/.socket2.sock" | while read -r line; do handle_event "$line" done & LISTENER_PID=$! # Give socat a moment to actually connect before the initial bind. sleep 0.2 bind_workspaces # Re-bind once more after the DRM subsystem has had time to enumerate # external connectors, in case they were not yet present at session start. (sleep 3; bind_workspaces) & wait $LISTENER_PID ''; in { options = { mod.hyprland = { enable = lib.mkEnableOption "enable hyprland module"; }; }; config = lib.mkIf enabled { programs.hyprland = { enable = true; withUWSM = true; package = inputs.hyprland.packages.${pkgs.stdenv.hostPlatform.system}.hyprland; portalPackage = inputs.hyprland.packages.${pkgs.stdenv.hostPlatform.system}.xdg-desktop-portal-hyprland; xwayland = { enable = true; }; }; home-manager.users.alex = { wayland.windowManager.hyprland = { enable = true; systemd.enable = false; configType = "lua"; settings = { config = { xwayland.force_zero_scaling = true; input = { kb_layout = "se"; # 2 - Cursor focus will be detached from keyboard focus. Clicking on a window will move keyboard focus to that window. follow_mouse = 2; sensitivity = 0.3; accel_profile = "flat"; touchpad = { natural_scroll = false; tap_and_drag = false; }; }; cursor = { zoom_factor = 1; zoom_rigid = true; }; general = { layout = "dwindle"; gaps_in = 0; # gaps between windows gaps_out = 0; # gaps between windows and monitor edges col = { active_border = "rgba(${config.lib.colors.foreground}ff)"; inactive_border = "rgba(${config.lib.colors.background}ff)"; }; }; dwindle.force_split = 2; misc = { disable_hyprland_logo = true; disable_splash_rendering = true; }; animations.enabled = false; }; monitor = [ { output = "eDP-1"; mode = "1920x1200"; position = "auto-center-down"; scale = "1"; } { output = "HDMI-A-1"; mode = "2560x1440@100"; position = "auto-center-up"; scale = "1"; } { output = "DP-3"; mode = "2560x1440@60"; position = "auto-center-up"; scale = "1"; } ]; # Static defaults only: a workspace can only have one `monitor` in # hl.workspace_rule (later calls overwrite it, unlike hyprlang's # rule list). The hyprland-monitors service re-dispatches these to # whichever external monitor is actually connected at startup and # on hotplug, so this only matters for the brief window before it # runs. workspace_rule = [ { workspace = "1"; monitor = "HDMI-A-1"; default = true; } { workspace = "2"; monitor = "HDMI-A-1"; } { workspace = "3"; monitor = "HDMI-A-1"; } { workspace = "4"; monitor = "HDMI-A-1"; } { workspace = "5"; monitor = "HDMI-A-1"; } { workspace = "6"; monitor = "eDP-1"; default = true; } { workspace = "7"; monitor = "eDP-1"; } { workspace = "8"; monitor = "eDP-1"; } { workspace = "9"; monitor = "eDP-1"; } { workspace = "10"; monitor = "eDP-1"; } { workspace = "w[tv1]"; gaps_out = 0; gaps_in = 0; } { workspace = "f[1]"; gaps_out = 0; gaps_in = 0; } ]; window_rule = [ { name = "no-gaps-wtv1"; match = { float = false; workspace = "w[tv1]"; }; border_size = 0; rounding = 0; } { name = "no-gaps-f1"; match = { float = false; workspace = "f[1]"; }; border_size = 0; rounding = 0; } { # https://wiki.archlinux.org/title/Hyprland#Jetbrains_apps_focus_issues name = "xwayland-no-initial-focus"; match.xwayland = true; no_initial_focus = true; } ]; env = [ { _args = [ "GDK_DPI_SCALE" "1.5" ]; } { _args = [ "HYPRCURSOR_THEME" "Adwaita" ]; } { _args = [ "HYPRCURSOR_SIZE" "24" ]; } ]; on = { _args = [ "hyprland.start" (lib.generators.mkLuaInline '' function() hl.exec_cmd("uwsm app -- waybar") hl.exec_cmd("uwsm app -- hyprctl setcursor Adwaita 24") end '') ]; }; bind = let ws = x: let n = if (x + 1) < 10 then (x + 1) else 0; in builtins.toString n; focusWs = x: { _args = [ "${mod} + ${ws x}" (lib.generators.mkLuaInline "hl.dsp.focus({ workspace = ${builtins.toString (x + 1)} })") ]; }; moveToWs = x: { _args = [ "${mod} + SHIFT + ${ws x}" (lib.generators.mkLuaInline "hl.dsp.window.move({ workspace = ${builtins.toString (x + 1)}, follow = false })" ) ]; }; select = builtins.genList focusWs 10; move = builtins.genList moveToWs 10; magnifier = pkgs.writeShellScript "magnifier" '' CURRENT=$(${pkgs.hyprland}/bin/hyprctl getoption cursor:zoom_factor -j | ${pkgs.jq}/bin/jq .float) DELTA=0.1 UPDATED=1 case $1 in --increase) UPDATED=$(echo $CURRENT + $DELTA | ${pkgs.bc}/bin/bc) ;; --decrease) UPDATED=$(echo $CURRENT - $DELTA | ${pkgs.bc}/bin/bc) ;; --reset) UPDATED=1 esac if (( $(echo "$UPDATED < 1" | bc) )); then UPDATED=1; fi ${pkgs.hyprland}/bin/hyprctl keyword cursor:zoom_factor $UPDATED ''; in select ++ move ++ [ { _args = [ "${mod} + ESCAPE" (lib.generators.mkLuaInline "hl.dsp.window.close()") ]; } { _args = [ "${mod} + f" (lib.generators.mkLuaInline ''hl.dsp.window.fullscreen({ mode = "maximized" })'') ]; } { _args = [ "${mod} + SHIFT + f" (lib.generators.mkLuaInline ''hl.dsp.window.float({ action = "toggle" })'') ]; } { _args = [ "${mod} + h" (lib.generators.mkLuaInline ''hl.dsp.focus({ direction = "left" })'') ]; } { _args = [ "${mod} + j" (lib.generators.mkLuaInline ''hl.dsp.focus({ direction = "down" })'') ]; } { _args = [ "${mod} + k" (lib.generators.mkLuaInline ''hl.dsp.focus({ direction = "up" })'') ]; } { _args = [ "${mod} + l" (lib.generators.mkLuaInline ''hl.dsp.focus({ direction = "right" })'') ]; } { _args = [ "${mod} + CONTROL + 1" (lib.generators.mkLuaInline ''hl.dsp.exec_cmd("${magnifier} --increase")'') ]; } { _args = [ "${mod} + CONTROL + 2" (lib.generators.mkLuaInline ''hl.dsp.exec_cmd("${magnifier} --decrease")'') ]; } { _args = [ "${mod} + CONTROL + 3" (lib.generators.mkLuaInline ''hl.dsp.exec_cmd("${magnifier} --reset")'') ]; } # mouse movements: hyprland's lua API has no separate `bindm`, # mouse binds are plain `hl.bind` with `{ mouse = true }`. { _args = [ "${mod} + mouse:272" # left click (lib.generators.mkLuaInline "hl.dsp.window.drag()") { mouse = true; } ]; } { _args = [ "${mod} + mouse:273" # right click (lib.generators.mkLuaInline "hl.dsp.window.resize()") { mouse = true; } ]; } ]; }; }; home.packages = [ pkgs.wdisplays pkgs.bc ]; systemd.user.services.hyprland-monitors = { Unit = { Description = "Hyprland monitor hotplug handler"; PartOf = [ "graphical-session.target" ]; After = [ "graphical-session.target" ]; }; Service = { Type = "simple"; ExecStart = "${monitorScript}"; Restart = "on-failure"; RestartSec = 5; }; Install = { WantedBy = [ "graphical-session.target" ]; }; }; }; # To start electron apps like `chromium` with wayland support environment.sessionVariables.NIXOS_OZONE_WL = "1"; # The XDG portal is needed for screen sharing xdg.portal = { enable = true; # override "trace: warning: xdg-desktop-portal 1.17 reworked how portal implementations are loaded ..." config.common.default = "*"; extraPortals = [ pkgs.xdg-desktop-portal-wlr ]; }; # openGL is needed for wayland/hyprland hardware.graphics.enable = true; boot.kernelParams = [ "i915.enable_psr=0" ]; }; }