Toolchain
Hey Toolchain & System Integration
A centralized, adaptive orchestrator for NixOS, bridging Janet-based logic, NixOS module system, and Zsh performance.
🏗️ Architecture Overview
The toolchain is designed to be WM-agnostic and declarative. It centralizes duplicated logic (volume, brightness, screenshots, hooks) into a unified system that adapts at runtime to the current environment.
Core Components
hey(Janet): The primary entry point. A high-level CLI that handles subcommand routing, state management, and hook dispatching.lib/hey/(Janet Modules): Core libraries for command definition (cmd.janet), path resolution (init.janet), and system utilities.lib/zsh/(Zsh Functions): Autoloaded helpers that provide instant access to system state (WM name, mode) for shell scripts.bin/(Unified Scripts): The implementation layer. Adaptive scripts that perform the actual work (locking, OSD, etc.).justfile: A high-level task runner providing a human-friendly menu forheyand bootstrap tasks.
🧬 System-Level Integration (NixOS)
hey is a foundational layer of the NixOS configuration, not just a standalone script.
1. The hey Input Argument
In lib/nixos.nix, a special hey object is created and passed as a module argument to all NixOS modules.
- Attributes:
hey.dir,hey.binDir,hey.configDir,hey.modulesDir,hey.themesDir, etc. - Usage: Allows modules to reference dotfile assets without hardcoding paths.
2. Pure Hermetic Flake
The flake is fully hermetic — zero --impure, zero getEnv, zero HEYENV.
Build-time path resolution uses toString self (the flake store path), which
contains all config files, modules, and themes. No environment variables are
needed for evaluation.
Where host/user/theme come from
| Value | Source |
|---|---|
| Host name | mapAttrs key from hosts/<name>/ directory → networking.hostName |
| User identity | modules/user.nix option declarations, set per-user in modules/profiles/user/<name>.nix |
| Theme | modules.theme.active set in each host’s configuration.nix |
The Janet (flake) function reads $HOST and $USER for CLI runtime context
(default host for sync, path resolution). Theme is read purely from info.json
(generated by modules/hey.nix). These are Janet runtime concerns — not
Nix evaluation inputs.
3. Module: modules/hey.nix
Handles the system plumbing:
- Paths: Adds
bin/andXDG_BIN_HOMEto the systemPATH. - FPath: Adds
lib/zsh/to the Zshfpathfor autoloading functions. - Metadata: Generates
~/.local/share/hey/info.jsonfor scripts to consume.
🐚 Shell & App Integration
Zsh Bridge (lib/zsh/)
Every shell script has access to hey.* functions:
hey.wm.name: Detects ‘hypr’, ‘niri’, or ‘bspwm’.hey.wm.mode: Detects ‘dms’ or ‘traditional’ UI modes.hey.log: Unified logging with levels and colors.
App-Level (Theming)
- Data Flow:
modules/themes/apps.nixuses theme colors to generate config for apps (Alacritty, Waybar, Rofi). - Dynamic Updates:
bin/termcolors.shpushes live color updates to running terminals.
📖 Operator Manual
The hey toolchain is categorized by operational lifecycle. For most daily tasks, the justfile provides the fastest interface.
🛠️ Provisioning & Deployment (Day 0)
These commands are used when setting up new hardware or performing low-level disk operations.
| Command | Task | Example |
|---|---|---|
hey disko format | Wipe, partition, and mount disks | hey disko format |
hey disko mount | Mount existing partitions without formatting | hey disko mount |
hey install | Install NixOS onto a target root | hey install --root /mnt --host bio-smart |
just bootstrap-hey | Rebuild hey from source on a new system | just bootstrap-hey |
🔄 System Maintenance (Day 1)
Commands for keeping your system updated and clean.
| Command | Task | Example |
|---|---|---|
hey .open-term | Singleton terminal/tmux orchestrator | hey .open-term kitty |
hey sync | Rebuild and switch to the current flake | hey sync (or just sync) |
hey sync boot | Rebuild and set as default for next boot | hey sync boot |
hey gc | Clean old generations and optimize store | hey gc (or just gc) |
hey pull | Update flake inputs (flake.lock) | hey pull |
🧪 Development & Verification (Day 2)
Tools for refactoring modules and building custom images.
| Command | Task | Example |
|---|---|---|
hey check all | Run syntax, flake, and host evaluation | hey check all |
hey check eval | Check for infinite recursion on a host | hey check eval --host id3-eniac |
hey build iso | Generate a bootable installer ISO | hey build iso |
hey build raw-efi | Generate a raw EFI disk image | hey build raw-efi |
hey build image | Build a full disk image via Disko | hey build image |
📊 Information & Introspection
Commands for inspecting the state of the system, flake, or network.
| Command | Task | Example |
|---|---|---|
hey info closure | Calculate Nix closure size (from current dotfiles) | hey info closure vps-pacman |
hey info ip | Get local IP address (or WAN with -w) | hey info ip --wan |
hey info <keys...> | Query current flake/host metadata | hey info user host theme |
Note:
hey info closureevaluates the current state of your dotfiles repository, allowing you to preview the size of a configuration before you sync or deploy it.
🔍 Deep Dive: Under the Hood
To effectively use and extend the toolchain, it is important to understand how hey resolves your intent into action.
1. How hey <subcommand> Works (Janet Dispatch)
The hey binary (a Janet script) uses a declarative dispatcher to route commands.
- Subcommand Registry: When you run
hey sync, the dispatcher looks for a registered keyword:sync. - Dynamic Loading: It calls
(cmd 'sync), which uses Janet’simportto loadbin/hey.d/sync.janeton demand. This keeps the tool fast by only loading the code required for the current task. - Type-Safe Options: Each subcommand uses the
defcmdmacro, which automatically parses flags (like--fastor--host) and validates their types before the script logic even runs.
2. How hey .<script> Works (The “Dot” Syntax)
The dot syntax is a shortcut for executing “binscripts”—standalone shell or Janet scripts located in the bin/ directory.
When hey sees a command starting with a ., it performs a scoped search using the following priority:
- Host-Specific:
hosts/$HOST/bin/(Machine-specific overrides). - WM-Specific:
config/$WM/bin/(Compositor-aware behavior). - Global:
bin/(The project-wide implementation).
Example: Running hey .lock will first check if id3-eniac has a custom locking script, then check if there is a Niri-specific locking script, before falling back to the default bin/lock.zsh. This allows for incredible portability across different hardware and desktop environments.
3. How hey @<app> Works (Rofi & Orchestration)
The @ prefix is a specialized dispatcher for application-specific tools and interactive menus, primarily used for Rofi integration.
When hey sees a command starting with @, it searches for scripts in config/$APP/bin/.
Example: Running hey @rofi wifimenu
- Resolution:
heymaps@rofito the directory$DOTFILES_HOME/config/rofi/bin/. - Execution: It searches for
wifimenuwithin that directory, supporting multiple backends:- Janet (
.janet): Used for complex logic (e.g.,wifimenu.janet,audiomenu.janet).heyautomatically calls themainfunction with arguments (provided the script usesdefmainwithupscope). - Zsh (
.zsh/.sh): Used for shell-heavy tasks (e.g.,powermenu.zsh,appmenu.sh).
- Janet (
- UI Integration: These scripts typically use the
hey/rofilibrary to render interactive menus that integrate with the system’s theme and window manager.
🚨 Developer Note: Clean Capture
When writing Janet scripts for @ commands, be aware that the $<_ macro captures both stdout and stderr. System utilities often print warnings (like locale issues) to stderr, which will corrupt JSON parsing.
Always use this pattern for machine-readable output:
($<_ sh -c "pactl -f json list sinks 2>/dev/null")
** Launcher Integration**:
The exec strings defined in NixOS modules (like modules/desktop/apps/rofi.nix) leverage this syntax:
exec = "hey @rofi wifimenu"; # Calls the wifimenu janet script
4. How the Hook System Works
Hooks are triggered via hey hook <event>. This is the primary way the system reacts to environmental changes (e.g., hey hook battery).
Resolution Logic:
The hook command (in bin/hey.d/hook.janet) searches for scripts named after the event. It doesn’t just run one script; it searches through a precedence hierarchy and executes the most specific one it finds:
hosts/$HOST/hooks/: Highest priority. Used for machine-specific reactions (e.g., unique power-saving steps for a laptop).config/$WM/hooks/: Medium priority. Used for compositor-specific logic (e.g., sending a specific IPC command to Niri).bin/hooks/: Global priority. Standardized logic used by the entire fleet.~/.local/share/hey/hooks.d/: Generated hooks. Used by the Nix module system to inject logic into hooks based on enabled modules (e.g., adding a specific service restart to thereloadhook).
4. Integration with Zsh (lib/zsh/)
The toolchain provides “shell primitives” that scripts can use to remain environment-aware without being brittle.
hey.wm.name: Returns the current WM string (fetched viaXDG_CURRENT_DESKTOP).hey.wm.mode: Returns the desktop mode (e.g.,dmsortraditional).hey.vars: A lightweight key-value store (using JSON files in$XDG_STATE_HOME/hey/vars.json) that allows scripts to persist state across reboots or shared between different languages (Janet and Zsh).
5. The Rebuild and Activation Lifecycle (hey sync vs initHey)
A common point of confusion is when the hey toolchain actually rebuilds itself during a system update.
hey sync: This command (which defaults toboot) builds the new NixOS configuration and adds it to your bootloader menu. It does not activate the system immediately. Therefore, theheybinary is not rebuilt during this phase.system.userActivationScripts.initHey: This NixOS activation script is responsible for runningjpm depsandjpm run deployto compile theheybinary as your local user.- The Lifecycle: Because
hey synconly stages the configuration for the next boot,initHeywill only run when you reboot and log into the new system (or if you explicitly runhey sync switchto force immediate activation).
Immediate Rebuild Options:
If you need to update the hey binary immediately without rebooting, use one of the following:
hey build hey: Manually triggers the exact compilation steps used byinitHeywithout touching the rest of the system../scripts/build_hey.zsh: A standalone script that mirrors the activation logic.hey sync switch: Builds the system and forces an immediate activation.
🛠️ Workflows
1. The “Safe Refactor”
When modifying a shared module or library:
- Modify the code.
- Verify:
just check-all(Evaluates every host in the fleet to catch recursion). - Apply:
just sync.
2. Provisioning a New Host
- Boot from a NixOS installer ISO.
just disko format --host <name>(Prepare disks).just install --host <name>(Deploy system).
3. Testing in a VM
To test a configuration without touching your hardware:
hey build vm --host <name>- This generates a script to launch the configuration in QEMU.