Migration This is my new public git source viewer. Source coming soon. Please contact with concerns/bugs. Contact me

Compare

7108fc28e06a6326f7a04251cc95bbe50ced6133 … main · 16 commits

merge base 7108fc28e06a…

Changed files

FileChanges
MIGRATION.md +0 −249 raw
README.md +0 −44 raw
files/darwin-settings/vorssaint_settings.plist +789 −0 raw
flake.lock +3 −3 raw
flake.nix +2 −0 raw
hosts/RyanMac/configuration.nix +9 −85 raw
modules/darwin/yabai/default.nix +296 −0 raw
users/ryan/common.nix +57 −17 raw
users/ryan/darwin.nix +1 −14 raw
users/ryan/linux.nix +35 −73 raw
users/ryan/linux/firefox-nightly/.to-be-deleted +0 −0 raw
users/ryan/linux/foreign.nix +25 −0 raw
users/ryan/linux/guix.nix +33 −0 raw
users/ryan/linux/hypr/hyprland.conf +28 −28 raw
users/ryan/linux/hypr/scripts/autostart.sh +0 −2 raw
users/ryan/linux/mailvelope/gpgmejson.json +0 −7 raw
users/ryan/linux/mailvelope/start.sh +0 −4 raw
users/ryan/linux/spotify-player/app.toml +0 −34 raw
users/ryan/linux/waybar/config +78 −34 raw
users/ryan/linux/waybar/modules/storage.sh +61 −16 raw
users/ryan/linux/waybar/style.css +1 −1 raw
users/ryan/linux/wpaperd/config.toml +1 −1 raw
users/ryan/modules/aria2/default.nix +185 −0 raw
users/ryan/modules/targets/guix/default.nix +254 −0 raw
users/ryan/modules/targets/guix/shepherd.nix +408 −0 raw
users/ryan/modules/zen/default.nix +94 −0 raw
users/ryan/modules/zen/extensions.lock.json +62 −0 raw
users/ryan/modules/zen/extensions.nix +89 −0 raw
users/ryan/modules/zen/lock.sh +61 −0 raw
users/ryan/modules/zen/policies.nix +78 −0 raw
users/ryan/{zen/zenProfile.nix => modules/zen/profile.nix} +0 −0 raw
users/ryan/zen/zenPolicies.nix +0 −128 raw

Commits

HashSubjectAuthorDate
3c1f45ed enable xdg since i guess it wasn't Ryan Schanzenbacher
aeb91079 genericize guix Ryan Schanzenbacher
42a3265e add obs studio to linux Ryan Schanzenbacher
ff4f79c0 add extra socket for ssh gpg stuff Ryan Schanzenbacher
b7d807c3 lock zen extensions; move to new zen module system Ryan Schanzenbacher
4e6cd553 initial aria2 downloader Ryan Schanzenbacher
3d323731 various changes Ryan Schanzenbacher
a2c01524 Change default to be for any screen other than internal so its a bit more gene… Ryan Schanzenbacher
c589cef1 Added vorsaint and settings file, refactor yabai Ryan Schanzenbacher
cd1edb81 thought kdenlive worked on darwin Ryan Schanzenbacher
2e301216 remove unneeded AI documentation Ryan Schanzenbacher
397af8b5 Should be good, need to clean slop still Ryan Schanzenbacher
181f59e7 ssh fix in guix Ryan Schanzenbacher
cabf188a various fixes to guix Ryan Schanzenbacher
0ce4331f extra linux package Ryan Schanzenbacher
08d200c7 cleanup work Ryan Schanzenbacher
diff --git a/MIGRATION.md b/MIGRATION.md
deleted file mode 100644
index 92f6392..0000000
--- a/MIGRATION.md
+++ /dev/null
@@ -1,249 +0,0 @@
-# Migrating guix-dotfiles onto nix-combined
-
-This repo supersedes `guix-dotfiles/home-config/nix-home-manager` (the ad-hoc
-home-manager flake guix-dotfiles was already using) and pulls Linux desktop
-config that used to live in `guix-dotfiles/home-config/home-configuration.scm`
-into home-manager proper. It does **not** replace Guix itself: package
-management, the system config, and three specific home services stay in
-Guix (see "Stays in Guix" below).
-
-`nix-combined` is deliberately its own repo (own history, own remote) so
-that darwin machines never need to clone anything guix-related. It's wired
-into `guix-dotfiles` as a **git submodule** at
-`guix-dotfiles/home-config/nix-combined` - the rest of this section covers
-that wiring; everything after "Stays in Guix" is still a plan for you to
-apply by hand.
-
-## 1. Submodule wiring (already done in this working tree)
-
-Already applied, not yet committed/pushed - review with `git status`/`git
-diff` in `guix-dotfiles` before committing:
-
-- `guix-dotfiles/.gitmodules` added, pointing `home-config/nix-combined` at
- **the local filesystem path** to this checkout of `nix-combined`. That
- only works on this machine. **Before pushing**, create the real remote
- (recommend `https://git.rschanz.org/nix-combined`, matching the
- `nix-dotfiles`/`guix-dotfiles` naming convention) and push `nix-combined`
- there, then run, from `guix-dotfiles`:
- ```
- git submodule set-url home-config/nix-combined https://git.rschanz.org/nix-combined
- git add .gitmodules
- git commit
- ```
-- `home-configuration.scm`'s `%home-symlinks` now points
- `.config/guix/home-config/nix-combined` at `.config/home-manager`
- (previously `.config/guix/home-config/nix-home-manager`).
-- `deploy.sh`'s fresh-install clone now uses `git clone --recurse-submodules`
- so a from-scratch deploy pulls `nix-combined` in the same step.
-
-**On existing machines**, after pulling this change:
-```
-git submodule update --init --recursive
-```
-(or `git clone --recurse-submodules` next time you clone `guix-dotfiles`
-fresh). Then switch with `home-manager switch --flake ~/.config/home-manager#ryan`
-(the `rebuild` zsh alias does this on Linux).
-
-**Updating `nix-combined` later**: since the submodule pins an exact commit,
-pulling new `nix-combined` changes needs a bump in `guix-dotfiles`:
-```
-cd home-config/nix-combined && git pull origin main && cd -
-git add home-config/nix-combined
-git commit -m "bump nix-combined"
-```
-
-The old `guix-dotfiles/home-config/nix-home-manager` directory (the
-pre-submodule ad-hoc flake) can be deleted once you've confirmed the new
-one builds and switches cleanly.
-
-**Leave the `nix-config` symlink entry alone for the first switch.** It's
-what sets `experimental-features = nix-command flakes` for your user today,
-which is what lets `home-manager`/`nix` read this flake at all -
-home-manager can't bootstrap its own prerequisite. `linux.nix` now writes
-its own `~/.config/nix/nix.conf` and `~/.config/nix/registry.json` via
-`xdg.configFile`, so *after* the first successful switch, remove the
-`nix-config` entry from `%home-symlinks` too - otherwise HM's files resolve
-through that symlink and land as untracked files inside the guix-dotfiles
-checkout instead of in the Nix store.
-
-## 2. Remove entries `home-configuration.scm` now duplicates
-
-Once nix-combined delivers a path, Guix must stop also writing it, or
-activation will collide (two things trying to own the same symlink target).
-In `home-configuration.scm`'s `home-xdg-configuration-files-service-type` and
-`home-files-service-type`, remove these lines (nix-combined now owns them):
-
-- `"nvim/init.vim"`, `"nvim/after/ftplugin/markdown/custom.vim"`,
- `"nvim/after/ftplugin/mail/custom.vim"` - nix-combined's neovim config
- (lazy.nvim-based, in `users/ryan/modules/nvim`) is the canonical one now;
- the old vim-plug based `nvim/config/init.vim` + `plug.vim` is superseded.
-- `"hypr/monitors.conf"`, `"hypr/pyprland.toml"`, `"hypr/family_guy.mp4"`,
- `"hypr/hyprlock.conf"`, `"hypr/hypridle.conf"`, `"hypr/kanshi.conf"`,
- `"hypr/subwaysurfer.webm"`, `"hypr/scripts"` (the `hyprland.conf` itself
- was already coming from the old nix-home-manager flake, unchanged in
- spirit here - just re-homed)
-- `"wpaperd"`, `"mpv"`, `"foot"`, `"waybar"`, `"alacritty"`, `"aerc"`
-- `"pulse/client.conf"`
-- `.local/share/nvim/site/autoload/plug.vim` (dead once nvim is lazy.nvim-based)
-- `.local/share/mailvelope`, `.mozilla/native-messaging-hosts/tridactyl.json`,
- `.local/share/tridactyl`
-- **`.icons`** (from `home-files-service-type`) - this one isn't optional
- cleanup, it's a hard prerequisite. Guix delivers `.icons` today as a
- single recursive `local-file` symlink, but `linux.nix`'s
- `home.pointerCursor` (with `x11.enable = true`) needs to write
- `~/.icons/default/index.theme` itself. With the old symlink still in
- place, HM can't create a file inside a symlinked-to-store directory and
- activation fails outright. Remove this entry before your first
- `home-manager switch`.
-- `.gitconfig` (nix-combined's `programs.git` now owns this, with the
- `filter.lfs` and `[sendemail]` sections merged in - they existed in
- guix-dotfiles' `.gitconfig` but were missing from the old nix-dotfiles config)
-- `.ssh/config` (same story - nix-combined's `programs.ssh.settings` is a
- strict superset: added the `linode` host, the `192.168.216.1`
- legacy-algorithm host, the `KexAlgorithms` exclusion on `Host *`, and the
- `Match host * exec "gpg-connect-agent ..."` line)
-- `.docker/cli-plugins` (docker-compose plugin - if you still want this,
- it's cheap to add to `users/ryan/linux.nix`'s `home.file`; it wasn't
- carried over because nothing else referenced it and I didn't want to
- guess at whether you still use it)
-
-`hypr/Wallpapers` and the sway config were left alone deliberately - you said
-you'd handle wallpapers yourself, and `sway` isn't referenced by anything
-active (Hyprland is the real WM; see "Dead/unused" below).
-
-## 3. Switch the login shell to zsh
-
-`guix-dotfiles/modules/ryan-config/base-system.scm`'s `base-operating-system`
-has:
-
-```scheme
-;(shell (file-append zsh "/bin/zsh"))
-```
-
-commented out, so your account's login shell is still bash even though the
-canonical config (nix-dotfiles, and now nix-combined) is zsh. Uncomment that
-line (and rebuild/reconfigure the system) so `programs.zsh` in
-`users/ryan/common.nix` is actually what runs on login. Until you do, bash
-still runs (Guix's `home-bash-service-type` config, if still present, keeps
-working - just don't remove it from `home-configuration.scm` until the shell
-is actually switched, or you'll drop to a shell with no rc file at all).
-
-## Stays in Guix - no home-manager equivalent without systemd
-
-Home-manager's `services.*` modules assume a systemd user session to
-autostart things (or launchd on macOS). Guix uses shepherd instead, and
-home-manager has no shepherd backend. These three keep running exactly as
-they do today, defined in `guix-dotfiles/modules/ryan-services/`:
-
-- **`home-pipewire-service-type`** (pipewire + wireplumber daemons)
-- **`home-dbus-service-type`** (per-user session dbus, since Guix has no
- systemd --user to provide one)
-- **`downloads-garbage-collector-service-type`** (the weekly Downloads
- cleanup shepherd timer)
-
-`home-gpg-agent-service-type` is the one service that *did* migrate cleanly:
-gpg-agent doesn't need a supervisor. It's socket-activated the same way on
-both platforms via the zsh init in `common.nix`
-(`gpgconf --launch gpg-agent` + exporting `SSH_AUTH_SOCK`), which is exactly
-what nix-dotfiles' darwin config already did. `linux.nix` points
-`services.gpg-agent.pinentry.package` at nixpkgs' `pinentry-qt` rather than
-Guix's system profile path, so it isn't cross-dependent on Guix.
-
-`home-spotifyd-service-type` is already commented out/disabled in
-`home-configuration.scm` - nothing to do there.
-
-## Dead/unused - candidates to delete from guix-dotfiles
-
-Found while comparing the two repos; none of these are referenced by
-anything active, so nothing carried them into nix-combined:
-
-- **fish** (`home-config/fish/`) - the `fish` package is commented out of
- `home-configuration.scm`'s package list, but the dotfiles are still
- checked in. Orphaned.
-- **`.nix-channels`** - classic (non-flake) nix-channel file. Now that
- everything is flake-based, this shouldn't be needed.
-- **`icons/XCursor-Pro-Dark*`** - not referenced anywhere; only
- `Bibata-Modern-*` appears in `hypr/hyprland.conf`.
-- **`fonts/Fira Code`, `fonts/Lilex`, `fonts/Monofur For Powerline`,
- `fonts/AnonymicePro`, `fonts/MonofurNerdFont`** - superseded by
- `nerd-fonts.{fira-code,lilex,anonymice,monofur}` from nixpkgs
- (`linux.nix`'s `home.packages`), which ship the same font families.
-- **`home-spotifyd-service-type`** usage - already disabled.
-- **`alacritty`** - config exists in both repos, but no package installs the
- `alacritty` binary in either the guix or the old nix-home-manager config.
- `foot` is what's actually wired into the system (Guix package list,
- kmscon login). I added `pkgs.alacritty` to `linux.nix` so the config isn't
- dead weight, but confirm whether you still use it - if not, drop both the
- package and `users/ryan/linux/alacritty/`.
-- **`sway` config** - not referenced by anything; Hyprland (via
- home-manager) is the actual compositor in use.
-
-## Additive merges (things nix-dotfiles was missing, now fixed)
-
-- **aerc**: `signature_personal`, `signature_rit_personal`,
- `templates/quoted_reply`, `templates/thanks` existed only in
- guix-dotfiles' aerc config, even though `accounts.conf`/`aerc.conf`
- already referenced them by path. Copied into `users/ryan/aerc/`.
-- **git**: `filter.lfs` and `[sendemail]` sections, merged into
- `common.nix`'s `programs.git.settings`.
-- **ssh**: `linode`, `192.168.216.1` (legacy host key/pubkey algorithms),
- the `KexAlgorithms` exclusion, and the `Match host * exec ...` line,
- merged into `common.nix`'s `programs.ssh`.
-
-## Linux desktop stack re-pin (see also the flake-version conversation)
-
-The old `guix-dotfiles/home-config/nix-home-manager/flake.nix` pinned
-home-manager to a 2024 commit specifically so it'd match separately-pinned
-`hyprland`/`hyprlock`/`hyprpicker`/`wpaperd` flake inputs (plus a
-`ryan77627/nixGL` fork and a `rust-overlay` pin for wpaperd). Sharing
-`common.nix` with the darwin config (which tracks home-manager master)
-required moving Linux onto the same nixpkgs/home-manager as darwin.
-
-The good news: current nixpkgs-unstable now packages `hyprland` (0.52.2),
-`hyprlock`, `hypridle`, `hyprpicker`, `wpaperd`, `kanshi`, and `pyprland`
-directly, so those five extra flake inputs (and the rust-overlay pin) are
-gone entirely - `linux.nix` just uses `pkgs.hyprland` etc. Only two
-Linux-specific flake inputs remain in `flake.nix`:
-
-- `nixgl` (still needed - it exists specifically to patch Guix's mesa/GL
- mismatch, unrelated to nixpkgs version)
-- `clipboard-sync` (not in nixpkgs)
-
-**Dropped**: the `hypr-dynamic-cursors` Hyprland plugin (animated cursor
-trails). It has no nixpkgs package and pinning a matching flake input just
-for one cosmetic plugin didn't seem worth the added re-pin risk. If you want
-it back, add it as a flake input with `inputs.nixpkgs.follows = "nixpkgs"`
-and pass it as a plugin in `wayland.windowManager.hyprland.plugins`.
-
-**Dropped**: `mozff` (firefox-nightly flake) - unused. The
-`firefox-nightly` module's package line was already commented out in the
-guix config; it just configures `programs.firefox` (nixpkgs' own package).
-
-**Cursor theme changed**: the vendored `Bibata-Modern-Classic-Hyprland` /
-`Bibata-Modern-daed9-v1.0.2-x11` asset had oddly-versioned internal theme
-names from however that release was originally packaged. Replaced with
-nixpkgs' `bibata-cursors` (`home.pointerCursor` in `linux.nix`) - the
-`HYPRCURSOR_THEME`/`XCURSOR_THEME` env vars in `hyprland.conf` were updated
-to the plain name `Bibata-Modern-Classic` to match. nixpkgs' build doesn't
-have a `-Hyprland`-suffixed hyprcursor variant; Hyprland falls back to
-rendering the XCursor theme directly, which works fine, just without the
-optional cleaner hyprcursor rendering path.
-
-## Verification note
-
-`homeConfigurations.ryan` was checked with:
-
-```
-nix eval --raw .#homeConfigurations.ryan.activationPackage.drvPath
-```
-
-which forces full module evaluation (catches missing options and
-darwin-only packages referenced on Linux) without building anything.
-
-`darwinConfigurations.RyanMac` could not be evaluated on this machine (no
-aarch64-darwin builder). It was verified by structural diff instead:
-`flake.nix`'s darwin-facing inputs/outputs, `hosts/RyanMac/configuration.nix`,
-and `modules/darwin/random-wallpaper` are unchanged from `nix-dotfiles`;
-`common.nix`/`darwin.nix` reproduce `nix-dotfiles/users/ryan/home.nix`
-line-for-line except where noted above. Please run
-`darwin-rebuild build --flake .#RyanMac` yourself before switching for real.
diff --git a/README.md b/README.md
deleted file mode 100644
index 8476171..0000000
--- a/README.md
+++ /dev/null
@@ -1,44 +0,0 @@
-# nix-combined
-
-One flake, two targets:
-
-- `darwinConfigurations.RyanMac` - the mac system (nix-darwin + home-manager),
- equivalent to what `nix-dotfiles` built.
-- `homeConfigurations.ryan` - standalone home-manager for `x86_64-linux`,
- meant to run *on top of* Guix System as the home-manager substitute for
- what `guix-dotfiles/home-config/home-configuration.scm` used to fully own.
- Guix still owns the system config, package management, and three home
- services that need shepherd (pipewire, dbus, downloads-gc) - see
- `MIGRATION.md`.
-
-`users/ryan/common.nix` holds everything shared by both (git, ssh, gpg-agent,
-starship, zsh, zen-browser, neovim). `darwin.nix` and `linux.nix` hold the
-platform-specific rest.
-
-## Darwin
-
-```
-sudo nix run --extra-experimental-features "nix-command flakes" github:nix-darwin/nix-darwin -- switch --flake .#RyanMac \
- --option "extra-substituters" "https://virby-nix-darwin.cachix.org" \
- --option "extra-trusted-public-keys" "virby-nix-darwin.cachix.org-1:z9GiEZeBU5bEeoDQjyfHPMGPBaIQJOOvYOOjGMKIlLo="
-```
-
-Subsequent rebuilds: `rebuild` (zsh alias) or
-`sudo darwin-rebuild switch --flake ~/.config/nix/.`
-
-## Linux (Guix)
-
-Requires `experimental-features = nix-command flakes` in
-`~/.config/nix/nix.conf` (guix-dotfiles already symlinks this in).
-
-```
-nix run --extra-experimental-features "nix-command flakes" home-manager -- switch --flake .#ryan
-```
-
-Subsequent rebuilds: `rebuild` (zsh alias) or
-`home-manager switch --flake ~/.config/home-manager#ryan`.
-
-See `MIGRATION.md` for what needs to change in `guix-dotfiles` for this to
-slot in cleanly (symlink target, overlapping config entries to remove, the
-zsh login-shell switch), what stays Guix-native and why, and what got
-dropped as dead weight along the way.
diff --git a/files/darwin-settings/vorssaint_settings.plist b/files/darwin-settings/vorssaint_settings.plist
new file mode 100644
index 0000000..d4dfed4
--- /dev/null
+++ b/files/darwin-settings/vorssaint_settings.plist
@@ -0,0 +1,789 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>settings</key>
+ <dict>
+ <key>appAppearance</key>
+ <string>system</string>
+ <key>appUpdatesCheckFrequency</key>
+ <string>off</string>
+ <key>appUpdatesIncludeAppStore</key>
+ <true/>
+ <key>appUpdatesNotify</key>
+ <true/>
+ <key>autoCheckUpdates</key>
+ <true/>
+ <key>autoQuitEnabled</key>
+ <true/>
+ <key>autoQuitExceptions</key>
+ <array>
+ <string>com.apple.finder</string>
+ </array>
+ <key>batteryLimitPercent</key>
+ <integer>10</integer>
+ <key>brightnessControlEnabled</key>
+ <false/>
+ <key>brightnessKeysEnabled</key>
+ <false/>
+ <key>brightnessOSDEnabled</key>
+ <false/>
+ <key>cameraPreviewShortcut</key>
+ <string>control+option+command:13</string>
+ <key>cameraPreviewShortcutEnabled</key>
+ <false/>
+ <key>clamshellPreferred</key>
+ <false/>
+ <key>cleanerScheduleFrequency</key>
+ <string>off</string>
+ <key>cleanerScheduleHour</key>
+ <integer>9</integer>
+ <key>cleanerScheduleMinute</key>
+ <integer>0</integer>
+ <key>cleanerScheduleNotify</key>
+ <true/>
+ <key>cleanerScheduleWeekday</key>
+ <integer>2</integer>
+ <key>clipboardHistoryEnabled</key>
+ <false/>
+ <key>clipboardHistoryIncludeImagesFiles</key>
+ <true/>
+ <key>clipboardHistoryLimit</key>
+ <integer>50</integer>
+ <key>clipboardHistoryShortcut</key>
+ <string>control+option+command:9</string>
+ <key>clipboardHistoryShortcutEnabled</key>
+ <true/>
+ <key>clipboardHistorySkipSensitive</key>
+ <true/>
+ <key>colorPickerBareHex</key>
+ <false/>
+ <key>colorPickerFormat</key>
+ <string>hex</string>
+ <key>colorPickerShortcut</key>
+ <string>control+option+command:8</string>
+ <key>colorPickerShortcutEnabled</key>
+ <false/>
+ <key>commandBarAliases</key>
+ <string></string>
+ <key>commandBarDisabledSources</key>
+ <string></string>
+ <key>commandBarHidden</key>
+ <string></string>
+ <key>commandBarPins</key>
+ <string></string>
+ <key>commandBarShortcut</key>
+ <string>option:49</string>
+ <key>commandBarShortcutEnabled</key>
+ <false/>
+ <key>defaultDurationMinutes</key>
+ <integer>0</integer>
+ <key>dockClickCycleWindows</key>
+ <false/>
+ <key>dockClickMinimize</key>
+ <true/>
+ <key>dockPreviewEnabled</key>
+ <true/>
+ <key>extraBrightnessEnabled</key>
+ <false/>
+ <key>extraBrightnessLevel</key>
+ <integer>100</integer>
+ <key>featureAvailable.appUpdates</key>
+ <false/>
+ <key>featureAvailable.autoQuit</key>
+ <true/>
+ <key>featureAvailable.brightness</key>
+ <false/>
+ <key>featureAvailable.cameraPreview</key>
+ <true/>
+ <key>featureAvailable.cleaner</key>
+ <false/>
+ <key>featureAvailable.cleaningMode</key>
+ <false/>
+ <key>featureAvailable.clipboardHistory</key>
+ <false/>
+ <key>featureAvailable.colorPicker</key>
+ <true/>
+ <key>featureAvailable.commandBar</key>
+ <false/>
+ <key>featureAvailable.dockClick</key>
+ <false/>
+ <key>featureAvailable.dockPreview</key>
+ <false/>
+ <key>featureAvailable.extraBrightness</key>
+ <false/>
+ <key>featureAvailable.finderCutPaste</key>
+ <true/>
+ <key>featureAvailable.homebrew</key>
+ <false/>
+ <key>featureAvailable.keepAwake</key>
+ <false/>
+ <key>featureAvailable.keyboardDebounce</key>
+ <false/>
+ <key>featureAvailable.mediaTools</key>
+ <false/>
+ <key>featureAvailable.micMute</key>
+ <false/>
+ <key>featureAvailable.middleClick</key>
+ <false/>
+ <key>featureAvailable.mixer</key>
+ <true/>
+ <key>featureAvailable.monitorCPU</key>
+ <true/>
+ <key>featureAvailable.monitorDisk</key>
+ <true/>
+ <key>featureAvailable.monitorGPU</key>
+ <true/>
+ <key>featureAvailable.monitorMemory</key>
+ <true/>
+ <key>featureAvailable.monitorNetwork</key>
+ <true/>
+ <key>featureAvailable.monitorPower</key>
+ <true/>
+ <key>featureAvailable.mouseButtonShortcuts</key>
+ <false/>
+ <key>featureAvailable.mouseNavigation</key>
+ <false/>
+ <key>featureAvailable.musicBlock</key>
+ <false/>
+ <key>featureAvailable.pastePlain</key>
+ <true/>
+ <key>featureAvailable.quickLauncher</key>
+ <true/>
+ <key>featureAvailable.quickToggles</key>
+ <false/>
+ <key>featureAvailable.radialMenu</key>
+ <true/>
+ <key>featureAvailable.scratchpad</key>
+ <false/>
+ <key>featureAvailable.screenOCR</key>
+ <false/>
+ <key>featureAvailable.screenshot</key>
+ <true/>
+ <key>featureAvailable.scrollInverter</key>
+ <true/>
+ <key>featureAvailable.shelf</key>
+ <true/>
+ <key>featureAvailable.smoothScroll</key>
+ <false/>
+ <key>featureAvailable.soundOutputSwitcher</key>
+ <false/>
+ <key>featureAvailable.superKey</key>
+ <false/>
+ <key>featureAvailable.switcher</key>
+ <true/>
+ <key>featureAvailable.textSnippets</key>
+ <false/>
+ <key>featureAvailable.uninstaller</key>
+ <false/>
+ <key>featureAvailable.urlCleaner</key>
+ <false/>
+ <key>featureAvailable.windowLayout</key>
+ <false/>
+ <key>featureAvailable.windowMaximizer</key>
+ <false/>
+ <key>featuresOnboardingVersion</key>
+ <integer>4</integer>
+ <key>finderCutPasteEnabled</key>
+ <true/>
+ <key>hasOnboarded</key>
+ <true/>
+ <key>hotkeyEnabled</key>
+ <true/>
+ <key>keepAwakeActiveIcon</key>
+ <string>vorssaint</string>
+ <key>keepAwakeAutoStart</key>
+ <false/>
+ <key>keepAwakeConnectedToPower</key>
+ <false/>
+ <key>keepAwakeExternalDisplay</key>
+ <false/>
+ <key>keepAwakeIconTint</key>
+ <string>orange</string>
+ <key>keepAwakeMouseJiggleEnabled</key>
+ <false/>
+ <key>keepAwakeMouseJiggleIntervalMinutes</key>
+ <integer>5</integer>
+ <key>keepAwakeShortcut</key>
+ <string>control+option+command:40</string>
+ <key>keyboardDebounceEnabled</key>
+ <false/>
+ <key>keyboardDebounceKeyWindows</key>
+ <string></string>
+ <key>keyboardDebounceWindowMs</key>
+ <integer>5</integer>
+ <key>lastUpdateIntroVersion</key>
+ <string>3.2.0</string>
+ <key>launchAtLoginWanted</key>
+ <true/>
+ <key>mediaGIFEnd</key>
+ <real>0.0</real>
+ <key>mediaGIFFPS</key>
+ <real>12</real>
+ <key>mediaGIFLoops</key>
+ <true/>
+ <key>mediaGIFQuality</key>
+ <real>0.73999999999999999</real>
+ <key>mediaGIFStart</key>
+ <real>0.0</real>
+ <key>mediaGIFWidth</key>
+ <integer>720</integer>
+ <key>mediaImageFormat</key>
+ <string>jpeg</string>
+ <key>mediaImageMaxDimension</key>
+ <integer>1600</integer>
+ <key>mediaImageQuality</key>
+ <real>0.71999999999999997</real>
+ <key>mediaImageStripMetadata</key>
+ <true/>
+ <key>mediaLastTool</key>
+ <string>videoCompressor</string>
+ <key>mediaTextAccurate</key>
+ <true/>
+ <key>mediaTextLanguageCorrection</key>
+ <true/>
+ <key>mediaVideoCodec</key>
+ <string>h264</string>
+ <key>mediaVideoEnd</key>
+ <real>0.0</real>
+ <key>mediaVideoFPS</key>
+ <real>30</real>
+ <key>mediaVideoKeepAudio</key>
+ <true/>
+ <key>mediaVideoMaxDimension</key>
+ <integer>1280</integer>
+ <key>mediaVideoQuality</key>
+ <real>0.68000000000000005</real>
+ <key>mediaVideoStart</key>
+ <real>0.0</real>
+ <key>menuBarBatteryTemperature</key>
+ <false/>
+ <key>menuBarBatteryTime</key>
+ <false/>
+ <key>menuBarCPU</key>
+ <false/>
+ <key>menuBarCPUTemperature</key>
+ <false/>
+ <key>menuBarCombineTemperatures</key>
+ <true/>
+ <key>menuBarDiskActivity</key>
+ <false/>
+ <key>menuBarDiskUsage</key>
+ <false/>
+ <key>menuBarGPU</key>
+ <false/>
+ <key>menuBarGPUTemperature</key>
+ <false/>
+ <key>menuBarHideIconWithMetrics</key>
+ <false/>
+ <key>menuBarLabelStyle</key>
+ <string>compact</string>
+ <key>menuBarMemoryStyle</key>
+ <string>percent</string>
+ <key>menuBarMetricAppearance</key>
+ <string>values</string>
+ <key>menuBarMetricOrder</key>
+ <string>cpu,cpuTemperature,gpu,gpuTemperature,memory,battery,batteryTime,batteryTemperature,peripheralBattery,network,diskUsage,diskActivity,power</string>
+ <key>menuBarMetricSpacing</key>
+ <string>compact</string>
+ <key>menuBarNetworkUploadFirst</key>
+ <false/>
+ <key>menuBarPeripheralBattery</key>
+ <false/>
+ <key>menuBarPreset</key>
+ <string>dense</string>
+ <key>menuBarSeparateMetrics</key>
+ <false/>
+ <key>menuBarUsageBarCriticalColor</key>
+ <string>#FF453A</string>
+ <key>menuBarUsageBarElevatedColor</key>
+ <string>#FFD60A</string>
+ <key>menuBarUsageBarHighThreshold</key>
+ <integer>90</integer>
+ <key>menuBarUsageBarMediumThreshold</key>
+ <integer>70</integer>
+ <key>menuBarUsageBarNormalColor</key>
+ <string>#64D2FF</string>
+ <key>micMuteMenuBarIndicator</key>
+ <true/>
+ <key>micMuteShortcut</key>
+ <string>control+option+command:46</string>
+ <key>micMuteShortcutEnabled</key>
+ <false/>
+ <key>middleClickEnabled</key>
+ <false/>
+ <key>middleClickExceptions</key>
+ <array/>
+ <key>middleClickTapFingers</key>
+ <integer>0</integer>
+ <key>mixerHeadphonesDisconnectVolumePercent</key>
+ <integer>25</integer>
+ <key>mixerLowerVolumeOnHeadphonesDisconnect</key>
+ <false/>
+ <key>mixerShowFinder</key>
+ <true/>
+ <key>monitorAlertBattery</key>
+ <false/>
+ <key>monitorAlertBatteryPercent</key>
+ <integer>15</integer>
+ <key>monitorAlertCPU</key>
+ <false/>
+ <key>monitorAlertCPUTemperature</key>
+ <false/>
+ <key>monitorAlertCPUTemperatureThreshold</key>
+ <integer>90</integer>
+ <key>monitorAlertCPUThreshold</key>
+ <integer>90</integer>
+ <key>monitorAlertCooldownMinutes</key>
+ <integer>15</integer>
+ <key>monitorAlertDisk</key>
+ <false/>
+ <key>monitorAlertDiskFreePercent</key>
+ <integer>10</integer>
+ <key>monitorAlertMemory</key>
+ <false/>
+ <key>monitorDiskActivity</key>
+ <true/>
+ <key>monitorDiskProtection</key>
+ <true/>
+ <key>monitorDiskSMART</key>
+ <true/>
+ <key>monitorDiskTools</key>
+ <true/>
+ <key>monitorDiskUsage</key>
+ <true/>
+ <key>monitorGraphBattery</key>
+ <true/>
+ <key>monitorGraphCPU</key>
+ <true/>
+ <key>monitorGraphDisk</key>
+ <true/>
+ <key>monitorGraphGPU</key>
+ <true/>
+ <key>monitorGraphMemory</key>
+ <true/>
+ <key>monitorGraphNetwork</key>
+ <true/>
+ <key>monitorGraphPower</key>
+ <true/>
+ <key>monitorIntervalSeconds</key>
+ <integer>2</integer>
+ <key>monitorNetApps</key>
+ <true/>
+ <key>monitorNetSpeed</key>
+ <true/>
+ <key>monitorNetTest</key>
+ <true/>
+ <key>monitorNetTotals</key>
+ <true/>
+ <key>monitorPwrAdapter</key>
+ <true/>
+ <key>monitorPwrBattery</key>
+ <true/>
+ <key>monitorPwrHealth</key>
+ <true/>
+ <key>monitorPwrSystem</key>
+ <true/>
+ <key>monitorPwrTimeRemaining</key>
+ <true/>
+ <key>monitorShowDisk</key>
+ <true/>
+ <key>monitorShowFanControlBeta</key>
+ <false/>
+ <key>monitorShowMixer</key>
+ <true/>
+ <key>monitorShowNetwork</key>
+ <true/>
+ <key>monitorShowPower</key>
+ <true/>
+ <key>monitorShowSystem</key>
+ <true/>
+ <key>monitorSysAlerts</key>
+ <true/>
+ <key>monitorSysBattery</key>
+ <true/>
+ <key>monitorSysCPU</key>
+ <true/>
+ <key>monitorSysGPU</key>
+ <true/>
+ <key>monitorSysMemory</key>
+ <true/>
+ <key>monitorSysTemps</key>
+ <true/>
+ <key>monitorSysUptime</key>
+ <true/>
+ <key>mouseButtonExceptions</key>
+ <array/>
+ <key>mouseButtonShortcuts</key>
+ <dict/>
+ <key>mouseButtonShortcutsEnabled</key>
+ <false/>
+ <key>mouseNavigationEnabled</key>
+ <false/>
+ <key>mouseNavigationExceptions</key>
+ <array/>
+ <key>musicBlockEnabled</key>
+ <false/>
+ <key>musicBlockReplacementPath</key>
+ <string></string>
+ <key>onboardingStep</key>
+ <integer>0</integer>
+ <key>panelCollapsedResetVersion</key>
+ <string>2.15.1</string>
+ <key>panelControlAutoQuit</key>
+ <true/>
+ <key>panelControlCutPaste</key>
+ <true/>
+ <key>panelControlDockClick</key>
+ <true/>
+ <key>panelControlDockClickCycle</key>
+ <true/>
+ <key>panelControlDockPreview</key>
+ <true/>
+ <key>panelControlFilesExpanded</key>
+ <false/>
+ <key>panelControlInputExpanded</key>
+ <false/>
+ <key>panelControlKeyDebounce</key>
+ <true/>
+ <key>panelControlMiddleClick</key>
+ <true/>
+ <key>panelControlMouseButtonShortcuts</key>
+ <true/>
+ <key>panelControlMouseNavigation</key>
+ <true/>
+ <key>panelControlMouseScroll</key>
+ <true/>
+ <key>panelControlRadialMenu</key>
+ <true/>
+ <key>panelControlShelf</key>
+ <true/>
+ <key>panelControlSuperKey</key>
+ <true/>
+ <key>panelControlSwitcher</key>
+ <true/>
+ <key>panelControlTextSnippets</key>
+ <true/>
+ <key>panelControlWindowMaximize</key>
+ <true/>
+ <key>panelControlWindowsExpanded</key>
+ <false/>
+ <key>panelNavigationEnabled</key>
+ <true/>
+ <key>panelPowerOrder</key>
+ <string>system,adapter,battery,remaining,health</string>
+ <key>panelSectionOrder</key>
+ <string>keepAwake,brightness,system,mixer,power,disk,network,fanControl,utilities,controls,toggles</string>
+ <key>panelShowBrightness</key>
+ <true/>
+ <key>panelShowControls</key>
+ <true/>
+ <key>panelShowKeepAwake</key>
+ <true/>
+ <key>panelShowToggles</key>
+ <true/>
+ <key>panelShowUtilities</key>
+ <true/>
+ <key>panelToggleDarkMode</key>
+ <true/>
+ <key>panelToggleDesktopIcons</key>
+ <true/>
+ <key>panelToggleDisplayOff</key>
+ <true/>
+ <key>panelToggleEjectDisks</key>
+ <true/>
+ <key>panelToggleEmptyTrash</key>
+ <true/>
+ <key>panelToggleHiddenFiles</key>
+ <true/>
+ <key>panelToggleLockScreen</key>
+ <true/>
+ <key>panelToggleScreenSaver</key>
+ <true/>
+ <key>panelUtilityAppUpdates</key>
+ <true/>
+ <key>panelUtilityCameraPreview</key>
+ <true/>
+ <key>panelUtilityCleaner</key>
+ <true/>
+ <key>panelUtilityCleaning</key>
+ <true/>
+ <key>panelUtilityClipboard</key>
+ <true/>
+ <key>panelUtilityColorPicker</key>
+ <true/>
+ <key>panelUtilityCommandBar</key>
+ <true/>
+ <key>panelUtilityHomebrew</key>
+ <true/>
+ <key>panelUtilityMedia</key>
+ <true/>
+ <key>panelUtilityMicMute</key>
+ <true/>
+ <key>panelUtilityQuickLauncher</key>
+ <true/>
+ <key>panelUtilityScratchpad</key>
+ <true/>
+ <key>panelUtilityScreenOCR</key>
+ <true/>
+ <key>panelUtilityScreenshot</key>
+ <true/>
+ <key>panelUtilityURLCleaner</key>
+ <true/>
+ <key>panelUtilityUninstaller</key>
+ <true/>
+ <key>panelUtilityWindowLayout</key>
+ <true/>
+ <key>pastePlainEnabled</key>
+ <true/>
+ <key>pastePlainShortcut</key>
+ <string>shift+command:9</string>
+ <key>previewSize</key>
+ <string>normal</string>
+ <key>quickLauncherHiddenItems</key>
+ <string></string>
+ <key>quickLauncherShortcut</key>
+ <string>option+shift:9</string>
+ <key>quickLauncherShortcutEnabled</key>
+ <true/>
+ <key>radialMenuActivationMode</key>
+ <string>pressOrHold</string>
+ <key>radialMenuAtPointer</key>
+ <true/>
+ <key>radialMenuEnabled</key>
+ <false/>
+ <key>radialMenuItems</key>
+ <data>
+ W3sicGF5bG9hZCI6InNjcmVlbnNob3QiLCJjaGlsZHJlbiI6W10sIm5hbWUi
+ OiIiLCJpZCI6IjU1N0M5QjY0LURBRUQtNEY0OS1CMjAyLUE2RTRFMjg3QkE3
+ MSIsImtpbmQiOiJ0b29sIiwic3ltYm9sTmFtZSI6IiJ9LHsicGF5bG9hZCI6
+ ImNvbG9yUGlja2VyIiwiY2hpbGRyZW4iOltdLCJuYW1lIjoiIiwiaWQiOiI4
+ NDc1RTg2OS0yNjFELTQ2OEUtODJEOC1BMjFGM0VFN0VGQjgiLCJraW5kIjoi
+ dG9vbCIsInN5bWJvbE5hbWUiOiIifSx7InBheWxvYWQiOiJjYW1lcmFQcmV2
+ aWV3IiwiY2hpbGRyZW4iOltdLCJuYW1lIjoiIiwiaWQiOiIzRTNCRUU3MS05
+ QzgxLTQ5QjMtOTEzNS03OTA5RkY5MDc4QUQiLCJraW5kIjoidG9vbCIsInN5
+ bWJvbE5hbWUiOiIifV0=
+ </data>
+ <key>radialMenuMouseButton</key>
+ <string>off</string>
+ <key>radialMenuShortcut</key>
+ <string>control+option+command:49</string>
+ <key>releaseNotesOnUpdate</key>
+ <true/>
+ <key>scratchpadCloseOnClickOutside</key>
+ <true/>
+ <key>scratchpadRetention</key>
+ <string>never</string>
+ <key>scratchpadShortcut</key>
+ <string>control+option+command:45</string>
+ <key>scratchpadShortcutEnabled</key>
+ <false/>
+ <key>screenOCRDetectQRCodes</key>
+ <true/>
+ <key>screenOCRShortcut</key>
+ <string>control+option+command:17</string>
+ <key>screenOCRShortcutEnabled</key>
+ <false/>
+ <key>screenshotAnnotationShadows</key>
+ <false/>
+ <key>screenshotBackdropPresets</key>
+ <string>[]</string>
+ <key>screenshotBackdropStyle</key>
+ <string></string>
+ <key>screenshotCopyToClipboard</key>
+ <true/>
+ <key>screenshotDefaultAction</key>
+ <string></string>
+ <key>screenshotDelay</key>
+ <integer>0</integer>
+ <key>screenshotDownscale</key>
+ <false/>
+ <key>screenshotFileNamePattern</key>
+ <string></string>
+ <key>screenshotFileNumberNext</key>
+ <integer>1</integer>
+ <key>screenshotFileNumberStart</key>
+ <integer>1</integer>
+ <key>screenshotFreeze</key>
+ <true/>
+ <key>screenshotIncludePointer</key>
+ <false/>
+ <key>screenshotLastColor</key>
+ <string>red</string>
+ <key>screenshotLastSticker</key>
+ <string>check</string>
+ <key>screenshotLastStroke</key>
+ <string>medium</string>
+ <key>screenshotLastTool</key>
+ <string>select</string>
+ <key>screenshotOpenEditorDirectly</key>
+ <false/>
+ <key>screenshotSaveFolder</key>
+ <string></string>
+ <key>screenshotSaveSubfolder</key>
+ <string></string>
+ <key>screenshotShortcut</key>
+ <string>control+option+command:21</string>
+ <key>screenshotShortcutEnabled</key>
+ <true/>
+ <key>screenshotShowLastRegion</key>
+ <true/>
+ <key>screenshotToolOrder</key>
+ <string>select,arrow,pixelate,crop,text,sticker,rect,highlight,freehand,line,ellipse,counter,redact</string>
+ <key>screenshotToolShortcutsEnabled</key>
+ <true/>
+ <key>scrollInverterEnabled</key>
+ <true/>
+ <key>scrollInverterExceptions</key>
+ <array/>
+ <key>shelfAutomaticExclusions</key>
+ <array/>
+ <key>shelfCloseAfterDrop</key>
+ <false/>
+ <key>shelfDropZoneEnabled</key>
+ <true/>
+ <key>shelfEnabled</key>
+ <true/>
+ <key>shelfRemoveAfterDrop</key>
+ <true/>
+ <key>shelfShakeToOpen</key>
+ <true/>
+ <key>shelfShortcut</key>
+ <string>control+option+command:2</string>
+ <key>shelfShortcutEnabled</key>
+ <true/>
+ <key>showCountdownInMenuBar</key>
+ <false/>
+ <key>smoothScrollEnabled</key>
+ <false/>
+ <key>smoothScrollExceptions</key>
+ <array/>
+ <key>smoothScrollStep</key>
+ <integer>40</integer>
+ <key>snippetLibraryEnabled</key>
+ <false/>
+ <key>snippetLibraryShortcut</key>
+ <string>control+option+command:37</string>
+ <key>soundOutputSwitcherEnabled</key>
+ <false/>
+ <key>soundOutputSwitcherShortcut</key>
+ <string>control+option+command:1</string>
+ <key>superKeyEnabled</key>
+ <false/>
+ <key>superKeySoloAction</key>
+ <string>none</string>
+ <key>supportUpdateIntroVersion</key>
+ <string>3.1.13</string>
+ <key>switcherCurrentSpaceOnly</key>
+ <false/>
+ <key>switcherEnabled</key>
+ <true/>
+ <key>switcherIconRowMode</key>
+ <false/>
+ <key>switcherMergeTabs</key>
+ <false/>
+ <key>switcherShortcut</key>
+ <string>command:48</string>
+ <key>switcherShowWindowlessFinder</key>
+ <true/>
+ <key>switcherSimpleMode</key>
+ <false/>
+ <key>switcherWindowShortcut</key>
+ <string>command:50</string>
+ <key>switcherWindowlessApps</key>
+ <string>finder</string>
+ <key>temperatureUnit</key>
+ <string>celsius</string>
+ <key>textSnippetsEnabled</key>
+ <false/>
+ <key>updateHighlightsSeenVersion</key>
+ <string>3.2.0</string>
+ <key>urlCleanerEnabled</key>
+ <false/>
+ <key>whatsAppDownloadsAutomaticEnabled</key>
+ <false/>
+ <key>whatsAppDownloadsCategories</key>
+ <string>image,video,audio</string>
+ <key>whatsAppDownloadsIncludeExisting</key>
+ <false/>
+ <key>whatsAppDownloadsNotify</key>
+ <true/>
+ <key>whatsAppDownloadsRetentionDays</key>
+ <integer>7</integer>
+ <key>whatsAppOrganizerCategories</key>
+ <string>image,video,audio,document,archive,other</string>
+ <key>whatsAppOrganizerDelayMinutes</key>
+ <integer>5</integer>
+ <key>whatsAppOrganizerDuplicateAction</key>
+ <string>trashNew</string>
+ <key>whatsAppOrganizerEnabled</key>
+ <false/>
+ <key>whatsAppOrganizerLayout</key>
+ <string>flat</string>
+ <key>windowGestureEnabled</key>
+ <false/>
+ <key>windowGestureModifiers</key>
+ <string>control+command</string>
+ <key>windowGestureRaiseWindow</key>
+ <false/>
+ <key>windowLayoutHiddenActions</key>
+ <string></string>
+ <key>windowLayoutShortcutBottom</key>
+ <string>control+option:125</string>
+ <key>windowLayoutShortcutBottomCenterSixth</key>
+ <string>none</string>
+ <key>windowLayoutShortcutBottomLeft</key>
+ <string>control+option:38</string>
+ <key>windowLayoutShortcutBottomLeftSixth</key>
+ <string>none</string>
+ <key>windowLayoutShortcutBottomRight</key>
+ <string>control+option:40</string>
+ <key>windowLayoutShortcutBottomRightSixth</key>
+ <string>none</string>
+ <key>windowLayoutShortcutCenter</key>
+ <string>control+option:8</string>
+ <key>windowLayoutShortcutCenterThird</key>
+ <string>control+option:3</string>
+ <key>windowLayoutShortcutFullScreen</key>
+ <string>none</string>
+ <key>windowLayoutShortcutLeft</key>
+ <string>control+option:123</string>
+ <key>windowLayoutShortcutLeftThird</key>
+ <string>control+option:2</string>
+ <key>windowLayoutShortcutLeftTwoThirds</key>
+ <string>control+option:14</string>
+ <key>windowLayoutShortcutMaximize</key>
+ <string>control+option:36</string>
+ <key>windowLayoutShortcutNextDisplay</key>
+ <string>control+option+command:124</string>
+ <key>windowLayoutShortcutRestore</key>
+ <string>control+option:15</string>
+ <key>windowLayoutShortcutRight</key>
+ <string>control+option:124</string>
+ <key>windowLayoutShortcutRightThird</key>
+ <string>control+option:5</string>
+ <key>windowLayoutShortcutRightTwoThirds</key>
+ <string>control+option:17</string>
+ <key>windowLayoutShortcutTop</key>
+ <string>control+option:126</string>
+ <key>windowLayoutShortcutTopCenterSixth</key>
+ <string>none</string>
+ <key>windowLayoutShortcutTopLeft</key>
+ <string>control+option:32</string>
+ <key>windowLayoutShortcutTopLeftSixth</key>
+ <string>none</string>
+ <key>windowLayoutShortcutTopRight</key>
+ <string>control+option:34</string>
+ <key>windowLayoutShortcutTopRightSixth</key>
+ <string>none</string>
+ <key>windowLayoutShortcutsEnabled</key>
+ <false/>
+ <key>windowMaximizeEnabled</key>
+ <true/>
+ </dict>
+ <key>vorssaintBackupAppVersion</key>
+ <string>3.2.0</string>
+ <key>vorssaintBackupVersion</key>
+ <integer>1</integer>
+</dict>
+</plist>
diff --git a/flake.lock b/flake.lock
index 1a14c7f..4ff553b 100644
--- a/flake.lock
+++ b/flake.lock
@@ -293,11 +293,11 @@
]
},
"locked": {
- "lastModified": 1785569296,
- "narHash": "sha256-ux2+60xcjl4LloKPevqrkP+uY4DNZXhzVd+TYrfE2+w=",
+ "lastModified": 1785784141,
+ "narHash": "sha256-VG4SKltdvSWd6Y2ppsDdSZC8m3x9xtFevh5wndYqo08=",
"owner": "0xc000022070",
"repo": "zen-browser-flake",
- "rev": "945e834a12a97a5e3b08869960e35c7ccfde6f69",
+ "rev": "073be825a844ac51ece75ebfe9d82b5e2ee89a5c",
"type": "github"
},
"original": {
diff --git a/flake.nix b/flake.nix
index 1fb7bff..3f110c1 100644
--- a/flake.nix
+++ b/flake.nix
@@ -88,6 +88,8 @@
inputs.zen-browser.homeModules.beta
./users/ryan/common.nix
./users/ryan/linux.nix
+ ./users/ryan/linux/foreign.nix
+ ./users/ryan/linux/guix.nix
];
};
};
diff --git a/hosts/RyanMac/configuration.nix b/hosts/RyanMac/configuration.nix
index e8339e6..e98db39 100644
--- a/hosts/RyanMac/configuration.nix
+++ b/hosts/RyanMac/configuration.nix
@@ -24,20 +24,6 @@ let
];
});
- mkSpace = i: {
- name = toString (117+i);
- value = {
- enabled = true;
- value = {
- type = "standard";
- parameters = [ (48 + i) (builtins.elemAt [ 18 19 20 21 22 23 25 26 28 ] (i - 1)) 524288 ]; # opt + num
- #parameters = [ (48 + i) (builtins.elemAt [ 18 19 20 21 22 23 25 26 28 ] (i - 1)) 1048576 ]; # cmd + num
- };
- };
- };
-
- spaceHotkeys = builtins.listToAttrs (map mkSpace (lib.range 1 9));
-
manualHotkeys = {
"64" = {
enabled = true;
@@ -49,6 +35,7 @@ in {
imports = [
inputs.nix-homebrew.darwinModules.nix-homebrew
../../modules/darwin/random-wallpaper
+ ../../modules/darwin/yabai
];
# Define the system's user and home dir location
@@ -69,43 +56,14 @@ in {
};
# Configure yabai
- services.yabai = {
+ local.yabai = {
enable = true;
- enableScriptingAddition = false;
- config = {
- mouse_follows_focus = "on";
- layout = "bsp";
- window_placement = "second_child";
- top_padding = "10";
- bottom_padding = "10";
- left_padding = "10";
- right_padding = "10";
- window_gap = "10";
- mouse_modifier = "alt";
- mouse_drop_action = "swap";
- mouse_action1 = "move";
- mouse_action2 = "resize";
- focus_follows_mouse = "autofocus";
+ defaultPadding = 10;
+ defaultTopPadding = 40;
+ displayTopPadding = {
+ "37D8832A-2D66-02CA-B9F7-8F30A301B230" = 10; # macOS retina display
};
- extraConfig = ''
- yabai -m rule --add app='System Settings' manage=off
- yabai -m rule --add app="^Zen$" title="^Picture-in-Picture$" manage=off
- ''
- # Add simple-bar signals if enabled
- + lib.optionalString config.home-manager.users.${username}.ryan.simple-bar.enable ''
- yabai -m signal --add event=window_focused action="curl http://localhost:7776/yabai/spaces/refresh && curl http://localhost:7776/yabai/windows/refresh" label="Refresh simple-bar spaces & windows when focused application changes"
- yabai -m signal --add event=window_resized action="curl http://localhost:7776/yabai/spaces/refresh && curl http://localhost:7776/yabai/windows/refresh" label="Refresh simple-bar spaces & windows when a window is resized"
- yabai -m signal --add event=window_destroyed action="curl http://localhost:7776/yabai/spaces/refresh && curl http://localhost:7776/yabai/windows/refresh" label="Refresh simple-ba spaces & windows when an application window is closed"
- yabai -m signal --add event=space_changed action="curl http://localhost:7776/yabai/spaces/refresh && curl http://localhost:7776/yabai/windows/refresh" label="Refresh simple-bar spaces & windows on space change"
- yabai -m signal --add event=display_changed action="curl http://localhost:7776/yabai/spaces/refresh && curl http://localhost:7776/yabai/windows/refresh" label="Refresh simple-bar spaces & windows on display focus change"
- yabai -m signal --add event=window_title_changed action="curl http://localhost:7776/yabai/spaces/refresh && curl http://localhost:7776/yabai/windows/refresh" label="Refresh simple-bar spaces & windows when current window title changes"
- yabai -m signal --add event=space_destroyed action="curl http://localhost:7776/yabai/spaces/refresh && curl http://localhost:7776/yabai/windows/refresh" label="Refresh simple-bar spaces & windows on space removal"
- yabai -m signal --add event=space_created action="curl http://localhost:7776/yabai/spaces/refresh && curl http://localhost:7776/yabai/windows/refresh" label="Refresh simple-bar spaces & windows on space creation"
- yabai -m signal --add event=application_activated action="curl http://localhost:7776/yabai/spaces/refresh && curl http://localhost:7776/yabai/windows/refresh" label="Refresh simple-bar spaces & windows when application is activated"
- yabai -m signal --add event=display_added action="curl http://localhost:7776/yabai/displays/refresh" label="Refresh simple-bar displays when a new dispay is added"
- yabai -m signal --add event=display_removed action="curl http://localhost:7776/yabai/displays/refresh" label="Refresh simple-bar displays when a dispay is removed"
- yabai -m signal --add event=display_moved action="curl http://localhost:7776/yabai/displays/refresh" label="Refresh simple-bar displays when a dispay is moved"
- '';
+ extraSymbolicHotkeys = manualHotkeys;
};
# Configure JankyBorders
@@ -200,6 +158,8 @@ in {
"gimp"
"kdenlive"
"rustdesk"
+ "vorssaint"
+ "obs"
];
};
@@ -209,35 +169,6 @@ in {
skhdConfig = ''
alt - d : osascript -e 'tell application "System Events" to key code 49 using {command down}'
alt - return : osascript -e 'tell application "Ghostty"' -e 'new window' -e 'activate' -e 'end tell'
-
- shift + alt - q : ${pkgs.yabai}/bin/yabai -m window --close
-
- alt - up : ${pkgs.yabai}/bin/yabai -m window --focus north || ${pkgs.yabai}/bin/yabai -m display --focus north
- shift + alt - up : ${pkgs.yabai}/bin/yabai -m window --swap north || ${pkgs.yabai}/bin/yabai -m window --display north
- alt - down : ${pkgs.yabai}/bin/yabai -m window --focus south || ${pkgs.yabai}/bin/yabai -m display --focus south
- shift + alt - down : ${pkgs.yabai}/bin/yabai -m window --swap south || ${pkgs.yabai}/bin/yabai -m window --display south
- alt - right : ${pkgs.yabai}/bin/yabai -m window --focus east || ${pkgs.yabai}/bin/yabai -m display --focus east
- shift + alt - right : ${pkgs.yabai}/bin/yabai -m window --swap east || ${pkgs.yabai}/bin/yabai -m window --display east
- alt - left : ${pkgs.yabai}/bin/yabai -m window --focus west || ${pkgs.yabai}/bin/yabai -m display --focus west
- shift + alt - left: ${pkgs.yabai}/bin/yabai -m window --swap west || ${pkgs.yabai}/bin/yabai -m window --display west
-
- shift + alt - space: ${pkgs.yabai}/bin/yabai -m window --toggle float
-
- shift + alt - return: ${pkgs.yabai}/bin/yabai -m window --toggle sticky
-
- alt - f : ${pkgs.yabai}/bin/yabai -m window --toggle zoom-fullscreen
-
- shift + alt - f: ${pkgs.yabai}/bin/yabai -m window --toggle native-fullscreen
-
- shift + alt - 1 : ${pkgs.yabai}/bin/yabai -m window --space 1
- shift + alt - 2 : ${pkgs.yabai}/bin/yabai -m window --space 2
- shift + alt - 3 : ${pkgs.yabai}/bin/yabai -m window --space 3
- shift + alt - 4 : ${pkgs.yabai}/bin/yabai -m window --space 4
- shift + alt - 5 : ${pkgs.yabai}/bin/yabai -m window --space 5
- shift + alt - 6 : ${pkgs.yabai}/bin/yabai -m window --space 6
- shift + alt - 7 : ${pkgs.yabai}/bin/yabai -m window --space 7
- shift + alt - 8 : ${pkgs.yabai}/bin/yabai -m window --space 8
- shift + alt - 9 : ${pkgs.yabai}/bin/yabai -m window --space 9
'';
};
@@ -250,9 +181,6 @@ in {
};
system.defaults = {
- WindowManager = {
- EnableTilingOptionAccelerator = false;
- };
NSGlobalDomain = {
# 24 hour time
AppleICUForce24HourTime = true;
@@ -391,10 +319,6 @@ in {
"com.apple.Accessibility" = {
ReduceMotionEnabled = 1;
};
-
- "com.apple.symbolichotkeys" = {
- AppleSymbolicHotKeys = manualHotkeys // spaceHotkeys;
- };
};
universalaccess.reduceMotion = true;
};
diff --git a/modules/darwin/yabai/default.nix b/modules/darwin/yabai/default.nix
new file mode 100644
index 0000000..8aa4fbf
--- /dev/null
+++ b/modules/darwin/yabai/default.nix
@@ -0,0 +1,296 @@
+# modules/darwin/yabai/default.nix
+{ config, lib, pkgs, ... }:
+
+let
+ cfg = config.local.yabai;
+
+ spaceFocusBinds = lib.concatStringsSep "\n"
+ (map (i: "shift + alt - ${toString i} : ${yabaiBin} -m window --space ${toString i}")
+ (lib.range 1 cfg.spaceHotkeys.count));
+
+ # symbolic hotkey IDs 118..126 == "Switch to Desktop 1".."Switch to Desktop 9"
+ spaceKeycodes = [ 18 19 20 21 22 23 25 26 28 ];
+
+ mkSpaceHotkey = i: {
+ name = toString (117 + i);
+ value = {
+ enabled = true;
+ value = {
+ type = "standard";
+ parameters = [
+ (48 + i) # ASCII '1'..'9'
+ (builtins.elemAt spaceKeycodes (i - 1)) # virtual keycode
+ cfg.spaceHotkeys.modifierMask
+ ];
+ };
+ };
+ };
+
+ spaceHotkeys = builtins.listToAttrs
+ (map mkSpaceHotkey (lib.range 1 cfg.spaceHotkeys.count));
+
+ padArms = lib.concatStringsSep "\n" (lib.mapAttrsToList
+ (uuid: pad: " ${uuid}) pad=${toString pad} ;;")
+ cfg.displayTopPadding);
+
+ paddingSignals = ''
+ yabai -m signal --add event=display_added action="${displayPaddingScript}" label="per-display padding (added)"
+ yabai -m signal --add event=display_removed action="${displayPaddingScript}" label="per-display padding (removed)"
+ yabai -m signal --add event=space_created action="${displayPaddingScript}" label="per-display padding (space)"
+ ${displayPaddingScript}
+ '';
+
+
+ displayPaddingScript = pkgs.writeShellScript "yabai-display-padding" ''
+ set -eu
+ yabai="${cfg.package}/bin/yabai"
+
+ "$yabai" -m query --displays \
+ | ${lib.getExe pkgs.jq} -r '.[] | "\(.uuid) \(.spaces | map(tostring) | join(" "))"' \
+ | while read -r uuid spaces; do
+ case "$uuid" in
+${padArms}
+ *) pad=${toString cfg.defaultTopPadding} ;;
+ esac
+ for s in $spaces; do
+ for k in top_padding; do
+ "$yabai" -m config --space "$s" "$k" "$pad" || true
+ done
+ done
+ done
+ '';
+
+ yabaiBin = "${cfg.package}/bin/yabai";
+
+ sudoersFile = pkgs.runCommand "sudoers-yabai" { } ''
+ YABAI_BIN="${yabaiBin}"
+ SHASUM=$(sha256sum "$YABAI_BIN" | cut -d' ' -f1)
+ cat > "$out" <<EOF
+ ${cfg.user} ALL=(root) NOPASSWD: sha256:$SHASUM $YABAI_BIN --load-sa
+ EOF
+ '';
+
+ dockRestartSignal = ''
+ yabai -m signal --add event=dock_did_restart action="/usr/bin/sudo ${yabaiBin} --load-sa" label="reload scripting addition"
+ '';
+
+ directionBinds = lib.concatStringsSep "\n" (map (d: ''
+ alt - ${d.key} : ${yabaiBin} -m window --focus ${d.dir} || ${yabaiBin} -m display --focus ${d.dir}
+ shift + alt - ${d.key} : ${yabaiBin} -m window --swap ${d.dir} || ${yabaiBin} -m window --display ${d.dir}
+ '') [
+ { key = "up"; dir = "north"; }
+ { key = "down"; dir = "south"; }
+ { key = "right"; dir = "east"; }
+ { key = "left"; dir = "west"; }
+ ]);
+
+ spaceMoveBinds = lib.concatStringsSep "\n"
+ (map (i: "shift + alt - ${toString i} : ${yabaiBin} -m window --space ${toString i}")
+ (lib.range 1 cfg.spaceHotkeys.count));
+
+in {
+ options.local.yabai = {
+ enable = lib.mkEnableOption "yabai tiling window manager setup";
+
+ package = lib.mkOption {
+ type = lib.types.package;
+ default = pkgs.yabai;
+ description = "yabai package. Referenced by keybindings and the padding script.";
+ };
+
+ user = lib.mkOption {
+ type = lib.types.str;
+ default = config.system.primaryUser;
+ defaultText = lib.literalExpression "config.system.primaryUser";
+ description = ''
+ User that is granted permission to run --load-sa
+ '';
+ };
+
+ enableScriptingAddition = lib.mkOption {
+ type = lib.types.bool;
+ default = false;
+ description = ''
+ Only gates space creation/destruction and moving windows between
+ spaces. Window geometry and padding work without it.
+ '';
+ };
+
+ useYabaiSpaceFocus = lib.mkOption {
+ type = lib.types.bool;
+ default = false;
+ description = ''
+ Bind alt+n to yabai native control rather than skhd. Requires scripting
+ additions.
+ '';
+ };
+
+ defaultPadding = lib.mkOption {
+ type = lib.types.int;
+ default = 10;
+ description = ''
+ Padding and window_gap for any display not listed in displayTopPadding.
+ Sized for Retina (2x) panels.
+ '';
+ };
+
+ defaultTopPadding = lib.mkOption {
+ type = lib.types.int;
+ default = 40;
+ description = ''
+ Padding for top edge of screen
+ '';
+ };
+
+ displayTopPadding = lib.mkOption {
+ type = lib.types.attrsOf lib.types.int;
+ default = { };
+ example = lib.literalExpression ''
+ { "37D8832A-2D66-02CA-B9F7-8F30A301B230" = 40; }
+ '';
+ description = ''
+ Display UUID -> padding value. The value is applied to all four
+ paddings and to window_gap for every space on that display.
+
+ Get UUIDs with:
+ yabai -m query --displays | jq -r '.[] | "\(.uuid) \(.frame)"'
+
+ Do not key off frame dimensions: yabai reports points, not pixels,
+ so a scaled 1080p panel will not report 1920x1080.
+ '';
+ };
+
+ manageKeybindings = lib.mkOption {
+ type = lib.types.bool;
+ default = true;
+ description = "Emit yabai skhd bindings into services.skhd.skhdConfig.";
+ };
+
+ spaceHotkeys = {
+ enable = lib.mkEnableOption "opt+<n> macOS space-switching hotkeys" // {
+ default = true;
+ };
+
+ count = lib.mkOption {
+ type = lib.types.ints.between 1 9;
+ default = 9;
+ description = "Number of spaces to bind. macOS exposes 9 symbolic hotkeys.";
+ };
+
+ modifierMask = lib.mkOption {
+ type = lib.types.int;
+ default = 524288; # option
+ description = "Carbon modifier mask. 524288 = option, 1048576 = command.";
+ };
+ };
+
+ extraSymbolicHotkeys = lib.mkOption {
+ type = lib.types.attrs;
+ default = { };
+ description = ''
+ Merged into com.apple.symbolichotkeys alongside the space hotkeys.
+ Put non-yabai symbolic hotkeys here rather than defining
+ CustomUserPreferences."com.apple.symbolichotkeys" in the host config --
+ CustomUserPreferences is types.attrs and merges shallowly, so a second
+ definition of that domain silently clobbers this one.
+ '';
+ };
+
+ extraSettings = lib.mkOption {
+ type = lib.types.attrs;
+ default = { };
+ description = "Extra keys merged into services.yabai.config.";
+ };
+
+ extraConfig = lib.mkOption {
+ type = lib.types.lines;
+ default = "";
+ description = "Extra shell appended to yabairc (rules, signals).";
+ };
+ };
+
+ config = lib.mkIf cfg.enable {
+ assertions = [
+ {
+ assertion = cfg.user != null && cfg.user != "";
+ message = ''
+ local.yabai.user is unset. Set it or system.primaryUser
+ '';
+ }
+ {
+ assertion = cfg.useYabaiSpaceFocus -> cfg.enableScriptingAssition;
+ message = ''
+ local.yabai.useYabaiSpaceFocus requires enableScriptingAddition.
+ '';
+ }
+ ];
+
+ services.yabai = {
+ enable = true;
+ inherit (cfg) package enableScriptingAddition;
+
+ config = {
+ mouse_follows_focus = "on";
+ layout = "bsp";
+ window_placement = "second_child";
+ top_padding = toString cfg.defaultTopPadding;
+ bottom_padding = toString cfg.defaultPadding;
+ left_padding = toString cfg.defaultPadding;
+ right_padding = toString cfg.defaultPadding;
+ window_gap = toString cfg.defaultPadding;
+ mouse_modifier = "alt";
+ mouse_drop_action = "swap";
+ mouse_action1 = "move";
+ mouse_action2 = "resize";
+ focus_follows_mouse = "autofocus";
+ } // cfg.extraSettings;
+
+ extraConfig = lib.concatStringsSep "\n" (
+ [
+ ''
+ yabai -m rule --add app='System Settings' manage=off
+ yabai -m rule --add app="^Zen$" title="^Picture-in-Picture$" manage=off
+ ''
+ ]
+ ++ lib.optional cfg.enableScriptingAddition dockRestartSignal
+ ++ lib.optional (cfg.displayTopPadding != { }) paddingSignals
+ ++ lib.optional (cfg.extraConfig != "") cfg.extraConfig
+ );
+ };
+
+ environment.etc."sudoers.d/yabai".source =
+ lib.mkIf cfg.enableScriptingAddition (lib.mkForce sudoersFile);
+
+ services.skhd = lib.mkIf cfg.manageKeybindings {
+ enable = true;
+ skhdConfig = lib.concatStringsSep "\n" (
+ [
+ ''
+ shift + alt - q : ${yabaiBin} -m window --close
+
+ ${directionBinds}
+
+ shift + alt - space : ${yabaiBin} -m window --toggle float
+ shift + alt - return : ${yabaiBin} -m window --toggle sticky
+ alt - f : ${yabaiBin} -m window --toggle zoom-fullscreen
+ shift + alt - f : ${yabaiBin} -m window --toggle native-fullscreen
+
+ ${spaceMoveBinds}
+ ''
+ ]
+ ++ lib.optional cfg.useYabaiSpaceFocus spaceFocusBinds
+ );
+ };
+
+ # Prerequisites, not preferences: yabai requires per-display spaces, and
+ # macOS's own tiling fights it for window placement.
+ system.defaults = {
+ spaces."spans-displays" = false;
+ WindowManager.EnableTilingOptionAccelerator = false;
+
+ CustomUserPreferences."com.apple.symbolichotkeys".AppleSymbolicHotKeys =
+ cfg.extraSymbolicHotkeys
+ // (lib.optionalAttrs cfg.spaceHotkeys.enable spaceHotkeys);
+ };
+ };
+}
diff --git a/users/ryan/common.nix b/users/ryan/common.nix
index 1beaa7c..e2e5890 100644
--- a/users/ryan/common.nix
+++ b/users/ryan/common.nix
@@ -1,13 +1,20 @@
{ config, pkgs, lib, ... }:
{
- imports = [ ./modules/nvim ];
+ imports = [ ./modules/nvim ./modules/aria2 ./modules/zen ];
news.display = "silent";
+ # Keep everything in lockstep
+ home.stateVersion = "26.05";
+
# My own modules
ryan.neovim.enable = true;
+ ryan.aria2.enable = true;
+
+ ryan.zen.enable = true;
+
programs.starship = {
enable = true;
settings = {
@@ -24,14 +31,6 @@
};
};
- # Package/platform overrides (darwinDefaultsId, package = null for homebrew,
- # etc.) live in darwin.nix/linux.nix. Policies/profile are shared.
- programs.zen-browser = {
- enable = true;
- policies = import ./zen/zenPolicies.nix;
- profiles.default = import ./zen/zenProfile.nix;
- };
-
programs.eza = {
enable = true;
enableZshIntegration = true;
@@ -117,6 +116,12 @@
services.gpg-agent = {
enable = pkgs.stdenv.isLinux;
enableSshSupport = true;
+ enableExtraSocket = true;
+ };
+
+ programs.fzf = {
+ enable = true;
+ enableZshIntegration = true;
};
programs.zsh = {
@@ -125,13 +130,16 @@
brewPath = if pkgs.stdenv.isDarwin && pkgs.stdenv.isAarch64 then ''
eval "$(/opt/homebrew/bin/brew shellenv)"
'' else "";
- in ''
- export GPG_TTY="$(tty)"
- export SSH_AUTH_SOCK="$(gpgconf --list-dirs agent-ssh-socket)"
- gpgconf --launch gpg-agent
- gpg-connect-agent updatestartuptty /bye > /dev/null
- ${brewPath}
- '';
+ in lib.mkMerge [
+ (lib.mkOrder 550 "ZVM_INIT_MODE=sourcing")
+ ''
+ export GPG_TTY="$(tty)"
+ export SSH_AUTH_SOCK="$(gpgconf --list-dirs agent-ssh-socket)"
+ gpgconf --launch gpg-agent
+ gpg-connect-agent updatestartuptty /bye > /dev/null
+ ${brewPath}
+ ''
+ ];
shellAliases = {
cat = "bat --paging=never";
diff = "delta";
@@ -143,12 +151,36 @@
then "sudo darwin-rebuild switch --flake ~/.config/nix/."
else "home-manager switch --flake ~/.config/home-manager/#ryan";
};
+ plugins = [
+ {
+ name = "zsh-vi-mode";
+ src = "${pkgs.zsh-vi-mode}/share/zsh-vi-mode";
+ }
+ ];
};
# Shared across darwin and linux; the guix side already deferred these
# packages to nix (all commented out of home-configuration.scm's package
# list), same as the darwin config always has.
home.packages = with pkgs; [
+ bat
+ fd
+ fzf
+ tmux
+ restic
+ rsync
+ pv
+ jq
+ htop
+ (pkgs.zathura.override {
+ plugins = [ pkgs.zathuraPkgs.zathura_pdf_mupdf ];
+ })
+ go
+ pass
+ file
+ pandoc
+ weechat
+ python3
yt-dlp
aerc
gdu
@@ -156,6 +188,13 @@
delta
prismlauncher
git-lfs
+ audacity
+ blender
+
+ nerd-fonts.anonymice
+ nerd-fonts.monofur
+ nerd-fonts.lilex
+ nerd-fonts.fira-code
];
xdg.configFile."aerc" = {
@@ -163,8 +202,9 @@
recursive = true;
};
+ xdg.enable = true;
+
home.sessionVariables = {
- XDG_CONFIG_HOME = "${config.home.homeDirectory}/.config";
EDITOR = "${pkgs.neovim}/bin/nvim";
};
diff --git a/users/ryan/darwin.nix b/users/ryan/darwin.nix
index 0557509..0744b2f 100644
--- a/users/ryan/darwin.nix
+++ b/users/ryan/darwin.nix
@@ -8,18 +8,12 @@
./modules/ubersicht
];
- home.stateVersion = "26.05"; # Please read the comment before changing.
-
ryan.sketchybar.enable = true;
- programs.zen-browser = {
- package = null; # managed via homebrew
- darwinDefaultsId = "app.zen-browser.zen";
- };
-
# Need special config for gpg-agent on macOS
home.file.".gnupg/gpg-agent.conf".text = ''
pinentry-program ${pkgs.pinentry_mac}/Applications/pinentry-mac.app/Contents/MacOS/pinentry-mac
+ extra-socket ${config.home.homeDirectory}/.gnupg/S.gpg-agent.extra
enable-ssh-support
'';
@@ -29,14 +23,7 @@
home.file.".config/homebrew".source = config.lib.file.mkOutOfStoreSymlink "${config.home.homeDirectory}/.homebrew";
home.packages = with pkgs; [
- bat
- pass
- pandoc
iina # Media player (frontend to mpv essentially)
- audacity
- ffmpeg
- blender
- nerd-fonts.anonymice
jujutsu
netbird-tui
];
diff --git a/users/ryan/linux.nix b/users/ryan/linux.nix
index a86cad1..241ed3f 100644
--- a/users/ryan/linux.nix
+++ b/users/ryan/linux.nix
@@ -1,56 +1,43 @@
-{ config, pkgs, lib, inputs, ... }:
+{ pkgs, lib, inputs, ... }:
{
imports = [
- ./linux/firefox-nightly
];
home.username = "ryan";
home.homeDirectory = "/home/ryan";
- home.stateVersion = "23.11"; # Please read the comment before changing.
nixpkgs.config.allowUnfree = true;
- programs.zsh.shellAliases = {
- ls = "ls -p --color=auto";
- spt = "spotify_player";
- python = "python3";
+ fonts.fontconfig.enable = true;
+ fonts.fontconfig.defaultFonts = {
+ monospace = [ "DejaVu Sans Mono" ];
+ sansSerif = [ "Noto Sans" "DejaVu Sans" ];
+ serif = [ "Noto Serif" "DejaVu Serif" ];
};
- # `nix shell nixpkgs#...`/`nix run nixpkgs#...` etc. resolve `nixpkgs` to
- # this flake's own pinned input instead of fetching latest nixpkgs-unstable.
- # This is the standalone-HM equivalent of RyanMac's /etc/nix/flake-registry.json.
- # HM owns the whole ~/.config/nix directory (including nix.conf) so this
- # doesn't end up writing through guix-dotfiles' `nix-config` symlink - see
- # MIGRATION.md.
- xdg.configFile."nix/nix.conf".text = "experimental-features = nix-command flakes\n";
- xdg.configFile."nix/registry.json".text = builtins.toJSON {
- version = 2;
- flakes = [
- {
- from = { type = "indirect"; id = "nixpkgs"; };
- to = {
- type = "path";
- path = inputs.nixpkgs.outPath;
- lastModified = inputs.nixpkgs.lastModified;
- narHash = inputs.nixpkgs.narHash;
- };
- }
- ];
+ gtk = {
+ enable = true;
+ font = {
+ name = "Noto Sans";
+ size = 10;
+ };
+ theme = {
+ name = "Adwaita";
+ package = pkgs.gnome-themes-extra;
+ };
+ iconTheme = {
+ name = "Adwaita";
+ package = pkgs.adwaita-icon-theme;
+ };
+ colorScheme = "dark";
};
- # Non-NixOS (Guix), so HM has to write its own fontconfig instead of
- # relying on a system module.
- fonts.fontconfig.enable = true;
- fonts.fontconfig.defaultFonts = {
- monospace = [ "DejaVu Sans Mono" ];
- sansSerif = [ "DejaVu Sans" ];
- serif = [ "DejaVu Serif" ];
+ qt = {
+ enable = true;
+ platformTheme.name = "gtk3";
};
- # Cursor theme. Replaces the previously hand-vendored Bibata asset (which
- # embedded oddly-versioned theme names) with nixpkgs' own build - see the
- # comment in linux/hypr/hyprland.conf for the corresponding env var change.
home.pointerCursor = {
enable = true;
package = pkgs.bibata-cursors;
@@ -60,13 +47,12 @@
x11.enable = true;
};
- # gpg-agent itself is enabled in common.nix; nixpkgs' pinentry here instead
- # of depending on Guix's system profile path.
services.gpg-agent.pinentry.package = pkgs.pinentry-qt;
wayland.windowManager.hyprland = {
enable = true;
package = pkgs.hyprland;
+ configType = "hyprlang";
portalPackage = pkgs.xdg-desktop-portal-hyprland;
extraConfig = builtins.readFile ./linux/hypr/hyprland.conf;
};
@@ -80,6 +66,7 @@
"hypr/scripts" = { source = ./linux/hypr/scripts; recursive = true; };
"hypr/family_guy.mp4".source = ./linux/hypr/family_guy.mp4;
"hypr/subwaysurfer.webm".source = ./linux/hypr/subwaysurfer.webm;
+ "wallpapers".source = ../../files/Wallpapers;
"waybar" = { source = ./linux/waybar; recursive = true; };
"alacritty" = { source = ./linux/alacritty; recursive = true; };
@@ -88,26 +75,18 @@
"mpv" = { source = ./linux/mpv; recursive = true; };
"pulse/client.conf".source = ./linux/pulseaudio/client.conf;
- "spotify-player/app.toml".source = ./linux/spotify-player/app.toml;
};
home.file = {
- ".local/share/mailvelope" = { source = ./linux/mailvelope; recursive = true; };
- ".local/share/tridactyl" = { source = ./linux/tridactyl; recursive = true; };
- ".mozilla/native-messaging-hosts/tridactyl.json".source = ./linux/tridactyl/tridactyl.json;
+ # Files that need to be in ~ go here
};
home.packages = with pkgs; [
- rustup
- zoxide
- pnpm
gcc
pkg-config
wttrbar
wl-clip-persist
- gifski
waypaper
- spotify-player
xwayland
xdg-desktop-portal
xdg-desktop-portal-gtk
@@ -121,42 +100,25 @@
wl-mirror
pyprland
satty
- fluxcd
sops
restic
- kdePackages.kdenlive
bluetui
supercell-wx
chromium
+ foot
+ kdePackages.kdenlive
+ obs-studio
+
+ waybar
+ fuzzel
- # foot is Guix system-packaged and is the terminal actually wired into
- # the WM; alacritty's config was carried over from guix-dotfiles but
- # nothing provided its binary there either - see MIGRATION.md.
- alacritty
-
- # Nerd Fonts, replacing the hand-vendored ttf files that used to live in
- # guix-dotfiles/home-config/fonts (Fira Code and "Monofur For Powerline"
- # dropped - superseded by these, unreferenced anywhere)
- nerd-fonts.anonymice
- nerd-fonts.monofur
- nerd-fonts.lilex
- nerd-fonts.fira-code
noto-fonts
dejavu_fonts
liberation_ttf
noto-fonts-cjk-sans
noto-fonts-color-emoji
- # hyprlock built by nix needs Guix's PAM/fontconfig libs preloaded to run
- # under the Guix system profile - same interop hack as before.
- (pkgs.writeScriptBin "hyprlock" ''
- #! ${pkgs.bash}/bin/bash
- export LD_PRELOAD="/run/current-system/profile/lib/libpam.so.0:/run/current-system/profile/lib/libfontconfig.so:$LD_PRELOAD"
- exec ${pkgs.hyprlock}/bin/hyprlock "$@"
- '')
-
- # Guix's mesa doesn't match nixpkgs', so GL apps built by nix need nixGL.
- inputs.nixgl.packages.${pkgs.system}.nixGLIntel
- inputs.clipboard-sync.packages.${pkgs.system}.default
+ inputs.clipboard-sync.packages.${pkgs.stdenv.hostPlatform.system}.default
+
];
}
diff --git a/users/ryan/linux/firefox-nightly/.to-be-deleted b/users/ryan/linux/firefox-nightly/.to-be-deleted
new file mode 100644
index 0000000..e69de29
diff --git a/users/ryan/linux/foreign.nix b/users/ryan/linux/foreign.nix
new file mode 100644
index 0000000..b1b06ab
--- /dev/null
+++ b/users/ryan/linux/foreign.nix
@@ -0,0 +1,25 @@
+{ inputs, ... }:
+
+# Applies to any standalone (non-NixOS) home-manager install, where
+# home-manager owns the user profile on its own instead of being wired in
+# via a NixOS module. NixOS manages nix.conf/the flake registry and doesn't
+# need the genericLinux target at all, so this is skipped on NixOS hosts.
+{
+ targets.genericLinux.enable = true;
+
+ xdg.configFile."nix/nix.conf".text = "experimental-features = nix-command flakes\n";
+ xdg.configFile."nix/registry.json".text = builtins.toJSON {
+ version = 2;
+ flakes = [
+ {
+ from = { type = "indirect"; id = "nixpkgs"; };
+ to = {
+ type = "path";
+ path = inputs.nixpkgs.outPath;
+ lastModified = inputs.nixpkgs.lastModified;
+ narHash = inputs.nixpkgs.narHash;
+ };
+ }
+ ];
+ };
+}
diff --git a/users/ryan/linux/guix.nix b/users/ryan/linux/guix.nix
new file mode 100644
index 0000000..db305af
--- /dev/null
+++ b/users/ryan/linux/guix.nix
@@ -0,0 +1,33 @@
+{ config, pkgs, lib, inputs, ... }:
+
+# Host glue for the Guix System laptop. Everything generic about running
+# standalone home-manager on Guix lives in ../modules/targets/guix; this file
+# only holds what's specific to this machine.
+{
+ imports = [ ../modules/targets/guix ];
+
+ targets.guix.enable = true;
+
+ # Guix's mesa doesn't match nixpkgs', so GL apps built by nix go through
+ # nixGL. Setting packages also makes config.lib.nixGL.wrap real (it's a
+ # no-op otherwise) and supersedes HM's systemd-tmpfiles-based GPU setup.
+ targets.genericLinux.nixGL = {
+ packages = inputs.nixgl.packages;
+ defaultWrapper = "mesa"; # nixGLIntel under the hood
+ };
+ home.packages = [
+ # Kept under its old name for anything that calls it directly.
+ inputs.nixgl.packages.${pkgs.stdenv.hostPlatform.system}.nixGLIntel
+ ];
+
+ targets.guix.pamWrapped = [ pkgs.hyprlock ];
+
+ # gpg-agent is socket-activated through Shepherd now (translated from HM's
+ # gpg-agent.socket units), so no launch here; Hyprland's own environment
+ # still needs the socket path for apps it spawns.
+ wayland.windowManager.hyprland.extraConfig = ''
+ exec-once = sh -c 'hyprctl setenv SSH_AUTH_SOCK "$(gpgconf --list-dirs agent-ssh-socket)"'
+ '';
+
+ programs.gpg.scdaemonSettings.disable-ccid = true;
+}
diff --git a/users/ryan/linux/hypr/hyprland.conf b/users/ryan/linux/hypr/hyprland.conf
index e51b54c..42d8a52 100644
--- a/users/ryan/linux/hypr/hyprland.conf
+++ b/users/ryan/linux/hypr/hyprland.conf
@@ -113,7 +113,7 @@ animations {
dwindle {
# See https://wiki.hyprland.org/Configuring/Dwindle-Layout/ for more
- pseudotile = yes # master switch for pseudotiling. Enabling is bound to mainMod + P in the keybinds section below
+ #pseudotile = yes # master switch for pseudotiling. Enabling is bound to mainMod + P in the keybinds section below
preserve_split = yes # you probably want this
}
@@ -130,9 +130,9 @@ device {
}
# Example windowrule v1
-# windowrule = float, ^(kitty)$
+# windowrule = float on, ^(kitty)$
# Example windowrule v2
-# windowrulev2 = float,class:^(kitty)$,title:^(kitty)$
+# windowrulev2 = float on,match:class ^(kitty)$,match:title ^(kitty)$
# See https://wiki.hyprland.org/Configuring/Window-Rules/ for more
@@ -147,8 +147,8 @@ bind = $mainMod SHIFT, E, exit
bind = $mainMod SHIFT, Space, togglefloating
bind = $mainMod SHIFT, Return, pin, active
bind = $mainMod, D, exec, fuzzel --width=35 --font='Hack:weight=bold:size=12' --background-color='282a36fa' --selection-color='3d4460fa' --line-height=20 --border-radius=20
-bind = $mainMod, P, pseudo, # dwindle
-bind = $mainMod, J, togglesplit, # dwindle
+#bind = $mainMod, P, pseudo, # dwindle
+#bind = $mainMod, J, togglesplit, # dwindle
bind = $mainMod, F, fullscreen, 1
bind = $mainMod SHIFT, F, fullscreen
bind = ,XF86MonBrightnessUp, exec, light -A 10
@@ -252,46 +252,46 @@ bind = $mainMod SHIFT CTRL ALT, L, exec, xdg-open https://linkedin.com
# Window Rules
# Firefox PiP
-windowrulev2 = float, class:^(zen-beta)$, title:^(Picture-in-Picture)$
-windowrulev2 = pin, class:^(zen-beta)$, title:^(Picture-in-Picture)$
-windowrulev2 = size 22% 22%, class:^(zen-beta)$, title:^(Picture-in-Picture)$
-windowrulev2 = move 76% 8%, class:^(zen-beta)$, title:^(Picture-in-Picture)$
+windowrule = float on, match:class ^(zen-beta)$, match:title ^(Picture-in-Picture)$
+windowrule = pin on, match:class ^(zen-beta)$, match:title ^(Picture-in-Picture)$
+windowrule = size 22% 22%, match:class ^(zen-beta)$, match:title ^(Picture-in-Picture)$
+windowrule = move 76% 8%, match:class ^(zen-beta)$, match:title ^(Picture-in-Picture)$
# Firefox Sharing Indicator
-windowrulev2 = float, class:^(firefox)$, title:^(Firefox — Sharing Indicator)$
-windowrulev2 = size 56 31, class:^(firefox)$, title:^(Firefox — Sharing Indicator)$
-windowrulev2 = move 50% 0, class:^(firefox)$, title:^(Firefox — Sharing Indicator)$
-windowrulev2 = suppressevent fullscreen maximize, class:^(firefox)$, title:^(Firefox — Sharing Indicator)$
+windowrule = float on, match:class ^(firefox)$, match:title ^(Firefox — Sharing Indicator)$
+windowrule = size 56 31, match:class ^(firefox)$, match:title ^(Firefox — Sharing Indicator)$
+windowrule = move 50% 0, match:class ^(firefox)$, match:title ^(Firefox — Sharing Indicator)$
+windowrule = suppress_event = fullscreen maximize, match:class ^(firefox)$, match:title ^(Firefox — Sharing Indicator)$
# Weather
-windowrulev2 = float,class:^(ala-wttr)$
-windowrulev2 = size 822 1026, class:^(ala-wttr)$
+windowrule = float on,match:class ^(ala-wttr)$
+windowrule = size 822 1026, match:class ^(ala-wttr)$
# Floating terminal
-windowrulev2 = float,class:^(float-foot)$
-windowrulev2 = size 930 680,class:^(float-foot)$
-windowrulev2 = center,class:^(float-foot)$
+windowrule = float on,match:class ^(float-foot)$
+windowrule = size 930 680,match:class ^(float-foot)$
+windowrule = center on,match:class ^(float-foot)$
# Change color of pinned windows
-windowrulev2 = bordercolor rgba(bf0000ee) rgba(ab00adee) 45deg,pinned:1
+windowrule = border_color rgba(bf0000ee) rgba(ab00adee) 45deg,match:pin 1
# Fun Keybinds
# Subway surfer
bind = $mainMod CTRL SHIFT, S, exec, mpv ~/.config/hypr/subwaysurfer.webm
-windowrulev2 = float,class:^(mpv)$, title:^(subwaysurfer.webm - mpv)$
-windowrulev2 = size 310 554,class:^(mpv)$, title:^(subwaysurfer.webm - mpv)$
-windowrulev2 = move 81% 46% ,class:^(mpv)$, title:^(subwaysurfer.webm - mpv)$
-windowrulev2 = pin,class:^(mpv)$, title:^(subwaysurfer.webm - mpv)$
+windowrule = float on,match:class ^(mpv)$, match:title ^(subwaysurfer.webm - mpv)$
+windowrule = size 310 554,match:class ^(mpv)$, match:title ^(subwaysurfer.webm - mpv)$
+windowrule = move 81% 46% ,match:class ^(mpv)$, match:title ^(subwaysurfer.webm - mpv)$
+windowrule = pin on,match:class ^(mpv)$, match:title ^(subwaysurfer.webm - mpv)$
# Family guy funny moments
bind = $mainMod CTRL SHIFT, F, exec, mpv ~/.config/hypr/family_guy.mp4
-windowrulev2 = float,class:^(mpv)$, title:^(family_guy.mp4 - mpv)$
-windowrulev2 = size 595 336,class:^(mpv)$, title:^(family_guy.mp4 - mpv)$
-windowrulev2 = move 67% 5% ,class:^(mpv)$, title:^(family_guy.mp4 - mpv)$
-windowrulev2 = pin,class:^(mpv)$, title:^(family_guy.mp4 - mpv)$
+windowrule = float on,match:class ^(mpv)$, match:title ^(family_guy.mp4 - mpv)$
+windowrule = size 595 336,match:class ^(mpv)$, match:title ^(family_guy.mp4 - mpv)$
+windowrule = move 67% 5% ,match:class ^(mpv)$, match:title ^(family_guy.mp4 - mpv)$
+windowrule = pin on,match:class ^(mpv)$, match:title ^(family_guy.mp4 - mpv)$
# Satty windowrules
-windowrulev2 = fullscreen,class:^(com.gabm.satty)$
+windowrule = fullscreen on,match:class ^(com.gabm.satty)$
exec-once = ~/.config/hypr/scripts/autostart.sh
diff --git a/users/ryan/linux/hypr/scripts/autostart.sh b/users/ryan/linux/hypr/scripts/autostart.sh
index 706f82f..080f6e9 100755
--- a/users/ryan/linux/hypr/scripts/autostart.sh
+++ b/users/ryan/linux/hypr/scripts/autostart.sh
@@ -6,8 +6,6 @@ mako &
#swaybg -i ~/.config/hypr/Wallpapers/above-clouds.jpg &
-nice -n13 swww init &
-
#~/.config/hypr/wallpaper-daemon.sh &
wpaperd -d &
diff --git a/users/ryan/linux/mailvelope/gpgmejson.json b/users/ryan/linux/mailvelope/gpgmejson.json
deleted file mode 100644
index 7ad8586..0000000
--- a/users/ryan/linux/mailvelope/gpgmejson.json
+++ /dev/null
@@ -1,7 +0,0 @@
-{
- "name": "gpgmejson",
- "description": "JavaScript binding for GnuPG",
- "path": "/home/ryan/.local/share/mailvelope/start.sh",
- "type": "stdio",
- "allowed_extensions": ["jid1-AQqSMBYb0a8ADg@jetpack"]
-}
diff --git a/users/ryan/linux/mailvelope/start.sh b/users/ryan/linux/mailvelope/start.sh
deleted file mode 100755
index 01b3ee2..0000000
--- a/users/ryan/linux/mailvelope/start.sh
+++ /dev/null
@@ -1,4 +0,0 @@
-#!/usr/bin/env bash
-
-# Wrap for gpgme-json so firefox can use it
-exec gpgme-json $@
diff --git a/users/ryan/linux/spotify-player/app.toml b/users/ryan/linux/spotify-player/app.toml
deleted file mode 100644
index 4eac5e4..0000000
--- a/users/ryan/linux/spotify-player/app.toml
+++ /dev/null
@@ -1,34 +0,0 @@
-theme = "dracula"
-client_id = "4b1219bdc29945bc8ca914f80a8a6a47"
-client_port = 8888
-playback_format = """
-{track} • {artists}
-{album}
-{metadata}"""
-tracks_playback_limit = 50
-app_refresh_duration_in_ms = 32
-playback_refresh_duration_in_ms = 0
-cover_image_refresh_duration_in_ms = 2000
-page_size_in_rows = 20
-play_icon = "▶"
-pause_icon = "▌▌"
-liked_icon = "♥"
-border_type = "Plain"
-progress_bar_type = "Rectangle"
-playback_window_position = "Top"
-cover_img_length = 9
-cover_img_width = 5
-cover_img_scale = 1.0
-playback_window_width = 6
-enable_media_control = true
-enable_streaming = "DaemonOnly"
-enable_cover_image_cache = true
-default_device = "RyanThinkpad"
-
-[copy_command]
-command = "wlcopy"
-args = []
-
-[notify_format]
-summary = "{track} • {artists}"
-body = "{album}"
diff --git a/users/ryan/linux/waybar/config b/users/ryan/linux/waybar/config
index 62e6b20..4459ce7 100644
--- a/users/ryan/linux/waybar/config
+++ b/users/ryan/linux/waybar/config
@@ -5,50 +5,69 @@
"modules-left": ["hyprland/workspaces", "hyprland/submap"],
"modules-center": ["hyprland/window"],
- "modules-right": ["tray", "custom/spotify", "privacy", "custom/weather", "custom/mail", "custom/storage", "backlight", "bluetooth", "pulseaudio", "network", "idle_inhibitor", "battery", "battery#num2", "clock"],
+ "modules-right": ["tray", "privacy", "custom/weather", "custom/mail", "custom/storage", "backlight", "bluetooth", "pulseaudio", "network", "idle_inhibitor", "battery", "battery#num2", "clock"],
+
"hyprland/submap": {
"format": " {}"
},
+
"hyprland/workspaces": {
"format": "{id}",
"all-outputs": false,
"disable-scroll": true
},
+
"hyprland/window": {
"max-length": 80,
"tooltip": false,
"separate-outputs": true
},
+
"clock": {
"format": "{:%a %d %b %H:%M}",
"tooltip": false
},
+
"battery": {
"format": "{capacity}% {icon}",
"format-alt": "{time} {icon}",
- "format-icons": ["", "", "", "", ""],
- "format-charging": "{capacity}% ",
+ "format-icons": [
+ "󰁺",
+ "󰁻",
+ "󰁼",
+ "󰁽",
+ "󰁹"
+ ],
+ "format-charging": "{capacity}% 󰂄",
"interval": 30,
"states": {
"warning": 25,
"critical": 10
},
"tooltip": false,
- "bat":"BAT0"
+ "bat": "BAT0"
},
+
"battery#num2": {
"format": "{capacity}% {icon}",
"format-alt": "{time} {icon}",
- "format-icons": ["", "", "", "", ""],
- "format-charging": "{capacity}% ",
+ "format-icons": [
+ "󰁺",
+ "󰁻",
+ "󰁼",
+ "󰁽",
+ "󰁹"
+ ],
+ "format-charging": "{capacity}% 󰂄",
"interval": 30,
"states": {
"warning": 25,
"critical": 10
},
"tooltip": false,
- "bat":"BAT1"
+ "bat": "BAT1"
},
+
"bluetooth": {
"format": "󰂯",
"format-disabled": "󰂲",
@@ -56,60 +75,75 @@
"format-connected-battery": "󰂱 {device_battery_percentage}%",
"on-click": "foot bluetui",
"tooltip-format": "{controller_alias}\t{controller_address}\n\n{num_connections} connected",
- "tooltip-format-connected": "{controller_alias}\t{controller_address}\n\n{num_connections} connected\n\n{device_enumerate}",
- "tooltip-format-enumerate-connected": "{device_alias}\t{device_address}",
- "tooltip-format-enumerate-connected-battery": "{device_alias}\t{device_address}\t{device_battery_percentage}%"
+ "tooltip-format-connected": "{controller_alias}\t{controller_address}\n\n{num_connections} connected\n\n{device_enumerate}",
+ "tooltip-format-enumerate-connected": "{device_alias}\t{device_address}",
+ "tooltip-format-enumerate-connected-battery": "{device_alias}\t{device_address}\t{device_battery_percentage}%"
},
+
"network": {
"format-alt": "{ipaddr}/{cidr} {icon} {bandwidthTotalBytes}󰹹",
"format": "{ipaddr}/{cidr} {icon}",
"format-alt-click": "click-right",
"format-icons": {
- "wifi": ["󰤟", "󰤢" ,"󰤥", "󰤨"],
- "ethernet": [""],
- "disconnected": ["󰤭"]
+ "wifi": [
+ "󰤟",
+ "󰤢",
+ "󰤥",
+ "󰤨"
+ ],
+ "ethernet": [
+ "󰈀"
+ ],
+ "disconnected": [
+ "󰤭"
+ ]
},
"on-click": "foot -t alacritty doas nmtui",
"tooltip": false
},
+
"privacy": {
- "icon-spacing":4,
- "icon-size":18,
- "transition-duration":250,
+ "icon-spacing": 4,
+ "icon-size": 18,
+ "transition-duration": 250,
"modules": [
{
- "type":"screenshare",
- "tooltip":true,
- "tooltip-icon-size":24
+ "type": "screenshare",
+ "tooltip": true,
+ "tooltip-icon-size": 24
},
{
- "type":"audio-in",
- "tooltip":true,
- "tooltip-icon-size":24
+ "type": "audio-in",
+ "tooltip": true,
+ "tooltip-icon-size": 24
}
]
},
+
"pulseaudio": {
"format-alt": "{icon}",
"format": "{volume} {icon}",
"format-alt-click": "click-right",
"format-muted": "",
"format-icons": {
- "phone": [" ", " ", " ", " "],
- "default": ["", "", "", ""]
+ "phone": [
+ " ",
+ " ",
+ " ",
+ " "
+ ],
+ "default": [
+ "",
+ "",
+ "",
+ ""
+ ]
},
"scroll-step": 1,
"on-click": "pavucontrol",
"tooltip": false
},
- "custom/spotify": {
- "interval": 2,
- "return-type": "json",
- "exec": "~/.config/waybar/modules/spotify.sh",
- "exec-if": "~/.config/waybar/modules/check-for-music.sh",
- "escape": true,
- "on-click": "playerctl play-pause"
- },
+
"custom/storage": {
"format-alt": "{} ",
"format": "{percentage}% ",
@@ -118,14 +152,19 @@
"interval": 60,
"exec": "~/.config/waybar/modules/storage.sh"
},
+
"backlight": {
"format-alt": "{icon}",
"format": "{percent}% {icon}",
"format-alt-click": "click-right",
- "format-icons": ["", ""],
+ "format-icons": [
+ "",
+ "󰃞"
+ ],
"on-scroll-up": "light -A 1",
"on-scroll-down": "light -U 1"
},
+
"custom/weather_OLD": {
"format-alt": "{}",
"format": "{alt}: {}",
@@ -136,14 +175,16 @@
"exec": "~/.config/waybar/modules/weather.sh",
"exec-if": "ping wttr.in -c1"
},
+
"custom/weather": {
"format": "{} °F",
"tooltip": true,
"interval": 3600,
- "exec": "exit=101; while [ $exit = 101 ]; do wttrbar --main-indicator temp_F --fahrenheit; exit=$(echo $?); sleep 1; done",
+ "exec": "exit=101; while [ $exit = 101 ]; do wttrbar --main-indicator temp_F --fahrenheit --nerd; exit=$(echo $?); sleep 1; done",
"on-click": "foot -a ala-wttr -H curl https://v2.wttr.in",
"return-type": "json"
},
+
"idle_inhibitor": {
"format": "{icon}",
"format-icons": {
@@ -152,6 +193,7 @@
},
"tooltip": false
},
+
"custom/mail": {
"format": "",
"format-alt": "{alt} ",
@@ -161,7 +203,9 @@
"exec": "~/.config/waybar/modules/mail.py",
"tooltip": false
},
+
"tray": {
"icon-size": 18
}
}
+
diff --git a/users/ryan/linux/waybar/modules/storage.sh b/users/ryan/linux/waybar/modules/storage.sh
index 90cc3dc..7cb7249 100755
--- a/users/ryan/linux/waybar/modules/storage.sh
+++ b/users/ryan/linux/waybar/modules/storage.sh
@@ -1,25 +1,70 @@
#!/bin/sh
-mount="/"
+set -eu
+
+mountpoint="/"
warning=20
critical=10
-df -h -P -l "$mount" | awk -v warning=$warning -v critical=$critical '
-/\/.*/ {
- text=$4
- tooltip="Filesystem: "$1"\rSize: "$2"\rUsed: "$3"\rAvail: "$4"\rUse%: "$5"\rMounted on: "$6
- use=$5
- exit 0
+error() {
+ jq -cn --arg message "$1" '{
+ text: "N/A",
+ percentage: 0,
+ tooltip: $message,
+ class: "critical"
+ }'
+ exit 0
}
-END {
- class=""
- gsub(/%$/,"",use)
- if ((100 - use) < critical) {
+
+line=$(df -hP -l "$mountpoint" 2>/dev/null | tail -n 1) ||
+ error "Unable to read filesystem statistics"
+
+# df -P guarantees a single line per filesystem.
+set -- $line
+
+[ "$#" -ge 6 ] || error "Unable to parse filesystem statistics"
+
+filesystem=$1
+size=$2
+used=$3
+available=$4
+use=${5%\%}
+mounted=$6
+
+case "$use" in
+ ''|*[!0-9]*) error "Invalid filesystem usage percentage: $use%" ;;
+esac
+
+free=$((100 - use))
+
+if [ "$free" -le "$critical" ]; then
class="critical"
- } else if ((100 - use) < warning) {
+elif [ "$free" -le "$warning" ]; then
class="warning"
- }
- print "{\"text\":\""text"\", \"percentage\":"use",\"tooltip\":\""tooltip"\", \"class\":\""class"\"}"
-}
-'
+else
+ class=""
+fi
+
+jq -cn \
+ --arg text "${use}%" \
+ --argjson percentage "$use" \
+ --arg filesystem "$filesystem" \
+ --arg size "$size" \
+ --arg used "$used" \
+ --arg available "$available" \
+ --arg mounted "$mounted" \
+ --arg class "$class" \
+ '{
+ text: $text,
+ percentage: $percentage,
+ tooltip: (
+ "Filesystem: " + $filesystem +
+ "\rSize: " + $size +
+ "\rUsed: " + $used +
+ "\rAvail: " + $available +
+ "\rUse%: " + $text +
+ "\rMounted on: " + $mounted
+ ),
+ class: $class
+ }'
diff --git a/users/ryan/linux/waybar/style.css b/users/ryan/linux/waybar/style.css
index 682a14e..ac78984 100644
--- a/users/ryan/linux/waybar/style.css
+++ b/users/ryan/linux/waybar/style.css
@@ -1,7 +1,7 @@
* {
border: none;
border-radius: 0;
- font-family: AnonymicePro Nerd Font;
+ font-family: FiraCode Nerd Font;
font-size: 15px;
box-shadow: none;
text-shadow: none;
diff --git a/users/ryan/linux/wpaperd/config.toml b/users/ryan/linux/wpaperd/config.toml
index 9947370..9735c20 100644
--- a/users/ryan/linux/wpaperd/config.toml
+++ b/users/ryan/linux/wpaperd/config.toml
@@ -1,4 +1,4 @@
[default]
duration = "30m"
-path = "~/.config/hypr/Wallpapers/static/"
+path = "/home/ryan/.config/wallpapers/"
transition-time = 2000
diff --git a/users/ryan/modules/aria2/default.nix b/users/ryan/modules/aria2/default.nix
new file mode 100644
index 0000000..8082fc1
--- /dev/null
+++ b/users/ryan/modules/aria2/default.nix
@@ -0,0 +1,185 @@
+# users/ryan/modules/aria2/default.nix
+#
+# aria2c RPC daemon + Zen hand-off via baptistecdr's "Aria2 Integration"
+# (AMO slug aria2-extension). The RPC secret is generated once at activation
+# into $XDG_STATE_HOME/aria2/rpc-secret (0600) and never touches the repo or
+# the store; the same activation splices it into both aria2's runtime conf and
+# the extension's storage.sync row.
+{ config, lib, pkgs, ... }:
+let
+ cfg = config.ryan.aria2;
+ zen = config.programs.zen-browser;
+ inherit (pkgs.stdenv.hostPlatform) isDarwin isLinux;
+
+ stateDir = "${config.xdg.stateHome}/aria2";
+ secretFile = "${stateDir}/rpc-secret";
+ runtimeConf = "${stateDir}/aria2.conf";
+ sessionFile = "${stateDir}/session";
+
+ extId = "baptistecdr@users.noreply.github.com";
+ # Fixed so captureServer can point at it and re-seeding is idempotent.
+ serverUuid = "6f1c2b7e-0a43-4d8e-9c55-3b2a1e0d9f10";
+
+ # Secret-free half of the config; rpc-secret is appended at activation.
+ baseConf = (pkgs.formats.keyValue { }).generate "aria2-base.conf" ({
+ enable-rpc = true;
+ rpc-listen-all = false;
+ rpc-listen-port = cfg.rpcPort;
+ dir = cfg.downloadDir;
+ input-file = sessionFile;
+ save-session = sessionFile;
+ save-session-interval = 60;
+ continue = true;
+ max-connection-per-server = 8;
+ split = 8;
+ min-split-size = "1M";
+ file-allocation = if isDarwin then "none" else "falloc";
+ log = "${stateDir}/aria2.log";
+ log-level = "warn";
+ } // lib.optionalAttrs isLinux {
+ #ca-certificate = "${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt";
+ });
+
+ # Extension state as stored by src/models/extension-options.ts: one
+ # storage.sync key, "options", holding a *stringified* JSON blob.
+ extOptions = pkgs.writeText "aria2-ext-options.json" (builtins.toJSON {
+ servers.${serverUuid} = {
+ uuid = serverUuid;
+ name = "Local aria2";
+ secure = false;
+ host = "127.0.0.1";
+ port = cfg.rpcPort;
+ path = "/jsonrpc";
+ secret = ""; # filled at activation
+ rpcParameters = { };
+ };
+ captureServer = serverUuid;
+ captureDownloads = true;
+ minFileSizeInBytes = 0;
+ excludedProtocols = [ "blob" "data" ];
+ excludedSites = [ ];
+ excludedFileTypes = [ ];
+ useCompleteFilePath = false;
+ notifyUrlIsAdded = true;
+ notifyFileIsAdded = false;
+ notifyErrorOccurs = true;
+ });
+
+ daemon = pkgs.writeShellApplication {
+ name = "aria2-daemon";
+ runtimeInputs = [ pkgs.aria2 ];
+ text = ''exec aria2c --conf-path="${runtimeConf}" "$@"'';
+ };
+in
+{
+ options.ryan.aria2 = {
+ enable = lib.mkEnableOption "aria2 RPC daemon wired into Zen";
+ rpcPort = lib.mkOption { type = lib.types.port; default = 6800; };
+ downloadDir = lib.mkOption {
+ type = lib.types.str;
+ default = "${config.home.homeDirectory}/Downloads";
+ };
+ daemon = lib.mkOption {
+ type = lib.types.package;
+ readOnly = true;
+ default = daemon;
+ description = "Wrapper that runs aria2c against the runtime conf.";
+ };
+ };
+
+ config = lib.mkIf cfg.enable (lib.mkMerge [
+ {
+ home.packages = [ pkgs.aria2 daemon ];
+
+ # Secret + runtime conf. Must precede the launchd/systemd/Shepherd reload and the
+ # Zen fragment, all of which consume what this writes.
+ home.activation.aria2Runtime =
+ lib.hm.dag.entryBetween
+ [ "setupLaunchAgents" "reloadSystemd" "shepherdServices" "zen-browser-default" ]
+ [ "writeBoundary" ] ''
+ if [[ ! -v DRY_RUN ]]; then
+ install -d -m 0700 "${stateDir}"
+ if [ ! -s "${secretFile}" ]; then
+ (umask 077; ${pkgs.openssl}/bin/openssl rand -hex 32 > "${secretFile}")
+ fi
+ [ -e "${sessionFile}" ] || touch "${sessionFile}"
+ tmp="$(mktemp "${stateDir}/.aria2.conf.XXXXXX")"
+ { cat ${baseConf}; printf 'rpc-secret=%s\n' "$(cat "${secretFile}")"; } > "$tmp"
+ chmod 0600 "$tmp"
+ mv -f "$tmp" "${runtimeConf}"
+ fi
+ '';
+ }
+
+ (lib.mkIf config.ryan.zen.enable {
+ ryan.zen.extensions.aria2 = {
+ id = extId;
+ version = "4.15.4";
+ };
+
+ # The extension only reads storage.sync (no storage.managed), so
+ # 3rdparty policy can't configure it. With Sync disabled, storage.sync
+ # is the local webext-storage DB; upsert our row there while Zen is
+ # closed. Declarative-owned: UI edits to this extension get reverted.
+ programs.zen-browser.activationFragments.default = [{
+ requiresLock = true;
+ skipSubject = "aria2 extension config";
+ text = ''
+ db="${zen.profilesPath}/${zen.profiles.default.path}/storage-sync-v2.sqlite"
+ if [ ! -f "$db" ]; then
+ echo "aria2: $db not created yet; start Zen once (extension initialises storage.sync), quit, rebuild."
+ elif [[ ! -v DRY_RUN ]]; then
+ payload="$(mktemp)"
+ ${lib.getExe pkgs.jq} -nc \
+ --arg secret "$(cat "${secretFile}")" \
+ --arg uuid "${serverUuid}" \
+ --slurpfile opts ${extOptions} \
+ '{options: ($opts[0] | .servers[$uuid].secret = $secret | tojson)}' > "$payload"
+ ${lib.getBin pkgs.sqlite}/bin/sqlite3 "$db" <<SQL
+ INSERT INTO storage_sync_data (ext_id, data, sync_change_counter)
+ VALUES ('${extId}', CAST(readfile('$payload') AS TEXT), 1)
+ ON CONFLICT(ext_id) DO UPDATE SET
+ data = excluded.data,
+ sync_change_counter = sync_change_counter + 1;
+ SQL
+ rm -f "$payload"
+ fi
+ '';
+ }];
+ })
+
+ (lib.mkIf isDarwin {
+ launchd.agents.aria2 = {
+ enable = true;
+ config = {
+ ProgramArguments = [ (lib.getExe daemon) ];
+ RunAtLoad = true;
+ KeepAlive = true;
+ ProcessType = "Background";
+ # Not read by aria2; changes the plist whenever the base conf
+ # changes, so setupLaunchAgents reloads the agent.
+ EnvironmentVariables.ARIA2_BASE_CONF = "${baseConf}";
+ };
+ };
+ })
+
+ # Linux: a plain systemd user unit. On Guix, targets.guix translates it
+ # to a Shepherd service; on NixOS / systemd distros it runs as-is.
+ (lib.mkIf isLinux {
+ systemd.user.services.aria2 = {
+ Unit = {
+ Description = "aria2 RPC daemon";
+ Documentation = "man:aria2c(1)";
+ # Unit text changes with the base conf -> sd-switch restarts it.
+ X-Restart-Triggers = [ "${baseConf}" ];
+ };
+ Service = {
+ ExecStart = lib.getExe daemon;
+ Restart = "on-failure";
+ };
+ Install.WantedBy = [ "default.target" ];
+ };
+ })
+
+ ]);
+}
diff --git a/users/ryan/modules/targets/guix/default.nix b/users/ryan/modules/targets/guix/default.nix
new file mode 100644
index 0000000..8433bb6
--- /dev/null
+++ b/users/ryan/modules/targets/guix/default.nix
@@ -0,0 +1,254 @@
+# targets.guix: make standalone home-manager behave on Guix System.
+#
+# The point is that other modules stay written against upstream
+# home-manager options (systemd.user.services, services.*, programs.*) and
+# this target adapts them, instead of each module growing a Guix branch.
+{ config, lib, pkgs, osConfig ? null, ... }:
+let
+ cfg = config.targets.guix;
+ sh = import ./shepherd.nix { inherit lib; setpriv = pkgs.util-linux; };
+
+ result = sh.translate {
+ systemdUser = config.systemd.user;
+ inherit (cfg.shepherd) unenforced ignoreUnits;
+ };
+
+ servicesDir = pkgs.runCommandLocal "hm-shepherd-services" { } (''
+ mkdir -p $out
+ '' + lib.concatStrings (lib.mapAttrsToList (file: text: ''
+ cp ${pkgs.writeText file text} $out/${file}
+ '') result.files));
+
+ stateDir = "${config.xdg.stateHome}/home-manager/shepherd";
+
+ findHerd = ''
+ herd="$HOME/.guix-home/profile/bin/herd"
+ [ -x "$herd" ] || herd="$(command -v herd || true)"
+ '';
+
+ # Compositor -> Shepherd environment bridge (replaces
+ # `dbus-update-activation-environment --systemd` + hyprland-session.target).
+ sessionBridge = pkgs.writeShellScript "hm-shepherd-session" ''
+ ${findHerd}
+ [ -n "$herd" ] || { echo "hm-shepherd-session: herd not found" >&2; exit 1; }
+ # Stop first: dependents go down with it and come back with the new env.
+ "$herd" stop ${sh.graphicalSym} >/dev/null 2>&1 || true
+ expr="(begin"
+ for v in ${lib.escapeShellArgs cfg.shepherd.sessionVariables}; do
+ if [ -n "''${!v+x}" ]; then
+ val="''${!v}"; val="''${val//\\/\\\\}"; val="''${val//\"/\\\"}"
+ expr+=" (setenv \"$v\" \"$val\")"
+ else
+ expr+=" (unsetenv \"$v\")"
+ fi
+ done
+ expr+=" #t)"
+ "$herd" eval root "$expr"
+ "$herd" start ${sh.graphicalSym}
+ ${lib.concatMapStrings (s: ''
+ "$herd" start ${lib.escapeShellArg s}
+ '') result.graphical}
+ '';
+in
+{
+ options.targets.guix = {
+ enable = lib.mkEnableOption "Guix System integration for standalone home-manager";
+
+ pamWrapped = lib.mkOption {
+ type = lib.types.listOf lib.types.package;
+ default = [ ];
+ example = lib.literalExpression "[ pkgs.hyprlock ]";
+ description = ''
+ Packages that authenticate through PAM. Nix's libpam can't drive Guix's
+ PAM stack, so their binaries are re-exported with Guix's libpam
+ preloaded. Each must set meta.mainProgram.
+ '';
+ };
+
+ shepherd = {
+ unenforced = lib.mkOption {
+ type = with lib.types; attrsOf (listOf str);
+ default = { };
+ example = { "foo.service" = [ "Service.ProtectSystem" "Service.PrivateTmp" ]; };
+ description = ''
+ Per-unit acknowledgement that the listed `Section.Key`s (or
+ `Section.*`) are dropped when translating to Shepherd. Without an
+ entry, any untranslatable key is an evaluation error. Each dropped
+ key is reported as a build warning on every switch.
+ '';
+ };
+ ignoreUnits = lib.mkOption {
+ type = with lib.types; listOf str;
+ default = [ ];
+ example = [ "tray.target" ];
+ description = "Full unit names to skip entirely (not translated, no error).";
+ };
+ sessionBridge = lib.mkOption {
+ type = lib.types.package;
+ readOnly = true;
+ default = sessionBridge;
+ description = "Script the compositor runs (exec-once) to start graphical services; also usable by hand.";
+ };
+ sessionVariables = lib.mkOption {
+ type = with lib.types; listOf str;
+ default = config.wayland.windowManager.hyprland.systemd.variables
+ ++ [ "DBUS_SESSION_BUS_ADDRESS" "SSH_AUTH_SOCK" ];
+ defaultText = lib.literalExpression ''hyprland.systemd.variables ++ [ "DBUS_SESSION_BUS_ADDRESS" "SSH_AUTH_SOCK" ]'';
+ description = "Variables the compositor pushes into Shepherd before starting graphical services.";
+ };
+ };
+ };
+
+ config = lib.mkIf cfg.enable (lib.mkMerge [
+ {
+ assertions = [
+ {
+ # osConfig is only passed when home-manager runs as a NixOS or
+ # nix-darwin module, i.e. never on a Guix host.
+ assertion = osConfig == null;
+ message = "targets.guix is enabled inside a NixOS/nix-darwin home-manager module; it only applies to standalone home-manager on Guix System.";
+ }
+ {
+ assertion = config.targets.genericLinux.enable;
+ message = "targets.guix builds on targets.genericLinux; enable it too.";
+ }
+ ] ++ map (e: { assertion = false; message = "targets.guix: ${e}"; }) result.errors;
+
+ warnings = result.warnings;
+
+ # HM's ssh-auth-sock module (pulled in by gpg-agent's SSH support) adds
+ # a oneshot that runs `dbus-update-activation-environment --systemd
+ # SSH_AUTH_SOCK`; its whole job is systemd/D-Bus environment plumbing.
+ # The shell integration from the same module still sets the variable,
+ # and the session bridge pushes it into Shepherd (sessionVariables).
+ targets.guix.shepherd.ignoreUnits = [ "set-SSH_AUTH_SOCK.service" ];
+
+ # No systemd user manager: units are translated to Shepherd instead.
+ systemd.user.startServices = false;
+
+ # HM's GPU setup installs /etc/tmpfiles.d + runs systemd-tmpfiles as
+ # root; neither exists on Guix, so it only produces a warning per switch.
+ targets.genericLinux.gpu.enable = lib.mkDefault false;
+
+ # Guix's SSH patches openssh to not allow files outside of the GNU store
+ # so we need to copy the SSH config from the store into the userr's
+ # home directory to not get a permission error
+ home.activation.removeSshConfigSymlink = lib.mkIf config.programs.ssh.enable (
+ lib.hm.dag.entryBefore [ "checkLinkTargets" ] ''
+ if [ -n "''${HOME:-}" ] && [ -e "$HOME/.ssh/config" ]; then
+ rm -f "$HOME/.ssh/config"
+ fi
+ ''
+ );
+
+ home.activation.fixSshPermissions = lib.mkIf config.programs.ssh.enable (
+ lib.hm.dag.entryAfter [ "linkGeneration" ] ''
+ run install -d -m 0700 "$HOME/.ssh"
+ if [ -L "$HOME/.ssh/config" ]; then
+ src="$(readlink -f "$HOME/.ssh/config")"
+ run rm -f "$HOME/.ssh/config"
+ run install -m 0600 "$src" "$HOME/.ssh/config"
+ fi
+ ''
+ );
+
+ # Its exec-once runs `dbus-update-activation-environment --systemd &&
+ # systemctl --user ...`, which fails at the first step on Guix. The
+ # bridge below does the Shepherd equivalent.
+ wayland.windowManager.hyprland.systemd.enable = lib.mkDefault false;
+ wayland.windowManager.hyprland.extraConfig = lib.mkIf config.wayland.windowManager.hyprland.enable ''
+ exec-once = ${sessionBridge}
+ '';
+
+ dconf.enable = lib.mkDefault false;
+
+ # Guix Home owns ~/.zprofile's job of loading its environment (and
+ # starting the user Shepherd via on-first-login); keep that working
+ # under an HM-managed zsh.
+ programs.zsh.profileExtra = lib.mkBefore ''
+ if [ -f "$HOME/.guix-home/setup-environment" ]; then
+ HOME_ENVIRONMENT="$HOME/.guix-home"
+ . "$HOME_ENVIRONMENT/setup-environment"
+ "$HOME_ENVIRONMENT/on-first-login"
+ unset HOME_ENVIRONMENT
+ fi
+
+ # /etc/profile puts ~/.config/guix/current (guix pull profile with
+ # our channels) ahead of the system profile, but only bash login
+ # shells source it.
+ if [ -d "$HOME/.config/guix/current" ]; then
+ export PATH="$HOME/.config/guix/current/bin:$PATH"
+ export INFOPATH="$HOME/.config/guix/current/share/info''${INFOPATH:+:}$INFOPATH"
+ fi
+ '';
+
+ # No /etc/fonts/fonts.conf from Nix's point of view on Guix.
+ xdg.configFile."fontconfig-nix/fonts.conf".text = ''
+ <?xml version='1.0'?>
+ <!DOCTYPE fontconfig SYSTEM 'fonts.dtd'>
+ <fontconfig>
+ <include ignore_missing="yes">${pkgs.fontconfig.out}/etc/fonts/conf.d</include>
+ <include ignore_missing="yes">${config.xdg.configHome}/fontconfig/conf.d</include>
+ <dir>${config.home.homeDirectory}/.guix-home/profile/share/fonts</dir>
+ <cachedir>${config.xdg.cacheHome}/fontconfig</cachedir>
+ </fontconfig>
+ '';
+ home.sessionVariables.FONTCONFIG_FILE = "${config.xdg.configHome}/fontconfig-nix/fonts.conf";
+
+ home.packages = map
+ (p: lib.hiPrio (pkgs.writeShellScriptBin p.meta.mainProgram ''
+ export LD_PRELOAD="/run/current-system/profile/lib/libpam.so.0''${LD_PRELOAD:+:$LD_PRELOAD}"
+ exec ${lib.getExe p} "$@"
+ ''))
+ cfg.pamWrapped;
+
+ # Sync generated Shepherd services; reload only what changed.
+ home.activation.shepherdServices = lib.hm.dag.entryAfter [ "linkGeneration" "reloadSystemd" ] ''
+ if [ ! -x /run/current-system/profile/bin/guix ]; then
+ errorEcho "targets.guix: this host isn't Guix System; refusing to manage Shepherd services."
+ exit 1
+ fi
+
+ ${findHerd}
+ live=
+ if [ -n "$herd" ] && "$herd" status root >/dev/null 2>&1; then live=1; fi
+
+ dst=${lib.escapeShellArg stateDir}
+ run mkdir -p "$dst"
+
+ for f in "$dst"/*.scm; do
+ [ -e "$f" ] || continue
+ n="$(basename "$f" .scm)"
+ if [ ! -e ${servicesDir}/"$n".scm ]; then
+ [ -n "$live" ] && run "$herd" unload root "$n" || true
+ run rm -f "$f"
+ fi
+ done
+
+ for f in ${servicesDir}/*.scm; do
+ n="$(basename "$f" .scm)"
+ if ! cmp -s "$f" "$dst/$n.scm"; then
+ run install -m 0644 "$f" "$dst/$n.scm"
+ if [ -n "$live" ]; then
+ run "$herd" unload root "$n" >/dev/null 2>&1 || true
+ run "$herd" load root "$dst/$n.scm"
+ # Autostart services restart themselves from the loaded file;
+ # graphical ones only if a session is up (else they'd start
+ # with no compositor environment).
+ case " ${lib.concatStringsSep " " result.graphical} " in
+ *" $n "*)
+ if "$herd" status ${sh.graphicalSym} 2>/dev/null | grep -q 'It is running'; then
+ run "$herd" start "$n"
+ fi ;;
+ esac
+ fi
+ fi
+ done
+
+ if [ -z "$live" ]; then
+ warnEcho "targets.guix: user Shepherd not reachable; services register at next login via the Guix Home loader."
+ fi
+ '';
+ }
+ ]);
+}
diff --git a/users/ryan/modules/targets/guix/shepherd.nix b/users/ryan/modules/targets/guix/shepherd.nix
new file mode 100644
index 0000000..55750ba
--- /dev/null
+++ b/users/ryan/modules/targets/guix/shepherd.nix
@@ -0,0 +1,408 @@
+# systemd user units (as home-manager models them) -> GNU Shepherd services.
+#
+# Fail-closed by design: every [Section] Key must be either translated with
+# the same semantics, or on the explicit `ignoredKeys` list below (keys that
+# don't change what runs or with what privileges). Anything else is an
+# eval-time error, unless the user acknowledges it per unit via
+# `targets.guix.shepherd.unenforced`, in which case it's dropped *with a
+# build warning*. Sandboxing (Protect*, Private*, SystemCallFilter, ...) is
+# deliberately never translated: a service that declares it is relying on it.
+{ lib, setpriv }:
+let
+ inherit (lib)
+ concatStringsSep concatMapStringsSep concatMapStrings concatMap mapAttrs mapAttrsToList
+ filterAttrs attrNames hasSuffix removeSuffix optional optionals
+ optionalString elem toList last unique;
+
+ # ---------------------------------------------------------------- helpers
+
+ # JSON string escapes are a subset of Guile's (\" \\ \n \uXXXX).
+ q = builtins.toJSON;
+ sym = s: "(string->symbol ${q s})";
+
+ # HM's unit types leave null / [] placeholders for typed-but-unset keys
+ # (Description, Documentation, X-*-Triggers, Environment, ExecStart); HM's
+ # own INI renderer drops them, so do the same.
+ clean = unit:
+ filterAttrs (_: s: s != { })
+ (mapAttrs (_: filterAttrs (_: v: v != null && v != [ ])) unit);
+
+ str = v: if builtins.isBool v then (if v then "true" else "false") else toString v;
+ # Scalar keys: systemd's last assignment wins.
+ scalar = v: str (last (toList v));
+ isTrue = v: elem (scalar v) [ "true" "yes" "on" "1" ];
+
+ # ------------------------------------------------------- key allowlists
+
+ # Keys with no effect on what runs, how, or with what privileges.
+ ignoredKeys = {
+ Unit = [
+ "Description" # consumed as #:documentation
+ "Documentation"
+ "After" # pure ordering; dependencies below imply ordering in Shepherd
+ "Before"
+ "RefuseManualStart"
+ "RefuseManualStop"
+ "X-SwitchMethod"
+ ];
+ Service = [ "ExecReload" ]; # only reachable via `systemctl reload`
+ Socket = [ ];
+ Install = [ ];
+ };
+
+ limitMap = {
+ LimitCPU = "cpu"; LimitFSIZE = "fsize"; LimitDATA = "data";
+ LimitSTACK = "stack"; LimitCORE = "core"; LimitRSS = "rss";
+ LimitNOFILE = "nofile"; LimitAS = "as"; LimitNPROC = "nproc";
+ LimitMEMLOCK = "memlock";
+ };
+
+ translatedKeys = {
+ # Triggers are embedded in the generated file, so a trigger change is a
+ # content change and activation reloads the service (reload -> restart,
+ # the conservative direction).
+ Unit = [ "Wants" "Requires" "BindsTo" "PartOf" "X-Restart-Triggers" "X-Reload-Triggers" ];
+ Service = [
+ "Type" "ExecStart" "Environment" "WorkingDirectory" "Restart"
+ "RestartSec" "UMask" "NoNewPrivileges"
+ ] ++ attrNames limitMap;
+ Socket = [
+ "ListenStream" "FileDescriptorName" "Service" "Accept"
+ "SocketMode" "DirectoryMode"
+ ];
+ Install = [ "WantedBy" ];
+ };
+
+ sectionsFor = kind: [ "Unit" "Install" (if kind == "service" then "Service" else "Socket") ];
+
+ # ----------------------------------------------------- systemd specifiers
+
+ specExprs = {
+ t = ''(getenv "XDG_RUNTIME_DIR")'';
+ h = ''(getenv "HOME")'';
+ U = "(number->string (getuid))";
+ u = "(passwd:name (getpwuid (getuid)))";
+ "%" = q "%";
+ };
+ specParts = s: builtins.split "%(.)" s;
+ badSpecs = s:
+ concatMap
+ (p: optional (builtins.isList p && !(specExprs ? ${builtins.head p})) "%${builtins.head p}")
+ (specParts s);
+ # -> Scheme expression, evaluated at service start (not at load).
+ expand = s:
+ let
+ exprs = concatMap
+ (p: if builtins.isList p then [ specExprs.${builtins.head p} ] else optional (p != "") (q p))
+ (specParts s);
+ in
+ if exprs == [ ] then q ""
+ else if builtins.length exprs == 1 then builtins.head exprs
+ else "(string-append ${concatStringsSep " " exprs})";
+
+ # ------------------------------------------------- command-line splitting
+
+ # systemd quoting, minus the parts we refuse: backslash escapes (C-style in
+ # systemd, so not a literal-char escape) and $VAR expansion are rejected by
+ # cmdErrors rather than approximated.
+ tokenize = s:
+ let
+ ws = c: c == " " || c == "\t" || c == "\n";
+ step = st: c:
+ if st.q != null then
+ (if c == st.q then st // { q = null; } else st // { cur = st.cur + c; })
+ else if c == "\"" || c == "'" then
+ st // { q = c; cur = if st.cur == null then "" else st.cur; }
+ else if ws c then
+ (if st.cur == null then st else st // { out = st.out ++ [ st.cur ]; cur = null; })
+ else
+ st // { cur = (if st.cur == null then "" else st.cur) + c; };
+ end = builtins.foldl' step { out = [ ]; cur = null; q = null; }
+ (lib.stringToCharacters s);
+ in
+ {
+ tokens = end.out ++ optional (end.cur != null) end.cur;
+ unterminated = end.q != null;
+ };
+
+ cmdErrors = what: s:
+ let t = tokenize s; first = if t.tokens == [ ] then "" else builtins.head t.tokens;
+ in
+ optional (t.tokens == [ ]) "${what} is empty"
+ ++ optional t.unterminated "${what} has an unterminated quote"
+ ++ optional (lib.hasInfix "\\" s) "${what} uses backslash escapes (not translated)"
+ ++ optional (lib.hasInfix "$" s) "${what} uses $VAR expansion (not translated)"
+ ++ optional (builtins.match "[-@:+!|].*" first != null)
+ "${what} uses an executable prefix (${builtins.substring 0 1 first}) with no Shepherd equivalent"
+ ++ map (sp: "${what} uses unsupported specifier ${sp}") (badSpecs s);
+
+ # ----------------------------------------------------------- validation
+
+ octal = s: builtins.match "0?[0-7]{3,4}" s != null;
+ limitVal = s: builtins.match "(infinity|[0-9]+)(:(infinity|[0-9]+))?" s != null;
+ seconds = s: builtins.match "([0-9]+)s?" s;
+
+ keyErrors = { kind, name, unit, unenforced }:
+ let
+ full = "${name}.${kind}";
+ allowed = sectionsFor kind;
+ in
+ concatMap
+ (sec:
+ if !(elem sec allowed) then
+ optional (!(elem "${sec}.*" unenforced)) "${full}: section [${sec}] has no Shepherd equivalent"
+ else
+ concatMap
+ (key: optional
+ (!(elem key (translatedKeys.${sec} ++ ignoredKeys.${sec}))
+ && !(elem "${sec}.${key}" unenforced))
+ "${full}: ${sec}.${key} has no Shepherd equivalent")
+ (attrNames unit.${sec}))
+ (attrNames unit);
+
+ # Names Shepherd knows about, used to resolve dependency edges.
+ graphicalSym = "hm-graphical-session";
+
+ # Which service a socket unit activates (systemd default: same name).
+ socketTarget = sname: sock:
+ removeSuffix ".service" (scalar (sock.Socket.Service or "${sname}.service"));
+
+ resolveDep = { self, services, sockets }: dep:
+ if dep == "graphical-session.target" then { ok = graphicalSym; }
+ else if hasSuffix ".service" dep && services ? ${removeSuffix ".service" dep} then
+ { ok = removeSuffix ".service" dep; }
+ else if hasSuffix ".socket" dep && sockets ? ${removeSuffix ".socket" dep} then
+ let target = socketTarget (removeSuffix ".socket" dep) sockets.${removeSuffix ".socket" dep};
+ in if target == self then { skip = true; } else { ok = target; }
+ else { err = "dependency ${dep} is not a translated unit"; };
+
+ serviceErrors = { name, unit, services, sockets, unenforced }:
+ let
+ full = "${name}.service";
+ svc = unit.Service or { };
+ exec = toList (svc.ExecStart or [ ]);
+ deps = concatMap (k: toList (unit.Unit.${k} or [ ])) [ "Wants" "Requires" "BindsTo" "PartOf" ];
+ envTokens = concatMap (e: (tokenize (str e)).tokens) (toList (svc.Environment or [ ]));
+ limits = filterAttrs (k: _: limitMap ? ${k}) svc;
+ in
+ keyErrors { kind = "service"; inherit name unit unenforced; }
+ ++ optional (!(elem (scalar (svc.Type or "simple")) [ "simple" "exec" ]))
+ "${full}: Type=${scalar svc.Type} is not translated (only simple/exec)"
+ ++ optional (builtins.length exec != 1)
+ "${full}: needs exactly one ExecStart (has ${toString (builtins.length exec)})"
+ ++ concatMap (cmdErrors "${full}: ExecStart") (map str exec)
+ ++ concatMap (e: optional (!(lib.hasInfix "=" e)) "${full}: Environment entry '${e}' is not K=V") envTokens
+ ++ concatMap (e: map (sp: "${full}: Environment uses unsupported specifier ${sp}") (badSpecs e)) envTokens
+ ++ optional (lib.any (e: lib.hasInfix "$" e || lib.hasInfix "\\" e) (map str (toList (svc.Environment or [ ]))))
+ "${full}: Environment uses escapes or $VAR (not translated)"
+ ++ optionals (svc ? WorkingDirectory) (
+ let d = scalar svc.WorkingDirectory; in
+ optional (lib.hasPrefix "-" d) "${full}: WorkingDirectory=-... (ignore-missing) is not translated"
+ ++ map (sp: "${full}: WorkingDirectory uses unsupported specifier ${sp}") (badSpecs d))
+ ++ optional (svc ? Restart && !(elem (scalar svc.Restart) [ "no" "always" "on-failure" "on-abnormal" ]))
+ "${full}: Restart=${scalar svc.Restart} is not translated"
+ ++ optional (svc ? RestartSec && seconds (scalar svc.RestartSec) == null)
+ "${full}: RestartSec must be whole seconds"
+ ++ optional (svc ? UMask && !(octal (scalar svc.UMask))) "${full}: UMask must be octal"
+ ++ mapAttrsToList (k: v: "${full}: ${k}=${scalar v} is not a plain integer/infinity limit")
+ (filterAttrs (_: v: !(limitVal (scalar v))) limits)
+ ++ concatMap (d: let r = resolveDep { self = name; inherit services sockets; } d;
+ in optional (r ? err) "${full}: ${r.err}") deps
+ ++ concatMap (t: optional (!(elem t [ "default.target" "graphical-session.target" ]))
+ "${full}: WantedBy=${t} is not translated")
+ (toList (unit.Install.WantedBy or [ ]));
+
+ socketErrors = { name, unit, services, unenforced }:
+ let
+ full = "${name}.socket";
+ sock = unit.Socket or { };
+ target = socketTarget name unit;
+ paths = map str (toList (sock.ListenStream or [ ]));
+ dirMode = if sock ? DirectoryMode then scalar sock.DirectoryMode else null;
+ in
+ keyErrors { kind = "socket"; inherit name unit unenforced; }
+ ++ optional (!(services ? ${target})) "${full}: activates ${target}.service, which is not translated"
+ ++ optional (paths == [ ]) "${full}: no ListenStream"
+ ++ concatMap (p: optional (builtins.match "(/|%t|%h).*" p == null)
+ "${full}: ListenStream=${p} is not a unix socket path (TCP/UDP not translated)")
+ paths
+ ++ concatMap (p: map (sp: "${full}: ListenStream uses unsupported specifier ${sp}") (badSpecs p)) paths
+ ++ optional (sock ? Accept && isTrue sock.Accept) "${full}: Accept=yes (inetd-style) is not translated"
+ ++ optional (dirMode != null && !(octal dirMode)) "${full}: DirectoryMode must be octal"
+ # Shepherd can set the parent directory's mode but not the socket's. A
+ # 0700 parent makes the socket's own mode moot; anything looser doesn't.
+ ++ optional (sock ? SocketMode && !(elem dirMode [ "0700" "700" ]))
+ "${full}: SocketMode is only honoured with DirectoryMode=0700 (Shepherd can't chmod the socket)"
+ ++ concatMap (t: optional (t != "sockets.target") "${full}: WantedBy=${t} is not translated")
+ (toList (unit.Install.WantedBy or [ ]));
+
+ # ------------------------------------------------------------- emission
+
+ prelude = ''
+ (use-modules (shepherd service))
+
+ ;; Unit Environment= first, then Shepherd's *current* environment (which
+ ;; the compositor bridge updates via `herd eval root (setenv ...)`).
+ (define (hm-env overrides)
+ (let ((keys (map (lambda (kv) (substring kv 0 (string-index kv #\=)))
+ overrides)))
+ (append overrides
+ (filter (lambda (kv)
+ (let ((i (string-index kv #\=)))
+ (not (and i (member (substring kv 0 i) keys)))))
+ (environ)))))
+ '';
+
+ limitExpr = v:
+ let
+ parts = lib.splitString ":" (scalar v);
+ one = x: if x == "infinity" then "#f" else x;
+ in
+ if builtins.length parts == 1 then "${one (builtins.head parts)} ${one (builtins.head parts)}"
+ else "${one (builtins.elemAt parts 0)} ${one (builtins.elemAt parts 1)}";
+
+ octalExpr = s: "#o${lib.removePrefix "0" s}";
+
+ emitService = { name, unit, services, sockets }:
+ let
+ svc = unit.Service or { };
+ mySockets = filterAttrs (sn: s: socketTarget sn s == name) sockets;
+ socketActivated = mySockets != { };
+
+ argv0 = (tokenize (str (builtins.head (toList svc.ExecStart)))).tokens;
+ argv = optionals (svc ? NoNewPrivileges && isTrue svc.NoNewPrivileges)
+ [ "${setpriv}/bin/setpriv" "--no-new-privs" ]
+ ++ argv0;
+ cmd = "(list ${concatMapStringsSep " " expand argv})";
+
+ env = concatMap (e: (tokenize (str e)).tokens) (toList (svc.Environment or [ ]));
+ envExpr = "(hm-env (list ${concatMapStringsSep " " (kv:
+ let i = lib.stringLength (builtins.head (lib.splitString "=" kv)); in
+ "(string-append ${q (builtins.substring 0 (i + 1) kv)} ${expand (builtins.substring (i + 1) (-1) kv)})")
+ env}))";
+
+ deps = unique (concatMap
+ (d: let r = resolveDep { self = name; inherit services sockets; } d;
+ in optional (r ? ok) r.ok)
+ (concatMap (k: toList (unit.Unit.${k} or [ ])) [ "Wants" "Requires" "BindsTo" "PartOf" ]));
+ wantedBy = toList (unit.Install.WantedBy or [ ]);
+ graphical = elem "graphical-session.target" wantedBy || elem graphicalSym deps;
+ requirement = unique (deps ++ optional graphical graphicalSym);
+ autostart = elem "default.target" wantedBy
+ || lib.any (s: elem "sockets.target" (toList (s.Install.WantedBy or [ ]))) (lib.attrValues mySockets);
+
+ limits = filterAttrs (k: _: limitMap ? ${k}) svc;
+ opts = concatStringsSep "\n "
+ ([ "#:environment-variables ${envExpr}" ]
+ ++ optional (svc ? WorkingDirectory)
+ "#:directory ${let d = scalar svc.WorkingDirectory; in if d == "~" then specExprs.h else expand d}"
+ ++ optional (svc ? UMask) "#:file-creation-mask ${octalExpr (scalar svc.UMask)}"
+ ++ optional (limits != { }) "#:resource-limits (list ${concatStringsSep " "
+ (mapAttrsToList (k: v: "(list '${limitMap.${k}} ${limitExpr v})") limits)})");
+
+ endpoints = concatStringsSep "\n " (concatMap
+ (sn:
+ let
+ s = mySockets.${sn}.Socket;
+ fdName = scalar (s.FileDescriptorName or sn);
+ dirMode = if s ? DirectoryMode then octalExpr (scalar s.DirectoryMode) else "#o755";
+ in
+ map (p: "(endpoint (make-socket-address AF_UNIX ${expand (str p)}) #:name ${q fdName} #:socket-directory-permissions ${dirMode})")
+ (toList s.ListenStream))
+ (attrNames mySockets));
+
+ # Socket units keep listening after the daemon exits regardless of
+ # Restart= (that's socket-activation semantics), hence respawn? here.
+ respawn = socketActivated || elem (scalar (svc.Restart or "no")) [ "always" "on-failure" "on-abnormal" ];
+ restartSec = if svc ? RestartSec then builtins.head (seconds (scalar svc.RestartSec)) else null;
+
+ constructor =
+ if socketActivated then ''
+ (make-systemd-constructor ${cmd}
+ (list ${endpoints})
+ #:lazy-start? #t
+ ${opts})''
+ else ''
+ (make-forkexec-constructor ${cmd}
+ ${opts})'';
+ in
+ {
+ inherit graphical autostart;
+ text = ''
+ ;; Generated by home-manager (targets.guix) from ${name}.service${
+ optionalString socketActivated " + ${concatMapStringsSep ", " (s: "${s}.socket") (attrNames mySockets)}"
+ }.
+ ;; Do not edit; regenerated on every activation.${
+ concatMapStrings (t: "\n;; trigger: ${str t}")
+ (concatMap (k: toList (unit.Unit.${k} or [ ])) [ "X-Restart-Triggers" "X-Reload-Triggers" ])}
+ ${prelude}
+ (register-services
+ (list
+ (service (list ${sym name})
+ #:documentation ${q (scalar (unit.Unit.Description or "${name} (from home-manager)"))}
+ #:requirement (list ${concatMapStringsSep " " sym requirement})
+ #:respawn? ${if respawn then "#t" else "#f"}${
+ optionalString (restartSec != null) "\n #:respawn-delay ${restartSec}"}
+ ;; Constructed at start time so specifiers and the inherited
+ ;; environment reflect the session at that moment.
+ #:start (lambda args
+ (apply ${constructor}
+ args))
+ #:stop ${if socketActivated then "(make-systemd-destructor)" else "(make-kill-destructor)"})))
+ ${optionalString (autostart && !graphical) "\n(start-in-the-background (list ${sym name}))"}
+ '';
+ };
+
+ graphicalSessionFile = ''
+ ;; Generated by home-manager (targets.guix). Marker for graphical-session.target:
+ ;; started by the compositor bridge after it pushes its environment into
+ ;; Shepherd; stopping it stops every graphical service.
+ (use-modules (shepherd service))
+ (register-services
+ (list (service (list ${sym graphicalSym})
+ #:documentation "Graphical session (home-manager graphical-session.target)"
+ #:start (const #t)
+ #:stop (const #f))))
+ '';
+in
+{
+ inherit graphicalSym;
+
+ # -> { errors, warnings, files = { "<name>.scm" = text; }, graphical = [ names ] }
+ translate = { systemdUser, unenforced, ignoreUnits }:
+ let
+ keep = kind: units: filterAttrs (n: _: !(elem "${n}.${kind}" ignoreUnits)) (mapAttrs (_: clean) units);
+ services = keep "service" (systemdUser.services or { });
+ sockets = keep "socket" (systemdUser.sockets or { });
+ unen = full: unenforced.${full} or [ ];
+
+ # Units that make things happen on their own. Targets and slices are
+ # inert unless something references them, and any such reference
+ # (Wants=/WantedBy=/PartOf=/Slice=) is already an error above.
+ otherKinds = [ "timers" "paths" "mounts" "automounts" ];
+ otherErrors = concatMap
+ (k: map (n: "${n}.${removeSuffix "s" k}: ${removeSuffix "s" k} units are not translated (add to targets.guix.shepherd.ignoreUnits to skip)")
+ (attrNames (keep (removeSuffix "s" k) (systemdUser.${k} or { }))))
+ otherKinds;
+
+ errors =
+ concatMap (n: serviceErrors { name = n; unit = services.${n}; inherit services sockets; unenforced = unen "${n}.service"; }) (attrNames services)
+ ++ concatMap (n: socketErrors { name = n; unit = sockets.${n}; inherit services; unenforced = unen "${n}.socket"; }) (attrNames sockets)
+ ++ otherErrors
+ ++ concatMap (full: optional (!(services ? ${removeSuffix ".service" full}) && !(sockets ? ${removeSuffix ".socket" full}))
+ "targets.guix.shepherd.unenforced: ${full} is not a defined service/socket")
+ (attrNames unenforced);
+
+ warnings = concatMap
+ (full: map (k: "targets.guix: ${full}: ${k} dropped (not enforced under Shepherd)") unenforced.${full})
+ (attrNames unenforced);
+
+ emitted = mapAttrs (n: u: emitService { name = n; unit = u; inherit services sockets; }) services;
+ in
+ {
+ inherit errors warnings;
+ graphical = attrNames (filterAttrs (_: e: e.graphical) emitted);
+ files = { "${graphicalSym}.scm" = graphicalSessionFile; }
+ // lib.mapAttrs' (n: e: lib.nameValuePair "${n}.scm" e.text) emitted;
+ };
+}
diff --git a/users/ryan/modules/zen/default.nix b/users/ryan/modules/zen/default.nix
new file mode 100644
index 0000000..8e7a73b
--- /dev/null
+++ b/users/ryan/modules/zen/default.nix
@@ -0,0 +1,94 @@
+{ config, lib, pkgs, ... }:
+let
+ inherit (lib) mkOption mkEnableOption types mkIf mkMerge;
+ inherit (pkgs.stdenv.hostPlatform) isDarwin;
+ cfg = config.ryan.zen;
+
+ extensionType = types.submodule {
+ options = {
+ enable = mkOption {
+ type = types.bool;
+ default = true;
+ description = "Set false to drop an entry defined elsewhere (attrsOf can't delete keys).";
+ };
+ id = mkOption {
+ type = types.str;
+ description = "Add-on GUID; also the policy key and the AMO API lookup key.";
+ };
+ version = mkOption {
+ type = types.nullOr types.str;
+ default = null;
+ description = "Hard pin. null = whatever extensions.lock.json holds (bump with lock.sh).";
+ };
+ mode = mkOption {
+ type = types.enum [ "force_installed" "normal_installed" ];
+ default = "force_installed";
+ };
+ privateBrowsing = mkOption {
+ type = types.nullOr types.bool;
+ default = null;
+ description = "Emit private_browsing only when non-null.";
+ };
+ extraSettings = mkOption {
+ type = types.attrsOf types.anything;
+ default = { };
+ description = "Merged last into the ExtensionSettings entry (e.g. default_area).";
+ };
+ };
+ };
+in
+{
+ imports = [ ./extensions.nix ];
+
+ options.ryan.zen = {
+ enable = mkEnableOption "Zen browser with locked policies, profile and extensions";
+
+ extensions = mkOption {
+ type = types.attrsOf extensionType;
+ default = { };
+ description = "Policy-managed extensions, resolved through extensions.lock.json.";
+ };
+
+ exclusive = mkOption {
+ type = types.bool;
+ default = true;
+ description = "Disallow all undeclared extensions and disable all temporary loading";
+ };
+ };
+
+ config = mkIf cfg.enable (mkMerge [
+ {
+ programs.zen-browser = {
+ enable = true;
+ policies = import ./policies.nix;
+ profiles.default = import ./profile.nix;
+ };
+
+ ryan.zen.extensions = {
+ ublock-origin = {
+ id = "uBlock0@raymondhill.net";
+ privateBrowsing = true;
+ };
+ bitwarden = {
+ id = "{446900e4-71c2-419f-a6a7-df9c091e268b}";
+ mode = "normal_installed";
+ privateBrowsing = true;
+ };
+ sponsorblock.id = "sponsorBlocker@ajay.app";
+ dearrow.id = "deArrow@ajay.app";
+ return-youtube-dislikes.id = "{762f9885-5a13-4abd-9c77-433dcd38b8fd}";
+ youtube-nonstop.id = "{0d7cafdd-501c-49ca-8ebb-e3341caaa55e}";
+ tampermonkey.id = "firefox@tampermonkey.net";
+ floccus.id = "floccus@handmadeideas.org";
+ mailvelope.id = "jid1-AQqSMBYb0a8ADg@jetpack";
+ };
+ }
+
+ (mkIf isDarwin {
+ programs.zen-browser = {
+ package = null; # managed via homebrew
+ darwinDefaultsId = "app.zen-browser.zen";
+ };
+ })
+ ]);
+}
diff --git a/users/ryan/modules/zen/extensions.lock.json b/users/ryan/modules/zen/extensions.lock.json
new file mode 100644
index 0000000..cd0df78
--- /dev/null
+++ b/users/ryan/modules/zen/extensions.lock.json
@@ -0,0 +1,62 @@
+{
+ "aria2": {
+ "hash": "sha256-0V6zKAqz8uKlDE4q/szSW732B9X0TUKdo8qUChv2IW0=",
+ "id": "baptistecdr@users.noreply.github.com",
+ "url": "https://addons.mozilla.org/firefox/downloads/file/5055892/aria2_extension-4.15.4.xpi",
+ "version": "4.15.4"
+ },
+ "bitwarden": {
+ "hash": "sha256-XdbvpdIo2+rHoMaog/lReH3GbleYUyLOVJIQsN+NBUw=",
+ "id": "{446900e4-71c2-419f-a6a7-df9c091e268b}",
+ "url": "https://addons.mozilla.org/firefox/downloads/file/5076543/bitwarden_password_manager-2026.9.3.xpi",
+ "version": "2026.9.3"
+ },
+ "dearrow": {
+ "hash": "sha256-MVGlHbgJN0a+ZGwEGvPq1VPl3pX6Tr1frgM+A54QVtE=",
+ "id": "deArrow@ajay.app",
+ "url": "https://addons.mozilla.org/firefox/downloads/file/4897568/dearrow-2.3.10.xpi",
+ "version": "2.3.10"
+ },
+ "floccus": {
+ "hash": "sha256-FOk0NhveQToZjq88hRU3wjJVwR82ZQ/DJAz6hqlkJYg=",
+ "id": "floccus@handmadeideas.org",
+ "url": "https://addons.mozilla.org/firefox/downloads/file/5063393/floccus-5.11.0.xpi",
+ "version": "5.11.0"
+ },
+ "mailvelope": {
+ "hash": "sha256-MUAU3OzESJWHE9zkuylyMh8/4nxj725urSDouFA5Juw=",
+ "id": "jid1-AQqSMBYb0a8ADg@jetpack",
+ "url": "https://addons.mozilla.org/firefox/downloads/file/4846833/mailvelope-6.3.0.xpi",
+ "version": "6.3.0"
+ },
+ "return-youtube-dislikes": {
+ "hash": "sha256-WXGXSfbfOMFgHKXznAIVjV8AXEPp7uofGVJ/RoyUEhc=",
+ "id": "{762f9885-5a13-4abd-9c77-433dcd38b8fd}",
+ "url": "https://addons.mozilla.org/firefox/downloads/file/5012638/return_youtube_dislikes-4.0.6.xpi",
+ "version": "4.0.6"
+ },
+ "sponsorblock": {
+ "hash": "sha256-DVDhYyxvFe4VpUPmcOHFcpdGBaXAJiKRbgjgJoA9+D8=",
+ "id": "sponsorBlocker@ajay.app",
+ "url": "https://addons.mozilla.org/firefox/downloads/file/4897574/sponsorblock-6.1.7.xpi",
+ "version": "6.1.7"
+ },
+ "tampermonkey": {
+ "hash": "sha256-GQAxx428VpYRSDVgHyyOa4Va0eE0313yePj8FYwGWQg=",
+ "id": "firefox@tampermonkey.net",
+ "url": "https://addons.mozilla.org/firefox/downloads/file/4797143/tampermonkey-5.5.0.xpi",
+ "version": "5.5.0"
+ },
+ "ublock-origin": {
+ "hash": "sha256-W3RBWGBFY3BkS9gPFhJehlsObDVrtd/PuEBpln6qUoc=",
+ "id": "uBlock0@raymondhill.net",
+ "url": "https://addons.mozilla.org/firefox/downloads/file/5034826/ublock_origin-1.75.0.xpi",
+ "version": "1.75.0"
+ },
+ "youtube-nonstop": {
+ "hash": "sha256-dlnRgPduqQjqgbhO2b3RiGJOqqYriKzL5titToyu/zg=",
+ "id": "{0d7cafdd-501c-49ca-8ebb-e3341caaa55e}",
+ "url": "https://addons.mozilla.org/firefox/downloads/file/4187690/youtube_nonstop-0.9.2.xpi",
+ "version": "0.9.2"
+ }
+}
diff --git a/users/ryan/modules/zen/extensions.nix b/users/ryan/modules/zen/extensions.nix
new file mode 100644
index 0000000..f91984b
--- /dev/null
+++ b/users/ryan/modules/zen/extensions.nix
@@ -0,0 +1,89 @@
+# version = null -> whatever the lock holds; `lock.sh <attr>` bumps it
+# version = "x.y.z" -> hard pin; lock.sh resolves exactly that version and
+# eval fails if the lock disagrees
+{ config, lib, pkgs, ... }:
+let
+ inherit (lib) mapAttrs' mapAttrsToList nameValuePair optionalAttrs;
+ cfg = config.ryan.zen;
+ exts = lib.filterAttrs (_: e: e.enable) cfg.extensions;
+ zen = config.programs.zen-browser;
+
+ lockFile = ./extensions.lock.json;
+ lock = lib.importJSON lockFile;
+
+ # Validated lock entry. Throws (not assertions) so the message surfaces no
+ # matter which consumer forces the policy first (HM wrapper, darwin defaults).
+ locked = name: e:
+ let
+ l = lock.${name} or (throw
+ "zen extension '${name}' has no entry in extensions.lock.json; run modules/zen/lock.sh");
+ in
+ if l.id != e.id then throw
+ "zen extension '${name}': lock has id ${l.id}, config has ${e.id}; re-run lock.sh"
+ else if e.version != null && l.version != e.version then throw
+ "zen extension '${name}': pinned ${e.version}, lock has ${l.version}; re-run lock.sh"
+ else l;
+
+ xpi = name: e:
+ let l = locked name e; in
+ pkgs.fetchurl {
+ name = "zen-ext-${name}-${l.version}.xpi";
+ inherit (l) url hash;
+ };
+
+ settingsFor = name: e:
+ {
+ install_url = "file://${xpi name e}";
+ installation_mode = e.mode;
+ updates_disabled = true;
+ }
+ // optionalAttrs (e.privateBrowsing != null) { private_browsing = e.privateBrowsing; }
+ // e.extraSettings;
+
+ pins = pkgs.writeText "zen-extension-pins.json" (builtins.toJSON
+ (mapAttrsToList (name: e: { inherit (e) id; version = (locked name e).version; }) exts));
+
+ jq = lib.getExe pkgs.jq;
+ sort = "${pkgs.coreutils}/bin/sort";
+in
+{
+ config = lib.mkIf (cfg.enable && exts != { }) {
+ programs.zen-browser.policies = {
+ # Locks extensions.update.enabled=false; belt to updates_disabled's braces.
+ ExtensionUpdate = false;
+ ExtensionSettings =
+ mapAttrs' (name: e: nameValuePair e.id (settingsFor name e)) exts
+ // optionalAttrs cfg.exclusive { "*".installation_mode = "blocked"; };
+ };
+
+ # Explicit GC root for every pinned XPI, independent of how the policy
+ # gets serialized on each platform. Also a handy place to inspect them.
+ home.file.".local/share/zen-extensions".source = pkgs.linkFarm "zen-extensions"
+ (mapAttrsToList (name: e: { name = "${name}.xpi"; path = xpi name e; }) exts);
+
+ # The policy engine refuses downgrades (installed > pinned is cancelled in
+ # PoliciesHelpers installAddonFromURL). For exactly that case, delete the
+ # installed XPI while Zen is closed; the next start drops it from the DB
+ # and the policy installs the pin. Upgrades are left to the policy engine.
+ programs.zen-browser.activationFragments.default = [{
+ requiresLock = true;
+ skipSubject = "zen extension downgrade check";
+ text = ''
+ prof="${zen.profilesPath}/${zen.profiles.default.path}"
+ if [ -f "$prof/extensions.json" ]; then
+ while IFS=$'\t' read -r id want have; do
+ [ -n "$have" ] && [ "$have" != "$want" ] || continue
+ newest="$(printf '%s\n%s\n' "$want" "$have" | ${sort} -V | tail -n1)"
+ [ "$newest" = "$have" ] || continue
+ echo "zen-extensions: $id installed $have > pinned $want; removing installed XPI"
+ [[ -v DRY_RUN ]] || rm -f "$prof/extensions/$id.xpi"
+ done < <(${jq} -r --slurpfile ej "$prof/extensions.json" '
+ .[] | . as $p
+ | [ $p.id, $p.version,
+ ([ $ej[0].addons[] | select(.id == $p.id and .location == "app-profile") | .version ][0] // "") ]
+ | @tsv' ${pins})
+ fi
+ '';
+ }];
+ };
+}
diff --git a/users/ryan/modules/zen/lock.sh b/users/ryan/modules/zen/lock.sh
new file mode 100755
index 0000000..a375fd8
--- /dev/null
+++ b/users/ryan/modules/zen/lock.sh
@@ -0,0 +1,61 @@
+#!/usr/bin/env bash
+# Resolve ryan.zen.extensions into extensions.lock.json via AMO's v5 API.
+#
+# lock.sh <attr> [--missing]
+#
+# <attr> is the evaluated home-manager config, e.g.
+# ~/.config/home-manager#homeConfigurations.ryan.config
+# ~/.config/nix#darwinConfigurations.<host>.config.home-manager.users.ryan
+#
+# Default: re-resolve everything (unpinned entries move to AMO's current version).
+# --missing: keep existing entries that still satisfy the spec; resolve the rest.
+#
+# Only ryan.zen.extensions.*.{id,version} is evaluated, which never touches the
+# lock, so this works when the lock is empty or stale.
+# Needs: nix (>= 2.19 for `nix hash convert`), curl, jq.
+set -euo pipefail
+
+attr="${1:?usage: lock.sh <hm-config-attr> [--missing]}"
+mode="${2:-all}"
+here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+lockfile="$here/extensions.lock.json"
+api="https://addons.mozilla.org/api/v5/addons/addon"
+
+spec="$(nix eval --json "$attr.ryan.zen.extensions" \
+ --apply 'es: builtins.mapAttrs (_: e: { inherit (e) id version; }) (builtins.removeAttrs es (builtins.filter (n: !es.${n}.enable) (builtins.attrNames es)))')"
+old="$(cat "$lockfile" 2>/dev/null || echo '{}')"
+new='{}'
+
+uri() { jq -rn --arg s "$1" '$s|@uri'; }
+
+for name in $(jq -r 'keys[]' <<<"$spec"); do
+ id="$(jq -r --arg n "$name" '.[$n].id' <<<"$spec")"
+ want="$(jq -r --arg n "$name" '.[$n].version // empty' <<<"$spec")"
+
+ if [ "$mode" = "--missing" ]; then
+ keep="$(jq -c --arg n "$name" --arg id "$id" --arg v "$want" \
+ '.[$n] | select(. != null and .id == $id and ($v == "" or .version == $v))' <<<"$old")"
+ if [ -n "$keep" ]; then
+ new="$(jq --arg n "$name" --argjson e "$keep" '.[$n] = $e' <<<"$new")"
+ continue
+ fi
+ fi
+
+ if [ -n "$want" ]; then
+ v="$(curl -fsSL "$api/$(uri "$id")/versions/$(uri "$want")/")"
+ else
+ v="$(curl -fsSL "$api/$(uri "$id")/" | jq '.current_version')"
+ fi
+
+ version="$(jq -r '.version' <<<"$v")"
+ url="$(jq -r '.file.url' <<<"$v")"
+ algo_hex="$(jq -r '.file.hash' <<<"$v")" # "sha256:<hex>"
+ [ "${algo_hex%%:*}" = sha256 ] || { echo "$name: unexpected hash $algo_hex" >&2; exit 1; }
+ hash="$(nix hash convert --hash-algo sha256 --to sri "${algo_hex#sha256:}")"
+
+ echo "$name ($id) -> $version" >&2
+ new="$(jq --arg n "$name" --arg id "$id" --arg ver "$version" --arg url "$url" --arg h "$hash" \
+ '.[$n] = {id: $id, version: $ver, url: $url, hash: $h}' <<<"$new")"
+done
+
+jq -S . <<<"$new" > "$lockfile.tmp" && mv "$lockfile.tmp" "$lockfile"
diff --git a/users/ryan/modules/zen/policies.nix b/users/ryan/modules/zen/policies.nix
new file mode 100644
index 0000000..93a5324
--- /dev/null
+++ b/users/ryan/modules/zen/policies.nix
@@ -0,0 +1,78 @@
+let
+ lock-false = {
+ Value = false;
+ Status = "locked";
+ };
+ lock-true = {
+ Value = true;
+ Status = "locked";
+ };
+in
+{
+ EnableTrackingProtection = {
+ Value = true;
+ Locked = true;
+ Cryptomining = true;
+ Fingerprinting = true;
+ EmailTracking = true;
+ };
+ UserMessaging = {
+ WhatsNew = false;
+ ExtensionRecommendations = false;
+ FeatureRecommendations = false;
+ UrlbarInterventions = false;
+ SkipOnboarding = true;
+ MoreFromMozilla = false;
+ Labs = false;
+ Locked = true;
+ };
+ DisableAppUpdate = true;
+ DisableAccounts = true;
+ DisableFirefoxAccounts = true;
+ DisableFirefoxStudies = true;
+ DisablePocket = true;
+ DisableTelemetry = true;
+ AutofillAddressEnabled = false;
+ AutofillCreditCardEnabled = false;
+ DisableMasterPasswordCreation = true;
+ PasswordManagerEnabled = false;
+ PrimaryPassword = false;
+ OfferToSaveLogins = false;
+ NoDefaultBookmarks = true;
+ OverrideFirstRunPage = "";
+ OverridePostUpdatePage = "";
+ FirefoxHome = {
+ Search = true;
+ TopSites = true;
+ SponsoredTopSites = false;
+ Highlights = false;
+ Pocket = false;
+ SponsoredPocket = false;
+ Snippets = false;
+ Locked = true;
+ };
+ SearchSuggestEnabled = true;
+ FirefoxSuggest = {
+ WebSuggestions = true;
+ SponsoredSuggestions = false;
+ ImproveSuggest = false;
+ Locked = true;
+ };
+ PictureInPicture = lock-true;
+ HardwareAcceleration = true;
+ Certificates = {
+ ImportEnterpriseRoots = true;
+ };
+ Preferences = {
+ "xpinstall.whitelist.required" = lock-true;
+ "dom.webgpu.enabled" = lock-true;
+ "media.eme.enabled" = lock-true;
+ "ui.preferReducedMotion" = lock-false;
+ "general.autoScroll" = lock-true;
+ "general.smoothScroll" = lock-true;
+ "browser.crashReports.unsubmittedCheck.autoSubmit2" = lock-false;
+ "browser.aboutConfig.showWarning" = lock-false;
+ "media.videocontrols.picture-in-picture.enable-when-switching-tabs.enabled" = lock-true;
+ };
+}
+
diff --git a/users/ryan/zen/zenProfile.nix b/users/ryan/modules/zen/profile.nix
similarity index 100%
rename from users/ryan/zen/zenProfile.nix
rename to users/ryan/modules/zen/profile.nix
diff --git a/users/ryan/zen/zenPolicies.nix b/users/ryan/zen/zenPolicies.nix
deleted file mode 100644
index 8d39689..0000000
--- a/users/ryan/zen/zenPolicies.nix
+++ /dev/null
@@ -1,128 +0,0 @@
-let
- lock-false = {
- Value = false;
- Status = "locked";
- };
- lock-true = {
- Value = true;
- Status = "locked";
- };
-in
-{
- EnableTrackingProtection = {
- Value = true;
- Locked = true;
- Cryptomining = true;
- Fingerprinting = true;
- EmailTracking = true;
- };
- UserMessaging = {
- WhatsNew = false;
- ExtensionRecommendations = false;
- FeatureRecommendations = false;
- UrlbarInterventions = false;
- SkipOnboarding = true;
- MoreFromMozilla = false;
- Labs = false;
- Locked = true;
- };
- DisableAppUpdate = true;
- DisableAccounts = true;
- DisableFirefoxAccounts = true;
- DisableFirefoxStudies = true;
- DisablePocket = true;
- DisableTelemetry = true;
- AutofillAddressEnabled = false;
- AutofillCreditCardEnabled = false;
- DisableMasterPasswordCreation = true;
- PasswordManagerEnabled = false;
- PrimaryPassword = false;
- OfferToSaveLogins = false;
- NoDefaultBookmarks = true;
- OverrideFirstRunPage = "";
- OverridePostUpdatePage = "";
- FirefoxHome = {
- Search = true;
- TopSites = true;
- SponsoredTopSites = false;
- Highlights = false;
- Pocket = false;
- SponsoredPocket = false;
- Snippets = false;
- Locked = true;
- };
- SearchSuggestEnabled = true;
- FirefoxSuggest = {
- WebSuggestions = true;
- SponsoredSuggestions = false;
- ImproveSuggest = false;
- Locked = true;
- };
- PictureInPicture = lock-true;
- HardwareAcceleration = true;
- Certificates = {
- ImportEnterpriseRoots = true;
- };
- ExtensionSettings = {
- #"*".installation_mode = "blocked";
- # uBlock Origin
- "uBlock0@raymondhill.net" = {
- install_url = "https://addons.mozilla.org/firefox/downloads/latest/ublock-origin/latest.xpi";
- installation_mode = "force_installed";
- private_browsing = true;
- };
- # Bitwarden
- "{446900e4-71c2-419f-a6a7-df9c091e268b}" = {
- install_url = "https://addons.mozilla.org/firefox/downloads/latest/bitwarden-password-manager/latest.xpi";
- installation_mode = "normal_installed";
- private_browsing = true;
- };
- # SponsorBlock
- "sponsorBlocker@ajay.app" = {
- install_url = "https://addons.mozilla.org/firefox/downloads/latest/sponsorblock/latest.xpi";
- installation_mode = "force_installed";
- };
- # DeArrow
- "deArrow@ajay.app" = {
- install_url = "https://addons.mozilla.org/firefox/downloads/latest/dearrow/latest.xpi";
- installation_mode = "force_installed";
- };
- # Return Youtube Dislike
- "{762f9885-5a13-4abd-9c77-433dcd38b8fd}" = {
- install_url = "https://addons.mozilla.org/firefox/downloads/latest/return-youtube-dislikes/latest.xpi";
- installation_mode = "force_installed";
- };
- # Youtube Nonstop
- "{0d7cafdd-501c-49ca-8ebb-e3341caaa55e}" = {
- install_url = "https://addons.mozilla.org/firefox/downloads/latest/youtube-nonstop/latest.xpi";
- installation_mode = "force_installed";
- };
- # TamperMonkey
- "firefox@tampermonkey.net" = {
- install_url = "https://addons.mozilla.org/firefox/downloads/latest/tampermonkey/latest.xpi";
- installation_mode = "force_installed";
- };
- # Floccus
- "floccus@handmadeideas.org" = {
- install_url = "https://addons.mozilla.org/firefox/downloads/latest/floccus/latest.xpi";
- installation_mode = "force_installed";
- };
- # Mailvelope
- "jid1-AQqSMBYb0a8ADg@jetpack" = {
- install_url = "https://addons.mozilla.org/firefox/downloads/latest/mailvelope/latest.xpi";
- installation_mode = "force_installed";
- };
- };
- Preferences = {
- "xpinstall.whitelist.required" = lock-true;
- "dom.webgpu.enabled" = lock-true;
- "media.eme.enabled" = lock-true;
- "ui.preferReducedMotion" = lock-false;
- "general.autoScroll" = lock-true;
- "general.smoothScroll" = lock-true;
- "browser.crashReports.unsubmittedCheck.autoSubmit2" = lock-false;
- "browser.aboutConfig.showWarning" = lock-false;
- "media.videocontrols.picture-in-picture.enable-when-switching-tabs.enabled" = lock-true;
- };
-}
-