Configuration is now held by the `.org` files. All `.nix` files are tangled from the org-mode files.
420 lines
14 KiB
Org Mode
420 lines
14 KiB
Org Mode
#+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
|