Files
nixos-configs/hosts/pinwheel/modules/hyprland/default.nix
T
Alexander Heldt 5ec6f7d636 hyprland: Fix mouse binds, hl.bindm does not exist
Hyprland's lua API only registers hl.bind; there is no separate
hl.bindm. The settings.bindm key rendered as a literal hl.bindm(...)
call, which errored with "attempt to call a nil value" at runtime.
Fold the mouse binds into settings.bind with { mouse = true }, per
the wiki's own example config.
2026-08-26 14:14:07 +02:00

452 lines
13 KiB
Nix

{
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" ];
};
}