From a2749383684ab57a9f391a80753fa605b77d54bf Mon Sep 17 00:00:00 2001 From: Nils Pukropp Date: Mon, 16 Mar 2026 17:41:22 +0100 Subject: [PATCH] rename + final ui design --- .gitea/workflows/release.yaml | 8 +-- Cargo.lock | 32 ++++----- Cargo.toml | 4 +- LICENSE | 2 +- README.md | 48 ++++++++----- src/cli.rs | 2 +- src/config.rs | 2 +- src/ui.rs | 123 +++++++++++++++++++++------------- 8 files changed, 131 insertions(+), 90 deletions(-) diff --git a/.gitea/workflows/release.yaml b/.gitea/workflows/release.yaml index 1a6b62f..c074cc6 100644 --- a/.gitea/workflows/release.yaml +++ b/.gitea/workflows/release.yaml @@ -36,10 +36,6 @@ jobs: id: check_release shell: bash run: | - # Use curl to check if a release for the current TAG already exists on Gitea - # This prevents the workflow from failing if a push to main doesn't update the version - # Assuming standard Gitea API (e.g., https://gitea.example.com/api/v1/repos/{owner}/{repo}/releases/tags/{tag}) - # github.repository is usually 'owner/repo' HTTP_STATUS=$(curl -s -o /dev/null -w "%{http_code}" \ -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \ "${{ github.server_url }}/api/v1/repos/${{ github.repository }}/releases/tags/${{ steps.get_version.outputs.TAG }}") @@ -55,7 +51,7 @@ jobs: if: steps.check_release.outputs.EXISTS == 'false' run: | cargo build --release - mv target/release/cenv target/release/cenv-${{ steps.get_version.outputs.TAG }}-linux-amd64 + mv target/release/mould target/release/mould-${{ steps.get_version.outputs.TAG }}-linux-amd64 - name: Create Release and Upload Asset if: steps.check_release.outputs.EXISTS == 'false' @@ -67,7 +63,7 @@ jobs: Automated release for version ${{ steps.get_version.outputs.VERSION }} Commit: ${{ github.sha }} Branch: ${{ github.ref_name }} - files: target/release/cenv-${{ steps.get_version.outputs.TAG }}-linux-amd64 + files: target/release/mould-${{ steps.get_version.outputs.TAG }}-linux-amd64 draft: false prerelease: false env: diff --git a/Cargo.lock b/Cargo.lock index afc8502..0b61e3c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -151,22 +151,6 @@ dependencies = [ "rustversion", ] -[[package]] -name = "cenv-rs" -version = "0.2.0" -dependencies = [ - "clap", - "crossterm", - "dirs", - "ratatui", - "serde", - "serde_json", - "serde_yaml", - "tempfile", - "toml", - "tui-input", -] - [[package]] name = "cfg-if" version = "1.0.4" @@ -796,6 +780,22 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "mould" +version = "0.2.0" +dependencies = [ + "clap", + "crossterm", + "dirs", + "ratatui", + "serde", + "serde_json", + "serde_yaml", + "tempfile", + "toml", + "tui-input", +] + [[package]] name = "nix" version = "0.29.0" diff --git a/Cargo.toml b/Cargo.toml index a2a8a3d..eb75373 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,11 +1,11 @@ [package] -name = "cenv-rs" +name = "mould" version = "0.2.0" edition = "2024" authors = ["Nils Pukropp "] [[bin]] -name = "cenv" +name = "mould" path = "src/main.rs" [dependencies] diff --git a/LICENSE b/LICENSE index f3218c3..db498fc 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2026 The cenv-rs Contributors +Copyright (c) 2026 The mould Contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index a24f28a..f4ec789 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,16 @@ -# cenv-rs +# mould -cenv-rs is a Rust-based Terminal User Interface (TUI) tool designed to help developers interactively generate `.env` files from `.env.example` templates. With a focus on speed and usability, it features Vim-like keybindings and out-of-the-box support for theming, defaulting to the Catppuccin Mocha palette. +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`, or `TOML` configurations, mould provides a fast, Vim-inspired workflow to get your environment ready. ## Features -- Parse `.env.example` files to extract keys, default values, and comments. -- Vim-like keybindings for quick navigation and editing. -- Built-in theming support with Catppuccin Mocha as the default. -- Configurable through a standard TOML file. +- **Universal Format Support**: Handle `.env`, `JSON`, `YAML`, and `TOML` seamlessly. +- **Hierarchical Flattening**: Edit nested data structures (JSON, YAML, TOML) in a flat, searchable list. +- **Docker Compose Integration**: Automatically generate `docker-compose.override.yml` from `docker-compose.yml`. +- **Vim-inspired Workflow**: Navigate with `j`/`k`, edit with `i`, and save with `:w`. +- **Modern UI**: A polished, rounded interface featuring the Catppuccin Mocha palette. +- **Highly Configurable**: Customize keybindings and themes via a simple TOML configuration. +- **Dynamic Alignment**: Automatically aligns keys and values for perfect vertical readability. ## Installation @@ -21,41 +24,54 @@ Alternatively, you can build from source: ```sh git clone -cd cenv-rs +cd mould cargo build --release ``` +The binary will be installed as `mould`. + ## Usage -Navigate to a directory containing a `.env.example` file and run: +Provide an input template file to start editing: ```sh -cenv-rs +mould .env.example +mould docker-compose.yml +mould config.template.json -o config.json ``` -### Keybindings +### Keybindings (Default) - **Normal Mode** - `j` / `Down`: Move selection down - `k` / `Up`: Move selection up - `i`: Edit the value of the currently selected key (Enter Insert Mode) - - `:w` or `Enter`: Save the current configuration to `.env` - - `q` or `:q`: Quit the application without saving - - `Esc`: Clear current prompt or return from actions + - `:w` or `Enter`: Save the current configuration to the output file + - `:q` or `q`: Quit the application + - `:wq`: Save and quit + - `Esc`: Clear current command prompt - **Insert Mode** - Type your value for the selected key. - - `Esc`: Return to Normal Mode + - Arrow keys: Navigate within the input field + - `Enter` / `Esc`: Commit the value and return to Normal Mode ## Configuration -cenv-rs can be configured using a `config.toml` file located in your user configuration directory (e.g., `~/.config/cenv-rs/config.toml` on Linux/macOS). +mould can be configured using a `config.toml` file located in your user configuration directory (e.g., `~/.config/mould/config.toml` on Linux/macOS). Example configuration: ```toml +[keybinds] +down = "j" +up = "k" +edit = "i" +save = ":w" +quit = ":q" +normal_mode = "Esc" + [theme] -# Default theme is "catppuccin_mocha" name = "catppuccin_mocha" ``` diff --git a/src/cli.rs b/src/cli.rs index fdb1dbb..447e9d5 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -2,7 +2,7 @@ use clap::Parser; use std::path::PathBuf; #[derive(Parser, Debug)] -#[command(author, version, about = "TUI tool to generate and edit configuration files (.env, json, yaml, toml)")] +#[command(author, version, about = "mould: A TUI tool to generate and edit configuration files (.env, json, yaml, toml)")] pub struct Cli { /// The input template file (e.g., .env.example, config.json.template, docker-compose.yml) pub input: PathBuf, diff --git a/src/config.rs b/src/config.rs index 88334d8..0773f72 100644 --- a/src/config.rs +++ b/src/config.rs @@ -47,7 +47,7 @@ pub struct Config { pub fn load_config() -> Config { if let Some(mut config_dir) = dirs::config_dir() { - config_dir.push("cenv-rs"); + config_dir.push("mould"); config_dir.push("config.toml"); if config_dir.exists() { diff --git a/src/ui.rs b/src/ui.rs index 5636f9c..e40bba2 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -4,39 +4,55 @@ use ratatui::{ layout::{Constraint, Direction, Layout}, style::{Color, Modifier, Style}, text::{Line, Span}, - widgets::{Block, Borders, List, ListItem, ListState, Paragraph}, + widgets::{Block, BorderType, Borders, List, ListItem, ListState, Paragraph}, Frame, }; // Catppuccin Mocha Palette -const MANTLE: Color = Color::Rgb(24, 24, 37); -const BASE: Color = Color::Rgb(30, 30, 46); +const CRUST: Color = Color::Rgb(17, 17, 27); +const SURFACE0: Color = Color::Rgb(49, 50, 68); +const SURFACE1: Color = Color::Rgb(69, 71, 90); const TEXT: Color = Color::Rgb(205, 214, 244); const BLUE: Color = Color::Rgb(137, 180, 250); const GREEN: Color = Color::Rgb(166, 227, 161); -const SURFACE1: Color = Color::Rgb(69, 71, 90); +const LAVENDER: Color = Color::Rgb(180, 190, 254); +const MAUVE: Color = Color::Rgb(203, 166, 247); +const PEACH: Color = Color::Rgb(250, 179, 135); pub fn draw(f: &mut Frame, app: &mut App, _config: &Config) { let size = f.area(); - // Theming - let bg_color = BASE; - let fg_color = TEXT; - let highlight_color = BLUE; - let insert_color = GREEN; - // Background - let block = Block::default().style(Style::default().bg(bg_color).fg(fg_color)); - f.render_widget(block, size); + f.render_widget(Block::default().style(Style::default().bg(CRUST)), size); + + // Main layout with horizontal padding + let outer_layout = Layout::default() + .direction(Direction::Horizontal) + .constraints([ + Constraint::Length(1), // Left padding + Constraint::Min(0), // Content + Constraint::Length(1), // Right padding + ]) + .split(size); let chunks = Layout::default() .direction(Direction::Vertical) .constraints([ + Constraint::Length(1), // Top padding Constraint::Min(3), // List Constraint::Length(3), // Input area + Constraint::Length(1), // Bottom padding Constraint::Length(1), // Status bar ]) - .split(size); + .split(outer_layout[1]); + + let max_key_len = app + .vars + .iter() + .map(|v| v.key.len()) + .max() + .unwrap_or(20) + .min(40); // Cap at 40 to prevent long keys from hiding values // List let items: Vec = app @@ -44,23 +60,39 @@ pub fn draw(f: &mut Frame, app: &mut App, _config: &Config) { .iter() .enumerate() .map(|(i, var)| { - let style = if i == app.selected { - Style::default() - .fg(bg_color) - .bg(highlight_color) - .add_modifier(Modifier::BOLD) - } else { - Style::default().fg(fg_color) - }; - - let val = if i == app.selected && matches!(app.mode, Mode::Insert) { + let is_selected = i == app.selected; + + let val = if is_selected && matches!(app.mode, Mode::Insert) { app.input.value() } else { &var.value }; - let content = format!(" {} = {} ", var.key, val); - ListItem::new(Line::from(content)).style(style) + let key_style = if is_selected { + Style::default().fg(CRUST).add_modifier(Modifier::BOLD) + } else { + Style::default().fg(LAVENDER) + }; + + let value_style = if is_selected { + Style::default().fg(CRUST) + } else { + Style::default().fg(TEXT) + }; + + let line = Line::from(vec![ + Span::styled(format!(" {: insert_color, + let input_border_color = match app.mode { + Mode::Insert => GREEN, Mode::Normal => SURFACE1, }; @@ -103,37 +131,38 @@ pub fn draw(f: &mut Frame, app: &mut App, _config: &Config) { let cursor_pos = app.input.visual_cursor(); let input = Paragraph::new(input_text) - .style(Style::default().fg(fg_color)) + .style(Style::default().fg(TEXT)) .block( Block::default() .borders(Borders::ALL) + .border_type(BorderType::Rounded) .title(input_title) - .border_style(Style::default().fg(input_color)), + .title_style(Style::default().fg(PEACH).add_modifier(Modifier::BOLD)) + .border_style(Style::default().fg(input_border_color)), ); - f.render_widget(input, chunks[1]); + f.render_widget(input, chunks[2]); if let Mode::Insert = app.mode { f.set_cursor_position(ratatui::layout::Position::new( - chunks[1].x + 1 + cursor_pos as u16, - chunks[1].y + 1, + chunks[2].x + 1 + cursor_pos as u16, + chunks[2].y + 1, )); } - // Status bar - let status_style = Style::default().bg(MANTLE).fg(fg_color); + // Status bar (modern pill style at the bottom) let (mode_str, mode_style) = match app.mode { Mode::Normal => ( " NORMAL ", Style::default() .bg(BLUE) - .fg(bg_color) + .fg(CRUST) .add_modifier(Modifier::BOLD), ), Mode::Insert => ( " INSERT ", Style::default() .bg(GREEN) - .fg(bg_color) + .fg(CRUST) .add_modifier(Modifier::BOLD), ), }; @@ -147,9 +176,9 @@ pub fn draw(f: &mut Frame, app: &mut App, _config: &Config) { let status_line = Line::from(vec![ Span::styled(mode_str, mode_style), - Span::styled(format!(" {} ", status_msg), status_style), + Span::styled(format!(" {} ", status_msg), Style::default().bg(SURFACE0).fg(TEXT)), ]); - let status = Paragraph::new(status_line).style(status_style); - f.render_widget(status, chunks[2]); + let status = Paragraph::new(status_line).style(Style::default().bg(SURFACE0)); + f.render_widget(status, chunks[4]); }