Architecture & Design
This document describes how Linutil is structured internally — the crate layout, data model, TUI design, script execution pipeline, and the build tooling.
Workspace Layout
Linutil is a Cargo workspace with three crates:
linutil/
├── core/ # linutil_core — backend library
├── tui/ # linutil_tui — binary (the TUI you run)
└── xtask/ # build tooling (cargo xtask docgen)core/ — linutil_core
The library crate. Responsible for:
- Defining the data model (
Tab,ListNode,Command) - Parsing all
tab_data.tomlfiles and building the menu tree - Embedding all scripts into the binary at compile time using
include_dir! - Extracting embedded scripts to a temp directory at runtime
- Evaluating preconditions to filter out scripts unsupported on the current system
- Parsing the user’s TOML config file
tui/ — linutil_tui
The binary crate. Responsible for:
- Setting up the terminal (crossterm alternate screen, raw mode)
- Running the main event loop
- Rendering the entire TUI layout via
ratatui - Handling all keyboard and mouse input
- Launching scripts in a pseudo-terminal (PTY) via
portable-pty - Parsing CLI arguments via
clap
xtask/
Cargo’s task runner extension. Run with:
cargo xtask docgenThis reads all tab_data.toml files and generates docs/content/userguide/userguide.md — the auto-generated walkthrough page. Always run this after adding or editing a script entry.
Data Model
The menu is a tree of ListNode items, grouped into named Tabs.
Tab
pub struct Tab {
pub name: String,
pub tree: Tree<Rc<ListNode>>,
}Each Tab maps to one top-level category. The five built-in tabs are defined in core/tabs/tabs.toml:
directories = [
"applications-setup",
"gaming",
"security",
"system-setup",
"utils"
]ListNode
pub struct ListNode {
pub name: String,
pub description: String,
pub command: Command,
pub task_list: String,
pub multi_select: bool,
}Every item in the TUI is a ListNode. A node is either a directory (has children, command = Command::None) or a leaf command (no children, has a runnable command).
Command
pub enum Command {
Raw(String), // inline shell command
LocalFile { // shell script file
executable: String,
args: Vec<String>,
file: PathBuf,
},
None, // directory node
}Raw— a short command string run directly by the shellLocalFile— a script file whose interpreter is read from the shebang line (e.g.#!/bin/bash)None— marks a category/folder node
Tab Data Format
Each tab is defined by a tab_data.toml file inside core/tabs/<tab-name>/. Example:
name = "Applications Setup"
[[data]]
name = "Communication Apps"
[[data.entries]]
name = "Discord"
description = "Discord is a versatile communication platform..."
script = "communication-apps/discord-setup.sh"
task_list = "I"
[[data.entries]]
name = "Some Inline Command"
description = "Runs a quick command"
command = "echo hello"
task_list = "MP"Entry Fields
| Field | Required | Description |
|---|---|---|
name | Yes | Display name shown in the TUI |
description | No | Shown in the description floating window (d key) |
script | One of | Path to a shell script (relative to the tab directory) |
command | One of | Inline shell command string |
entries | One of | Nested sub-entries (makes this node a directory) |
task_list | No | One or more flag codes shown next to the item name |
multi_select | No | Whether this command can be queued in multi-select mode (default: true) |
preconditions | No | Conditions that must pass for the entry to be shown |
Task List Flags
Flags shown to the right of each command name, defined in state.rs:
| Flag | Meaning |
|---|---|
D | Disk modifications (privileged) |
FI | Flatpak installation |
FM | File modification |
I | Installation (privileged) |
K | Kernel modifications (privileged) |
MP | Package manager actions |
SI | Full system installation |
SS | Systemd actions (privileged) |
RP | Package removal |
Preconditions
Preconditions let a script declare when it should be visible. If any precondition fails, the entry is hidden from the TUI.
[[data.entries]]
name = "Paru AUR Helper"
script = "paru-setup.sh"
[[data.entries.preconditions]]
matches = true
data = { containing_file = "/etc/os-release" }
values = ["Arch Linux", "Manjaro"]Precondition Types
| Type | Checks |
|---|---|
environment | Whether an environment variable equals one of the given values |
containing_file | Whether a file’s contents contain all of the given strings |
command_exists | Whether a command is present on $PATH |
file_exists | Whether a file path exists on disk |
The matches field inverts the check when false (i.e. “must NOT match”).
Script Embedding
All files under core/tabs/ are embedded into the compiled binary at build time using the include_dir! macro:
const TAB_DATA: Dir = include_dir!("$CARGO_MANIFEST_DIR/tabs");At runtime, get_tabs() extracts the embedded directory to a system temp directory (/tmp/linutil_scripts_XXXX), and all LocalFile commands reference scripts inside that temp dir. The temp directory is cleaned up automatically when TabList is dropped.
This means a single binary contains everything — no external script files needed after build.
Script Execution Pipeline
When a user selects a command and confirms it:
AppState::handle_confirm_command()creates aRunningCommandfrom the list of selectedCommandvaluesRunningCommand::new()allocates a PTY viaportable-pty(NativePtySystem)- The command is spawned inside the PTY as a child process
- A reader thread reads output from the PTY master and writes it into a shared
Arc<Mutex<Vec<u8>>>buffer - An atomic flag (
TERMINAL_UPDATED) is set when new output arrives, triggering a TUI redraw - The PTY output is decoded by a
vt100parser and rendered as aPseudoTerminalwidget (fromtui-term) inside a floating window - The user can scroll up/down to review output, or press
Ctrl-Cto kill the process - When the process exits, the floating window title changes to
SUCCESS(green) orFAILED(red)
Using a real PTY (instead of piped stdio) means scripts that use terminal colors, interactive prompts, or check isatty() work correctly.
TUI Layout
The TUI is rendered by AppState::draw() and divided into these regions:
┌──────────────┬────────────────────────────────┐
│ Logo / │ [ Search bar ] │
│ Version ├────────────────────────────────┤
│ │ │
│ Tab List │ Item List │
│ │ │
│ System Info │ │
├──────────────┴────────────────────────────────┤
│ Keyboard hint bar │
└───────────────────────────────────────────────┘- Left column: Logo (or version label), tab list, system info panel
- Right column: Search bar (top) + scrollable item list
- Bottom bar: Context-sensitive keyboard shortcut hints
- Floating windows: Overlaid on the item list for running commands, previews, descriptions, and confirmation prompts
Focus State Machine
AppState tracks a Focus enum:
| State | Description |
|---|---|
TabList | User is navigating the left tab panel |
List | User is navigating the item list |
Search | The search bar is active |
FloatingWindow | A modal is open (preview, description, running command, guide) |
ConfirmationPrompt | User is being asked to confirm before running a command |
Input is dispatched to the currently focused component.
Config File
Linutil reads an optional TOML config file at startup (--config path). The linutil_core::Config struct deserializes it:
pub struct Config {
auto_execute: Option<Vec<String>>,
skip_confirmation: Option<bool>,
size_bypass: Option<bool>,
}After parsing, auto_execute command names are looked up by name in the loaded TabList (using Tab::find_command_by_name), and the resulting Vec<Rc<ListNode>> is placed directly into selected_commands to be run immediately on startup.
Adding a New Script
- Create a shell script in
core/tabs/<tab-name>/<category>/your-script.sh - Add an entry to the corresponding
tab_data.toml:
[[data.entries]]
name = "Your Script"
description = "What it does."
script = "<category>/your-script.sh"
task_list = "I"- Add preconditions if the script is distro-specific
- Run
cargo xtask docgento update the documentation - Run
cargo runto test it locally
Key Dependencies
| Crate | Purpose |
|---|---|
ratatui | TUI rendering framework |
crossterm | Cross-platform terminal control (raw mode, events) |
portable-pty | Pseudo-terminal allocation for running commands |
tui-term | PTY output rendering widget for ratatui |
vt100-ctt | VT100 terminal emulator (parses ANSI escape codes) |
ego-tree | Generic tree structure used for the menu |
include_dir | Embed entire directory trees into the binary at compile time |
serde + toml | Deserialize tab_data.toml and config files |
clap | CLI argument parsing |
tree-sitter-bash | Bash syntax highlighting in script previews |