Imported from cernoh/dendritic (
modules/features/noctalia/AGENTS.md). Install upstream withnpx skills add cernoh/dendritic --skill noctalia. Copyright stays with the author.
noctalia — Noctalia desktop shell feature
Purpose
Noctalia v5 desktop shell: bars, panels, launcher, lock screen. Exposes flake.nixosModules.noctalia (system install + recommendedServices) and flake.homeManagerModules.noctalia (declarative settings into ~/.config/noctalia/). Owns the cernoh/terminal plugin and the ghostty-term helper it needs. Settings are per-host values.
Ownership
default.nix— NixOS/HM modules, theghostty-termpackage, the out-of-store plugin symlink, and the system-side brightness dependencies._ghostty-term.pkg.nix— derivation for the helper (single C translation unit againstpkgs.libghostty-vt).ghostty-term.c— PTY and libghostty-vt helper; owns the frame protocol.plugins/terminal/— thecernoh/terminalplugin:plugin.toml,service.luau,panel.luau,bar.luau,shortcut.luau.noctalia-full-config.toml— reference dump of the ASAHI live config (data, not a source of truth).
Local Contracts
- The feature owns monitor brightness. The NixOS module installs
ddcutilintoenvironment.systemPackagesand setshardware.i2c.enable, because the noctalia user service carries no shell PATH and DDC/CI needs/dev/i2c-*. A host turns the DDC/CI path on withbrightness.enable_ddcutil = truein its_noctalia-settings.nix; the kernel backlight interface covers internal panels only. Both hosts have ani2c-devkernel (=mon NIXPC, built in on ASAHI), so no kernel patch is needed. - The
noctaliainput tracks the upstreamcachixbranch and thenoctalia-greeterinput tracksmain; neither followsnixpkgs. Upstream publishes both packages tonoctalia.cachix.orgagainst its own locked nixpkgs, so a follow changes the store path and forces a local build (issue #179). - Plugin directories hold Luau and TOML only. The Noctalia plugin runtime has no foreign function interface, so a plugin cannot link a C library. Any native work goes in a separate derivation that the plugin spawns as a process.
ghostty-termis the bridge. It owns the pseudo-terminal and alibghostty-vtterminal, and writes one JSON frame per screen change on stdout.pkgs.libghostty-vtis a standalone package, separate frompkgs.ghostty; theghosttybuild ships only the sequence parsers under the same soname, and itsvt.hincludes headers it does not install. The helper is offered only wherelib.meta.availableOnreportspkgs.libghostty-vt(no x86_64-darwin build).- Frame protocol (one JSON object per stdout line).
service.luauis the only reader:{"t":"init","c":cols,"r":rows}— geometry, sent once at start.{"t":"f","s":seq,"c":cols,"r":rows,"dfg":"#rrggbb","dbg":"#rrggbb","L":[row,...]}— screen.Lholds one array per row; each row holds text runs of{"t":text,"f":"#rrggbb","b":true}.fappears only when a run differs fromdfg;bonly when bold.seqis monotonic, and the service drops a frame that is not newer.{"t":"exit"}— the shell exited.
- Command protocol (one command per line on the FIFO at
<pluginDataDir>/term.fifo):i<escaped text>— write text to the shell. Escapes:\n,\r,\t,\e,\\. A bare newline ends the command, so a newline meant for the shell travels as\n.k<key name>— send an encoded key event (seeKEY_TABLEinghostty-term.c). libghostty encodes it for the mode the running program selected, so the helper never hardcodes escape bytes.s<n>— scroll the viewport bynrows.z<COLS>x<ROWS>— resize.f— emit a frame now.q— quit.
- Frames are coalesced at 80 ms (
FRAME_MIN_MS). A build log dirties the screen on every write, and the panel rebuilds its whole tree per frame. A gated write stays pending, so the last update always arrives. - Entries exchange plain values only.
noctalia.statecopies values and forbids functions, so the panel posts work to therequestkey and bumpsrequest_rev. The counter is required: two identical requests in a row must both run. - Render within the panel API's limits. There is no canvas, no grid, and no per-cell background, and
ui.labelhas no per-span styling. The panel draws oneui.rowper terminal line and oneui.labelper colour run, in a gap-free row, so a monospace font keeps the columns aligned. Backgrounds, italics, and underlines are not transmitted because nothing can draw them. - Only declared chords arrive.
plugin.tomlcapture_keyslists what the panel forwards; Noctalia delivers only those, sends them toonKey(chord, pressed), and never sends a key it did not list.escapeis reserved for the panel-close action and cannot be captured, and super chords belong to the compositor. A focusedui.inputreceives printable keys first, so typing goes through the input andcapture_keyscarries arrows, tab, and control chords. - The plugin is symlinked out-of-store to
modules/features/noctalia/plugins/terminalin this checkout, so plugin edits are live without a rebuild. The path is hardcoded to~/.config/dendritic, so a worktree copy is not live.
Work Guidance
- Change the frame or command protocol: edit
ghostty-term.candservice.luautogether, and update the two contract lists above. - Add a key the panel may forward: add a row to
KEY_TABLEinghostty-term.c, then add the same chord tocapture_keysinplugin.toml. Use a chord name Noctalia documents as valid; an unknown name risks the manifest. - The panel reads frames; it must not talk to the helper. New work goes through the service's request channel.
- Keep
plugin.tomlplugin_apiat the lowest level that covers the features in use. The terminal plugin needs 21.
Verification
nix-instantiate --parse modules/features/noctalia/default.nixnix-instantiate --parse modules/features/noctalia/_ghostty-term.pkg.nixnix build .#ghostty-term— builds the helper with-Werror.nix eval --impure .#nixosConfigurations.NIXPC.config.home-manager.users.davr.home.packages— the helper reaches the host.- Brightness wiring:
nix eval --impure .#nixosConfigurations.<HOST>.config.hardware.i2c.enablereturns true, andnix eval --impure .#nixosConfigurations.<HOST>.config.boot.kernelModuleslistsi2c-dev. - Brightness config:
nix build --no-link --impure --expr 'let f = builtins.getFlake (toString ./.); in f.nixosConfigurations.NIXPC.config.home-manager.users.davr.xdg.configFile."noctalia/config.toml".source'— the build runsnoctalia config validate, so a badbrightnesskey fails here. - Live brightness on the host:
ddcutil detectlists the monitors,noctalia msg brightness-set <connector> 60changes one, and the Control Center Monitor tab shows a slider per monitor. nix flake check --impure
Child DOX Index
No children yet.