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 ( - ); } +function Volume(): JSX.Element { + if (!wirePlumber) return ; + + // WirePlumber swaps out the whole endpoint object whenever the default + // sink changes, e.g. when a Bluetooth headset connects. Binding to the + // endpoint that happened to be default at startup leaves the widget stuck + // on that device forever, so track the property and rebuild on every swap. + const speaker = createBinding(wirePlumber.audio, "defaultSpeaker"); + return ( + + {(endpoint: Wp.Endpoint | null) => + endpoint ? : + } + + ); +} + function Workspaces({ connector }: { connector: string }): JSX.Element { return ( diff --git a/home/helium/default.nix b/home/helium/default.nix index c6145da..e720802 100644 --- a/home/helium/default.nix +++ b/home/helium/default.nix @@ -8,7 +8,7 @@ sha256 = "sha256-qzc135IP5F2btxtOMUGMz+0azJhYL9KI0lcPG2KjcxU="; }; extraPkgs = pkgs: [pkgs.tzdata]; - profile = "export TZ=Europe/Brussels"; + profile = "export TZ=Asia/Bangkok"; }) ]; xdg.desktopEntries = { diff --git a/home/utils/common.nix b/home/utils/common.nix index bbf7e21..a383135 100644 --- a/home/utils/common.nix +++ b/home/utils/common.nix @@ -32,6 +32,8 @@ ncdu jq rclone + claude-code + uv ]; home.sessionVariables = { XDG_CONFIG_HOME = "${config.xdg.configHome}"; diff --git a/home/waybar/default.nix b/home/waybar/default.nix index 8b6b086..f600c67 100644 --- a/home/waybar/default.nix +++ b/home/waybar/default.nix @@ -46,12 +46,12 @@ "hyprland/window" = {max-length = 50;}; tray = {spacing = 10;}; "clock#clock2" = { - timezone = "Europe/Brussels"; + timezone = "Asia/Bangkok"; format = "{:%H:%M:%S}"; interval = 1; }; clock = { - timezone = "Europe/Brussels"; + timezone = "Asia/Bangkok"; format = "{:%Y-%m-%d}"; interval = 3600; }; diff --git a/hosts/Aloria/default.nix b/hosts/Aloria/default.nix index 9fd2e2a..a5813b2 100644 --- a/hosts/Aloria/default.nix +++ b/hosts/Aloria/default.nix @@ -177,6 +177,11 @@ in { ]; authKeyFile = "/home/user/.secrets/Tailscale/Aloria/authkey"; }; + programs.winbox = { + enable = true; + openFirewall = true; + }; + services.usbmuxd.enable = true; environment.systemPackages = with pkgs; [ libcamera diff --git a/hosts/Common/desktop.nix b/hosts/Common/desktop.nix index 9c2e69e..ae46c4e 100644 --- a/hosts/Common/desktop.nix +++ b/hosts/Common/desktop.nix @@ -11,6 +11,15 @@ vulkan-extension-layer ]; }; + + environment.systemPackages = with pkgs; [ + wiremix + gnome-disk-utility + moonlight-qt + gnome-power-manager + smile + ]; + # programs.hyprland = { # enable = true; # withUWSM = true; diff --git a/hosts/Common/zfs.nix b/hosts/Common/zfs.nix index 3c2019d..583920c 100644 --- a/hosts/Common/zfs.nix +++ b/hosts/Common/zfs.nix @@ -17,7 +17,7 @@ autoprune = true; autosnap = true; }; - "rpool/storage" = { + "rpool/Storage" = { frequently = 8; yearly = 0; monthly = 0;