diff --git a/README.md b/README.md
index 0f60360..2c5041c 100644
--- a/README.md
+++ b/README.md
@@ -49,25 +49,88 @@ Partition the disk:
parted --script /dev/nvme0n1 -- mklabel gpt mkpart esp fat32 1MiB 512MiB mkpart primary 512MiB 100% set 1 boot on
```
-Create an encrypted pool:
+Create an encrypted pool. The `-O` flags set filesystem properties that every
+dataset inherits, so they only need to be given once here:
```bash
-zpool create -f -o ashift=12 -O encryption=on -O keyformat=passphrase -O mountpoint=none rpool /dev/nvme0n1p2
+zpool create -f \
+ -o ashift=12 \
+ -o autotrim=on \
+ -O encryption=on -O keyformat=passphrase \
+ -O compression=zstd \
+ -O dnodesize=auto \
+ -O xattr=sa \
+ -O acltype=posixacl \
+ -O relatime=on \
+ -O redundant_metadata=most \
+ -O mountpoint=none \
+ rpool /dev/nvme0n1p2
```
-Create a root partition:
+Create the datasets. Keeping `/nix` separate from `/` means the store can be
+excluded from snapshot and rollback policy, which is what makes an
+erase-your-darlings setup possible later:
```bash
zfs create -o mountpoint=legacy rpool/root
-mkdir -p /mnt
-mount -t zfs rpool/root /mnt
+zfs create -o mountpoint=legacy rpool/nix
+zfs create -o mountpoint=legacy rpool/home
+zfs create -o mountpoint=legacy -o recordsize=1M rpool/Storage
+zfs create -o mountpoint=none -o refreservation=2G rpool/reserved
```
-And home partition:
+Mount them:
```bash
-zfs create -o mountpoint=legacy -o compression=on rpool/home
-mkdir -p /mnt/home
+mkdir -p /mnt
+mount -t zfs rpool/root /mnt
+mkdir -p /mnt/nix /mnt/home /mnt/Storage
+mount -t zfs rpool/nix /mnt/nix
mount -t zfs rpool/home /mnt/home
+mount -t zfs rpool/Storage /mnt/Storage
```
+#### Why these properties
+
+Some of these can only be chosen when the pool or dataset is created, so they
+are worth getting right the first time.
+
+Creation-only, no way to change later without recreating the dataset:
+
+- `encryption` and `keyformat` enable native ZFS encryption. This cannot be
+ turned on afterwards.
+- `dnodesize=auto` allows dnodes larger than 512 bytes. Combined with
+ `xattr=sa` this keeps extended attributes inline instead of spilling into
+ separate blocks, which matters for a Nix store holding a hundred thousand
+ small files. Existing files keep whatever size they were written with.
+- `ashift=12` matches the 4K physical sector size of modern NVMe drives.
+
+Changeable later, but only applied to newly written blocks:
+
+- `compression=zstd` gives a better ratio than the `lz4` that plain
+ `compression=on` selects. The Nix store compresses particularly well.
+- `acltype=posixacl` is needed for systemd-journald to grant journal access
+ through ACLs. With the default of `off` that mechanism silently does nothing.
+- `redundant_metadata=most` reduces the number of duplicate metadata writes.
+- `recordsize=1M` suits bulk file storage. Leave it at the 128K default for
+ general purpose datasets, and drop it to 16K for databases or VM images.
+- `autotrim=on` issues TRIM continuously. The `zpool-trim.timer` unit that
+ NixOS enables covers this periodically instead, so either approach works.
+
+The `rpool/reserved` dataset never gets mounted. Its `refreservation` holds
+space back so that the pool can be brought below its full mark by destroying
+that one dataset. ZFS performance degrades noticeably above roughly 80%
+capacity.
+
+On a single disk pool there is no redundancy, so ZFS can detect corruption but
+never repair it. Setting `copies=2` on the datasets that hold irreplaceable
+data stores a second copy of each block at the cost of double the space:
+```bash
+zfs set copies=2 rpool/home
+```
+
+Snapshots are configured through `services.sanoid` in `hosts/Common/zfs.nix`.
+ZFS dataset names are case sensitive, so an entry there has to match the
+dataset exactly. A name that does not resolve leaves that dataset with no
+snapshots at all while the service still reports success.
+
Format the boot partition:
```bash
mkfs.fat -F 32 -n BOOT /dev/nvme0n1p1
@@ -112,7 +175,7 @@ sudo nixos-install --impure --flake git+https://git.thomasave.be/thomasave/Dotfi
Clean-up:
```bash
-umount /mnt/{home,boot}
+umount /mnt/{home,nix,Storage,boot}
umount /mnt
```
diff --git a/flake.nix b/flake.nix
index 75476f5..143f4f1 100644
--- a/flake.nix
+++ b/flake.nix
@@ -40,7 +40,7 @@
({pkgs, ...}: {
nixpkgs.config.allowUnfree = true;
networking.hostName = host;
- time.timeZone = "Europe/Brussels";
+ time.timeZone = "Asia/Bangkok";
nix.settings = {
substituters = [
"https://nix-community.cachix.org"
@@ -56,6 +56,13 @@
keep-derivations = true;
auto-optimise-store = true;
};
+
+ # Resolve the bare "nixpkgs" flakeref to the exact revision this system
+ # was built from. Dev shells that use it then reuse the store paths the
+ # system already has, rather than each locking its own nixos-unstable
+ # snapshot and pulling in a duplicate toolchain.
+ nix.registry.nixpkgs.flake = nixpkgs;
+ nix.nixPath = ["nixpkgs=${nixpkgs}"];
users.users.${user} = {
isNormalUser = true;
extraGroups = ["wheel" "video"]; # Enable 'sudo' for the user.
diff --git a/home/Aloria.nix b/home/Aloria.nix
index ff90d0b..af8220a 100644
--- a/home/Aloria.nix
+++ b/home/Aloria.nix
@@ -26,20 +26,11 @@ in {
home.packages = with pkgs; [
jetbrains.pycharm
- rclone
- opencode
- claude-code
- # zed-editor
- uv
google-cloud-sdk
awscli2
distrobox
- gnome-disk-utility
- moonlight-qt
vscode
texliveFull
- gnome-power-manager
- smile
podman-compose
vesktop
sox
diff --git a/home/ags/files/Bar.tsx b/home/ags/files/Bar.tsx
index e853bf6..9e8a233 100644
--- a/home/ags/files/Bar.tsx
+++ b/home/ags/files/Bar.tsx
@@ -1,6 +1,13 @@
import { Astal, Gdk, Gtk } from "ags/gtk4";
import app from "ags/gtk4/app";
-import { createBinding, createState, For, With, Accessor } from "ags";
+import {
+ createBinding,
+ createComputed,
+ createState,
+ For,
+ With,
+ Accessor,
+} from "ags";
import { createPoll } from "ags/time";
import { subprocess, execAsync } from "ags/process";
import Pango from "gi://Pango";
@@ -275,56 +282,55 @@ function Icons() {
);
}
-function Volume(): JSX.Element {
- if (!wirePlumber) return ;
+function SpeakerButton({ speaker }: { speaker: Wp.Endpoint }): JSX.Element {
+ const volume = createBinding(speaker, "volume");
+ const mute = createBinding(speaker, "mute");
- const audio = wirePlumber.audio;
- const icon = createBinding(audio.default_speaker, "volume").as((volume) => {
- const vol = volume * 100;
- const icon = [
+ const icon = volume.as((vol) => {
+ const percentage = vol * 100;
+ const level = [
[101, "overamplified"],
[67, "high"],
[34, "medium"],
[1, "low"],
[0, "muted"],
- ].find(([threshold]) => Number(threshold) <= vol)?.[1];
- return `audio-volume-${icon}-symbolic`;
+ ].find(([threshold]) => Number(threshold) <= percentage)?.[1];
+ return `audio-volume-${level}-symbolic`;
});
- const css = createBinding(audio.default_speaker, "mute").as((mute) => {
- return mute ? "margin-left:0;" : "margin-left: 0.7em;";
- });
- let volume = createBinding(audio.default_speaker, "volume");
- let mute = createBinding(audio.default_speaker, "mute");
+ const label = createComputed(() =>
+ mute() ? "" : `${Math.floor(volume() * 100)}%`,
+ );
+ const css = mute.as((muted) =>
+ muted ? "margin-left: 0;" : "margin-left: 0.7em;",
+ );
+
return (
-