nvrlandClaude Opus 5 e8634c17a2
Version Check / check-version (pull_request) Successful in 10s
fix: make the template workflow real, and the UI legible
The headline flow — fill a config from its template — did not work. Three
bugs, each individually enough to make the tool feel unfinished:

- `add_missing` was unreachable. `find_binding` returned the first match in
  a linear array and `edit_append` was also bound to `a`, 17 entries earlier.
  Bindings now resolve through one table built from the config, and
  `KeybindsConfig::conflicts()` reports any key bound twice instead of
  letting one silently shadow the other. `add_missing` moves to `<Space>`,
  with `A` to take every missing default at once.
- Item status was ignored on write. Every writer fell back to
  `template_value`, so "(missing)" keys landed in the output regardless and
  saving was close to a copy. Unaccepted entries are now omitted.
- Parse errors were swallowed, then the good file was overwritten. A
  malformed target opened as an empty editor and `:w` truncated it. Parse
  failures are now fatal, reported with context, before the TUI starts.

Also:

- Split "differs from the template" (ItemStatus::Overridden) from "you
  edited this" (ConfigItem::dirty). A real config differs from its example
  on nearly every line, so the old Modified flag painted the whole list
  orange and carried no signal.
- Splice template-only entries in at their natural position instead of
  appending them, so a missing nested key no longer strands itself at the
  bottom of the tree at depth 3.
- Preserve .env comments and blank lines across a save.
- Header naming the output file and the template it is compared against,
  plus missing/changed counts. Previously nothing on screen said which
  files were in play or where `:w` would write.
- `?` opens a two-column keybinding reference built from the user's own
  config, scrollable on short terminals. The status bar drops to a few
  contextual hints; the old one joined 11+ hints into an unwrapped line
  that truncated at 80 columns.
- Foldable groups, which the `+ ` prefix had always implied.
- Search covers values as well as keys, reports `match n/m`, and `Esc`
  restores the pre-search cursor.
- Quitting with unsaved changes is refused; added `:q!`, `:x`, `:w <path>`.
- Status messages expire after 4s instead of permanently replacing hints.
- Cancelling a rename on a just-created item removes it again.
- Editing a Number field with text demotes the type instead of failing
  obscurely at write time.
- Undo-tree branches are reachable via `g-`/`g+` and reported on undo.
- Poll instead of blocking, so resizes repaint; drop the unused mouse
  capture that was costing terminal text selection for nothing.
- `--format` accepts xml, ini and properties, which were already supported.

36 tests, up from 13.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C5mrbEFmwv2A4cdzbcSiS1
2026-09-05 05:01:39 +02:00
2026-03-16 17:41:22 +01:00
2026-03-20 10:48:00 +01:00
2026-03-16 15:46:24 +01:00
2026-03-16 17:59:52 +01:00

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, and Properties files seamlessly.
  • Tree View Navigation: Edit nested data structures in a depth-colored tree view, with foldable groups so a large docker-compose.yml stays readable.
  • Smart Template Discovery: Rule-based resolver automatically discovers relationships (e.g., .env.example vs .env, config.template.properties vs config.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> (or A for all of them).
  • Comment Preservation: .env comments and blank lines survive a save, so the documentation in your .env.example is 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 with i, 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 selection
  • gg / G: Jump to the top / bottom
  • Tab or za: Fold or unfold the selected group
  • zM / zR: Fold / unfold everything
  • /: Search over keys and values. Enter keeps the query for n / N; Esc cancels and restores the cursor
  • n / N: Jump to the next / previous match

Edit

  • i: Edit the value with the cursor at the start. On a group, renames it instead
  • a: Edit the value with the cursor at the end
  • s / 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 / above
  • t: Toggle the selected item between a group and a plain value
  • dd: Delete the selected item and everything nested under it

Template

  • Space: Take the template default for the selected (missing) entry
  • A: Take every missing template default at once

History

  • u / U: Undo / redo
  • g- / g+: Switch between alternative branches of the undo tree

Session

  • ?: Toggle the keybinding reference
  • :: Open the command prompt
  • q: 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
  • :wq or :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 collisions
  • Esc: 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

  1. State Management & Undo Tree (src/app.rs & src/undo.rs)

    • The central state is maintained in the App struct, 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.
  2. 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 ConfigItem represents a single configuration entry. It contains:
      • key: The display key (e.g., port or [0]).
      • path: A Vec<PathSegment> (composed of PathSegment::Key(String) for object keys and PathSegment::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 by dirty, 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.
  3. Format Handlers (src/format/*)

    • Each file format has a dedicated handler (EnvHandler, IniHandler, HierarchicalHandler, PropertiesHandler) implementing the FormatHandler trait.
    • HierarchicalHandler: A generalized processor leveraging serde_json::Value as an intermediary layer to parse and write nested JSON, YAML, TOML, and even XML (via quick-xml transposition). 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.
  4. 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 .example or .template suffixes to find target output files dynamically.
  5. Terminal UI & Event Loop (src/ui.rs & src/runner.rs)

    • UI Rendering (src/ui.rs): Powered by the ratatui library. 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 by crossterm. 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 (like dd or gg), delegates character input to the tui-input backend during active editing, and dispatches actions to mutate the App state.
  6. Neovim Plugin (lua/mould/init.lua)

    • Implements a Lua wrapper that calculates terminal geometries and launches the CLI mould binary inside an ephemeral, floating terminal buffer, ensuring automatic Neovim checktime synchronization upon mould's exit.

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 InsertKey mode and careful handling of InsertVariant demonstrates 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.

S
Description
No description provided
Readme MIT
201 KiB
0 Stars 1 Watchers 0 Forks
2026-09-05 05:07:29 +02:00
Languages
Rust 98.2%
Lua 1.8%