fix: make the template workflow real, and the UI legible
Version Check / check-version (pull_request) Successful in 10s
Version Check / check-version (pull_request) Successful in 10s
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
This commit is contained in:
1 parent
2e974e0503
commit
e8634c17a2
16 files changed
+2218
-915
No files matched your search
+35
-29
@@ -16,8 +16,8 @@ use log::{error, info, warn};
|
||||
use runner::AppRunner;
|
||||
use std::io;
|
||||
|
||||
use anyhow::Context;
|
||||
use crossterm::{
|
||||
event::{DisableMouseCapture, EnableMouseCapture},
|
||||
execute,
|
||||
terminal::{EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode},
|
||||
};
|
||||
@@ -73,9 +73,15 @@ fn main() -> anyhow::Result<()> {
|
||||
|
||||
info!("Output: {}", output_path.display());
|
||||
|
||||
// 1. Load active config if it exists
|
||||
// 1. Load active config if it exists.
|
||||
//
|
||||
// A parse failure must be fatal: opening an editor over a file we could not
|
||||
// read would present an empty document, and the next `:w` would truncate a
|
||||
// perfectly good config.
|
||||
let mut vars = if let Some(active) = &active_path {
|
||||
handler.parse(active).unwrap_or_default()
|
||||
handler
|
||||
.parse(active)
|
||||
.with_context(|| format!("could not parse {}", active.display()))?
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
@@ -83,57 +89,57 @@ fn main() -> anyhow::Result<()> {
|
||||
// 2. Load template config and merge
|
||||
if let Some(template) = &template_path {
|
||||
info!("Comparing with template: {}", template.display());
|
||||
let template_vars = handler.parse(template).unwrap_or_default();
|
||||
if vars.is_empty() {
|
||||
vars = template_vars;
|
||||
// If we only have template, everything is missing from active initially
|
||||
for v in vars.iter_mut() {
|
||||
v.status = crate::format::ItemStatus::MissingFromActive;
|
||||
v.value = None;
|
||||
}
|
||||
vars = handler
|
||||
.parse(template)
|
||||
.with_context(|| format!("could not parse template {}", template.display()))?;
|
||||
// Only the template exists, so nothing has been accepted into the
|
||||
// active config yet. Keep each template value as the default on offer.
|
||||
for v in vars.iter_mut() {
|
||||
if !v.is_group {
|
||||
v.status = crate::format::ItemStatus::MissingFromActive;
|
||||
v.template_value = v.value.take();
|
||||
v.default_value = v.template_value.clone();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Merge template into active
|
||||
handler.merge(template, &mut vars).unwrap_or_default();
|
||||
handler
|
||||
.merge(template, &mut vars)
|
||||
.with_context(|| format!("could not parse template {}", template.display()))?;
|
||||
}
|
||||
} else if vars.is_empty() {
|
||||
// Fallback if no template and active is empty
|
||||
vars = handler.parse(&input_path)?;
|
||||
vars = handler
|
||||
.parse(&input_path)
|
||||
.with_context(|| format!("could not parse {}", input_path.display()))?;
|
||||
}
|
||||
|
||||
if vars.is_empty() {
|
||||
warn!("No variables found.");
|
||||
}
|
||||
|
||||
let config = load_config();
|
||||
let mut app = App::new(vars);
|
||||
let (config, config_error) = load_config();
|
||||
let mut app = App::new(vars, output_path.clone(), template_path.clone());
|
||||
if let Some(err) = config_error {
|
||||
app.set_status(format!("config.toml ignored: {}", err));
|
||||
}
|
||||
|
||||
// Terminal lifecycle
|
||||
enable_raw_mode()
|
||||
.map_err(|e| MouldError::Terminal(format!("Failed to enable raw mode: {}", e)))?;
|
||||
let mut stdout = io::stdout();
|
||||
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)
|
||||
execute!(stdout, EnterAlternateScreen)
|
||||
.map_err(|e| MouldError::Terminal(format!("Failed to enter alternate screen: {}", e)))?;
|
||||
let backend = CrosstermBackend::new(stdout);
|
||||
let mut terminal = Terminal::new(backend)
|
||||
.map_err(|e| MouldError::Terminal(format!("Failed to create terminal backend: {}", e)))?;
|
||||
|
||||
let mut runner = AppRunner::new(
|
||||
&mut terminal,
|
||||
&mut app,
|
||||
&config,
|
||||
&output_path,
|
||||
handler.as_ref(),
|
||||
);
|
||||
let mut runner = AppRunner::new(&mut terminal, &mut app, &config, handler.as_ref());
|
||||
let res = runner.run();
|
||||
|
||||
// Restoration
|
||||
disable_raw_mode().ok();
|
||||
execute!(
|
||||
terminal.backend_mut(),
|
||||
LeaveAlternateScreen,
|
||||
DisableMouseCapture
|
||||
)
|
||||
.ok();
|
||||
execute!(terminal.backend_mut(), LeaveAlternateScreen).ok();
|
||||
terminal.show_cursor().ok();
|
||||
|
||||
match res {
|
||||
|
||||
Reference in new issue
Block a user