Skip to content

Latest commit

 

History

History
147 lines (121 loc) · 14.8 KB

AGENTS.md

File metadata and controls

147 lines (121 loc) · 14.8 KB

AGENTS.md

Project

  • Arch Linux dotfiles for Hyprland (Lua config) and Niri, deployed with GNU Stow. Arch is authoritative for packages, runtime and installation.
  • The shell is mantle/.config/mantle/, a Lua 5.4 config for the upstream Mantle engine. The engine ships the mantle binary, Lua API and capabilities; this repo holds only the Lua.
  • NixConfig/ is inactive. Ignore it unless asked; never use it as context or sync it with Arch.

Commands

  • Never run stow or start the shell (mantle, mantle -d) unless told to.
  • Saving a .lua reloads the running shell in place. A failed reload keeps the last scene and shows the error in the bar.
Command Use
just Gate before done: lua (parse + format), types (LuaLS on the root .luarc.json), mantle. just fmt formats
mantle check -c mantle/.config/mantle Run after every edit (just mantle). Evaluates the config with no Wayland and no subprocesses, then lays it out twice: every capability nil, then one sample push each (one-entry lists). Writes no state
mantle log [-f] Running shell output. Debug from Lua with log.debug(...), log.info(...), log.warn(...): stamped with time and level, unlike a bare print()
mantle set, toggle, call <name> Drive live state and action names like a keybind; changes the live UI
luac5.4 -p file.lua Syntax check. Plain luac is Lua 5.5
hyprctl repl '<lua>' Evaluate hl.* in the running Hyprland without a reload
shellcheck script.sh Lint Bash

Engine reference

Upstream checkout at /mnt/Work/0Coding/1Rust/mantle. Read it; never edit it from here unless asked to.

Question Read
What a config can declare and call docs/introduction.md, then docs/guide/, docs/nodes/, docs/surfaces/
Capability fields and actions lua-meta/mantle.lua (generated), docs/capabilities/<name>.md
Node and surface properties lua-meta/nodes.lua, lua-meta/surfaces.lua
Lua change or engine gap docs/roadmap.md. Flag a real engine gap instead of working around it
Terms (generation, named state, capability) docs/glossary.md; engine-internal ones in CONTEXT.md

Shell structure

mantle/.config/mantle/
  shell.lua     Entry: font chain, requires modules, returns the surface list
  config/       Tokens: theme.lua (wallpaper Material roles or a preset), colour_presets.lua, icons.lua, dev_tools.lua
  components/   Reusable widgets with no state of their own
  lib/          Node-free domain logic and state; ui_state owns navigation, disclosure owns panel resets
  modules/      bar/{indicators,panels}, global/, notification/, osd/, shell/panel_host.lua
  shaders/      `.frag` sources compiled by the engine at runtime: wallpaper transitions and the launcher rail
  templates/    App colour files `lib/app_colours.lua` fills from the wallpaper

Patterns

Topic Rule
Signals Pass the signal itself to keep a property live; :get() is a snapshot. Derive with :map, computed, delay, pulse
Hydration Capabilities read nil until the first push. Every map handles nil
Actions mantle.audio:set_volume(0.5) returns nothing. Observe state for the outcome
Keybinds Navigation state lives in lib/ui_state.lua; domain state stays with its owning lib/ module; action(name, fn) backs mantle call. A rename also updates hypr/.config/hypr/config/keybinds.lua and niri/.config/niri/config.kdl
Transient UI Bar panels share the always-shown bar surface, so reset_on_close never fires there: declare a disclosure with disclosure.state from lib/disclosure.lua, which resets on every panel open. A modal resets its state in ui_state.on_modal_close and lists its scrolls in modal { reset_on_close }
Persistence One persistent_table in lib/store.lua (~/.local/state/mantle/state.json). Add keys to its defaults
Processes process.run dies with the generation, process.detach outlives the shell, session_process survives reloads
Compositor Per-compositor commands go in lib/compositor.lua; behavior reads mantle.workspaces:get().compositor
Theme config/theme.lua tokens and config/icons.lua glyphs. Never hardcode colours, sizes or spacing
App colours lib/app_colours.lua writes templates/ and config/wallpaper_colours.lua (gitignored, reloads the shell) from the first screen's wallpaper. The wallpaper picker sets the source (colour_source: the wallpaper, or a config/colour_presets.lua scheme on Catppuccin's slot names), the wallpaper style (colour_style), dark or light (colour_scheme, also mantle call colours.mode) and the accent: per wallpaper among Score's candidates (colour_accents), per preset by name (colour_preset_accents). Never edit a generated file; add a role to theme.roles or a template. Once per host, outside the repo: btop's color_theme to mantle, Zed's theme light and dark to Mantle, Vesktop's enabledThemes to mantle.css (while it is closed), Zen's chrome/userChrome.css on GTK system colours plus zen.theme.use-system-colors, Thunderbird's theme to System
Panels Four bands, top down. panel_header first, its subtitle one live state line, trailing only a badge (count), panel_action_icon (verb) or toggle (the subject's power). Then one row of controls: panel_toggle_card switches or accent action_buttons; a radio anywhere is one segmented bar, never a row of tiles; every primary in a panel is that tinted glass, solid belongs to a modal's confirm, danger to the one action that ends something running. Then a flat body: section_header + panel_rows; "more" is always panel_row { expanded } in place, never a view swap; a panel_card only wraps a composite control (row + slider, status + meter, a table). Last, text buttons that conclude what the body showed. A modal is the same panel with a theme.font.xl masthead (on_close included), lg padding, md spacing and its one scroll area on an outlined card; a search modal's search bar is its masthead
Shaders A new shaders/wallpaper_<name>.frag shows in the picker as <name>; other .frag files stay out of it. Add a lib/wallpaper.lua row only for non-zero uniforms

Ponytail: lazy senior dev mode

Lazy means efficient, not careless. The best code is the code never written.

  • Output. Everything an agent writes (code, chat, comments, docs, commit messages) is to the point. No walls of text: tables first, bullets second, short prose last.
  • Diffs. Add the fewest lines possible and remove as many as possible. Delete dead, redundant and duplicated code around the change. The smallest diff in the wrong place is a second bug.
  • Root cause. Fix the cause, not the symptom. Grep every caller and fix the shared function once.
  • Boring. No new abstractions, dependencies, boilerplate or files unless required. Between similar-sized approaches, take the edge-case-correct one.
  • Ceilings. Mark a deliberate simplification (global lock, O(n²) scan, naive heuristic) with a ponytail: comment naming its ceiling and upgrade path.
  • Checks. Non-trivial logic gets one runnable check; trivial one-liners get none.
  • Full effort. Understanding the problem, trust-boundary validation, data loss, security, accessibility, real-hardware calibration and explicit requests are never lazy.

Before writing code, trace the real flow end to end, then stop at the first rung that holds:

  1. Does it need to exist? (YAGNI)
  2. Does the codebase already have it? Reuse it.
  3. Does the Lua API, a capability, the platform or an installed tool cover it? Use it.
  4. Can it be one line?
  5. Only then, the minimum code that works.

Notable files

Path Holds
mantle/.config/mantle/demo/ Effects demo run as its own instance (mantle -c), sharing config/, components/ and lib/ through symlinks. Not required by shell.lua, but its edits still reload the main shell
bin/.local/bin/arch-install.sh Full Arch install: Wolverine, Mentalist or a new host detected from its hardware
home/.profile XDG dirs, NVIDIA env, Wayland toolkit config, PATH
home/.stowrc Points stow at this repo and ~
.luarc.json One LuaLS config for the mantle, Hyprland, Neovim and mpv Lua
fish/.config/fish/conf.d/various.fish Custom fish functions
.local_secrets/ Gitignored secrets; .gitconfig symlinks here

The default terminal resolves through xdg-terminal-exec.

Lessons learned

  • Record only non-obvious failures likely to recur: what fails, why, what to do instead. Name a version only when the behavior is version-bound.
  • After edits, delete the stale comments, docs and entries here that the work exposed. Delete obsolete guidance rather than adding exceptions.

Mantle Lua

Trap Fix
The VM has no io, debug or FFI; os has only time, date, clock, getenv Shell out with process.run
require returns a second value, the path, so as the last item of a table constructor it adds a string entry Bind modules to locals first, or wrap the call: (require(...))
:map, computed, on_change, action and timer bodies have a 2.5 ms CPU budget (exceeded the 2.5ms CPU budget) Keep them cheap; build nodes once and let a map only pick between them
A node keeps its properties until a signal it read is written. os.time(), os.date() with no time, or a local read in a map stays at its last answer Derive time from mantle.system; keep changing values in a state
A signal nested in children, a list's source, a structural field (a panel's anchor) or a table a signal returns does not resolve; elsewhere in a property table it does Derive the whole value there
A node rejects unknown fields (node.expanded = x) only when its subtree resolves, so mantle check misses ones under a closed panel Export signals from a lib/ module, never on a node; check mantle log after opening the panel
visible = false keeps a frozen subtree Switch views through children
clip defaults to "none": children, shadows and transforms paint past a box Set clip = "box" where a box must cut: a fill hiding its label copy, a slide-in, an animated width
A row's align_h packs its children and also places the row in a stacking parent, so a fixed-width row centres itself Wrap it in a rect (or width = "fill") to centre only the children
Named state resets when its scalar seed changes Keep the seed stable
timer, action and on_change last one evaluation Expect them to re-register on every reload
fonts is read once at startup Restart the shell after editing it
mantle input takes surface-logical coordinates, and a surface anchored to every edge starts below the bar's exclusive zone Subtract the bar height (modal_host sits 42 px down on 1440p)
UPower's PendingCharge also follows Discharging Only Charging to PendingCharge means charging stopped (modules/global/power_events.lua)

Tooling

Trap Fix
Stow folds a target directory that does not exist yet (~/.local) into one symlink to the repo, so apps write into Dots Create it first; arch-install.sh runs mkdir -p ~/.local before stow
Zed reads only the root .luarc.json; nested ones are ignored Keep all Lua settings in the root file
LuaLS builtin os types reject valid mantle os.time calls Keep builtin os and debug disabled in .luarc.json
A running LuaLS ignores runtime.* changes Restart the language server
Hyprland 0.56 parses its socket as Lua, so hyprctl dispatch exit fails hyprctl dispatch 'hl.dsp.exit()'
Hyprland 0.56.1 fades mapped Top surfaces when fullscreen starts, but not ones mapped after Handle both on Hyprland; Niri does neither
Hyprland sends the pointer only to a layer with Exclusive keyboard interactivity, so other surfaces stop taking clicks OnDemand on Hyprland; it still takes focus when it maps
A niri workspace switch hands an OnDemand layer's keyboard to a window Exclusive on niri (modules/global/modal_host.lua)
niri msg action spawn passes an activation token that overrides open-focused false Spawn through env -u XDG_ACTIVATION_TOKEN, then focus explicitly
niri --config x validate is rejected niri validate --config x
systemd-run --scope rejects --pipe, and fixed-name scopes linger after exit Transient service with --pipe --collect
Upstream qt6ct never reports a colour scheme, so Qt apps that check dark mode pick light; Kirigami apps (KDE Connect) read only a KDE .colors scheme qt6ct-kde with a .colors scheme; keep QT_QPA_PLATFORMTHEME=qt6ct
qt6ct-kde's KDE icon engine drops -symbolic when the theme has the full-colour icon, so Telegram (org.telegram.desktop-symbolic) shows its colour tray icon and hides "Use monochrome icon" arch-install.sh links Tela's telegram*-panel.svg under those names in ~/.local/share/icons/Tela-circle-dracula
GTK_THEME in the environment overrides every GTK theme and libadwaita colour Never set it; lib/app_colours.lua sets the theme through gsettings
Running apps never re-read colours under the same name: KSharedConfig caches a .colors by path, GTK3 reloads its user gtk.css never and its theme only on a name change. GtkSourceView never rescans at all, so Text Editor recolours on its next launch Alternate Mantle-A/Mantle-B copies (KDE scheme in qt6ct.conf, GTK theme in gsettings)
GTK4 re-reads ~/.config/gtk-4.0/gtk.css imports only when a portal preference changes; a color-scheme flip flashes every app light Colours in an import; flip org.gnome.desktop.a11y.interface reduced-motion for 50 ms
A GTK3 theme without gtk-3.0/gtk-dark.css falls back to Adwaita in dark mode Ship gtk-dark.css
Zen (Gecko) follows the GTK theme live only through system colours in userChrome.css, and pairs AccentColor/AccentColorText differently per mode; web content gets stand-ins, not GTK colours Accent as LinkText (adw-gtk3's accent_color; Zen 1.23.4b dropped -moz-nativehyperlinktext, so every declaration using it goes transparent), surfaces as -moz-dialog/Field/-moz-headerbar; leave userContent.css static
Zen's folder icons and selected tab fill through light-dark(color-mix(var(--zen-primary-color))); with that on a system colour, Gecko drops them when the GTK theme and colour scheme flip together, until a restart Override --zen-folder-behind-bgcolor, -front-bgcolor, -stroke and --tab-background-color-selected (on zen-workspace) in userChrome.css without light-dark()
Vencord watches quickCss.css as a file, so the watch dies at the first atomic write (file.write renames) Write a theme into the watched themes/ directory instead