Reviewed-on: #19
mould
mould is a modern Terminal User Interface (TUI) tool designed for interactively generating and editing configuration files from templates. Whether you are setting up a .env file from an example, creating a docker-compose.override.yml, or editing nested JSON, YAML, TOML, XML, INI, or Properties configurations, mould provides a fast, Vim-inspired workflow to get your environment ready.
Features
- Universal Format Support: Read, edit, and write
.env,JSON,YAML,TOML,XML,INI, andPropertiesfiles seamlessly. - Tree View Navigation: Edit nested data structures in a depth-colored tree view, with foldable groups so a large
docker-compose.ymlstays readable. - Smart Template Discovery: Rule-based resolver automatically discovers relationships (e.g.,
.env.examplevs.env,config.template.propertiesvsconfig.properties) and highlights missing keys. - Strict Type Preservation: Intelligently preserves data types (integers, booleans, strings) during edit-save cycles, ensuring your configuration stays valid.
- Nothing Written Behind Your Back: Keys that exist only in the template are marked
(missing)and stay out of the output until you pull them in with<Space>(orAfor all of them). - Comment Preservation:
.envcomments and blank lines survive a save, so the documentation in your.env.exampleis not the price of using the tool. - Discoverable: A header names the file being written and the template it is compared against;
?opens the full keybinding reference, built from your config. - Advanced Undo/Redo Engine: Features a tree-based undo/redo history that ensures you never lose changes during complex branching edits. It properly tracks all modifications, including nested renames and item additions/deletions.
- Vim-inspired Workflow: Navigate with
j/k,gg/G, edit withi, search with/, and save with:w. - Modern UI: A polished, rounded interface featuring a semantic Catppuccin Mocha palette with support for terminal transparency.
- Highly Configurable: Customize keybindings and semantic themes via a simple TOML user configuration.
- Neovim Integration: Comes with a built-in Neovim plugin for seamless in-editor configuration management.
Installation
1. CLI Application
Ensure you have Rust and Cargo installed, then run:
# Install directly from the local path
cargo install --path .
Alternatively, you can build from source:
git clone https://git.narl.io/nvrl/mould-rs
cd mould-rs
cargo build --release
# The executable will be located in target/release/mould
2. Neovim Plugin
If you want to use mould directly inside Neovim, the repository includes a built-in Lua plugin that opens mould in a floating terminal buffer and synchronizes the file upon exit.
Using mini.deps:
add({
source = 'https://git.narl.io/nvrl/mould-rs',
checkout = 'main',
})
Usage
Provide an input template file to start editing. mould is smart enough to figure out if it's looking at a template or an active file, and will search for its counterpart to provide live diffing.
mould .env.example
mould docker-compose.yml
mould config.template.json -o config.json
If you just run mould in a directory, it will automatically look for common template patterns like .env.example or docker-compose.yml.
Inside Neovim
Open any configuration file in Neovim and run :Mould. It will launch a floating window targeting your active buffer.
Keybindings (Default)
Press ? inside mould for the same list, rendered from your own configuration.
Navigate
j/k(or arrow keys): Move the selectiongg/G: Jump to the top / bottomTaborza: Fold or unfold the selected groupzM/zR: Fold / unfold everything/: Search over keys and values.Enterkeeps the query forn/N;Esccancels and restores the cursorn/N: Jump to the next / previous match
Edit
i: Edit the value with the cursor at the start. On a group, renames it insteada: Edit the value with the cursor at the ends/S: Replace the value (clears it first)r: Rename the current key or group (array indices cannot be renamed)
Structure
o/O: Add an item below / above (as a sibling, or as an array element)alt+o/alt+O: Add a child group below / abovet: Toggle the selected item between a group and a plain valuedd: Delete the selected item and everything nested under it
Template
Space: Take the template default for the selected(missing)entryA: Take every missing template default at once
History
u/U: Undo / redog-/g+: Switch between alternative branches of the undo tree
Session
?: Toggle the keybinding reference:: Open the command promptq: Quit (refused while there are unsaved changes)
Commands
:w: Write the output file:w <path>: Write somewhere else, and keep writing there:q: Quit;:q!quits and discards changes:wqor:x: Write and quit
Insert / Rename Modes
- Type your value or key. Arrow keys navigate within the field
Enter: Commit and return to Normal mode. Renames are checked for collisionsEsc: Cancel. An item you just created and never named is removed again
Configuration
mould can be configured using a config.toml file located in your user configuration directory:
- Linux/macOS:
~/.config/mould/config.toml - Windows:
%AppData%\mould\config.toml
Example configuration:
[theme]
transparent = false
bg_normal = "#1e1e2e"
bg_highlight = "#89b4fa"
bg_active = "#a6e3a1"
bg_search = "#cba6f7"
fg_normal = "#cdd6f4"
fg_dimmed = "#6c7086"
fg_highlight = "#1e1e2e"
fg_warning = "#f38ba8"
fg_modified = "#fab387"
fg_accent = "#b4befe"
border_normal = "#45475a"
border_active = "#a6e3a1"
tree_depth_1 = "#b4befe"
tree_depth_2 = "#cba6f7"
tree_depth_3 = "#89b4fa"
tree_depth_4 = "#fab387"
[keybinds]
# A binding may list alternatives separated by commas.
# Named keys are written in angle brackets: <Space>, <Tab>.
down = "j"
up = "k"
edit = "i"
edit_append = "a"
edit_substitute = "s,S"
save = ":w"
quit = ":q"
quit_key = "q"
command = ":"
search = "/"
next_match = "n"
previous_match = "N"
jump_top = "gg"
jump_bottom = "G"
append_item = "o"
prepend_item = "O"
delete_item = "dd"
undo = "u"
redo = "U"
undo_branch_prev = "g-"
undo_branch_next = "g+"
rename = "r"
append_group = "alt+o"
prepend_group = "alt+O"
toggle_group = "t"
add_missing = "<Space>"
accept_all = "A"
toggle_fold = "<Tab>,za"
fold_all = "zM"
unfold_all = "zR"
help = "?"
Development & Architecture
mould is written in Rust and architected to decouple the file format parsing from the UI representation. This allows the TUI to render complex, nested configuration files in a unified tree-view.
Core Architectural Principles:
- Separation of Concerns: Clear boundaries between UI rendering, application state, input handling, and file format logic.
- Unified Data Model: All parsed configuration data is normalized into a single
Vec<ConfigItem>structure, simplifying application logic across different file types. - Vim-inspired Modality: The application operates in distinct modes (
Normal,Insert,InsertKey,Search,Help), each with specific keybinding behaviors, enabling efficient interaction. - One Keybinding Table: Every Normal-mode key resolves through
KeybindsConfig::resolved(), so a rebind takes effect in the dispatcher, the status-bar hints and the help overlay at once.KeybindsConfig::conflicts()reports any key bound to two actions rather than letting one silently shadow the other. - Non-linear Undo/Redo: A robust undo tree allows users to revert and re-apply changes across complex branching edit histories.
Key Components
-
State Management & Undo Tree (
src/app.rs&src/undo.rs)- The central state is maintained in the
Appstruct, which tracks the currently loaded configuration variables, application modes, and user input buffer. - It integrates an UndoTree, providing non-linear, branching history tracking for complex edits (additions, deletions, nested renaming). Each significant state change (
save_undo_state) pushes a snapshot to this tree.
- The central state is maintained in the
-
Unified Data Model (
src/format/mod.rs)- Regardless of the underlying file format (JSON, YAML, .env, etc.), all data is translated into a flattened
Vec<ConfigItem>. - A
ConfigItemrepresents a single configuration entry. It contains:key: The display key (e.g.,portor[0]).path: AVec<PathSegment>(composed ofPathSegment::Key(String)for object keys andPathSegment::Index(usize)for array indices) that defines its full hierarchical location.value:Option<String>holding the actual value.is_group: A boolean indicating if this item is a structural node (object or array).status: (ItemStatus::Present,MissingFromActive,Overridden) reflecting comparison with a template. Whether you edited an entry is tracked separately bydirty, so a config that legitimately differs from its example file does not render as if every line had just been touched.value_type: (ValueType::String,Number,Bool,Null) to ensure type preservation during writes.collapsed/leading_trivia: fold state, and the comment lines that preceded the entry in the source file.
- Regardless of the underlying file format (JSON, YAML, .env, etc.), all data is translated into a flattened
-
Format Handlers (
src/format/*)- Each file format has a dedicated handler (
EnvHandler,IniHandler,HierarchicalHandler,PropertiesHandler) implementing theFormatHandlertrait. HierarchicalHandler: A generalized processor leveragingserde_json::Valueas an intermediary layer to parse and write nestedJSON,YAML,TOML, and evenXML(viaquick-xmltransposition). This allows complex structures to be flattened for editing and then re-nested accurately.- These handlers are responsible for translating between the file's native format and the
Vec<ConfigItem>internal representation.
- Each file format has a dedicated handler (
-
Template Resolver (
src/resolver.rs)- Automatically determines template-active file pairings without explicit user instruction.
- Uses hardcoded exact rules (e.g.,
compose.yml->compose.override.yml) and generic fallback rules to strip.exampleor.templatesuffixes to find target output files dynamically.
-
Terminal UI & Event Loop (
src/ui.rs&src/runner.rs)- UI Rendering (
src/ui.rs): Powered by theratatuilibrary. Renders a flexible layout consisting of a styled hierarchical list, an active input field for editing, and a dynamic status bar. - Event Runner (
src/runner.rs): Powered bycrossterm. It polls for events rather than blocking, so terminal resizes and expiring status messages repaint without waiting for a keystroke. It resolves multi-key sequences (likeddorgg), delegates character input to thetui-inputbackend during active editing, and dispatches actions to mutate theAppstate.
- UI Rendering (
-
Neovim Plugin (
lua/mould/init.lua)- Implements a Lua wrapper that calculates terminal geometries and launches the CLI
mouldbinary inside an ephemeral, floating terminal buffer, ensuring automatic Neovimchecktimesynchronization uponmould's exit.
- Implements a Lua wrapper that calculates terminal geometries and launches the CLI
Development Process Highlights:
- Iterative Refinement: Features like key renaming, group creation, and advanced undo/redo were developed iteratively, responding to user feedback and progressively enhancing the core data model and interaction logic.
- Robust Error Handling: Key functions (
commit_input,enter_insert_key) include collision detection and validation to ensure data integrity during user modifications. - Modality-driven Design: The introduction of
InsertKeymode and careful handling ofInsertVariantdemonstrates a commitment to a precise, context-aware user experience, minimizing ambiguity during editing. - Debugging and Performance: Issues like UI "hangs" were traced to subtle interactions in key event processing and input buffer management, leading to refactored key sequence logic for improved responsiveness.
License
This project is licensed under the MIT License. See the LICENSE file for details.