Compare commits
26 Commits
v1
...
86802f244e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
86802f244e | ||
|
|
f3325385c1 | ||
|
|
aed41793f8 | ||
|
|
685126ab47 | ||
|
|
c8d0b09044 | ||
|
|
c2f90c7d0a | ||
|
|
5747dc9839 | ||
|
|
d1344712b8 | ||
|
|
37204f5f45 | ||
| 97b5944dc1 | |||
| 0c483f9512 | |||
| dfb57c2e34 | |||
| 995810927d | |||
| e1cf190dd2 | |||
|
|
70334e68bb | ||
|
|
6a4af69b0f | ||
|
|
d9466d6555 | ||
|
|
0e42763aea | ||
|
|
a6d6860054 | ||
|
|
5c43a93285 | ||
|
|
09c308b93c | ||
|
|
bdf20f2d8e | ||
|
|
c57d26864e | ||
|
|
019fdfc8bb | ||
|
|
c2281dbc61 | ||
|
|
b4fe52261b |
40
README.md
@@ -182,6 +182,13 @@ behaviour (input/misc/monitor/chrome) is `mkDefault` so a plain `home.nix`
|
||||
assignment wins, and bind/exec-once lists concatenate. Full guide with
|
||||
examples: **[docs/OVERRIDES.md](docs/OVERRIDES.md)**.
|
||||
|
||||
**Where each option goes.** `nomarchy.system.*`, `nomarchy.hardware.*`, and
|
||||
`nomarchy.services.*` are NixOS options — set them in `system.nix`. Everything
|
||||
else under `nomarchy.*` is a Home Manager option — set it in `home.nix`. The
|
||||
two tables below are split along exactly that line.
|
||||
|
||||
**`home.nix`** (Home Manager — the desktop):
|
||||
|
||||
| Option | Default | Purpose |
|
||||
|---|---|---|
|
||||
| `nomarchy.stateFile` | — (required) | Path to your theme-state.json |
|
||||
@@ -190,7 +197,8 @@ examples: **[docs/OVERRIDES.md](docs/OVERRIDES.md)**.
|
||||
| `nomarchy.keyboard.variant` | `""` | XKB variant for the session |
|
||||
| `nomarchy.keyboard.devices` | `{}` | Per-device layout overrides (Hyprland `device` blocks keyed by `hyprctl devices` name) — e.g. an external keyboard with its own layout/variant |
|
||||
| `nomarchy.keyboard.layouts` | `[]` | Extra candidate layouts; when set, a watcher prompts (rofi) for a layout on a newly-connected keyboard and remembers it per-device |
|
||||
| `nomarchy.nightlight.enable` | `false` | Scheduled blue-light filter (hyprsunset) — warm at night (`.temperature`, default 4000K) between `.sunset`/`.sunrise`, no shift by day |
|
||||
| `nomarchy.nightlight.enable` | `false` | Scheduled blue-light filter (hyprsunset) — warm at night (`.temperature`, default 4000K) between `.sunset`/`.sunrise`, no shift by day; off by default — enable it from the menu (System › Night light; the first enable rebuilds), then toggle on/off instantly from the menu or the Waybar moon indicator (writes `settings.nightlight.on` in your flake, no rebuild) and it survives reboot, so it stays reproducible |
|
||||
| `nomarchy.updates.enable` | `false` | Passive update awareness: a background check (`.interval`, default daily) that flags when flake inputs (nixpkgs, the Nomarchy input, …) are behind upstream — and, with Flatpak on, when apps have updates (`.flatpak`) — via a Waybar indicator + notification. Never changes anything; click the indicator to run the upgrade flow |
|
||||
| `nomarchy.hyprland.enable` | `true` | Nomarchy's Hyprland config |
|
||||
| `nomarchy.waybar.enable` | `true` | Nomarchy's Waybar |
|
||||
| `nomarchy.rofi.enable` | `true` | Themed rofi launcher + `nomarchy-menu` dispatcher |
|
||||
@@ -207,6 +215,11 @@ examples: **[docs/OVERRIDES.md](docs/OVERRIDES.md)**.
|
||||
| `nomarchy.displays.enable` | `true` | nwg-displays interactive monitor arranger (helper for `nomarchy.monitors`) |
|
||||
| `nomarchy.monitors` | `[]` | Declarative per-output layout → Hyprland `monitor` rules (applied on hotplug); `,preferred,auto,1` wildcard kept as fallback |
|
||||
| `nomarchy.themesDir` | Nomarchy's `themes/` | Where per-theme app overrides are probed |
|
||||
|
||||
**`system.nix`** (NixOS — the machine):
|
||||
|
||||
| Option | Default | Purpose |
|
||||
|---|---|---|
|
||||
| `nomarchy.system.plymouth.enable` | `true` | Branded boot splash, background from the theme JSON (recolors on system rebuilds) |
|
||||
| `nomarchy.system.fileManager.enable` | `true` | Thunar GUI + gvfs/tumbler/udisks2 (the "open folder" handler) |
|
||||
| `nomarchy.system.greeter.enable` | `true` | greetd/tuigreet |
|
||||
@@ -219,6 +232,14 @@ examples: **[docs/OVERRIDES.md](docs/OVERRIDES.md)**.
|
||||
| `nomarchy.system.power.laptop` | `false` | Marks a laptop, gating battery-only features; the installer sets it when a battery is present |
|
||||
| `nomarchy.system.power.thermal.enable` | `false` | thermald (Intel-only); the installer enables it on a GenuineIntel CPU |
|
||||
| `nomarchy.system.power.batteryChargeLimit` | `null` | Stop charging at this % (e.g. `80`) where the hardware supports it; needs `power.laptop` |
|
||||
| `nomarchy.hardware.intel.enable` | `false` | Intel enablement above nixos-hardware (GuC/HuC via `i915.enable_guc=3`); the installer sets it on an Intel CPU/GPU |
|
||||
| `nomarchy.hardware.intel.computeRuntime` | `false` | Opt-in: Intel GPU compute — OpenCL/Level-Zero (`intel-compute-runtime`) + oneVPL (`vpl-gpu-rt`) |
|
||||
| `nomarchy.hardware.amd.enable` | `false` | AMD enablement above nixos-hardware (amd-pstate EPP + radeonsi VA-API); installer-set on an AMD CPU/GPU |
|
||||
| `nomarchy.hardware.amd.rocm.enable` | `false` | Opt-in: ROCm HIP/OpenCL GPU compute (multi-GB); pair with `.gfxOverride` (e.g. `"11.0.0"`) for an unlisted iGPU |
|
||||
| `nomarchy.hardware.fingerprint.enable` | `false` | fprintd for a detected fingerprint reader (installer-set); enroll with `fprintd-enroll` |
|
||||
| `nomarchy.hardware.fingerprint.pam` | `false` | Opt-in: use the fingerprint for login + sudo (PAM) |
|
||||
| `nomarchy.hardware.npu.enable` | `false` | Opt-in/experimental: load the on-die NPU driver (`amdxdna`/`intel_vpu`); userspace runtime is BYO |
|
||||
| `nomarchy.hardware.latestKernel` | `false` | Opt-in: ship `linuxPackages_latest` instead of the default kernel — for very new hardware whose drivers landed recently |
|
||||
| `nomarchy.services.tailscale.enable` | `false` | Opt-in: Tailscale mesh VPN (then `sudo tailscale up`) |
|
||||
| `nomarchy.services.syncthing.enable` | `false` | Opt-in: Syncthing file sync as the login user (GUI at `127.0.0.1:8384`) |
|
||||
| `nomarchy.services.podman.enable` | `false` | Opt-in: rootless Podman (`docker` aliased to it) |
|
||||
@@ -271,6 +292,23 @@ imperative; nothing in Nix consumes the path): applied at session start and
|
||||
after every switch via a tiny activation hook, cycled instantly with
|
||||
`bg next`.
|
||||
|
||||
### Config the menu writes (not just themes)
|
||||
|
||||
The in-flake-state model isn't only for appearance. **Feature toggles you flip
|
||||
from the menu are written into a `settings.*` section of the *same* state file**
|
||||
— git-tracked, reproducible, never stashed in `~/.local/state`. The menu is just
|
||||
an ergonomic writer for your flake, so version-controlling your downstream
|
||||
reproduces the machine, settings and all. Where a toggle can take effect without
|
||||
a rebuild it does: the menu writes the key (`--no-switch`) and flips the running
|
||||
service, which reads the *live* flake state at session start, so the choice is
|
||||
both instant and survives reboot.
|
||||
|
||||
**Night light** is the first to use this — enable it from the menu (System ›
|
||||
Night light; the first enable rebuilds to install hyprsunset), then on/off is
|
||||
instant. Internally it's two keys: `settings.nightlight.installed` (sticky —
|
||||
gates the unit, the first enable rebuilds) and `settings.nightlight.on` (the
|
||||
instant on/off). Expect more `nomarchy.*` toggles to migrate to this pattern.
|
||||
|
||||
### Per-theme app assets (`themes/<slug>/`)
|
||||
|
||||
Recoloring covers 95% of theming; the rest is one optional assets directory
|
||||
|
||||
311
docs/ROADMAP.md
@@ -51,6 +51,11 @@ how to override it. Items marked ✓ are shipped.
|
||||
one-off — any new feature that earns a menu entry must be placed in the
|
||||
right submenu (don't let the root creep back to a flat list), with its
|
||||
direct `SUPER+CTRL+<mnemonic>` bind and self-gating as applicable.
|
||||
- ✓ **Back everywhere:** every list menu now ends with a `↩ Back` entry that
|
||||
returns one level up (power/theme/power-profile/clipboard/files/capture,
|
||||
not just Tools/System), so you never have to Esc out and reopen. A `back`
|
||||
helper + a shared `BACK` label keep it uniform; matched exactly so it can't
|
||||
collide with clipboard/filename content. Esc still quits instantly.
|
||||
- ✓ **Menu modules from rofi plugins:** the old `calc` flow committed the
|
||||
expression blind (result only in the *next* menu's `-mesg`) and `qalc -t`
|
||||
misparsed common phrasings (`15% of 200` → `rem(15, 1 B)`, the natural-
|
||||
@@ -67,25 +72,130 @@ how to override it. Items marked ✓ are shipped.
|
||||
launcher, and yazi (SUPER+E) already covers real browsing.
|
||||
- **More menu modules from rofi tools:** the script-based counterparts
|
||||
(run via `rofi -dmenu`, like the hand-rolled modules), each a deliberate
|
||||
replacement of an existing flow: **rofi-network-manager** (a keyboard
|
||||
wifi/VPN picker vs today's `nmtui`-in-terminal `network`), **rofi-rbw /
|
||||
rofi-pass** (a secrets module — Bitwarden via rbw, or `pass` — pairs with
|
||||
`keys.nix`), and **rofi-pulse-select** (an audio sink/source switcher).
|
||||
Decide per-module whether it earns replacing the current path.
|
||||
replacement of an existing flow.
|
||||
- ✓ **Network** (`networkmanager_dmenu`): a native rofi wifi/VPN picker
|
||||
replacing the old `nmtui`-in-terminal `network` flow. Configured (xdg
|
||||
`networkmanager-dmenu/config.ini`) to drive rofi with `rofi_highlight`
|
||||
for connected/available rows; editing a connection drops to nmtui in the
|
||||
terminal. Inherits the generated theme like every other module.
|
||||
- ✓ **Audio** (`rofi-pulse-select`): a PipeWire/pulse sink/source switcher
|
||||
under System → Audio (Output/Input), self-gated on the pulse socket.
|
||||
- Deferred: **rofi-rbw / rofi-pass** secrets module — no Bitwarden/`pass`
|
||||
setup here today (secrets are gpg/ssh + gnome-keyring, see `keys.nix`),
|
||||
so it'd mean adopting a new secret manager. Revisit if that changes.
|
||||
- Also: menu search is now **case-insensitive fuzzy** (`matching = fuzzy`,
|
||||
`sorting-method = fzf`) across every module + the launcher.
|
||||
- ✓ **Printer setup menu module:** a `nomarchy-menu printers` entry in the
|
||||
**System** submenu opens **system-config-printer** (the CUPS admin GUI —
|
||||
discovery, drivers/PPDs, options, test page), mirroring the bluetooth/blueman
|
||||
pattern: the printing service ships the package (`environment.systemPackages`)
|
||||
and the menu execs it, self-gated on the `system-config-printer` binary so
|
||||
the entry appears only when `nomarchy.services.printing` is on. No direct
|
||||
`SUPER+CTRL` bind — printer setup is a rare one-off, not a frequent utility
|
||||
(left out deliberately; easy to add). Chose the GUI over a rofi-native
|
||||
`lpadmin` flow (driver/PPD picking is impractical in rofi) and the CUPS web
|
||||
UI (an unthemed browser page). Validated: the generated menu script's
|
||||
`bash -n` build check passes, and an eval confirms printing-on puts
|
||||
system-config-printer in `systemPackages`. Pending an on-machine check.
|
||||
- **VPN setup & management menu:** a dedicated **System → VPN** flow that goes
|
||||
past today's Network module (`networkmanager_dmenu` only connects/disconnects
|
||||
*existing* VPN connections) to a guided **setup + management** surface across
|
||||
the three common kinds:
|
||||
- **WireGuard:** import a `.conf` into NetworkManager (`nmcli connection import
|
||||
type wireguard file …` — NM handles wg tunnels natively, no extra plugin)
|
||||
and toggle it up/down.
|
||||
- **OpenVPN:** import an `.ovpn` (`nmcli connection import type openvpn file …`),
|
||||
which needs the `networkmanager-openvpn` plugin shipped — gate the import
|
||||
entry on it (the wireguard path has no such dependency).
|
||||
- **Tailscale:** `tailscale up/down`, status, and **exit-node** selection — it
|
||||
lives outside NetworkManager, so the menu drives the `tailscale` CLI
|
||||
directly, self-gated on `nomarchy.services.tailscale` (already an opt-in
|
||||
service) like the other hardware-gated System entries.
|
||||
Shape: a `nomarchy-menu vpn` rofi submenu under **System** (Connect/Disconnect
|
||||
with each connection's live up/down state like the wifi picker · Import config
|
||||
via the existing Files/`fd` picker pattern · the Tailscale block when present),
|
||||
ending in `↩ Back`. A self-gating Waybar `custom/vpn` indicator (shield glyph
|
||||
while a tunnel or Tailscale is up) for parity with the other status modules,
|
||||
added to the summer whole-swaps too. Secrets stay in the connection manager's
|
||||
own store (NetworkManager / Tailscale state) — no new secret manager (cf. the
|
||||
deferred rofi-rbw/pass note above). Open question: how much in-menu *creation*
|
||||
(vs import) to support — a from-scratch WireGuard keypair/peer editor is a lot
|
||||
of rofi surface, so likely import-first, deferring creation to
|
||||
`nm-connection-editor`. Complements the existing Network item rather than
|
||||
replacing it.
|
||||
- **Theme parity with legacy:** summer-day/night now carry their legacy
|
||||
bar layouts as `waybar.jsonc` whole-swaps (adapted: dead legacy script
|
||||
modules dropped, Nerd-Fonts-v2 codepoints remapped to FontAwesome/v3,
|
||||
logo button opens nomarchy-menu); the other four identity themes are
|
||||
palette recolors and already match. Remaining: a visual pass over all
|
||||
six on the live ISO
|
||||
- **Per-theme rofi identity:** the `themes/<slug>/rofi.rasi` whole-swap
|
||||
ships, and summer-day/night carry their legacy designs (inverted window,
|
||||
green inputbar, yellow bottom-border). Remaining: author `.rasi`
|
||||
identities for the other four ported themes if/when they want one (the
|
||||
generated palette theme is the default and looks fine)
|
||||
- **Faster switches:** move `backgrounds/` out of the flake source (the 86 MB
|
||||
re-copy on every state write is the main eval tax), then pre-built theme
|
||||
variants if still needed
|
||||
- ✓ **Per-theme rofi identity:** the `themes/<slug>/rofi.rasi` whole-swap
|
||||
ships, and all six identity themes now carry a designed `.rasi`. summer-day/
|
||||
night keep their legacy ports (inverted window, green inputbar, yellow
|
||||
bottom-border); the other four were authored from each theme's character
|
||||
(no legacy layout to port — their waybar whole-swaps are palette-only):
|
||||
**nord** a soft rounded "frost panel" (frost border, brighter-frost
|
||||
selection, aurora-purple prompt); **retro-82** a sharp CRT terminal (square
|
||||
corners, amber-on-navy, a teal scanline underline, mono); **lumon** a
|
||||
clinical cyan "bezel" (thick cyan frame, a *framed*-not-filled readout
|
||||
inputbar, mono); **kanagawa** ink-and-paper (warm washi-paper frame, a
|
||||
deeper ink-well inputbar, crystal-blue wave reserved for the selection).
|
||||
Each is self-contained (it replaces the generated theme) and keeps the
|
||||
element structure the theme-grid picker's per-invocation `-theme-str`
|
||||
layers onto; all four parse clean under `rofi -dump-theme`. The generated
|
||||
palette theme stays the default for the other 15 presets. Remaining: a
|
||||
visual pass over the four on hardware (the parse check confirms syntax, not
|
||||
aesthetics).
|
||||
- ✓ **Visual theme picker (preview thumbnails):** `nomarchy-menu theme` is now
|
||||
a rofi **icon grid of real desktop previews** instead of a plain-text list —
|
||||
each theme a screenshot of its themed desktop (waybar + floating terminal),
|
||||
pretty name beneath, **grouped dark-first then light** in one scrollable grid
|
||||
(the previews make the mode obvious, so no light/dark submenu split), the
|
||||
active theme marked `✓`, ending in `↩ Back`. The grid, the Name→slug map and
|
||||
the active mark are all **generated at eval time** from the preset JSONs in
|
||||
`rofi.nix` (`builtins.readDir` + `fromJSON`); "active" is just `t.slug`, since
|
||||
every switch rebuilds the menu. Grid layout via a per-invocation `-theme-str`
|
||||
(`listview { columns: 3; flow: horizontal; }` so Down scrolls row-by-row, +
|
||||
vertical `element` cards, name centred below). **Sizing gotcha (learned the
|
||||
hard way):** rofi's `element-icon` `size` is a **single value → a square
|
||||
cell** (a two-value `WxH` is silently collapsed), and the icon is *contained*
|
||||
in that square. So a 16:9 preview letterboxes (theme-coloured bands top/
|
||||
bottom), and a cell can't be shorter-than-square without the bands returning.
|
||||
The fix: a build-time imagemagick step **centre-crops each preview to a
|
||||
square** so it fills the cell edge-to-edge; the window width is derived so a
|
||||
column is exactly the icon side (no slack margins). One knob, `themeGridIconW`
|
||||
(240px), drives both icon and window. **Workflow:** Bernardo captures previews
|
||||
on real hardware and commits them as `themes/<slug>/preview.png`, **already
|
||||
downscaled to 480×270** (~2.4 MB total for all 21, vs ~28 MB full-res) — the
|
||||
source stays 16:9 and untouched; only the *displayed* thumb is squared at
|
||||
build, so the crop is reversible. Graceful fallback when a theme has no
|
||||
`preview.png`: a plain-name row, so it degrades cleanly.
|
||||
(A headless VM-render route — `runNixOSTest` + software-GL Hyprland
|
||||
(`LIBGL_ALWAYS_SOFTWARE` on virtio-gpu) + `machine.screenshot()` QMP
|
||||
framebuffer dump — was prototyped 2026-06-19 and **works** (themed waybar
|
||||
rendered + captured); real-hardware capture was chosen for fidelity +
|
||||
simplicity, with the VM route as a documented fallback if hand-capturing all
|
||||
themes gets tedious.)
|
||||
- **Faster switches:** move `backgrounds/` out of the flake source. Diagnosis
|
||||
(confirmed): `themes/` is 86 MB and `backgrounds/` is **all** of it — the
|
||||
palette JSONs + per-theme overrides are only ~208 KB, and the wallpapers are
|
||||
**never read at Nix eval** (only the Python `swww` path uses them). But
|
||||
`theme-state.json` is git-tracked, so every `apply` rewrites it → the flake
|
||||
tree changes → Nix re-copies the whole 86 MB source before `home-manager
|
||||
switch` can evaluate. You pay an 86 MB copy to change a 1 KB file.
|
||||
**Decided approach (deferred — don't want the extra moving part yet):**
|
||||
Option 1, a **separate pinned wallpapers artifact** — a `Nomarchy-wallpapers`
|
||||
repo or release tarball, pulled once via a flake input / `fetchurl` (pinned
|
||||
by hash → content-addressed, never re-copied on a state write), with
|
||||
`nomarchy-theme-sync` reading wallpapers from that stable store path (the
|
||||
`NOMARCHY_DEFAULT_THEMES` env hook already anticipates external theme
|
||||
assets). Keeps eval **pure**; the live ISO still bakes them in (fetched at
|
||||
build). A state write then re-copies only ~208 KB. Rejected alternative:
|
||||
moving `theme-state.json` out to `~/.config` + `--impure` eval (one repo, no
|
||||
second artifact, but trades away the in-tree-pinned-state reproducibility).
|
||||
Follow-on if `home-manager switch` itself is still the bottleneck after the
|
||||
copy is gone: **pre-built theme variants** (build each theme's generation
|
||||
ahead of time so a switch just activates a cached one).
|
||||
- Greeter (tuigreet/SDDM) theming from the same JSON (Plymouth ships since
|
||||
v1: `nomarchy.system.plymouth.*`, background tinted from the state file)
|
||||
- Installer round 2: multi-disk BTRFS RAID, impermanence, BIOS/legacy
|
||||
@@ -263,6 +373,31 @@ how to override it. Items marked ✓ are shipped.
|
||||
Complements nixos-hardware (model quirks) rather than replacing it; keep
|
||||
the detection in the installer's `hardware-db` so an installed machine
|
||||
bakes the right defaults, all overridable through `nomarchy.hardware.*`.
|
||||
- ✓ **Mechanism + broad seed shipped** (`modules/nixos/hardware.nix`): a
|
||||
vendor-keyed `nomarchy.hardware.*` surface (`intel`, `amd`, `fingerprint`,
|
||||
`npu`), `hardware-db.sh` extended to detect Intel/AMD, a fingerprint reader
|
||||
(libfprint USB vendor IDs) and an NPU (Intel VPU / AMD XDNA PCI IDs), and
|
||||
the installer bakes the matching toggles into `system.nix` — safe defaults
|
||||
active (GuC/HuC, amd-pstate, AMD VA-API env, fprintd), heavy/experimental
|
||||
opt-ins commented (Intel compute-runtime, ROCm, fingerprint PAM, NPU
|
||||
driver). Audited against the nixos-hardware commons so it only fills the
|
||||
gap above them — microcode / the media-driver VA-API stack / weekly fstrim
|
||||
already come from `common-cpu-*` / `common-gpu-*` / `common-pc-ssd`.
|
||||
Exercised live on the dev machine's detection (an AMD Ryzen-AI laptop: AMD
|
||||
+ fingerprint + NPU all detected) and a toggles-on build (`amd_pstate=active`
|
||||
+ `i915.enable_guc=3` in kernel-params, `fprintd.service` present).
|
||||
**Remaining: on-hardware verification of the runtime bits (AMD/NPU/Intel
|
||||
GPU-compute) — none are testable in CI or on the Intel Latitudes for the
|
||||
AMD paths.** SSD TRIM is intentionally left to `common-pc-ssd`.
|
||||
- ✓ **Hardening (driver-gen + kernel awareness):** `intel.guc` emits the
|
||||
*i915* param, so the installer turns it off when the GPU is on the newer
|
||||
`xe` driver (Lunar Lake / Battlemage / Panther Lake — GuC is default-on
|
||||
there); the NPU detector matches the PCI *accelerator class* (not just a
|
||||
device-ID list) so new gens are caught without a code change; and a
|
||||
`nomarchy.hardware.latestKernel` escape hatch (+ a build-time warning when
|
||||
`npu.enable` predates the shipped kernel) covers very-new hardware whose
|
||||
drivers only just landed. A config-assertion VM test guards the wiring
|
||||
(`checks.hardware-toggles`: kernel cmdline + fprintd + PAM, booted in a VM).
|
||||
Worked example — ThinkPad T14s (Ryzen 7 PRO 7840U + Radeon 780M):
|
||||
- **GPU compute — ROCm:** the 780M is an RDNA3 iGPU (gfx1103) ROCm doesn't
|
||||
officially list, so it needs `HSA_OVERRIDE_GFX_VERSION=11.0.0`. Multi-GB
|
||||
@@ -353,10 +488,60 @@ how to override it. Items marked ✓ are shipped.
|
||||
warm at night, identity (no shift) by day. `modules/home/nightlight.nix`
|
||||
drives the HM `services.hyprsunset` with two time-based profiles
|
||||
(`sunrise` → identity, `sunset` → `temperature`), so hyprsunset handles the
|
||||
schedule and the on-login state. Needs an on-hardware check that hyprsunset
|
||||
applies the active profile at session start. Remaining (optional): a menu +
|
||||
Waybar toggle to force it on/off, and geo (lat/long) auto sunset/sunrise
|
||||
(would mean wlsunset, which schedules by location).
|
||||
schedule and the on-login state. Active-profile-at-session-start was
|
||||
confirmed on hardware (2026-06-18). ✓ **Menu + Waybar toggle (opt-in,
|
||||
instant, in-flake state):** two git-tracked keys in the state file, both
|
||||
menu-written (exposed via the new `nomarchy.settings` option) —
|
||||
`settings.nightlight.installed` (**sticky**: gates the hyprsunset unit;
|
||||
`nomarchy.nightlight.enable` `mkDefault`-reads it) and
|
||||
`settings.nightlight.on` (runtime on/off). **Off by default.** The *first*
|
||||
enable from the menu writes `installed` and rebuilds to create the unit (the
|
||||
one accepted rebuild); **every toggle after is instant** —
|
||||
`nomarchy-theme-sync set settings.nightlight.on … --no-switch` (atomic +
|
||||
`git add -N`, no rebuild) plus a `systemctl` start/stop. Splitting the sticky
|
||||
flag from the on/off is what avoids a **decay** bug: an instant-off writes
|
||||
only `on`, so an unrelated later rebuild (e.g. `sys-update`) never drops the
|
||||
unit and "on" stays instant. Persistence across logout/reboot comes from an
|
||||
`ExecCondition` on the unit (`nomarchy-nightlight should-start`) that reads the
|
||||
**live** working-tree on/off at start time — *not* the eval-frozen store copy
|
||||
— so an off survives a reboot with no rebuild; a later rebuild bakes the same
|
||||
value. No more `~/.local/state` marker, no marker `ConditionPathExists`. The
|
||||
`nomarchy-menu` System-submenu entry is always shown (current on/off) so the
|
||||
feature can be enabled from the menu; the self-gating Waybar
|
||||
`custom/nightlight` indicator shows the moon while running and hides otherwise.
|
||||
This is the first cut of a broader principle — **any user-settable config
|
||||
gets a menu writer that lands it in the downstream flake; no state lives
|
||||
outside the checkout** (Phase 1 night-light + Phase 2 the per-device
|
||||
keyboard-layout memory below are done — both now in the git-tracked state
|
||||
file; next target: opt-in auto-commit of the flake on each mutation, owned
|
||||
files only). Remaining (optional): geo (lat/long) auto
|
||||
sunset/sunrise (would mean wlsunset, which schedules by location). Pending an
|
||||
on-machine check (first enable rebuilds + comes on; later toggles instant; off
|
||||
persists across reboot via ExecCondition; stopping hyprsunset restores gamma).
|
||||
- ✓ **Automatic timezone (location-following clock):** `nomarchy.system.autoTimezone`
|
||||
(opt-in, off by default) — geoclue + `services.automatic-timezoned` drive
|
||||
`/etc/localtime` from your location, so travelling to another zone updates the
|
||||
Waybar clock on its own. **Menu-driven, in-flake state** (the night-light /
|
||||
keyboard philosophy): the flag is `settings.autoTimezone` in theme-state.json,
|
||||
git-tracked; the System-menu entry (`nomarchy-menu autotimezone` →
|
||||
`nomarchy-autotimezone`) writes it and rebuilds. **Not instant like
|
||||
night-light** — it's a *system* service (not a user unit you can start/stop),
|
||||
so the toggle drives a `sudo nixos-rebuild` (bakes the service + the
|
||||
`time.timeZone` override) plus a `home-manager switch` (the Waybar-refresh
|
||||
watcher), both off the one flag. **time.timeZone handling:** a runtime zone
|
||||
needs `/etc/localtime` writable; automatic-timezoned sets `time.timeZone = null`
|
||||
itself, but the installer's static value would collide (a hard eval error), so
|
||||
the module forces it null (`mkForce`) over the installer's value when enabled,
|
||||
and reverts to it when disabled. **Live clock refresh:** Waybar's clock module
|
||||
captures the zone at construction, so `modules/home/timezone.nix` runs a tiny
|
||||
user service that watches timedate1's change signal and reloads Waybar
|
||||
(SIGUSR2) on a real zone change (also catches a manual `timedatectl
|
||||
set-timezone`). Eval-verified both ways (on: geoclue+daemon on, tz forced null
|
||||
over a static installer value, watcher present; off: static tz intact, no
|
||||
service/watcher). **Pending an on-hardware check** — geoclue detection and the
|
||||
SIGUSR2 clock refresh aren't testable in CI (if SIGUSR2 proves insufficient,
|
||||
fall back to restarting waybar). Remaining (optional): a Waybar tooltip line
|
||||
showing the detected zone.
|
||||
- **Keyboard layouts (per-device + switching):**
|
||||
- ✓ **Per-device declarative layout:** `nomarchy.keyboard.devices`
|
||||
(`{ "<hyprctl-device-name>" = { layout; variant; }; }`) generates Hyprland
|
||||
@@ -372,15 +557,29 @@ how to override it. Items marked ✓ are shipped.
|
||||
watch` daemon runs (exec-once): it polls `hyprctl devices`, and when a
|
||||
keyboard connects *after* login that isn't in `keyboard.devices` and
|
||||
hasn't been chosen before, it pops a rofi layout picker, applies the
|
||||
choice with `hyprctl switchxkblayout` (an index into the candidate set,
|
||||
which `input.kb_layout` carries), and remembers it per-device in
|
||||
`~/.local/state/nomarchy/keyboard-layouts` — re-applied silently on later
|
||||
reconnects. The boot-time set (incl. the built-in keyboard) is never
|
||||
prompted. The runtime-remember complement to the declarative
|
||||
`keyboard.devices`; a stateful runtime piece by design. **Pending an
|
||||
on-hardware test** (hotplug isn't verifiable in CI). Remaining (bonus):
|
||||
offer to write a choice into `keyboard.devices` so it graduates to the
|
||||
reproducible config.
|
||||
choice as a **per-device** `hyprctl keyword device[<name>]:kb_layout` (the
|
||||
runtime twin of the declarative `device{}` blocks — so it isolates that
|
||||
one keyboard and never touches the built-in board), and remembers it. The
|
||||
boot-time set (incl. the built-in keyboard) is never prompted. The
|
||||
runtime-remember complement to the declarative `keyboard.devices`.
|
||||
**Picker verified on hardware (2026-06-18)**; the first cut applied the
|
||||
choice with `switchxkblayout` (a global layout-index flip) and merged the
|
||||
candidate pool into `input.kb_layout`, so a selection leaked onto the
|
||||
laptop keyboard and stuck after unplug — fixed by the per-device keyword +
|
||||
keeping the pool out of the session layout.
|
||||
- ✓ **In-flake state + graduation (2026-06-23):** the remembered picks moved
|
||||
out of `~/.local/state/nomarchy/keyboard-layouts` into the git-tracked
|
||||
state file (`settings.keyboard.devices`, a device-name → layout map) —
|
||||
Phase 2 of the in-flake-state principle, the same path night-light took.
|
||||
The watcher reads the **live** working tree (a pick is honoured at once)
|
||||
and writes instantly with `--no-switch` (no rebuild), merging the whole map
|
||||
back at the dot-free parent path so a device name containing a dot can't
|
||||
corrupt the dotted set-path. Each remembered device **graduates** into
|
||||
`nomarchy.keyboard.devices` on the next rebuild (a generated Hyprland
|
||||
`device{}` block, `mkDefault` so a hand-written entry still wins), after
|
||||
which the watcher sees it as declared and steps back. Eval-verified
|
||||
(graduation + precedence) and the writer round-trips; **still needs the
|
||||
on-hardware hotplug re-verify** (the picker path isn't testable in CI).
|
||||
- Remaining: a multi-layout cycle bind (`hyprctl switchxkblayout` / xkb
|
||||
`grp:` options) for switching layouts on one keyboard; add the
|
||||
`hyprland/language` module to the summer whole-swap themes for parity
|
||||
@@ -410,15 +609,57 @@ how to override it. Items marked ✓ are shipped.
|
||||
artifact and will crash on hardware too). The menu wiring is correct and
|
||||
the app is installed, but the GUI doesn't open. The snapshot *backend*
|
||||
(the actual snapshots) is unaffected. Fix path: a nixpkgs override aligning
|
||||
its libbtrfsutil, or wait for an upstream bump; meanwhile the menu entry
|
||||
is a no-op when the binary crashes. Fallback if it lingers: a rofi-based
|
||||
snapshot menu (no btrfs-assistant dependency).
|
||||
- Remaining (optional): a keyboard-driven rofi browse for quick glances;
|
||||
boot-from-snapshot needs a systemd-boot equivalent of grub-btrfs.
|
||||
- **Update awareness:** updates are manual today (`sys-update`/`home-update`);
|
||||
add a Waybar indicator / notification when flake inputs are stale or a new
|
||||
nixpkgs rev is available (optionally with a pending-change count). Augments,
|
||||
never replaces, the explicit rebuild flow.
|
||||
its libbtrfsutil, or wait for an upstream bump. ✓ **Fallback shipped:**
|
||||
the menu now launches `nomarchy-snapshots` (system-side, gated on
|
||||
`nomarchy.system.snapper`) instead — a keyboard-driven snapper
|
||||
browser/restore with no btrfs-assistant dependency. snapper is root-only
|
||||
here (no `ALLOW_USERS`) and rofi can't run as root under Wayland, so it
|
||||
runs in a terminal via `sudo` (one password prompt) and uses `fzf` to pick:
|
||||
browse/diff (read-only), restore files (`undochange`), or roll back (root
|
||||
config only) — each behind a typed-`yes` confirmation, which is *safer*
|
||||
than the fat-fingerable one-click rofi rollback this entry originally
|
||||
avoided. btrfs-assistant stays installed for when nixpkgs fixes it.
|
||||
**Pending an on-machine check of the restore/rollback paths.**
|
||||
- Remaining (optional): boot-from-snapshot needs a systemd-boot equivalent
|
||||
of grub-btrfs.
|
||||
- ✓ **Update awareness:** `nomarchy.updates.enable` (opt-in, `modules/home/
|
||||
updates.nix`) — a `nomarchy-updates` checker on a systemd user timer
|
||||
(`.interval`, default daily) compares each **direct** branch-tracking flake
|
||||
input (nixpkgs, the Nomarchy input, home-manager, stylix … via `git
|
||||
ls-remote` vs the locked rev; offline → skipped, no false alarm) and, when
|
||||
the `flatpak` CLI is present (`.flatpak`), counts Flatpak updates. A
|
||||
self-gating Waybar `custom/updates` indicator (hidden until something's
|
||||
available; accent ` N`, signal-refreshed) + a notification that fires only
|
||||
when the count *grows* (so a daily timer never nags). Click → the upgrade
|
||||
flow in a terminal (`sys-update` / `flatpak update`, each confirmed). Purely
|
||||
passive — it never changes anything, augmenting the explicit rebuild flow.
|
||||
Indicator is in the generated bar and both summer whole-swaps. **Pending an
|
||||
on-machine check** (the ls-remote/notify/timer path needs a live session).
|
||||
- **Automated upstream lock bumps (maintainer CI):** the upstream twin of the
|
||||
user-side checker above — automate advancing Nomarchy's *own* `flake.lock`,
|
||||
which is the entire delivery channel (a downstream takes a single input,
|
||||
`?ref=v1`, so nixpkgs/home-manager/stylix/nixos-hardware reach it
|
||||
*transitively pinned* through that lock; users can't bump them independently).
|
||||
**Tiered by what CI can't do:** the hardware-QA gate (the Latitude 5410 + the
|
||||
AMD dev box — AMD/NPU/Intel-GPU-compute runtime bits aren't testable in CI or
|
||||
on the Intel Latitudes for the AMD paths) can't be automated, so a bot drives
|
||||
`main` and a human still promotes `main → v1`. A scheduled job (Forgejo
|
||||
Actions, or Renovate's Nix manager opening a reviewable lock-diff PR) runs
|
||||
`nix flake update` → `nix flake check` + the existing `runNixOSTest` suite +
|
||||
builds `system.build.toplevel`, and on green lands on `main` — which nobody
|
||||
tracks, so a bad bump costs a debug session, not a user outage; `v1` stays
|
||||
the manual on-hardware gate (it only ever fast-forwards, never force-pushed,
|
||||
or a user's locked rev can vanish from history). **Cadence weekly**
|
||||
(release-26.05 doesn't move fast; nightly is just churn), and `nix flake
|
||||
update` stays *within* the pinned release branches — no surprise major bump
|
||||
(a 26.11 jump is a deliberate `v2`, hand-edited in flake.nix). Side benefit:
|
||||
shrinks the security-patch latency the single-input model otherwise imposes
|
||||
(users are gated on the maintainer for nixpkgs CVEs), since a fix is usually
|
||||
already eval/build/VM-tested and one hardware promotion away from `v1` —
|
||||
worth fast-laning lock-only/security bumps ahead of feature batches. The repo
|
||||
has **no CI today** (manual `nix flake update` only). Explicitly *not* a
|
||||
binary cache: compile-from-source is a deliberate values call (Gentoo-style),
|
||||
so this automates the *config/lock* channel, not artifact distribution.
|
||||
- **"nomarchy" control center:** a single TUI/GUI front-end over the common
|
||||
toggles — theme, power profile, opt-in services, display, DND — built on
|
||||
the same `nomarchy-theme-sync` / `nomarchy.*` surface the menu already
|
||||
|
||||
25
flake.nix
@@ -134,6 +134,31 @@
|
||||
downstream.nixosConfigurations.default.config.system.build.toplevel;
|
||||
downstream-template-home =
|
||||
downstream.homeConfigurations.me.activationPackage;
|
||||
|
||||
# Config-level VM assertions for the nomarchy.hardware.* toggles —
|
||||
# what they SET (kernel cmdline, fprintd, PAM). Real hardware
|
||||
# behaviour (firmware/driver/device) needs bare metal and is out of
|
||||
# scope here. Imports only hardware.nix, so the VM stays minimal.
|
||||
hardware-toggles = pkgs.testers.runNixOSTest {
|
||||
name = "nomarchy-hardware-toggles";
|
||||
nodes.machine = { ... }: {
|
||||
imports = [ ./modules/nixos/hardware.nix ];
|
||||
nomarchy.hardware = {
|
||||
intel.enable = true;
|
||||
amd.enable = true;
|
||||
fingerprint = { enable = true; pam = true; };
|
||||
npu.enable = true;
|
||||
};
|
||||
};
|
||||
testScript = ''
|
||||
machine.wait_for_unit("multi-user.target")
|
||||
cmdline = machine.succeed("cat /proc/cmdline")
|
||||
assert "amd_pstate=active" in cmdline, cmdline
|
||||
assert "i915.enable_guc=3" in cmdline, cmdline
|
||||
machine.succeed("systemctl cat fprintd.service")
|
||||
machine.succeed("grep -q pam_fprintd /etc/pam.d/sudo")
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
# ─── Reference host ────────────────────────────────────────────
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Nomarchy — Home Manager entry point.
|
||||
# Consume this via homeModules.nomarchy (flake.nix), which also pulls in
|
||||
# the stylix home module that stylix.nix configures.
|
||||
{ config, pkgs, ... }:
|
||||
{ config, lib, pkgs, ... }:
|
||||
|
||||
{
|
||||
imports = [
|
||||
@@ -18,6 +18,8 @@
|
||||
./yazi.nix # flagship TUI file manager, themed + plugins
|
||||
./osd.nix # swayosd volume/brightness OSD, themed
|
||||
./nightlight.nix # scheduled blue-light filter (hyprsunset), opt-in
|
||||
./timezone.nix # keep the Waybar clock in step with auto-timezone changes
|
||||
./updates.nix # passive update-awareness indicator + notification, opt-in
|
||||
./shell.nix # zsh + starship + bat/eza/zoxide, themed
|
||||
./keys.nix # gpg-agent (fronts SSH) + pinentry-qt
|
||||
./fastfetch.nix # system info with the themed Nomarchy logo
|
||||
@@ -32,6 +34,16 @@
|
||||
services.network-manager-applet.enable = true;
|
||||
xsession.preferStatusNotifierItems = true;
|
||||
|
||||
# Standard XDG user directories (Downloads, Documents, Pictures, Music,
|
||||
# Videos, Desktop, Public, Templates): written to user-dirs.dirs so file
|
||||
# pickers/browsers resolve them, and created on activation so a fresh
|
||||
# install lands with them present (not just on first app use). mkDefault
|
||||
# so a downstream home.nix can flip it off or remap individual paths.
|
||||
xdg.userDirs = {
|
||||
enable = lib.mkDefault true;
|
||||
createDirectories = lib.mkDefault true;
|
||||
};
|
||||
|
||||
home.stateVersion = "26.05";
|
||||
|
||||
home.packages = with pkgs; [
|
||||
|
||||
@@ -8,6 +8,10 @@ let
|
||||
c = t.colors;
|
||||
inherit (config.nomarchy.lib) rgb rgba;
|
||||
|
||||
# nomarchy-theme-sync — the in-flake state writer the keyboard watcher uses
|
||||
# to remember per-device layouts (same path night-light's toggle takes).
|
||||
sync = lib.getExe config.nomarchy.package;
|
||||
|
||||
# swayosd's `--input-volume mute-toggle` draws the OSD but never flips the
|
||||
# source mute (verified on hardware: wpctl toggles the state, swayosd's
|
||||
# input-mute path is a no-op — unlike its working output-mute path). Do the
|
||||
@@ -55,10 +59,14 @@ let
|
||||
++ lib.optional (m.extra != "") m.extra
|
||||
);
|
||||
|
||||
# Keyboard layout candidates: the primary nomarchy.keyboard.layout (split on
|
||||
# commas) plus any nomarchy.keyboard.layouts. Drives input.kb_layout so all
|
||||
# of them are switchable, and feeds the watcher's picker.
|
||||
kbLayouts = lib.unique (
|
||||
# Candidate layouts offered by the new-keyboard picker: the session
|
||||
# layout(s) (nomarchy.keyboard.layout, comma-split for a multi-layout
|
||||
# session) plus the extra nomarchy.keyboard.layouts pool. The pool is
|
||||
# deliberately NOT merged into input.kb_layout — those candidates are for
|
||||
# *external* keyboards and get applied per-device by the watcher's apply().
|
||||
# Loading them onto the session keyboard is what let a global switch flip
|
||||
# the built-in board to the wrong layout.
|
||||
pickerLayouts = lib.unique (
|
||||
(lib.splitString "," config.nomarchy.keyboard.layout)
|
||||
++ config.nomarchy.keyboard.layouts
|
||||
);
|
||||
@@ -68,21 +76,41 @@ let
|
||||
# keyboard.devices, not already remembered), ask for a layout and persist it
|
||||
# per-device, re-applying silently on later reconnects. Stateful runtime
|
||||
# piece — the complement to the declarative keyboard.devices. Reliable
|
||||
# primitives only: poll hyprctl devices, apply via switchxkblayout (an index
|
||||
# into kbLayouts). NOTE: needs an on-hardware test (hotplug isn't verifiable
|
||||
# in CI).
|
||||
# primitives only: poll hyprctl devices, apply with a per-device
|
||||
# `device[<name>]:kb_layout` keyword — the runtime twin of the declarative
|
||||
# device blocks, so it isolates that one keyboard and never disturbs the
|
||||
# built-in board (switchxkblayout flipped the *shared* layout, which leaked
|
||||
# onto the laptop and stuck after unplug).
|
||||
#
|
||||
# The remembered choices live in the git-tracked in-flake state
|
||||
# (settings.keyboard.devices), NOT ~/.local/state — read live from the
|
||||
# working tree (so a pick this session is honoured at once) and written
|
||||
# instantly with `--no-switch` (no rebuild). A later rebuild graduates them
|
||||
# into nomarchy.keyboard.devices (generated device{} blocks), after which the
|
||||
# watcher treats them as declared and steps back. NOTE: needs an on-hardware
|
||||
# test (hotplug isn't verifiable in CI).
|
||||
keyboardWatch = pkgs.writeShellScriptBin "nomarchy-keyboard-watch" ''
|
||||
set -u
|
||||
layouts="${lib.concatStringsSep " " kbLayouts}"
|
||||
layouts="${lib.concatStringsSep " " pickerLayouts}"
|
||||
declared="${lib.concatStringsSep " " (builtins.attrNames config.nomarchy.keyboard.devices)}"
|
||||
state="''${XDG_STATE_HOME:-$HOME/.local/state}/nomarchy/keyboard-layouts"
|
||||
mkdir -p "$(dirname "$state")"; [ -f "$state" ] || : > "$state"
|
||||
sync=${sync}
|
||||
|
||||
layout_index() { i=0; for l in $layouts; do [ "$l" = "$1" ] && { printf %s "$i"; return; }; i=$((i + 1)); done; printf %s -1; }
|
||||
apply() { idx=$(layout_index "$2"); [ "$idx" -ge 0 ] && hyprctl switchxkblayout "$1" "$idx" >/dev/null 2>&1; }
|
||||
apply() { hyprctl keyword "device[$1]:kb_layout" "$2" >/dev/null 2>&1; }
|
||||
keyboards() { hyprctl devices -j | jq -r '.keyboards[].name'; }
|
||||
is_declared() { case " $declared " in *" $1 "*) return 0 ;; *) return 1 ;; esac; }
|
||||
saved_for() { while IFS='=' read -r k v; do [ "$k" = "$1" ] && { printf '%s' "$v"; return; }; done < "$state"; }
|
||||
|
||||
# The remembered map (device-name -> layout) from the LIVE in-flake state;
|
||||
# an absent key (sparse state) reads as an empty object.
|
||||
saved_map() { "$sync" get settings.keyboard.devices 2>/dev/null || echo '{}'; }
|
||||
saved_for() { saved_map | jq -r --arg k "$1" '.[$k] // empty'; }
|
||||
|
||||
# Persist a pick INSTANTLY: merge it into the map and write the whole object
|
||||
# back at the dot-free parent path, so a device name containing a dot can't
|
||||
# corrupt the dotted set-path. --no-switch = write only, no rebuild.
|
||||
remember() {
|
||||
map=$(saved_map | jq -c --arg k "$1" --arg v "$2" '. + {($k): $v}') || return
|
||||
"$sync" --quiet set settings.keyboard.devices "$map" --no-switch
|
||||
}
|
||||
|
||||
# Startup: re-apply remembered layouts, never prompt — so the built-in
|
||||
# keyboard just stays on the session default.
|
||||
@@ -106,7 +134,7 @@ let
|
||||
apply "$kb" "$s"
|
||||
else
|
||||
choice=$(printf '%s\n' $layouts | rofi -dmenu -p "Layout · $kb")
|
||||
[ -n "$choice" ] && { printf '%s=%s\n' "$kb" "$choice" >> "$state"; apply "$kb" "$choice"; }
|
||||
[ -n "$choice" ] && { remember "$kb" "$choice"; apply "$kb" "$choice"; }
|
||||
fi
|
||||
done
|
||||
prev="$cur"
|
||||
@@ -146,7 +174,13 @@ in
|
||||
# Paint the wallpaper as soon as the session is up (waits for
|
||||
# the daemon internally).
|
||||
"nomarchy-theme-sync wallpaper"
|
||||
] ++ lib.optional kbAutoSwitch "${keyboardWatch}/bin/nomarchy-keyboard-watch";
|
||||
# Waybar is launched here rather than as a systemd user service: bound
|
||||
# to graphical-session.target the unit raced Hyprland's IPC on relogin
|
||||
# and landed in `failed`; exec-once only fires once the compositor is
|
||||
# up. Reloaded on theme switch via SIGUSR2 (nomarchy-theme-sync). See
|
||||
# waybar.nix.
|
||||
] ++ lib.optional config.nomarchy.waybar.enable "waybar"
|
||||
++ lib.optional kbAutoSwitch "${keyboardWatch}/bin/nomarchy-keyboard-watch";
|
||||
|
||||
# ── Theme-driven look ──────────────────────────────────────────
|
||||
# These flow from theme-state.json and stay at NORMAL priority:
|
||||
@@ -201,8 +235,10 @@ in
|
||||
|
||||
input = {
|
||||
# kb_layout/variant come from nomarchy.keyboard.* — set that
|
||||
# option (the installer writes it; it also drives tty/LUKS).
|
||||
kb_layout = lib.concatStringsSep "," kbLayouts;
|
||||
# option (the installer writes it; it also drives tty/LUKS). Only the
|
||||
# session layout(s) land here; the keyboard.layouts pool is applied
|
||||
# per-device to external boards, never onto the session keyboard.
|
||||
kb_layout = config.nomarchy.keyboard.layout;
|
||||
kb_variant = config.nomarchy.keyboard.variant;
|
||||
follow_mouse = lib.mkDefault 1;
|
||||
touchpad.natural_scroll = lib.mkDefault true;
|
||||
@@ -266,11 +302,24 @@ in
|
||||
};
|
||||
};
|
||||
|
||||
# Runtime-remembered per-device layouts (settings.keyboard.devices, written
|
||||
# by the watcher into the in-flake state) graduate into the declarative
|
||||
# keyboard.devices: each becomes a generated Hyprland device{} block —
|
||||
# reproducible and applied natively on hotplug — after which the watcher sees
|
||||
# it as declared and steps back. mkDefault so a hand-written
|
||||
# keyboard.devices.<name> in home.nix still wins for the same keyboard.
|
||||
nomarchy.keyboard.devices = lib.mapAttrs
|
||||
(_name: layout: { layout = lib.mkDefault layout; })
|
||||
config.nomarchy.settings.keyboard.devices;
|
||||
|
||||
# nwg-displays: interactive monitor arranger (applies live via hyprctl and
|
||||
# writes a config file). A helper to find values for nomarchy.monitors —
|
||||
# the declarative config stays the source of truth (we don't source its
|
||||
# output). Gated on its toggle; pointless without the Hyprland session.
|
||||
home.packages = lib.optionals
|
||||
(config.nomarchy.hyprland.enable && config.nomarchy.displays.enable)
|
||||
[ pkgs.nwg-displays ];
|
||||
home.packages =
|
||||
lib.optionals (config.nomarchy.hyprland.enable && config.nomarchy.displays.enable)
|
||||
[ pkgs.nwg-displays ]
|
||||
# The new-keyboard watcher on PATH (when enabled) so it's discoverable and
|
||||
# runnable by name for debugging — Hyprland still exec-once's it by store path.
|
||||
++ lib.optional (config.nomarchy.hyprland.enable && kbAutoSwitch) keyboardWatch;
|
||||
}
|
||||
|
||||
@@ -1,18 +1,75 @@
|
||||
# Night light — a scheduled blue-light filter via hyprsunset (Hyprland's own
|
||||
# gamma/temperature tool). Warm at night, identity (no shift) by day;
|
||||
# hyprsunset's time-based `profile` entries handle the schedule and pick the
|
||||
# right state on session start. Opt-in via nomarchy.nightlight.enable.
|
||||
# gamma/temperature tool). Warm at night, identity (no shift) by day; the
|
||||
# schedule (temperature/sunrise/sunset) is tuned via nomarchy.nightlight.* in
|
||||
# home.nix and baked into the unit's time-based `profile`.
|
||||
#
|
||||
# The hyprsunset HM service module is provided by home-manager; this only
|
||||
# configures it. Override anything with plain services.hyprsunset.* options.
|
||||
{ config, lib, ... }:
|
||||
# Off by default and opt-in. Two git-tracked flags in the state file, both
|
||||
# menu-written (no ~/.local/state):
|
||||
# settings.nightlight.installed — does the hyprsunset unit exist? Sticky; the
|
||||
# option mkDefault-reads it, so the FIRST enable from the menu rebuilds (to
|
||||
# create the unit) and an instant-off is never undone by a later rebuild.
|
||||
# settings.nightlight.on — runtime on/off. Toggled INSTANTLY (write + systemctl,
|
||||
# no rebuild); read by the unit's ExecCondition (should-start) at session
|
||||
# start so the choice survives logout/reboot via the *live* state, not the
|
||||
# eval-frozen store copy. A later rebuild bakes the same value (no divergence).
|
||||
{ config, lib, pkgs, ... }:
|
||||
|
||||
let
|
||||
cfg = config.nomarchy.nightlight;
|
||||
s = config.nomarchy.settings.nightlight;
|
||||
sync = lib.getExe config.nomarchy.package;
|
||||
# Runtime-on default for when the `on` key hasn't been written yet (e.g. right
|
||||
# after the first enable). Baked at eval; only used when the live key is absent.
|
||||
onDefault = lib.boolToString s.on;
|
||||
|
||||
nomarchy-nightlight = pkgs.writeShellScriptBin "nomarchy-nightlight" ''
|
||||
unit=hyprsunset.service
|
||||
# Instant runtime on/off: write the in-flake state WITHOUT a rebuild.
|
||||
write_on() { ${sync} --quiet set settings.nightlight.on "$1" --no-switch; }
|
||||
# First enable: mark the feature installed and REBUILD to create the unit
|
||||
# (the one rebuild we accept; every toggle after is instant).
|
||||
install_feature() { ${sync} --quiet set settings.nightlight.installed true; }
|
||||
# Read the LIVE working-tree on/off (~/.nomarchy via $NOMARCHY_PATH), not the
|
||||
# store copy baked into this generation; fall back to the eval-time default
|
||||
# when absent. Normalise Python's True/False bool rendering.
|
||||
is_on() {
|
||||
v=$(${sync} get settings.nightlight.on 2>/dev/null) || v=${onDefault}
|
||||
case "$v" in true|True) return 0 ;; *) return 1 ;; esac
|
||||
}
|
||||
installed() { systemctl --user cat "$unit" >/dev/null 2>&1; }
|
||||
start() { systemctl --user start "$unit" 2>/dev/null || true; }
|
||||
stop() { systemctl --user stop "$unit" 2>/dev/null || true; }
|
||||
case "''${1:-toggle}" in
|
||||
should-start) is_on ;; # ExecCondition gate (login/reboot)
|
||||
status)
|
||||
# Waybar (polls every 3s): moon while running; print nothing otherwise so
|
||||
# the module self-hides — enable / re-enable from the System menu.
|
||||
systemctl --user is-active --quiet "$unit" \
|
||||
&& printf '{"text":"","tooltip":"Night light on — warm on schedule (click to disable)","class":"on"}\n'
|
||||
exit 0 ;;
|
||||
on)
|
||||
if installed; then write_on true; start; else install_feature; start; fi ;;
|
||||
off) write_on false; stop ;;
|
||||
toggle)
|
||||
if systemctl --user is-active --quiet "$unit"; then
|
||||
write_on false; stop # on -> off (instant)
|
||||
elif installed; then
|
||||
write_on true; start # installed, off -> on (instant)
|
||||
else
|
||||
install_feature; start # first enable (rebuilds)
|
||||
fi ;;
|
||||
*) echo "usage: nomarchy-nightlight [toggle|status|on|off|should-start]" >&2; exit 64 ;;
|
||||
esac
|
||||
'';
|
||||
in
|
||||
{
|
||||
config = lib.mkIf cfg.enable {
|
||||
services.hyprsunset = {
|
||||
config = {
|
||||
# Unit presence tracks the sticky `installed` flag the menu writes (first
|
||||
# enable rebuilds). mkDefault so a hand-set nomarchy.nightlight.enable in
|
||||
# home.nix also works as a declarative opt-in.
|
||||
nomarchy.nightlight.enable = lib.mkDefault s.installed;
|
||||
|
||||
services.hyprsunset = lib.mkIf cfg.enable {
|
||||
enable = true;
|
||||
settings.profile = [
|
||||
# Daytime: identity = no colour change.
|
||||
@@ -21,5 +78,13 @@ in
|
||||
{ time = cfg.sunset; temperature = cfg.temperature; }
|
||||
];
|
||||
};
|
||||
|
||||
# Gate the unit on the LIVE on/off state at start time (login/reboot), not
|
||||
# at eval time — so a menu toggle (written without a rebuild) is honoured on
|
||||
# the next session. A failed condition skips the unit (inactive, not failed).
|
||||
systemd.user.services.hyprsunset.Service.ExecCondition =
|
||||
lib.mkIf cfg.enable "${nomarchy-nightlight}/bin/nomarchy-nightlight should-start";
|
||||
|
||||
home.packages = [ nomarchy-nightlight ];
|
||||
};
|
||||
}
|
||||
|
||||
@@ -105,10 +105,14 @@ in
|
||||
picker. When non-empty, a small watcher runs in the session: when a
|
||||
keyboard connects that isn't covered by nomarchy.keyboard.devices and
|
||||
hasn't been chosen before, it pops a rofi picker (these layouts plus
|
||||
the primary nomarchy.keyboard.layout), applies the choice, and
|
||||
remembers it per-device (~/.local/state) — re-applying automatically
|
||||
on later reconnects. The runtime-remember complement to the
|
||||
declarative keyboard.devices; a stateful runtime piece by design.
|
||||
the primary nomarchy.keyboard.layout), applies the choice to that
|
||||
keyboard only (a per-device kb_layout, so the built-in board is left
|
||||
alone), and remembers it in the git-tracked in-flake state
|
||||
(settings.keyboard.devices, not ~/.local/state) — re-applied
|
||||
automatically on later reconnects and across reboots. Each remembered
|
||||
choice graduates into nomarchy.keyboard.devices on the next rebuild
|
||||
(a generated device block). The runtime-remember complement to the
|
||||
declarative keyboard.devices.
|
||||
'';
|
||||
};
|
||||
|
||||
@@ -135,7 +139,9 @@ in
|
||||
nomarchy.keyboard.layout for that keyboard only — e.g. an external
|
||||
keyboard that's physically a different layout than the laptop's
|
||||
built-in one. Hyprland applies it automatically whenever that
|
||||
keyboard is connected.
|
||||
keyboard is connected. The interactive watcher
|
||||
(nomarchy.keyboard.layouts) also writes its remembered picks here on
|
||||
the next rebuild, so a runtime choice graduates into reproducible config.
|
||||
'';
|
||||
};
|
||||
|
||||
@@ -184,6 +190,32 @@ in
|
||||
};
|
||||
};
|
||||
|
||||
updates = {
|
||||
enable = lib.mkEnableOption ''
|
||||
passive update awareness: a background check (systemd user timer) that
|
||||
compares the flake's locked inputs (nixpkgs, the Nomarchy input, …)
|
||||
against upstream and — when Flatpak is enabled — counts Flatpak
|
||||
updates, surfacing a Waybar indicator + a notification when something
|
||||
is available. It never changes anything; you still run sys-update /
|
||||
home-update / flatpak update yourself'';
|
||||
|
||||
interval = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = "daily";
|
||||
example = "6h";
|
||||
description = "How often to check, as a systemd OnCalendar expression.";
|
||||
};
|
||||
|
||||
flatpak = lib.mkOption {
|
||||
type = lib.types.bool;
|
||||
default = true;
|
||||
description = ''
|
||||
Also count available Flatpak updates when the `flatpak` CLI is
|
||||
present (i.e. nomarchy.services.flatpak is on). No effect otherwise.
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
monitors = lib.mkOption {
|
||||
type = lib.types.listOf monitorType;
|
||||
default = [ ];
|
||||
@@ -228,6 +260,17 @@ in
|
||||
description = "The parsed theme state (stateFile merged over defaults).";
|
||||
};
|
||||
|
||||
settings = lib.mkOption {
|
||||
type = lib.types.attrs;
|
||||
readOnly = true;
|
||||
description = ''
|
||||
Parsed feature settings — the `settings` section of the state file,
|
||||
what the menu/Waybar toggles write (e.g. settings.nightlight.enable).
|
||||
Feature options mkDefault-read from here, so a menu toggle lands in the
|
||||
in-flake state (git-tracked, reproducible) rather than ~/.local/state.
|
||||
'';
|
||||
};
|
||||
|
||||
lib = lib.mkOption {
|
||||
type = lib.types.attrs;
|
||||
readOnly = true;
|
||||
|
||||
@@ -19,6 +19,92 @@ let
|
||||
rasiOverride = cfg.themesDir + "/${t.slug}/rofi.rasi";
|
||||
hasRasiOverride = builtins.pathExists rasiOverride;
|
||||
|
||||
# ── Visual theme picker ──────────────────────────────────────────────
|
||||
# Every preset (themes/<slug>.json) is read at eval time to build a grid
|
||||
# of real desktop previews. Grouped dark-first then light — the previews
|
||||
# make the mode obvious at a glance, so no light/dark submenu split — and
|
||||
# the active theme is marked ✓. "Active" is just t.slug: every switch
|
||||
# rebuilds this script, so the baked-in mark always tracks the live theme.
|
||||
themeFiles = lib.filterAttrs
|
||||
(n: ty: ty == "regular" && lib.hasSuffix ".json" n)
|
||||
(builtins.readDir cfg.themesDir);
|
||||
themeData = lib.mapAttrsToList (fname: _:
|
||||
let
|
||||
j = builtins.fromJSON (builtins.readFile (cfg.themesDir + "/${fname}"));
|
||||
slug = j.slug or (lib.removeSuffix ".json" fname);
|
||||
in {
|
||||
inherit slug;
|
||||
name = j.name or slug;
|
||||
mode = j.mode or "dark";
|
||||
hasPreview = builtins.pathExists (cfg.themesDir + "/${slug}/preview.png");
|
||||
}) themeFiles;
|
||||
|
||||
byName = lib.sort (a: b: a.name < b.name);
|
||||
orderedThemes =
|
||||
byName (lib.filter (th: th.mode != "light") themeData) # dark group
|
||||
++ byName (lib.filter (th: th.mode == "light") themeData); # then light
|
||||
|
||||
# rofi's element-icon cells are SQUARE, so a 16:9 preview would letterbox
|
||||
# (theme-coloured bands top/bottom). Build-time crop each committed 480×270
|
||||
# preview to a centred square so it fills the cell edge-to-edge. The source
|
||||
# preview.png stays 480×270 (untouched) — only the displayed thumb is square.
|
||||
themeThumbs = pkgs.runCommand "nomarchy-theme-thumbs"
|
||||
{ nativeBuildInputs = [ pkgs.imagemagick ]; } ''
|
||||
mkdir -p $out
|
||||
${lib.concatMapStringsSep "\n"
|
||||
(th: lib.optionalString th.hasPreview ''
|
||||
magick ${cfg.themesDir + "/${th.slug}/preview.png"} \
|
||||
-resize 360x360^ -gravity center -extent 360x360 -strip $out/${th.slug}.png
|
||||
'')
|
||||
orderedThemes}
|
||||
'';
|
||||
|
||||
# One grid row per theme (square thumb + name, ✓ on the active one), and a
|
||||
# Name→slug map so the picked label resolves back to the preset that
|
||||
# nomarchy-theme-sync applies (pretty names ≠ slugs, hence the map). Themes
|
||||
# with no preview degrade to a plain-name row.
|
||||
themeRows = lib.concatMapStringsSep "\n" (th:
|
||||
let label = th.name + lib.optionalString (th.slug == t.slug) " ✓";
|
||||
in if th.hasPreview
|
||||
then "row ${lib.escapeShellArg label} ${themeThumbs}/${th.slug}.png"
|
||||
else "printf '%s\\n' ${lib.escapeShellArg label}")
|
||||
orderedThemes;
|
||||
themeSlugMap = lib.concatMapStringsSep "\n" (th:
|
||||
" [${lib.escapeShellArg th.name}]=${lib.escapeShellArg th.slug}")
|
||||
orderedThemes;
|
||||
|
||||
# Per-invocation grid layout (cards: a 16:9 preview above a centered name),
|
||||
# overriding the single-column list theme just for this menu.
|
||||
# · The icon box is sized 16:9 (rofi 2.0 takes a two-value `size`) to
|
||||
# match the 480×270 previews exactly — they fill it with no letterbox.
|
||||
# · The window is sized to the *content* (3 × icon + paddings), not a
|
||||
# percentage, so a column is no wider than its card — otherwise the
|
||||
# selection highlight balloons out around the image.
|
||||
# · flow: horizontal lays cards out row-by-row (rofi's default Vertical
|
||||
# fills column-by-column, so Down at a column's foot jumped to the top
|
||||
# of the next column instead of scrolling the page).
|
||||
# Grid dial — the previews are as big as the icon px allows; screen *height*
|
||||
# caps it, so showing 2 rows (not 3) lets each card grow a lot. Scrolling
|
||||
# reaches the rest. Tune themeGridIconW for size; the window resizes to fit.
|
||||
# rofi's element-icon `size` is a SINGLE value (the manual only documents
|
||||
# one) — a two-value "WxH" is silently collapsed, which is why the preview
|
||||
# used to render tiny inside a too-wide column. So: size = the card *width*,
|
||||
# rofi fits the 16:9 image tight to it (no square letterbox), and the window
|
||||
# is sized so a column is exactly that width — the preview fills the cell.
|
||||
themeGridCols = 3;
|
||||
themeGridLines = 3;
|
||||
themeGridIconW = 240; # square preview card side (px) — the size knob
|
||||
themeGridPad = 0; # margin around the preview inside its cell (px)
|
||||
themeGridGap = 8; # gap between cards (px)
|
||||
themeGridThemeStr = lib.escapeShellArg (lib.concatStringsSep " " [
|
||||
# window width = cards + their padding + inter-card gaps + chrome (~20px)
|
||||
"window { width: ${toString (themeGridCols * (themeGridIconW + 2 * themeGridPad) + (themeGridCols - 1) * themeGridGap + 20)}px; }"
|
||||
"listview { columns: ${toString themeGridCols}; lines: ${toString themeGridLines}; spacing: ${toString themeGridGap}px; flow: horizontal; }"
|
||||
"element { orientation: vertical; padding: ${toString themeGridPad}px; spacing: 2px; }"
|
||||
"element-icon { size: ${toString themeGridIconW}px; }"
|
||||
"element-text { horizontal-align: 0.5; }"
|
||||
]);
|
||||
|
||||
# Keybindings cheatsheet (SUPER+? → the `keybinds` menu module). Built
|
||||
# from the SAME ./keybinds.nix that hyprland.nix binds, so it can never
|
||||
# drift from the live shortcuts. "$mod" reads as SUPER; rows are padded
|
||||
@@ -64,6 +150,14 @@ let
|
||||
# rofi has show-icons (on globally; passed explicitly on the icon menus).
|
||||
row() { printf '%s\0icon\x1f%s\n' "$1" "$2"; }
|
||||
|
||||
# General menu convention: every list menu ends with a "↩ Back" entry that
|
||||
# returns one level up (so you never have to Esc out and reopen). `back`
|
||||
# emits it with an icon; plain pick-lists append the bare label instead.
|
||||
# Always matched EXACTLY ("↩ Back") so it can't collide with clipboard or
|
||||
# filename content. The arrow + go-previous icon read as "go back".
|
||||
BACK="↩ Back"
|
||||
back() { row "$BACK" go-previous; }
|
||||
|
||||
case "''${1:-}" in
|
||||
power)
|
||||
choice=$( {
|
||||
@@ -73,8 +167,10 @@ let
|
||||
row "Hibernate" system-hibernate
|
||||
row "Reboot" system-reboot
|
||||
row "Shutdown" system-shutdown
|
||||
back
|
||||
} | rofi -dmenu -show-icons -p Power) || exit 0
|
||||
case "$choice" in
|
||||
"$BACK") exec "$0" ;;
|
||||
*Lock) loginctl lock-session ;;
|
||||
*Logout) hyprctl dispatch exit ;;
|
||||
*Suspend) systemctl suspend ;;
|
||||
@@ -92,17 +188,30 @@ let
|
||||
exit 0
|
||||
fi
|
||||
cur=$(powerprofilesctl get 2>/dev/null)
|
||||
choice=$(powerprofilesctl list 2>/dev/null \
|
||||
| sed -nE 's/^[* ] ([a-z-]+):$/\1/p' \
|
||||
choice=$( { powerprofilesctl list 2>/dev/null | sed -nE 's/^[* ] ([a-z-]+):$/\1/p'; printf '%s\n' "$BACK"; } \
|
||||
| rofi -dmenu -p "profile (now: $cur)") || exit 0
|
||||
[ "$choice" = "$BACK" ] && exec "$0" system
|
||||
[ -n "$choice" ] && powerprofilesctl set "$choice" ;;
|
||||
|
||||
theme)
|
||||
choice=$(nomarchy-theme-sync list | rofi -dmenu -p theme) || exit 0
|
||||
[ -n "$choice" ] && exec nomarchy-theme-sync apply "$choice" ;;
|
||||
# Visual picker: a grid of real desktop previews (rows + Name→slug
|
||||
# map generated from the presets at build time). Pick a card → apply
|
||||
# that slug. The grid layout is a per-invocation -theme-str override.
|
||||
declare -A THEME_SLUG=(
|
||||
${themeSlugMap}
|
||||
)
|
||||
choice=$( {
|
||||
${themeRows}
|
||||
back
|
||||
} | rofi -dmenu -show-icons -p Theme -theme-str ${themeGridThemeStr}) || exit 0
|
||||
[ "$choice" = "$BACK" ] && exec "$0"
|
||||
choice="''${choice% ✓}" # drop the active marker if present
|
||||
slug="''${THEME_SLUG[$choice]:-}"
|
||||
[ -n "$slug" ] && exec nomarchy-theme-sync apply "$slug" ;;
|
||||
|
||||
clipboard)
|
||||
sel=$(cliphist list | rofi -dmenu -p clip) || exit 0
|
||||
sel=$( { cliphist list; printf '%s\n' "$BACK"; } | rofi -dmenu -p clip) || exit 0
|
||||
[ "$sel" = "$BACK" ] && exec "$0" tools
|
||||
printf '%s' "$sel" | cliphist decode | wl-copy ;;
|
||||
|
||||
calc)
|
||||
@@ -114,10 +223,10 @@ let
|
||||
-calc-command "echo -n '{result}' | wl-copy" ;;
|
||||
|
||||
files)
|
||||
sel=$(fd . "$HOME" --type f --hidden --exclude .git --exclude .cache 2>/dev/null \
|
||||
| head -n 50000 \
|
||||
| sed "s|^$HOME/||" \
|
||||
sel=$( { fd . "$HOME" --type f --hidden --exclude .git --exclude .cache 2>/dev/null \
|
||||
| head -n 50000 | sed "s|^$HOME/||"; printf '%s\n' "$BACK"; } \
|
||||
| rofi -dmenu -p file) || exit 0
|
||||
[ "$sel" = "$BACK" ] && exec "$0" tools
|
||||
[ -n "$sel" ] && exec xdg-open "$HOME/$sel" ;;
|
||||
|
||||
emoji)
|
||||
@@ -132,23 +241,52 @@ let
|
||||
exec xdg-open "https://www.google.com/search?q=$(urlencode "$q")" ;;
|
||||
|
||||
network)
|
||||
# nmtui in a terminal (NetworkManager is the system network stack).
|
||||
exec ${cfg.terminal} -e nmtui ;;
|
||||
# Native rofi wifi/VPN picker (networkmanager_dmenu) over the system
|
||||
# NetworkManager — replaces the old nmtui-in-a-terminal flow. Reads
|
||||
# its config from xdg.configFile below (told to drive rofi).
|
||||
exec networkmanager_dmenu ;;
|
||||
|
||||
bluetooth)
|
||||
# blueman-manager GUI (services.blueman.enable, system-side).
|
||||
exec blueman-manager ;;
|
||||
|
||||
printers)
|
||||
# system-config-printer GUI (the CUPS admin app), installed by
|
||||
# nomarchy.services.printing. Self-gated in the System menu; guard
|
||||
# here too in case it's invoked directly.
|
||||
command -v system-config-printer >/dev/null 2>&1 \
|
||||
&& exec system-config-printer
|
||||
notify-send "Printers" "Not available (nomarchy.services.printing off?)."; exit 0 ;;
|
||||
|
||||
audio)
|
||||
# PipeWire (pulse) sink/source switcher via rofi-pulse-select. Self-
|
||||
# gated in the System menu on the pulse socket; guarded here too.
|
||||
if [ ! -e "''${XDG_RUNTIME_DIR:-/run/user/$(id -u)}/pulse/native" ]; then
|
||||
notify-send "Audio" "No PipeWire/PulseAudio socket (nomarchy.audio off?)."; exit 0
|
||||
fi
|
||||
choice=$( {
|
||||
row "Output device" audio-volume-high
|
||||
row "Input device" audio-input-microphone
|
||||
back
|
||||
} | rofi -dmenu -show-icons -p Audio) || exit 0
|
||||
case "$choice" in
|
||||
"$BACK") exec "$0" system ;;
|
||||
*Output*) exec rofi-pulse-select sink ;;
|
||||
*Input*) exec rofi-pulse-select source ;;
|
||||
esac ;;
|
||||
|
||||
capture)
|
||||
choice=$( {
|
||||
row "Region → clipboard" applets-screenshooter
|
||||
row "Region → file" applets-screenshooter
|
||||
row "Full screen → clipboard" camera-photo
|
||||
row "Full screen → file" camera-photo
|
||||
back
|
||||
} | rofi -dmenu -show-icons -p Capture) || exit 0
|
||||
dir="$HOME/Pictures/Screenshots"
|
||||
file="$dir/$(date +%Y%m%d-%H%M%S).png"
|
||||
case "$choice" in
|
||||
"$BACK") exec "$0" tools ;;
|
||||
*"Region → clipboard"*) grim -g "$(slurp)" - | wl-copy ;;
|
||||
*"Region → file"*) mkdir -p "$dir"; grim -g "$(slurp)" "$file" \
|
||||
&& notify-send "Screenshot saved" "$file" ;;
|
||||
@@ -178,13 +316,31 @@ let
|
||||
&& notify-send "Do Not Disturb off" "Notifications resumed."
|
||||
exit 0 ;;
|
||||
|
||||
nightlight)
|
||||
# Force the scheduled blue-light filter on/off for the session by
|
||||
# starting/stopping hyprsunset (nomarchy-nightlight, from nightlight.nix).
|
||||
# Self-gated in the menu; guard here too in case it's invoked directly.
|
||||
command -v nomarchy-nightlight >/dev/null 2>&1 \
|
||||
&& exec nomarchy-nightlight toggle
|
||||
notify-send "Night light" "Not available (nomarchy.nightlight off?)."; exit 0 ;;
|
||||
|
||||
autotimezone)
|
||||
# Toggle automatic timezone detection (geoclue + automatic-timezoned).
|
||||
# A SYSTEM service, so flipping it writes the in-flake flag and runs a
|
||||
# system rebuild (sudo) + a home switch — in a terminal, like Snapshots.
|
||||
command -v nomarchy-autotimezone >/dev/null 2>&1 \
|
||||
|| { notify-send "Auto timezone" "Unavailable on this machine."; exit 0; }
|
||||
exec ${cfg.terminal} -e nomarchy-autotimezone toggle ;;
|
||||
|
||||
snapshot)
|
||||
# btrfs-assistant: snapshot browse / diff / restore / rollback over
|
||||
# snapper, elevating via polkit. Shipped system-side with
|
||||
# nomarchy.system.snapper; self-gate so the entry no-ops elsewhere.
|
||||
command -v btrfs-assistant >/dev/null 2>&1 \
|
||||
|| { notify-send "Snapshots" "btrfs-assistant isn't installed (BTRFS snapshots off?)."; exit 0; }
|
||||
exec btrfs-assistant ;;
|
||||
# btrfs-assistant 2.2 segfaults on 26.05 (libbtrfsutil ABI), so launch
|
||||
# the keyboard-driven snapper browser instead: a terminal running it
|
||||
# via sudo (snapper is root-only here — one password prompt), with
|
||||
# browse/diff, restore (undochange) and rollback, each confirmed.
|
||||
# Self-gated to nomarchy.system.snapper, so it no-ops elsewhere.
|
||||
command -v nomarchy-snapshots >/dev/null 2>&1 \
|
||||
|| { notify-send "Snapshots" "Snapshot tools unavailable (nomarchy.system.snapper off?)."; exit 0; }
|
||||
exec ${cfg.terminal} -e sudo nomarchy-snapshots ;;
|
||||
|
||||
tools)
|
||||
# Tools submenu — utilities you invoke. Each leaf is also reachable
|
||||
@@ -197,9 +353,10 @@ let
|
||||
row "Web search" system-search
|
||||
row "Capture" applets-screenshooter
|
||||
row "Ask Claude" internet-chat
|
||||
row "Back" go-previous
|
||||
back
|
||||
} | rofi -dmenu -show-icons -p Tools) || exit 0
|
||||
case "$choice" in
|
||||
"$BACK") exec "$0" ;;
|
||||
*Calc*) exec "$0" calc ;;
|
||||
*Clipboard*) exec "$0" clipboard ;;
|
||||
*Emoji*) exec "$0" emoji ;;
|
||||
@@ -207,7 +364,6 @@ let
|
||||
*Web*) exec "$0" web ;;
|
||||
*Capture*) exec "$0" capture ;;
|
||||
*Ask*) exec "$0" ask ;;
|
||||
*Back*) exec "$0" ;;
|
||||
esac ;;
|
||||
|
||||
system)
|
||||
@@ -218,20 +374,35 @@ let
|
||||
choice=$( {
|
||||
row "Network" network-wireless
|
||||
row "Bluetooth" bluetooth
|
||||
[ -e "''${XDG_RUNTIME_DIR:-/run/user/$(id -u)}/pulse/native" ] \
|
||||
&& row "Audio" audio-volume-high
|
||||
command -v system-config-printer >/dev/null 2>&1 && row "Printers" printer
|
||||
row "Do Not Disturb" notification-disabled
|
||||
command -v btrfs-assistant >/dev/null 2>&1 && row "Snapshots" timeshift
|
||||
if systemctl --user is-active --quiet hyprsunset.service 2>/dev/null
|
||||
then row "Night light (on)" weather-clear-night
|
||||
else row "Night light (off)" weather-clear-night
|
||||
fi
|
||||
if [ "$(nomarchy-theme-sync get settings.autoTimezone 2>/dev/null)" = true ]
|
||||
then row "Auto timezone (on)" preferences-system-time
|
||||
else row "Auto timezone (off)" preferences-system-time
|
||||
fi
|
||||
command -v nomarchy-snapshots >/dev/null 2>&1 && row "Snapshots" timeshift
|
||||
if [ -e "''${bats[0]}" ] && command -v powerprofilesctl >/dev/null 2>&1; then
|
||||
row "Power profile" preferences-system-power
|
||||
fi
|
||||
row "Back" go-previous
|
||||
back
|
||||
} | rofi -dmenu -show-icons -p System) || exit 0
|
||||
case "$choice" in
|
||||
"$BACK") exec "$0" ;;
|
||||
*Network*) exec "$0" network ;;
|
||||
*Bluetooth*) exec "$0" bluetooth ;;
|
||||
*Audio*) exec "$0" audio ;;
|
||||
*Printers*) exec "$0" printers ;;
|
||||
*"Do Not Disturb"*) exec "$0" dnd ;;
|
||||
*"Night light"*) exec "$0" nightlight ;;
|
||||
*"Auto timezone"*) exec "$0" autotimezone ;;
|
||||
*Snapshots*) exec "$0" snapshot ;;
|
||||
*"Power profile"*) exec "$0" power-profile ;;
|
||||
*Back*) exec "$0" ;;
|
||||
esac ;;
|
||||
|
||||
"")
|
||||
@@ -256,7 +427,7 @@ let
|
||||
esac ;;
|
||||
|
||||
*)
|
||||
echo "usage: nomarchy-menu [tools|system|power|power-profile|theme|clipboard|calc|files|emoji|web|network|bluetooth|capture|keybinds|ask|dnd|snapshot]" >&2
|
||||
echo "usage: nomarchy-menu [tools|system|power|power-profile|theme|clipboard|calc|files|emoji|web|network|bluetooth|audio|printers|capture|keybinds|ask|dnd|nightlight|autotimezone|snapshot]" >&2
|
||||
exit 64 ;;
|
||||
esac
|
||||
'';
|
||||
@@ -265,11 +436,34 @@ in
|
||||
config = lib.mkIf cfg.rofi.enable {
|
||||
home.packages = [
|
||||
nomarchy-menu
|
||||
pkgs.fd # files module (fuzzy search over $HOME)
|
||||
pkgs.xdg-utils # xdg-open for the files + web modules
|
||||
pkgs.nodejs # npx, for the Ask Claude module (claude-code from npm)
|
||||
pkgs.fd # files module (fuzzy search over $HOME)
|
||||
pkgs.xdg-utils # xdg-open for the files + web modules
|
||||
pkgs.nodejs # npx, for the Ask Claude module (claude-code from npm)
|
||||
pkgs.networkmanager_dmenu # network module: rofi wifi/VPN picker
|
||||
pkgs.rofi-pulse-select # audio module: sink/source switcher
|
||||
];
|
||||
|
||||
# networkmanager_dmenu drives rofi (not bare dmenu) and uses rofi's
|
||||
# active/urgent row styling for the connected/available networks, so it
|
||||
# inherits the generated theme like every other menu module. Editing a
|
||||
# connection drops to nmtui in the configured terminal. force: the tool
|
||||
# writes a default config on first run, so an unmanaged file may already
|
||||
# sit at this path — we fully own it, so overwrite rather than abort.
|
||||
xdg.configFile."networkmanager-dmenu/config.ini" = {
|
||||
force = true;
|
||||
text = ''
|
||||
[dmenu]
|
||||
dmenu_command = rofi
|
||||
rofi_highlight = True
|
||||
compact = False
|
||||
wifi_chars = ▂▄▆█
|
||||
|
||||
[editor]
|
||||
terminal = ${cfg.terminal}
|
||||
gui_if_available = False
|
||||
'';
|
||||
};
|
||||
|
||||
programs.rofi = {
|
||||
enable = true;
|
||||
terminal = cfg.terminal;
|
||||
@@ -292,6 +486,14 @@ in
|
||||
# reserving `lines` (8) — so a 6-entry menu doesn't leave empty space,
|
||||
# while the app launcher still fills + scrolls past `lines`.
|
||||
fixed-num-lines = false;
|
||||
# Search: case-insensitive fuzzy across every menu + the launcher, so
|
||||
# "system" finds "System" and "fzr" finds "Firefox". fzf sorting ranks
|
||||
# the closest match to the top (the per-keystroke modules — calc/emoji
|
||||
# — opt out per-invocation with -no-sort).
|
||||
matching = "fuzzy";
|
||||
case-sensitive = false;
|
||||
sort = true;
|
||||
sorting-method = "fzf";
|
||||
};
|
||||
|
||||
# Whole-swap themes bring their own rofi.rasi; otherwise the theme
|
||||
|
||||
@@ -54,6 +54,23 @@ let
|
||||
# tone for kanagawa/summer-*). Each preset declares its own so a theme
|
||||
# switch always replaces it (deep_merge would otherwise leave it stuck).
|
||||
border = { active = "accent"; inactive = "overlay"; };
|
||||
|
||||
# Non-appearance feature settings the menu/watchers write into this same
|
||||
# in-flake state. nomarchy.nightlight: `installed` (sticky — gates the unit,
|
||||
# so the first enable rebuilds) and `on` (instant runtime on/off).
|
||||
# settings.keyboard.devices: per-device layouts the new-keyboard watcher
|
||||
# remembers (device-name -> XKB layout), git-tracked instead of
|
||||
# ~/.local/state; they graduate into nomarchy.keyboard.devices on the next
|
||||
# rebuild. Defaulted so a sparse/older state file still evaluates;
|
||||
# nomarchy.settings exposes them.
|
||||
settings = {
|
||||
nightlight = { installed = false; on = true; };
|
||||
keyboard.devices = { };
|
||||
# Automatic timezone detection (nomarchy.system.autoTimezone): a system
|
||||
# service, but the flag lives here so both sides read one source — the
|
||||
# home side gates the Waybar-refresh watcher (timezone.nix) on it.
|
||||
autoTimezone = false;
|
||||
};
|
||||
};
|
||||
|
||||
parsed = lib.recursiveUpdate defaults themeState;
|
||||
@@ -78,6 +95,11 @@ in
|
||||
config = {
|
||||
nomarchy.theme = parsed // { inherit iconTheme border; };
|
||||
|
||||
# Feature toggles the menu writes (settings.nightlight.enable, …), exposed
|
||||
# alongside the appearance state. Feature modules mkDefault-read from here
|
||||
# so a menu toggle lands in the flake instead of in ~/.local/state.
|
||||
nomarchy.settings = parsed.settings;
|
||||
|
||||
nomarchy.lib = {
|
||||
# "#7aa2f7" -> "rgb(7aa2f7)" (Hyprland color syntax)
|
||||
rgb = c: "rgb(${lib.removePrefix "#" c})";
|
||||
|
||||
44
modules/home/timezone.nix
Normal file
@@ -0,0 +1,44 @@
|
||||
# Auto-timezone, home side: keep the Waybar clock in step with the system
|
||||
# timezone. Waybar's clock module captures the zone once at construction, so a
|
||||
# runtime timezone change (automatic-timezoned, nomarchy.system.autoTimezone)
|
||||
# would NOT show until a relogin. A tiny watcher subscribes to timedate1's
|
||||
# change signal and reloads Waybar (SIGUSR2 = the same reload theme-sync uses),
|
||||
# so the clock follows your location live. Also catches a manual
|
||||
# `timedatectl set-timezone`.
|
||||
#
|
||||
# Gated on the same in-flake flag the system side reads (settings.autoTimezone,
|
||||
# exposed via nomarchy.settings) — so it only runs when the feature is on. The
|
||||
# menu toggle (nomarchy-autotimezone) rebuilds both sides off that one flag.
|
||||
{ config, lib, pkgs, ... }:
|
||||
|
||||
let
|
||||
cfg = config.nomarchy;
|
||||
enabled = cfg.waybar.enable && (cfg.settings.autoTimezone or false);
|
||||
|
||||
tzWatch = pkgs.writeShellScript "nomarchy-tz-watch" ''
|
||||
last=$(${pkgs.systemd}/bin/timedatectl show -p Timezone --value 2>/dev/null || true)
|
||||
${pkgs.dbus}/bin/dbus-monitor --system \
|
||||
"type='signal',interface='org.freedesktop.DBus.Properties',path='/org/freedesktop/timedate1',member='PropertiesChanged'" \
|
||||
2>/dev/null |
|
||||
while read -r _; do
|
||||
cur=$(${pkgs.systemd}/bin/timedatectl show -p Timezone --value 2>/dev/null || true)
|
||||
[ "$cur" = "$last" ] && continue
|
||||
last=$cur
|
||||
${pkgs.procps}/bin/pkill -SIGUSR2 -x waybar 2>/dev/null || true
|
||||
done
|
||||
'';
|
||||
in
|
||||
{
|
||||
systemd.user.services.nomarchy-tz-watch = lib.mkIf enabled {
|
||||
Unit = {
|
||||
Description = "Reload Waybar on timezone change (auto-timezone)";
|
||||
PartOf = [ "graphical-session.target" ];
|
||||
After = [ "graphical-session.target" ];
|
||||
};
|
||||
Service = {
|
||||
ExecStart = "${tzWatch}";
|
||||
Restart = "on-failure";
|
||||
};
|
||||
Install.WantedBy = [ "graphical-session.target" ];
|
||||
};
|
||||
}
|
||||
141
modules/home/updates.nix
Normal file
@@ -0,0 +1,141 @@
|
||||
# Update awareness (opt-in, nomarchy.updates.enable) — a passive background
|
||||
# check that surfaces a Waybar indicator + a notification when updates are
|
||||
# available, without ever changing anything (you still run sys-update /
|
||||
# home-update / flatpak update). It counts:
|
||||
# • flake inputs whose locked rev is behind upstream (nixpkgs, the Nomarchy
|
||||
# input, home-manager …) — via `git ls-remote` on each branch-tracking
|
||||
# github/git input in flake.lock; offline → skipped, never a false alarm.
|
||||
# • Flatpak updates, when the `flatpak` CLI is present (services.flatpak on).
|
||||
#
|
||||
# nomarchy-updates is always on PATH and self-gates (status prints nothing
|
||||
# until the timer has found something), so the Waybar module — generated and
|
||||
# whole-swap — can exec it by name even when the feature is off.
|
||||
{ config, lib, pkgs, ... }:
|
||||
|
||||
let
|
||||
cfg = config.nomarchy.updates;
|
||||
|
||||
nomarchy-updates = pkgs.writeShellScriptBin "nomarchy-updates" ''
|
||||
set -u
|
||||
# System + user profiles, so flatpak / sys-update resolve from a timer-run
|
||||
# service too (build-time tools below use absolute store paths regardless).
|
||||
export PATH="$PATH:/run/current-system/sw/bin:/etc/profiles/per-user/$USER/bin"
|
||||
GIT=${pkgs.git}/bin/git
|
||||
JQ=${pkgs.jq}/bin/jq
|
||||
cache="''${XDG_CACHE_HOME:-$HOME/.cache}/nomarchy"
|
||||
state="$cache/updates.json"
|
||||
flake="''${NOMARCHY_PATH:-$HOME/.nomarchy}"
|
||||
mkdir -p "$cache"
|
||||
|
||||
count_nix() {
|
||||
local lock="$flake/flake.lock" n=0 name kind owner repo url ref locked giturl up
|
||||
[ -f "$lock" ] || { echo 0; return; }
|
||||
while IFS=$'\t' read -r name kind owner repo url ref locked; do
|
||||
[ -n "$locked" ] || continue
|
||||
case "$kind" in
|
||||
github) giturl="https://github.com/$owner/$repo" ;;
|
||||
git) giturl="$url" ;;
|
||||
*) continue ;;
|
||||
esac
|
||||
up=$("$GIT" ls-remote "$giturl" "$ref" 2>/dev/null | ${pkgs.gawk}/bin/awk 'NR==1{print $1}')
|
||||
[ -n "$up" ] || continue # offline / unknown → skip (no false alarm)
|
||||
[ "$up" != "$locked" ] && n=$((n + 1))
|
||||
done < <(
|
||||
# Only the flake's DIRECT inputs (root.inputs) — not the transitive
|
||||
# closure — so a deep dependency bump doesn't nag as an "update".
|
||||
"$JQ" -r '
|
||||
.nodes as $nodes
|
||||
| ($nodes.root.inputs | [ .[] | if type == "array" then .[0] else . end ]) as $direct
|
||||
| $nodes | to_entries[]
|
||||
| .key as $k | .value as $v
|
||||
| select($direct | index($k))
|
||||
| select(($v.original.type? == "github") or ($v.original.type? == "git"))
|
||||
| select(($v.original.rev? // "") == "") # branch-tracking only
|
||||
| [ $k, $v.original.type,
|
||||
($v.original.owner? // ""), ($v.original.repo? // ""),
|
||||
($v.original.url? // ""), ($v.original.ref? // "HEAD"),
|
||||
($v.locked.rev? // "") ] | @tsv
|
||||
' "$lock"
|
||||
)
|
||||
echo "$n"
|
||||
}
|
||||
|
||||
count_flatpak() {
|
||||
${lib.optionalString cfg.flatpak ''
|
||||
if command -v flatpak >/dev/null 2>&1; then
|
||||
flatpak remote-ls --updates --columns=application 2>/dev/null | ${pkgs.gnugrep}/bin/grep -c . || true
|
||||
return
|
||||
fi
|
||||
''}
|
||||
echo 0
|
||||
}
|
||||
|
||||
refresh_bar() { ${pkgs.procps}/bin/pkill -RTMIN+9 -x waybar 2>/dev/null || true; }
|
||||
|
||||
case "''${1:-status}" in
|
||||
check)
|
||||
nix=$(count_nix); fp=$(count_flatpak); total=$((nix + fp))
|
||||
prev=$("$JQ" -r '.total // 0' "$state" 2>/dev/null || echo 0)
|
||||
printf '{"nix":%d,"flatpak":%d,"total":%d,"ts":%d}\n' \
|
||||
"$nix" "$fp" "$total" "$(${pkgs.coreutils}/bin/date +%s)" > "$state"
|
||||
# Notify only when NEW updates appear, so a daily timer doesn't nag.
|
||||
if [ "$total" -gt 0 ] && [ "$total" -gt "$prev" ]; then
|
||||
msg="$nix flake input(s)"
|
||||
[ "$fp" -gt 0 ] && msg="$msg · $fp Flatpak(s)"
|
||||
${pkgs.libnotify}/bin/notify-send -a Nomarchy "Updates available" \
|
||||
"$msg — click the bar icon, or run sys-update."
|
||||
fi
|
||||
refresh_bar ;;
|
||||
status)
|
||||
total=$("$JQ" -r '.total // 0' "$state" 2>/dev/null || echo 0)
|
||||
[ "$total" -gt 0 ] 2>/dev/null || exit 0 # up to date / unchecked → hide
|
||||
nix=$("$JQ" -r '.nix // 0' "$state"); fp=$("$JQ" -r '.flatpak // 0' "$state")
|
||||
tip="Updates available"
|
||||
[ "$nix" -gt 0 ] && tip="$tip\n• $nix flake input(s) — sys-update"
|
||||
[ "$fp" -gt 0 ] && tip="$tip\n• $fp Flatpak(s) — flatpak update"
|
||||
printf '{"text":" %d","tooltip":"%s","class":"available"}\n' "$total" "$tip" ;;
|
||||
upgrade)
|
||||
echo "Checking…"; "$0" check
|
||||
nix=$("$JQ" -r '.nix // 0' "$state" 2>/dev/null || echo 0)
|
||||
fp=$("$JQ" -r '.flatpak // 0' "$state" 2>/dev/null || echo 0)
|
||||
echo "Pending: $nix flake input(s), $fp Flatpak(s)."
|
||||
if [ "$nix" -gt 0 ] && command -v sys-update >/dev/null 2>&1; then
|
||||
read -rp "Run sys-update (flake update + system rebuild)? [y/N] " a
|
||||
[ "$a" = y ] && sys-update
|
||||
fi
|
||||
if [ "$fp" -gt 0 ] && command -v flatpak >/dev/null 2>&1; then
|
||||
read -rp "Run flatpak update? [y/N] " a
|
||||
[ "$a" = y ] && flatpak update
|
||||
fi
|
||||
"$0" check
|
||||
echo "Done — press enter."; read -r _ || true ;;
|
||||
*) echo "usage: nomarchy-updates [check|status|upgrade]" >&2; exit 64 ;;
|
||||
esac
|
||||
'';
|
||||
in
|
||||
{
|
||||
config = lib.mkMerge [
|
||||
# Always on PATH so the Waybar module (incl. the static whole-swap themes)
|
||||
# can exec it; it self-gates at runtime.
|
||||
{ home.packages = [ nomarchy-updates ]; }
|
||||
|
||||
(lib.mkIf cfg.enable {
|
||||
systemd.user.services.nomarchy-updates = {
|
||||
Unit.Description = "Check for Nomarchy / nixpkgs / Flatpak updates";
|
||||
Service = {
|
||||
Type = "oneshot";
|
||||
ExecStart = "${nomarchy-updates}/bin/nomarchy-updates check";
|
||||
};
|
||||
};
|
||||
systemd.user.timers.nomarchy-updates = {
|
||||
Unit.Description = "Periodic update-awareness check";
|
||||
Timer = {
|
||||
OnStartupSec = "2min";
|
||||
OnCalendar = cfg.interval;
|
||||
Persistent = true;
|
||||
};
|
||||
Install.WantedBy = [ "timers.target" ];
|
||||
};
|
||||
})
|
||||
];
|
||||
}
|
||||
@@ -77,9 +77,9 @@ let
|
||||
|
||||
modules-left = [ "hyprland/workspaces" "hyprland/window" ];
|
||||
modules-center = [ "clock" ];
|
||||
modules-right = [ "tray" "pulseaudio" "network" "cpu" "memory" "custom/powerprofile" ]
|
||||
modules-right = [ "tray" "pulseaudio" "cpu" "memory" "custom/powerprofile" "custom/nightlight" ]
|
||||
++ lib.optional showLanguage "hyprland/language"
|
||||
++ [ "battery" "custom/notification" ];
|
||||
++ [ "battery" "custom/updates" "custom/notification" ];
|
||||
|
||||
"hyprland/workspaces" = {
|
||||
format = "{icon}";
|
||||
@@ -111,15 +111,8 @@ let
|
||||
on-click = "wpctl set-mute @DEFAULT_AUDIO_SINK@ toggle";
|
||||
};
|
||||
|
||||
network = {
|
||||
format-wifi = " {essid}";
|
||||
format-ethernet = "";
|
||||
format-disconnected = "";
|
||||
tooltip-format = "{ipaddr} via {gwaddr}";
|
||||
# nm-applet sits in the tray for the GUI path; this is the
|
||||
# keyboard-friendly one.
|
||||
on-click = "${config.nomarchy.terminal} -e nmtui";
|
||||
};
|
||||
# No network module: nm-applet lives in the tray (the GUI path), so the
|
||||
# bar's wifi/ethernet indicator would just duplicate it.
|
||||
|
||||
cpu.format = " {usage}%";
|
||||
memory.format = " {percentage}%";
|
||||
@@ -138,6 +131,30 @@ let
|
||||
on-click = "nomarchy-powerprofile-cycle";
|
||||
};
|
||||
|
||||
# Night-light (hyprsunset) indicator. Self-gates: the moon shows only while
|
||||
# the schedule runs; otherwise the status helper prints nothing => hidden
|
||||
# (enable / re-enable from the System menu). Click toggles instantly — writes
|
||||
# the in-flake on/off (settings.nightlight.on, no rebuild) and flips the unit
|
||||
# with systemctl, so the choice lands in the flake and survives reboot.
|
||||
"custom/nightlight" = {
|
||||
exec = "nomarchy-nightlight status";
|
||||
return-type = "json";
|
||||
interval = 3;
|
||||
on-click = "nomarchy-nightlight toggle";
|
||||
};
|
||||
|
||||
# Update awareness. Self-gates: hidden unless nomarchy.updates is enabled
|
||||
# AND the periodic check found something (the helper prints nothing then).
|
||||
# signal 9 lets the checker refresh it instantly; click opens the upgrade
|
||||
# flow in a terminal.
|
||||
"custom/updates" = {
|
||||
exec = "nomarchy-updates status";
|
||||
return-type = "json";
|
||||
interval = 1800;
|
||||
signal = 9;
|
||||
on-click = "${config.nomarchy.terminal} -e nomarchy-updates upgrade";
|
||||
};
|
||||
|
||||
# swaync notification bell + Do-Not-Disturb state. `-swb` streams JSON
|
||||
# (text/tooltip/class) on every change, so it tracks count and DND with
|
||||
# no polling. Left-click toggles the panel; right-click toggles DND.
|
||||
@@ -209,11 +226,21 @@ let
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
#tray, #pulseaudio, #network, #cpu, #memory, #custom-powerprofile, #language, #battery, #custom-notification {
|
||||
#tray, #pulseaudio, #cpu, #memory, #custom-powerprofile, #custom-nightlight, #custom-updates, #language, #battery, #custom-notification {
|
||||
color: @subtext;
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
/* Night-light active → warm tone, matching the filter it represents. */
|
||||
#custom-nightlight.on { color: @warn; }
|
||||
|
||||
/* Updates pending → accent, to draw the eye. */
|
||||
#custom-updates.available { color: @accent; }
|
||||
|
||||
/* The power-profile speedometer glyph renders small in its em box —
|
||||
size it up so it reads at a glance like the other indicators. */
|
||||
#custom-powerprofile { font-size: ${toString (t.fonts.size + 3)}pt; }
|
||||
|
||||
/* notifications waiting → accent; Do-Not-Disturb → muted bell-off */
|
||||
#custom-notification.notification { color: @accent; }
|
||||
#custom-notification.dnd-none,
|
||||
@@ -228,7 +255,14 @@ in
|
||||
{
|
||||
programs.waybar = lib.mkIf config.nomarchy.waybar.enable {
|
||||
enable = true;
|
||||
systemd.enable = true; # started/stopped with graphical-session.target
|
||||
# Launched from Hyprland's exec-once (hyprland.nix), NOT a systemd user
|
||||
# service. Bound to graphical-session.target the unit raced Hyprland's IPC
|
||||
# on a warm relogin — it started before the socket was up, exited, landed
|
||||
# in `failed`, and was never retried, so the bar vanished. exec-once only
|
||||
# fires once Hyprland is up, dodging the race; theme switches reload the
|
||||
# running bar via SIGUSR2 (nomarchy-theme-sync). No uwsm here to manage the
|
||||
# session target, so we don't depend on its lifecycle.
|
||||
systemd.enable = false;
|
||||
|
||||
# mkDefault so downstream can replace the whole bar config/style with
|
||||
# a plain home.nix assignment. For per-theme identity, prefer the
|
||||
|
||||
@@ -22,7 +22,7 @@ let
|
||||
'';
|
||||
in
|
||||
{
|
||||
imports = [ ./options.nix ./plymouth.nix ./file-manager.nix ./power.nix ./services.nix ];
|
||||
imports = [ ./options.nix ./plymouth.nix ./file-manager.nix ./power.nix ./services.nix ./hardware.nix ./timezone.nix ];
|
||||
|
||||
config = {
|
||||
# The safe half of distro branding: distroName flows into
|
||||
@@ -69,6 +69,11 @@ in
|
||||
console.earlySetup = lib.mkDefault true;
|
||||
boot.initrd.systemd.enable = lib.mkDefault true;
|
||||
|
||||
# Nomarchy roots are BTRFS, not ZFS, so adopt the 26.11 default early and
|
||||
# silence the eval warning the old `true` default emits. mkDefault, so a
|
||||
# genuine ZFS-root downstream can still force it back on.
|
||||
boot.zfs.forceImportRoot = lib.mkDefault false;
|
||||
|
||||
# ── Wayland session: Hyprland ────────────────────────────────────
|
||||
# Installs the binary, registers the session, wires up
|
||||
# xdg-desktop-portal-hyprland. Configuration is Home Manager's job.
|
||||
@@ -320,8 +325,69 @@ in
|
||||
nixos-rebuild switch --flake /etc/nixos#default "$@"
|
||||
'')
|
||||
# The desktop snapshot manager (browse / diff / restore / rollback over
|
||||
# snapper, elevating via polkit) — what `nomarchy-menu snapshot` launches.
|
||||
++ lib.optional cfg.snapper.enable pkgs.btrfs-assistant;
|
||||
# snapper, elevating via polkit). Kept for when nixpkgs fixes its crash.
|
||||
++ lib.optional cfg.snapper.enable pkgs.btrfs-assistant
|
||||
# Keyboard-driven snapper browser/restore — the fallback `nomarchy-menu
|
||||
# snapshot` actually launches while btrfs-assistant 2.2 segfaults on 26.05
|
||||
# (libbtrfsutil ABI). Runs as root (snapper is root-only here; the menu
|
||||
# opens it in a terminal via sudo, one password prompt), fzf to pick, with
|
||||
# browse/diff (read-only) and typed-`yes` confirmation before any write.
|
||||
++ lib.optional cfg.snapper.enable (pkgs.writeShellApplication {
|
||||
name = "nomarchy-snapshots";
|
||||
runtimeInputs = with pkgs; [ snapper fzf gawk gnugrep less coreutils systemd ];
|
||||
text = ''
|
||||
if [ "$(id -u)" -ne 0 ]; then
|
||||
echo "nomarchy-snapshots must run as root (snapper needs it) — use sudo." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mapfile -t configs < <(snapper list-configs | awk 'NR>2 {print $1}' | grep .)
|
||||
if [ "''${#configs[@]}" -eq 0 ]; then
|
||||
echo "No snapper configs found." >&2; exit 1
|
||||
elif [ "''${#configs[@]}" -eq 1 ]; then
|
||||
config="''${configs[0]}"
|
||||
else
|
||||
config=$(printf '%s\n' "''${configs[@]}" | fzf --prompt="snapper config> ") || exit 0
|
||||
fi
|
||||
|
||||
while :; do
|
||||
snap=$(snapper -c "$config" list \
|
||||
| fzf --header-lines=2 --prompt="[$config] pick a snapshot (Esc quits)> ") || exit 0
|
||||
num=$(awk '{print $1}' <<<"$snap")
|
||||
case "$num" in ""|*[!0-9]*) continue ;; esac
|
||||
|
||||
action=$(printf '%s\n' \
|
||||
"Browse changes since #$num (read-only)" \
|
||||
"Restore changed files to #$num (undochange)" \
|
||||
"Roll the system back to #$num (reboot)" \
|
||||
"↩ Back to the snapshot list" \
|
||||
| fzf --prompt="snapshot #$num> ") || exit 0
|
||||
|
||||
case "$action" in
|
||||
Browse*)
|
||||
snapper -c "$config" status "$num..0" | less -R || true ;;
|
||||
Restore*)
|
||||
read -rp "Revert files in '$config' to snapshot #$num? Type yes to confirm: " ans || continue
|
||||
if [ "$ans" = yes ]; then
|
||||
snapper -c "$config" undochange "$num..0"
|
||||
echo "Files restored. Press enter."; read -r _ || true
|
||||
fi ;;
|
||||
Roll*)
|
||||
if [ "$config" != root ]; then
|
||||
echo "Rollback applies to the 'root' config only; use Restore for '$config'. Press enter."
|
||||
read -r _ || true
|
||||
else
|
||||
read -rp "Roll the SYSTEM back to #$num and REBOOT now? Type yes to confirm: " ans || continue
|
||||
if [ "$ans" = yes ]; then
|
||||
snapper -c root rollback "$num"
|
||||
echo "Rolled back — rebooting…"; systemctl reboot
|
||||
fi
|
||||
fi ;;
|
||||
*) continue ;;
|
||||
esac
|
||||
done
|
||||
'';
|
||||
});
|
||||
|
||||
# Don't let boot entries fill the ESP over the years.
|
||||
boot.loader.systemd-boot.configurationLimit = lib.mkDefault 10;
|
||||
|
||||
195
modules/nixos/hardware.nix
Normal file
@@ -0,0 +1,195 @@
|
||||
# Hardware enablement beyond nixos-hardware.
|
||||
#
|
||||
# The nixos-hardware "common-*" profiles the installer selects cover the
|
||||
# BASICS (microcode, the Intel/AMD VA-API media stack, weekly fstrim), and
|
||||
# power.nix adds thermald + power-profiles-daemon. This module fills the GAP
|
||||
# above those: broadly-beneficial bits that default ON when the installer
|
||||
# detects the vendor (opt-OUT), and heavier/experimental bits behind opt-IN
|
||||
# toggles. The installer's hardware-db.sh probes what's present and writes the
|
||||
# matching nomarchy.hardware.* into the generated system.nix.
|
||||
#
|
||||
# Audited against the commons so we don't double-set: we add GuC/HuC, the
|
||||
# amd-pstate governor, the AMD VA-API env, GPU-compute runtimes, fprintd, and
|
||||
# the NPU driver — none of which the commons turn on.
|
||||
{ config, lib, pkgs, ... }:
|
||||
|
||||
let
|
||||
cfg = config.nomarchy.hardware;
|
||||
in
|
||||
{
|
||||
options.nomarchy.hardware = {
|
||||
intel = {
|
||||
enable = lib.mkEnableOption ''
|
||||
Intel CPU/GPU enablement (the installer turns this on when it detects
|
||||
an Intel CPU or GPU). Complements nixos-hardware's common-gpu-intel'';
|
||||
|
||||
guc = lib.mkOption {
|
||||
type = lib.types.bool;
|
||||
default = cfg.intel.enable;
|
||||
defaultText = lib.literalExpression "config.nomarchy.hardware.intel.enable";
|
||||
description = ''
|
||||
Load the GPU's GuC/HuC firmware via the i915 param
|
||||
(i915.enable_guc=3) — better power management and HuC-accelerated
|
||||
media. On by default with intel.enable. NOTE: this is the *i915*
|
||||
driver's param; the newer `xe` driver (Lunar Lake / Battlemage /
|
||||
Panther Lake and other recent Xe GPUs) enables GuC by default and
|
||||
ignores it, so the installer turns this off on xe-driver hardware.
|
||||
'';
|
||||
};
|
||||
|
||||
computeRuntime = lib.mkEnableOption ''
|
||||
Intel GPU compute: the OpenCL / Level Zero (intel-compute-runtime) and
|
||||
oneVPL (vpl-gpu-rt) runtimes for GPU compute and transcode. Opt-in (a
|
||||
few hundred MB) — the Intel counterpart to AMD ROCm'';
|
||||
};
|
||||
|
||||
amd = {
|
||||
enable = lib.mkEnableOption ''
|
||||
AMD CPU/GPU enablement (installer-set on an AMD CPU or GPU).
|
||||
Complements nixos-hardware's common-cpu-amd / common-gpu-amd'';
|
||||
|
||||
pstate = lib.mkOption {
|
||||
type = lib.types.bool;
|
||||
default = cfg.amd.enable;
|
||||
defaultText = lib.literalExpression "config.nomarchy.hardware.amd.enable";
|
||||
description = ''
|
||||
Use the amd-pstate EPP driver (amd_pstate=active) — the modern Zen
|
||||
power/perf governor that power-profiles-daemon drives per profile.
|
||||
On by default with amd.enable (broadly beneficial on Zen 2+).
|
||||
'';
|
||||
};
|
||||
|
||||
vaapi = lib.mkOption {
|
||||
type = lib.types.bool;
|
||||
default = cfg.amd.enable;
|
||||
defaultText = lib.literalExpression "config.nomarchy.hardware.amd.enable";
|
||||
description = ''
|
||||
Point VA-API at mesa's radeonsi (LIBVA_DRIVER_NAME=radeonsi) for
|
||||
hardware video decode/encode. On by default with amd.enable.
|
||||
'';
|
||||
};
|
||||
|
||||
rocm = {
|
||||
enable = lib.mkEnableOption ''
|
||||
AMD ROCm: the HIP / OpenCL GPU-compute stack (multi-GB closure).
|
||||
Opt-in — unlocks GPU PyTorch / Ollama on Radeon'';
|
||||
|
||||
gfxOverride = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = "";
|
||||
example = "11.0.0";
|
||||
description = ''
|
||||
HSA_OVERRIDE_GFX_VERSION for GPUs ROCm doesn't officially list
|
||||
(e.g. an RDNA3 780M iGPU, gfx1103, needs "11.0.0"). Empty = no
|
||||
override.
|
||||
'';
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
fingerprint = {
|
||||
enable = lib.mkEnableOption ''
|
||||
a fingerprint reader via fprintd (the installer turns this on when it
|
||||
detects a known reader). Enroll with `fprintd-enroll`'';
|
||||
|
||||
pam = lib.mkEnableOption ''
|
||||
using the fingerprint for login and sudo (PAM). Opt-in — password-only
|
||||
stays the default for the cautious; enroll a finger first'';
|
||||
};
|
||||
|
||||
npu.enable = lib.mkEnableOption ''
|
||||
the on-die NPU (AI accelerator) kernel driver — amdxdna on AMD (Ryzen
|
||||
AI), intel_vpu on Intel (Core Ultra and newer). Opt-in and experimental:
|
||||
this loads the in-kernel driver only; the userspace runtime (AMD XRT /
|
||||
oneAPI Level Zero NPU) is yours to add. Needs a recent kernel (see
|
||||
latestKernel)'';
|
||||
|
||||
latestKernel = lib.mkEnableOption ''
|
||||
the latest mainline kernel (pkgs.linuxPackages_latest) instead of the
|
||||
distro default — for very new hardware whose drivers (a fresh NPU, the
|
||||
`xe` GPU driver, new-platform enablement) only landed recently. Off by
|
||||
default; the default kernel already carries amd-pstate and amdxdna (6.14+)'';
|
||||
};
|
||||
|
||||
config = lib.mkMerge [
|
||||
# ── Intel ──────────────────────────────────────────────────────────
|
||||
(lib.mkIf cfg.intel.guc {
|
||||
boot.kernelParams = [ "i915.enable_guc=3" ];
|
||||
})
|
||||
(lib.mkIf cfg.intel.computeRuntime {
|
||||
hardware.graphics.extraPackages = with pkgs; [ intel-compute-runtime vpl-gpu-rt ];
|
||||
})
|
||||
|
||||
# ── AMD ────────────────────────────────────────────────────────────
|
||||
(lib.mkIf cfg.amd.pstate {
|
||||
boot.kernelParams = [ "amd_pstate=active" ];
|
||||
})
|
||||
(lib.mkIf cfg.amd.vaapi {
|
||||
# radeonsi itself comes from mesa (via common-gpu-amd); this just steers
|
||||
# libva at it. mkDefault so a hand-set value or another module wins.
|
||||
environment.sessionVariables.LIBVA_DRIVER_NAME = lib.mkDefault "radeonsi";
|
||||
hardware.graphics.extraPackages = [ pkgs.libva ];
|
||||
})
|
||||
(lib.mkIf cfg.amd.rocm.enable {
|
||||
hardware.graphics.extraPackages = [ pkgs.rocmPackages.clr pkgs.rocmPackages.clr.icd ];
|
||||
environment.sessionVariables = lib.optionalAttrs (cfg.amd.rocm.gfxOverride != "") {
|
||||
HSA_OVERRIDE_GFX_VERSION = cfg.amd.rocm.gfxOverride;
|
||||
};
|
||||
})
|
||||
|
||||
# ── Fingerprint ────────────────────────────────────────────────────
|
||||
(lib.mkIf cfg.fingerprint.enable {
|
||||
services.fprintd.enable = true;
|
||||
})
|
||||
(lib.mkIf (cfg.fingerprint.enable && cfg.fingerprint.pam) {
|
||||
security.pam.services.login.fprintAuth = true;
|
||||
security.pam.services.sudo.fprintAuth = true;
|
||||
})
|
||||
|
||||
# ── Newest kernel for very-new hardware (opt-in escape hatch) ──────
|
||||
(lib.mkIf cfg.latestKernel {
|
||||
boot.kernelPackages = lib.mkDefault pkgs.linuxPackages_latest;
|
||||
})
|
||||
|
||||
# ── NPU (in-kernel driver only; userspace runtime is BYO) ──────────
|
||||
# The driver has to actually be in the running kernel — warn (don't fail)
|
||||
# when it predates the shipped one, pointing at latestKernel.
|
||||
(lib.mkIf (cfg.npu.enable && cfg.amd.enable) {
|
||||
boot.kernelModules = [ "amdxdna" ];
|
||||
warnings = lib.optional
|
||||
(!lib.versionAtLeast config.boot.kernelPackages.kernel.version "6.14")
|
||||
''
|
||||
nomarchy.hardware.npu: the amdxdna driver needs kernel >= 6.14, but
|
||||
this config ships ${config.boot.kernelPackages.kernel.version}. Set
|
||||
nomarchy.hardware.latestKernel = true.'';
|
||||
})
|
||||
(lib.mkIf (cfg.npu.enable && cfg.intel.enable) {
|
||||
boot.kernelModules = [ "intel_vpu" ];
|
||||
warnings = lib.optional
|
||||
(!lib.versionAtLeast config.boot.kernelPackages.kernel.version "6.11")
|
||||
''
|
||||
nomarchy.hardware.npu: the intel_vpu driver (especially for newer
|
||||
NPUs) wants a recent kernel, but this config ships
|
||||
${config.boot.kernelPackages.kernel.version}. Consider
|
||||
nomarchy.hardware.latestKernel = true.'';
|
||||
})
|
||||
|
||||
# ── Sanity ─────────────────────────────────────────────────────────
|
||||
{
|
||||
assertions = [
|
||||
{
|
||||
assertion = cfg.amd.rocm.enable -> cfg.amd.enable;
|
||||
message = "nomarchy.hardware.amd.rocm.enable needs nomarchy.hardware.amd.enable.";
|
||||
}
|
||||
{
|
||||
assertion = cfg.intel.computeRuntime -> cfg.intel.enable;
|
||||
message = "nomarchy.hardware.intel.computeRuntime needs nomarchy.hardware.intel.enable.";
|
||||
}
|
||||
{
|
||||
assertion = cfg.npu.enable -> (cfg.amd.enable || cfg.intel.enable);
|
||||
message = "nomarchy.hardware.npu.enable needs a detected Intel or AMD platform.";
|
||||
}
|
||||
];
|
||||
}
|
||||
];
|
||||
}
|
||||
@@ -46,6 +46,16 @@
|
||||
audio.enable = lib.mkEnableOption "the Pipewire audio stack" // { default = true; };
|
||||
bluetooth.enable = lib.mkEnableOption "Bluetooth support with blueman" // { default = true; };
|
||||
|
||||
autoTimezone.enable = lib.mkEnableOption ''
|
||||
automatic timezone detection (geoclue + automatic-timezoned): the
|
||||
system timezone — and so the Waybar clock — follows your location, so
|
||||
travelling to another zone updates the time on its own. Off by default
|
||||
(it's a location service and needs the network); toggle it from the
|
||||
System menu, which lands the choice in the in-flake state file. Enabling
|
||||
it unsets the static time.timeZone for you (a runtime timezone needs
|
||||
/etc/localtime writable), so the menu toggle drives a system rebuild''
|
||||
// { default = false; };
|
||||
|
||||
snapper.enable = lib.mkEnableOption ''
|
||||
hourly/daily BTRFS timeline snapshots of / via snapper, plus the
|
||||
`nixos-rebuild-snap` pre-rebuild-snapshot helper. No-op unless the
|
||||
|
||||
@@ -75,7 +75,8 @@ in
|
||||
|
||||
printing.enable = lib.mkEnableOption ''
|
||||
CUPS printing with Avahi/mDNS, so network printers are auto-discovered
|
||||
(add vendor drivers via `services.printing.drivers`)'';
|
||||
(add vendor drivers via `services.printing.drivers`); the menu's
|
||||
System ▸ Printers entry opens the system-config-printer GUI'';
|
||||
|
||||
openrgb.enable = lib.mkEnableOption ''
|
||||
the OpenRGB daemon and GUI for controlling RGB lighting on peripherals
|
||||
@@ -246,6 +247,9 @@ in
|
||||
nssmdns4 = true;
|
||||
openFirewall = true;
|
||||
};
|
||||
# The CUPS admin GUI — the menu's System ▸ Printers entry execs it
|
||||
# (self-gated on this binary), so it ships with the printing service.
|
||||
environment.systemPackages = [ pkgs.system-config-printer ];
|
||||
})
|
||||
|
||||
(lib.mkIf cfg.openrgb.enable {
|
||||
|
||||
87
modules/nixos/timezone.nix
Normal file
@@ -0,0 +1,87 @@
|
||||
# Automatic timezone detection (opt-in) — the system timezone, and so the
|
||||
# Waybar clock, follows your location, so travelling to another zone updates
|
||||
# the time on its own. Geoclue feeds `automatic-timezoned`, which drives
|
||||
# /etc/localtime at runtime.
|
||||
#
|
||||
# In-flake state, menu-driven (the keyboard/night-light philosophy): the on/off
|
||||
# flag lives in the same theme-state.json under `settings.autoTimezone`
|
||||
# (git-tracked, reproducible), written by the System-menu toggle
|
||||
# (nomarchy-autotimezone). Because this is a SYSTEM service — not a user unit it
|
||||
# can start/stop instantly like night-light — the toggle drives a system rebuild
|
||||
# (plus a home switch for the Waybar-refresh watcher in timezone.nix home-side).
|
||||
{ config, lib, pkgs, ... }:
|
||||
|
||||
let
|
||||
cfg = config.nomarchy.system;
|
||||
|
||||
# Read the same state file the rest of the system side uses (Plymouth too),
|
||||
# wired by lib.mkFlake. The flag defaults off when the file is absent/sparse.
|
||||
state =
|
||||
if cfg.stateFile != null
|
||||
then builtins.fromJSON (builtins.readFile cfg.stateFile)
|
||||
else { };
|
||||
stateEnabled = (state.settings or { }).autoTimezone or false;
|
||||
|
||||
sync = lib.getExe pkgs.nomarchy-theme-sync;
|
||||
|
||||
# Menu/CLI toggle. Runs as the normal user (it owns the flake checkout +
|
||||
# writes the state); sudos only the system switch, like sys-update. Writes
|
||||
# the in-flake flag, then rebuilds: the system rebuild bakes the service +
|
||||
# the time.timeZone override, the home switch installs/removes the Waybar
|
||||
# refresh watcher — both read the same flag we just wrote.
|
||||
nomarchy-autotimezone = pkgs.writeShellScriptBin "nomarchy-autotimezone" ''
|
||||
set -e
|
||||
if [ "$(id -u)" -eq 0 ]; then
|
||||
echo "nomarchy-autotimezone: run as your normal user (it sudos the rebuild itself)" >&2
|
||||
exit 1
|
||||
fi
|
||||
flake="''${NOMARCHY_PATH:-$HOME/.nomarchy}"
|
||||
|
||||
cur=$(${sync} get settings.autoTimezone 2>/dev/null) || cur=false
|
||||
case "''${1:-toggle}" in
|
||||
on) new=true ;;
|
||||
off) new=false ;;
|
||||
toggle) case "$cur" in true|True) new=false ;; *) new=true ;; esac ;;
|
||||
status) echo "$cur"; exit 0 ;;
|
||||
*) echo "usage: nomarchy-autotimezone [toggle|on|off|status]" >&2; exit 64 ;;
|
||||
esac
|
||||
|
||||
${sync} --quiet set settings.autoTimezone "$new" --no-switch
|
||||
|
||||
if [ "$new" = true ]; then
|
||||
notify-send "Auto timezone" "Enabling — rebuilding the system…" 2>/dev/null || true
|
||||
else
|
||||
notify-send "Auto timezone" "Disabling — rebuilding the system…" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
sudo nixos-rebuild switch --flake "$flake#default"
|
||||
home-manager switch --flake "$flake"
|
||||
|
||||
if [ "$new" = true ]; then
|
||||
notify-send "Auto timezone on" "The clock now follows your location." 2>/dev/null || true
|
||||
else
|
||||
notify-send "Auto timezone off" "Back to the fixed timezone in system.nix." 2>/dev/null || true
|
||||
fi
|
||||
'';
|
||||
in
|
||||
{
|
||||
config = lib.mkMerge [
|
||||
{
|
||||
# Shipped unconditionally so the menu can enable the feature even while
|
||||
# it's off. Track the in-flake flag; mkDefault so a hand-set
|
||||
# nomarchy.system.autoTimezone.enable in system.nix still wins.
|
||||
environment.systemPackages = [ nomarchy-autotimezone ];
|
||||
nomarchy.system.autoTimezone.enable = lib.mkDefault stateEnabled;
|
||||
}
|
||||
|
||||
(lib.mkIf cfg.autoTimezone.enable {
|
||||
services.geoclue2.enable = true;
|
||||
services.automatic-timezoned.enable = true;
|
||||
# A runtime timezone needs /etc/localtime writable. automatic-timezoned
|
||||
# sets time.timeZone = null itself, but the installer writes a static
|
||||
# value at normal priority, which would collide (a hard eval error) —
|
||||
# mkForce null overrides both and resolves cleanly.
|
||||
time.timeZone = lib.mkForce null;
|
||||
})
|
||||
];
|
||||
}
|
||||
@@ -112,6 +112,7 @@ HARDWARE_DB=(
|
||||
# ----------------------------------------------------------------------------
|
||||
nomarchy_detect_hw() {
|
||||
local sys_vendor product_name cpu_vendor
|
||||
local nvidia=0 amdgpu=0 intelgpu=0
|
||||
sys_vendor=$(cat /sys/class/dmi/id/sys_vendor 2>/dev/null || echo "")
|
||||
product_name=$(cat /sys/class/dmi/id/product_name 2>/dev/null || echo "")
|
||||
cpu_vendor=$(lscpu 2>/dev/null | awk -F: '/Vendor ID/{gsub(/ /,"",$2); print $2; exit}')
|
||||
@@ -126,7 +127,7 @@ nomarchy_detect_hw() {
|
||||
|
||||
# GPU (lspci may list several; report all)
|
||||
if command -v lspci >/dev/null 2>&1; then
|
||||
local gpu_line nvidia=0 amdgpu=0 intelgpu=0
|
||||
local gpu_line
|
||||
while IFS= read -r gpu_line; do
|
||||
case "$gpu_line" in
|
||||
*"[10de:"*|*"NVIDIA"*) nvidia=1 ;;
|
||||
@@ -140,6 +141,67 @@ nomarchy_detect_hw() {
|
||||
(( intelgpu )) && { echo "MODULE common-gpu-intel"; echo "DETAIL gpu: Intel"; detected=1; }
|
||||
fi
|
||||
|
||||
# ── nomarchy.hardware.* enablement — the gap ABOVE the nixos-hardware
|
||||
# commons (GuC/HuC, amd-pstate, the AMD VA-API env, GPU-compute runtimes,
|
||||
# fprintd, the NPU driver). Emitted as NOMARCHY lines the installer turns
|
||||
# into nomarchy.hardware.* in system.nix; safe bits active, heavy opt-ins
|
||||
# commented.
|
||||
if [[ "$cpu_vendor" == "GenuineIntel" || $intelgpu -eq 1 ]]; then
|
||||
echo "NOMARCHY hardware.intel.enable=true"
|
||||
echo "DETAIL nomarchy.hardware.intel: GuC/HuC on; GPU-compute runtime opt-in"
|
||||
# i915.enable_guc applies to the i915 driver only. The newer `xe` driver
|
||||
# (Lunar Lake / Battlemage / Panther Lake, recent Xe GPUs) enables GuC by
|
||||
# default and ignores the param — so turn the toggle off when xe is bound.
|
||||
if lspci -k 2>/dev/null | grep -qiE 'driver in use: xe\b'; then
|
||||
echo "NOMARCHY hardware.intel.guc=false"
|
||||
echo "DETAIL Intel GPU on the xe driver → GuC default-on (i915 param skipped)"
|
||||
fi
|
||||
fi
|
||||
if [[ "$cpu_vendor" == "AuthenticAMD" || $amdgpu -eq 1 ]]; then
|
||||
echo "NOMARCHY hardware.amd.enable=true"
|
||||
echo "DETAIL nomarchy.hardware.amd: amd-pstate + VA-API on; ROCm opt-in"
|
||||
fi
|
||||
|
||||
# Fingerprint reader — libfprint's common USB vendor IDs (Goodix,
|
||||
# Synaptics/Validity, Elan, EgisTec, Upek, AuthenTec, FocalTech, NB).
|
||||
local have_fp=0
|
||||
if command -v lsusb >/dev/null 2>&1; then
|
||||
local vid
|
||||
for vid in 27c6 06cb 138a 04f3 1c7a 147e 08ff 2808 1fae; do
|
||||
lsusb 2>/dev/null | grep -qiE "ID ${vid}:" && { have_fp=1; break; }
|
||||
done
|
||||
else
|
||||
local f
|
||||
for f in /sys/bus/usb/devices/*/idVendor; do
|
||||
[[ -e "$f" ]] || continue
|
||||
case "$(cat "$f" 2>/dev/null)" in
|
||||
27c6|06cb|138a|04f3|1c7a|147e|08ff|2808|1fae) have_fp=1; break ;;
|
||||
esac
|
||||
done
|
||||
fi
|
||||
if [[ $have_fp -eq 1 ]]; then
|
||||
echo "NOMARCHY hardware.fingerprint.enable=true"
|
||||
echo "DETAIL fingerprint reader detected → fprintd (PAM login/sudo opt-in)"
|
||||
fi
|
||||
|
||||
# NPU (detect-only → a commented, experimental opt-in). Match the PCI
|
||||
# "Processing accelerators" class [1200] (or accelerator keywords) and
|
||||
# attribute by vendor — future-proof vs a per-device-ID list, so new gens
|
||||
# (e.g. Panther Lake) are caught without a code change. Known IDs kept for
|
||||
# reference: Intel VPU 8086:{7d1d,643e,ad1d,b03e}, AMD XDNA 1022:1502.
|
||||
if command -v lspci >/dev/null 2>&1; then
|
||||
local npu_line
|
||||
npu_line=$(lspci -nn 2>/dev/null \
|
||||
| grep -iE '\[1200\]|processing accelerat|neural|\[8086:(7d1d|643e|ad1d|b03e)\]|\[1022:1502\]' \
|
||||
| head -1)
|
||||
case "$npu_line" in
|
||||
*"[8086:"*|*Intel*)
|
||||
echo "NOMARCHY-NPU intel"; echo "DETAIL Intel NPU detected (opt-in, experimental)" ;;
|
||||
*"[1022:"*|*"Advanced Micro Devices"*|*AMD*)
|
||||
echo "NOMARCHY-NPU amd"; echo "DETAIL AMD XDNA NPU detected (opt-in, experimental)" ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
# Chassis (glob test, not compgen — nixpkgs' non-interactive bash
|
||||
# is built without the completion builtins)
|
||||
local bats=(/sys/class/power_supply/BAT*)
|
||||
|
||||
@@ -54,6 +54,15 @@ if [[ $EUID -ne 0 ]]; then
|
||||
exec sudo --preserve-env "$0" "$@"
|
||||
fi
|
||||
|
||||
# `sudo --preserve-env` (needed to carry the NOMARCHY_* vars) also drags in
|
||||
# the live session user's HOME=/home/nomarchy. Root-run `nix` calls below
|
||||
# would then scribble an eval cache + .nix-defexpr into /home/nomarchy —
|
||||
# and the in-chroot one lands on the TARGET disk as a stray, orphaned
|
||||
# /home/nomarchy (no such user on the installed system). Pin root's own
|
||||
# HOME so every root nix invocation stays in /root; the user activation
|
||||
# sets HOME=/home/$USERNAME explicitly and is unaffected.
|
||||
export HOME=/root
|
||||
|
||||
header "Nomarchy installer" "NixOS, themed and ready to go."
|
||||
|
||||
[[ -d /sys/firmware/efi ]] \
|
||||
@@ -210,6 +219,8 @@ section "Hardware detection"
|
||||
source "$SHARE/hardware-db.sh"
|
||||
|
||||
HW_PROFILES=()
|
||||
HW_NOMARCHY=() # NOMARCHY hardware.* assignments from detection
|
||||
NPU_VENDOR="" # "intel" | "amd" if an NPU was detected (commented opt-in)
|
||||
hw_mode="${NOMARCHY_HW:-auto}"
|
||||
if [[ "$hw_mode" == "none" ]]; then
|
||||
info "Hardware profiles skipped."
|
||||
@@ -220,8 +231,10 @@ else
|
||||
if [[ -n "$detection" ]]; then
|
||||
while IFS= read -r line; do
|
||||
case "$line" in
|
||||
MODULE\ *) HW_PROFILES+=("${line#MODULE }") ;;
|
||||
DETAIL\ *) info "→ ${line#DETAIL }" ;;
|
||||
MODULE\ *) HW_PROFILES+=("${line#MODULE }") ;;
|
||||
NOMARCHY-NPU\ *) NPU_VENDOR="${line#NOMARCHY-NPU }" ;;
|
||||
NOMARCHY\ *) HW_NOMARCHY+=("${line#NOMARCHY }") ;;
|
||||
DETAIL\ *) info "→ ${line#DETAIL }" ;;
|
||||
esac
|
||||
done <<< "$detection"
|
||||
fi
|
||||
@@ -428,6 +441,57 @@ NIX
|
||||
)
|
||||
fi
|
||||
|
||||
# Hardware enablement (nomarchy.hardware.*): what hardware-db.sh detected
|
||||
# above the nixos-hardware commons. Safe defaults active; the heavier or
|
||||
# experimental opt-ins written commented for the user to flip on.
|
||||
HARDWARE_CONFIG=""
|
||||
if [[ ${#HW_NOMARCHY[@]} -gt 0 || -n "$NPU_VENDOR" ]]; then
|
||||
has_intel=0; has_amd=0; has_fp=0; intel_guc_off=0
|
||||
if [[ ${#HW_NOMARCHY[@]} -gt 0 ]]; then
|
||||
for nm in "${HW_NOMARCHY[@]}"; do
|
||||
case "$nm" in
|
||||
hardware.intel.enable=true) has_intel=1 ;;
|
||||
hardware.intel.guc=false) intel_guc_off=1 ;;
|
||||
hardware.amd.enable=true) has_amd=1 ;;
|
||||
hardware.fingerprint.enable=true) has_fp=1 ;;
|
||||
esac
|
||||
done
|
||||
fi
|
||||
hw_lines=" # Hardware enablement (auto-detected). Safe defaults are active;
|
||||
# the heavier opt-ins are commented — uncomment to turn them on."
|
||||
if [[ $has_intel -eq 1 ]]; then
|
||||
hw_lines+="
|
||||
nomarchy.hardware.intel.enable = true; # GuC/HuC firmware (i915.enable_guc=3)"
|
||||
if [[ $intel_guc_off -eq 1 ]]; then
|
||||
hw_lines+="
|
||||
nomarchy.hardware.intel.guc = false; # GPU on the xe driver → GuC is default-on"
|
||||
fi
|
||||
hw_lines+="
|
||||
# nomarchy.hardware.intel.computeRuntime = true; # OpenCL/oneVPL GPU compute (opt-in)"
|
||||
fi
|
||||
if [[ $has_amd -eq 1 ]]; then
|
||||
hw_lines+="
|
||||
nomarchy.hardware.amd.enable = true; # amd-pstate EPP + radeonsi VA-API
|
||||
# nomarchy.hardware.amd.rocm.enable = true; # ROCm GPU compute (multi-GB, opt-in)
|
||||
# nomarchy.hardware.amd.rocm.gfxOverride = \"\"; # e.g. \"11.0.0\" for an unlisted iGPU"
|
||||
fi
|
||||
if [[ $has_fp -eq 1 ]]; then
|
||||
hw_lines+="
|
||||
nomarchy.hardware.fingerprint.enable = true; # fprintd (enroll: fprintd-enroll)
|
||||
# nomarchy.hardware.fingerprint.pam = true; # use it for login + sudo (opt-in)"
|
||||
fi
|
||||
if [[ -n "$NPU_VENDOR" ]]; then
|
||||
hw_lines+="
|
||||
# nomarchy.hardware.npu.enable = true; # $NPU_VENDOR NPU driver (experimental; userspace runtime BYO)
|
||||
# nomarchy.hardware.latestKernel = true; # newest kernel if the NPU driver isn't in the shipped one"
|
||||
fi
|
||||
HARDWARE_CONFIG=$(cat <<NIX
|
||||
|
||||
$hw_lines
|
||||
NIX
|
||||
)
|
||||
fi
|
||||
|
||||
# initialHashedPassword is safe to template: mkpasswd's alphabet is
|
||||
# [a-zA-Z0-9./$] — no Nix string metacharacters.
|
||||
cat > "$FLAKE_DIR/system.nix" <<EOF
|
||||
@@ -459,7 +523,7 @@ cat > "$FLAKE_DIR/system.nix" <<EOF
|
||||
extraGroups = [ "wheel" "networkmanager" "video" "input" ];
|
||||
initialHashedPassword = "$HASHED_PASSWORD";
|
||||
};
|
||||
$AUTOLOGIN_CONFIG$POWER_CONFIG$RESUME_CONFIG
|
||||
$AUTOLOGIN_CONFIG$POWER_CONFIG$HARDWARE_CONFIG$RESUME_CONFIG
|
||||
# Hourly/daily BTRFS timeline snapshots + nixos-rebuild-snap.
|
||||
nomarchy.system.snapper.enable = true;
|
||||
|
||||
@@ -573,6 +637,8 @@ cat > /mnt/root/nomarchy-hm-activate.sh <<EOF
|
||||
set -ex
|
||||
exec > /var/log/nomarchy-hm-preactivate.log 2>&1
|
||||
export PATH=/run/current-system/sw/bin:\$PATH
|
||||
# Keep root's nix state in /root, not a stray /home/nomarchy on the target.
|
||||
export HOME=/root
|
||||
# Normally pre-built in the live env and copied over; the in-chroot
|
||||
# build (default substituters — the live-side build already proved the
|
||||
# no-network case) is a last-resort fallback.
|
||||
|
||||
@@ -165,12 +165,17 @@ def run_switch() -> None:
|
||||
# Persistent (timeout 0): stays up for the whole rebuild — replaced
|
||||
# in place by the success/failure notification below — so a multi-
|
||||
# minute switch never looks like it silently failed.
|
||||
notify("Applying theme — rebuilding the desktop…", persistent=True)
|
||||
notify("Applying changes — rebuilding the desktop…", persistent=True)
|
||||
result = subprocess.run(argv) # stream output to the caller's terminal
|
||||
if result.returncode != 0:
|
||||
notify("Theme rebuild FAILED — see terminal / journal", urgency="critical")
|
||||
notify("Rebuild FAILED — see terminal / journal", urgency="critical")
|
||||
die("rebuild failed (state file already updated; fix and re-run)")
|
||||
notify("Theme applied ✓")
|
||||
notify("Changes applied ✓")
|
||||
# Waybar runs from Hyprland's exec-once (not a systemd unit HM would restart
|
||||
# on switch), so nudge the running bar to re-read its freshly rebuilt
|
||||
# config/style. SIGUSR2 = reload; harmless no-op if waybar isn't running.
|
||||
if shutil.which("pkill"):
|
||||
subprocess.run(["pkill", "--signal", "SIGUSR2", "-x", "waybar"], check=False)
|
||||
|
||||
|
||||
# ─── Theme assets (wallpapers) ────────────────────────────────────────────
|
||||
@@ -289,7 +294,11 @@ def cmd_set(args) -> None:
|
||||
check_fonts(state)
|
||||
if not args.no_switch:
|
||||
run_switch()
|
||||
apply_wallpaper(state)
|
||||
# Only wallpaper/theme keys change what swww shows; skip the re-apply
|
||||
# (and its transition) for unrelated keys like ui.* or settings.* so a
|
||||
# gaps tweak or a feature toggle doesn't flash the background.
|
||||
if args.path.split(".")[0] in ("wallpaper", "slug"):
|
||||
apply_wallpaper(state)
|
||||
|
||||
|
||||
def cmd_get(args) -> None:
|
||||
|
||||
@@ -16,12 +16,19 @@
|
||||
|
||||
# ── Opt-in features — uncomment and tweak to enable ─────────────────
|
||||
# nomarchy.nightlight = { # scheduled blue-light filter (hyprsunset)
|
||||
# enable = true;
|
||||
# enable = true; # declarative opt-in (or just enable it from
|
||||
# # the menu: System › Night light)
|
||||
# temperature = 4000; # night warmth in K — lower is warmer
|
||||
# sunset = "20:00";
|
||||
# sunrise = "07:00";
|
||||
# };
|
||||
#
|
||||
# nomarchy.updates = { # passive update-awareness indicator + notification
|
||||
# enable = true;
|
||||
# interval = "daily"; # how often to check (systemd OnCalendar)
|
||||
# flatpak = true; # also count Flatpak updates when flatpak is on
|
||||
# };
|
||||
#
|
||||
# nomarchy.monitors = [ # declarative per-output layout, hotplug-applied
|
||||
# { name = "eDP-1"; position = "0x0"; }
|
||||
# { name = "HDMI-A-1"; position = "auto-right"; }
|
||||
@@ -31,8 +38,9 @@
|
||||
# "keychron-keychron-k2" = { layout = "de"; };
|
||||
# };
|
||||
#
|
||||
# nomarchy.keyboard.layouts = [ "de" "fr" ]; # rofi-prompt for a layout when a
|
||||
# # new keyboard connects + remember it
|
||||
# nomarchy.keyboard.layouts = [ "de" "fr" ]; # rofi-prompt for a layout when a new
|
||||
# # keyboard connects + remember it in the
|
||||
# # flake state (graduates to .devices above)
|
||||
|
||||
# ── Application suite ───────────────────────────────────────────────
|
||||
# A starter complete-workstation set, installed for your user — yours to
|
||||
|
||||
@@ -31,6 +31,23 @@
|
||||
# thermal.enable = true; # thermald (Intel CPUs)
|
||||
# };
|
||||
#
|
||||
# nomarchy.system.autoTimezone.enable = true; # clock follows your location
|
||||
# # (geoclue); travelling updates the
|
||||
# # time on its own. Unsets time.timeZone
|
||||
# # above. Easier toggled from System menu.
|
||||
#
|
||||
# nomarchy.hardware = { # enablement above nixos-hardware (installer-detected)
|
||||
# intel.enable = true; # GuC/HuC firmware; installer sets this on Intel
|
||||
# intel.computeRuntime = true; # OpenCL/oneVPL GPU compute (opt-in)
|
||||
# amd.enable = true; # amd-pstate + radeonsi VA-API; installer-set on AMD
|
||||
# amd.rocm.enable = true; # ROCm GPU compute (multi-GB, opt-in)
|
||||
# amd.rocm.gfxOverride = "11.0.0"; # HSA override for an unlisted iGPU
|
||||
# fingerprint.enable = true; # fprintd; installer-set when a reader is detected
|
||||
# fingerprint.pam = true; # use the fingerprint for login + sudo
|
||||
# npu.enable = true; # on-die NPU driver (experimental; userspace runtime BYO)
|
||||
# latestKernel = true; # newest kernel for very-new hardware (drivers not yet in the default)
|
||||
# };
|
||||
#
|
||||
# nomarchy.services.tailscale.enable = true; # mesh VPN — then `sudo tailscale up`
|
||||
# nomarchy.services.syncthing.enable = true; # file sync — GUI at http://127.0.0.1:8384
|
||||
# nomarchy.services.podman.enable = true; # rootless containers (docker → podman)
|
||||
|
||||
@@ -55,5 +55,6 @@
|
||||
"terminalOpacity": 0.96,
|
||||
"blur": true,
|
||||
"shadow": true
|
||||
}
|
||||
},
|
||||
"settings": {}
|
||||
}
|
||||
|
||||
BIN
themes/catppuccin-latte/preview.png
Normal file
|
After Width: | Height: | Size: 107 KiB |
BIN
themes/catppuccin/preview.png
Normal file
|
After Width: | Height: | Size: 39 KiB |
BIN
themes/ethereal/preview.png
Normal file
|
After Width: | Height: | Size: 152 KiB |
BIN
themes/everforest/preview.png
Normal file
|
After Width: | Height: | Size: 92 KiB |
BIN
themes/flexoki-light/preview.png
Normal file
|
After Width: | Height: | Size: 31 KiB |
BIN
themes/gruvbox/preview.png
Normal file
|
After Width: | Height: | Size: 221 KiB |
BIN
themes/hackerman/preview.png
Normal file
|
After Width: | Height: | Size: 112 KiB |
BIN
themes/kanagawa/preview.png
Normal file
|
After Width: | Height: | Size: 229 KiB |
107
themes/kanagawa/rofi.rasi
Normal file
@@ -0,0 +1,107 @@
|
||||
/**
|
||||
* Nomarchy Kanagawa — rofi whole-swap (themes/<slug>/rofi.rasi).
|
||||
* Ink-and-paper identity: a sumi-ink canvas inside a warm washi-paper frame
|
||||
* (the same text-toned border the theme uses on windows — the only warm
|
||||
* frame of the four), the inputbar a deeper ink well, and the crystal-blue
|
||||
* wave reserved for the selected row. Soft rounding, an unhurried feel.
|
||||
* The `configuration {}` block is omitted on purpose — modi/terminal/
|
||||
* show-icons come from modules/home/rofi.nix.
|
||||
*/
|
||||
|
||||
* {
|
||||
sumi: #1f1f28; /* sumi ink — the canvas */
|
||||
well: #090618; /* deeper ink — inputbar + zebra */
|
||||
paper: #dcd7ba; /* washi paper — text + frame */
|
||||
parch: #c8c093; /* parchment — the prompt */
|
||||
grey: #727169;
|
||||
wave: #7e9cd8; /* crystal-blue wave — the selection */
|
||||
|
||||
font: "JetBrainsMono Nerd Font 14";
|
||||
background-color: transparent;
|
||||
text-color: @paper;
|
||||
}
|
||||
|
||||
window {
|
||||
background-color: @sumi;
|
||||
border: 2px;
|
||||
border-color: @paper; /* the warm paper frame (kanagawa's border tone) */
|
||||
border-radius: 10px;
|
||||
width: 40%;
|
||||
location: center;
|
||||
anchor: center;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
mainbox {
|
||||
background-color: transparent;
|
||||
children: [ inputbar, message, listview ];
|
||||
spacing: 0px;
|
||||
}
|
||||
|
||||
inputbar {
|
||||
children: [ prompt, entry ];
|
||||
background-color: @well; /* the ink well */
|
||||
text-color: @paper;
|
||||
border-radius: 8px;
|
||||
padding: 10px 12px;
|
||||
spacing: 8px;
|
||||
margin: 0px 0px 8px 0px;
|
||||
}
|
||||
|
||||
prompt { text-color: @parch; }
|
||||
|
||||
entry {
|
||||
text-color: @paper;
|
||||
cursor: text;
|
||||
placeholder: "search…";
|
||||
placeholder-color: @grey;
|
||||
}
|
||||
|
||||
listview {
|
||||
background-color: transparent;
|
||||
columns: 1;
|
||||
lines: 8;
|
||||
dynamic: true;
|
||||
scrollbar: false;
|
||||
spacing: 4px;
|
||||
padding: 8px 0px 0px 0px;
|
||||
}
|
||||
|
||||
element {
|
||||
background-color: transparent;
|
||||
text-color: @paper;
|
||||
orientation: horizontal;
|
||||
border-radius: 8px;
|
||||
padding: 10px 14px;
|
||||
spacing: 12px;
|
||||
}
|
||||
|
||||
element alternate.normal {
|
||||
background-color: @well;
|
||||
}
|
||||
element selected.normal {
|
||||
background-color: @wave;
|
||||
text-color: @sumi;
|
||||
}
|
||||
|
||||
element-icon {
|
||||
background-color: transparent;
|
||||
size: 36px;
|
||||
cursor: inherit;
|
||||
}
|
||||
element-text {
|
||||
background-color: transparent;
|
||||
text-color: inherit;
|
||||
highlight: bold;
|
||||
vertical-align: 0.5;
|
||||
}
|
||||
|
||||
message { padding: 8px 0px 0px 0px; }
|
||||
textbox {
|
||||
background-color: @well;
|
||||
text-color: @paper;
|
||||
border-radius: 8px;
|
||||
padding: 8px 12px;
|
||||
}
|
||||
|
||||
scrollbar { width: 0px; }
|
||||
BIN
themes/lumon/preview.png
Normal file
|
After Width: | Height: | Size: 30 KiB |
110
themes/lumon/rofi.rasi
Normal file
@@ -0,0 +1,110 @@
|
||||
/**
|
||||
* Nomarchy Lumon — rofi whole-swap (themes/<slug>/rofi.rasi).
|
||||
* Clinical-terminal identity (the MDR readout): a thick cyan bezel around a
|
||||
* cold teal screen, the inputbar FRAMED rather than filled (a data field,
|
||||
* not a button), near-white headings, cyan selection. Faintly softened
|
||||
* corners keep it boxy without going razor-sharp like Retro 82.
|
||||
* The `configuration {}` block is omitted on purpose — modi/terminal/
|
||||
* show-icons come from modules/home/rofi.nix.
|
||||
*/
|
||||
|
||||
* {
|
||||
base: #16242d;
|
||||
surface: #1b2d40;
|
||||
overlay: #304860;
|
||||
text: #d6e2ee;
|
||||
ice: #f2fcff; /* near-white — the prompt */
|
||||
cyan: #8bc9eb; /* the Lumon glow — bezel + select */
|
||||
|
||||
font: "JetBrainsMono Nerd Font 13";
|
||||
background-color: transparent;
|
||||
text-color: @text;
|
||||
}
|
||||
|
||||
window {
|
||||
background-color: @base;
|
||||
border: 3px; /* a screen bezel */
|
||||
border-color: @cyan;
|
||||
border-radius: 4px; /* boxy, faintly softened */
|
||||
width: 40%;
|
||||
location: center;
|
||||
anchor: center;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
mainbox {
|
||||
background-color: transparent;
|
||||
children: [ inputbar, message, listview ];
|
||||
spacing: 0px;
|
||||
}
|
||||
|
||||
/* Framed, not filled — a readout field rather than a button. */
|
||||
inputbar {
|
||||
children: [ prompt, entry ];
|
||||
background-color: transparent;
|
||||
text-color: @text;
|
||||
border: 1px;
|
||||
border-color: @overlay;
|
||||
border-radius: 4px;
|
||||
padding: 10px 12px;
|
||||
spacing: 8px;
|
||||
margin: 0px 0px 8px 0px;
|
||||
}
|
||||
|
||||
prompt { text-color: @ice; }
|
||||
|
||||
entry {
|
||||
text-color: @text;
|
||||
cursor: text;
|
||||
placeholder: "refine…";
|
||||
placeholder-color: @overlay;
|
||||
}
|
||||
|
||||
listview {
|
||||
background-color: transparent;
|
||||
columns: 1;
|
||||
lines: 8;
|
||||
dynamic: true;
|
||||
scrollbar: false;
|
||||
spacing: 4px;
|
||||
padding: 8px 0px 0px 0px;
|
||||
}
|
||||
|
||||
element {
|
||||
background-color: transparent;
|
||||
text-color: @text;
|
||||
orientation: horizontal;
|
||||
border-radius: 4px;
|
||||
padding: 10px 14px;
|
||||
spacing: 12px;
|
||||
}
|
||||
|
||||
element alternate.normal {
|
||||
background-color: @surface;
|
||||
}
|
||||
element selected.normal {
|
||||
background-color: @cyan;
|
||||
text-color: @base;
|
||||
}
|
||||
|
||||
element-icon {
|
||||
background-color: transparent;
|
||||
size: 36px;
|
||||
cursor: inherit;
|
||||
}
|
||||
element-text {
|
||||
background-color: transparent;
|
||||
text-color: inherit;
|
||||
highlight: bold;
|
||||
vertical-align: 0.5;
|
||||
}
|
||||
|
||||
message { padding: 8px 0px 0px 0px; }
|
||||
textbox {
|
||||
background-color: @surface;
|
||||
text-color: @text;
|
||||
border-radius: 4px;
|
||||
padding: 8px 12px;
|
||||
}
|
||||
|
||||
scrollbar { width: 0px; }
|
||||
BIN
themes/matte-black/preview.png
Normal file
|
After Width: | Height: | Size: 172 KiB |
BIN
themes/miasma/preview.png
Normal file
|
After Width: | Height: | Size: 101 KiB |
BIN
themes/nord/preview.png
Normal file
|
After Width: | Height: | Size: 187 KiB |
110
themes/nord/rofi.rasi
Normal file
@@ -0,0 +1,110 @@
|
||||
/**
|
||||
* Nomarchy Nord — rofi whole-swap (themes/<slug>/rofi.rasi).
|
||||
* Frost identity: a soft, rounded "frost panel". The window wears a calm
|
||||
* frost border; the selected row lifts to the brighter frost, and the
|
||||
* prompt picks up the aurora purple for the one point of warmth.
|
||||
* The `configuration {}` block is omitted on purpose — modi/terminal/
|
||||
* show-icons come from modules/home/rofi.nix. Element structure mirrors
|
||||
* the generated theme so the theme-grid picker's per-invocation
|
||||
* -theme-str (vertical cards) still lays out correctly.
|
||||
*/
|
||||
|
||||
* {
|
||||
base: #2e3440;
|
||||
surface: #3b4252;
|
||||
overlay: #4c566a;
|
||||
text: #d8dee9;
|
||||
frost1: #81a1c1; /* calm frost — the border */
|
||||
frost2: #88c0d0; /* bright frost — the selection */
|
||||
aurora: #b48ead; /* the single accent pop — prompt */
|
||||
|
||||
font: "JetBrainsMono Nerd Font 13";
|
||||
background-color: transparent;
|
||||
text-color: @text;
|
||||
}
|
||||
|
||||
window {
|
||||
background-color: @base;
|
||||
border: 2px;
|
||||
border-color: @frost1;
|
||||
border-radius: 14px; /* Nord is soft and rounded */
|
||||
width: 40%;
|
||||
location: center;
|
||||
anchor: center;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
mainbox {
|
||||
background-color: transparent;
|
||||
children: [ inputbar, message, listview ];
|
||||
spacing: 0px;
|
||||
}
|
||||
|
||||
inputbar {
|
||||
children: [ prompt, entry ];
|
||||
background-color: @surface;
|
||||
text-color: @text;
|
||||
border-radius: 10px;
|
||||
padding: 10px 12px;
|
||||
spacing: 8px;
|
||||
margin: 0px 0px 8px 0px;
|
||||
}
|
||||
|
||||
prompt { text-color: @aurora; }
|
||||
|
||||
entry {
|
||||
text-color: @text;
|
||||
cursor: text;
|
||||
placeholder: "Search…";
|
||||
placeholder-color: @overlay;
|
||||
}
|
||||
|
||||
listview {
|
||||
background-color: transparent;
|
||||
columns: 1;
|
||||
lines: 8;
|
||||
dynamic: true;
|
||||
scrollbar: false;
|
||||
spacing: 4px;
|
||||
padding: 8px 0px 0px 0px;
|
||||
}
|
||||
|
||||
element {
|
||||
background-color: transparent;
|
||||
text-color: @text;
|
||||
orientation: horizontal;
|
||||
border-radius: 8px;
|
||||
padding: 10px 14px;
|
||||
spacing: 12px;
|
||||
}
|
||||
|
||||
/* Faint frost-grey zebra; the selected row below still wins. */
|
||||
element alternate.normal {
|
||||
background-color: @surface;
|
||||
}
|
||||
element selected.normal {
|
||||
background-color: @frost2;
|
||||
text-color: @base;
|
||||
}
|
||||
|
||||
element-icon {
|
||||
background-color: transparent;
|
||||
size: 36px;
|
||||
cursor: inherit;
|
||||
}
|
||||
element-text {
|
||||
background-color: transparent;
|
||||
text-color: inherit;
|
||||
highlight: bold;
|
||||
vertical-align: 0.5;
|
||||
}
|
||||
|
||||
message { padding: 8px 0px 0px 0px; }
|
||||
textbox {
|
||||
background-color: @surface;
|
||||
text-color: @text;
|
||||
border-radius: 8px;
|
||||
padding: 8px 12px;
|
||||
}
|
||||
|
||||
scrollbar { width: 0px; }
|
||||
BIN
themes/osaka-jade/preview.png
Normal file
|
After Width: | Height: | Size: 138 KiB |
BIN
themes/retro-82/preview.png
Normal file
|
After Width: | Height: | Size: 160 KiB |
109
themes/retro-82/rofi.rasi
Normal file
@@ -0,0 +1,109 @@
|
||||
/**
|
||||
* Nomarchy Retro 82 — rofi whole-swap (themes/<slug>/rofi.rasi).
|
||||
* CRT-terminal identity: SQUARE corners (the giveaway against every rounded
|
||||
* theme), amber-on-navy phosphor, a monospace face. The window wears a hard
|
||||
* amber frame; the inputbar sits over a teal underline (a stray scanline),
|
||||
* and the prompt + selected row burn amber.
|
||||
* The `configuration {}` block is omitted on purpose — modi/terminal/
|
||||
* show-icons come from modules/home/rofi.nix.
|
||||
*/
|
||||
|
||||
* {
|
||||
navy: #05182e;
|
||||
panel: #00172e;
|
||||
overlay: #134e5a;
|
||||
sand: #f6dcac; /* phosphor cream — the foreground */
|
||||
teal: #3f8f8a; /* the underline scanline */
|
||||
amber: #faa968; /* the glow — frame, prompt, select */
|
||||
|
||||
font: "JetBrainsMono Nerd Font 13";
|
||||
background-color: transparent;
|
||||
text-color: @sand;
|
||||
}
|
||||
|
||||
window {
|
||||
background-color: @navy;
|
||||
border: 2px;
|
||||
border-color: @amber;
|
||||
border-radius: 0px; /* sharp: this is a terminal */
|
||||
width: 40%;
|
||||
location: center;
|
||||
anchor: center;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
mainbox {
|
||||
background-color: transparent;
|
||||
children: [ inputbar, message, listview ];
|
||||
spacing: 0px;
|
||||
}
|
||||
|
||||
inputbar {
|
||||
children: [ prompt, entry ];
|
||||
background-color: @panel;
|
||||
text-color: @sand;
|
||||
border-bottom: 2px; /* a teal scanline under the field */
|
||||
border-color: @teal;
|
||||
border-radius: 0px;
|
||||
padding: 10px 12px;
|
||||
spacing: 8px;
|
||||
margin: 0px 0px 8px 0px;
|
||||
}
|
||||
|
||||
prompt { text-color: @amber; }
|
||||
|
||||
entry {
|
||||
text-color: @sand;
|
||||
cursor: text;
|
||||
placeholder: "search…";
|
||||
placeholder-color: @overlay;
|
||||
}
|
||||
|
||||
listview {
|
||||
background-color: transparent;
|
||||
columns: 1;
|
||||
lines: 8;
|
||||
dynamic: true;
|
||||
scrollbar: false;
|
||||
spacing: 2px;
|
||||
padding: 8px 0px 0px 0px;
|
||||
}
|
||||
|
||||
element {
|
||||
background-color: transparent;
|
||||
text-color: @sand;
|
||||
orientation: horizontal;
|
||||
border-radius: 0px; /* square rows too */
|
||||
padding: 10px 14px;
|
||||
spacing: 12px;
|
||||
}
|
||||
|
||||
element alternate.normal {
|
||||
background-color: @panel;
|
||||
}
|
||||
element selected.normal {
|
||||
background-color: @amber;
|
||||
text-color: @navy;
|
||||
}
|
||||
|
||||
element-icon {
|
||||
background-color: transparent;
|
||||
size: 36px;
|
||||
cursor: inherit;
|
||||
}
|
||||
element-text {
|
||||
background-color: transparent;
|
||||
text-color: inherit;
|
||||
highlight: bold;
|
||||
vertical-align: 0.5;
|
||||
}
|
||||
|
||||
message { padding: 8px 0px 0px 0px; }
|
||||
textbox {
|
||||
background-color: @panel;
|
||||
text-color: @sand;
|
||||
border-radius: 0px;
|
||||
padding: 8px 12px;
|
||||
}
|
||||
|
||||
scrollbar { width: 0px; }
|
||||
BIN
themes/ristretto/preview.png
Normal file
|
After Width: | Height: | Size: 80 KiB |
BIN
themes/rose-pine/preview.png
Normal file
|
After Width: | Height: | Size: 70 KiB |
BIN
themes/summer-day/preview.png
Normal file
|
After Width: | Height: | Size: 93 KiB |
@@ -47,8 +47,9 @@ window#waybar {
|
||||
#clock-date,
|
||||
#workspaces,
|
||||
#pulseaudio,
|
||||
#network,
|
||||
#custom-powerprofile,
|
||||
#custom-nightlight,
|
||||
#custom-updates,
|
||||
#battery,
|
||||
#tray,
|
||||
#custom-notification,
|
||||
@@ -71,6 +72,11 @@ window#waybar {
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
/* The power-profile speedometer glyph renders small in its em box. */
|
||||
#custom-powerprofile {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
#workspaces button.active {
|
||||
background-color: @blue;
|
||||
color: @bg0;
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
|
||||
"modules-left": ["custom/launcher", "clock", "clock#date"],
|
||||
"modules-center": ["hyprland/workspaces"],
|
||||
"modules-right": ["pulseaudio", "network", "custom/powerprofile", "battery", "tray", "custom/notification", "custom/powermenu"],
|
||||
"modules-right": ["pulseaudio", "custom/powerprofile", "custom/nightlight", "custom/updates", "battery", "tray", "custom/notification", "custom/powermenu"],
|
||||
|
||||
"hyprland/workspaces": {
|
||||
"disable-scroll": true,
|
||||
@@ -46,14 +46,6 @@
|
||||
"tooltip-format": "Playing at {volume}%"
|
||||
},
|
||||
|
||||
"network": {
|
||||
"format-wifi": " {signalStrength}%",
|
||||
"format-ethernet": "",
|
||||
"format-disconnected": "",
|
||||
"on-click": "sh -c '$TERMINAL -e nmtui'",
|
||||
"tooltip-format": "{ipaddr} via {gwaddr}"
|
||||
},
|
||||
|
||||
"battery": {
|
||||
"interval": 60,
|
||||
"states": { "warning": 30, "critical": 15 },
|
||||
@@ -87,6 +79,21 @@
|
||||
"on-click": "nomarchy-powerprofile-cycle"
|
||||
},
|
||||
|
||||
"custom/nightlight": {
|
||||
"return-type": "json",
|
||||
"interval": 3,
|
||||
"exec": "nomarchy-nightlight status",
|
||||
"on-click": "nomarchy-nightlight toggle"
|
||||
},
|
||||
|
||||
"custom/updates": {
|
||||
"return-type": "json",
|
||||
"interval": 1800,
|
||||
"signal": 9,
|
||||
"exec": "nomarchy-updates status",
|
||||
"on-click": "sh -c '$TERMINAL -e nomarchy-updates upgrade'"
|
||||
},
|
||||
|
||||
"custom/notification": {
|
||||
"format": "{icon}",
|
||||
"return-type": "json",
|
||||
|
||||
BIN
themes/summer-night/preview.png
Normal file
|
After Width: | Height: | Size: 81 KiB |
@@ -53,8 +53,9 @@ window#waybar {
|
||||
#clock.date,
|
||||
#workspaces,
|
||||
#pulseaudio,
|
||||
#network,
|
||||
#custom-powerprofile,
|
||||
#custom-nightlight,
|
||||
#custom-updates,
|
||||
#idle_inhibitor,
|
||||
#battery,
|
||||
#custom-notification,
|
||||
@@ -75,6 +76,11 @@ window#waybar {
|
||||
color: @fg;
|
||||
}
|
||||
|
||||
/* The power-profile speedometer glyph renders small in its em box. */
|
||||
#custom-powerprofile {
|
||||
font-size: 17px;
|
||||
}
|
||||
|
||||
#tray > .needs-attention {
|
||||
color: @green;
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
|
||||
"modules-left": ["custom/nomarchy", "clock", "clock#date"],
|
||||
"modules-center": ["hyprland/workspaces"],
|
||||
"modules-right": ["idle_inhibitor", "pulseaudio", "network", "custom/powerprofile", "battery", "tray", "custom/notification", "custom/powermenu"],
|
||||
"modules-right": ["idle_inhibitor", "pulseaudio", "custom/powerprofile", "custom/nightlight", "custom/updates", "battery", "tray", "custom/notification", "custom/powermenu"],
|
||||
|
||||
"hyprland/workspaces": {
|
||||
"disable-scroll": true,
|
||||
@@ -55,14 +55,6 @@
|
||||
"tooltip-format": "{desc} | {volume}%"
|
||||
},
|
||||
|
||||
"network": {
|
||||
"format-wifi": " {essid}",
|
||||
"format-ethernet": "",
|
||||
"format-disconnected": "",
|
||||
"on-click": "sh -c '$TERMINAL -e nmtui'",
|
||||
"tooltip-format": "{ipaddr} via {gwaddr}"
|
||||
},
|
||||
|
||||
"battery": {
|
||||
"interval": 30,
|
||||
"states": { "warning": 25, "critical": 10 },
|
||||
@@ -94,6 +86,21 @@
|
||||
"on-click": "nomarchy-powerprofile-cycle"
|
||||
},
|
||||
|
||||
"custom/nightlight": {
|
||||
"return-type": "json",
|
||||
"interval": 3,
|
||||
"exec": "nomarchy-nightlight status",
|
||||
"on-click": "nomarchy-nightlight toggle"
|
||||
},
|
||||
|
||||
"custom/updates": {
|
||||
"return-type": "json",
|
||||
"interval": 1800,
|
||||
"signal": 9,
|
||||
"exec": "nomarchy-updates status",
|
||||
"on-click": "sh -c '$TERMINAL -e nomarchy-updates upgrade'"
|
||||
},
|
||||
|
||||
"custom/notification": {
|
||||
"format": "{icon}",
|
||||
"return-type": "json",
|
||||
|
||||
BIN
themes/tokyo-night/preview.png
Normal file
|
After Width: | Height: | Size: 173 KiB |
BIN
themes/vantablack/preview.png
Normal file
|
After Width: | Height: | Size: 67 KiB |
BIN
themes/white/preview.png
Normal file
|
After Width: | Height: | Size: 49 KiB |