Files
nix-config/modules/desktop/hyprland.org
T
phundrak a3df5e937f refactor: change to litterate config
Configuration is now held by the `.org` files. All `.nix` files are
tangled from the org-mode files.
2026-10-26 00:43:40 +01:00

420 lines
14 KiB
Org Mode
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#+title: Hyprland
#+setupfile: ../headers
#+property: header-args:conf :noweb yes :tangle no :exports code
* Hyprland
[[https://hyprland.org/][Hyprland]] is my main Wayland compositor, dynamic tiling with nice
animations out of the box. I used to keep the NixOS and home-manager
halves of its config in separate files, but there was never a good
reason for that, so they now both live here: one turns the program on
system-wide, the other is where all of my actual config lives. Here’s
the skeleton of both.
#+begin_src nix :tangle yes
{
flake.modules = {
nixos.hyprland = {
<<nixos-config>>
};
homeManager.hyprland = {
config,
lib,
pkgs,
...
}:
with lib; let
cfg = config.home.desktop.hyprland;
laptops = ["gampo"];
caelestiaEnabled = config.home.desktop.caelestia.enable;
in {
<<hm-options>>
config = {
<<desktop-defaults>>
<<blueman-applet>>
<<hyprland-settings>>
};
};
};
}
#+end_src
** Turning Hyprland On
On the NixOS side, there’s really nothing to it: just turn the program
on. =withUWSM= hands session startup over to [[https://github.com/Vladimir-csp/uwsm][UWSM]] instead of
Hyprland’s own half-baked systemd integration, which plays nicer with
everything else expecting a proper systemd user session.
#+name: nixos-config
#+begin_src nix
programs.hyprland = {
enable = true;
withUWSM = true;
};
#+end_src
** Declaring the Options
=emacsPkg= exists so the rest of my config can reach whatever Emacs
package I’m actually running, rather than assuming the default one.
=host= is the more important one: it tells this module which machine
it’s running on, which decides the monitor layout below and whether
waybar bothers showing a battery indicator.
#+name: hm-options
#+begin_src nix
options.home.desktop.hyprland = {
emacsPkg = mkOption {
type = types.package;
default = config.home.dev.editors.emacs.package or pkgs.emacs;
example = pkgs.emacs;
};
host = mkOption {
type = types.enum ["gampo" "marpa"];
description = ''
Which host is Hyprland running on.
This helps determine the monitors layout and enable battery support in waybar.
'';
};
};
#+end_src
** Letting Caelestia Take Over
These days I run [[https://github.com/caelestia-dots/shell][caelestia]] on top of Hyprland, and it already covers
the wallpaper, a bar, a notification centre, and night-light handling
on its own. So whenever caelestia is enabled, I don’t want my own
=hyprpaper=, =waybar=, =swaync=, and =wlsunset= fighting it for the same
job. They’re all =mkDefault=, though, so a host still running bare
Hyprland without caelestia can turn them back on — =rofi= and the
battery indicator on waybar are the exceptions, since I always want
=rofi= around, and only =gampo= (my laptop) needs a battery in the bar.
=blueman-applet= is a bit different: caelestia doesn’t have its own
Bluetooth applet, so that one is just a straight negation rather than
a default, with no way for a host to override it short of changing
this module.
#+name: desktop-defaults
#+begin_src nix
home.desktop = {
hyprpaper.enable = mkDefault (! caelestiaEnabled);
rofi.enable = mkDefault true;
swaync.enable = mkDefault (! caelestiaEnabled);
waybar = {
enable = mkDefault (! caelestiaEnabled);
battery = mkDefault (builtins.elem cfg.host laptops);
};
wlsunset.enable = mkDefault true;
};
#+end_src
#+name: blueman-applet
#+begin_src nix
services.blueman-applet.enable = ! caelestiaEnabled;
#+end_src
** The Hyprland Config Itself
Everything from here on is the actual =wayland.windowManager.hyprland=
config.
#+name: hyprland-settings
#+begin_src nix
wayland.windowManager.hyprland = {
enable = true;
systemd.enable = false;
importantPrefixes = ["$left" "$right" "$up" "$down" "$menu"];
settings = {
<<env>>
<<input>>
<<device>>
<<monitor>>
<<general>>
<<master>>
<<workspace>>
<<decoration>>
<<animations>>
dwindle.preserve_split = true;
exec-once =
[
<<exec-once-apps>>
]
++ lib.lists.optional (! caelestiaEnabled) "${pkgs.networkmanagerapplet}/bin/nm-applet";
};
extraConfig = ''
<<keybind-device>>
<<keybind-vars>>
<<keybind-launch>>
<<keybind-media>>
<<keybind-movement>>
<<keybind-mouse-actions>>
<<keybind-workspaces>>
'';
};
#+end_src
*** Environment and Input
=XMODIFIERS= points at fcitx so [[file:../i18n/input.org][my input methods]] actually work inside
Hyprland, and =XCURSOR_SIZE= just keeps the cursor from being
comically tiny on my monitors. My keyboard uses the AFNOR standard for
the bépo layout, same as everywhere else, with a US layout kept around
as a fallback I can switch to on demand. I also keep numlock on by
default and disable touchpad natural scrolling, since I never got used
to it.
#+name: env
#+begin_src nix
env = [
"XMODIFIERS,@im=fcitx"
"XCURSOR_SIZE,12"
];
#+end_src
#+name: input
#+begin_src nix
input = {
kb_layout = "fr,us";
kb_variant = "bepo_afnor,";
numlock_by_default = true;
follow_mouse = 1;
touchpad.natural_scroll = false;
sensitivity = "0";
};
#+end_src
On top of that, my [[file:../hardware/input/opentablet.org][graphics tablet]] gets its own sensitivity tuned down a
bit, since the default is far too twitchy for actual drawing.
#+name: device
#+begin_src nix
device = {
name = "wacom-usb-bamboo-pad-finger";
sensitivity = 0.5;
};
#+end_src
** Monitors
Hyprland just ignores a monitor it can’t find, but between =marpa= and
=gampo= I want genuinely different layouts rather than one config that
happens to degrade gracefully, so this picks the right list by host
name instead. On =marpa=, =DP-1= is my main ultrawide and =DP-2= sits
to its left as a secondary vertical-ish screen; the commented-out line
underneath is a lower resolution I switch to by hand when streaming,
since the full resolution is more than my upload can comfortably push
through. =gampo= is just its own laptop panel, so there’s nothing to
list — Hyprland figures that one out on its own.
#+name: monitor
#+begin_src nix
monitor =
{
"marpa" = [
"DP-1, 3440x1440@144, 1080x550, 1"
# "DP-1, 2560x1440@144, 1080x550, 1" # streaming
"DP-2, 2560x1080@60, 0x0, 1, transform, 1"
];
"gampo" = [];
}."${cfg.host}";
#+end_src
** Look and Feel
I like my gaps, and keeping windows relatively tight together but with
a generous gap around the whole screen looks nice to me. The border
colours come from the Nord palette again, nord9 fading into nord14 for
the active window and a plain nord3 for inactive ones.
#+name: general
#+begin_src nix
general = {
gaps_in = 5;
gaps_out = 20;
border_size = 2;
"col.active_border" = "rgb(81a1c1) rgb(a3be8c) 45deg";
"col.inactive_border" = "rgb(4c566a)";
layout = "master";
};
#+end_src
I switched over to the =master= layout, centred, with new windows
inheriting whatever status the window they split off from had.
#+name: master
#+begin_src nix
master = {
orientation = "center";
new_status = "inherit";
};
#+end_src
Workspaces 1, 9 and 10 get their master area pinned to the bottom
instead of the side, which just suits how I actually use them better.
#+name: workspace
#+begin_src nix
workspace = [
"1, layoutopt:orientation:bottom"
"9, layoutopt:orientation:bottom"
"10, layoutopt:orientation:bottom"
];
#+end_src
Decoration-wise I like a fairly generous rounding on my windows, no
shadow or blur needed.
#+name: decoration
#+begin_src nix
decoration = {
rounding = 20;
};
#+end_src
Animations are fun, and I’ve mostly kept close to Hyprland’s own
example config here, minus the window-open animation, which I never
liked as much as everything else.
#+name: animations
#+begin_src nix
animations = {
enabled = true;
animation = [
# "windows, 1, 7, myBezier"
"windowsOut, 1, 7, default, popin 80%"
"border, 1, 10, default"
"borderangle, 1, 8, default"
"fade, 1, 7, default"
"workspaces, 1, 6, default"
];
};
#+end_src
** Autolaunch of Programs
=module-switch-on-connect= means Pipewire hops to a new audio device
the moment it’s plugged in, and I stop =mpc= right away just in case
something was already playing. The Polkit agent needs to be running
for a lot of GUI programs to be able to ask for elevated privileges at
all, and =mozc_server=/=fcitx5= are the input method daemons behind [[file:../i18n/input.org][my
input method setup]]. =nm-applet= only joins the party when caelestia
isn’t around to show network status itself.
#+name: exec-once-apps
#+begin_src nix
"pactl load-module module-switch-on-connect"
"${pkgs.mpc}/bin/mpc stop"
"${pkgs.polkit_gnome}/libexec/polkit-gnome-authentication-agent-1"
"${pkgs.mozc}/lib/mozc/mozc_server"
"${pkgs.fcitx5}/bin/fcitx5 -d"
#+end_src
** Keybindings
The rest of the config is plain Hyprland syntax, so it lives in
=extraConfig= rather than the =settings= attrset. I don’t bother with
submaps here, unlike my old standalone config — everything’s a direct
=SUPER=-something bind.
First, the tablet gets a second, more specific declaration here with
an adaptive acceleration profile, since that bit isn’t exposed through
the =settings= attrset the way the rest of =device= is. Then come my
usual =$left=/=$right=/=$up=/=$down= variables — remember that =hjkl=
becomes =ctsr= on a bépo keyboard — and =$menu= for my combi launcher.
#+name: keybind-device
#+begin_src conf
device {
name = wacom-usb-bamboo-pad-finger
sensitivity = 0.5
accel_profile = adaptive
}
#+end_src
#+name: keybind-vars
#+begin_src conf
$left = c
$right = r
$up = s
$down = t
$menu = rofi -combi-modi drun,calc -show combi
#+end_src
A terminal and my [[file:../home/desktop/wlr-which-key.org][wlr-which-key]] menu (bound to both =SUPER+Space= and
=Print=, so I can reach it either way) are the only two things I
actually launch by hand.
#+name: keybind-launch
#+begin_src conf :noweb no
bind = SUPER, Return, exec, ${pkgs.kitty}/bin/kitty ${pkgs.tmux}/bin/tmux
bind = SUPER, Space, exec, ${pkgs.wlr-which-key}/bin/wlr-which-key
bind = , Print, exec, ${pkgs.wlr-which-key}/bin/wlr-which-key -k s
#+end_src
The media keys are the same as always: playback and volume through
=playerctl=/=pamixer=, and brightness through =xbacklight= — including
the keyboard backlight keys, which just reuse the same command since
my keyboard doesn’t expose a separate control for it.
#+name: keybind-media
#+begin_src conf
bindl = , XF86AudioPlay, exec, playerctl play-pause
bindl = , XF86AudioPause, exec, playerctl pause
bindl = , XF86AudioStop, exec, playerctl stop
bindl = , XF86AudioPrev, exec, playerctl previous
bindl = , XF86AudioNext, exec, playerctl next
bindl = , XF86AudioForward, exec, playerctl position +1
bindl = , XF86AudioRewind, exec, playerctl position -1
bindl = , XF86AudioRaiseVolume, exec, pamixer -i 2
bindl = , XF86AudioLowerVolume, exec, pamixer -d 2
bindl = , XF86MonBrightnessUp, exec, xbacklight -perceived -inc 2
bindl = , XF86MonBrightnessDown, exec, xbacklight -perceived -dec 2
bindl = , XF86KbdBrightnessUp, exec, xbacklight -perceived -inc 2
bindl = , XF86KbdBrightnessDown, exec, xbacklight -perceived -dec 2
bind = SUPER, a, exec, hyprctl switchxkblayout glove80-keyboard next
#+end_src
Window focus and movement all go through the =$left=/=$right=/=$up=/=$down=
variables above, plus the usual =Tab= cycling and mouse-driven move/resize.
#+name: keybind-movement
#+begin_src conf
bind = SUPER, $left, movefocus, l
bind = SUPER, $right, movefocus, r
bind = SUPER, $up, movefocus, u
bind = SUPER, $down, movefocus, d
bind = SUPER_SHIFT, $left, movewindow, l
bind = SUPER_SHIFT, $right, movewindow, r
bind = SUPER_SHIFT, $up, movewindow, u
bind = SUPER_SHIFT, $down, movewindow, d
bind = SUPER_CTRL_SHIFT, $left, moveworkspacetomonitor, e+0 +1
bind = SUPER_CTRL_SHIFT, $right, moveworkspacetomonitor, e+0 -1
bind = SUPER, Tab, cyclenext,
bind = SUPER_SHIFT, Tab, cyclenext, prev
#+end_src
#+name: keybind-mouse-actions
#+begin_src conf
bindm = SUPER, mouse:272, movewindow
bindm = SUPER, mouse:273, resizewindow
#+end_src
Workspace switching is where the bépo layout bites back: the number
row gives me punctuation, not digits, so the binds below go by key
name instead. As a reminder, here’s what each one actually is and
which digit sits on it under Shift:
| Character name | Character | Number |
|----------------+-----------+--------|
| quotedbl | ="= | 1 |
| guillemotleft | =«= | 2 |
| guillemotright | =»= | 3 |
| parenleft | =(= | 4 |
| parenright | =)= | 5 |
| at | =@= | 6 |
| plus | =+= | 7 |
| minus | =-= | 8 |
| slash | =/= | 9 |
| asterisk | =*= | 0 |
#+name: keybind-workspaces
#+begin_src conf
bind = SUPER, quotedbl, workspace, 1
bind = SUPER, guillemotleft, workspace, 2
bind = SUPER, guillemotright, workspace, 3
bind = SUPER, parenleft, workspace, 4
bind = SUPER, parenright, workspace, 5
bind = SUPER, at, workspace, 6
bind = SUPER, plus, workspace, 7
bind = SUPER, minus, workspace, 8
bind = SUPER, slash, workspace, 9
bind = SUPER, asterisk, workspace, 10
bind = SUPER, mouse_down, workspace, e+1
bind = SUPER, mouse_up, workspace, e-1
bind = SUPER_SHIFT, quotedbl, movetoworkspace, 1
bind = SUPER_SHIFT, guillemotleft, movetoworkspace, 2
bind = SUPER_SHIFT, guillemotright, movetoworkspace, 3
bind = SUPER_SHIFT, parenleft, movetoworkspace, 4
bind = SUPER_SHIFT, parenright, movetoworkspace, 5
bind = SUPER_SHIFT, at, movetoworkspace, 6
bind = SUPER_SHIFT, plus, movetoworkspace, 7
bind = SUPER_SHIFT, minus, movetoworkspace, 8
bind = SUPER_SHIFT, slash, movetoworkspace, 9
bind = SUPER_SHIFT, asterisk, movetoworkspace, 10
#+end_src