Skip to content

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.toml files 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 docgen

This 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 shell
  • LocalFile — 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

FieldRequiredDescription
nameYesDisplay name shown in the TUI
descriptionNoShown in the description floating window (d key)
scriptOne ofPath to a shell script (relative to the tab directory)
commandOne ofInline shell command string
entriesOne ofNested sub-entries (makes this node a directory)
task_listNoOne or more flag codes shown next to the item name
multi_selectNoWhether this command can be queued in multi-select mode (default: true)
preconditionsNoConditions 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:

FlagMeaning
DDisk modifications (privileged)
FIFlatpak installation
FMFile modification
IInstallation (privileged)
KKernel modifications (privileged)
MPPackage manager actions
SIFull system installation
SSSystemd actions (privileged)
RPPackage 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

TypeChecks
environmentWhether an environment variable equals one of the given values
containing_fileWhether a file’s contents contain all of the given strings
command_existsWhether a command is present on $PATH
file_existsWhether 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:

  1. AppState::handle_confirm_command() creates a RunningCommand from the list of selected Command values
  2. RunningCommand::new() allocates a PTY via portable-pty (NativePtySystem)
  3. The command is spawned inside the PTY as a child process
  4. A reader thread reads output from the PTY master and writes it into a shared Arc<Mutex<Vec<u8>>> buffer
  5. An atomic flag (TERMINAL_UPDATED) is set when new output arrives, triggering a TUI redraw
  6. The PTY output is decoded by a vt100 parser and rendered as a PseudoTerminal widget (from tui-term) inside a floating window
  7. The user can scroll up/down to review output, or press Ctrl-C to kill the process
  8. When the process exits, the floating window title changes to SUCCESS (green) or FAILED (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:

StateDescription
TabListUser is navigating the left tab panel
ListUser is navigating the item list
SearchThe search bar is active
FloatingWindowA modal is open (preview, description, running command, guide)
ConfirmationPromptUser 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

  1. Create a shell script in core/tabs/<tab-name>/<category>/your-script.sh
  2. 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"
  1. Add preconditions if the script is distro-specific
  2. Run cargo xtask docgen to update the documentation
  3. Run cargo run to test it locally

Key Dependencies

CratePurpose
ratatuiTUI rendering framework
crosstermCross-platform terminal control (raw mode, events)
portable-ptyPseudo-terminal allocation for running commands
tui-termPTY output rendering widget for ratatui
vt100-cttVT100 terminal emulator (parses ANSI escape codes)
ego-treeGeneric tree structure used for the menu
include_dirEmbed entire directory trees into the binary at compile time
serde + tomlDeserialize tab_data.toml and config files
clapCLI argument parsing
tree-sitter-bashBash syntax highlighting in script previews