This is part 6 in my series of posts learning how to setup NixOS on my Framework 16" AMD laptop. It is not meant as a guide, necessarily, but more as a captain’s log of my thoughts and processes along the way.
I had some other plans for what I was going to do next, but I realized that I haven’t really moved into my laptop yet – none of the posts that I’ve written so far have been written on my laptop. That changes today. I setup Obsidian, re-setup my 1Password vault, get a browser setup, and figure out if I can fix the touchpad sensitivity that has already screwed up my writing like 4 times while writing this small paragraph.
As I previously noted, 1Password is my password manager of choice, but also isn’t free and required being explicitly declared as a non-free package in my home.nix
file. The same goes for Obsidian. I want to be explicit about my choices for non-free packages, so I’m going to reiterate the way we set that up in a previous post, as well as add obsidian to my list. In home.nix
, add the following:
{
nixpkgs = {
config = {
allowUnfreePredicate = pkg: builtins.elem (lib.getName pkg) [
"1password"
"obsidian"
];
};
};
}
Note that when we add these unfree predicates, they are wrapped in quotes, which is different than what we do when we add new packages. Then we’ll have add the following to our home.packages
, further down in the file:
home.packages = with pkgs; [
...
_1password_gui
obsidian
];
We can do a switch
and away we go, we now have Obsidian and 1Password available to us in our launcher (which we can access with
SUPER+R
, or
WIN+R
).
While most of the time I like that my windows automatically tile in Hyprland, 1Password is a good example of a window that I typically don’t want to open in a new tile; I’d rather it open as a floating window that I can quickly reference in whatever my current context is, then get it out of my way and get back to whatever I’m doing.
Thankfully, Hyprland provides a way to do this with Window Rules. We can open up our nix-managed hyprland.conf file, ~/nixcfg/home/dade/.config/hypr/hyprland.conf
, and add a new window rule. I’m going to do this towards the bottom, because there is already one window rule towards the bottom, and I’d like the window rules to be grouped together.
In order to create the window rule, we need to decide if we’re going to use the class or the title of the window to apply the rule. To figure out the class of my 1Password window, I opened up 1Password using rofi with
SUPER+R
. Once it was open, I ran hyprctl clients
in my terminal, which gave me information about all of my open windows.
Ultimately, I decided to use class
instead of title
, since title
could match things like opening 1Password
in a browser window, among other things. I anticipate that the class
of my 1Password vault won’t change often. So I’m going to add this line to the bottom of ~/nixcfg/home/dade/.config/hypr/hyprland.conf
.
windowrulev2 = float,class:(1Password)
After running another switch
, I tried re-opening 1Password and it didn’t float automatically like I would have expected. In hindsight, this makes sense, since rebuilding nix and putting the config file in place didn’t actually cause Hyprland to reload the config. Thankfully hyprctl
provides a useful command here, too – hyprctl reload
.
After reloading the config, I launched 1Password again, and now it was floating. Fantastic. Now how do I move the floating window? Thankfully, this is already included in the default Hyprland config file I have:
bindm = $mainMod, mouse:272, movewindow
bindm = $mainMod, mouse:273, resizewindow
mouse:272
maps to the left mouse button, and mouse:273
maps to the right mouse button. I don’t really care to resize my 1Password window, but I can do that by clicking in the bottom right corner of my Touchpad if I do ever need to. I just want to be able to move it around so I can more easily see the primary page I’m working with.
Based on what I was able to find, this shouldn’t be a problem by default on Hyprland, but for some reason it was a problem for me. While I was typing this post, my cursor was randomly moving to wherever my pointer was. At first I just attributed this to learning to type on a new laptop keyboard, but it kept happening frequently enough that I thought surely something must be wrong.
Hyprland uses libinput, according to a random reddit thread I found while searching for this problem, and one thing people recommended checking was whether or not the touchpad was being detected as a touchpad or as a generic mouse. I can use libinput list-devices
for this. But I don’t have it installed right now, so instead I’m going to run it in a nix-shell. sudo nix-shell -p libinput --run "libinput list-devices"
shows that I have a mouse at /dev/input/event8
and a touchpad at /dev/input/event9
. I’m unclear what the Mouse device is, but the fact that a Touchpad device is seen is a good sign.
Just to make explicit my preferences, I’m going to update my hyprland.conf
file with the following:
input {
...
touchpad {
natural_scroll = true
disable_while_typing = true
}
}
The default in my configuration file set natural_scroll
to false and didn’t set disable_while_typing
at all, so I figured it couldn’t hurt to mark it as true
explicitly.
I did another switch, did another hyprctl reload
, and confirmed that my natural scrolling change took effect. Additionally, my touchpad seems to be messing up my typing considerably less frequently than it was, though it is definitely still happening occasionally. This might just be me adjusting to typing on this keyboard and might have done nothing at all, but worst case scenario, I’ve at least made my preference more explicit.
Another interesting thing I noticed is that natural_scroll
did change my scroll behavior, but when I re-ran libinput list-devices
, it still showed the Touchpad device as having Natural scrolling disabled.
While hunting for a solution to a later problem in this post, I remembered the official NixOS wiki has a page for the Framework 16, and it explicitly calls out that the keyboard shows up as an external USB device, which causes the touchpad to stay enabled while typing. This was the opposite of what I was looking for (where the mouse might be an external USB device). But thankfully, the wiki provides a useful libinput quirks file that we should be able to use to fix the problem. I’m going to add the following to my ~/nixcfg/hosts/serenity/configuration.nix
file.
environment.etc = {
"libinput/local-overrides.quirks".text = ''
[Keyboard]
MatchUdevType=keyboard
MatchName=Framework Laptop 16 Keyboard Module - ANSI Keyboard
AttrKeyboardIntegration=internal
'';
};
After adding this to my configuration, I ran another switch
and one thing I immediately noticed is that my palm doesn’t seem to be triggering the touchpad as much, so this is probably the right fix, considerably moreso than the explicit setting in my hyprland.conf
. I’ll leave the change to my hyprland.conf
file anyways, just to be explicit.
I’ve managed to make it this far without a browser installed, and as nice as that’s been, I’m going to need a browser at some point. In fact, I’m probably going to need more than one browser at some point. For now, let’s start with Firefox, since I’ve become a little disenfranchised with the direction that Chrome browsers have been heading. I will note that in the future, I will probably need the non-free Chrome, as the Google Workspace integrations there can be quite useful for restricting access to only “managed” devices. But I’m getting well ahead of myself, so let’s just add firefox
to our home.nix
packages:
home.packages = with pkgs; [
...
firefox
];
After we switch
, we can launch firefox with rofi (
SUPER+R
). It opens up in a new tile on the current workspace, which I don’t love. Let’s see if we can get a window rule in place so that firefox always opens up in a consistent place, we’ll put it on workspace 2. That way I can always know that going to workspace 2 will be my browser.
Back in nixcfg/home/dade/.config/hypr/hyprland.conf
, we’re going to add another windowrulev2
:
windowrulev2 = workspace 2, class:(firefox)
Time to switch
again, followed by another hyprctl reload
. As a note: It does get kind of annoying having to do a nixos-rebuild every time I want to change my window manager settings. I’ve actually seen some people rant and quit NixOS because of this pattern. It could be interesting to see if I can use simple symlinks for some files that I might want to change more frequently or without requiring a whole new nix derivation, but that can be another project for another day.
After reloading Hyprland, it’s time to close firefox and launch it again. Sure enough, when it launches, it automatically puts Firefox in workspace 2 and switches me over to it. This is great, though I’m likely to often forget and instead try to use my launcher to open firefox, even if it’s already open. It could be cool to have rofi see if firefox is already running before deciding to open a new one, otherwise switch to the existing one. Not sure if this is possible, but again, could be a useful thing to look into later.
One thing that would be cool here is to setup firefox to only allow extensions that are installed via nix. It looks like the firefox nixpkg even might support this, however I can’t find basically any extensions I’m interested in in nixpkgs, so maybe this isn’t as good an idea as I’d hoped.
There are a few things I’ve learned about Wayland while learning how to setup my laptop, and one of those things is about how things like screen-sharing and screenshot tools are mostly broken in Wayland. This is in large part due to the security benefits of Wayland, as I understand it. Basically, if an application has access to the X socket in a typical X11 system, that application can listen to all inputs, see information about other windows open, etc. There are several tools out there for abusing this very access. Wayland, on the other hand, offers a more limited set of controls to applications and then composites those application windows together. Or something like that. I’ll be honest, I only know that X has some well-known ways to abuse it, and part of why I picked a Wayland based window manager was so that I could avoid those shortcomings.
But in real life, it’s often necessary to record my screen or take screenshots, so I’m going to use PipeWire, which is what I’ve seen come up time and time again when talking about this problem.
It also appears that PipeWire will manage capture and playback for sound as well as video. So I can switch over to using PipeWire as my sound output and disable sound.enable
in my configuration file. In hosts/serenity/configuration.nix
, I made the following changes:
{
...
sound.enable = false; # previously true
nixpkgs.config.pulseaudio = false; # previously true
hardware.pulseaudio.enable = false; # previously true
services.pipewire = { # net new block
enable = true;
alsa.enable = true;
alsa.support32Bit = true;
pulse.enable = true;
};
}
I then did another switch
and my audio still worked. To test it out, I opened up firefox and began playing the best hacker song of all time. This is either a great testament to how far linux has come, a scathing review of pulseaudio, or an indicator that I need to reboot before I see any issues.
But that’s okay, I want to reboot anyways. In the Framework 16 NixOS wiki page, there’s a comment about switching a setting in UEFI configuration to “Linux Audio Compatibility” to improve speaker audio quality. So I’m going to go ahead and do that next and see if my audio still works after reboot.
Hello, I have returned from a reboot. I went into my Framework’s UEFI “Advanced Settings” and switched “Linux Audio Compatibility” to Linux
instead of Windows
. I am once again listening to best hacker song of all time. Honestly, I can’t really tell the difference. But maybe that’s because the volume has been quite quiet, and my volume control buttons aren’t working…
Framework’s function keys can be used to control volume (mute, lower, higher), as well as rewind, pause/play, and fast forward. They can also be used to control the screen brightness and toggle into airplane mode. Only one problem, none of these keys are working for me.
I’d heard some complaints about the sound quality on the Framework based on the direction of the speakers, so I wanted to try it out myself. But probably the first thing I noticed was actually that the volume was relatively low, and I needed it to be a bit louder.
My Waybar configuration does show the speaker and microphone volumes, which does help me confirm that it’s quieter than it can be. But it doesn’t control my speaker and microphone volumes. So we have to figure that part out on our own.
I was surprised and a little annoyed to learn that there wasn’t a standard system-wide way to make these keys work. My understanding up until this point was that the keys sent pretty standardized scan codes to the host, and that I’d just need to install some daemon that responds to the scan codes correctly. But I think in hindsight, this makes some sense that there isn’t a single standard way to do this. After all, I’m not using a desktop environment – I’m using a window manager. So why am I surprised that I have to bring my own support for controlling the sound system I’m using, or controlling the screen brightness?
Since the first thing I want to solve for is my audio controls, let’s figure out what commands we can use to toggle mute, lower the volume, and increase the volume. These will map to my F1 , F2 , and F3 keys, respectively – or more particularly, they will map to these keys when these keys aren’t being used as F1 , F2 , and F3 .
After a bit of googling for things like “media controls hyprland”, I found a reddit post in r/hyprland) that helpfully provided the following commands for Pipewire, via the wpctl
command. While I was hopeful I’d find a simple “toggle this input setting and it’ll auto map media controls”, I was thankful to at least get these commands working, and then I can go from there.
$ wpctl set-volume -l 1.4 @DEFAULT_AUDIO_SINK@ 5%+
$ wpctl set-volume -l 1.4 @DEFAULT_AUDIO_SINK@ 5%-
$ wpctl set-mute @DEFAULT_AUDIO_SINK@ toggle
The post also includes some information about how to bind specific keys to run these commands. But I’m not super familiar with the Hyprland bind syntax, other than a few minor changes I’ve made. So first I want to make sure I understand the syntax here.
binde =, XF86AudioRaiseVolume, exec, wpctl set-volume -l 1.4 @DEFAULT_AUDIO_SINK@ 5%+
binde =, XF86AudioLowerVolume, exec, wpctl set-volume -l 1.4 @DEFAULT_AUDIO_SINK@ 5%-
bind =, XF86AudioMute, exec, wpctl set-mute @DEFAULT_AUDIO_SINK@ toggle
Reading the Hyprland wiki page on Binds, I was able to figure out that binde
is a flag for bind
that indicates that the bind will repeat when the key is held. This means that if I hold down the volume up or volume down buttons, it should automatically raise and lower the volume without having to spam tap it. I’m probably going to spam tap it anyways, but it’s nice to know I don’t have to.
This is the strangest syntax I’ve ever seen, but after reading about bind configurations from the wiki, I understand that the first argument to a bind is for the modifier key (such as SHIFT , CTRL , ALT , SUPER ), and in these cases, we don’t care about a modifier key. In theory the keyboard should be handling that translation for us via fn / fn-lock , so the window manager doesn’t need to translate it. At least, I think that’s the case.
Based on other bind syntax, this should be the key that we’re actually pressing. But I definitely don’t have a key labeled XF86AudioRaiseVolume. Thankfully, the Hyprland wiki also has us covered and links to this xkbcommon-keysyms.h file, which we can search through and find references to XF86. The naming convention comes from XFree86, and they are key symbols that map to various multimedia controls. This is actually super helpful, because the file also tells us the symbols for monitor and keyboard brightness, both of which I don’t know how to control at present.
This section tells Hyprland what to do when the key bind is triggered. exec
means we want to execute some system command. This is in contrast to doing things like resizing windows, moving windows, switching workspaces, switching focus, etc.
Now that I understand how the binds work, I’m going to add them to my ~/nixcfg/home/dade/.config/hypr/hyprland.conf
and do a switch
. In the section of the config with the rest of my bind definitions, I’m going to make a new section that looks like this:
# MediaControls
binde =, XF86AudioRaiseVolume, exec, wpctl set-volume -l 1.4 @DEFAULT_AUDIO_SINK@ 5%+
binde =, XF86AudioLowerVolume, exec, wpctl set-volume -l 1.4 @DEFAULT_AUDIO_SINK@ 5%-
bind =, XF86AudioMute, exec, wpctl set-mute @DEFAULT_AUDIO_SINK@ toggle
After running switch
to add the new version of my hyprland config to the nix store, I ran hyprctl reload
and after a little fiddling to determine the status of my fn lock, I was able to toggle mute, lower the volume, and raise the volume, using only my keyboard binds. The status of my volume in my Waybar also automatically updated in real time, which was nice to see – I didn’t want to have to go debugging that next.
It’s kind of funny that in Linux in general, I really have to dig in to understand this stuff that I just take for granted working in Mac or Windows (or honestly even many other Linux distributions). But that’s why I’m writing these posts, too – because there are a lot of things I’ve taken for granted for a long time, especially in the desktop environment.
Now that I’ve got the volume keys mapped, I’d love to get the monitor brightness keys working. Even if I skip the rewind, pause/play, fast forward keys, the ones I really care about are volume and brightness.
After a bit of searching around, it seems like brightnessctl is a common package that I should be able to use. Before installing it system-wide, I’m going to test it out in nix-shell -p brightnessctl
. Once I’m in a shell, I can run brightnessctl -l
to see what devices it sees and their corresponding current and maximum brightness.
[nix-shell:~/nixcfg]$ brightnessctl -l
Available devices:
Device 'amdgpu_bl1' of class 'backlight':
Current brightness: 255 (100%)
Max brightness: 255
...
Now we can use brightnessctl to change the brightness level of the backlight
class of devices.
brightnessctl -c backlight s "10%-"
After running this command a few times, I was able to see a brightness difference on my backlight, so it’s time to setup some keybinds to make our keyboard work correctly. We’re going to use a similar pattern that we used for volume controls, where we use binde
so we can hold the key down. In our ~/nixcfg/home/dade/.config/hypr/hyprland.conf
file, we’re going to add these binds:
binde =, XF86MonBrightnessUp, exec, brightnessctl -c backlight s "10%+"
binde =, XF86MonBrightnessDown, exec, brightnessctl -c backlight s "10%-"
We also need to add brightnessctl
to our system packages in hosts/serenity/configuration.nix
:
environment.systemPackages = with pkgs; [
...
brightnessctl # backlight brightness controls
];
Now we can run switch
, hyprctl reload
, and press the brightness up and down keys to see our backlight changing brightness.
There are a few other keys I’d love to figure out how to map – the F9 key looks like it is meant to change the display method for external displays (such as mirror vs extend), and the F10 key is for airplane mode. But for now, I have addressed the most important keys.
It’s been a couple weeks since I started working on my laptop, and I haven’t really updated any packages yet. Since my whole system is declared in my flake.nix
file, I can run nix flake update
to update the dependency manifest and then run switch
to actually build a new derivation with the new dependencies.
After running switch
, I got a couple warnings for deprecated settings, so we’re going to update them:
trace: warning: The option `hardware.opengl.driSupport32Bit' defined in `/nix/store/9zsjhdd42lnm603vaha6xdr70p8hkfq6-source/hosts/serenity/configuration.nix' has been renamed to `hardware.graphics.enable32Bit'.
trace: warning: The option `hardware.opengl.enable' defined in `/nix/store/9zsjhdd42lnm603vaha6xdr70p8hkfq6-source/hosts/serenity/configuration.nix' has been renamed to `hardware.graphics.enable'.
We can simply change the configuration settings to their new names, switch
, and we have no more warnings.
I would love to see how I can automate this. I’ve seen some mentions in places of an auto-update setting in nix system configurations, and we could probably run nix flake update
in a github workflow or something.