This doc is designed to give a high level overview of how this codebase works, to make contributing to it easier.
The flake included in the project root contains a dev shell which will give you all of the tools you need to work on the project. If you're on NixOS or have nixpkgs installed on your machine, you can just use
nix developIf not, make sure you have cargo installed. Also, run cargo fmt before you make any commits please :)
The program itself has five core components:
- The event loop - manages current UI and installer state
- The
Pagetrait - defines the main UI screens, essentially containers for widgets - The
ConfigWidgettrait - re-usable UI components that make up pages - The
Installerstruct - contains all of the information input by the user - The
nixgenmodule - responsible for serializing theInstallerstruct into aconfiguration.nixfile
The event loop contains a stack of Box<dyn Page>, and whenever a page is entered, that page is pushed onto the stack. Whenever a page is exited, that page is popped from the stack. Every iteration of the event loop does two things:
- Calls the
render()method of the page on top of the stack - Polls for user input, and if any is received, passes that input to the
handle_input()method of the page on top of the stack. The pages communicate with the event loop using theSignalenum.Signal::Popmakes the event loop pop from the page stack, for instance.
The Page trait is the main interface used to define the different pages of the installer. The main methods of this trait are render() and handle_input(). Each page is itself a collection of widgets, which each implement the ConfigWidget trait. Pages are navigated to by returning Signal::Push(Box::new(<page>)) from the handle_input() method, which tells the event loop to push a new page onto the stack. Pages are navigated away from using Signal::Pop.
The ConfigWidget trait is the main interface used to define page components. Like Page, the ConfigWidget trait exposes render() and handle_input(). handle_input() is useful when input must be passed to the widget using the interface, like in the case of said widget being stored as a trait object. render() is usually given a chunk of the screen by it's Page to try to render inside of.
Generally speaking, inputs are caught and handled at the page level, as delegating all input to the individual widgets ends up fostering more presumptuous or general logic, where page-specific logic is generally more favorable in this case.
The trickiest part of setting up new Page or ConfigWidget structs is defining how they use the space that they are given in their respective render() methods. Take this for example:
impl Page for EnableFlakes {
fn render(&mut self, _installer: &mut Installer, f: &mut Frame, area: Rect) {
let chunks = Layout::default()
.direction(Direction::Vertical)
.margin(1)
.constraints(
[
Constraint::Percentage(40),
Constraint::Percentage(60)
].as_ref()
)
.split(area);
let hor_chunks = Layout::default()
.direction(Direction::Horizontal)
.margin(1)
.constraints(
[
Constraint::Percentage(30),
Constraint::Percentage(40),
Constraint::Percentage(30),
]
.as_ref(),
)
.split(chunks[1]);
let info_box = InfoBox::new(
"",
... info box content ...
);
info_box.render(f, chunks[0]);
self.buttons.render(f, hor_chunks[1]);
self.help_modal.render(f, area);
}
...This is the render() method of the "Enable Flakes" page. It cuts up the space given to it vertically first, and then horizontally.
The method uses Ratatui's Layout system to divide the terminal screen area into smaller rectangular chunks. First, it splits the available space vertically into two regions: the top 40% (for the info_box) and the bottom 60%. Then it subdivides the bottom 60% horizontally into three parts: 30%, 40%, and 30%. The middle horizontal chunk is used to render the buttons widget.
Each widget’s render() method is called with the frame and the specific chunk of the terminal space it should draw itself within. This way, each widget knows exactly how much space it has, and where it should be positioned on the screen.
This approach of dividing and subdividing the UI space using Ratatui’s layout tools allows pages to arrange their child widgets precisely and responsively, adapting to terminal size changes.
The Installer struct (defined in src/installer/mod.rs) serves as the central data store for all user configuration choices throughout the installation process. It acts as the single source of truth that gets populated as users navigate through different pages and make selections.
The struct contains fields for every configurable aspect of a NixOS installation:
System Configuration:
hostname,timezone,locale,language- Basic system settingskeyboard_layout- Keyboard layout configurationenable_flakes- Whether to enable Nix flakes supportbootloader- Boot loader choice (e.g., "systemd-boot", "grub")
Hardware & Storage:
drives- Vector ofDiskobjects representing storage configurationuse_swap- Whether to enable swap partitionkernels- Available kernel optionsaudio_backend- Audio system configuration
User Management:
root_passwd_hash- Hashed root passwordusers- Vector ofUserstructs containing user account information
Desktop Environment:
desktop_environment- Selected DE (e.g., "KDE Plasma", "GNOME")greeter- Display manager choiceprofile- Installation profile selection
Packages & Services:
system_pkgs- Vector of system packages to installnetwork_backend- Network management systemflake_path- Optional path to user's flake configuration
The Installer struct provides several important methods:
new()- Creates a new instance with default valueshas_all_requirements()- Validates that all required fields are populated before installation can proceed. Checks for root password, at least one user, drive configuration, and bootloader selection.
Throughout the application, pages and widgets receive a mutable reference to the Installer struct, allowing them to read current values and update fields based on user input. This centralized approach ensures data consistency and makes it easy to validate the complete configuration before proceeding with installation.
The nixgen module (located in src/nixgen.rs) is responsible for converting the user's configuration choices stored in the Installer struct into valid Nix configuration files. This module serves as the bridge between the TUI application and the actual NixOS configuration system.
NixWriter Struct:
The main component is the NixWriter struct, which takes a JSON Value representation of the configuration and provides methods to generate different types of Nix configuration files.
Key Functions:
nixstr(val)- Utility function that wraps strings in quotes for valid Nix syntaxfmt_nix(nix)- Formats generated Nix code using thenixfmttoolhighlight_nix(nix)- Syntax highlights Nix code usingbatfor display purposesattrset!- A macro that allows you to write Nix attribute sets. Returns aString.
The NixWriter generates two main types of configuration:
System Configuration (write_sys_config):
- Converts user choices into a complete
configuration.nixfile - Handles system-level settings like networking, desktop environments, users, and services
- Uses helper functions like
parse_network_backend()andparse_locale()to convert user-friendly selections into proper Nix attribute sets - Manages conditional logic for features like Home Manager integration
Disko Configuration (write_disko_config):
- Generates disk partitioning and filesystem configuration
- Creates the declarative disk setup that Disko will execute during installation
- Handles different storage configurations based on user selections
The write_configs() method returns a Configs struct containing:
system- The complete NixOS system configuration as a Nix stringdisko- The disk configuration for the Disko toolflake_path- Optional path to user's existing flake configuration
This separation allows the installer to:
- Maintain a clean separation between UI logic and configuration generation
- Generate human-readable, properly formatted Nix configurations
- Support both traditional NixOS configurations and flake-based setups
- Provide immediate validation and preview of the generated configurations before installation