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 for hey and 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

ValueSource
Host namemapAttrs key from hosts/<name>/ directory → networking.hostName
User identitymodules/user.nix option declarations, set per-user in modules/profiles/user/<name>.nix
Thememodules.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/ and XDG_BIN_HOME to the system PATH.
  • FPath: Adds lib/zsh/ to the Zsh fpath for autoloading functions.
  • Metadata: Generates ~/.local/share/hey/info.json for 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.nix uses theme colors to generate config for apps (Alacritty, Waybar, Rofi).
  • Dynamic Updates: bin/termcolors.sh pushes 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.

CommandTaskExample
hey disko formatWipe, partition, and mount diskshey disko format
hey disko mountMount existing partitions without formattinghey disko mount
hey installInstall NixOS onto a target roothey install --root /mnt --host bio-smart
just bootstrap-heyRebuild hey from source on a new systemjust bootstrap-hey

🔄 System Maintenance (Day 1)

Commands for keeping your system updated and clean.

CommandTaskExample
hey .open-termSingleton terminal/tmux orchestratorhey .open-term kitty
hey syncRebuild and switch to the current flakehey sync (or just sync)
hey sync bootRebuild and set as default for next boothey sync boot
hey gcClean old generations and optimize storehey gc (or just gc)
hey pullUpdate flake inputs (flake.lock)hey pull

🧪 Development & Verification (Day 2)

Tools for refactoring modules and building custom images.

CommandTaskExample
hey check allRun syntax, flake, and host evaluationhey check all
hey check evalCheck for infinite recursion on a hosthey check eval --host id3-eniac
hey build isoGenerate a bootable installer ISOhey build iso
hey build raw-efiGenerate a raw EFI disk imagehey build raw-efi
hey build imageBuild a full disk image via Diskohey build image

📊 Information & Introspection

Commands for inspecting the state of the system, flake, or network.

CommandTaskExample
hey info closureCalculate Nix closure size (from current dotfiles)hey info closure vps-pacman
hey info ipGet local IP address (or WAN with -w)hey info ip --wan
hey info <keys...>Query current flake/host metadatahey info user host theme

Note: hey info closure evaluates 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’s import to load bin/hey.d/sync.janet on demand. This keeps the tool fast by only loading the code required for the current task.
  • Type-Safe Options: Each subcommand uses the defcmd macro, which automatically parses flags (like --fast or --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:

  1. Host-Specific: hosts/$HOST/bin/ (Machine-specific overrides).
  2. WM-Specific: config/$WM/bin/ (Compositor-aware behavior).
  3. 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

  1. Resolution: hey maps @rofi to the directory $DOTFILES_HOME/config/rofi/bin/.
  2. Execution: It searches for wifimenu within that directory, supporting multiple backends:
    • Janet (.janet): Used for complex logic (e.g., wifimenu.janet, audiomenu.janet). hey automatically calls the main function with arguments (provided the script uses defmain with upscope).
    • Zsh (.zsh / .sh): Used for shell-heavy tasks (e.g., powermenu.zsh, appmenu.sh).
  3. UI Integration: These scripts typically use the hey/rofi library 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:

  1. hosts/$HOST/hooks/: Highest priority. Used for machine-specific reactions (e.g., unique power-saving steps for a laptop).
  2. config/$WM/hooks/: Medium priority. Used for compositor-specific logic (e.g., sending a specific IPC command to Niri).
  3. bin/hooks/: Global priority. Standardized logic used by the entire fleet.
  4. ~/.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 the reload hook).

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 via XDG_CURRENT_DESKTOP).
  • hey.wm.mode: Returns the desktop mode (e.g., dms or traditional).
  • 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 to boot) builds the new NixOS configuration and adds it to your bootloader menu. It does not activate the system immediately. Therefore, the hey binary is not rebuilt during this phase.
  • system.userActivationScripts.initHey: This NixOS activation script is responsible for running jpm deps and jpm run deploy to compile the hey binary as your local user.
  • The Lifecycle: Because hey sync only stages the configuration for the next boot, initHey will only run when you reboot and log into the new system (or if you explicitly run hey sync switch to force immediate activation).

Immediate Rebuild Options: If you need to update the hey binary immediately without rebooting, use one of the following:

  1. hey build hey: Manually triggers the exact compilation steps used by initHey without touching the rest of the system.
  2. ./scripts/build_hey.zsh: A standalone script that mirrors the activation logic.
  3. hey sync switch: Builds the system and forces an immediate activation.

🛠️ Workflows

1. The “Safe Refactor”

When modifying a shared module or library:

  1. Modify the code.
  2. Verify: just check-all (Evaluates every host in the fleet to catch recursion).
  3. Apply: just sync.

2. Provisioning a New Host

  1. Boot from a NixOS installer ISO.
  2. just disko format --host <name> (Prepare disks).
  3. 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.