#+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 = { <> }; homeManager.hyprland = { config, lib, pkgs, ... }: with lib; let cfg = config.home.desktop.hyprland; laptops = ["gampo"]; caelestiaEnabled = config.home.desktop.caelestia.enable; in { <> config = { <> <> <> }; }; }; } #+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 = { <> <> <> <> <> <> <> <> <> dwindle.preserve_split = true; exec-once = [ <> ] ++ lib.lists.optional (! caelestiaEnabled) "${pkgs.networkmanagerapplet}/bin/nm-applet"; }; extraConfig = '' <> <> <> <> <> <> <> ''; }; #+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