Configuration is now held by the `.org` files. All `.nix` files are tangled from the org-mode files.
14 KiB
Hyprland
Hyprland
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.
{
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>>
};
};
};
}
Turning Hyprland On
On the NixOS side, there’s really nothing to it: just turn the program
on. withUWSM hands session startup over to UWSM instead of
Hyprland’s own half-baked systemd integration, which plays nicer with
everything else expecting a proper systemd user session.
programs.hyprland = {
enable = true;
withUWSM = true;
};
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.
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.
'';
};
};
Letting Caelestia Take Over
These days I run 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.
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;
};
services.blueman-applet.enable = ! caelestiaEnabled;
The Hyprland Config Itself
Everything from here on is the actual wayland.windowManager.hyprland
config.
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>>
'';
};
Environment and Input
XMODIFIERS points at fcitx so 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.
env = [
"XMODIFIERS,@im=fcitx"
"XCURSOR_SIZE,12"
];
input = {
kb_layout = "fr,us";
kb_variant = "bepo_afnor,";
numlock_by_default = true;
follow_mouse = 1;
touchpad.natural_scroll = false;
sensitivity = "0";
};
On top of that, my graphics tablet gets its own sensitivity tuned down a bit, since the default is far too twitchy for actual drawing.
device = {
name = "wacom-usb-bamboo-pad-finger";
sensitivity = 0.5;
};
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.
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}";
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.
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";
};
I switched over to the master layout, centred, with new windows
inheriting whatever status the window they split off from had.
master = {
orientation = "center";
new_status = "inherit";
};
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.
workspace = [
"1, layoutopt:orientation:bottom"
"9, layoutopt:orientation:bottom"
"10, layoutopt:orientation:bottom"
];
Decoration-wise I like a fairly generous rounding on my windows, no shadow or blur needed.
decoration = {
rounding = 20;
};
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.
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"
];
};
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 my
input method setup. nm-applet only joins the party when caelestia
isn’t around to show network status itself.
"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"
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.
device {
name = wacom-usb-bamboo-pad-finger
sensitivity = 0.5
accel_profile = adaptive
}
$left = c
$right = r
$up = s
$down = t
$menu = rofi -combi-modi drun,calc -show combi
A terminal and my 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.
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
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.
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
Window focus and movement all go through the $left=/=$right=/=$up=/=$down
variables above, plus the usual Tab cycling and mouse-driven move/resize.
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
bindm = SUPER, mouse:272, movewindow
bindm = SUPER, mouse:273, resizewindow
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 |
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