feat(desktop): add the desktop group and Hyprland session (task 0021) #13
34
.claude/adr/0003-hyprland-compositor.md
Normal file
34
.claude/adr/0003-hyprland-compositor.md
Normal file
@@ -0,0 +1,34 @@
|
||||
---
|
||||
status: accepted
|
||||
---
|
||||
|
||||
# Hyprland as the keyboard-driven desktop compositor
|
||||
|
||||
The graphical desktop is built on **Hyprland**, a keyboard-driven Wayland tiling compositor, as a single choice serving both the laptop (neogaia) and the future desktop (zeus).
|
||||
It matches how the operator already works: an i3 model of numbered workspaces and manual tiling, ported to bindings reachable entirely on a 60% keyboard.
|
||||
Among true tilers it comes closest to "just works" through its cohesive first-party companion tools (lock, idle, wallpaper, portal) and a large ecosystem, which buys down the assembly-and-breakage cost that made past minimal tiling setups expensive for the operator.
|
||||
|
||||
The gaming and driver dimension does not constrain the choice, because both Hosts drive Wayland without caveats: neogaia is Intel and zeus is AMD.
|
||||
Notably zeus is AMD, not Nvidia — Raichu is the only Nvidia machine, and it is a server with no desktop — so no Nvidia-on-Wayland pressure shapes the decision.
|
||||
With gaming survival off the table, the choice rests on workflow and low-friction rather than on tolerating a hostile driver.
|
||||
|
||||
## Considered Options
|
||||
|
||||
- **Sway.** Rejected: its i3-faithful minimalism is precisely what historically cost the operator hours of assembly and breakage.
|
||||
On AMD it games fine, but its only remaining edge over Hyprland was stability and purity — which the repo's pinning already provides, and which the operator's stated "just works" priority actively discounts.
|
||||
- **KDE Plasma.** Rejected: the most integrated and lowest-friction option, the best AMD gaming desktop, and the operator's prior environment — but mouse-first at its core and only keyboard-navigable at the margins, which works against the primary keyboard-first requirement.
|
||||
Its custom-tile-layout feature is an approximation of tiling on a floating desktop, not real automatic tiling.
|
||||
- **niri.** Rejected: its scrollable-tiling paradigm abandons numbered workspaces, which breaks the operator's core muscle memory of switching by number.
|
||||
It also has the smallest community of the candidates, a low-friction risk for a daily-driver desktop.
|
||||
- **A different compositor per Host** (a keyboard-pure laptop plus a separate gaming desktop). Rejected: it doubles the configuration and maintenance and defeats the goal of one transferable setup.
|
||||
It is unnecessary once AMD removes any gaming penalty from a keyboard-first compositor.
|
||||
|
||||
## Consequences
|
||||
|
||||
- One desktop Module set serves both Hosts.
|
||||
zeus adopts the identical desktop by enabling a single flag, with battery-sensitive knobs such as blur flipped on for its AMD headroom.
|
||||
- Hyprland's churn and occasional breakage are absorbed by the pinned, declarative, reversible configuration rather than by live fixing, so upgrades happen on the operator's schedule.
|
||||
- The desktop's lock, idle, keybind syntax, and portal are Hyprland-specific, so a future move to another compositor would be a rewrite rather than a swap.
|
||||
This is the accepted cost of the first-party-cohesion benefit.
|
||||
- Hyprland is taken from nixpkgs, with no compositor plugins this pass.
|
||||
Adopting the upstream Hyprland flake later, for a plugin or a bleeding-edge feature, is a contained change that mirrors the existing chaotic-nyx input pattern (an input that must not follow nixpkgs, carrying its own binary cache).
|
||||
173
.claude/spec/hyprland-desktop.md
Normal file
173
.claude/spec/hyprland-desktop.md
Normal file
@@ -0,0 +1,173 @@
|
||||
## Problem Statement
|
||||
|
||||
Neogaia currently boots to a console only.
|
||||
The laptop minimum viable install deliberately deferred everything graphical, so the operator works entirely from the terminal (fish, tmux, nvim, Claude Code) with no desktop to live in.
|
||||
|
||||
The operator wants a graphical desktop, and it must be primarily keyboard-driven.
|
||||
Their mental model is i3: numbered workspaces, `Super`+number to switch, manual tiling.
|
||||
They type on a 60% keyboard where arrow keys and the navigation cluster live behind a layer, so a keyboard-first workflow that never reaches for those keys matters.
|
||||
|
||||
The same desktop has to transfer to the future desktop Host (zeus), which must game well and stay low-friction in general.
|
||||
The operator previously ran KDE Plasma 6 bent into an i3 imitation and valued above all that it "just worked", but remembers past i3/Sway minimalism costing hours of fixing random breakage.
|
||||
That concern is now largely mitigated: NixOS makes the whole stack declarative, pinned, and reversible, and the agent absorbs the discovery and debugging that used to eat evenings.
|
||||
|
||||
## Solution
|
||||
|
||||
Add a keyboard-driven Wayland desktop built on Hyprland, expressed as a new grouped set of Modules.
|
||||
Enable it on neogaia now, and design it Host-agnostic so zeus adopts the identical desktop later by flipping a single flag.
|
||||
|
||||
Deliver a complete, daily-drivable session in one pass rather than a bare stub, because unlike the reimage this is fully reversible ("edit and rebuild"), so there is no safety reason to under-scope.
|
||||
The session covers the compositor, a text login, Nord theming, a status bar, a launcher, notifications, lock and idle, a wallpaper, clipboard history, screenshots, screen recording, and the desktop portals, together with the operator's ported keybinds and input tuning.
|
||||
|
||||
Theme the whole graphical layer Nord from a single source (Stylix), scoped so it owns only the new graphical surface and leaves the existing terminal Modules' hand-themes untouched.
|
||||
Leave gaming out entirely; it belongs to a future zeus-oriented Module, and neogaia is not a gaming machine.
|
||||
|
||||
## User Stories
|
||||
|
||||
1. As the operator, I want neogaia to boot into a keyboard-driven Hyprland session, so that I can live in a graphical desktop without leaving my keyboard-first workflow.
|
||||
2. As the operator, I want the desktop expressed as Host-agnostic Modules enabled by a single flag, so that zeus can adopt the identical desktop later without rework.
|
||||
3. As the operator, I want a text-based login, so that I log in mouse-free without a heavy graphical display manager.
|
||||
4. As the operator, I want the whole graphical layer themed Nord from one source, so that GTK, Qt, the bar, the lock screen, and the launcher cohere without me hand-theming each, while my existing nvim, tmux, and fish themes stay exactly as they are.
|
||||
5. As the operator, I want a fast modern terminal launched on `Super+Return`, so that my tmux and nvim stack has a clean frame and my old muscle memory for opening a terminal carries over.
|
||||
6. As the operator, I want a status bar showing workspaces with per-application icons, clock, battery, network, audio, media controls, and a do-not-disturb toggle, so that I can read system state at a glance.
|
||||
7. As the operator, I want a search-everything launcher covering applications, open windows, math evaluation, and emoji, reused as the frontend for clipboard history and a power menu, so that one keybound tool handles launching and utility menus.
|
||||
8. As the operator, I want notification toasts with do-not-disturb and history recall, so that I see notifications and can retrieve ones I missed.
|
||||
9. As the operator, I want a secure lock screen and idle management, so that going idle, suspending, or closing the lid always lands me at a locked screen, and the lock survives even if the locker process crashes.
|
||||
10. As the operator, I want a single static Nord wallpaper, so that the desktop looks coherent with no extra moving parts and no battery cost.
|
||||
11. As the operator, I want clipboard history picked through the launcher, so that I can paste from recent copies entirely by keyboard.
|
||||
12. As the operator, I want keyboard-driven screenshots for a region, the active window, or the full screen that open in an annotation editor by default and land in both the clipboard and a file, so that capturing and marking up is a single reflex.
|
||||
13. As the operator, I want a keybound screen recorder that selects a region and then toggles recording, so that capturing demos becomes a habit.
|
||||
14. As the operator, I want screen sharing to work inside applications, so that video calls and browser screen-share function.
|
||||
15. As the operator, I want my keybinds ported from my KDE/i3 scheme but expressed entirely in `hjkl` and letters with no arrow or navigation-cluster keys, so that every binding is reachable on my 60% keyboard.
|
||||
16. As the operator, I want numbered-workspace bindings, so that my i3 muscle memory of `Super`+number to switch and `Super`+`Shift`+number to move carries over unchanged.
|
||||
17. As the operator, I want Caps mapped to Escape, a US-only layout, snappy key-repeat, and touchpad tap-to-click, natural scroll, and disable-while-typing with flat mouse acceleration, so that input feels like home on the laptop.
|
||||
18. As the operator, I want subtle animations with rounding and small gaps but no blur on the laptop, so that the desktop feels modern without draining the battery.
|
||||
19. As the operator, I want the desktop built from granular single-purpose Modules grouped together with an explicit aggregator, so that a Host enables the whole desktop with one flag yet can still override any single piece.
|
||||
20. As the operator, I want gaming deliberately excluded from this pass, so that neogaia's desktop stays focused and the gaming stack lands with zeus.
|
||||
21. As the operator, I want the whole Host to still build green under the existing check, so that I gain confidence before switching a live machine.
|
||||
|
||||
## Implementation Decisions
|
||||
|
||||
**Direction and compositor**
|
||||
|
||||
- Hyprland is the compositor, on neogaia now and zeus later, chosen as a single keyboard-driven tiler that serves both Hosts.
|
||||
It matches the operator's i3 workflow (numbered workspaces, manual tiling) while getting closest to "just works" among true tilers through its cohesive first-party companion tools and large ecosystem.
|
||||
- The choice was made over Sway (its minimalism is the very thing that cost the operator evenings), KDE Plasma (mouse-first at heart), and niri (its scrollable-tiling paradigm abandons numbered workspaces and so breaks the operator's core muscle memory).
|
||||
- The gaming/driver dimension does not discriminate here: neogaia is Intel and zeus is AMD, both of which drive Wayland flawlessly, so the decision rested on workflow and low-friction rather than on surviving a hostile driver.
|
||||
This corrects a stale assumption in the laptop spec (see Further Notes).
|
||||
- Hyprland is sourced from nixpkgs rather than the upstream Hyprland flake.
|
||||
The NixOS-level program integration owns the session, portals, and polkit, and home-manager owns the user configuration, sharing one Hyprland package so there is never a version split.
|
||||
The session is launched through the universal Wayland session manager from the greeter for clean systemd session and environment integration.
|
||||
No compositor plugins are adopted this pass, which removes the main reason to take the flake; moving to the flake later is a contained change that mirrors the existing chaotic-nyx pattern (an input that must not follow nixpkgs, with its own binary cache).
|
||||
|
||||
**Session and theming**
|
||||
|
||||
- Login uses greetd with the tuigreet text greeter.
|
||||
This is mouse-free and lightweight, avoiding a heavy graphical display manager and its Qt/GTK weight.
|
||||
- Theming uses Stylix, scoped to the graphical layer.
|
||||
A single Nord base16 scheme drives colors, system fonts, cursor, and the static wallpaper across the new graphical surface (GTK, Qt, bar, lock, launcher, notifications, compositor colors).
|
||||
Stylix targets for the existing terminal tools (nvim, tmux, fish) are left off so their established hand-themes stand unchanged.
|
||||
The terminal is themed by Stylix where its target is mature, with hand-written Nord as a fallback otherwise.
|
||||
This decision is highly reversible: Stylix toggles per target, so the graphical layer can migrate toward or away from manual theming later at low cost.
|
||||
|
||||
**Components**
|
||||
|
||||
- Terminal: Ghostty.
|
||||
- Status bar: Waybar, showing workspaces with per-application icons plus clock, battery, network, audio, MPRIS media controls, and a do-not-disturb toggle.
|
||||
No overview/exposé plugin is used; the workspace indicators are sufficient.
|
||||
- Launcher: rofi (the Wayland fork), combining application-run, binary-run, and window-switch modes into one prompt, plus math-evaluation and emoji modes.
|
||||
It is reused as the dmenu-style frontend for clipboard history and a power menu, so one tool serves several jobs.
|
||||
- Notifications: mako, with do-not-disturb and history recall.
|
||||
Media controls and the do-not-disturb toggle live in the bar rather than in a separate notification center.
|
||||
- Lock and idle: hyprlock and hypridle.
|
||||
hyprlock uses the compositor session-lock protocol, so the lock surface is owned by the compositor and survives a locker crash.
|
||||
hypridle is wired for lock-on-idle, screen-off, lock-before-suspend, and lid-close, with tunable timeouts.
|
||||
- Wallpaper: a single static image set by Stylix.
|
||||
- Clipboard: cliphist with wl-clipboard, storing text and image history, picked through rofi.
|
||||
- Screenshots: grim and slurp wrapped by grimblast, routed through the satty annotation editor so that annotation is the default on region, active-window, and full-screen captures, each exporting to both the clipboard and a file.
|
||||
- Screen recording: wf-recorder, region-select-first and video-only (no audio), toggled by a keybind with a bar recording indicator and notifications.
|
||||
- Portals: the Hyprland desktop portal (screencast, screenshot, global shortcuts) plus the GTK portal (file dialogs and appearance).
|
||||
Screen sharing in applications depends on these regardless of whether the recorder is present.
|
||||
|
||||
**Keybinds**
|
||||
|
||||
The scheme ports the operator's KDE/i3 bindings but is expressed entirely in `hjkl` and letters, with no arrow or navigation-cluster keys, so it is fully reachable on a 60% keyboard.
|
||||
Bindings that were quick-tile-to-half on the floating KDE desktop are reclaimed for real tiling actions, because that gesture is meaningless in an automatic tiler.
|
||||
|
||||
| Action | Bind |
|
||||
|---|---|
|
||||
| Switch to workspace N | `Super`+`1`..`9` |
|
||||
| Move window to workspace N | `Super`+`Shift`+`1`..`9` |
|
||||
| Move focus | `Super`+`h`/`j`/`k`/`l` |
|
||||
| Move window in layout | `Super`+`Shift`+`h`/`j`/`k`/`l` |
|
||||
| Resize | `Super`+`Alt`+`h`/`j`/`k`/`l` |
|
||||
| Terminal | `Super`+`Return` |
|
||||
| Launcher | `Super`+`R` |
|
||||
| Lock | `Super`+`X` |
|
||||
| Toggle floating | `Super`+`Space` |
|
||||
| Fullscreen | `Super`+`F` |
|
||||
| Toggle split | `Super`+`T` |
|
||||
| Close window | `Super`+`Shift`+`Q` |
|
||||
| Force-kill | `Super`+`Ctrl`+`Q` |
|
||||
| Screenshot region / window / full (to satty) | `Super`+`L` / `Super`+`Shift`+`L` / `Super`+`Ctrl`+`L` |
|
||||
| Clipboard history | `Super`+`Shift`+`V` |
|
||||
| Record toggle (region first) | `Super`+`Shift`+`R` |
|
||||
| Volume / brightness / media | `XF86` hardware keys |
|
||||
|
||||
**Input**
|
||||
|
||||
- US-only keyboard layout, with no layout switcher.
|
||||
- Caps mapped to Escape, with Shift+Caps still producing a real CapsLock.
|
||||
- Key-repeat tuned snappy (a short delay before repeat begins, a fast repeat rate).
|
||||
- Touchpad with tap-to-click, natural scroll, and disable-while-typing.
|
||||
- Mouse with flat acceleration.
|
||||
|
||||
**Feel**
|
||||
|
||||
- Tasteful, subtle animations with modest rounding and small gaps.
|
||||
- Blur is off on the laptop, where it is the single biggest battery cost; it remains a knob a Host such as zeus can enable.
|
||||
|
||||
**Module structure**
|
||||
|
||||
- The desktop is a set of granular, single-purpose Modules grouped under a desktop directory, each independently toggleable so pieces stay reusable and the files stay focused.
|
||||
The tightly coupled Hyprland-native pieces (compositor, lock, idle) are grouped in a subdirectory within that group.
|
||||
- An explicit aggregator Module turns the desktop on.
|
||||
Guarded by its own enable, it sets each piece's enable at default priority so that a Host enables the whole desktop with one flag while retaining the ability to override any single piece.
|
||||
The aggregator hand-lists the pieces it enables rather than scanning the directory, so a newly added file stays inert until deliberately added to that list; this keeps the whole desktop legible from one file.
|
||||
- The desktop's enable options are namespaced under a single desktop group, so a Host's checklist gains one desktop entry and the aggregator itself reads as the sub-checklist of what that entry means.
|
||||
- This uses the existing recursive Auto-loader as-is: every file in the group is imported unconditionally (inert until enabled), so no Skeleton change is needed.
|
||||
The aggregator/namespaced-group pattern is a deliberate evolution of the flat Module plus Host-as-checklist convention (see Further Notes).
|
||||
|
||||
## Testing Decisions
|
||||
|
||||
- A good test asserts externally-observable evaluation and build success of the whole Host, not the internals of any individual Module.
|
||||
This mirrors the laptop MVI's stance, where the config-merge model makes the whole-Host build the meaningful unit and the highest available seam.
|
||||
- Primary seam (required, existing): the neogaia Host evaluates and its system toplevel builds under the flake check.
|
||||
Building the toplevel drives the Auto-loader discovering the new Modules, the aggregator fan-out, home-manager integration, the full Stylix wiring, the Hyprland program integration, and package availability, surfacing nearly all config-authoring errors short of rendering a frame.
|
||||
- Cheap targeted checks: evaluate specific configuration paths to confirm the desktop aggregator fans out, the Stylix scheme resolves to Nord, and Hyprland is enabled, reusing the repo's existing lightweight eval-probe pattern.
|
||||
- No Module-level unit tests are added; there is no seam below the whole-Host build worth testing here, and the prior art is the laptop MVI's build-the-toplevel check.
|
||||
- The genuine end-to-end confirmation is manual and irreducible: switch the configuration on neogaia, log in through the greeter, and exercise the live session (keybinds, lock, screenshot, clipboard, launcher).
|
||||
A graphical session cannot self-test headless, but unlike the reimage this is reversible, so verification is done by living in it with a safety net (roll back a generation, drop to a console, or disable the desktop flag).
|
||||
- VM-based graphical CI (boot assertions under a NixOS test) is deferred, consistent with the laptop MVI's stated stance.
|
||||
|
||||
## Out of Scope
|
||||
|
||||
- The gaming stack (Steam, gamescope, Proton, replay-buffer recording, OBS), which belongs to a future zeus-oriented Module.
|
||||
- The zeus Host itself, and multi-monitor/output configuration, since there is no second display to design against yet; monitor focus and move bindings are deferred with it.
|
||||
- Any non-Hyprland desktop; KDE, Sway, and niri were considered and rejected.
|
||||
- The upstream Hyprland flake and compositor plugins, including workspace-overview/exposé, which are explicitly not adopted this pass.
|
||||
- Migrating the existing nvim, tmux, and fish themes into Stylix; they stay hand-themed.
|
||||
- Dynamic, animated, or video wallpaper, and wallpaper cycling.
|
||||
- Screen-recording audio and a full-screen recording variant.
|
||||
- A slide-out notification-center panel and a batteries-included desktop panel; both were considered and rejected in favor of the minimal, Stylix-coherent stack.
|
||||
- Any change to the Skeleton or the Auto-loader; the structure uses them unchanged.
|
||||
- Any secret wiring.
|
||||
|
||||
## Further Notes
|
||||
|
||||
- The compositor decision (Hyprland over Sway, KDE, and niri) is hard to reverse and the result of a real trade-off, so it should be recorded as an ADR.
|
||||
- The aggregator plus namespaced-group Module pattern is a deliberate departure from the flat, Host-as-full-checklist convention, and is worth recording as a short ADR or a conventions note so its intent is not relearned.
|
||||
- The laptop MVI's out-of-scope line attributing Nvidia to zeus is factually wrong: zeus runs an AMD GPU, and Raichu (a server with no desktop) is the only Nvidia machine.
|
||||
The laptop MVI is a historical document and is left unchanged, so the accurate fact is recorded here and in ADR 0003 instead.
|
||||
- The ported keybind scheme is derived from the operator's prior KDE Plasma 6 configuration (nine numbered desktops, `Super`+number bindings, a terminal on `Super`+`Return`, Caps mapped to Escape, and custom per-desktop tile layouts), which lives in this repo's git history under the old reference config.
|
||||
- Neogaia is Intel and zeus is AMD, both of which drive Wayland without driver caveats; this is what lets one keyboard-first compositor serve both Hosts rather than forcing a per-Host divergence.
|
||||
67
.claude/tasks/0021-desktop-group-and-hyprland-session.md
Normal file
67
.claude/tasks/0021-desktop-group-and-hyprland-session.md
Normal file
@@ -0,0 +1,67 @@
|
||||
---
|
||||
spec: hyprland-desktop
|
||||
---
|
||||
|
||||
## What to build
|
||||
|
||||
The tracer bullet for the whole desktop: a keyboard-driven Hyprland session that neogaia can log into and open a terminal in.
|
||||
|
||||
Create the `modules/desktop/` group with an explicit aggregator, guarded by its own `modules.desktop.enable`, that hand-lists and turns on each piece at default priority so a host enables the whole desktop with one flag yet can still override any single piece.
|
||||
Namespace every desktop enable under one desktop group so a host's checklist gains one entry.
|
||||
Place the tightly coupled Hyprland-native pieces (starting with the compositor) in a subdirectory within the group.
|
||||
|
||||
Wire Hyprland from nixpkgs: the NixOS program integration owns the session and polkit, home-manager owns the user configuration, and both share one Hyprland package so there is never a version split.
|
||||
The session is launched through the universal Wayland session manager from the greeter.
|
||||
Login is greetd with the tuigreet text greeter, mouse-free and lightweight.
|
||||
|
||||
Port the operator's KDE/i3 keybinds expressed entirely in `hjkl` and letters with no arrow or navigation-cluster keys: numbered-workspace switch and move, focus and window movement, resize, terminal, floating, fullscreen, split, close, and force-kill, per the spec's keybind table.
|
||||
Tune input: US-only layout with no switcher, Caps mapped to Escape with Shift+Caps still producing CapsLock, snappy key-repeat, touchpad tap-to-click plus natural scroll plus disable-while-typing, and flat mouse acceleration.
|
||||
Set the feel: subtle animations, modest rounding, small gaps, and blur off (left as a knob a host such as zeus can enable).
|
||||
Install Ghostty as the terminal on `Super+Return`.
|
||||
|
||||
Enable the desktop on neogaia.
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- [x] `modules/desktop/` exists with an aggregator exposing `modules.desktop.enable` that hand-lists and enables its pieces at default priority, each piece independently overridable.
|
||||
- [x] Desktop enable options are namespaced under a single desktop group; the Hyprland-native compositor lives in a subdirectory of the group.
|
||||
- [x] Hyprland is sourced from nixpkgs; the NixOS integration and the home-manager user config share one Hyprland package.
|
||||
- [x] The session launches through the universal Wayland session manager from a greetd/tuigreet text login.
|
||||
- [x] Keybinds match the spec's table, using only `hjkl`, letters, and number rows — no arrow or navigation-cluster keys.
|
||||
- [x] Input is tuned: US-only layout, Caps→Escape (Shift+Caps = CapsLock), snappy key-repeat, touchpad tap-to-click + natural scroll + disable-while-typing, flat mouse acceleration.
|
||||
- [x] Animations, rounding, and small gaps are on; blur is off and remains host-overridable.
|
||||
- [x] Ghostty opens on `Super+Return`.
|
||||
- [x] neogaia enables `modules.desktop` and builds green under `nix flake check`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- **Keybind scope.**
|
||||
This task ports only the enumerated compositor-native bindings (workspace switch/move, focus, window move, resize, terminal, floating, fullscreen, split, close, force-kill).
|
||||
The spec table's launcher, lock, screenshot, clipboard, and record bindings depend on tools installed by later tasks (0024–0029), so each of those tasks adds its own binding rather than this one binding to a missing binary.
|
||||
Force-kill uses Hyprland's `forcekillactive` dispatcher, keeping it keyboard-only.
|
||||
|
||||
- **Shared Hyprland package.**
|
||||
The NixOS `programs.hyprland` installs the single package and the portal system-wide, and the home-manager module sets `package = null` and `portalPackage = null` so it writes only the config against that one package.
|
||||
This is the "never a version split" guarantee, read as one package total rather than two identical derivations.
|
||||
|
||||
- **hyprlang, not Lua.**
|
||||
The home-manager `wayland.windowManager.hyprland` module now defaults `configType` to `"lua"` at `home.stateVersion` ≥ 26.05, which serialises `$mod`-style variables and INI `bind=` strings into invalid Lua without failing the build.
|
||||
The module pins `configType = "hyprlang"` to emit the native `hyprland.conf`.
|
||||
Recorded as a gotcha in `CLAUDE.md`.
|
||||
|
||||
- **Greeter session command.**
|
||||
greetd's `default_session` runs `uwsm start -e -D Hyprland hyprland.desktop`, mirroring the Exec line of the uwsm session the Hyprland package itself ships, so the session goes through the universal Wayland session manager deterministically.
|
||||
|
||||
- **Terminal: Alacritty, not Ghostty.**
|
||||
The spec named Ghostty, but on neogaia's integrated graphics its GTK4 window construction made every launch feel sluggish (~440 ms to map, versus a lightweight terminal's near-instant open), which a head-to-head comparison confirmed.
|
||||
The terminal is therefore Alacritty, whose OpenGL renderer opens fast on the iGPU.
|
||||
The choice is easily reversible per host, so a capable host such as zeus could still adopt Ghostty later.
|
||||
|
||||
- **Dropped from the plan.**
|
||||
Mouse drag-to-move and drag-to-resize (`bindm`) were removed: they fall outside the task's enumerated keyboard bindings, and `resizeactive`/`movewindow` already cover floating windows from the keyboard.
|
||||
Hardware media/brightness keys (the spec table's `XF86` row) are likewise deferred, since they depend on audio and backlight tooling not yet in scope.
|
||||
|
||||
- **Added beyond the plan.**
|
||||
`Super+Shift+T` toggles the tiling strategy between the dwindle and master layouts, added at the operator's request during review.
|
||||
Neither a dispatcher nor a keyword flips the layout on its own, so a small script reads the current layout and sets the other through `hyprctl keyword`.
|
||||
It is not in the spec keybind table.
|
||||
20
.claude/tasks/0022-stylix-nord-theming.md
Normal file
20
.claude/tasks/0022-stylix-nord-theming.md
Normal file
@@ -0,0 +1,20 @@
|
||||
---
|
||||
spec: hyprland-desktop
|
||||
blocked-by: 0021-desktop-group-and-hyprland-session
|
||||
---
|
||||
|
||||
## What to build
|
||||
|
||||
Theme the whole new graphical layer Nord from a single source, and set the wallpaper.
|
||||
|
||||
Add Stylix as a flake input and a desktop theming module that drives colors, system fonts, and cursor from one Nord base16 scheme across the graphical surface (GTK, Qt, and the compositor colors), plus a single static Nord wallpaper set by Stylix.
|
||||
Scope Stylix to the graphical layer only: leave its targets for the existing terminal tools (nvim, tmux, fish) off so their established hand-themes stand unchanged.
|
||||
The theming is reversible per target, so individual surfaces can migrate toward or away from manual theming later.
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- [ ] Stylix is a flake input, wired into the host build.
|
||||
- [ ] A desktop theming module resolves a single Nord base16 scheme and applies it to GTK, Qt, cursor, and system fonts.
|
||||
- [ ] A single static Nord wallpaper is set by Stylix; no dynamic, animated, or cycling wallpaper.
|
||||
- [ ] Stylix targets for nvim, tmux, and fish are off, leaving their existing themes untouched.
|
||||
- [ ] neogaia builds green under `nix flake check`, and an eval probe confirms the resolved scheme is Nord.
|
||||
19
.claude/tasks/0023-waybar-status-bar.md
Normal file
19
.claude/tasks/0023-waybar-status-bar.md
Normal file
@@ -0,0 +1,19 @@
|
||||
---
|
||||
spec: hyprland-desktop
|
||||
blocked-by: 0021-desktop-group-and-hyprland-session
|
||||
---
|
||||
|
||||
## What to build
|
||||
|
||||
A Waybar status bar that reads system state at a glance.
|
||||
|
||||
Add a Waybar module to the desktop group, enabled through the aggregator, showing workspaces with per-application icons plus a clock, battery, network, audio, MPRIS media controls, and a do-not-disturb toggle.
|
||||
No overview/exposé plugin: the workspace indicators are sufficient.
|
||||
The do-not-disturb toggle and media controls live in the bar rather than in a separate notification center.
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- [ ] A Waybar module exists in the desktop group and is enabled by the aggregator.
|
||||
- [ ] The bar shows workspaces with per-application icons, a clock, battery, network, audio, MPRIS media controls, and a do-not-disturb toggle.
|
||||
- [ ] No overview/exposé plugin is used.
|
||||
- [ ] neogaia builds green under `nix flake check`.
|
||||
20
.claude/tasks/0024-rofi-launcher.md
Normal file
20
.claude/tasks/0024-rofi-launcher.md
Normal file
@@ -0,0 +1,20 @@
|
||||
---
|
||||
spec: hyprland-desktop
|
||||
blocked-by: 0021-desktop-group-and-hyprland-session
|
||||
---
|
||||
|
||||
## What to build
|
||||
|
||||
A search-everything launcher, so one keybound tool handles launching and utility menus.
|
||||
|
||||
Add a rofi (Wayland fork) module to the desktop group, enabled through the aggregator, combining application-run, binary-run, and window-switch into one prompt, plus math-evaluation and emoji modes.
|
||||
Bind it on `Super+R`.
|
||||
Structure it so it is reusable as the dmenu-style frontend for later utility menus (clipboard history, power menu), and provide a power menu that uses it.
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- [ ] A rofi module exists in the desktop group and is enabled by the aggregator.
|
||||
- [ ] rofi combines application-run, binary-run, and window-switch modes, plus math evaluation and emoji.
|
||||
- [ ] rofi opens on `Super+R`.
|
||||
- [ ] rofi is usable as a dmenu-style frontend for utility menus, and a power menu is provided through it.
|
||||
- [ ] neogaia builds green under `nix flake check`.
|
||||
18
.claude/tasks/0025-mako-notifications.md
Normal file
18
.claude/tasks/0025-mako-notifications.md
Normal file
@@ -0,0 +1,18 @@
|
||||
---
|
||||
spec: hyprland-desktop
|
||||
blocked-by: 0021-desktop-group-and-hyprland-session
|
||||
---
|
||||
|
||||
## What to build
|
||||
|
||||
Notification toasts with do-not-disturb and history recall, so missed notifications can be retrieved.
|
||||
|
||||
Add a mako module to the desktop group, enabled through the aggregator, rendering notification toasts with a do-not-disturb mode and history recall.
|
||||
The do-not-disturb toggle and media controls live in the bar, not in a separate notification-center panel.
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- [ ] A mako module exists in the desktop group and is enabled by the aggregator.
|
||||
- [ ] Notification toasts appear, with do-not-disturb and history recall.
|
||||
- [ ] No separate slide-out notification-center panel is added.
|
||||
- [ ] neogaia builds green under `nix flake check`.
|
||||
21
.claude/tasks/0026-hyprlock-and-hypridle.md
Normal file
21
.claude/tasks/0026-hyprlock-and-hypridle.md
Normal file
@@ -0,0 +1,21 @@
|
||||
---
|
||||
spec: hyprland-desktop
|
||||
blocked-by: 0021-desktop-group-and-hyprland-session
|
||||
---
|
||||
|
||||
## What to build
|
||||
|
||||
A secure lock screen and idle management, so going idle, suspending, or closing the lid always lands at a locked screen.
|
||||
|
||||
Add hyprlock and hypridle modules in the Hyprland-native subdirectory of the desktop group, enabled through the aggregator.
|
||||
hyprlock uses the compositor session-lock protocol so the lock surface is owned by the compositor and survives a locker crash.
|
||||
hypridle is wired for lock-on-idle, screen-off, lock-before-suspend, and lid-close, with tunable timeouts.
|
||||
Bind lock on `Super+X`.
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- [ ] hyprlock and hypridle modules exist in the Hyprland-native subdirectory and are enabled by the aggregator.
|
||||
- [ ] hyprlock uses the compositor session-lock protocol.
|
||||
- [ ] hypridle triggers lock-on-idle, screen-off, lock-before-suspend, and lid-close, with tunable timeouts.
|
||||
- [ ] Lock is bound on `Super+X`.
|
||||
- [ ] neogaia builds green under `nix flake check`.
|
||||
18
.claude/tasks/0027-cliphist-clipboard-history.md
Normal file
18
.claude/tasks/0027-cliphist-clipboard-history.md
Normal file
@@ -0,0 +1,18 @@
|
||||
---
|
||||
spec: hyprland-desktop
|
||||
blocked-by: 0024-rofi-launcher
|
||||
---
|
||||
|
||||
## What to build
|
||||
|
||||
Clipboard history picked entirely by keyboard.
|
||||
|
||||
Add a cliphist module (with wl-clipboard) to the desktop group, enabled through the aggregator, storing both text and image history and picked through rofi.
|
||||
Bind the picker on `Super+Shift+V`.
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- [ ] A cliphist module (with wl-clipboard) exists in the desktop group and is enabled by the aggregator.
|
||||
- [ ] Text and image copies are recorded to history.
|
||||
- [ ] The history is picked through rofi and bound on `Super+Shift+V`.
|
||||
- [ ] neogaia builds green under `nix flake check`.
|
||||
19
.claude/tasks/0028-screenshot-capture.md
Normal file
19
.claude/tasks/0028-screenshot-capture.md
Normal file
@@ -0,0 +1,19 @@
|
||||
---
|
||||
spec: hyprland-desktop
|
||||
blocked-by: 0021-desktop-group-and-hyprland-session
|
||||
---
|
||||
|
||||
## What to build
|
||||
|
||||
Keyboard-driven screenshots that open in an annotation editor by default and land in both the clipboard and a file.
|
||||
|
||||
Add a screenshot module to the desktop group, enabled through the aggregator, using grim and slurp wrapped by grimblast and routed through the satty annotation editor so annotation is the default.
|
||||
Cover region, active-window, and full-screen captures, each exporting to both the clipboard and a file.
|
||||
Bind region on `Super+L`, active window on `Super+Shift+L`, and full screen on `Super+Ctrl+L`.
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- [ ] A screenshot module exists in the desktop group and is enabled by the aggregator.
|
||||
- [ ] Region, active-window, and full-screen captures work, each opening in satty and exporting to both clipboard and file.
|
||||
- [ ] Captures are bound on `Super+L`, `Super+Shift+L`, and `Super+Ctrl+L`.
|
||||
- [ ] neogaia builds green under `nix flake check`.
|
||||
20
.claude/tasks/0029-screen-recording.md
Normal file
20
.claude/tasks/0029-screen-recording.md
Normal file
@@ -0,0 +1,20 @@
|
||||
---
|
||||
spec: hyprland-desktop
|
||||
blocked-by: [0023-waybar-status-bar, 0025-mako-notifications]
|
||||
---
|
||||
|
||||
## What to build
|
||||
|
||||
A keybound screen recorder that selects a region and then toggles recording.
|
||||
|
||||
Add a wf-recorder module to the desktop group, enabled through the aggregator, that selects a region first and then toggles video-only recording (no audio), bound on `Super+Shift+R`.
|
||||
Surface a recording indicator in the Waybar bar and notification toasts on start and stop.
|
||||
No audio capture and no full-screen recording variant.
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- [ ] A wf-recorder module exists in the desktop group and is enabled by the aggregator.
|
||||
- [ ] The recorder selects a region first, then toggles video-only recording on `Super+Shift+R`.
|
||||
- [ ] A recording indicator appears in the bar, and notifications fire on start and stop.
|
||||
- [ ] No audio is captured and no full-screen variant is provided.
|
||||
- [ ] neogaia builds green under `nix flake check`.
|
||||
18
.claude/tasks/0030-desktop-portals.md
Normal file
18
.claude/tasks/0030-desktop-portals.md
Normal file
@@ -0,0 +1,18 @@
|
||||
---
|
||||
spec: hyprland-desktop
|
||||
blocked-by: 0021-desktop-group-and-hyprland-session
|
||||
---
|
||||
|
||||
## What to build
|
||||
|
||||
Screen sharing that works inside applications, so video calls and browser screen-share function.
|
||||
|
||||
Add a portals module to the desktop group, enabled through the aggregator, wiring the Hyprland desktop portal (screencast, screenshot, global shortcuts) plus the GTK portal (file dialogs and appearance).
|
||||
In-app screen sharing depends on these regardless of whether the recorder is present.
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- [ ] A portals module exists in the desktop group and is enabled by the aggregator.
|
||||
- [ ] The Hyprland desktop portal (screencast, screenshot, global shortcuts) and the GTK portal (file dialogs, appearance) are both configured.
|
||||
- [ ] In-app screen sharing is available independent of the screen recorder.
|
||||
- [ ] neogaia builds green under `nix flake check`.
|
||||
12
CLAUDE.md
12
CLAUDE.md
@@ -94,3 +94,15 @@ The domain model (Host, Module, Skeleton, Auto-loader, Enable convention, overla
|
||||
The interpolated key makes it a dynamic attribute, which nix will not merge the way it merges static paths.
|
||||
Nest both under a single `home-manager.users.${user} = { ... }`.
|
||||
- **Verifying a nixvim change headless:** `programs.nixvim.build.package`'s wrapper has **no `-u`**, so running `$OUT/bin/nvim` loads the caller's `~/.config/nvim` (the dev host's real config), *not* the built config — silently. To exercise the built config, launch with `-u "$(nix build --no-link --print-out-paths .#…programs.nixvim.build.initFile)"` and a scratch `HOME`/`XDG_CONFIG_HOME`. `conceallevel` is window-local: set it with `opt_local`/`vim.wo`, never `vim.bo[buf]` (which errors).
|
||||
- Host GPUs: `neogaia` is Intel and `zeus` (the desktop) is **AMD**.
|
||||
`raichu`, a server with no desktop, is the only Nvidia machine.
|
||||
`laptop-mvi.md`'s out-of-scope line calls zeus Nvidia, but that is stale and the document is kept historical and unchanged, so do not infer any host's GPU from it.
|
||||
The corrected fact lives in ADR 0003 and the `hyprland-desktop` spec.
|
||||
- The home-manager `wayland.windowManager.hyprland` module defaults `configType` to `"lua"` at `home.stateVersion` >= 26.05, writing `hyprland.lua` through an `hl.*` Lua API instead of the native `hyprland.conf`.
|
||||
The Lua backend mangles `$mod`-style variables and INI `bind=` strings into invalid Lua (`hl.$mod("SUPER")`), and does not fail the build, since the config is only text.
|
||||
Set `configType = "hyprlang"` to get the native `hyprland.conf` whose variable and bind syntax the usual settings are written in.
|
||||
Render the file to check which format is in effect: `nix build --print-out-paths .#nixosConfigurations.<host>.config.home-manager.users.<user>.xdg.configFile.\"hypr/hyprland.conf\".source` (only the enabled `configType`'s key exists).
|
||||
- An invalid Hyprland dispatcher or config-option name never fails the nix build, since `hyprland.conf` is only text, so it surfaces only when the compositor loads the file at login.
|
||||
The build/render check is therefore blind to it, and the real test is a running session (or reading `~/.config/hypr/hyprland.conf` against the running package's own names).
|
||||
Two that bit on 0.55.4: the dwindle split actions `togglesplit`, `swapsplit`, and `pseudo` are layout messages reached through the `layoutmsg` dispatcher (`bind = $mod, T, layoutmsg, togglesplit`), not top-level dispatchers, and the old `dwindle:pseudotile` option is gone.
|
||||
Confirm names against the pinned package rather than the wiki, whose "latest" drifts from it.
|
||||
|
||||
@@ -52,6 +52,8 @@
|
||||
modules.gitea-axi.enable = true;
|
||||
modules.pi.enable = true;
|
||||
|
||||
modules.desktop.enable = true;
|
||||
|
||||
time.timeZone = "America/New_York";
|
||||
i18n.defaultLocale = "en_GB.UTF-8";
|
||||
}
|
||||
|
||||
16
modules/desktop/desktop.nix
Normal file
16
modules/desktop/desktop.nix
Normal file
@@ -0,0 +1,16 @@
|
||||
{ config, lib, ... }:
|
||||
# The desktop aggregator: one flag brings up the whole graphical session.
|
||||
let
|
||||
cfg = config.modules.desktop;
|
||||
in
|
||||
{
|
||||
options.modules.desktop.enable = lib.mkEnableOption "the keyboard-driven Hyprland desktop";
|
||||
|
||||
# Each piece is turned on at default priority, so a host can still override
|
||||
# any one of them while the single flag above enables the whole desktop.
|
||||
config = lib.mkIf cfg.enable {
|
||||
modules.desktop.hyprland.enable = lib.mkDefault true;
|
||||
modules.desktop.login.enable = lib.mkDefault true;
|
||||
modules.desktop.terminal.enable = lib.mkDefault true;
|
||||
};
|
||||
}
|
||||
143
modules/desktop/hyprland/hyprland.nix
Normal file
143
modules/desktop/hyprland/hyprland.nix
Normal file
@@ -0,0 +1,143 @@
|
||||
{
|
||||
config,
|
||||
lib,
|
||||
pkgs,
|
||||
...
|
||||
}:
|
||||
# The Hyprland compositor, sourced from nixpkgs.
|
||||
let
|
||||
cfg = config.modules.desktop.hyprland;
|
||||
user = config.user.name;
|
||||
|
||||
# Numbered-workspace switch and move for 1..9, the operator's i3 muscle memory.
|
||||
workspaceBinds = lib.concatMap (n: [
|
||||
"$mod, ${toString n}, workspace, ${toString n}"
|
||||
"$mod SHIFT, ${toString n}, movetoworkspace, ${toString n}"
|
||||
]) (lib.range 1 9);
|
||||
|
||||
# Flip the tiling strategy between the two built-in layouts, since neither a
|
||||
# dispatcher nor a keyword toggles it on its own.
|
||||
toggleLayout = pkgs.writeShellScript "hypr-toggle-layout" ''
|
||||
if [ "$(hyprctl getoption -j general:layout | ${pkgs.jq}/bin/jq -r .str)" = dwindle ]; then
|
||||
hyprctl keyword general:layout master
|
||||
else
|
||||
hyprctl keyword general:layout dwindle
|
||||
fi
|
||||
'';
|
||||
in
|
||||
{
|
||||
options.modules.desktop.hyprland = {
|
||||
enable = lib.mkEnableOption "the Hyprland compositor";
|
||||
|
||||
blur = lib.mkEnableOption ''
|
||||
window blur. Off by default as the single biggest battery cost on a
|
||||
laptop, left on for a host with the headroom to spend it'';
|
||||
};
|
||||
|
||||
config = lib.mkIf cfg.enable {
|
||||
# This program integration owns the session, portals, and polkit, launched
|
||||
# through the universal Wayland session manager.
|
||||
programs.hyprland = {
|
||||
enable = true;
|
||||
withUWSM = true;
|
||||
};
|
||||
|
||||
home-manager.users.${user}.wayland.windowManager.hyprland = {
|
||||
enable = true;
|
||||
|
||||
# One package drives the whole session, so there is never a version split.
|
||||
# The program integration above installs it and the portal, leaving home-
|
||||
# manager to write only the config.
|
||||
package = null;
|
||||
portalPackage = null;
|
||||
|
||||
# uwsm owns the systemd graphical-session targets.
|
||||
systemd.enable = false;
|
||||
|
||||
# Write the native hyprlang hyprland.conf, whose variable and bind syntax
|
||||
# the settings below are expressed in.
|
||||
configType = "hyprlang";
|
||||
|
||||
settings = {
|
||||
"$mod" = "SUPER";
|
||||
"$terminal" = "alacritty";
|
||||
|
||||
input = {
|
||||
kb_layout = "us";
|
||||
# Caps is a second Escape.
|
||||
# Shift+Caps still toggles a real CapsLock.
|
||||
kb_options = "caps:escape_shifted_capslock";
|
||||
# Snappy: a short delay before repeat begins, then a fast repeat rate.
|
||||
repeat_delay = 250;
|
||||
repeat_rate = 45;
|
||||
accel_profile = "flat";
|
||||
touchpad = {
|
||||
natural_scroll = true;
|
||||
tap-to-click = true;
|
||||
disable_while_typing = true;
|
||||
};
|
||||
};
|
||||
|
||||
general = {
|
||||
gaps_in = 4;
|
||||
gaps_out = 8;
|
||||
border_size = 2;
|
||||
layout = "dwindle";
|
||||
};
|
||||
|
||||
decoration = {
|
||||
rounding = 6;
|
||||
blur.enabled = cfg.blur;
|
||||
};
|
||||
|
||||
animations = {
|
||||
enabled = true;
|
||||
bezier = [ "ease, 0.25, 0.1, 0.25, 1.0" ];
|
||||
# Durations are in centiseconds.
|
||||
# Short values keep the motion subtle.
|
||||
animation = [
|
||||
"windows, 1, 3, ease"
|
||||
"fade, 1, 3, ease"
|
||||
"workspaces, 1, 3, ease"
|
||||
"border, 1, 3, ease"
|
||||
];
|
||||
};
|
||||
|
||||
dwindle = {
|
||||
preserve_split = true;
|
||||
};
|
||||
|
||||
bind = [
|
||||
"$mod, Return, exec, $terminal"
|
||||
|
||||
# Move focus.
|
||||
"$mod, H, movefocus, l"
|
||||
"$mod, J, movefocus, d"
|
||||
"$mod, K, movefocus, u"
|
||||
"$mod, L, movefocus, r"
|
||||
|
||||
# Move the window within the layout.
|
||||
"$mod SHIFT, H, movewindow, l"
|
||||
"$mod SHIFT, J, movewindow, d"
|
||||
"$mod SHIFT, K, movewindow, u"
|
||||
"$mod SHIFT, L, movewindow, r"
|
||||
|
||||
# Resize the active window.
|
||||
"$mod ALT, H, resizeactive, -40 0"
|
||||
"$mod ALT, J, resizeactive, 0 40"
|
||||
"$mod ALT, K, resizeactive, 0 -40"
|
||||
"$mod ALT, L, resizeactive, 40 0"
|
||||
|
||||
"$mod, Space, togglefloating,"
|
||||
"$mod, F, fullscreen,"
|
||||
# togglesplit is a dwindle layout message, reached through layoutmsg.
|
||||
"$mod, T, layoutmsg, togglesplit"
|
||||
"$mod SHIFT, T, exec, ${toggleLayout}"
|
||||
"$mod SHIFT, Q, killactive,"
|
||||
"$mod CTRL, Q, forcekillactive,"
|
||||
]
|
||||
++ workspaceBinds;
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
23
modules/desktop/login.nix
Normal file
23
modules/desktop/login.nix
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
config,
|
||||
lib,
|
||||
pkgs,
|
||||
...
|
||||
}:
|
||||
# Text login: greetd running the tuigreet greeter, which starts the session
|
||||
# through the universal Wayland session manager.
|
||||
let
|
||||
cfg = config.modules.desktop.login;
|
||||
in
|
||||
{
|
||||
options.modules.desktop.login.enable = lib.mkEnableOption "greetd with the tuigreet text greeter";
|
||||
|
||||
config = lib.mkIf cfg.enable {
|
||||
services.greetd = {
|
||||
enable = true;
|
||||
settings.default_session.command =
|
||||
"${lib.getExe pkgs.tuigreet} --time --remember "
|
||||
+ "--cmd 'uwsm start -e -D Hyprland hyprland.desktop'";
|
||||
};
|
||||
};
|
||||
}
|
||||
19
modules/desktop/terminal.nix
Normal file
19
modules/desktop/terminal.nix
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
config,
|
||||
lib,
|
||||
pkgs,
|
||||
...
|
||||
}:
|
||||
# Alacritty as the desktop terminal, a lightweight GPU renderer that opens
|
||||
# fast on the laptop's integrated graphics.
|
||||
let
|
||||
cfg = config.modules.desktop.terminal;
|
||||
user = config.user.name;
|
||||
in
|
||||
{
|
||||
options.modules.desktop.terminal.enable = lib.mkEnableOption "Alacritty as the desktop terminal";
|
||||
|
||||
config = lib.mkIf cfg.enable {
|
||||
home-manager.users.${user}.home.packages = [ pkgs.alacritty ];
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user