Documentation / Architecture
Architecture
Aphotic’s repo mirrors what actually gets installed, plus the machinery that decides what that is. There’s no separate “build system” — install.sh resolves a plan from declarative TOML data and copies files into place.
Aphotic was previously known as Noctis-Hypr; the CLI, install script, and every path below are the current, renamed project.
Repo layout
Aphotic-Hypr/
├── install.sh / uninstall.sh Thin orchestrators: guided setup, wizard or flags in, resolved plan out
├── aphotic.toml Generated on first install: the resolved source of truth
├── lib/
│ ├── install/ Guided setup + wizard prompts, AUR helper bootstrap, backups, config
│ │ deploy, greeter, snapshot recovery, failure reports
│ ├── toml/ Profile + layer merge logic (merge.py)
│ └── ai/ The Aphotic Assistant's prompt template
├── profiles/
│ ├── base/ minimal.toml, full.toml
│ └── layers/ gaming.toml, dev.toml, ai.toml, exploit.toml (meta) + exploit-*.toml sublayers
├── themes/ Swappable theme presets (THEME_SPEC.md documents the contract)
└── Configs/ Mirrors ~/.config: the configs that land on disk
├── systemd/user/ aphotic-shell.service (Restart=on-failure supervision for qs),
│ aphotic-recovery.service, aphotic-agent-usage.service/.timer,
│ aphotic-greeter-sync + aphotic-sddm-sync .service/.timer,
│ aphotic-package-check.service/.timer, kdeconnectd.service
├── greetd/ The Aphotic greeter (Quickshell login surface) and its compositor config
├── quickshell/aphotic/ Hand-vendored Quickshell shell (see below)
│ ├── config/ Tokens/Config/GlobalConfig singletons (hand-written, no native plugin)
│ ├── services/ Colours (reads the palette snapshot), Audio, Hypr, Players, Notifs,
│ │ │ PluginRegistry, RuntimeContext, Surfaces, ResourcePosture, ...
│ │ ├── ai/ AiConfig/AiKeys/AiProviders (AI Chat + Settings → AI), AgentEvents,
│ │ │ InferenceMode, the local-inference claimants (Ollama, llama-swap, LM Studio)
│ │ └── profile/ ProfileEngine (profile lifecycle state machine), ResourceEngine
│ │ (cross-domain claim arbitration), DevProfile, SecurityProfile,
│ │ WorkloadPassports, ActionReceipts, StateSnapshot
│ ├── components/ Shared UI primitives (StyledText, MaterialIcon, StateLayer,
│ │ SettingsRow/SettingsGroup/SettingsToggleRow/SettingsPresetRow, Logo, ...)
│ ├── modules/ bar/ (+ real popouts), notch/, launcher/, switcher/ (Alt+Tab),
│ │ dashboard/ (Command Center), flow/, settings/ (Control Center, 16 panes),
│ │ notifications/, notificationcenter/, osd/, lock/, session/,
│ │ intelligence/, negotiation/, wallpaperpicker/, background/,
│ │ workspace/, overlay/, areapicker/, colorpicker/, keybinds/, pkginstall/
│ │ -- plugin surfaces (Agent Graph and the rest) link in under
│ │ modules/plugins/ at install time, see below
│ └── RUNTIME.md The shell runtime contracts: RuntimeContext, Surfaces, ResourcePosture
├── hypr/ hyprland.lua, keybinds.lua, custom.lua (never overwritten, see below)
└── .local/
├── bin/aphotic aphotic CLI entry point, symlinked onto PATH by install.sh
└── lib/aphotic/ CLI internals: commands/ (one cmd_*.sh per subcommand),
globalcontrol.sh, agent_hook.py/.sh (Claude Code hook worker),
agent_usage.py (token-usage aggregation)
Installation system
install.sh is a thin orchestrator, not a script full of hardcoded package arrays. On the stable channel it first checks out the newest release tag. Run with no flags on a terminal, it walks through a four-step guided setup and one summary to accept; --opt-in is just the layer picker, and flags like --profile/--with/--theme skip the corresponding prompts. Either way it:
- Resolves a package plan at runtime by merging
profiles/base/<profile>.tomlwith each selectedprofiles/layers/<layer>.tomlandprofiles/custom_apps.lst, deduplicating as it goes (lib/toml/merge.py). - Detects an AUR helper (
yayorparu), and buildsyayif neither is present (lib/install/aur.sh). - Snapshots your current configs to a timestamped backup before touching anything (
lib/install/backup.sh), unless--no-backupis passed. - Writes the resolved choice to
aphotic.tomlat the repo root, the source of truth every re-run reads back. - Copies
Configs/over~/.config/, symlinks the parts that must track the checkout (most ofhypr/,wallust/,matugen/), installs the greeter as the login screen, and restarts the shell.
install.sh never hardcodes a package list — everything downstream (backups, AUR helper choice, config copying) reads from that one resolved plan. --dry-run prints the whole plan and exits before any sudo prompt, package install, or filesystem write happens.
Config resolution order
- Base profile (
profiles/base/<profile>.toml) - Selected layers (
profiles/layers/<layer>.toml) - Custom apps (
profiles/custom_apps.lst, still readable at the repo root as a symlink)
All three merge into one deduplicated plan at install time. See Profiles & Layers for what each profile/layer actually contains.
Backup system
Every install.sh run snapshots your current configuration first:
- Stored under
~/.config-backup/as timestamped directories. --keep-backups <N>controls how many are retained before pruning (default 5).- Restored automatically by
./uninstall.sh, which reverses the most recent snapshot on request. Pass--purge-packagesto also remove what your profile installed.
The separate aphotic backup CLI keeps its own manual snapshots under ~/.local/state/aphotic/backups/. See Installation → Backup System.
ai layer wiring
Selecting the ai layer enables the aphotic-agent-usage.timer systemd user unit (token-usage aggregation for the bar’s agent icon) and installs Ollama, llmfit and Claude Code. It does not wire any harness’s agent hooks anymore — as of the modular plugin architecture (v2.0.0), Claude Code/Codex/OpenCode hook wiring and Agent Graph are each their own opt-in plugin (claude-hooks/codex-hooks/opencode-hooks/agent-graph, installed via aphotic plugin install <name>), not something install.sh touches. See Agentic Workflows for those plugins’ own contract, and Plugin System for how the plugin mechanism itself works.
Config sync only
--config-only skips the wizard, package installs, and aphotic.toml entirely — it just backs up, copies Configs/ over ~/.config/, and restarts the shell. Most day-to-day updates (Quickshell QML, Hyprland config, keybinds) have no package churn behind them, so this is the fast path. aphotic sync (and Settings → About’s Sync button) runs it after a git pull; aphotic update does not, see Installation → Updating.
Configuration files
Configs/hypr/
hyprland.lua— main Hyprland configuration that loads the other Lua files.keybinds.lua— all keybindings, grouped logically.custom.lua— never overwritten by the installer once it exists. Put your own Hyprland tweaks here and a re-run oraphotic updatewon’t clobber them. See Advanced Usage for the Lua bind syntax.
Configs/quickshell/aphotic/
The Quickshell shell is hand-vendored — it isn’t an installed dependency, the QML lives directly in the repo, and no native C++ plugin is required to run it:
config/—Tokens,Config,GlobalConfigsingletons (hand-written, no native plugin).services/—Colours(reads the palette snapshot both colour engines write),Audio,Hypr,Players,Notifs,AgentProviders,InstallProfile,PluginRegistry, plus theai/subfolder backing AI Chat, Settings → AI and inference mode, and theprofile/subfolder (ProfileEngine, ResourceEngine, DevProfile, SecurityProfile, WorkloadPassports, StateSnapshot), among many others. The Gaming profile is a plugin, not core.components/— shared UI primitives (StyledText,MaterialIcon,StateLayer, theSettingsRow/SettingsGroupfamily,Logo, …).modules/— one directory per surface:bar/,notch/,launcher/,switcher/,dashboard/(Command Center),flow/,settings/(Control Center),notifications/,lock/,session/and more. Plugin surfaces link in undermodules/plugins/<name>when a plugin installs.
Colours.qml reads the palette snapshot at ~/.local/state/aphotic/palette.json (written by wallust, or matugen for a theme that pins it); Quickshell doesn’t bring its own theming engine. See Theming for the full pipeline.
Key design principles
Declarative over hardcoded
Package sets live as data (profiles/*.toml) rather than bash arrays buried in an install script. Changing what ships means editing a TOML file, not doing surgery on install.sh.
Composable, not monolithic
A base profile (minimal or full) plus any combination of layers (gaming, dev, ai, exploit and its sublayers) resolve into one merged package list at install time. Adding a layer doesn’t require touching the base, and layers dedupe against each other automatically.
Safe to run twice
Every install.sh run snapshots configs before making changes. Re-running the installer detects your saved aphotic.toml and re-resolves profile/layers against upstream changes instead of asking the same questions again.
Reversible
./uninstall.sh restores your most recent backup on request — trying Aphotic doesn’t require burning your current setup down first.
Protected customization
~/.config/hypr/custom.lua is the one file the installer never touches once it exists.
CLI integration
The aphotic CLI (Configs/.local/bin/aphotic, symlinked onto PATH by install.sh) is a single entry point over Configs/.local/lib/aphotic/commands/, one cmd_*.sh file per subcommand — the list is auto-discovered, so aphotic --help can’t drift out of sync with what’s actually implemented. Highlights:
- Config:
theme,scheme,wallpaper,bar,context,displaymanager,greeter,sddm,plugin,vpn,config - Core:
agent,diff,doctor,perf,reload,runtime,shell,status - Lifecycle:
backup,packages,reconcile,recovery,report,restore,rollback,safemode,sync,update,whatsnew - AI:
ai - Fun:
play
See CLI Reference for the full command list, and Theming for the theme/scheme/wallpaper subcommands specifically.
See also
- Profiles & Layers — the base-profile/layer merge system in detail
- Theming — the wallpaper-driven color pipeline
- Agentic Workflows — what the
ailayer wires up and what it collects - Resource Engine — the cross-domain claim arbitration this diagram’s
profile/folder implements - Supported Features — everything shipped, at a glance