Files

420 lines
14 KiB
Org Mode
Raw Permalink Normal View History

2026-10-03 09:56:48 +02:00
#+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