feat: complete and visually audit the Hyprland workstation
Refresh system/tool pins, install official Element Nightly, expand development tools, and finish desktop workflows with a shared charcoal/gold design. Retain host-specific updates and NixOS recovery generations.
This commit is contained in:
+3
-1
@@ -13,7 +13,9 @@
|
||||
!/.env.example
|
||||
/secrets/
|
||||
|
||||
# Editor temporary files.
|
||||
# Python bytecode and editor temporary files.
|
||||
__pycache__/
|
||||
*.pyc
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
+69
-53
@@ -1,76 +1,92 @@
|
||||
# Desktop research and proposed baseline
|
||||
# Workstation design and visual audit
|
||||
|
||||
Researched **2026-09-05 UTC**. **This is a proposal, not an installed desktop.** Hyprland and Firefox are selected; the supporting applications below are recommendations for review. No desktop, authentication, driver or input-lock changes have been activated.
|
||||
Reviewed 2026-09-06 UTC on the physical laptop and against the pinned upstream sources. JaKooLit is a **functional reference, not the visual template**. The goal is an informative, polished development workstation—not a sparse desktop and not a collection of theme selectors.
|
||||
|
||||
The comparison uses the current official NixOS Wiki, upstream documentation/release notes, project screenshots, and the actual package/module sources locked by this repository:
|
||||
## Design
|
||||
|
||||
- Nixpkgs: `d57af924f160a5084293c71c2043f058bd1cdb60`.
|
||||
- Home Manager: `65258d5c65a250189fde2e35f490d15e064c4c62`.
|
||||
- Package versions below are **available in that pin**, not necessarily current upstream releases. Screenshots illustrate upstream styling, not a locally tested session or a promise that every pictured feature exists in our pin.
|
||||
- Dark **One Ring** wallpaper, charcoal surfaces, parchment text and restrained old-gold accents. Terminal ANSI colors remain distinct; Neovim's theme and configuration are unchanged.
|
||||
- One continuous top bar: launcher/workspaces/help and active-window context on the left, date/time in the center, media, CPU/RAM/temperature, notification count, privacy and laptop status on the right. Details and actions are available without filling the bar with permanent buttons.
|
||||
- The bar's quick-settings panel owns audio, microphone, brightness, network/VPN, Bluetooth, power profiles and idle inhibition. Its **Actions** and **Health** buttons open the action palette and real system/update diagnostics.
|
||||
- SwayNC owns notification history, actions, DND and media—not a duplicate hardware settings dashboard.
|
||||
- Anyrun is the application/calculator launcher; Fuzzel provides consistent searchable action, clipboard, window and help pickers. Help is read from live described bindings and never executes a selected shortcut.
|
||||
- Flat NixOS modules, Home Manager files, native Hyprland Lua, standard systemd services and small packaged helpers. No copied dotfile framework, downloaded login scripts, dynamic theme generators or additional recovery framework.
|
||||
|
||||
## Recommended small desktop
|
||||
The laptop is **Intel Lunar Lake / Arc 130V–140V**, using `xe`, with a **1920×1200 internal panel at 150%**. The CPU temperature source is configured on this host, not guessed for every machine. No NVIDIA configuration, driver replacement, DNS change or storage migration is part of this work.
|
||||
|
||||
| Role | Recommendation | Pinned version | Rationale / trade-off |
|
||||
| --- | --- | --- | --- |
|
||||
| Compositor/session | Hyprland + UWSM | 0.55.4 / 0.26.4 | Selected compositor; native NixOS session integration, with one owner for systemd startup/shutdown. |
|
||||
| Browser | Firefox | 154.0.1 | Selected browser; keep its sandbox and ordinary browser configuration. Update freshness needs attention below. |
|
||||
| Terminal | **Alacritty** (Rust) | 0.17.0 | Focused terminal with native Wayland support. No built-in tabs/splits; Hyprland already arranges windows. Ghostty 1.3.1 (Zig) is the richer alternative if terminal tabs, splits or image protocols are wanted. |
|
||||
| Bar/quick settings | **ashell** (Rust/Iced) | 0.8.0 | Rounded, ready-made bar with workspaces, tray, audio controls and settings; avoids building a shell from widgets. Ironbar 0.19.0 (Rust/GTK4) is preferable for more bespoke panels/CSS. |
|
||||
| Launcher | **Anyrun** (Rust/GTK4) | 25.12.0 | Application search with optional calculator/symbol plugins; enable only useful plugins, not indexing/network integrations by default. Fuzzel 1.14.1 (C) is the simpler alternative with a direct launch-prefix option. |
|
||||
| Lock/idle | **hyprlock + hypridle** (C++) | 0.9.5 / 0.1.7 | Hyprland-native integration, PAM, session-lock protocol and lock-completion-aware sleep inhibition. Security and recovery matter more than implementation language here. |
|
||||
| Notifications | **mako** (C) | 1.11.0 | Small, styleable notification daemon. SwayNotificationCenter 0.12.6 (Vala) adds a full notification drawer if that is wanted later. |
|
||||
| Privilege prompts | **hyprpolkitagent** | 0.1.3 | Existing Home Manager session-bound integration. Soteria 0.3.1 is a genuine Rust alternative, but has session-registration considerations and a newer restart-related fix upstream. |
|
||||
| Audio/screensharing | PipeWire + WirePlumber; Hyprland and GTK portals | — | Audio, screen capture and file-picker plumbing, not optional decoration. Use native NixOS modules. |
|
||||
| Clipboard/screenshots | wl-clipboard; grim + slurp; **Satty** (Rust) for annotation | 2.3.0 / 1.5.0 / 1.5.0 / 0.20.1 | Normal copy/paste and area capture; annotation runs on demand. No persistent clipboard history by default. |
|
||||
## What the audit repaired
|
||||
|
||||
Use a small font/icon set, restrained dark colors, modest rounding and short animations. Do not add a theme framework, custom shell framework, downloaded startup scripts, weather accounts or a second bar. This does not change the Neovim theme or any Neovim source.
|
||||
1. **Session ownership:** the original SDDM selection started plain Hyprland without `graphical-session.target`; the configured bar, wallpaper, locker and polkit services consequently did not start. SDDM now offers only **Hyprland (uwsm-managed)**. Plasma and the `kbot` account are removed as requested; `/home/kbot` remains intact.
|
||||
2. **Physical updates:** the laptop previously excluded the updater, whose script selected the EC2 host. Host selection is now explicit. Daily physical updates stage checked generations for the next boot; they do not log out the user or reboot.
|
||||
3. **Shared styling:** native CSS and Lua are rendered from `colors.nix`, rather than keeping several unrelated palettes.
|
||||
4. **Launcher geometry:** upstream gives the row, boxes, image and labels the same `.match` class. Applying padding to that class multiplied row height. Padding now applies only to `row.match`; application descriptions are hidden, results are bounded, and calculator results remain readable. A session-owned Anyrun daemon supports calculator clipboard output.
|
||||
5. **Actual visual sizing:** screenshots led to a shorter/wider action picker, shorter help, smaller notification drawer, consistent borders, and removal of the thick upstream notification-focus background. Bar information was restored after an overly sparse iteration; information density is intentional.
|
||||
6. **Duplicate/broken controls:** an unconfigured SwayNC backlight widget was present but uninitialized. Hardware controls now live in ashell's working native panel, not in both panels.
|
||||
7. **Clipboard/lock behavior:** history is session-local; a clipboard write cannot race past the lock wipe, and a failed wipe cannot prevent the screen from locking. OSD notifications are transient and replace only other OSD messages, not screenshot/error notifications.
|
||||
8. **Toolkit and apps:** the expanded toolset is declarative. Element is the official pinned Nightly, with its matching Electron/native modules and libsecret storage. Thunar, archives, image/video viewers and MIME defaults complement Yazi.
|
||||
|
||||
**Optional wallpaper tool:** [awww](https://codeberg.org/LGFae/awww), Rust, pinned 0.12.1, if image switching/transitions are wanted. The old `swww` GitHub repository is archived and explicitly redirects to this renamed project; Nixpkgs warns about the old attribute. A solid background does not need another daemon.
|
||||
## Useful workflows
|
||||
|
||||
Visual references: [ashell gallery](https://github.com/MalpenZibo/ashell#-screenshots), [Ironbar examples](https://github.com/JakeStanger/ironbar), [Anyrun](https://github.com/anyrun-org/anyrun), [Sherlock](https://github.com/Skxxtz/sherlock), [Veila](https://github.com/naurissteins/Veila). The reviewed ashell design uses compact rounded groups and popovers; Ironbar's minimal example is a flatter, denser strip. Both are legitimate aesthetic choices.
|
||||
The authoritative full list is **Super-H** or **Super-Shift-K**, also available through the bar's `?` button.
|
||||
|
||||
## Rust screen lockers: real candidates, not a blanket dismissal
|
||||
| Capability | Entry point / implementation |
|
||||
| --- | --- |
|
||||
| Apps and calculator | **Super-D**, bar launcher; Anyrun applications/Rink |
|
||||
| Files | **Super-E** Thunar; **Super-Ctrl-E** Yazi in Kitty |
|
||||
| Window overview | **Super-A**, **Super-Ctrl-S**; workspace-labelled picker, validated addresses |
|
||||
| Window operations | Super-arrows focus; Ctrl modifier moves, Alt swaps, Shift resizes; Super-G groups; Super-Ctrl-Tab changes group tab |
|
||||
| Floating/fullscreen | **Super-Space** floating; **Super-Shift-F** fullscreen; **Super-Ctrl-F** maximize |
|
||||
| Workspaces | Super-1…0; Shift moves and follows, Ctrl moves silently; Super-Tab cycles |
|
||||
| Scratch/drop-down terminal | **Super-U** scratchpad; **Super-Shift-U** move to it; **Super-Shift-Enter** persistent drop-down terminal |
|
||||
| Actions / quick settings | **Super-Shift-E** action palette; right side of bar opens hardware controls |
|
||||
| Notifications / DND | **Super-Shift-N** history; **Super-Ctrl-N** DND; bell/count in bar |
|
||||
| Clipboard | **Super-Alt-V**; text/images, delete, clear and pause/resume from Actions |
|
||||
| Screenshots | Print menu; Super-Print output; Super-Shift-Print region; Alt-Print window; Ctrl variants delay 5/10 seconds; Super-Shift-S annotation |
|
||||
| Recording | **Super-Alt-R**; region/output, no audio by default, explicit desktop-audio option; **REC** bar control stops recording |
|
||||
| Capture feedback/privacy | Screenshot copy/save notification; recording owns a notification inhibitor without overwriting DND preferences; bar privacy indicators |
|
||||
| Media/OSD | MPRIS bar module and media keys; volume/mic/brightness/keyboard-backlight keys have replacing feedback |
|
||||
| Night light | **Super-N**; 4200 K from 21:00, identity from 07:00; manual toggle |
|
||||
| Laptop controls | Power profile, airplane mode and temporary display scaling in Actions; Super-Alt-T touchpad; low/critical battery alerts |
|
||||
| Emoji / color / search | Super-Alt-E emoji; color picker in Actions; Super-S URL-encoded web search |
|
||||
| Session | Ctrl-Alt-L lock; Ctrl-Alt-P power menu; destructive menu actions require confirmation |
|
||||
| Health | Bar settings → Health, Actions → System/update health, or `desktop health`; real units, journal and running/booted/selected generation |
|
||||
|
||||
- **Veila** is an attractive, standalone Rust option using `ext-session-lock-v1`, with a packaged build in this Nixpkgs. However, the pin has **0.4.0** and upstream **0.4.4** explicitly lists password-memory handling, daemon-authorized unlock, and fail-closed unlock-handoff fixes. Do **not** select the older package merely to keep the desktop Rust-based. Its daemon and PAM setup must also be declared. [0.4.4 release notes](https://github.com/naurissteins/Veila/releases/tag/0.4.4).
|
||||
- **Cthulock** is Rust/Slint with a configurable UI and the same session-lock protocol. Latest observed release: 0.1.2, 2025-08-31. Its upstream Nix integration is an additional flake rather than a package/module already present in this pin. It is a possible experiment, not the lowest-maintenance baseline. [Project](https://github.com/FriederHannenheim/cthulock).
|
||||
- **veiland** is a newer Rust locker with process-isolated GPU plugins, including upstream reports of NVIDIA suspend/hotplug testing. That is upstream evidence, not our testing. Its extensible scene/plugin architecture is more than this minimal desktop needs. [Project](https://github.com/sylflo/veiland).
|
||||
- **hyprlock** remains the recommendation, not a claim of audited or bug-free software. Its newer 0.9.6 release also contains PAM, rendering and output-handling fixes; review it when refreshing the pin. [Release notes](https://github.com/hyprwm/hyprlock/releases/tag/v0.9.6).
|
||||
Clipboard data lives in `$XDG_RUNTIME_DIR/workstation`, with a 200-item limit and private permissions. It is cleared on lock and service/session exit. Sources which label sensitive clipboard content are excluded; **universal password detection is not promised**. Pause history when appropriate.
|
||||
|
||||
The important property is a compositor-enforced lock, not a fullscreen password window. The [Wayland session-lock protocol](https://wayland.app/protocols/ext-session-lock-v1) requires the session to stay locked if the locker dies after acquiring the lock. It does not prevent authentication bugs in a client, guarantee successful initial locking, or protect against every compositor/GPU failure.
|
||||
Screenshots go to the XDG Pictures directory under `Screenshots`; recordings go to XDG Videos under `Recordings`. Region cancellation produces no empty capture. Recording has no microphone option enabled by default; the audio choice explicitly captures the output's monitor source. Recording suppression does not claim to detect every browser/portal screenshare.
|
||||
|
||||
**Login is separate:** ReGreet and tuigreet are Rust greeters for greetd, not screen lockers. ReGreet is the graphical option; tuigreet is the smaller terminal option. A display manager remains a separate choice, not a silent addition to the EC2 host. COSMIC's greeter is not a drop-in Hyprland locker.
|
||||
## Visual and interaction checks
|
||||
|
||||
## Compatibility details that affect implementation
|
||||
Native screenshots are kept locally under `~/.cache/desktop-audit/resume/`, **not committed**. They include the real 150% bar/wallpaper, application search, calculator, help, actions, notification drawer, quick settings and Element Nightly startup. Earlier iterations are retained for comparison; filenames alone are not proof that a check passed.
|
||||
|
||||
1. **Use the new Hyprland configuration format.** The [current NixOS Wiki](https://wiki.nixos.org/wiki/Hyprland) explicitly flags its older examples as outdated: Hyprland 0.55 introduced Lua configuration. Our locked Home Manager defaults to `configType = "lua"` for `home.stateVersion = "26.05"`. Write a small native `hyprland.lua`, using the [0.55.4 example/API](https://github.com/hyprwm/Hyprland/blob/v0.55.4/example/hyprland.lua), rather than copying old `bind = ...` examples into Lua settings. Do not change stateVersion to work around this.
|
||||
2. **Only one session manager.** NixOS recommends `programs.hyprland.withUWSM = true`; disable Home Manager's separate Hyprland systemd integration. Upstream also warns UWSM adds its own quirks. Bind bar/agent services to the graphical session and launch applications through the session manager. [Session documentation](https://wiki.hypr.land/Useful-Utilities/Systemd-start/).
|
||||
3. **ashell needs small but important overrides.** Its pinned default logout command is `loginctl kill-user $(whoami)`, which is broader than logging out of the desktop and could terminate other sessions. Use `uwsm stop`. The pinned Home Manager service has startup ordering but no `PartOf`; explicitly tie its lifetime to `graphical-session.target`. Version 0.8 does not advertise the notification manager shown in the current 0.10 README, so do not assume it replaces mako. [Pinned settings](https://github.com/MalpenZibo/ashell/blob/0.8.0/website/versioned_docs/version-0.8.0/configuration/modules/settings.md).
|
||||
4. **Rust does not eliminate NVIDIA rendering issues.** ashell 0.8 documents startup freezes with the Vulkan backend and an application-scoped `WGPU_BACKEND=gl` workaround. Anyrun documents a driver-dependent GTK close/hang problem and `GSK_RENDERER=ngl`. Test the actual target first; apply a needed workaround only to that application on that target. Do not export these globally to integrated-graphics machines. [ashell troubleshooting](https://github.com/MalpenZibo/ashell/blob/0.8.0/website/versioned_docs/version-0.8.0/configuration/troubleshooting.md), [Anyrun warning](https://github.com/anyrun-org/anyrun#anyrun).
|
||||
5. **Anyrun's provider is already packaged correctly.** Since 25.12 it needs `anyrun-provider`; our Nixpkgs wrapper supplies it and the plugin search path. Do not add a manual Cargo install or another flake. The applications plugin searches desktop entries, and its version-specific preprocessing hook must be used when arranging UWSM application launching.
|
||||
6. **Do not install the wrong Sherlock.** The Rust launcher is `pkgs.sherlock-launcher` (0.1.14-3), and `programs.sherlock` in Home Manager correctly selects it. `pkgs.sherlock` (0.16.0) is an unrelated Python social-account search tool. Sherlock has a polished widget-style UI, but Anyrun/Fuzzel are a more focused starting point.
|
||||
7. **Portals and idle services already have system integration.** The pinned NixOS Hyprland module adds both Hyprland and GTK portals; the latter supplies a file picker. Do not install competing portal stacks or add sleep/kill/restart hacks. The NixOS hyprlock module creates its PAM service and enables the system-provided hypridle user unit; Home Manager can supply idle configuration with `package = null` instead of creating another unit. A valid lock configuration is required: installing a locker is not enough.
|
||||
8. **Wait for locking, not an arbitrary delay.** The pinned hypridle supports `general.inhibit_sleep = 3`, waiting for Hyprland's lock notification before releasing its sleep inhibitor, subject to logind's inhibitor timeout. It does not support the newer conditional-timeout options in current upstream documentation. Configure idle lock/display-off without silently adding automatic suspend to a development host. [hypridle documentation](https://wiki.hypr.land/Hypr-Ecosystem/hypridle/).
|
||||
The native audit caught real issues that configuration evaluation did not: nested launcher padding, overly tall pickers, the notification focus slab and an uninitialized duplicate brightness control. The quick-settings screenshot confirms that the actual panel contains the audio/mic/brightness sliders, network/Bluetooth, idle inhibition, power profile, Actions and Health controls.
|
||||
|
||||
## RTX 4090 versus integrated graphics
|
||||
`desktop-test.py` exercises the disposable graphical VM: session ownership, fonts, real PipeWire nodes, launcher geometry at 100%/150%, clipboard picker, recording container and no-audio default, notification ownership/inhibition, described help, and real wrong/correct-password PAM locking. Test credentials never reach the host. `desktop-actions-test.py` covers cancellation, untrusted input, byte-preserving clipboard behavior, lock failure handling, display timeout restoration and recording-inhibitor cleanup.
|
||||
|
||||
Keep the shared desktop separate from a deliberately imported `nvidia.nix`. Do not put NVIDIA options/environment variables into every machine's common module, invent PRIME bus IDs, or reuse EC2's boot/storage configuration on a workstation.
|
||||
**Completed validation (2026-09-06):** flake evaluation, Nix formatting, generated Hyprland configuration, physical-host assertions, 124 CLI executable smoke checks, 14 desktop-action tests, both updater-policy regression suites, and the full graphical/PAM/clipboard/recording VM check all passed on the refreshed inputs. Native clipboard, recording and rootless Podman checks passed too. The 150% six-result launcher and help screenshots were inspected visually; the transparent launcher click-catcher's IPC dimensions are not mistaken for the visible palette bounds.
|
||||
|
||||
- **Intel/AMD-only targets:** start with the standard kernel/Mesa graphics stack. Device-generation-specific video decoding or firmware adjustments require actual hardware identification.
|
||||
- **RTX 4090 target:** NVIDIA's open kernel modules support Ada/RTX 4090. Use the NixOS driver module with `hardware.nvidia.open = true`, modesetting and power-management support. The userspace driver remains proprietary and needs a scoped unfree allowance. `services.xserver.videoDrivers = [ "nvidia" ]` selects the driver even for Wayland; this does not require enabling the X server.
|
||||
- **Pinned driver:** stable and production both resolve to **595.71.05**. With open modules, the pinned NixOS module selects the new **kernel suspend notifier** path. Enabling power management sets the appropriate module parameters without the legacy `nvidia-suspend`/`nvidia-resume` services. Do not unconditionally paste older service recipes on top.
|
||||
- **Suspend storage:** NVIDIA recommends enough temporary backing storage for total VRAM plus about 5%; a 24-GiB 4090 needs roughly 25.2 GiB in the conservative worst case. Check the target filesystem and `/tmp` policy before promising reliable suspend. Do not force early KMS or hibernation configuration without checking the machine.
|
||||
- **Firefox video decoding is a separate question.** The NVIDIA VA-API bridge's documented setup disables Firefox's RDD sandbox. Do not silently adopt that security trade-off for hardware video decoding. Native Wayland rendering and hardware video decoding are not the same feature.
|
||||
**Account/hardware boundaries:**
|
||||
|
||||
Sources: [official NixOS NVIDIA page](https://wiki.nixos.org/wiki/NVIDIA), [Hyprland NVIDIA guidance](https://wiki.hypr.land/Nvidia/), [NVIDIA supported GPUs](https://github.com/NVIDIA/open-gpu-kernel-modules#compatible-gpus), [595.71.05 power-management documentation](https://download.nvidia.com/XFree86/Linux-x86_64/595.71.05/README/powermanagement.html), [NVIDIA VA-API bridge](https://github.com/elFarto/nvidia-vaapi-driver#firefox).
|
||||
- Element Nightly launches with libsecret enforced. The live audit reached the encryption warning because no unlocked/configured Secret Service vault was available. The insecure fallback was **not selected**. Open/configure KeePassXC's Secret Service group before signing in; account/vault setup remains the user's responsibility.
|
||||
- Bluetooth pairing, real suspend/resume, external-monitor hotplug and interactive browser portal sharing require their respective hardware/account interaction. Package and VM tests are not substitutes.
|
||||
- Temporary display scaling preserves output mode, position and rotation and reverts on timeout/cancel. Specific dock/mirror profiles are not invented without attached displays.
|
||||
|
||||
## Before implementation/activation
|
||||
## Wallpaper provenance
|
||||
|
||||
- **Review an input refresh.** Mozilla's [release metadata](https://product-details.mozilla.org/1.0/firefox_versions.json) reports **155.0.1**, versus the pin's **154.0.1**. Reproducible does not mean current. Review the stable Nixpkgs update, browser/locker fixes, evaluation and build separately; no input was updated during this research. Do not move to development snapshots merely for cosmetic features.
|
||||
- **Establish local authentication.** `dev` currently has a locked Unix password. SSH authorization and passwordless sudo do not give a graphical greeter, locker or polkit agent a usable password. Choose and declare an appropriate credential/secret mechanism before enabling a usable local login/lock workflow. No invented password, plaintext secret in the Nix store, empty-password workaround or silent autologin.
|
||||
- **Keep the implementation flat:** shared desktop integration, native `hyprland.lua`, and opt-in NVIDIA settings, with each real machine retaining its own hardware/boot module. No deep host/profile framework and no Neovim changes.
|
||||
[Wallhaven 01e5v4](https://wallhaven.cc/w/01e5v4), a dark One Ring inscription, **1920×1200**. Wallhaven lists uploader **ulairi88**, not a verified original artist, and provides no redistribution license. No artist attribution or open license is invented.
|
||||
|
||||
## What was validated
|
||||
`wallpaper.nix` fetches immutable bytes:
|
||||
|
||||
Non-activating evaluations of candidate module compositions passed NixOS and Home Manager assertions for both Mesa-default and opt-in NVIDIA settings. They verified Lua defaults, UWSM ownership, both automatically supplied portals, PAM/idle integration without duplicate idle units, bar lifecycle/logout settings, unchanged Neovim package/source, and the NVIDIA 595 kernel-notifier parameters. These were evaluation fixtures using the existing host as a base, **not bootable physical-host definitions or desktop builds**.
|
||||
```text
|
||||
https://w.wallhaven.cc/full/01/wallhaven-01e5v4.jpg
|
||||
sha256-3jkKzJ0q4MTlHygwUs3SuSiUIjUjkiTqSaM+q8EL/oc=
|
||||
```
|
||||
|
||||
The EC2 host exposes only a simple framebuffer DRM device, with no render node. No actual Hyprland session, hardware acceleration, screen sharing, password unlock, hotplug or suspend/resume was tested. Those checks must be run on the target machines, including wrong/correct-password behavior, locker-crash behavior, monitor changes while locked, and repeated suspend/resume with recovery access retained.
|
||||
No wallpaper service is contacted at login. `wallpaper.svg` remains the original locally authored alternative. Existing generations retain the fetched image if the source later disappears.
|
||||
|
||||
## Reference, not imitation
|
||||
|
||||
Reviewed [JaKooLit/Hyprland-Dots](https://github.com/JaKooLit/Hyprland-Dots), its [keybindings](https://github.com/JaKooLit/Hyprland-Dots/blob/main/config/hypr/configs/Keybinds.conf), [scripts](https://github.com/JaKooLit/Hyprland-Dots/tree/main/config/hypr/scripts), Waybar modules and SwayNC configuration, plus the announced successor [LinuxBeginnings/Hyprland-Dots](https://github.com/LinuxBeginnings/Hyprland-Dots). Familiar general shortcuts are retained without copying the installers, mutable `.conf` edits, presentation style or duplicate ownership.
|
||||
|
||||
**Excluded deliberately:** animation/theme/bar-layout selectors, online radio/weather/location services, live wallpaper effects, opacity/layout preset collections, speculative GPU/game-mode tuning and broad process-killing refresh scripts. Alt-Tab/window search supplies a useful overview without a second desktop shell for thumbnails.
|
||||
|
||||
Implementation references: [Hyprland 0.55 Lua example](https://github.com/hyprwm/Hyprland/blob/v0.55.4/example/hyprland.lua), installed Lua API stubs, [ashell](https://github.com/MalpenZibo/ashell), [Anyrun](https://github.com/anyrun-org/anyrun), [Fuzzel](https://codeberg.org/dnkl/fuzzel), [SwayNC](https://github.com/ErikReider/SwayNotificationCenter), [cliphist](https://github.com/sentriz/cliphist), [UWSM](https://github.com/Vladimir-csp/uwsm), and the locked package/module sources.
|
||||
|
||||
@@ -1,168 +1,186 @@
|
||||
# Development hosts
|
||||
# Development workstation
|
||||
|
||||
Flat, explicit NixOS modules, with locked inputs. Required machine setup belongs in these configs—not a list of manual installs or dotfile copies.
|
||||
Flat, explicit NixOS modules with locked inputs. Required setup belongs here—not in manual package installs, copied dotfiles or an extra framework.
|
||||
|
||||
**Choose the host, not the login name:** `nixosConfigurations.nixos` is this physical UEFI/NVMe machine; `nixosConfigurations.dev` is EC2 only. Both add the `dev` account. Never activate the EC2 closure on the physical machine.
|
||||
**Select the host, not the login name:** `nixosConfigurations.nixos` is this physical UEFI/NVMe laptop; `nixosConfigurations.dev` is EC2 only. Both use the `dev` account. Never activate the EC2 closure on the laptop.
|
||||
|
||||
## Layout
|
||||
|
||||
| File | Owns |
|
||||
| --- | --- |
|
||||
| `flake.nix`, `flake.lock` | Host entry point and exact Nixpkgs/Home Manager/dotfile revisions and content hashes |
|
||||
| `configuration.nix` | EC2-only boot/storage/integration, updater and repo ownership |
|
||||
| `common.nix` | Shared dev environment, platform, Nix features and compatibility version |
|
||||
| `physical.nix`, `hardware-configuration.nix` | This physical host's systemd-boot/NVMe/Btrfs setup; retains `kbot`, Plasma/SDDM and NetworkManager while adding `dev` |
|
||||
| `physical-test.nix` | Regression checks for physical-host recovery access, storage, boot and VPN tools |
|
||||
| `users.nix` | `dev`, SSH/sudo, Home Manager integration and dev workspace ownership |
|
||||
| `dev-authorized-keys` | Public SSH keys for `dev`—never private keys |
|
||||
| `tools.nix`, `colors.nix` | Zsh, Kitty, Pi, Starship, fzf, Yazi, btop, Git policy and common CLI tools; shared readable palette |
|
||||
| `desktop.nix`, `hyprland.lua`, `anyrun.css`, `wallpaper.svg` | Hyprland/UWSM, bar, launcher, original wallpaper, notifications, lock/idle, PipeWire and desktop styling |
|
||||
| `apps.nix` | Firefox ESR, KeePassXC, Thunderbird, Steam, Element, Slack and Zathura |
|
||||
| `workstation.nix`, `nvidia.nix` | Separately selected physical-workstation/greeter and NVIDIA integration; not enabled on EC2 |
|
||||
| `updates.nix`, `update-system.sh` | Daily package-input updates in an isolated Git worktree; validated commits, no forced reboot or GC |
|
||||
| `desktop-test.nix`, `desktop-test.py`, `audit-desktop.sh`, `update-test.py` | Disposable graphical audit and updater failure/concurrency tests |
|
||||
| `network.nix` | WireGuard/OpenVPN clients and network diagnostics; optional resolved, host-owned interface management |
|
||||
| `neovim.nix`, `neovim-test.lua` | Editor, unchanged upstream dotfile deployment and opt-in native runtime audit |
|
||||
| [DESKTOP.md](DESKTOP.md) | Historical component research; executable configuration is in the files above |
|
||||
| `flake.nix`, `flake.lock` | Host entry points, exact Nixpkgs/Home Manager/tool/dotfile revisions and hashes |
|
||||
| `common.nix` | Shared environment, platform, Nix features and compatibility version |
|
||||
| `physical.nix`, `hardware-configuration.nix` | Laptop boot/storage, SDDM with only managed Hyprland, networking, Bluetooth, power, firmware and host-specific updates |
|
||||
| `configuration.nix` | EC2-only boot/storage/SSM integration, updater and repo ownership |
|
||||
| `users.nix` | `dev`, SSH authorization, sudo, Home Manager and workspace ownership |
|
||||
| `tools.nix`, `network.nix` | Development toolkit, terminal/shell, rootless Podman, VPN clients and network diagnostics |
|
||||
| `colors.nix`, `wallpaper.nix`, `wallpaper.svg` | Shared One Ring palette, hash-pinned wallpaper and original fallback artwork |
|
||||
| `desktop.nix`, `hyprland.lua`, `anyrun.css`, `swaync.css`, `desktop-help.py`, `desktop-actions.py` | Session/bar, launchers, described help, capture, clipboard, notifications, lock/idle and styling |
|
||||
| `apps.nix`, `element-nightly.nix` | Firefox ESR, KeePassXC, Thunderbird, Steam, pinned Element Nightly, Slack, file/media viewers and MIME defaults |
|
||||
| `neovim.nix`, `neovim-test.lua` | Unmodified upstream editor deployment and opt-in native runtime audit |
|
||||
| `updates.nix`, `update-system.sh`, `update-test.py` | Daily isolated-worktree updates and failure/concurrency regression tests |
|
||||
| `physical-test.nix`, `tools-test.nix` | Built host/session assertions and bounded offline tool/help smoke tests |
|
||||
| `desktop-test.nix`, `desktop-test.py`, `audit-desktop.sh` | Disposable graphical/PAM/audio/scaling audit |
|
||||
| `workstation.nix`, `nvidia.nix` | Optional ReGreet workstation and NVIDIA modules; not this Intel laptop's configuration |
|
||||
| [DESKTOP.md](DESKTOP.md) | Live audit, wallpaper provenance, JaKooLit comparison and explicit feature-completion plan |
|
||||
|
||||
## Account
|
||||
## Account and session
|
||||
|
||||
- Daily user: `dev`, UID 1001, home `/home/dev`, workspace `~/projects`.
|
||||
- Passwordless sudo is scoped to `dev`; EC2's SSM-user administration and root SSH recovery remain intact. The physical host retains `kbot`'s existing wheel/sudo access.
|
||||
- `dev` is authorized by the public key in this repo. No private key or password is embedded.
|
||||
- On EC2 only, `/etc/nixos` is writable by `dev` through a native tmpfiles ownership rule, which does not follow store symlinks. The physical target leaves the original `/etc/nixos` recovery configs and `/home/kbot` untouched; `/etc/nix` remains administrator-managed.
|
||||
- Nix daemon access stays untrusted for ordinary use. Sudo is a separate, explicit administrative capability.
|
||||
- The declared daily shell is Zsh, with completion, suggestions, highlighting, Starship, fzf (`Ctrl-R`, `Ctrl-T`, `Alt-C`), and zoxide (`z`, `zi`). Root/SSM shells are not changed.
|
||||
- Kitty uses an opaque dark background, 13pt JetBrains Mono and generous padding. `Ctrl-Shift-+` / `Ctrl-Shift--` zoom its font; `Ctrl-Shift-Backspace` resets it. `y` opens Yazi with shell-directory integration.
|
||||
- Desktop keys: `Super-Enter` Kitty, `Super-Space` launcher, `Super-E` Yazi, `Super-B` Firefox, `Super-P` KeePassXC, `Super-Escape` lock, `Super-Q` close, `Super-1…0` workspaces, `Print` screenshot/annotation. Launch the **Hyprland (uwsm-managed)** session.
|
||||
- KeePassXC preferences are seeded once and remain writable. Open/create your own vault and pair its browser extension; no vault, account or VPN credentials are embedded. The optional ReGreet module (`workstation.nix`, not used by either host here) requires a securely provisioned `hashedPasswordFile`; the physical host keeps SDDM and a locally set `dev` password. EC2 gets no greeter or autologin.
|
||||
- Element uses libsecret for encrypted local storage through KeePassXC. Before using it, create/open your own vault and select a dedicated application-token group in **Database Settings → Secret Service Integration**. Keep that vault unlocked when requested. Do not select Element's weaker/no-encryption fallback. Vault setup and application authentication are intentionally user-controlled.
|
||||
- `wg`, `wg-quick`, OpenVPN, `iperf3`, `nmap`, `traceroute`, `whois`, `mtr`, DNS tools, `tcpdump`, `ethtool`, `nc` and `socat` are installed without any tunnels, peers, keys or added firewall ports. The physical host also has NetworkManager's OpenVPN plugin and keeps its existing DNS setup. EC2 uses resolved; DHCP remains under each host's network manager.
|
||||
- Daily account: **dev**, UID 1001, `/home/dev`, workspace `~/projects`, Zsh.
|
||||
- `dev` has **full passwordless sudo**, not a rebuild-only privilege. Nix daemon trusted-user access is not separately granted.
|
||||
- The physical host uses the locally established `dev` password; mutable users preserve it. No plaintext password, private key or password hash is embedded in this repo/store.
|
||||
- At the user's request, **Plasma and the previously managed `kbot` account are removed**. This configuration does not delete `/home/kbot`; retained files and old generations are not erased. Home Manager only manages `dev`.
|
||||
- SDDM exposes **Hyprland (uwsm-managed)** only. Plain Hyprland is deliberately hidden because it does not start the session-bound bar, wallpaper, idle and polkit services. UWSM is the single session owner.
|
||||
- On EC2, SSM/root recovery and the host's integration remain intact; there is no graphical greeter or autologin. `/etc/nixos` is dev-owned there. On the laptop `/etc/nix` remains administrator-owned and the original `/etc/nixos` files are untouched.
|
||||
- The SSH key in `dev-authorized-keys` is public. Account/cloud/browser/VPN credentials and vaults remain user-controlled.
|
||||
|
||||
Enter from an administrator session with `sudo -iu dev`.
|
||||
### Everyday controls
|
||||
|
||||
Git author identity and remote destination are deliberately unset. `user.useConfigOnly` is now a Home Manager setting in `tools.nix`, not a required manual `git config` step.
|
||||
| Key / command | Action |
|
||||
| --- | --- |
|
||||
| `Super-H`, bar `?` | Search described shortcuts from the live compositor; selecting a row does not execute it |
|
||||
| `Super-Enter` | Kitty |
|
||||
| `Super-D` | Anyrun applications/calculator |
|
||||
| `Super-E`, `Super-Ctrl-E`, `y` | Thunar / Yazi; `y` includes shell-directory integration |
|
||||
| `Super-B`, `Super-Alt-P` | Firefox / KeePassXC |
|
||||
| `Ctrl-Alt-L`, `Super-Escape` | Lock |
|
||||
| `Super-Q`, `Super-Shift-F`, `Super-Space` | Close / fullscreen / floating |
|
||||
| `Super-1…0`, `Super-Shift-1…0` | Focus workspace / move and follow; Ctrl moves silently |
|
||||
| `Super-U`, `Super-Shift-U` | Scratch workspace / move window to it |
|
||||
| `Super-Shift-Enter` | Drop-down terminal |
|
||||
| `Super-Shift-E`, `Ctrl-Alt-P` | Actions / confirmed session-power menu |
|
||||
| `Super-Shift-N`, `Super-Ctrl-N` | Notification history / DND |
|
||||
| `Super-Alt-V` | Clipboard history |
|
||||
| `Print`, `Super-Shift-S` | Screenshot menu / region annotation |
|
||||
| `Super-Alt-R` | Start/stop recording; red REC indicator when active |
|
||||
| `Super-N`, `Super-Alt-E` | Night light / emoji |
|
||||
| `Super-A`, `Alt-Tab` | Window search / cycle windows |
|
||||
| `Ctrl-R`, `Ctrl-T`, `Alt-C` in Zsh | fzf history / files / directories |
|
||||
| `z`, `zi` | zoxide directory navigation |
|
||||
| `direnv allow` | Explicitly approve a project's `.envrc`; no automatic trust whitelist |
|
||||
| `tmux`, `zellij`, `lazygit` | Persistent terminal workspaces and Git UI; not auto-started or forced |
|
||||
|
||||
Kitty uses an opaque charcoal background, 12pt JetBrains Mono and compact padding. Font zoom remains `Ctrl-Shift-+` / `Ctrl-Shift--`; reset with `Ctrl-Shift-Backspace`. Semantic ANSI colors remain distinguishable from the gold UI accent. Neovim's own theme is unchanged.
|
||||
|
||||
The **informative top bar** includes workspaces/window context, clock, CPU/RAM/host temperature, media, notification count, privacy and laptop status. Click its right-hand status area for audio, microphone, brightness, network/Bluetooth, idle inhibition and power-profile controls. **Actions** and **Health** open the searchable action palette and real system/update diagnostics. Notification history is a separate compact drawer, not another settings dashboard.
|
||||
|
||||
See [DESKTOP.md](DESKTOP.md) for the screenshot-led audit, functional coverage, wallpaper provenance and remaining hardware/account checks. JaKooLit informed the general shortcuts and workflow coverage—not the visual design.
|
||||
|
||||
## Tool coverage
|
||||
|
||||
`tools.nix` is organized by purpose, not a single unexplained package dump:
|
||||
|
||||
- **Native/debug:** GCC, Make, pkg-config, CMake, Ninja, Meson, ccache, clang tools, GDB, LLDB, Valgrind, Heaptrack, rr, ELF utilities and bpftrace.
|
||||
- **Rust:** rustc/Cargo/rustfmt/Clippy/rust-analyzer, nextest, cargo-audit/deny/expand/edit.
|
||||
- **Go:** Go, gopls, Delve, golangci-lint.
|
||||
- **Python:** Python, uv, Ruff, Pyright; existing Lua/LuaRocks prerequisites stay for Neovim.
|
||||
- **JS/TS:** Node, pnpm, TypeScript, Biome, Bun, Deno.
|
||||
- **Additional ecosystems:** OpenJDK 25, Maven, Gradle 9, Kotlin, .NET SDK 10, Ruby/Bundler, PHP/Composer, Zig/ZLS and Elixir/Erlang.
|
||||
- **Project/CI:** just, watchexec, hyperfine, tokei, ShellCheck, shfmt, yamllint, actionlint, pre-commit, dprint, StyLua, Taplo, Marksman, markdownlint, SQLFluff, Hadolint, ast-grep and ripgrep-all.
|
||||
- **Source control:** Git/LFS, gh, glab, LazyGit, delta, difftastic, Jujutsu, git-absorb/filter-repo. No guessed Git identity or account authentication.
|
||||
- **Nix:** direnv/nix-direnv, nix-output-monitor, nvd, nix-tree/diff, nixd, statix, deadnix, nixpkgs-review and the repo formatter.
|
||||
- **Containers/cloud:** rootless Podman, Compose, Buildah, Skopeo, Dive, AWS CLI, kubectl, Helm, k9s, kubectx, Stern, Kustomize, OpenTofu, Ansible. No Docker daemon, docker-group privilege, deployed infrastructure or automatic image downloads.
|
||||
- **Data/API:** SQLite, DuckDB, pgcli/litecli, Redis tools, Miller, csvlens, jq/yq/jless, xh, grpcurl, websocat, Protobuf/Buf, Hurl and oha. Installing these does not start database servers or load-test an endpoint.
|
||||
- **Security/backup:** age, sops, GnuPG, Gitleaks, Trivy, Cosign, Syft, Grype, step, mkcert, restic, rclone, rsync, Mosh and SSHFS. No keys, trusted CA, backup destination, schedules or scan targets are created.
|
||||
- **Diagnostics:** btop, procs, lnav, sysstat, iotop, dust/duf/ncdu, strace/lsof, NVMe/SMART/USB/PCI/sensor tools.
|
||||
- **Media/documents:** FFmpeg, ImageMagick, ExifTool, MediaInfo, Poppler utilities, Pandoc, yt-dlp, Chafa, Asciinema, VHS and archive/compression tools.
|
||||
- **Networking (`network.nix`):** WireGuard/OpenVPN, NetworkManager VPN integration on the laptop, mtr, iperf3, nmap, tcpdump/tshark, doggo/dig, iftop/bandwhich, traceroute, whois, ethtool, netcat and socat. No tunnels, peers, credentials, extra capture privileges or opened firewall ports.
|
||||
|
||||
These are system-owned executables from Nix, not unmanaged `npm -g`, `pip install --user` or `cargo install` bootstraps. Project dependencies may still be downloaded by their ordinary package managers. `nix develop` / `.envrc` remain appropriate for project-specific versions; this is not a promise that every language project uses the same global toolchain.
|
||||
|
||||
## Build and apply
|
||||
|
||||
### Physical machine: keep kbot and add dev
|
||||
|
||||
`physical.nix` preserves `kbot` (UID 1000, `/home/kbot`, existing password, wheel/NetworkManager groups), KDE Plasma, SDDM, printing, locale/time zone, and the installed filesystems/boot loader. Home Manager manages **only `dev`**, UID 1001 with its own `/home/dev`. No autologin, password copying, or replacement greeter is enabled. The EC2 automatic updater is deliberately **not imported**.
|
||||
|
||||
The hardware file is a tracked copy of this machine's `/etc/nixos/hardware-configuration.nix`; do not use it unchanged on another machine. The original `/etc/nixos` files remain available as recovery configuration.
|
||||
|
||||
**Recovery from the earlier EC2 switch attempt:** that failure selected system-profile generation 2 even though generation 1 is still running and remains systemd-boot's default. Before testing the physical configuration, if the profile still points at the failed EC2 closure, restore its selection without activating anything:
|
||||
|
||||
```sh
|
||||
readlink -f /nix/var/nix/profiles/system
|
||||
sudo nix-env --profile /nix/var/nix/profiles/system --switch-generation 1
|
||||
```
|
||||
|
||||
This recovery command is specific to the observed generations on this machine; it is not a routine rebuild step.
|
||||
|
||||
From an administrator shell:
|
||||
On the **physical laptop**, from an administrator shell:
|
||||
|
||||
```sh
|
||||
cd /etc/nix
|
||||
nixfmt --check ./*.nix
|
||||
nix flake check --no-build --no-update-lock-file
|
||||
nix build \
|
||||
nix build .#checks.x86_64-linux.updates \
|
||||
.#checks.x86_64-linux.desktop-config \
|
||||
.#checks.x86_64-linux.physical-config \
|
||||
.#checks.x86_64-linux.updates .#checks.x86_64-linux.desktop-config \
|
||||
.#checks.x86_64-linux.tools .#checks.x86_64-linux.desktop-actions \
|
||||
--no-update-lock-file --no-link
|
||||
nix build \
|
||||
.#nixosConfigurations.nixos.config.system.build.toplevel \
|
||||
nix build .#nixosConfigurations.nixos.config.system.build.toplevel \
|
||||
--no-update-lock-file --out-link result-nixos
|
||||
built=$(readlink -f result-nixos)
|
||||
sudo "$built/sw/bin/nixos-rebuild" dry-activate --no-reexec --store-path "$built"
|
||||
```
|
||||
|
||||
Use **`result-nixos`**, not an old `result` pointing at the EC2 closure. The dry run must not remove `kbot`, SDDM, NetworkManager or the `/boot`, `/home`, `/nix` mounts. Existing services can still need a restart; keeping an account does not guarantee its current graphical session stays running.
|
||||
New source files must be added to Git for flakes to include them. A build or dry activation does not activate the result. Review the dry activation; removing Plasma and `kbot` is intentional, removing `dev`, NetworkManager or the installed mounts is not.
|
||||
|
||||
Save your work, then activate from a text console (`Ctrl-Alt-F3`, log in as `kbot`, or use root). Keep that console available during testing:
|
||||
**Save work before activating.** A desktop/display-manager change can end a graphical session. Use `Ctrl-Alt-F3`, log in as `dev`, and retain that console:
|
||||
|
||||
```sh
|
||||
cd /etc/nix
|
||||
built=$(readlink -f result-nixos) # Re-establish this shell variable on the new console.
|
||||
# Activates users/services, but does NOT change the boot default. This is not a dry run.
|
||||
built=$(readlink -f /etc/nix/result-nixos)
|
||||
sudo "$built/sw/bin/nixos-rebuild" test --no-reexec --store-path "$built"
|
||||
# Choose a new password locally; mutable users preserve it across later rebuilds.
|
||||
sudo passwd dev
|
||||
```
|
||||
|
||||
At SDDM, choose **dev → Hyprland (uwsm-managed)** to try the new desktop, or **kbot → Plasma** for the existing environment. Once satisfied, persist the exact tested closure:
|
||||
|
||||
```sh
|
||||
# After testing login/session, persist exactly that closure:
|
||||
sudo "$built/sw/bin/nixos-rebuild" switch --no-reexec --store-path "$built"
|
||||
```
|
||||
|
||||
If testing fails, use the retained text console to restore the booted system without changing the boot default:
|
||||
`test` is a real activation, not a dry run. For a non-disruptive deployment that takes effect on next boot, use `boot` instead of `test`/`switch`. The daily physical updater uses that policy.
|
||||
|
||||
On **EC2 only**, use `/etc/nixos` and `.#nixosConfigurations.dev.config.system.build.toplevel`. Run the same evaluation/checks and activate the exact output with its own `nixos-rebuild --no-reexec --store-path`. Never select a target merely because it matches your username.
|
||||
|
||||
### Recovery
|
||||
|
||||
Keep existing generations. Inspect what is actually selected and booted:
|
||||
|
||||
```sh
|
||||
readlink -f /run/current-system /run/booted-system /nix/var/nix/profiles/system
|
||||
sudo nix-env --profile /nix/var/nix/profiles/system --list-generations
|
||||
```
|
||||
|
||||
To restore the booted closure temporarily from the retained console:
|
||||
|
||||
```sh
|
||||
previous=$(readlink -f /run/booted-system)
|
||||
sudo "$previous/sw/bin/nixos-rebuild" test --no-reexec --store-path "$previous"
|
||||
```
|
||||
|
||||
If a later boot fails, select the original generation in systemd-boot (hold Space during startup). Keep recovery generations; do not garbage-collect them during migration. A working `kbot` account cannot recover an unbootable kernel/filesystem by itself.
|
||||
If boot fails, choose a known-good systemd-boot generation (hold Space at startup). Do not blindly select “generation 1”: generation numbers are machine/history-specific. System rollback does not restore mutable user data, Git changes, application databases or backups.
|
||||
|
||||
### EC2 only
|
||||
## Automatic freshness, precisely
|
||||
|
||||
As `dev`, from `/etc/nixos` on the EC2 host:
|
||||
- **Every day**, `nixos-update.timer` runs with up to one hour of jitter and catches missed runs. It advances `nixpkgs`, `home-manager`, and `nixpkgs-latest`; the Neovim source remains fixed.
|
||||
- The system/desktop use the stable `nixos-26.05` and matching Home Manager release branch. Most standalone CLI tools use the locked **Nixpkgs master** input to avoid stable-release feature lag. They are imported separately, **not** overlaid onto the desktop's GCC/Python/libraries. The Nix daemon and service-integrated packages remain from the system pin.
|
||||
- **Laptop:** administrator-owned `/etc/nix`, explicit `nixos` target, build/check/record and **stage for next boot**. No surprise logout, live compositor replacement or automatic reboot. Installed versions change when that generation is booted, or when explicitly switched by the user.
|
||||
- **EC2:** dev-owned `/etc/nixos`, explicit `dev` target and live `switch`, retaining the prior activation/rollback behavior.
|
||||
- Both policies skip dirty/detached repositories, serialize runs, use an isolated worktree, check for edits again after dry activation, commit only the tested lock, attempt rollback on failure and retain recovery generations. A failed rollback is reported as failure, not silently marked successful.
|
||||
- **“Current” means newest successfully checked/built versions packaged in those branch heads**, not a guarantee of every upstream release immediately. Master can contain breakage and uncached builds; failure retains the previous working generation. Upstream Pi, Mason/plugin downloads, browser add-ons, firmware and project dependencies are separate update boundaries. No runtime self-updater is bolted on to override Nix-owned executables.
|
||||
- The updater does not fetch/merge repository code from origin. Configuration code is reviewed separately. A dirty working tree intentionally prevents automatic input changes until work is committed/stashed.
|
||||
|
||||
Inspect or trigger:
|
||||
|
||||
```sh
|
||||
nixfmt --check ./*.nix
|
||||
nix flake check --no-build --no-update-lock-file
|
||||
nix build .#checks.x86_64-linux.updates .#checks.x86_64-linux.desktop-config --no-link
|
||||
nix build .#nixosConfigurations.dev.config.system.build.toplevel --no-update-lock-file
|
||||
systemctl list-timers nixos-update.timer
|
||||
journalctl -u nixos-update
|
||||
sudo systemctl start nixos-update.service
|
||||
sudo less /var/cache/nixos-update/last-success
|
||||
```
|
||||
|
||||
New source files must be added to Git for flakes to see them. Keep `flake.lock` in version control. A build does **not** activate changes.
|
||||
Manual refresh: `nix flake update nixpkgs home-manager nixpkgs-latest`, review `flake.lock`, then check/build. `system.stateVersion` / `home.stateVersion` remain `26.05`: they govern compatibility, not package freshness. Channels are disabled; the Nixpkgs registry and `<nixpkgs>` lookup follow the locked system input.
|
||||
|
||||
Review and activate exactly the built closure:
|
||||
## Editor and application data
|
||||
|
||||
Neovim imports [the existing neovim-dots source](https://git.cyber.ayyalasomayajula.net/marsultor/neovim-dots), pinned to `380eb86778a7c53a0f1c18e84f14037456155347`. Lua, AstroNvim, Lazy, Mason, plugins and keymaps are unchanged. No Nixvim, replacement loader or plugin-manager migration.
|
||||
|
||||
Home Manager links configuration files under `~/.config/nvim`. Lazy's writable lock lives under `~/.local/state/nvim/locks/<revision>.json`, seeded once per dotfile revision. An earlier manual checkout is preserved at `~/projects/neovim-dots-before-nix`; activation refuses to overwrite an existing backup. The host input is reproducible; Lazy/Mason's mutable runtime downloads are not claimed to be fully Nix-reproducible.
|
||||
|
||||
As `dev`, the separate runtime audit may download dependencies but does not modify managed Lua or save buffers:
|
||||
|
||||
```sh
|
||||
built=$(readlink -f result)
|
||||
sudo "$built/sw/bin/nixos-rebuild" dry-activate --no-reexec --store-path "$built"
|
||||
sudo "$built/sw/bin/nixos-rebuild" switch --no-reexec --store-path "$built"
|
||||
nvim --headless ~/.config/nvim/init.lua -c 'luafile /etc/nix/neovim-test.lua'
|
||||
```
|
||||
|
||||
Use the rebuild tool from that closure with `--no-reexec`: otherwise the bootstrap tool can try to rebuild itself through the old channel even when `--store-path` is supplied. Capturing `built` also keeps review and activation on the same immutable result.
|
||||
KeePassXC preferences are seeded once and stay writable. Create/open your own vault, pair the browser extension and choose a dedicated **Secret Service Integration** group for application tokens. Element Nightly is forced to libsecret storage; **do not select its unencrypted fallback**. The native audit confirmed that Nightly starts, but login needs an unlocked/configured vault. No vault, account or VPN credentials are embedded.
|
||||
|
||||
For initial deployment on a compatible NixOS EC2 base where flakes are not enabled yet, check out this repo and run the build as an existing administrator with the temporary CLI flag:
|
||||
`element-nightly.nix` packages the official Element Nightly **2026090401** Debian artifact by URL and SHA-256, preserving its matched Electron/native modules. It uses Chromium's user-namespace sandbox rather than the unusable setuid helper; `--no-sandbox` is not used. Its Nightly profile is separate from stable Element; existing account data is not copied or deleted. The launcher is **Element-Nightly**, command `element-desktop-nightly`; `element-desktop` is a compatibility alias.
|
||||
|
||||
```sh
|
||||
nix --extra-experimental-features 'nix-command flakes' build \
|
||||
.#nixosConfigurations.dev.config.system.build.toplevel --no-update-lock-file
|
||||
```
|
||||
**Nightly update boundary:** this upstream binary is not packaged by the pinned Nixpkgs inputs. Its URL/version/hash must be refreshed in `element-nightly.nix` from the official `packages.element.io` package index, then rebuilt. The daily flake-input updater does not silently mutate this source file. This intentionally keeps the setup simple and reproducible rather than adding another downloader/self-updater.
|
||||
|
||||
Then review/apply the resulting closure as above. The configuration creates `dev`, sets permissions and deploys its files. **No separate Neovim clone, copy, useradd, chown or global Git-config recipe is required.** After either host configuration is active, `nix-command` and `flakes` are system defaults through `nix.settings.experimental-features`; the temporary flag is only for bootstrapping older bases that cannot read flakes yet.
|
||||
## Validation and contributions
|
||||
|
||||
This host build does not use a mutable channel. NixOS's native flake integration pins the `nixpkgs` registry entry and login-shell `<nixpkgs>` lookup to the system input; the global `nix-path` setting keeps the same pin when `NIX_PATH` is unset. Old root channel profiles are retained for recovery, not used as build inputs. Dev-environment templates/composition remain deferred; there is no flake framework here.
|
||||
`./audit-desktop.sh` runs the disposable graphical VM; `./audit-desktop.sh /dev/dri/renderD128` optionally uses an accessible render node. Logs/screenshots go to `~/.cache/desktop-audit/run.*`. The VM tests session services, fonts, audio, scaling and real PAM with **test-only** credentials. The native live screenshot audit covers this laptop; neither substitutes for real hardware suspend, hotplug or browser portal tests.
|
||||
|
||||
## Neovim: import, do not rewrite
|
||||
|
||||
The input is [the existing neovim-dots repository](https://git.cyber.ayyalasomayajula.net/marsultor/neovim-dots), initially pinned to `380eb86778a7c53a0f1c18e84f14037456155347`.
|
||||
|
||||
Home Manager deploys its files under `~/.config/nvim`, with the **Lua, AstroNvim, Lazy, Mason, plugins and keymaps unchanged**. `programs.neovim.configure` stays empty so Neovim discovers `init.lua` normally. No Nixvim or custom Lua loader.
|
||||
|
||||
- `tools.nix` supplies GCC/Make, pkg-config, Python, Node, Lua 5.1/LuaRocks and `nix-ld` for the existing plugin builds and Mason's upstream Linux executables. These are runtime prerequisites, not a replacement plugin manager or project-template framework.
|
||||
- Configuration files are linked from the pinned source and managed by Nix. Change the upstream repo and its input revision rather than editing generated links.
|
||||
- Lazy's `lazy-lock.json` must remain writable. The config seeds a copy at `~/.local/state/nvim/locks/<dotfile-revision>.json` and links to it. A new dotfile revision gets its own original lock; repeated activation preserves runtime changes to an existing lock.
|
||||
- A declared migration preserves the earlier manual checkout intact at `~/projects/neovim-dots-before-nix`. It refuses to overwrite an existing backup. On a clean home this migration does nothing.
|
||||
|
||||
**Reproducibility boundary:** Nix locks the host inputs and dotfile source, and reproduces their deployment. The existing Lua still bootstraps Lazy and manages plugin/Mason downloads at runtime. The supplied Lazy lock records plugin revisions, but it is writable and Mason's tool versions are not pinned by this Nix config. This is not a claim that every runtime download/cache is a Nix-reproducible build. Changing that policy requires a separate agreement; do not silently replace the user's plugin managers.
|
||||
|
||||
## Validation and commits
|
||||
|
||||
The initial deployment was tested with an empty disposable home: all upstream files were reproduced byte-for-byte, the Lazy lock remained writable across repeated activation, and the migration preserved local data and refused to overwrite an existing backup. The real plugin bootstrap is a separate runtime test, not covered by these file-deployment checks. As `dev`, run `nvim --headless ~/.config/nvim/init.lua -c 'luafile /etc/nixos/neovim-test.lua'` for a bounded runtime check of Lazy, the configured Mason tools (including executable startup), and nine parsers. This uses the existing writable plugin/Mason cache and may download dependencies; it does not edit the managed Lua or save buffers.
|
||||
|
||||
Run the graphical test separately with `./audit-desktop.sh`. An optional accessible render node, for example `./audit-desktop.sh /dev/dri/renderD128`, moves rendering out of QEMU's CPU emulation without touching the host display. Screenshots and logs go to `~/.cache/desktop-audit/run.*`. The test uses an isolated VM, test-only credentials and an emulated sound card with a silent backend. It checks session services, fonts, 100%/150% scaling, real PAM lock/unlock, speaker volume, microphone mute and audio controls. Hardware audio, NVIDIA, suspend and mixed-monitor behavior still need the target workstation. `nix flake check` without `--no-build` also runs the software-rendered VM and can be very slow without KVM.
|
||||
|
||||
Make focused changes, format/evaluate them, and commit regularly. Build and activate a reviewed commit rather than accumulating uncommitted setup. `system.configurationRevision` records the source revision in the system generation. Do not put human identity guesses in Git settings or push to an unapproved remote.
|
||||
|
||||
## Updates and safety
|
||||
|
||||
- **EC2 only:** `nixos-update.timer` checks daily with up to one hour of jitter. Nixpkgs/Home Manager release branches and the latest developer-tool package input advance; Neovim's source stays fixed. The updater skips a dirty repository, builds in a detached worktree, records a tested commit and applies it without rebooting. It attempts rollback if activation fails and retains recovery generations. Inspect `journalctl -u nixos-update` or trigger it with `sudo systemctl start nixos-update`.
|
||||
- For a manual input refresh: `nix flake update nixpkgs home-manager nixpkgs-latest`, review `flake.lock`, then check/build. The exact resolved revisions and hashes remain committed.
|
||||
- Keep the EC2 module on the EC2 target only. Preserve sandboxing, signature verification and each host's recovery access.
|
||||
- `system.stateVersion` and `home.stateVersion` are both `26.05`; these preserve compatibility, not package versions.
|
||||
- `test` activates changes too; it is not a dry run. Keep the original system generation.
|
||||
- Rollbacks do not restore mutable user/application data, lockfile updates, backups, or this Git working tree.
|
||||
- No private keys, plaintext secrets, build outputs or agent notes in this repo.
|
||||
Plain `nix flake check` also builds/runs the VM and may be slow without KVM. Use `--no-build` for evaluation, then select bounded checks explicitly. Keep changes focused, format/evaluate/test them, and commit reviewed configuration. Do not push to an unapproved remote, invent a human Git identity, commit secrets, screenshots of personal windows, build outputs or agent scratch notes.
|
||||
|
||||
+17
-14
@@ -1,24 +1,27 @@
|
||||
* { font-family: Inter, sans-serif; font-size: 17px; }
|
||||
* { font-family: Inter, sans-serif; font-size: 16px; }
|
||||
window { background: transparent; }
|
||||
box.main {
|
||||
padding: 14px;
|
||||
margin: 18px;
|
||||
border-radius: 16px;
|
||||
border: 2px solid #78a9ff;
|
||||
background: #161616;
|
||||
padding: 12px;
|
||||
margin: 12px;
|
||||
border-radius: 6px;
|
||||
border: 2px solid @accent@;
|
||||
background: @background@;
|
||||
box-shadow: 0 8px 18px rgba(0, 0, 0, .35);
|
||||
}
|
||||
text {
|
||||
color: #f2f4f8;
|
||||
background: #262626;
|
||||
color: @text@;
|
||||
background: @surface@;
|
||||
min-height: 38px;
|
||||
padding: 10px 14px;
|
||||
border-radius: 9px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.matches, list.plugin { background: transparent; }
|
||||
box.plugin:first-child { margin-top: 10px; }
|
||||
.match { padding: 9px; border-radius: 9px; background: transparent; }
|
||||
.match:selected { background: #2e3f5f; }
|
||||
label.match { color: #f2f4f8; }
|
||||
label.match.description { font-size: 14px; color: #a2a9b0; }
|
||||
label.plugin.info { color: #a2a9b0; }
|
||||
/* The row, boxes, icon AND labels all share .match upstream. Padding that
|
||||
class multiplies row height until the launcher extends off the screen. */
|
||||
.match { background: transparent; }
|
||||
row.match { padding: 7px 10px; border-radius: 4px; }
|
||||
row.match:selected { background: @selection@; }
|
||||
label.match { color: @text@; }
|
||||
label.match.description { font-size: 14px; color: @muted@; }
|
||||
label.plugin.info { color: @muted@; }
|
||||
|
||||
@@ -7,16 +7,7 @@
|
||||
|
||||
let
|
||||
c = import ./colors.nix;
|
||||
# Select secure storage without rebuilding the cached Electron application.
|
||||
elementWithKeyring = pkgs.symlinkJoin {
|
||||
name = "element-desktop-with-keyring-${pkgs.element-desktop.version}";
|
||||
paths = [ pkgs.element-desktop ];
|
||||
nativeBuildInputs = [ pkgs.makeWrapper ];
|
||||
postBuild = ''
|
||||
wrapProgram "$out/bin/element-desktop" --add-flags "--password-store=gnome-libsecret"
|
||||
'';
|
||||
inherit (pkgs.element-desktop) meta;
|
||||
};
|
||||
elementNightly = import ./element-nightly.nix { inherit pkgs; };
|
||||
browserAddon = pkgs.fetchurl {
|
||||
name = "keepassxc-browser-1.10.3.xpi";
|
||||
url = "https://addons.mozilla.org/firefox/downloads/file/4831838/keepassxc_browser-1.10.3.xpi";
|
||||
@@ -58,6 +49,13 @@ in
|
||||
"nvidia-persistenced"
|
||||
]
|
||||
);
|
||||
programs.thunar = {
|
||||
enable = true;
|
||||
plugins = [
|
||||
pkgs.thunar-archive-plugin
|
||||
pkgs.thunar-volman
|
||||
];
|
||||
};
|
||||
programs.steam = {
|
||||
enable = true;
|
||||
remotePlay.openFirewall = false;
|
||||
@@ -66,11 +64,31 @@ in
|
||||
};
|
||||
programs.firefox = {
|
||||
enable = true;
|
||||
# Stable's ESR is security-current (153.2); regular 155.0 is one patch behind.
|
||||
# Use the security-supported ESR line from the system pin.
|
||||
package = pkgs.firefox-esr;
|
||||
nativeMessagingHosts.packages = [ pkgs.keepassxc ];
|
||||
policies = {
|
||||
DisableTelemetry = true;
|
||||
DontCheckDefaultBrowser = true;
|
||||
NoDefaultBookmarks = true;
|
||||
Preferences = {
|
||||
"browser.compactmode.show" = {
|
||||
Value = true;
|
||||
Status = "default";
|
||||
};
|
||||
"browser.uidensity" = {
|
||||
Value = 1;
|
||||
Status = "default";
|
||||
};
|
||||
"browser.newtabpage.activity-stream.showSponsored" = {
|
||||
Value = false;
|
||||
Status = "locked";
|
||||
};
|
||||
"browser.newtabpage.activity-stream.showSponsoredTopSites" = {
|
||||
Value = false;
|
||||
Status = "locked";
|
||||
};
|
||||
};
|
||||
OfferToSaveLogins = false;
|
||||
ExtensionSettings."keepassxc-browser@keepassxc.org" = {
|
||||
installation_mode = "normal_installed";
|
||||
@@ -83,11 +101,20 @@ in
|
||||
home-manager.users.dev = {
|
||||
home.packages = with pkgs; [
|
||||
thunderbird
|
||||
file-roller
|
||||
imv
|
||||
# Electron does not reliably detect a keyring under Hyprland. Use the
|
||||
# KeePassXC Secret Service explicitly; never fall back to basic_text.
|
||||
elementWithKeyring
|
||||
elementNightly
|
||||
slack
|
||||
];
|
||||
programs.mpv = {
|
||||
enable = true;
|
||||
config = {
|
||||
hwdec = "auto-safe";
|
||||
keep-open = true;
|
||||
};
|
||||
};
|
||||
programs.keepassxc = {
|
||||
enable = true;
|
||||
autostart = true;
|
||||
@@ -113,7 +140,7 @@ in
|
||||
inputbar-fg = c.text;
|
||||
completion-bg = c.background;
|
||||
completion-fg = c.text;
|
||||
completion-highlight-bg = "#354562";
|
||||
completion-highlight-bg = c.selection;
|
||||
completion-highlight-fg = c.text;
|
||||
notification-bg = c.surface;
|
||||
notification-fg = c.text;
|
||||
@@ -127,11 +154,21 @@ in
|
||||
enable = true;
|
||||
defaultApplications = {
|
||||
"application/pdf" = [ "org.pwmt.zathura.desktop" ];
|
||||
"inode/directory" = [ "thunar.desktop" ];
|
||||
"application/zip" = [ "org.gnome.FileRoller.desktop" ];
|
||||
"image/png" = [ "imv.desktop" ];
|
||||
"image/jpeg" = [ "imv.desktop" ];
|
||||
"image/webp" = [ "imv.desktop" ];
|
||||
"video/mp4" = [ "mpv.desktop" ];
|
||||
"video/x-matroska" = [ "mpv.desktop" ];
|
||||
"audio/mpeg" = [ "mpv.desktop" ];
|
||||
"text/html" = [ "firefox-esr.desktop" ];
|
||||
"x-scheme-handler/http" = [ "firefox-esr.desktop" ];
|
||||
"x-scheme-handler/https" = [ "firefox-esr.desktop" ];
|
||||
"x-scheme-handler/mailto" = [ "thunderbird.desktop" ];
|
||||
"x-scheme-handler/matrix" = [ "element-desktop.desktop" ];
|
||||
"x-scheme-handler/matrix" = [ "element-desktop-nightly.desktop" ];
|
||||
"x-scheme-handler/element" = [ "element-desktop-nightly.desktop" ];
|
||||
"x-scheme-handler/io.element.desktop" = [ "element-desktop-nightly.desktop" ];
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
+17
-14
@@ -1,16 +1,19 @@
|
||||
# Small shared palette, not a theme framework. Hyprland's native Lua uses the
|
||||
# same accent/border colors explicitly; wallpaper.svg is original source art.
|
||||
# One Ring: charcoal, old gold, parchment and muted woodland accents.
|
||||
# Semantic UI colors are separate from ANSI terminal colors. Consumers substitute
|
||||
# these values into native Lua/CSS; there is no runtime theme generator.
|
||||
{
|
||||
background = "#161616";
|
||||
surface = "#262626";
|
||||
raised = "#393939";
|
||||
border = "#525252";
|
||||
text = "#f2f4f8";
|
||||
muted = "#a2a9b0";
|
||||
blue = "#78a9ff";
|
||||
cyan = "#3ddbd9";
|
||||
purple = "#be95ff";
|
||||
green = "#42be65";
|
||||
yellow = "#f1c21b";
|
||||
red = "#ff6b7a";
|
||||
background = "#121311";
|
||||
surface = "#1e201b";
|
||||
raised = "#2c3027";
|
||||
border = "#4a5142";
|
||||
selection = "#3a3d2e";
|
||||
text = "#e9e4d5";
|
||||
muted = "#adb2a0";
|
||||
accent = "#c6a664";
|
||||
blue = "#8faeb5";
|
||||
cyan = "#92b8a0";
|
||||
purple = "#b5a5be";
|
||||
green = "#a3b878";
|
||||
yellow = "#d8bb78";
|
||||
red = "#df8b78";
|
||||
}
|
||||
|
||||
+2
-12
@@ -11,18 +11,8 @@
|
||||
];
|
||||
|
||||
nixpkgs.hostPlatform = "x86_64-linux";
|
||||
nixpkgs.overlays = [
|
||||
(_final: prev: {
|
||||
# Pi is a fast-moving agent harness; stable branches can be generations old.
|
||||
# Use the latest resolving Nixpkgs input while keeping the rest of the
|
||||
# system on the stable release branch.
|
||||
pi-coding-agent =
|
||||
(import inputs.nixpkgs-latest {
|
||||
inherit (prev.stdenv.hostPlatform) system;
|
||||
config = prev.config or { };
|
||||
}).pi-coding-agent;
|
||||
})
|
||||
];
|
||||
# tools.nix/network.nix select current standalone CLIs without overriding the
|
||||
# system package set (or mixing unstable libraries into the desktop stack).
|
||||
|
||||
nix = {
|
||||
channel.enable = false;
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
"""Offline safety/behavior tests for desktop actions: no host desktop or power calls."""
|
||||
import importlib.util
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
spec = importlib.util.spec_from_file_location("actions", sys.argv.pop(1))
|
||||
actions = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(actions)
|
||||
|
||||
|
||||
class ActionsTest(unittest.TestCase):
|
||||
def test_picker_cancel_and_untrusted_output(self):
|
||||
for code, text in [(1, "0"), (0, "$(touch /oops)"), (0, "-1"), (0, "9")]:
|
||||
with patch.object(actions, "run", return_value=subprocess.CompletedProcess([], code, text)):
|
||||
self.assertIsNone(actions.pick("Test", ["only item"]))
|
||||
|
||||
def test_picker_uses_index_and_sanitizes_labels(self):
|
||||
with patch.object(actions, "run", return_value=subprocess.CompletedProcess([], 0, "0\n")) as run:
|
||||
self.assertEqual(actions.pick("Test", ["title\nsecond\x00row"]), 0)
|
||||
self.assertEqual(run.call_args.kwargs["input"], "title second row")
|
||||
self.assertIn("--index", run.call_args.args)
|
||||
|
||||
def test_address_validation(self):
|
||||
self.assertEqual(actions.address("0xabc123"), "address:0xabc123")
|
||||
for value in ["", "0xABC; os.execute('bad')", "123", '"}']:
|
||||
with self.assertRaises(ValueError):
|
||||
actions.address(value)
|
||||
|
||||
def test_no_power_action_on_cancel(self):
|
||||
with patch.object(actions, "pick", return_value=None), patch.object(actions, "run") as run:
|
||||
actions.power()
|
||||
run.assert_not_called()
|
||||
with patch.object(actions, "pick", return_value=4), patch.object(actions, "confirm", return_value=False), patch.object(actions, "run") as run:
|
||||
actions.power()
|
||||
run.assert_not_called()
|
||||
|
||||
def test_screenshot_cancellation(self):
|
||||
with patch.object(actions, "capture_geometry", return_value=None), patch.object(actions, "run") as run:
|
||||
actions.screenshot()
|
||||
run.assert_not_called()
|
||||
|
||||
def test_sensitive_and_locked_clipboard_not_stored(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
with patch.dict(os.environ, {"XDG_RUNTIME_DIR": directory, "CLIPBOARD_STATE": "sensitive"}), patch.object(actions, "run") as run:
|
||||
actions.clipboard("store")
|
||||
run.assert_not_called()
|
||||
with patch.dict(os.environ, {"XDG_RUNTIME_DIR": directory, "CLIPBOARD_STATE": "data"}), patch.object(actions, "run") as run:
|
||||
(actions.runtime() / "locked").touch()
|
||||
actions.clipboard("store")
|
||||
run.assert_not_called()
|
||||
|
||||
def test_clear_removes_database_not_just_entries(self):
|
||||
with tempfile.TemporaryDirectory() as directory, patch.dict(os.environ, {"XDG_RUNTIME_DIR": directory}):
|
||||
database = actions.runtime() / "clipboard.db"
|
||||
database.write_bytes(b"old history pages")
|
||||
actions.clipboard("clear")
|
||||
self.assertFalse(database.exists())
|
||||
|
||||
def test_clipboard_roundtrip_preserves_bytes(self):
|
||||
data = b" leading\ntrailing \x00\xff"
|
||||
with tempfile.TemporaryDirectory() as directory, patch.dict(os.environ, {"XDG_RUNTIME_DIR": directory}):
|
||||
(actions.runtime() / "clipboard.db").touch()
|
||||
calls = []
|
||||
def fake(*args, **kwargs):
|
||||
calls.append((args, kwargs))
|
||||
value = "4\tpreview\n" if args == ("cliphist", "list") else data
|
||||
return subprocess.CompletedProcess(args, 0, value)
|
||||
with patch.object(actions, "run", side_effect=fake), patch.object(actions, "pick", return_value=0):
|
||||
actions.clipboard("pick")
|
||||
self.assertEqual(calls[-1][0], ("wl-copy",))
|
||||
self.assertEqual(calls[-1][1]["input"], data)
|
||||
|
||||
def test_web_search_is_url_encoded_not_shell(self):
|
||||
result = subprocess.CompletedProcess([], 0, "hello; $(bad) & stuff")
|
||||
with patch.object(actions, "run", return_value=result) as run:
|
||||
actions.web_search()
|
||||
self.assertEqual(run.call_args.args[:4], ("uwsm", "app", "--", "xdg-open"))
|
||||
self.assertIn("hello%3B+%24%28bad%29+%26+stuff", run.call_args.args[4])
|
||||
|
||||
def test_lua_strings_do_not_use_json_control_escapes(self):
|
||||
self.assertEqual(actions.lua('a"\n\x00'), '"\\097\\034\\010\\000"')
|
||||
|
||||
def test_lock_still_runs_when_clipboard_wipe_fails(self):
|
||||
with tempfile.TemporaryDirectory() as directory, patch.dict(os.environ, {"XDG_RUNTIME_DIR": directory}):
|
||||
with patch.object(actions, "clipboard", side_effect=subprocess.CalledProcessError(1, "cliphist")), patch.object(actions, "run", return_value=subprocess.CompletedProcess([], 0)) as run:
|
||||
actions.lock_start()
|
||||
run.assert_called_once_with("hyprlock", check=False)
|
||||
self.assertFalse((actions.runtime() / "locked").exists())
|
||||
|
||||
def test_record_inhibitor_is_removed_on_failure(self):
|
||||
calls = []
|
||||
def fake(*args, **kwargs):
|
||||
calls.append(args)
|
||||
if args[0] == "wf-recorder":
|
||||
raise subprocess.CalledProcessError(1, args)
|
||||
return subprocess.CompletedProcess(args, 0)
|
||||
with patch.object(actions.signal, "signal"), patch.object(actions, "run", side_effect=fake):
|
||||
with self.assertRaises(subprocess.CalledProcessError):
|
||||
actions.record_run(["-f", "test.mkv"])
|
||||
self.assertEqual(calls[-1], ("swaync-client", "-Ir", "workstation-recording", "-sw"))
|
||||
|
||||
def test_only_osd_notifications_are_transient(self):
|
||||
with patch.object(actions, "run") as run:
|
||||
actions.notify("Saved", "Screenshot")
|
||||
self.assertNotIn("int:transient:1", run.call_args.args)
|
||||
actions.notify("Volume", "50%", 50)
|
||||
self.assertIn("int:transient:1", run.call_args.args)
|
||||
|
||||
def test_display_timeout_restores_scale(self):
|
||||
monitor = '[{"name":"eDP-1","width":1920,"height":1200,"scale":1.5}]'
|
||||
calls = []
|
||||
def fake(*args, **kwargs):
|
||||
calls.append(args)
|
||||
if args[0] == "fuzzel":
|
||||
raise subprocess.TimeoutExpired(args, 15)
|
||||
return subprocess.CompletedProcess(args, 0, "ok")
|
||||
with patch.object(actions, "output", return_value=monitor), patch.object(actions, "pick", side_effect=[0, 0]), patch.object(actions, "run", side_effect=fake):
|
||||
actions.displays()
|
||||
self.assertIn("scale=1}", calls[0][-1])
|
||||
self.assertIn("scale=1.5}", calls[-1][-1])
|
||||
|
||||
|
||||
unittest.main()
|
||||
@@ -0,0 +1,520 @@
|
||||
"""Small desktop actions, not a shell framework. No selected text is executed.
|
||||
|
||||
Control state is session-local; explicit captures use XDG media directories.
|
||||
Dependencies are supplied by desktop.nix.
|
||||
"""
|
||||
import datetime
|
||||
import fcntl
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import shutil
|
||||
import signal
|
||||
import subprocess as sp
|
||||
import sys
|
||||
import time
|
||||
from urllib.parse import quote_plus
|
||||
|
||||
|
||||
def run(*args, check=True, **kwargs):
|
||||
return sp.run(list(args), check=check, **kwargs)
|
||||
|
||||
|
||||
def output(*args):
|
||||
return run(*args, capture_output=True, text=True).stdout.strip()
|
||||
|
||||
|
||||
def notify(title, body="", value=None):
|
||||
args = ["notify-send", "-a", "Workstation"]
|
||||
if value is not None:
|
||||
# OSD replaces only OSD, never a saved screenshot or action failure.
|
||||
args += ["-h", "string:x-canonical-private-synchronous:workstation-osd",
|
||||
"-h", "int:transient:1", "-h", f"int:value:{max(0, min(100, int(value)))}", "-t", "1500"]
|
||||
run(*args, title, body, check=False)
|
||||
|
||||
|
||||
def pick(prompt, choices):
|
||||
"""Return a validated index; Escape/custom unmatched input never acts."""
|
||||
if not choices:
|
||||
notify(prompt, "Nothing available")
|
||||
return None
|
||||
labels = [re.sub(r"[\x00-\x1f\x7f]", " ", str(x)) for x in choices]
|
||||
result = run("fuzzel", "--dmenu", "--index", "--prompt", prompt + " ",
|
||||
input="\n".join(labels), text=True, capture_output=True, check=False)
|
||||
if result.returncode != 0:
|
||||
return None
|
||||
try:
|
||||
index = int(result.stdout.strip())
|
||||
except ValueError:
|
||||
return None
|
||||
return index if 0 <= index < len(choices) else None
|
||||
|
||||
|
||||
def confirm(action):
|
||||
return pick(action + "?", ["Cancel", action]) == 1
|
||||
|
||||
|
||||
def runtime():
|
||||
path = Path(os.environ["XDG_RUNTIME_DIR"]) / "workstation"
|
||||
path.mkdir(mode=0o700, exist_ok=True)
|
||||
return path
|
||||
|
||||
|
||||
def clients():
|
||||
return json.loads(output("hyprctl", "-j", "clients"))
|
||||
|
||||
|
||||
def dispatch(expression):
|
||||
response = output("hyprctl", "dispatch", expression)
|
||||
if response not in ("", "ok"):
|
||||
raise RuntimeError(response)
|
||||
|
||||
|
||||
def lua(value):
|
||||
if isinstance(value, str):
|
||||
# Lua does not support JSON's \\uXXXX escapes. Fixed-width decimal byte
|
||||
# escapes also keep newlines, quotes and arbitrary device names inert.
|
||||
return '"' + ''.join(f'\\{byte:03d}' for byte in value.encode()) + '"'
|
||||
return json.dumps(value, allow_nan=False)
|
||||
|
||||
|
||||
def address(value):
|
||||
if not re.fullmatch(r"0x[0-9a-fA-F]+", value):
|
||||
raise ValueError("Invalid compositor window address")
|
||||
return "address:" + value
|
||||
|
||||
|
||||
def window_picker():
|
||||
windows = sorted(clients(), key=lambda c: (c["workspace"]["id"], c.get("focusHistoryID", 0)))
|
||||
labels = [f'{c["workspace"]["name"]} · {c["class"]} — {c["title"]}' for c in windows]
|
||||
index = pick("Windows", labels)
|
||||
if index is not None:
|
||||
dispatch("hl.dsp.focus({window=" + lua(address(windows[index]["address"])) + "})")
|
||||
|
||||
|
||||
def scratch():
|
||||
existing = any(c["class"] == "dropterminal" for c in clients())
|
||||
dispatch('hl.dsp.workspace.toggle_special("terminal")')
|
||||
if not existing:
|
||||
run("uwsm", "app", "--", "kitty", "--class", "dropterminal")
|
||||
|
||||
|
||||
def clipboard(action):
|
||||
# Lock checks and database writes together: an in-flight store cannot put
|
||||
# a clipboard item back AFTER the lock-screen wipe has completed.
|
||||
if action not in {"store", "clear"}:
|
||||
_clipboard(action)
|
||||
return
|
||||
with (runtime() / "clipboard.lock").open("w") as handle:
|
||||
fcntl.flock(handle, fcntl.LOCK_EX)
|
||||
_clipboard(action)
|
||||
|
||||
|
||||
def _clipboard(action):
|
||||
root = runtime()
|
||||
env = dict(os.environ, CLIPHIST_DB_PATH=str(root / "clipboard.db"), CLIPHIST_MAX_ITEMS="200")
|
||||
database = root / "clipboard.db"
|
||||
if action == "store":
|
||||
if os.environ.get("CLIPBOARD_STATE") in ("sensitive", "nil", "clear"):
|
||||
return
|
||||
if (root / "clipboard-paused").exists() or (root / "locked").exists():
|
||||
return
|
||||
run("cliphist", "store", env=env)
|
||||
elif action == "clear":
|
||||
# cliphist wipe leaves its database file (and free pages) behind.
|
||||
# Stores are serialized above, so remove the session database itself.
|
||||
database.unlink(missing_ok=True)
|
||||
elif action == "pause":
|
||||
marker = root / "clipboard-paused"
|
||||
if marker.exists():
|
||||
marker.unlink()
|
||||
notify("Clipboard history resumed")
|
||||
else:
|
||||
marker.touch(mode=0o600)
|
||||
notify("Clipboard history paused")
|
||||
elif action in ("pick", "delete"):
|
||||
if not database.exists():
|
||||
notify("Clipboard history", "No items in this session")
|
||||
return
|
||||
rows = run("cliphist", "list", env=env, capture_output=True, text=True).stdout.splitlines()
|
||||
index = pick("Clipboard" if action == "pick" else "Delete clipboard item", rows)
|
||||
if index is None:
|
||||
return
|
||||
row = rows[index] + "\n"
|
||||
if action == "delete":
|
||||
run("cliphist", "delete", env=env, input=row, text=True)
|
||||
else:
|
||||
data = run("cliphist", "decode", env=env, input=row.encode(), capture_output=True).stdout
|
||||
run("wl-copy", input=data)
|
||||
|
||||
|
||||
def lock_start():
|
||||
root = runtime()
|
||||
with (root / "locker.lock").open("w") as handle:
|
||||
try:
|
||||
fcntl.flock(handle, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
||||
except BlockingIOError:
|
||||
return
|
||||
(root / "locked").touch(mode=0o600)
|
||||
try:
|
||||
clipboard("clear")
|
||||
except (OSError, sp.SubprocessError) as error:
|
||||
# Clipboard trouble must NEVER prevent the screen from locking.
|
||||
print(f"Clipboard wipe failed before lock: {error}", file=sys.stderr)
|
||||
result = run("hyprlock", check=False)
|
||||
# A crash must not resume clipboard collection on a still-locked session.
|
||||
if result.returncode == 0:
|
||||
(root / "locked").unlink(missing_ok=True)
|
||||
else:
|
||||
raise RuntimeError("Locker failed; clipboard history remains paused")
|
||||
|
||||
|
||||
def power():
|
||||
choices = ["Lock", "Suspend", "Log out", "Reboot", "Shut down"]
|
||||
index = pick("Session", choices)
|
||||
if index is None:
|
||||
return
|
||||
if index == 0:
|
||||
run("loginctl", "lock-session")
|
||||
elif confirm(choices[index]):
|
||||
if index == 2:
|
||||
run("uwsm", "stop")
|
||||
else:
|
||||
run("systemctl", {1: "suspend", 3: "reboot", 4: "poweroff"}[index])
|
||||
|
||||
|
||||
def capture_geometry(mode):
|
||||
if mode == "area":
|
||||
result = run("slurp", capture_output=True, text=True, check=False)
|
||||
if result.returncode != 0 or not result.stdout.strip():
|
||||
return None
|
||||
return ["-g", result.stdout.strip()]
|
||||
if mode == "window":
|
||||
window = json.loads(output("hyprctl", "-j", "activewindow"))
|
||||
if not window.get("address"):
|
||||
return None
|
||||
x, y = map(int, window["at"])
|
||||
w, h = map(int, window["size"])
|
||||
if w <= 0 or h <= 0:
|
||||
return None
|
||||
return ["-g", f"{x},{y} {w}x{h}"]
|
||||
if mode == "output":
|
||||
monitors = json.loads(output("hyprctl", "-j", "monitors"))
|
||||
return ["-o", next(m["name"] for m in monitors if m["focused"])]
|
||||
if mode == "all":
|
||||
return []
|
||||
raise ValueError("Unknown capture mode")
|
||||
|
||||
|
||||
def destination(kind, extension):
|
||||
# User's XDG directory setting, with a conventional fallback.
|
||||
key = "PICTURES" if kind == "Screenshots" else "VIDEOS"
|
||||
base = Path(output("xdg-user-dir", key))
|
||||
folder = base / kind
|
||||
folder.mkdir(mode=0o700, parents=True, exist_ok=True)
|
||||
stamp = datetime.datetime.now().strftime("%Y-%m-%d_%H-%M-%S_%f")
|
||||
return folder / (stamp + extension)
|
||||
|
||||
|
||||
def screenshot(mode="area", annotate=False, delay=0, copy_only=False):
|
||||
if delay:
|
||||
notify("Screenshot", f"Capturing in {delay} seconds")
|
||||
time.sleep(delay)
|
||||
geometry = capture_geometry(mode)
|
||||
if geometry is None:
|
||||
return
|
||||
image = run("grim", *geometry, "-", capture_output=True).stdout
|
||||
if annotate:
|
||||
target = destination("Screenshots", ".png")
|
||||
run("satty", "--filename", "-", "--copy-command", "wl-copy",
|
||||
"--output-filename", str(target), input=image)
|
||||
else:
|
||||
run("wl-copy", "--type", "image/png", input=image)
|
||||
if not copy_only:
|
||||
target = destination("Screenshots", ".png")
|
||||
target.write_bytes(image)
|
||||
notify("Screenshot saved and copied", str(target))
|
||||
else:
|
||||
notify("Screenshot copied")
|
||||
|
||||
|
||||
def screenshot_menu():
|
||||
actions = [("Region · annotate", "area", True, 0, False),
|
||||
("Region · copy only", "area", False, 0, True),
|
||||
("Active window", "window", False, 0, False),
|
||||
("Current display", "output", False, 0, False),
|
||||
("All displays", "all", False, 0, False),
|
||||
("Current display · 5 second delay", "output", False, 5, False),
|
||||
("Current display · 10 second delay", "output", False, 10, False)]
|
||||
index = pick("Screenshot", [a[0] for a in actions])
|
||||
if index is not None:
|
||||
screenshot(*actions[index][1:])
|
||||
|
||||
|
||||
def recording():
|
||||
return run("systemctl", "--user", "is-active", "--quiet", "desktop-recording.service", check=False).returncode == 0
|
||||
|
||||
|
||||
def record():
|
||||
root = runtime()
|
||||
# Serialize two rapid keypresses so they cannot start competing recorders.
|
||||
with (root / "record.lock").open("w") as handle:
|
||||
fcntl.flock(handle, fcntl.LOCK_EX)
|
||||
if recording():
|
||||
run("systemctl", "--user", "stop", "desktop-recording.service")
|
||||
notify("Recording saved", (root / "recording-file").read_text() if (root / "recording-file").exists() else "")
|
||||
return
|
||||
index = pick("Record", ["Region · no audio", "Display · no audio", "Display · desktop audio"])
|
||||
if index is None:
|
||||
return
|
||||
geometry = capture_geometry("area" if index == 0 else "output")
|
||||
if geometry is None:
|
||||
return
|
||||
args = list(geometry)
|
||||
if index == 2:
|
||||
# Explicit monitor source: never silently record the microphone.
|
||||
sink = output("pactl", "get-default-sink")
|
||||
args += ["--audio=" + sink + ".monitor"]
|
||||
target = destination("Recordings", ".mkv")
|
||||
run("systemd-run", "--user", "--collect", "--unit=desktop-recording",
|
||||
"--property=KillSignal=SIGINT", "--property=TimeoutStopSec=20",
|
||||
"--property=PartOf=graphical-session.target", "--property=UMask=0077",
|
||||
"--setenv=PATH=" + os.environ["PATH"], "--",
|
||||
shutil.which("desktop"), "record-run", *args, "-f", str(target))
|
||||
(root / "recording-file").write_text(str(target))
|
||||
# The REC bar indicator is the start feedback. A popup here can race
|
||||
# the recorder's inhibitor and end up embedded in the captured video.
|
||||
|
||||
|
||||
def record_run(args):
|
||||
# A separate inhibitor does not overwrite the user's DND preference. The
|
||||
# unit sends SIGINT to both processes; let wf-recorder finish its container.
|
||||
signal.signal(signal.SIGINT, lambda *_: None)
|
||||
run("swaync-client", "-Ia", "workstation-recording", "-sw")
|
||||
try:
|
||||
run("wf-recorder", *args)
|
||||
finally:
|
||||
run("swaync-client", "-Ir", "workstation-recording", "-sw", check=False)
|
||||
|
||||
|
||||
def osd(kind, change):
|
||||
if kind in ("volume", "microphone"):
|
||||
device = "@DEFAULT_AUDIO_SOURCE@" if kind == "microphone" else "@DEFAULT_AUDIO_SINK@"
|
||||
if change == "mute":
|
||||
run("wpctl", "set-mute", device, "toggle")
|
||||
else:
|
||||
run("wpctl", "set-volume", "-l", "1", device, "5%+" if change == "up" else "5%-")
|
||||
value = output("wpctl", "get-volume", device)
|
||||
percent = round(float(value.split()[1]) * 100)
|
||||
notify("Microphone" if kind == "microphone" else "Volume", "Muted" if "MUTED" in value else f"{percent}%", percent)
|
||||
else:
|
||||
selector = ["-c", "leds", "-d", "*kbd_backlight*"] if kind == "keyboard" else ["-c", "backlight"]
|
||||
run("brightnessctl", *selector, "--min-value=1", "set", "+5%" if change == "up" else "5%-")
|
||||
value = output("brightnessctl", *selector, "-m")
|
||||
percent = int(value.split(",")[3].rstrip("%"))
|
||||
notify("Keyboard backlight" if kind == "keyboard" else "Brightness", f"{percent}%", percent)
|
||||
|
||||
|
||||
def night():
|
||||
current = output("hyprctl", "hyprsunset", "temperature")
|
||||
temperature = int(current)
|
||||
run("hyprctl", "hyprsunset", "identity" if temperature < 6000 else "temperature", *([] if temperature < 6000 else ["4200"]))
|
||||
notify("Night light", "Off" if temperature < 6000 else "4200 K")
|
||||
|
||||
|
||||
def displays():
|
||||
monitors = json.loads(output("hyprctl", "-j", "monitors"))
|
||||
choices = [f'{m["name"]} · {m["width"]}×{m["height"]} · {m["scale"]:g}×' for m in monitors]
|
||||
index = pick("Display", choices)
|
||||
if index is None:
|
||||
return
|
||||
m = monitors[index]
|
||||
actions = ["Scale 100%", "Scale 125%", "Scale 150%", "Scale 175%", "Scale 200%", "Restore declared configuration"]
|
||||
action = pick(m["name"], actions)
|
||||
if action is None:
|
||||
return
|
||||
if action == 5:
|
||||
run("hyprctl", "reload")
|
||||
return
|
||||
scale = [1, 1.25, 1.5, 1.75, 2][action]
|
||||
def set_scale(value):
|
||||
# Scaling must not silently change refresh rate, dock position or rotation.
|
||||
mode = f'{m["width"]}x{m["height"]}@{m.get("refreshRate", 60)}'
|
||||
position = f'{m.get("x", 0)}x{m.get("y", 0)}'
|
||||
run("hyprctl", "eval", 'hl.monitor({output=' + lua(m["name"]) + ',mode=' + lua(mode)
|
||||
+ ',position=' + lua(position) + ',transform=' + str(m.get("transform", 0))
|
||||
+ ',scale=' + str(value) + '})')
|
||||
set_scale(scale)
|
||||
# An unattended or invisible confirmation must revert, not strand the user.
|
||||
try:
|
||||
result = run("fuzzel", "--dmenu", "--index", "--prompt", "Keep display scale? ",
|
||||
input="Revert\nKeep\n", text=True, capture_output=True, check=False, timeout=15)
|
||||
keep = result.returncode == 0 and result.stdout.strip() == "1"
|
||||
except sp.TimeoutExpired:
|
||||
keep = False
|
||||
if not keep:
|
||||
set_scale(m["scale"])
|
||||
|
||||
|
||||
def web_search():
|
||||
result = run("fuzzel", "--dmenu", "--prompt", "Search web ", input="", text=True, capture_output=True, check=False)
|
||||
if result.returncode == 0 and result.stdout.strip():
|
||||
run("uwsm", "app", "--", "xdg-open", "https://duckduckgo.com/?q=" + quote_plus(result.stdout.strip()))
|
||||
|
||||
|
||||
def emoji():
|
||||
entries = []
|
||||
for line in Path(os.environ["DESKTOP_EMOJI_FILE"]).read_text().splitlines():
|
||||
if "; fully-qualified" not in line:
|
||||
continue
|
||||
code, detail = line.split("#", 1)
|
||||
character = "".join(chr(int(c, 16)) for c in code.split(";", 1)[0].split())
|
||||
description = re.sub(r"^\S+\s+E[\d.]+\s+", "", detail.strip())
|
||||
entries.append((character, character + " " + description))
|
||||
index = pick("Emoji", [entry[1] for entry in entries])
|
||||
if index is not None:
|
||||
run("wl-copy", input=entries[index][0].encode())
|
||||
notify("Emoji copied")
|
||||
|
||||
|
||||
def touchpad():
|
||||
devices = json.loads(output("hyprctl", "-j", "devices"))
|
||||
pads = [device["name"] for device in devices["mice"] if re.search("touchpad|trackpad", device["name"], re.I)]
|
||||
if not pads:
|
||||
notify("Touchpad", "No touchpad detected")
|
||||
return
|
||||
marker = runtime() / "touchpad-disabled"
|
||||
enabled = marker.exists()
|
||||
for name in pads:
|
||||
run("hyprctl", "eval", "hl.device({name=" + lua(name) + ",enabled=" + lua(enabled) + "})")
|
||||
if enabled:
|
||||
marker.unlink()
|
||||
else:
|
||||
marker.touch(mode=0o600)
|
||||
notify("Touchpad", "Enabled" if enabled else "Disabled")
|
||||
|
||||
|
||||
def airplane():
|
||||
disabled = output("nmcli", "radio", "wifi") == "disabled"
|
||||
if not disabled and not confirm("Disable wireless radios"):
|
||||
return
|
||||
run("nmcli", "radio", "wifi", "on" if disabled else "off")
|
||||
run("rfkill", "unblock" if disabled else "block", "bluetooth")
|
||||
notify("Airplane mode", "Off" if disabled else "On")
|
||||
|
||||
|
||||
def battery():
|
||||
last = None
|
||||
while True:
|
||||
for path in Path("/sys/class/power_supply").glob("*"):
|
||||
if (path / "type").read_text().strip() != "Battery" or not (path / "capacity").exists():
|
||||
continue
|
||||
capacity = int((path / "capacity").read_text())
|
||||
discharging = (path / "status").read_text().strip() == "Discharging"
|
||||
level = "critical" if capacity <= 10 else "low" if capacity <= 20 else None
|
||||
if discharging and level and level != last:
|
||||
run("notify-send", "-u", "critical", "Battery " + level, f"{capacity}% remaining — connect power")
|
||||
last = level if discharging else None
|
||||
time.sleep(60)
|
||||
|
||||
|
||||
def health():
|
||||
print("WORKSTATION HEALTH\n")
|
||||
for label, path in [("Running", "/run/current-system"), ("Booted", "/run/booted-system"),
|
||||
("Selected for boot", "/nix/var/nix/profiles/system")]:
|
||||
print(f"{label}: {Path(path).resolve()}")
|
||||
if Path("/run/booted-system").resolve() != Path("/nix/var/nix/profiles/system").resolve():
|
||||
print("\nA different generation is selected for the next boot. No automatic reboot.")
|
||||
run("systemctl", "show", "nixos-update.service", "-p", "ActiveState", "-p", "Result", "-p", "ExecMainStatus")
|
||||
run("systemctl", "list-timers", "nixos-update.timer", "--no-pager")
|
||||
run("systemctl", "--failed", "--no-pager")
|
||||
run("systemctl", "--user", "--failed", "--no-pager")
|
||||
print("\nRecent update journal (not a package-availability count):", flush=True)
|
||||
run("journalctl", "-u", "nixos-update.service", "-n", "30", "--no-pager", check=False)
|
||||
|
||||
|
||||
def status():
|
||||
while True:
|
||||
active = recording()
|
||||
print(json.dumps({"text": "REC" if active else "", "alt": "recording" if active else "idle"}), flush=True)
|
||||
time.sleep(2)
|
||||
|
||||
|
||||
def menu():
|
||||
entries = [
|
||||
("Applications / calculator", ["anyrun"]),
|
||||
("Help · all shortcuts", ["desktop-help"]),
|
||||
("Windows · overview", ["desktop", "windows"]),
|
||||
("Notifications", ["desktop", "notifications"]),
|
||||
("Do not disturb · toggle", ["desktop", "dnd"]),
|
||||
("Clipboard history", ["desktop", "clipboard"]),
|
||||
("Clipboard · delete an item", ["desktop", "clipboard-delete"]),
|
||||
("Clipboard · clear", ["desktop", "clipboard-clear"]),
|
||||
("Clipboard · pause/resume", ["desktop", "clipboard-pause"]),
|
||||
("Screenshot", ["desktop", "screenshot-menu"]),
|
||||
("Recording · start/stop", ["desktop", "record"]),
|
||||
("Color picker · copy HEX", ["hyprpicker", "--autocopy"]),
|
||||
("Emoji · copy", ["desktop", "emoji"]),
|
||||
("Search web", ["desktop", "search"]),
|
||||
("Audio · outputs and per-app volume", ["pavucontrol"]),
|
||||
("Microphone · input devices", ["pavucontrol", "-t", "4"]),
|
||||
("Network / VPN connections", ["nm-connection-editor"]),
|
||||
("Bluetooth devices", ["blueman-manager"]),
|
||||
("Airplane mode · toggle", ["desktop", "airplane"]),
|
||||
("Touchpad · toggle", ["desktop", "touchpad"]),
|
||||
("Night light · toggle", ["desktop", "night"]),
|
||||
("Display scale · temporary", ["desktop", "displays"]),
|
||||
("Power profile", ["desktop", "power-profile"]),
|
||||
("File manager", ["thunar"]),
|
||||
("System monitor", ["kitty", "-e", "btop"]),
|
||||
("System / update health", ["kitty", "--hold", "-e", "desktop", "health"]),
|
||||
("Session / power", ["desktop", "power"]),
|
||||
]
|
||||
index = pick("Workstation", [entry[0] for entry in entries])
|
||||
if index is not None:
|
||||
run("uwsm", "app", "--", *entries[index][1])
|
||||
|
||||
|
||||
def main():
|
||||
command = sys.argv[1] if len(sys.argv) > 1 else "menu"
|
||||
if command in {"clipboard", "clipboard-store", "clipboard-clear", "clipboard-delete", "clipboard-pause"}:
|
||||
clipboard({"clipboard": "pick"}.get(command, command.removeprefix("clipboard-")))
|
||||
elif command == "screenshot":
|
||||
screenshot(sys.argv[2] if len(sys.argv) > 2 else "area", annotate="--annotate" in sys.argv,
|
||||
delay=10 if "--delay10" in sys.argv else 5 if "--delay5" in sys.argv else 0)
|
||||
elif command == "record-run":
|
||||
record_run(sys.argv[2:])
|
||||
elif command == "health":
|
||||
health()
|
||||
elif command == "osd":
|
||||
osd(*sys.argv[2:4])
|
||||
elif command == "notifications":
|
||||
run("swaync-client", "-t", "-sw")
|
||||
elif command == "dnd":
|
||||
run("swaync-client", "-d", "-sw")
|
||||
elif command == "power-profile":
|
||||
profiles = output("powerprofilesctl", "list").splitlines()
|
||||
names = [p.strip().strip("* ").rstrip(":") for p in profiles if p.rstrip().endswith(":") and p.strip().strip("* ").rstrip(":") in ("balanced", "power-saver", "performance")]
|
||||
index = pick("Power profile", names)
|
||||
if index is not None:
|
||||
run("powerprofilesctl", "set", names[index])
|
||||
elif command in {"menu", "windows", "scratch", "power", "screenshot-menu", "record", "night", "displays", "search", "lock-start", "battery", "status", "emoji", "touchpad", "airplane"}:
|
||||
{"menu": menu, "windows": window_picker, "scratch": scratch, "power": power,
|
||||
"screenshot-menu": screenshot_menu, "record": record, "night": night,
|
||||
"displays": displays, "search": web_search, "lock-start": lock_start,
|
||||
"battery": battery, "status": status, "emoji": emoji,
|
||||
"touchpad": touchpad, "airplane": airplane}[command]()
|
||||
else:
|
||||
raise ValueError("Unknown desktop action: " + command)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
os.umask(0o077)
|
||||
main()
|
||||
except (OSError, ValueError, KeyError, RuntimeError, sp.SubprocessError) as error:
|
||||
print(f"desktop: {error}", file=sys.stderr)
|
||||
if len(sys.argv) < 2 or sys.argv[1] not in ("clipboard-store", "status"):
|
||||
notify("Desktop action failed", str(error))
|
||||
sys.exit(1)
|
||||
@@ -0,0 +1,47 @@
|
||||
"""Read-only shortcut search from Hyprland's running binding registry.
|
||||
|
||||
Descriptions come from native Lua bindings, not a second hand-maintained cheat
|
||||
sheet. Choosing a row does NOT execute its command (in particular power/close).
|
||||
"""
|
||||
import json
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
|
||||
def rows(bindings):
|
||||
modifiers = [(64, "Super"), (4, "Ctrl"), (8, "Alt"), (1, "Shift")]
|
||||
result = []
|
||||
for binding in bindings:
|
||||
description = binding.get("description", "")
|
||||
if not description:
|
||||
continue
|
||||
keys = [name for bit, name in modifiers if binding.get("modmask", 0) & bit]
|
||||
keys.append(binding.get("key") or f"code:{binding.get('keycode', 0)}")
|
||||
submap = binding.get("submap")
|
||||
context = f" [{submap}]" if submap else ""
|
||||
result.append(f"{description}{context} {' + '.join(keys)}")
|
||||
return sorted(set(result), key=str.casefold)
|
||||
|
||||
|
||||
def main():
|
||||
try:
|
||||
data = json.loads(subprocess.check_output(["hyprctl", "-j", "binds"], text=True))
|
||||
entries = rows(data)
|
||||
if "--print" in sys.argv:
|
||||
print("\n".join(entries))
|
||||
return
|
||||
if not entries:
|
||||
raise ValueError("No described shortcuts; reload the managed Hyprland configuration.")
|
||||
result = subprocess.run(
|
||||
["fuzzel", "--dmenu", "--prompt", "Shortcuts ", "--width", "68", "--lines", "12"],
|
||||
input="\n".join(entries), text=True, stdout=subprocess.DEVNULL,
|
||||
)
|
||||
if result.returncode not in (0, 1):
|
||||
raise RuntimeError(f"Shortcut picker exited with {result.returncode}")
|
||||
except (OSError, ValueError, subprocess.CalledProcessError, RuntimeError) as error:
|
||||
print(f"desktop-help: {error}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
+63
-7
@@ -35,7 +35,8 @@ def screenshot(name):
|
||||
|
||||
|
||||
def launch(name, command):
|
||||
user("systemd-run --quiet --user --collect --unit=audit-" + name + " " + command)
|
||||
# Match UWSM's application lifetime: clipboard owners can outlive main PID.
|
||||
user("systemd-run --quiet --user --collect --property=ExitType=cgroup --unit=audit-" + name + " " + command)
|
||||
|
||||
|
||||
machine.start()
|
||||
@@ -45,8 +46,7 @@ try:
|
||||
"runuser -u dev -- env XDG_RUNTIME_DIR=/run/user/1001 "
|
||||
"systemctl --user is-active graphical-session.target", timeout=180
|
||||
)
|
||||
# Mako is D-Bus activated on the first notification, not eagerly started.
|
||||
for unit in ["ashell", "awww", "hypridle", "hyprpolkitagent", "pipewire", "wireplumber"]:
|
||||
for unit in ["ashell", "awww", "hypridle", "hyprpolkitagent", "pipewire", "wireplumber", "swaync", "hyprsunset", "desktop-clipboard", "anyrun"]:
|
||||
machine.wait_until_succeeds(
|
||||
"runuser -u dev -- env XDG_RUNTIME_DIR=/run/user/1001 "
|
||||
"systemctl --user is-active " + unit + ".service", timeout=60
|
||||
@@ -78,7 +78,7 @@ launch("terminal", "kitty --title 'Workspace ready' sh -c " + shlex.quote(
|
||||
"printf '\\n WORKSPACE READY\\n\\n'; "
|
||||
"zsh --version; kitty --version; git --version; printf 'Pi: '; command -v pi; "
|
||||
"printf '\\n Ctrl-R history | Ctrl-T files\\n'; "
|
||||
"printf ' Alt-C directories | Super-Space launcher\\n'; "
|
||||
"printf ' Alt-C directories | Super-D launcher\\n'; "
|
||||
"printf ' Super-Enter terminal | Super-F fullscreen\\n\\n'; exec zsh -i"
|
||||
))
|
||||
launch("monitor", "kitty --title 'System monitor' -e btop")
|
||||
@@ -88,13 +88,46 @@ screenshot("desktop-100")
|
||||
|
||||
launch("launcher", "anyrun")
|
||||
wait_layer("anyrun")
|
||||
machine.sleep(5)
|
||||
machine.sleep(2)
|
||||
screenshot("launcher-empty-100")
|
||||
machine.send_chars("kitty", delay=0.1)
|
||||
machine.sleep(5)
|
||||
screenshot("launcher-100")
|
||||
machine.send_key("esc")
|
||||
|
||||
# Exercise clipboard through the actual picker, not only mocked byte handling.
|
||||
session("desktop clipboard-clear")
|
||||
launch("clipboard-source", "sh -c " + shlex.quote("printf 'clipboard audit marker' | wl-copy --foreground"))
|
||||
machine.wait_until_succeeds(as_user("test -s /run/user/1001/workstation/clipboard.db"), timeout=30)
|
||||
launch("clipboard", "desktop clipboard")
|
||||
wait_layer("launcher")
|
||||
machine.send_key("ret")
|
||||
machine.sleep(1)
|
||||
assert session("wl-paste --no-newline") == "clipboard audit marker"
|
||||
|
||||
# Record only this disposable desktop; no host screen/audio is captured.
|
||||
launch("record", "desktop record")
|
||||
wait_layer("launcher")
|
||||
machine.send_key("down") # current output, no audio
|
||||
machine.send_key("ret")
|
||||
machine.wait_until_succeeds(as_user("systemctl --user is-active desktop-recording.service"))
|
||||
machine.sleep(3)
|
||||
assert session("swaync-client -I -sw").strip() == "true"
|
||||
screenshot("recording-indicator-100")
|
||||
session("desktop record")
|
||||
assert session("swaync-client -I -sw").strip() == "false"
|
||||
video = session('cat "$XDG_RUNTIME_DIR/workstation/recording-file"').strip()
|
||||
probe = json.loads(session("ffprobe -v error -show_streams -of json " + shlex.quote(video)))
|
||||
assert any(stream["codec_type"] == "video" for stream in probe["streams"])
|
||||
assert not any(stream["codec_type"] == "audio" for stream in probe["streams"])
|
||||
|
||||
session("notify-send 'Desktop ready' 'Readable text, working audio and native Wayland services.'")
|
||||
assert '"mako"' in user("busctl --user call org.freedesktop.Notifications /org/freedesktop/Notifications org.freedesktop.Notifications GetServerInformation")
|
||||
assert "sway" in user("busctl --user call org.freedesktop.Notifications /org/freedesktop/Notifications org.freedesktop.Notifications GetServerInformation").lower()
|
||||
assert "Clipboard" in session("desktop-help --print")
|
||||
session("swaync-client -t -sw")
|
||||
machine.sleep(1)
|
||||
screenshot("notification-center-100")
|
||||
session("swaync-client -t -sw")
|
||||
machine.sleep(1)
|
||||
screenshot("notification-100")
|
||||
|
||||
@@ -105,6 +138,26 @@ session("hyprctl eval " + shlex.quote(
|
||||
))
|
||||
machine.sleep(4)
|
||||
assert json.loads(session("hyprctl -j monitors"))[0]["scale"] == 1.5
|
||||
launch("launcher150", "anyrun")
|
||||
wait_layer("anyrun")
|
||||
machine.sleep(2)
|
||||
screenshot("launcher-empty-150")
|
||||
# closeOnClick uses a transparent full-screen catcher; its IPC dimensions are
|
||||
# not the visible palette's bounds. Audit the rendered six-result screenshot.
|
||||
machine.send_chars("e")
|
||||
machine.sleep(1)
|
||||
screenshot("launcher-results-150")
|
||||
session("anyrun close")
|
||||
launch("help150", "desktop-help")
|
||||
wait_layer("launcher")
|
||||
machine.sleep(1)
|
||||
screenshot("help-150")
|
||||
machine.send_key("esc")
|
||||
launch("actions150", "desktop menu")
|
||||
wait_layer("launcher")
|
||||
machine.sleep(1)
|
||||
screenshot("actions-150")
|
||||
machine.send_key("esc")
|
||||
# At 150%, a half-screen btop is below its 80-column minimum: use Super-F.
|
||||
# Closing the launcher can restore focus to either terminal; select btop explicitly.
|
||||
monitor = next(c for c in json.loads(session("hyprctl -j clients")) if c["initialTitle"] == "System monitor")
|
||||
@@ -127,6 +180,7 @@ machine.succeed("pgrep -u dev hyprlock")
|
||||
machine.send_chars("desktop-test")
|
||||
machine.send_key("ret")
|
||||
machine.wait_until_fails("pgrep -u dev hyprlock", timeout=30)
|
||||
assert user('test ! -e /run/user/1001/workstation/locked; test ! -e /run/user/1001/workstation/clipboard.db') == ""
|
||||
|
||||
# Restore scale before inspecting settings and ordinary application windows.
|
||||
session("hyprctl eval " + shlex.quote(
|
||||
@@ -142,7 +196,9 @@ screenshot("audio-controls")
|
||||
# Preferences must not be a read-only Home Manager symlink.
|
||||
user("test -w ~/.config/keepassxc/keepassxc.ini && test ! -L ~/.config/keepassxc/keepassxc.ini")
|
||||
user("grep -q 'UpdateBinaryPath=false' ~/.config/keepassxc/keepassxc.ini")
|
||||
user("grep -q -- '--password-store=gnome-libsecret' \"$(command -v element-desktop)\"")
|
||||
user("grep -q -- '--password-store=gnome-libsecret' \"$(command -v element-desktop-nightly)\"")
|
||||
user("test -f /etc/profiles/per-user/dev/share/applications/element-desktop-nightly.desktop")
|
||||
user("test ! -e /etc/profiles/per-user/dev/share/applications/element-desktop.desktop")
|
||||
user("grep -Eq 'fade_on_empty *= *false' ~/.config/hypr/hyprlock.conf")
|
||||
assert "libapplications.so" in user("cat ~/.config/anyrun/config.ron")
|
||||
session("hyprctl clients")
|
||||
|
||||
+261
-69
@@ -7,9 +7,22 @@
|
||||
|
||||
let
|
||||
c = import ./colors.nix;
|
||||
wallpaper = pkgs.runCommand "quiet-orbit.png" { nativeBuildInputs = [ pkgs.resvg ]; } ''
|
||||
resvg ${./wallpaper.svg} "$out"
|
||||
'';
|
||||
renderColors =
|
||||
text:
|
||||
builtins.replaceStrings (map (name: "@${name}@") (
|
||||
builtins.attrNames c
|
||||
)) (builtins.attrValues c) text;
|
||||
rgb = color: "rgb(${lib.removePrefix "#" color})";
|
||||
wallpaper = import ./wallpaper.nix { inherit pkgs; };
|
||||
help = pkgs.writeShellApplication {
|
||||
name = "desktop-help";
|
||||
runtimeInputs = [
|
||||
pkgs.python3
|
||||
pkgs.hyprland
|
||||
pkgs.fuzzel
|
||||
];
|
||||
text = ''exec python ${./desktop-help.py} "$@"'';
|
||||
};
|
||||
wallpaperInit = pkgs.writeShellApplication {
|
||||
name = "initialize-wallpaper";
|
||||
runtimeInputs = [
|
||||
@@ -28,23 +41,42 @@ let
|
||||
exit 1
|
||||
'';
|
||||
};
|
||||
screenshot = pkgs.writeShellApplication {
|
||||
name = "desktop-screenshot";
|
||||
actions = pkgs.writeShellApplication {
|
||||
name = "desktop";
|
||||
runtimeInputs = with pkgs; [
|
||||
python3
|
||||
hyprland
|
||||
uwsm
|
||||
systemd
|
||||
fuzzel
|
||||
libnotify
|
||||
cliphist
|
||||
wl-clipboard
|
||||
grim
|
||||
slurp
|
||||
satty
|
||||
wl-clipboard
|
||||
coreutils
|
||||
wf-recorder
|
||||
wireplumber
|
||||
pulseaudio
|
||||
brightnessctl
|
||||
power-profiles-daemon
|
||||
xdg-user-dirs
|
||||
xdg-utils
|
||||
swaynotificationcenter
|
||||
hyprlock
|
||||
networkmanager
|
||||
util-linux
|
||||
];
|
||||
text = ''
|
||||
geometry=$(slurp) || exit 0
|
||||
[ -n "$geometry" ] || exit 0
|
||||
mkdir -p "$HOME/Pictures/Screenshots"
|
||||
grim -g "$geometry" - | satty --filename - --copy-command wl-copy \
|
||||
--output-filename "$HOME/Pictures/Screenshots/$(date +%Y-%m-%d_%H-%M-%S).png"
|
||||
export DESKTOP_EMOJI_FILE=${pkgs.unicode-emoji.emoji-test}/share/unicode/emoji/emoji-test.txt
|
||||
exec python ${./desktop-actions.py} "$@"
|
||||
'';
|
||||
};
|
||||
screenshot = pkgs.writeShellApplication {
|
||||
name = "desktop-screenshot";
|
||||
runtimeInputs = [ actions ];
|
||||
text = ''exec desktop screenshot area --annotate "$@"'';
|
||||
};
|
||||
launcherExec = pkgs.writeShellScript "anyrun-uwsm" ''
|
||||
kind=$1
|
||||
shift
|
||||
@@ -93,6 +125,10 @@ in
|
||||
slurp
|
||||
satty
|
||||
screenshot
|
||||
actions
|
||||
help
|
||||
hyprpicker
|
||||
wf-recorder
|
||||
pavucontrol
|
||||
playerctl
|
||||
brightnessctl
|
||||
@@ -155,7 +191,8 @@ in
|
||||
dconf.settings."org/gnome/desktop/interface" = {
|
||||
color-scheme = "prefer-dark";
|
||||
font-name = "Inter 11";
|
||||
monospace-font-name = "JetBrainsMono Nerd Font 13";
|
||||
monospace-font-name = "JetBrainsMono Nerd Font 12";
|
||||
accent-color = "yellow";
|
||||
};
|
||||
|
||||
wayland.windowManager.hyprland = {
|
||||
@@ -164,7 +201,7 @@ in
|
||||
portalPackage = null;
|
||||
systemd.enable = false; # UWSM owns the session and environment.
|
||||
configType = "lua";
|
||||
extraConfig = builtins.readFile ./hyprland.lua;
|
||||
extraConfig = renderColors (builtins.readFile ./hyprland.lua);
|
||||
};
|
||||
services.hyprpolkitagent.enable = true;
|
||||
services.awww.enable = true;
|
||||
@@ -180,21 +217,25 @@ in
|
||||
settings = {
|
||||
log_level = "warn";
|
||||
position = "Top";
|
||||
layer = "Top";
|
||||
modules = {
|
||||
left = [
|
||||
[
|
||||
"appLauncher"
|
||||
"Workspaces"
|
||||
"desktopHelp"
|
||||
]
|
||||
"WindowTitle"
|
||||
];
|
||||
center = [ "Tempo" ];
|
||||
right = [
|
||||
"recording"
|
||||
"MediaPlayer"
|
||||
"SystemInfo"
|
||||
[
|
||||
"Tray"
|
||||
"Privacy"
|
||||
"Settings"
|
||||
]
|
||||
"notifications"
|
||||
"Tray"
|
||||
"Privacy"
|
||||
"Settings"
|
||||
];
|
||||
};
|
||||
CustomModule = [
|
||||
@@ -203,22 +244,74 @@ in
|
||||
icon = "";
|
||||
command = "uwsm app -- anyrun";
|
||||
}
|
||||
{
|
||||
name = "desktopHelp";
|
||||
icon = "";
|
||||
command = "uwsm app -- desktop-help";
|
||||
}
|
||||
{
|
||||
name = "notifications";
|
||||
icon = "";
|
||||
command = "uwsm app -- desktop notifications";
|
||||
listen_cmd = "swaync-client -swb";
|
||||
icons."dnd.*" = "";
|
||||
alert = "notification";
|
||||
}
|
||||
{
|
||||
name = "recording";
|
||||
command = "uwsm app -- desktop record";
|
||||
listen_cmd = "desktop status";
|
||||
alert = "recording";
|
||||
}
|
||||
];
|
||||
tempo.clock_format = "%a %d %b %H:%M";
|
||||
workspaces = {
|
||||
visibility_mode = "MonitorSpecific";
|
||||
enable_workspace_filling = true;
|
||||
enable_esc_key = true;
|
||||
media_player.max_title_length = 18;
|
||||
window_title = {
|
||||
mode = "Title";
|
||||
truncate_title_after_length = 26;
|
||||
};
|
||||
system_info = {
|
||||
indicators = [
|
||||
"Cpu"
|
||||
"Memory"
|
||||
"Temperature"
|
||||
];
|
||||
interval = 5;
|
||||
};
|
||||
tempo.clock_format = "%a %d %b %H:%M";
|
||||
workspaces = {
|
||||
visibility_mode = "MonitorSpecific";
|
||||
enable_workspace_filling = false;
|
||||
workspace_names = [
|
||||
"01"
|
||||
"02"
|
||||
"03"
|
||||
"04"
|
||||
"05"
|
||||
"06"
|
||||
"07"
|
||||
"08"
|
||||
"09"
|
||||
"10"
|
||||
];
|
||||
};
|
||||
settings = {
|
||||
lock_cmd = "loginctl lock-session";
|
||||
logout_cmd = "uwsm stop";
|
||||
logout_cmd = "uwsm app -- desktop power";
|
||||
shutdown_cmd = "uwsm app -- desktop power";
|
||||
reboot_cmd = "uwsm app -- desktop power";
|
||||
CustomButton = [
|
||||
{
|
||||
name = "Actions";
|
||||
icon = "";
|
||||
command = "uwsm app -- desktop menu";
|
||||
}
|
||||
{
|
||||
name = "Health";
|
||||
icon = "";
|
||||
command = "uwsm app -- kitty --hold -e desktop health";
|
||||
}
|
||||
];
|
||||
audio_sinks_more_cmd = "uwsm app -- pavucontrol -t 3";
|
||||
audio_sources_more_cmd = "uwsm app -- pavucontrol -t 4";
|
||||
wifi_more_cmd = "uwsm app -- nm-connection-editor";
|
||||
@@ -227,26 +320,24 @@ in
|
||||
indicators = [
|
||||
"IdleInhibitor"
|
||||
"Audio"
|
||||
"Microphone"
|
||||
]
|
||||
++ lib.optionals networkManager [
|
||||
"Network"
|
||||
"Vpn"
|
||||
]
|
||||
++ lib.optionals bluetooth [ "Bluetooth" ]
|
||||
++ lib.optionals power [
|
||||
"PowerProfile"
|
||||
"Battery"
|
||||
"Brightness"
|
||||
];
|
||||
++ lib.optionals power [ "Battery" ];
|
||||
audio_indicator_format = "IconAndPercentage";
|
||||
};
|
||||
appearance = {
|
||||
font_name = "Inter";
|
||||
scale_factor = 1.15;
|
||||
style = "Islands";
|
||||
style = "Solid";
|
||||
opacity = 1.0;
|
||||
primary_color = c.blue;
|
||||
primary_color = {
|
||||
base = c.accent;
|
||||
text = c.background;
|
||||
};
|
||||
text_color = c.text;
|
||||
success_color = c.green;
|
||||
danger_color = c.red;
|
||||
@@ -256,11 +347,7 @@ in
|
||||
strong = c.raised;
|
||||
};
|
||||
secondary_color.base = c.surface;
|
||||
workspace_colors = [
|
||||
c.blue
|
||||
c.purple
|
||||
c.cyan
|
||||
];
|
||||
workspace_colors = [ c.accent ];
|
||||
};
|
||||
};
|
||||
};
|
||||
@@ -273,45 +360,149 @@ in
|
||||
"${pkgs.anyrun}/lib/libapplications.so"
|
||||
"${pkgs.anyrun}/lib/librink.so"
|
||||
];
|
||||
width.fraction = 0.42;
|
||||
y.fraction = 0.2;
|
||||
width.fraction = 0.5;
|
||||
y.fraction = 0.12;
|
||||
hidePluginInfo = true;
|
||||
closeOnClick = true;
|
||||
maxEntries = 8;
|
||||
showResultsImmediately = true;
|
||||
maxEntries = 6;
|
||||
};
|
||||
extraCss = builtins.readFile ./anyrun.css;
|
||||
extraCss = renderColors (builtins.readFile ./anyrun.css);
|
||||
extraConfigFiles."applications.ron".text = ''
|
||||
(
|
||||
desktop_actions: false,
|
||||
max_entries: 8,
|
||||
hide_description: true,
|
||||
max_entries: 6,
|
||||
terminal: Some((command: "uwsm", args: "app -- kitty -e {}")),
|
||||
preprocess_exec_script: Some("${launcherExec}"),
|
||||
)
|
||||
'';
|
||||
};
|
||||
|
||||
services.mako = {
|
||||
# This pinned launcher needs a daemon for calculator copy-to-clipboard.
|
||||
systemd.user.services.anyrun = {
|
||||
Unit = {
|
||||
Description = "Application launcher and calculator";
|
||||
After = [ "graphical-session.target" ];
|
||||
PartOf = [ "graphical-session.target" ];
|
||||
ConditionEnvironment = "WAYLAND_DISPLAY";
|
||||
};
|
||||
Service = {
|
||||
ExecStart = "${pkgs.anyrun}/bin/anyrun daemon";
|
||||
Restart = "on-failure";
|
||||
};
|
||||
Install.WantedBy = [ "graphical-session.target" ];
|
||||
};
|
||||
|
||||
# Fuzzel is the shared dmenu-style picker for actions/help; Anyrun remains
|
||||
# the app launcher/calculator, not a second competing autostarted shell.
|
||||
programs.fuzzel = {
|
||||
enable = true;
|
||||
settings = {
|
||||
font = "Inter 11";
|
||||
width = 360;
|
||||
height = 160;
|
||||
margin = "12";
|
||||
padding = "14";
|
||||
border-size = 2;
|
||||
border-radius = 12;
|
||||
background-color = c.background;
|
||||
text-color = c.text;
|
||||
border-color = c.border;
|
||||
default-timeout = 6000;
|
||||
max-visible = 4;
|
||||
max-icon-size = 48;
|
||||
"urgency=critical" = {
|
||||
border-color = c.red;
|
||||
default-timeout = 0;
|
||||
main = {
|
||||
font = "Inter:size=11";
|
||||
terminal = "kitty";
|
||||
"launch-prefix" = "uwsm app --";
|
||||
"line-height" = 22;
|
||||
width = 46;
|
||||
lines = 10;
|
||||
"horizontal-pad" = 18;
|
||||
"vertical-pad" = 14;
|
||||
};
|
||||
colors = {
|
||||
background = "${lib.removePrefix "#" c.background}ff";
|
||||
text = "${lib.removePrefix "#" c.text}ff";
|
||||
match = "${lib.removePrefix "#" c.accent}ff";
|
||||
selection = "${lib.removePrefix "#" c.selection}ff";
|
||||
"selection-text" = "${lib.removePrefix "#" c.text}ff";
|
||||
"selection-match" = "${lib.removePrefix "#" c.accent}ff";
|
||||
border = "${lib.removePrefix "#" c.border}ff";
|
||||
prompt = "${lib.removePrefix "#" c.muted}ff";
|
||||
};
|
||||
border = {
|
||||
width = 2;
|
||||
radius = 6;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
services.swaync = {
|
||||
enable = true;
|
||||
style = renderColors (builtins.readFile ./swaync.css);
|
||||
settings = {
|
||||
positionX = "right";
|
||||
positionY = "top";
|
||||
control-center-width = 340;
|
||||
control-center-height = 500;
|
||||
control-center-margin-top = 8;
|
||||
control-center-margin-right = 8;
|
||||
notification-window-width = 360;
|
||||
fit-to-screen = false;
|
||||
timeout = 6;
|
||||
timeout-critical = 0;
|
||||
keyboard-shortcuts = true;
|
||||
hide-on-action = true;
|
||||
widgets = [
|
||||
"title"
|
||||
"dnd"
|
||||
"mpris"
|
||||
"notifications"
|
||||
];
|
||||
widget-config = {
|
||||
title = {
|
||||
text = "Notifications";
|
||||
clear-all-button = true;
|
||||
button-text = "Clear";
|
||||
};
|
||||
dnd.text = "Do not disturb";
|
||||
mpris = {
|
||||
image-size = 64;
|
||||
image-radius = 4;
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
services.hyprsunset = {
|
||||
enable = true;
|
||||
settings.profile = [
|
||||
{
|
||||
time = "07:00";
|
||||
identity = true;
|
||||
}
|
||||
{
|
||||
time = "21:00";
|
||||
temperature = 4200;
|
||||
}
|
||||
];
|
||||
};
|
||||
systemd.user.services.desktop-clipboard = {
|
||||
Unit = {
|
||||
Description = "Session-local clipboard history";
|
||||
After = [ "graphical-session.target" ];
|
||||
PartOf = [ "graphical-session.target" ];
|
||||
ConditionEnvironment = "WAYLAND_DISPLAY";
|
||||
};
|
||||
Service = {
|
||||
ExecStart = "${pkgs.wl-clipboard}/bin/wl-paste --watch ${actions}/bin/desktop clipboard-store";
|
||||
ExecStopPost = "${actions}/bin/desktop clipboard-clear";
|
||||
Restart = "on-failure";
|
||||
UMask = "0077";
|
||||
};
|
||||
Install.WantedBy = [ "graphical-session.target" ];
|
||||
};
|
||||
systemd.user.services.desktop-battery = lib.mkIf power {
|
||||
Unit = {
|
||||
Description = "Low/critical battery notifications";
|
||||
After = [ "graphical-session.target" ];
|
||||
PartOf = [ "graphical-session.target" ];
|
||||
ConditionPathExistsGlob = "/sys/class/power_supply/BAT*";
|
||||
};
|
||||
Service = {
|
||||
ExecStart = "${actions}/bin/desktop battery";
|
||||
Restart = "on-failure";
|
||||
};
|
||||
Install.WantedBy = [ "graphical-session.target" ];
|
||||
};
|
||||
programs.hyprlock = {
|
||||
enable = true;
|
||||
package = null;
|
||||
@@ -331,9 +522,9 @@ in
|
||||
{
|
||||
monitor = "";
|
||||
text = "cmd[update:1000] date +'%H:%M'";
|
||||
font_size = 84;
|
||||
font_size = 64;
|
||||
font_family = "Inter";
|
||||
color = "rgb(f2f4f8)";
|
||||
color = rgb c.text;
|
||||
position = "0,100";
|
||||
halign = "center";
|
||||
valign = "center";
|
||||
@@ -343,7 +534,7 @@ in
|
||||
text = "$USER";
|
||||
font_size = 18;
|
||||
font_family = "Inter";
|
||||
color = "rgb(a2a9b0)";
|
||||
color = rgb c.muted;
|
||||
position = "0,0";
|
||||
halign = "center";
|
||||
valign = "center";
|
||||
@@ -360,12 +551,13 @@ in
|
||||
dots_center = true;
|
||||
fade_on_empty = false;
|
||||
font_family = "Inter";
|
||||
inner_color = "rgb(262626)";
|
||||
outer_color = "rgb(78a9ff)";
|
||||
font_color = "rgb(f2f4f8)";
|
||||
check_color = "rgb(3ddbd9)";
|
||||
fail_color = "rgb(ff6b7a)";
|
||||
capslock_color = "rgb(f1c21b)";
|
||||
rounding = 6;
|
||||
inner_color = rgb c.surface;
|
||||
outer_color = rgb c.accent;
|
||||
font_color = rgb c.text;
|
||||
check_color = rgb c.green;
|
||||
fail_color = rgb c.red;
|
||||
capslock_color = rgb c.yellow;
|
||||
placeholder_text = "<i>Password</i>";
|
||||
fail_text = "<i>Try again ($ATTEMPTS)</i>";
|
||||
}
|
||||
@@ -377,7 +569,7 @@ in
|
||||
package = null; # NixOS owns the service and PAM integration.
|
||||
settings = {
|
||||
general = {
|
||||
lock_cmd = "pidof hyprlock || hyprlock";
|
||||
lock_cmd = "${actions}/bin/desktop lock-start";
|
||||
before_sleep_cmd = "loginctl lock-session";
|
||||
after_sleep_cmd = "hyprctl dispatch 'hl.dsp.dpms({ action = \"enable\" })'";
|
||||
inhibit_sleep = 3;
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
# Official Nightly binary, pinned like the other external application sources.
|
||||
# Keep its matching Electron/native modules together; do not disable the sandbox.
|
||||
{ pkgs }:
|
||||
pkgs.stdenv.mkDerivation {
|
||||
pname = "element-nightly";
|
||||
version = "2026090401";
|
||||
src = pkgs.fetchurl {
|
||||
url = "https://packages.element.io/debian/pool/main/e/element-nightly/element-nightly_2026090401_amd64.deb";
|
||||
hash = "sha256-lLIkH6MtJWhGgohDFBmdOPEI/YdLTAJ9mkgr+TgSPLc=";
|
||||
};
|
||||
nativeBuildInputs = with pkgs; [
|
||||
dpkg
|
||||
autoPatchelfHook
|
||||
wrapGAppsHook3
|
||||
makeWrapper
|
||||
];
|
||||
buildInputs = with pkgs; [
|
||||
alsa-lib
|
||||
at-spi2-atk
|
||||
at-spi2-core
|
||||
cairo
|
||||
cups
|
||||
dbus
|
||||
expat
|
||||
glib
|
||||
gtk3
|
||||
libgbm
|
||||
libdrm
|
||||
libxkbcommon
|
||||
libx11
|
||||
libxcb
|
||||
libxcomposite
|
||||
libxdamage
|
||||
libxext
|
||||
libxfixes
|
||||
libxrandr
|
||||
libxrender
|
||||
libxtst
|
||||
libxscrnsaver
|
||||
libxshmfence
|
||||
nss
|
||||
nspr
|
||||
pango
|
||||
libsecret
|
||||
stdenv.cc.cc
|
||||
];
|
||||
runtimeDependencies = with pkgs; [
|
||||
libnotify
|
||||
libGL
|
||||
libsecret
|
||||
libpulseaudio
|
||||
systemd
|
||||
];
|
||||
dontUnpack = true;
|
||||
dontBuild = true;
|
||||
dontWrapGApps = true;
|
||||
installPhase = ''
|
||||
runHook preInstall
|
||||
dpkg --fsys-tarfile "$src" | tar --extract --no-same-owner
|
||||
mkdir -p "$out/lib" "$out/bin"
|
||||
mv opt/Element-Nightly "$out/lib/element-nightly"
|
||||
mv usr/share "$out/share"
|
||||
substituteInPlace "$out/share/applications/element-desktop-nightly.desktop" \
|
||||
--replace-fail /opt/Element-Nightly/element-desktop-nightly "$out/bin/element-desktop-nightly"
|
||||
runHook postInstall
|
||||
'';
|
||||
preFixup = ''
|
||||
makeWrapper "$out/lib/element-nightly/element-desktop-nightly" "$out/bin/element-desktop-nightly" \
|
||||
"''${gappsWrapperArgs[@]}" \
|
||||
--suffix PATH : ${pkgs.lib.makeBinPath [ pkgs.xdg-utils ]} \
|
||||
--add-flags "--password-store=gnome-libsecret --disable-setuid-sandbox" \
|
||||
--add-flags "\''${NIXOS_OZONE_WL:+\''${WAYLAND_DISPLAY:+--ozone-platform=wayland}}"
|
||||
# Compatibility command only, not a duplicate launcher or a second install.
|
||||
ln -s element-desktop-nightly "$out/bin/element-desktop"
|
||||
'';
|
||||
meta = {
|
||||
description = "Element Nightly, the upstream development build of the Matrix client";
|
||||
homepage = "https://element.io/download";
|
||||
license = pkgs.lib.licenses.agpl3Plus;
|
||||
platforms = [ "x86_64-linux" ];
|
||||
mainProgram = "element-desktop-nightly";
|
||||
};
|
||||
}
|
||||
Generated
+9
-9
@@ -7,11 +7,11 @@
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1787377438,
|
||||
"narHash": "sha256-Sxu1NLTD/Ern6hFGLlZmtKCSct3YQXZI/lls8RE1XeM=",
|
||||
"lastModified": 1788642154,
|
||||
"narHash": "sha256-sPpQFVaFTDqO/4vvCAhuAhqTgqN/ygu+9eJcs5eB0js=",
|
||||
"owner": "nix-community",
|
||||
"repo": "home-manager",
|
||||
"rev": "65258d5c65a250189fde2e35f490d15e064c4c62",
|
||||
"rev": "fd0956c99c41ae3c13a73a638f1f7e963aebc4ab",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -41,11 +41,11 @@
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1788405554,
|
||||
"narHash": "sha256-r2f1oUwixlgq9zOdYLqJLfS/lWBT60/IITjhTKI59JU=",
|
||||
"lastModified": 1788584326,
|
||||
"narHash": "sha256-Fd3OB8J9JhgliQwOKcqx4M672CInxi1I5VnwsaXeSQo=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "a5cc6f2c37bf518436dc8d1c288ccd0c43c2f4c4",
|
||||
"rev": "6713828a351efa628b025a1adf7f43cbf8597513",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -57,11 +57,11 @@
|
||||
},
|
||||
"nixpkgs-latest": {
|
||||
"locked": {
|
||||
"lastModified": 1788659161,
|
||||
"narHash": "sha256-rIzUUvgwhfJeWwm7y1ZF+xRo2Gep+QYkxpj9mJbG6cM=",
|
||||
"lastModified": 1788664981,
|
||||
"narHash": "sha256-7XcoeUSm9v1zn9NHNYNItxIbCUGBTBlTClQDG3qrLQk=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "de0ba0a81b0f6eed798a97fdec6f03a754b9d966",
|
||||
"rev": "94aa2762715b736cea911d96d6a2f3b448f0b296",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
];
|
||||
};
|
||||
|
||||
# Physical host: adds dev/Hyprland without removing kbot/Plasma.
|
||||
# Physical laptop: dev + managed Hyprland; preserve its boot/storage.
|
||||
nixosConfigurations.nixos = nixpkgs.lib.nixosSystem {
|
||||
specialArgs = { inherit inputs; };
|
||||
modules = [
|
||||
@@ -41,6 +41,9 @@
|
||||
};
|
||||
|
||||
formatter.x86_64-linux = nixpkgs.legacyPackages.x86_64-linux.nixfmt;
|
||||
packages.x86_64-linux.element-nightly = import ./element-nightly.nix {
|
||||
pkgs = nixpkgs.legacyPackages.x86_64-linux;
|
||||
};
|
||||
|
||||
nixosModules = {
|
||||
desktop = ./desktop.nix;
|
||||
@@ -56,16 +59,28 @@
|
||||
in
|
||||
{
|
||||
desktop = import ./desktop-test.nix { inherit inputs pkgs; };
|
||||
tools = import ./tools-test.nix {
|
||||
inherit pkgs;
|
||||
config = inputs.self.nixosConfigurations.nixos.config;
|
||||
};
|
||||
physical-config = import ./physical-test.nix {
|
||||
inherit pkgs;
|
||||
config = inputs.self.nixosConfigurations.nixos.config;
|
||||
};
|
||||
desktop-actions =
|
||||
pkgs.runCommand "desktop-actions-check" { nativeBuildInputs = [ pkgs.python3 ]; }
|
||||
''
|
||||
python ${./desktop-actions-test.py} ${./desktop-actions.py}
|
||||
touch "$out"
|
||||
'';
|
||||
desktop-config =
|
||||
pkgs.runCommand "hyprland-config-check" { nativeBuildInputs = [ pkgs.hyprland ]; }
|
||||
''
|
||||
export HOME="$TMPDIR/home" XDG_RUNTIME_DIR="$TMPDIR/runtime"
|
||||
mkdir -m 700 -p "$HOME" "$XDG_RUNTIME_DIR"
|
||||
Hyprland --verify-config -c ${./hyprland.lua}
|
||||
Hyprland --verify-config -c ${
|
||||
inputs.self.nixosConfigurations.nixos.config.home-manager.users.dev.xdg.configFile."hypr/hyprland.lua".source
|
||||
}
|
||||
touch "$out"
|
||||
'';
|
||||
updates =
|
||||
|
||||
+89
-37
@@ -1,18 +1,19 @@
|
||||
-- Native Hyprland 0.55 configuration, deployed by Home Manager.
|
||||
-- @color@ tokens are rendered from colors.nix at build time.
|
||||
-- Services belong to UWSM/systemd, never a second exec-once process tree.
|
||||
local function rgb(hex) return "rgb(" .. hex:sub(2) .. ")" end
|
||||
hl.monitor({ output = "", mode = "preferred", position = "auto", scale = "auto" })
|
||||
|
||||
hl.config({
|
||||
general = {
|
||||
gaps_in = 6, gaps_out = 12, border_size = 2,
|
||||
gaps_in = 4, gaps_out = 8, border_size = 2,
|
||||
layout = "dwindle", resize_on_border = true, allow_tearing = false,
|
||||
col = {
|
||||
active_border = { colors = { "rgb(78a9ff)", "rgb(be95ff)" }, angle = 45 },
|
||||
inactive_border = "rgb(393939)",
|
||||
active_border = { colors = { rgb("@accent@"), rgb("@green@") }, angle = 45 },
|
||||
inactive_border = rgb("@border@"),
|
||||
},
|
||||
},
|
||||
decoration = {
|
||||
rounding = 12,
|
||||
rounding = 6,
|
||||
active_opacity = 1.0, inactive_opacity = 1.0,
|
||||
shadow = { enabled = true, range = 16, render_power = 3, color = 0x55000000 },
|
||||
blur = { enabled = false },
|
||||
@@ -38,47 +39,98 @@ hl.animation({ leaf = "workspaces", enabled = true, speed = 2.5, bezier = "settl
|
||||
hl.animation({ leaf = "layers", enabled = true, speed = 2, bezier = "settle", style = "fade" })
|
||||
hl.gesture({ fingers = 3, direction = "horizontal", action = "workspace" })
|
||||
|
||||
-- Every binding is discoverable through hyprctl -j binds / desktop-help.
|
||||
local function bind(keys, action, description, options)
|
||||
options = options or {}
|
||||
options.description = description
|
||||
hl.bind(keys, action, options)
|
||||
end
|
||||
local function app(command) return hl.dsp.exec_cmd("uwsm app -- " .. command) end
|
||||
hl.bind("SUPER + Return", app("kitty"))
|
||||
hl.bind("SUPER + Space", app("anyrun"))
|
||||
hl.bind("SUPER + E", app("kitty --class files -e yazi"))
|
||||
hl.bind("SUPER + B", app("firefox-esr"))
|
||||
hl.bind("SUPER + P", app("keepassxc"))
|
||||
hl.bind("SUPER + Escape", hl.dsp.exec_cmd("loginctl lock-session"))
|
||||
hl.bind("SUPER + Q", hl.dsp.window.close())
|
||||
hl.bind("SUPER + V", hl.dsp.window.float({ action = "toggle" }))
|
||||
hl.bind("SUPER + F", hl.dsp.window.fullscreen())
|
||||
hl.bind("SUPER + J", hl.dsp.layout("togglesplit"))
|
||||
hl.bind("SUPER + N", hl.dsp.exec_cmd("makoctl dismiss"))
|
||||
hl.bind("SUPER + SHIFT + N", hl.dsp.exec_cmd("makoctl restore"))
|
||||
hl.bind("Print", hl.dsp.exec_cmd("desktop-screenshot"))
|
||||
bind("SUPER + Return", app("kitty"), "Apps · Terminal")
|
||||
bind("SUPER + D", app("anyrun"), "Apps · Search applications / calculator")
|
||||
bind("SUPER + A", app("desktop windows"), "Windows · Overview / search")
|
||||
bind("SUPER + CTRL + S", app("desktop windows"), "Windows · Search all windows")
|
||||
bind("SUPER + E", app("thunar"), "Apps · Graphical file manager")
|
||||
bind("SUPER + CTRL + E", app("kitty --class files -e yazi"), "Apps · Yazi terminal file manager")
|
||||
bind("SUPER + B", app("firefox-esr"), "Apps · Browser")
|
||||
bind("SUPER + ALT + P", app("keepassxc"), "Apps · Password vault")
|
||||
bind("SUPER + P", hl.dsp.window.pseudo(), "Windows · Toggle pseudotiling")
|
||||
bind("SUPER + H", app("desktop-help"), "Help · Search all keyboard shortcuts")
|
||||
bind("SUPER + SHIFT + K", app("desktop-help"), "Help · Search all keyboard shortcuts")
|
||||
bind("CTRL + ALT + L", hl.dsp.exec_cmd("loginctl lock-session"), "Session · Lock screen")
|
||||
bind("SUPER + Escape", hl.dsp.exec_cmd("loginctl lock-session"), "Session · Lock screen (alias)")
|
||||
bind("CTRL + ALT + P", app("desktop power"), "Session · Power / logout menu")
|
||||
bind("SUPER + SHIFT + E", app("desktop menu"), "Desktop · Quick settings / actions")
|
||||
bind("SUPER + ALT + V", app("desktop clipboard"), "Clipboard · History")
|
||||
bind("SUPER + ALT + E", app("desktop emoji"), "Clipboard · Emoji picker")
|
||||
bind("SUPER + ALT + C", app("anyrun"), "Apps · Calculator (enter an expression)")
|
||||
bind("SUPER + S", app("desktop search"), "Apps · Search web")
|
||||
bind("SUPER + SHIFT + Return", app("desktop scratch"), "Windows · Drop-down terminal")
|
||||
bind("SUPER + ALT + R", app("desktop record"), "Capture · Start / stop recording")
|
||||
bind("SUPER + ALT + T", app("desktop touchpad"), "Input · Toggle touchpad")
|
||||
bind("SUPER + Q", hl.dsp.window.close(), "Windows · Close active window")
|
||||
bind("SUPER + Space", hl.dsp.window.float({ action = "toggle" }), "Windows · Toggle floating")
|
||||
bind("SUPER + F", hl.dsp.window.fullscreen(), "Windows · Toggle fullscreen (alias)")
|
||||
bind("SUPER + SHIFT + F", hl.dsp.window.fullscreen(), "Windows · Toggle fullscreen")
|
||||
bind("SUPER + CTRL + F", hl.dsp.window.fullscreen({mode = "maximized"}), "Windows · Toggle maximize")
|
||||
bind("SUPER + SHIFT + I", hl.dsp.layout("togglesplit"), "Windows · Toggle split direction")
|
||||
bind("SUPER + N", app("desktop night"), "Display · Toggle night light")
|
||||
bind("SUPER + SHIFT + N", app("desktop notifications"), "Notifications · Open history / controls")
|
||||
bind("SUPER + CTRL + N", app("desktop dnd"), "Notifications · Toggle do not disturb")
|
||||
bind("ALT + Tab", hl.dsp.window.cycle_next(), "Windows · Cycle forward")
|
||||
bind("ALT + SHIFT + Tab", hl.dsp.window.cycle_next({next = false}), "Windows · Cycle backward")
|
||||
bind("SUPER + G", hl.dsp.group.toggle(), "Windows · Toggle tabbed group")
|
||||
bind("SUPER + CTRL + Tab", hl.dsp.group.next(), "Windows · Next group tab")
|
||||
bind("Print", app("desktop screenshot-menu"), "Capture · Screenshot menu")
|
||||
bind("SUPER + Print", app("desktop screenshot output"), "Capture · Current display")
|
||||
bind("SUPER + SHIFT + Print", app("desktop screenshot area"), "Capture · Region")
|
||||
bind("SUPER + CTRL + Print", app("desktop screenshot output --delay5"), "Capture · Display after 5 seconds")
|
||||
bind("SUPER + CTRL + SHIFT + Print", app("desktop screenshot output --delay10"), "Capture · Display after 10 seconds")
|
||||
bind("ALT + Print", app("desktop screenshot window"), "Capture · Active window")
|
||||
bind("SUPER + SHIFT + S", app("desktop-screenshot"), "Capture · Region and annotation")
|
||||
|
||||
for _, direction in ipairs({ "left", "right", "up", "down" }) do
|
||||
hl.bind("SUPER + " .. direction, hl.dsp.focus({ direction = direction }))
|
||||
hl.bind("SUPER + SHIFT + " .. direction, hl.dsp.window.move({ direction = direction }))
|
||||
bind("SUPER + " .. direction, hl.dsp.focus({ direction = direction }), "Windows · Focus " .. direction)
|
||||
bind("SUPER + CTRL + " .. direction, hl.dsp.window.move({ direction = direction }), "Windows · Move " .. direction)
|
||||
bind("SUPER + ALT + " .. direction, hl.dsp.window.swap({ direction = direction }), "Windows · Swap " .. direction)
|
||||
local dx = direction == "left" and -50 or direction == "right" and 50 or 0
|
||||
local dy = direction == "up" and -50 or direction == "down" and 50 or 0
|
||||
bind("SUPER + SHIFT + " .. direction, hl.dsp.window.resize({ x = dx, y = dy, relative = true }), "Windows · Resize " .. direction, {repeating = true})
|
||||
end
|
||||
for i = 1, 10 do
|
||||
local key = i % 10
|
||||
hl.bind("SUPER + " .. key, hl.dsp.focus({ workspace = i }))
|
||||
hl.bind("SUPER + SHIFT + " .. key, hl.dsp.window.move({ workspace = i }))
|
||||
bind("SUPER + " .. key, hl.dsp.focus({ workspace = i }), "Workspaces · Focus " .. i)
|
||||
bind("SUPER + SHIFT + " .. key, hl.dsp.window.move({ workspace = i, follow = true }), "Workspaces · Move and follow to " .. i)
|
||||
bind("SUPER + CTRL + " .. key, hl.dsp.window.move({ workspace = i, follow = false }), "Workspaces · Move silently to " .. i)
|
||||
end
|
||||
hl.bind("SUPER + S", hl.dsp.workspace.toggle_special("scratch"))
|
||||
hl.bind("SUPER + SHIFT + S", hl.dsp.window.move({ workspace = "special:scratch" }))
|
||||
hl.bind("SUPER + mouse_down", hl.dsp.focus({ workspace = "e+1" }))
|
||||
hl.bind("SUPER + mouse_up", hl.dsp.focus({ workspace = "e-1" }))
|
||||
hl.bind("SUPER + mouse:272", hl.dsp.window.drag(), { mouse = true })
|
||||
hl.bind("SUPER + mouse:273", hl.dsp.window.resize(), { mouse = true })
|
||||
bind("SUPER + U", hl.dsp.workspace.toggle_special("scratch"), "Workspaces · Toggle scratchpad")
|
||||
bind("SUPER + SHIFT + U", hl.dsp.window.move({ workspace = "special:scratch" }), "Workspaces · Move window to scratchpad")
|
||||
bind("SUPER + Tab", hl.dsp.focus({workspace = "m+1"}), "Workspaces · Next on this monitor")
|
||||
bind("SUPER + SHIFT + Tab", hl.dsp.focus({workspace = "m-1"}), "Workspaces · Previous on this monitor")
|
||||
for i, direction in ipairs({"left", "right", "up", "down"}) do
|
||||
bind("SUPER + CTRL + F" .. (8 + i), hl.dsp.workspace.move({monitor = direction}), "Monitors · Move workspace " .. direction)
|
||||
end
|
||||
bind("SUPER + mouse_down", hl.dsp.focus({ workspace = "e+1" }), "Workspaces · Next occupied workspace")
|
||||
bind("SUPER + mouse_up", hl.dsp.focus({ workspace = "e-1" }), "Workspaces · Previous occupied workspace")
|
||||
bind("SUPER + mouse:272", hl.dsp.window.drag(), "Windows · Drag with left mouse", { mouse = true })
|
||||
bind("SUPER + mouse:273", hl.dsp.window.resize(), "Windows · Resize with right mouse", { mouse = true })
|
||||
|
||||
hl.bind("XF86AudioRaiseVolume", hl.dsp.exec_cmd("wpctl set-volume -l 1 @DEFAULT_AUDIO_SINK@ 5%+"), { locked = true, repeating = true })
|
||||
hl.bind("XF86AudioLowerVolume", hl.dsp.exec_cmd("wpctl set-volume @DEFAULT_AUDIO_SINK@ 5%-"), { locked = true, repeating = true })
|
||||
hl.bind("XF86AudioMute", hl.dsp.exec_cmd("wpctl set-mute @DEFAULT_AUDIO_SINK@ toggle"), { locked = true })
|
||||
hl.bind("XF86AudioMicMute", hl.dsp.exec_cmd("wpctl set-mute @DEFAULT_AUDIO_SOURCE@ toggle"), { locked = true })
|
||||
hl.bind("XF86MonBrightnessUp", hl.dsp.exec_cmd("brightnessctl set +5%"), { locked = true, repeating = true })
|
||||
hl.bind("XF86MonBrightnessDown", hl.dsp.exec_cmd("brightnessctl --min-value=1 set 5%-"), { locked = true, repeating = true })
|
||||
hl.bind("XF86AudioPlay", hl.dsp.exec_cmd("playerctl play-pause"), { locked = true })
|
||||
hl.bind("XF86AudioNext", hl.dsp.exec_cmd("playerctl next"), { locked = true })
|
||||
hl.bind("XF86AudioPrev", hl.dsp.exec_cmd("playerctl previous"), { locked = true })
|
||||
bind("XF86AudioRaiseVolume", hl.dsp.exec_cmd("desktop osd volume up"), "Media · Volume up", { locked = true, repeating = true })
|
||||
bind("XF86AudioLowerVolume", hl.dsp.exec_cmd("desktop osd volume down"), "Media · Volume down", { locked = true, repeating = true })
|
||||
bind("XF86AudioMute", hl.dsp.exec_cmd("desktop osd volume mute"), "Media · Toggle speaker mute", { locked = true })
|
||||
bind("XF86AudioMicMute", hl.dsp.exec_cmd("desktop osd microphone mute"), "Media · Toggle microphone mute", { locked = true })
|
||||
bind("XF86MonBrightnessUp", hl.dsp.exec_cmd("desktop osd brightness up"), "Display · Brightness up", { locked = true, repeating = true })
|
||||
bind("XF86MonBrightnessDown", hl.dsp.exec_cmd("desktop osd brightness down"), "Display · Brightness down", { locked = true, repeating = true })
|
||||
bind("XF86AudioPlay", hl.dsp.exec_cmd("playerctl play-pause"), "Media · Play / pause", { locked = true })
|
||||
bind("XF86AudioNext", hl.dsp.exec_cmd("playerctl next"), "Media · Next track", { locked = true })
|
||||
bind("XF86AudioPrev", hl.dsp.exec_cmd("playerctl previous"), "Media · Previous track", { locked = true })
|
||||
|
||||
bind("XF86KbdBrightnessUp", hl.dsp.exec_cmd("desktop osd keyboard up"), "Input · Keyboard backlight up", {locked = true, repeating = true})
|
||||
bind("XF86KbdBrightnessDown", hl.dsp.exec_cmd("desktop osd keyboard down"), "Input · Keyboard backlight down", {locked = true, repeating = true})
|
||||
bind("XF86RFKill", app("desktop airplane"), "Network · Toggle airplane mode")
|
||||
bind("XF86Sleep", hl.dsp.exec_cmd("systemctl suspend"), "Session · Suspend")
|
||||
|
||||
hl.window_rule({name = "dropterminal", match = {class = "^dropterminal$"}, workspace = "special:terminal", float = true, center = true})
|
||||
hl.window_rule({ name = "ignore-maximize", match = { class = ".*" }, suppress_event = "maximize" })
|
||||
hl.window_rule({
|
||||
name = "fix-xwayland-drag", match = { class = "^$", title = "^$", xwayland = true, float = true, fullscreen = false, pin = false },
|
||||
|
||||
+21
-4
@@ -1,5 +1,15 @@
|
||||
{ lib, pkgs, ... }:
|
||||
|
||||
{
|
||||
inputs,
|
||||
lib,
|
||||
pkgs,
|
||||
...
|
||||
}:
|
||||
let
|
||||
latest = import inputs.nixpkgs-latest {
|
||||
inherit (pkgs.stdenv.hostPlatform) system;
|
||||
config = pkgs.config;
|
||||
};
|
||||
in
|
||||
{
|
||||
# Keep each host's existing interface/DHCP owner. On EC2 this is dhcpcd;
|
||||
# NixOS wires its resolvconf hook to resolved's compatibility interface.
|
||||
@@ -13,8 +23,11 @@
|
||||
};
|
||||
};
|
||||
|
||||
programs.mtr.enable = true;
|
||||
environment.systemPackages = with pkgs; [
|
||||
programs.mtr = {
|
||||
enable = true;
|
||||
package = latest.mtr;
|
||||
};
|
||||
environment.systemPackages = with latest; [
|
||||
wireguard-tools # wg and wg-quick; no interfaces or credentials are configured.
|
||||
openvpn
|
||||
iperf3
|
||||
@@ -26,6 +39,10 @@
|
||||
ethtool
|
||||
netcat-openbsd
|
||||
socat
|
||||
doggo
|
||||
iftop
|
||||
bandwhich
|
||||
wireshark-cli # tshark; no capture group/capabilities or daemon.
|
||||
];
|
||||
# Client tools only: no VPN services, peers, keys, routes or firewall ports.
|
||||
}
|
||||
|
||||
+19
-18
@@ -2,7 +2,6 @@
|
||||
{ config, pkgs }:
|
||||
let
|
||||
inherit (pkgs) lib;
|
||||
kbot = config.users.users.kbot;
|
||||
dev = config.users.users.dev;
|
||||
btrfsDevice = "/dev/disk/by-uuid/8a16015f-d6f8-4f74-8558-6261b9112216";
|
||||
mounts = {
|
||||
@@ -50,15 +49,11 @@ let
|
||||
{
|
||||
assertion =
|
||||
config.users.mutableUsers
|
||||
&& kbot.isNormalUser
|
||||
&& kbot.uid == 1000
|
||||
&& kbot.home == "/home/kbot"
|
||||
&& builtins.elem "wheel" kbot.extraGroups
|
||||
&& builtins.elem "networkmanager" kbot.extraGroups
|
||||
&& kbot.password == null
|
||||
&& kbot.hashedPassword == null
|
||||
&& kbot.hashedPasswordFile == null;
|
||||
message = "Keep kbot's identity, local password and administrative/network access.";
|
||||
&& !(config.users.users ? kbot)
|
||||
&& dev.password == null
|
||||
&& dev.hashedPassword == null
|
||||
&& dev.hashedPasswordFile == null;
|
||||
message = "Retire kbot without overwriting dev's locally established password.";
|
||||
}
|
||||
{
|
||||
assertion =
|
||||
@@ -68,17 +63,19 @@ let
|
||||
&& builtins.elem "wheel" dev.extraGroups
|
||||
&& builtins.elem "networkmanager" dev.extraGroups
|
||||
&& builtins.attrNames config.home-manager.users == [ "dev" ];
|
||||
message = "Add dev independently; Home Manager must not manage kbot's home.";
|
||||
message = "dev is the sole managed daily account, with local administration/network access.";
|
||||
}
|
||||
{
|
||||
assertion =
|
||||
config.services.displayManager.sddm.enable
|
||||
&& config.services.desktopManager.plasma6.enable
|
||||
&& !config.services.desktopManager.plasma6.enable
|
||||
&& config.programs.hyprland.enable
|
||||
&& config.programs.hyprland.withUWSM
|
||||
&& !config.services.greetd.enable
|
||||
&& !config.services.displayManager.autoLogin.enable;
|
||||
message = "Keep the Plasma/SDDM recovery login alongside Hyprland, without autologin.";
|
||||
&& !config.services.displayManager.autoLogin.enable
|
||||
&& config.services.displayManager.defaultSession == "hyprland-uwsm"
|
||||
&& !(builtins.elem "hyprland" config.services.displayManager.sessionData.sessionNames);
|
||||
message = "Offer only the managed Hyprland desktop in SDDM, without Plasma or autologin.";
|
||||
}
|
||||
{
|
||||
assertion =
|
||||
@@ -91,11 +88,14 @@ let
|
||||
}
|
||||
{
|
||||
assertion =
|
||||
!(config.systemd.services ? nixos-update)
|
||||
&& !(config.systemd.timers ? nixos-update)
|
||||
config.systemd.services.nixos-update.environment.NIXOS_UPDATE_HOST == "nixos"
|
||||
&& config.systemd.services.nixos-update.environment.NIXOS_CONFIG_REPO == "/etc/nix"
|
||||
&& config.systemd.services.nixos-update.environment.NIXOS_UPDATE_MODE == "boot"
|
||||
&& config.systemd.services.nixos-update.serviceConfig.User == "root"
|
||||
&& config.systemd.timers.nixos-update.timerConfig.Persistent
|
||||
&& !(config.systemd.services ? amazon-ssm-agent)
|
||||
&& !(builtins.elem "Z /etc/nixos - dev users -" config.systemd.tmpfiles.rules);
|
||||
message = "The physical host must not inherit EC2 services, updater or repo ownership.";
|
||||
message = "Physical updates must stage the physical target, never EC2 or live-session restarts.";
|
||||
}
|
||||
{
|
||||
assertion =
|
||||
@@ -132,8 +132,9 @@ in
|
||||
assert lib.all (test: lib.assertMsg test.assertion test.message) tests;
|
||||
pkgs.runCommand "physical-config-check" { } ''
|
||||
sessions=${config.services.displayManager.sessionData.desktops}/share
|
||||
test -f "$sessions/wayland-sessions/plasma.desktop"
|
||||
test ! -e "$sessions/wayland-sessions/plasma.desktop"
|
||||
test -f "$sessions/wayland-sessions/hyprland-uwsm.desktop"
|
||||
test ! -e "$sessions/wayland-sessions/hyprland.desktop"
|
||||
for tool in wg wg-quick openvpn iperf3 nmap traceroute whois mtr dig tcpdump ethtool nc socat nm-connection-editor; do
|
||||
test -x "${config.system.path}/bin/$tool"
|
||||
done
|
||||
|
||||
+64
-21
@@ -1,10 +1,28 @@
|
||||
# This machine: retain the installed kbot/Plasma recovery environment and add dev.
|
||||
# This machine: dev + managed Hyprland only; preserve boot/storage and old home data.
|
||||
# Hardware settings were copied from /etc/nixos; those original files stay untouched.
|
||||
{ pkgs, ... }:
|
||||
{
|
||||
config,
|
||||
lib,
|
||||
pkgs,
|
||||
...
|
||||
}:
|
||||
let
|
||||
managedHyprlandSession =
|
||||
pkgs.runCommand "hyprland-managed-session"
|
||||
{
|
||||
passthru.providedSessions = [ "hyprland-uwsm" ];
|
||||
}
|
||||
''
|
||||
mkdir -p "$out/share/wayland-sessions"
|
||||
ln -s ${config.programs.hyprland.package}/share/wayland-sessions/hyprland-uwsm.desktop \
|
||||
"$out/share/wayland-sessions/hyprland-uwsm.desktop"
|
||||
'';
|
||||
in
|
||||
{
|
||||
imports = [
|
||||
./hardware-configuration.nix
|
||||
./common.nix
|
||||
./updates.nix
|
||||
];
|
||||
|
||||
boot.loader = {
|
||||
@@ -17,7 +35,7 @@
|
||||
enable = true;
|
||||
plugins = [ pkgs.networkmanager-openvpn ];
|
||||
};
|
||||
# Keep the installed DNS setup while testing the additional desktop.
|
||||
# Keep NetworkManager's installed DNS policy.
|
||||
services.resolved.enable = false;
|
||||
|
||||
time.timeZone = "America/Chicago";
|
||||
@@ -34,8 +52,8 @@
|
||||
LC_TIME = "en_US.UTF-8";
|
||||
};
|
||||
|
||||
# SDDM offers both Plasma and Hyprland (uwsm-managed). Do not replace it with
|
||||
# workstation.nix's greetd/ReGreet or enable autologin during this migration.
|
||||
# Keep the working SDDM greeter; only the managed Hyprland session is offered.
|
||||
# No autologin, second desktop environment, or competing session owner.
|
||||
services.xserver = {
|
||||
enable = true;
|
||||
xkb = {
|
||||
@@ -43,32 +61,57 @@
|
||||
variant = "";
|
||||
};
|
||||
};
|
||||
services.displayManager.sddm.enable = true;
|
||||
services.desktopManager.plasma6.enable = true;
|
||||
services.displayManager = {
|
||||
sddm.enable = true;
|
||||
defaultSession = "hyprland-uwsm";
|
||||
# The upstream package also advertises a plain session that never starts
|
||||
# our bar/idle/polkit services. Hide that trap, not the managed startup.
|
||||
sessionPackages = lib.mkForce [ managedHyprlandSession ];
|
||||
};
|
||||
services.printing.enable = true;
|
||||
services.pulseaudio.enable = false;
|
||||
|
||||
# Preserve kbot's existing password and allow `sudo passwd dev` after activation.
|
||||
# No password hashes or credentials are copied into this repository/the store.
|
||||
# Preserve dev's locally established password. Removing the previously
|
||||
# managed kbot account does NOT request deletion of /home/kbot.
|
||||
users.mutableUsers = true;
|
||||
users.users.kbot = {
|
||||
isNormalUser = true;
|
||||
uid = 1000;
|
||||
home = "/home/kbot";
|
||||
description = "kbot";
|
||||
extraGroups = [
|
||||
"networkmanager"
|
||||
"wheel"
|
||||
];
|
||||
packages = [ pkgs.kdePackages.kate ];
|
||||
};
|
||||
users.users.dev.extraGroups = [ "networkmanager" ];
|
||||
environment.systemPackages = [ pkgs.networkmanagerapplet ];
|
||||
services.udisks2.enable = true;
|
||||
services.gvfs.enable = true;
|
||||
services.fwupd.enable = true;
|
||||
home-manager.users.dev.services.udiskie.enable = true;
|
||||
hardware.bluetooth = {
|
||||
enable = true;
|
||||
powerOnBoot = false;
|
||||
};
|
||||
services.blueman.enable = true;
|
||||
services.upower.enable = true;
|
||||
services.power-profiles-daemon.enable = true;
|
||||
# sysinfo's component label for this Intel CPU, not another laptop's ACPI zone.
|
||||
home-manager.users.dev.programs.ashell.settings.system_info.temperature.sensor =
|
||||
"coretemp Package id 0";
|
||||
home-manager.users.dev.xdg.configFile."autostart/blueman.desktop".text = ''
|
||||
[Desktop Entry]
|
||||
Type=Application
|
||||
Name=Blueman
|
||||
Hidden=true
|
||||
'';
|
||||
|
||||
# Retain the installed host's package policy.
|
||||
nixpkgs.config.allowUnfree = true;
|
||||
|
||||
# Intentionally no updates.nix: that updater builds the EC2 dev target.
|
||||
# /etc/nix is administrator-owned. Stage tested updates for the next boot,
|
||||
# rather than restarting a compositor/display manager under an active user.
|
||||
systemd.services.nixos-update = {
|
||||
environment = {
|
||||
NIXOS_CONFIG_REPO = "/etc/nix";
|
||||
NIXOS_UPDATE_HOST = "nixos";
|
||||
NIXOS_UPDATE_MODE = "boot";
|
||||
};
|
||||
serviceConfig = {
|
||||
User = "root";
|
||||
Group = "root";
|
||||
};
|
||||
};
|
||||
# Neither /etc/nixos nor /home/kbot is handed over to dev/Home Manager.
|
||||
}
|
||||
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
* { font-family: Inter, sans-serif; font-size: 14px; color: @text@; }
|
||||
.control-center {
|
||||
background: @background@;
|
||||
border: 1px solid @border@;
|
||||
border-radius: 6px;
|
||||
padding: 10px;
|
||||
}
|
||||
.notification {
|
||||
background: @surface@;
|
||||
border: 1px solid @border@;
|
||||
border-radius: 6px;
|
||||
margin: 6px;
|
||||
}
|
||||
.notification.critical { border-color: @red@; }
|
||||
/* Keep keyboard focus visible without the upstream thick gray outer slab. */
|
||||
.notification-row:focus, .notification-group:focus { background: transparent; }
|
||||
.notification-row:focus .notification, .notification-group:focus .notification { border-color: @accent@; }
|
||||
.notification-row .notification-background .notification { box-shadow: none; }
|
||||
.notification-content { padding: 12px; }
|
||||
.summary { font-weight: 600; }
|
||||
.time, .body { color: @muted@; }
|
||||
button { background: @raised@; border-radius: 4px; border: none; padding: 6px 10px; }
|
||||
button:hover { background: @selection@; }
|
||||
button:checked, switch:checked { background: @accent@; color: @background@; }
|
||||
.widget-title { margin: 8px; }
|
||||
.widget-title > label { font-size: 18px; font-weight: 600; }
|
||||
.widget-dnd, .widget-volume, .widget-backlight, .widget-mpris { margin: 8px; }
|
||||
.widget-mpris-player { background: @surface@; border-radius: 6px; padding: 8px; }
|
||||
trough { background: @raised@; border-radius: 4px; }
|
||||
highlight, progress { background: @accent@; border-radius: 4px; }
|
||||
.close-button { background: @raised@; color: @text@; }
|
||||
@@ -0,0 +1,58 @@
|
||||
# Bounded, offline smoke tests. No user caches, credentials, servers or downloads.
|
||||
{ config, pkgs }:
|
||||
pkgs.runCommand "workstation-tools-check"
|
||||
{
|
||||
nativeBuildInputs = [ pkgs.python3 ];
|
||||
}
|
||||
''
|
||||
export HOME="$TMPDIR/home"
|
||||
mkdir -p "$HOME"
|
||||
export PATH=${config.system.path}/bin:$PATH
|
||||
for tool in pi git git-lfs gh glab cmake ninja gdb \
|
||||
cargo rustc rustfmt cargo-clippy cargo-nextest go gopls dlv uv ruff pyright \
|
||||
node pnpm tsc biome bun deno shellcheck shfmt just hyperfine watchexec \
|
||||
nom nvd nix-tree nix-diff statix deadnix nixd \
|
||||
jq yq jless mlr csvlens sqlite3 duckdb pgcli \
|
||||
xh grpcurl websocat aws skopeo buildah podman podman-compose dive \
|
||||
kubectl helm k9s kubectx stern kustomize tofu ansible \
|
||||
age sops gpg gitleaks trivy restic rclone dust duf ncdu procs lnav \
|
||||
ffmpeg magick mediainfo exiftool pdftotext pandoc yt-dlp chafa desktop-help \
|
||||
valgrind heaptrack rr eu-readelf bpftrace java javac mvn gradle kotlin \
|
||||
dotnet ruby bundle php composer zig zls elixir erl protoc buf \
|
||||
dprint stylua taplo marksman markdownlint-cli2 sqlfluff hadolint ast-grep rga \
|
||||
hurl oha step mkcert cosign syft grype mosh sshfs asciinema vhs; do
|
||||
command -v "$tool" >/dev/null
|
||||
done
|
||||
test -x ${config.home-manager.users.dev.programs.lazygit.package}/bin/lazygit
|
||||
test -x ${config.home-manager.users.dev.programs.tmux.package}/bin/tmux
|
||||
pi --version
|
||||
uv --version
|
||||
ruff --version
|
||||
cargo --version
|
||||
rustc --version
|
||||
go version
|
||||
node --version
|
||||
java -version
|
||||
dotnet --version
|
||||
zig version
|
||||
ruby --version
|
||||
php --version | head -1
|
||||
printf 'select 42;\n' | sqlite3 | grep -qx 42
|
||||
python - <<'PY'
|
||||
import importlib.util
|
||||
spec = importlib.util.spec_from_file_location("desktop_help", "${./desktop-help.py}")
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
rows = module.rows([
|
||||
{"modmask": 65, "key": "H", "description": "Help"},
|
||||
{"modmask": 0, "key": "Print", "description": "Capture"},
|
||||
{"modmask": 64, "key": "Q", "description": "Close; $(touch /not-executed)"},
|
||||
{"modmask": 0, "key": "", "keycode": 20, "description": "Code", "submap": "resize"},
|
||||
{"key": "undocumented"},
|
||||
])
|
||||
assert "Help Super + Shift + H" in rows
|
||||
assert "Code [resize] code:20" in rows
|
||||
assert len(rows) == 4
|
||||
PY
|
||||
touch "$out"
|
||||
''
|
||||
@@ -1,40 +1,209 @@
|
||||
{ pkgs, ... }:
|
||||
{ inputs, pkgs, ... }:
|
||||
|
||||
let
|
||||
c = import ./colors.nix;
|
||||
# Standalone tools from the fast-moving pin, NOT an overlay of the system's
|
||||
# Python/GCC/libraries. Desktop, drivers and NixOS services remain coherent.
|
||||
latest = import inputs.nixpkgs-latest {
|
||||
inherit (pkgs.stdenv.hostPlatform) system;
|
||||
config = pkgs.config;
|
||||
};
|
||||
in
|
||||
{
|
||||
programs.git.enable = true;
|
||||
programs.git = {
|
||||
enable = true;
|
||||
package = latest.git;
|
||||
lfs.enable = true;
|
||||
lfs.package = latest.git-lfs;
|
||||
};
|
||||
programs.zsh.enable = true;
|
||||
# Native module supplies rootless mappings/networking. No Docker daemon,
|
||||
# docker-group access, public API socket, containers or images on activation.
|
||||
virtualisation.podman.enable = true;
|
||||
# Mason's upstream Linux executables expect a conventional dynamic loader.
|
||||
programs.nix-ld.enable = true;
|
||||
|
||||
environment.systemPackages = with pkgs; [
|
||||
nixfmt
|
||||
# Overridden in common.nix to stay current beyond stable release lag.
|
||||
environment.systemPackages = [
|
||||
pkgs.nixfmt
|
||||
]
|
||||
++ (with latest; [
|
||||
pi-coding-agent
|
||||
# Runtime build prerequisites for the unchanged Lazy/Mason plugin workflow.
|
||||
|
||||
# Native builds and the unchanged Lazy/Mason runtime prerequisites.
|
||||
gcc
|
||||
gnumake
|
||||
pkg-config
|
||||
cmake
|
||||
ninja
|
||||
meson
|
||||
ccache
|
||||
python3
|
||||
nodejs
|
||||
lua5_1
|
||||
luajitPackages.luarocks
|
||||
clang-tools
|
||||
gdb
|
||||
lldb
|
||||
valgrind
|
||||
heaptrack
|
||||
rr
|
||||
elfutils
|
||||
bpftrace
|
||||
|
||||
# Language toolchains, testing and dependency/security audits.
|
||||
rustc
|
||||
cargo
|
||||
rustfmt
|
||||
clippy
|
||||
rust-analyzer
|
||||
cargo-nextest
|
||||
cargo-audit
|
||||
cargo-deny
|
||||
cargo-expand
|
||||
cargo-edit
|
||||
go
|
||||
gopls
|
||||
delve
|
||||
golangci-lint
|
||||
uv
|
||||
ruff
|
||||
pyright
|
||||
pnpm
|
||||
typescript
|
||||
biome
|
||||
bun
|
||||
deno
|
||||
# JVM, .NET, Ruby/PHP, Zig and BEAM: project versions still belong in devShells.
|
||||
jdk25
|
||||
maven
|
||||
gradle_9
|
||||
kotlin
|
||||
dotnet-sdk_10
|
||||
ruby
|
||||
bundler
|
||||
php
|
||||
phpPackages.composer
|
||||
zig
|
||||
zls
|
||||
beamPackages.elixir
|
||||
beamPackages.erlang
|
||||
protobuf
|
||||
buf
|
||||
shellcheck
|
||||
shfmt
|
||||
just
|
||||
hyperfine
|
||||
watchexec
|
||||
tokei
|
||||
yamllint
|
||||
actionlint
|
||||
pre-commit
|
||||
dprint
|
||||
stylua
|
||||
taplo
|
||||
marksman
|
||||
markdownlint-cli2
|
||||
sqlfluff
|
||||
hadolint
|
||||
ast-grep
|
||||
ripgrep-all
|
||||
|
||||
# Version control: no invented identity, login or credentials.
|
||||
gh
|
||||
glab
|
||||
git-absorb
|
||||
git-filter-repo
|
||||
difftastic
|
||||
jujutsu
|
||||
|
||||
# Nix introspection and development; never replace the system Nix daemon.
|
||||
nix-output-monitor
|
||||
nvd
|
||||
nix-tree
|
||||
nix-diff
|
||||
statix
|
||||
deadnix
|
||||
nixd
|
||||
nixpkgs-review
|
||||
|
||||
# Shell, structured data, file navigation and documentation.
|
||||
ripgrep
|
||||
fd
|
||||
eza
|
||||
jq
|
||||
yq-go
|
||||
jless
|
||||
sd
|
||||
tree
|
||||
file
|
||||
hexyl
|
||||
parallel
|
||||
moreutils
|
||||
tealdeer
|
||||
zellij
|
||||
miller
|
||||
csvlens
|
||||
sqlite
|
||||
duckdb
|
||||
pgcli
|
||||
litecli
|
||||
redis
|
||||
|
||||
# HTTP/API clients. Packet-level tools belong in network.nix.
|
||||
curl
|
||||
wget
|
||||
file
|
||||
tree
|
||||
xh
|
||||
grpcurl
|
||||
websocat
|
||||
hurl
|
||||
oha
|
||||
step-cli
|
||||
mkcert # Installed only: no CA is created or trusted automatically.
|
||||
|
||||
# Cloud, containers and orchestration: clients only, no live infrastructure.
|
||||
awscli2
|
||||
skopeo
|
||||
buildah
|
||||
podman-compose
|
||||
dive
|
||||
kubectl
|
||||
kubernetes-helm
|
||||
k9s
|
||||
kubectx
|
||||
stern
|
||||
kustomize
|
||||
opentofu
|
||||
ansible
|
||||
|
||||
# Encryption, secret scanning, backup and transfer. No automatic jobs or keys.
|
||||
age
|
||||
sops
|
||||
gnupg
|
||||
gitleaks
|
||||
trivy
|
||||
cosign
|
||||
syft
|
||||
grype
|
||||
restic
|
||||
rclone
|
||||
rsync
|
||||
mosh
|
||||
sshfs
|
||||
openssl
|
||||
unzip
|
||||
zip
|
||||
p7zip
|
||||
rsync
|
||||
openssl
|
||||
zstd
|
||||
lz4
|
||||
|
||||
# Logs, storage, process and hardware diagnosis. No extra privileges granted.
|
||||
dust
|
||||
duf
|
||||
ncdu
|
||||
procs
|
||||
sysstat
|
||||
iotop
|
||||
lnav
|
||||
lsof
|
||||
strace
|
||||
psmisc
|
||||
@@ -45,7 +214,19 @@ in
|
||||
lm_sensors
|
||||
man-pages
|
||||
man-pages-posix
|
||||
];
|
||||
|
||||
# Media/document tooling and rich Yazi previews.
|
||||
ffmpeg
|
||||
imagemagick
|
||||
mediainfo
|
||||
exiftool
|
||||
poppler-utils
|
||||
pandoc
|
||||
yt-dlp
|
||||
chafa
|
||||
asciinema
|
||||
vhs
|
||||
]);
|
||||
|
||||
fonts.packages = [ pkgs.nerd-fonts.jetbrains-mono ];
|
||||
fonts.fontconfig.defaultFonts.monospace = [ "JetBrainsMono Nerd Font" ];
|
||||
@@ -57,6 +238,43 @@ in
|
||||
settings.user.useConfigOnly = true;
|
||||
};
|
||||
|
||||
programs.delta = {
|
||||
enable = true;
|
||||
package = latest.delta;
|
||||
enableGitIntegration = true;
|
||||
options = {
|
||||
navigate = true;
|
||||
line-numbers = true;
|
||||
};
|
||||
};
|
||||
programs.lazygit = {
|
||||
enable = true;
|
||||
package = latest.lazygit;
|
||||
settings.gui = {
|
||||
nerdFontsVersion = "3";
|
||||
showRandomTip = false;
|
||||
};
|
||||
};
|
||||
programs.direnv = {
|
||||
enable = true;
|
||||
package = latest.direnv;
|
||||
enableZshIntegration = true;
|
||||
nix-direnv = {
|
||||
enable = true;
|
||||
package = latest.nix-direnv;
|
||||
};
|
||||
# Deliberately no whitelist: each project's .envrc needs `direnv allow`.
|
||||
};
|
||||
programs.tmux = {
|
||||
enable = true;
|
||||
package = latest.tmux;
|
||||
terminal = "tmux-256color";
|
||||
mouse = true;
|
||||
keyMode = "vi";
|
||||
historyLimit = 50000;
|
||||
escapeTime = 10;
|
||||
};
|
||||
|
||||
programs.zsh = {
|
||||
enable = true;
|
||||
enableCompletion = true;
|
||||
@@ -86,6 +304,7 @@ in
|
||||
programs.fzf = {
|
||||
enable = true;
|
||||
enableZshIntegration = true;
|
||||
package = latest.fzf;
|
||||
defaultCommand = "fd --type f --hidden --exclude .git";
|
||||
fileWidgetCommand = "fd --type f --hidden --exclude .git";
|
||||
changeDirWidgetCommand = "fd --type d --hidden --exclude .git";
|
||||
@@ -103,7 +322,7 @@ in
|
||||
hl = c.cyan;
|
||||
"hl+" = c.cyan;
|
||||
border = c.border;
|
||||
prompt = c.blue;
|
||||
prompt = c.accent;
|
||||
pointer = c.purple;
|
||||
marker = c.green;
|
||||
info = c.muted;
|
||||
@@ -112,11 +331,12 @@ in
|
||||
|
||||
programs.starship = {
|
||||
enable = true;
|
||||
package = latest.starship;
|
||||
settings = {
|
||||
add_newline = true;
|
||||
format = "$username$hostname$directory$git_branch$git_status$nix_shell$cmd_duration\n$character";
|
||||
directory = {
|
||||
style = "bold ${c.blue}";
|
||||
style = "bold ${c.accent}";
|
||||
truncation_length = 4;
|
||||
truncation_symbol = "…/";
|
||||
read_only = " [read-only]";
|
||||
@@ -143,14 +363,17 @@ in
|
||||
|
||||
programs.zoxide = {
|
||||
enable = true;
|
||||
package = latest.zoxide;
|
||||
enableZshIntegration = true;
|
||||
};
|
||||
programs.bat = {
|
||||
enable = true;
|
||||
package = latest.bat;
|
||||
config.theme = "base16";
|
||||
};
|
||||
programs.btop = {
|
||||
enable = true;
|
||||
package = latest.btop;
|
||||
settings = {
|
||||
theme_background = false;
|
||||
rounded_corners = true;
|
||||
@@ -159,6 +382,7 @@ in
|
||||
};
|
||||
programs.yazi = {
|
||||
enable = true;
|
||||
package = latest.yazi;
|
||||
enableZshIntegration = true;
|
||||
settings.mgr = {
|
||||
show_hidden = true;
|
||||
@@ -171,10 +395,10 @@ in
|
||||
enable = true;
|
||||
font = {
|
||||
name = "JetBrainsMono Nerd Font";
|
||||
size = 13;
|
||||
size = 12;
|
||||
};
|
||||
settings = {
|
||||
window_padding_width = 14;
|
||||
window_padding_width = 10;
|
||||
background_opacity = "1.0";
|
||||
hide_window_decorations = true;
|
||||
scrollback_lines = 20000;
|
||||
@@ -186,9 +410,9 @@ in
|
||||
background = c.background;
|
||||
cursor = c.cyan;
|
||||
selection_foreground = c.text;
|
||||
selection_background = "#354562";
|
||||
selection_background = c.selection;
|
||||
url_color = c.blue;
|
||||
active_border_color = c.blue;
|
||||
active_border_color = c.accent;
|
||||
inactive_border_color = c.border;
|
||||
color0 = c.surface;
|
||||
color1 = c.red;
|
||||
@@ -197,14 +421,14 @@ in
|
||||
color4 = c.blue;
|
||||
color5 = c.purple;
|
||||
color6 = c.cyan;
|
||||
color7 = "#dde1e6";
|
||||
color7 = c.text;
|
||||
color8 = c.muted;
|
||||
color9 = "#ff99a0";
|
||||
color10 = "#6fdc8c";
|
||||
color11 = "#f7d75c";
|
||||
color12 = "#a6c8ff";
|
||||
color13 = "#d4bbff";
|
||||
color14 = "#82e9de";
|
||||
color9 = "#eda692";
|
||||
color10 = "#b8c992";
|
||||
color11 = "#e6ca91";
|
||||
color12 = "#a7c2c8";
|
||||
color13 = "#c9b9d1";
|
||||
color14 = "#adcbb7";
|
||||
color15 = c.text;
|
||||
};
|
||||
};
|
||||
|
||||
+37
-11
@@ -1,5 +1,12 @@
|
||||
# Run as dev; privileged activation uses the already declared scoped sudo rule.
|
||||
repo=${NIXOS_CONFIG_REPO:-/etc/nixos}
|
||||
# Host and activation policy come from the host's systemd unit, never inference
|
||||
# from a login name. Physical builds are staged for next boot; EC2 switches live.
|
||||
repo=${NIXOS_CONFIG_REPO:?Set NIXOS_CONFIG_REPO}
|
||||
host=${NIXOS_UPDATE_HOST:?Set NIXOS_UPDATE_HOST}
|
||||
mode=${NIXOS_UPDATE_MODE:?Set NIXOS_UPDATE_MODE}
|
||||
case "$host:$mode" in
|
||||
dev:switch|nixos:boot) ;;
|
||||
*) echo "Refusing unsupported update target/policy: $host:$mode" >&2; exit 2 ;;
|
||||
esac
|
||||
state=${CACHE_DIRECTORY:-/var/cache/nixos-update}
|
||||
mkdir -p "$state"
|
||||
exec 9>"$state/lock"
|
||||
@@ -11,7 +18,10 @@ if [ -n "$(git status --porcelain)" ]; then
|
||||
exit 0
|
||||
fi
|
||||
baseline=$(git rev-parse HEAD)
|
||||
branch=$(git symbolic-ref HEAD)
|
||||
if ! branch=$(git symbolic-ref -q HEAD); then
|
||||
echo 'Skipping automatic update: checkout is detached.'
|
||||
exit 0
|
||||
fi
|
||||
work=$(mktemp -d "$state/work.XXXXXXXX")
|
||||
cleanup() {
|
||||
git -C "$repo" worktree remove --force "$work" >/dev/null 2>&1 || true
|
||||
@@ -29,32 +39,48 @@ if git diff --quiet -- flake.lock; then
|
||||
fi
|
||||
nix flake check --no-build --no-update-lock-file
|
||||
nix build .#checks.x86_64-linux.updates .#checks.x86_64-linux.desktop-config \
|
||||
.#checks.x86_64-linux.physical-config .#checks.x86_64-linux.tools \
|
||||
.#checks.x86_64-linux.desktop-actions \
|
||||
--no-update-lock-file --no-link
|
||||
git add flake.lock
|
||||
git -c user.name='NixOS Updater' -c user.email='nixos-updater@localhost' \
|
||||
commit -m 'chore: update NixOS package inputs'
|
||||
built=$(nix build .#nixosConfigurations.dev.config.system.build.toplevel \
|
||||
built=$(nix build ".#nixosConfigurations.$host.config.system.build.toplevel" \
|
||||
--no-update-lock-file --no-link --print-out-paths)
|
||||
candidate=$(git rev-parse HEAD)
|
||||
|
||||
# Never overwrite work started while the candidate was building.
|
||||
cd "$repo"
|
||||
if [ "$(git rev-parse HEAD)" != "$baseline" ] || \
|
||||
[ "$(git symbolic-ref HEAD)" != "$branch" ] || \
|
||||
[ -n "$(git status --porcelain)" ]; then
|
||||
unchanged() {
|
||||
[ "$(git rev-parse HEAD)" = "$baseline" ] &&
|
||||
[ "$(git symbolic-ref -q HEAD)" = "$branch" ] &&
|
||||
[ -z "$(git status --porcelain)" ]
|
||||
}
|
||||
if ! unchanged; then
|
||||
echo 'Configuration changed during the build; leaving it untouched.'
|
||||
exit 0
|
||||
fi
|
||||
previous=$(readlink -f /run/current-system)
|
||||
if [ "$mode" = boot ]; then
|
||||
# Preserve an already staged generation on failure, not just the running one.
|
||||
previous=$(readlink -f /nix/var/nix/profiles/system)
|
||||
else
|
||||
previous=$(readlink -f /run/current-system)
|
||||
fi
|
||||
sudo "$built/sw/bin/nixos-rebuild" dry-activate --no-reexec --store-path "$built"
|
||||
if ! unchanged; then
|
||||
echo 'Configuration changed during dry activation; leaving it untouched.'
|
||||
exit 0
|
||||
fi
|
||||
git merge --ff-only "$candidate"
|
||||
if ! sudo "$built/sw/bin/nixos-rebuild" switch --no-reexec --store-path "$built"; then
|
||||
echo 'Activation failed; restoring the previous system. See the journal.' >&2
|
||||
sudo "$previous/sw/bin/nixos-rebuild" switch --no-reexec --store-path "$previous"
|
||||
if ! sudo "$built/sw/bin/nixos-rebuild" "$mode" --no-reexec --store-path "$built"; then
|
||||
echo 'Activation failed; restoring the previous system/profile. See the journal.' >&2
|
||||
sudo "$previous/sw/bin/nixos-rebuild" "$mode" --no-reexec --store-path "$previous"
|
||||
if [ "$(git rev-parse HEAD)" = "$candidate" ] && [ -z "$(git status --porcelain)" ]; then
|
||||
git -c user.name='NixOS Updater' -c user.email='nixos-updater@localhost' \
|
||||
revert --no-edit "$candidate"
|
||||
fi
|
||||
exit 1
|
||||
fi
|
||||
printf '%s %s %s\n' "$(date -Is)" "$candidate" "$built" > "$state/last-success"
|
||||
echo "Updated $host ($mode): $built"
|
||||
# No forced reboot or garbage collection: recovery generations are retained.
|
||||
|
||||
+33
-15
@@ -23,6 +23,9 @@ if name == "nix":
|
||||
elif args[:2] == ["flake", "check"]:
|
||||
if scenario == "evaluation-failure": sys.exit(42)
|
||||
elif args[0] == "build":
|
||||
targets = [arg for arg in args if "nixosConfigurations." in arg]
|
||||
if targets:
|
||||
assert targets == [f'.#nixosConfigurations.{os.environ["NIXOS_UPDATE_HOST"]}.config.system.build.toplevel']
|
||||
if scenario == "build-failure": sys.exit(43)
|
||||
if scenario == "concurrent-edit":
|
||||
(pathlib.Path(os.environ["NIXOS_CONFIG_REPO"]) / "notes").write_text("user work\n")
|
||||
@@ -32,17 +35,20 @@ if name == "nix":
|
||||
elif name == "sudo":
|
||||
assert "--no-reexec" in args and "--store-path" in args
|
||||
if scenario == "dry-activation-failure" and args[1] == "dry-activate": sys.exit(44)
|
||||
if scenario == "activation-failure" and args[1] == "switch" and args[-1] == os.environ["BUILT"]:
|
||||
sys.exit(45)
|
||||
if scenario == "concurrent-dry-edit" and args[1] == "dry-activate":
|
||||
(pathlib.Path(os.environ["NIXOS_CONFIG_REPO"]) / "notes").write_text("user work\n")
|
||||
if scenario in ("activation-failure", "rollback-failure") and args[1] == os.environ["NIXOS_UPDATE_MODE"]:
|
||||
if args[-1] == os.environ["BUILT"] or scenario == "rollback-failure": sys.exit(45)
|
||||
elif name == "readlink":
|
||||
assert args == ["-f", "/run/current-system"]
|
||||
previous = "/nix/var/nix/profiles/system" if os.environ["NIXOS_UPDATE_MODE"] == "boot" else "/run/current-system"
|
||||
assert args == ["-f", previous]
|
||||
print(os.environ["PREVIOUS"])
|
||||
else:
|
||||
raise AssertionError(name)
|
||||
'''
|
||||
|
||||
|
||||
def run_case(scenario):
|
||||
def run_case(scenario, host="dev", mode="switch"):
|
||||
with tempfile.TemporaryDirectory(prefix="update-test-") as directory:
|
||||
root = Path(directory)
|
||||
repo = root / "repo with spaces"
|
||||
@@ -52,7 +58,8 @@ def run_case(scenario):
|
||||
mocks.mkdir()
|
||||
calls_path = root / "calls.jsonl"
|
||||
env = dict(os.environ, HOME=str(root / "home"), CACHE_DIRECTORY=str(cache),
|
||||
NIXOS_CONFIG_REPO=str(repo), SCENARIO=scenario,
|
||||
NIXOS_CONFIG_REPO=str(repo), NIXOS_UPDATE_HOST=host,
|
||||
NIXOS_UPDATE_MODE=mode, SCENARIO=scenario,
|
||||
CALLS=str(calls_path), BUILT=str(root / "candidate-system"),
|
||||
PREVIOUS=str(root / "previous-system"),
|
||||
GIT_CONFIG_GLOBAL="/dev/null", GIT_CONFIG_SYSTEM="/dev/null")
|
||||
@@ -77,37 +84,48 @@ def run_case(scenario):
|
||||
baseline = git("rev-parse", "HEAD")
|
||||
if scenario == "dirty":
|
||||
(repo / "notes").write_text("user work\n")
|
||||
if scenario == "detached":
|
||||
git("checkout", "--detach", "--quiet")
|
||||
result = subprocess.run(
|
||||
[shutil.which("bash"), "-euo", "pipefail", str(script)],
|
||||
env=env, text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT
|
||||
)
|
||||
calls = [json.loads(line) for line in calls_path.read_text().splitlines()] if calls_path.exists() else []
|
||||
sudo = [call for call in calls if call[0] == "sudo"]
|
||||
failed = scenario.endswith("failure")
|
||||
failed = scenario.endswith("failure") or scenario == "invalid-host"
|
||||
assert (result.returncode != 0) == failed, (scenario, result.stdout)
|
||||
if scenario == "success":
|
||||
assert json.loads((repo / "flake.lock").read_text())["revision"] == 2
|
||||
assert git("log", "-1", "--format=%an <%ae>") == "NixOS Updater <nixos-updater@localhost>"
|
||||
assert [call[2] for call in sudo] == ["dry-activate", "switch"]
|
||||
assert [call[2] for call in sudo] == ["dry-activate", mode]
|
||||
assert (cache / "last-success").is_file()
|
||||
assert git("status", "--porcelain") == ""
|
||||
elif scenario == "activation-failure":
|
||||
assert json.loads((repo / "flake.lock").read_text())["revision"] == 1
|
||||
assert [call[2] for call in sudo] == ["dry-activate", "switch", "switch"]
|
||||
assert [call[2] for call in sudo] == ["dry-activate", mode, mode]
|
||||
assert sudo[-1][-1] != env["BUILT"]
|
||||
assert git("log", "-1", "--format=%s").startswith("Revert")
|
||||
elif scenario == "rollback-failure":
|
||||
# Keep the candidate commit for recovery, never claim rollback worked.
|
||||
assert json.loads((repo / "flake.lock").read_text())["revision"] == 2
|
||||
assert [call[2] for call in sudo] == ["dry-activate", mode, mode]
|
||||
assert not (cache / "last-success").exists()
|
||||
else:
|
||||
assert git("rev-parse", "HEAD") == baseline, (scenario, result.stdout)
|
||||
assert json.loads((repo / "flake.lock").read_text())["revision"] == 1
|
||||
assert not sudo or scenario == "dry-activation-failure"
|
||||
assert not sudo or scenario in ("dry-activation-failure", "concurrent-dry-edit")
|
||||
assert (repo / "unchanged-editor-input").read_text().strip() == "380eb86778a7c53a0f1c18e84f14037456155347"
|
||||
assert len(git("worktree", "list", "--porcelain").split("worktree ")) == 2
|
||||
if scenario in ["dirty", "concurrent-edit"]:
|
||||
if scenario in ["dirty", "concurrent-edit", "concurrent-dry-edit"]:
|
||||
assert (repo / "notes").read_text() == "user work\n"
|
||||
if scenario == "dirty":
|
||||
if scenario in ("dirty", "detached", "invalid-host"):
|
||||
assert not calls
|
||||
print("PASS", scenario)
|
||||
print("PASS", host, mode, scenario)
|
||||
|
||||
|
||||
for scenario in ["dirty", "unchanged", "evaluation-failure", "build-failure", "concurrent-edit",
|
||||
"dry-activation-failure", "activation-failure", "success"]:
|
||||
run_case(scenario)
|
||||
for host, mode in [("dev", "switch"), ("nixos", "boot")]:
|
||||
for scenario in ["dirty", "detached", "unchanged", "evaluation-failure", "build-failure",
|
||||
"concurrent-edit", "concurrent-dry-edit", "dry-activation-failure",
|
||||
"activation-failure", "rollback-failure", "success"]:
|
||||
run_case(scenario, host, mode)
|
||||
run_case("invalid-host", "nixos", "switch")
|
||||
|
||||
+15
-6
@@ -1,4 +1,9 @@
|
||||
{ pkgs, ... }:
|
||||
{
|
||||
config,
|
||||
lib,
|
||||
pkgs,
|
||||
...
|
||||
}:
|
||||
let
|
||||
updater = pkgs.writeShellApplication {
|
||||
name = "update-system";
|
||||
@@ -13,17 +18,21 @@ let
|
||||
in
|
||||
{
|
||||
environment.systemPackages = [ updater ];
|
||||
systemd.tmpfiles.rules = [ "d /var/cache/nixos-update 0700 dev users -" ];
|
||||
systemd.services.nixos-update = {
|
||||
description = "Build, record and apply stable NixOS updates without disturbing local work";
|
||||
description = "Build and record host-specific NixOS/tool updates without disturbing local work";
|
||||
environment = {
|
||||
NIXOS_CONFIG_REPO = lib.mkDefault "/etc/nixos";
|
||||
NIXOS_UPDATE_HOST = lib.mkDefault "dev";
|
||||
NIXOS_UPDATE_MODE = lib.mkDefault "switch";
|
||||
};
|
||||
wants = [ "network-online.target" ];
|
||||
after = [ "network-online.target" ];
|
||||
path = [ "/run/wrappers" ];
|
||||
serviceConfig = {
|
||||
Type = "oneshot";
|
||||
User = "dev";
|
||||
Group = "users";
|
||||
WorkingDirectory = "/etc/nixos";
|
||||
User = lib.mkDefault "dev";
|
||||
Group = lib.mkDefault "users";
|
||||
WorkingDirectory = config.systemd.services.nixos-update.environment.NIXOS_CONFIG_REPO;
|
||||
CacheDirectory = "nixos-update";
|
||||
UMask = "0077";
|
||||
Nice = 10;
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
# The image is fetched reproducibly, not downloaded by a login/startup script.
|
||||
# Source/attribution details and the original fallback are documented in DESKTOP.md.
|
||||
{ pkgs }:
|
||||
pkgs.fetchurl {
|
||||
name = "one-ring-dark-1920x1200.jpg";
|
||||
url = "https://w.wallhaven.cc/full/01/wallhaven-01e5v4.jpg";
|
||||
hash = "sha256-3jkKzJ0q4MTlHygwUs3SuSiUIjUjkiTqSaM+q8EL/oc=";
|
||||
}
|
||||
Reference in New Issue
Block a user