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
102 lines
3.3 KiB
Rust
102 lines
3.3 KiB
Rust
use super::{ConfigItem, FormatHandler, ValueType, PathSegment};
|
|
use java_properties::{LineContent, PropertiesIter, PropertiesWriter};
|
|
use std::fs::File;
|
|
use std::io::{BufReader, BufWriter};
|
|
use std::path::Path;
|
|
|
|
pub struct PropertiesHandler;
|
|
|
|
impl FormatHandler for PropertiesHandler {
|
|
fn parse(&self, path: &Path) -> anyhow::Result<Vec<ConfigItem>> {
|
|
let file = File::open(path)?;
|
|
let reader = BufReader::new(file);
|
|
let iter = PropertiesIter::new(reader);
|
|
|
|
let mut vars = Vec::new();
|
|
let mut groups = std::collections::HashSet::new();
|
|
|
|
for line_result in iter {
|
|
let line = line_result?;
|
|
|
|
if let LineContent::KVPair(path, value) = line.consume_content() {
|
|
// Add groups based on dot notation
|
|
let parts: Vec<&str> = path.split('.').collect();
|
|
let mut current_path = Vec::new();
|
|
|
|
for (i, part) in parts.iter().enumerate().take(parts.len().saturating_sub(1)) {
|
|
current_path.push(PathSegment::Key(part.to_string()));
|
|
|
|
if groups.insert(current_path.clone()) {
|
|
vars.push(ConfigItem::group(*part, current_path.clone(), i));
|
|
}
|
|
}
|
|
|
|
let mut final_path = current_path.clone();
|
|
let last_key = parts.last().unwrap_or(&"").to_string();
|
|
final_path.push(PathSegment::Key(last_key.clone()));
|
|
|
|
vars.push(ConfigItem::leaf(
|
|
last_key,
|
|
final_path,
|
|
value,
|
|
parts.len().saturating_sub(1),
|
|
ValueType::String,
|
|
));
|
|
}
|
|
}
|
|
|
|
|
|
Ok(vars)
|
|
}
|
|
|
|
fn write(&self, path: &Path, vars: &[ConfigItem]) -> anyhow::Result<()> {
|
|
let file = File::create(path)?;
|
|
let writer = BufWriter::new(file);
|
|
let mut prop_writer = PropertiesWriter::new(writer);
|
|
|
|
for var in vars {
|
|
if var.should_write() {
|
|
prop_writer.write(&var.path_string(), var.write_value())?;
|
|
}
|
|
}
|
|
|
|
prop_writer.finish()?;
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use tempfile::NamedTempFile;
|
|
|
|
#[test]
|
|
fn test_group_rename_write() {
|
|
let handler = PropertiesHandler;
|
|
let mut vars = vec![
|
|
ConfigItem::group("server", vec![PathSegment::Key("server".to_string())], 0),
|
|
ConfigItem::leaf(
|
|
"port",
|
|
vec![PathSegment::Key("server".to_string()), PathSegment::Key("port".to_string())],
|
|
"8080",
|
|
1,
|
|
ValueType::String,
|
|
),
|
|
];
|
|
|
|
// Rename "server" to "srv"
|
|
vars[0].key = "srv".to_string();
|
|
vars[0].path = vec![PathSegment::Key("srv".to_string())];
|
|
|
|
// Update child path
|
|
vars[1].path = vec![PathSegment::Key("srv".to_string()), PathSegment::Key("port".to_string())];
|
|
|
|
let file = NamedTempFile::new().unwrap();
|
|
handler.write(file.path(), &vars).unwrap();
|
|
|
|
let content = std::fs::read_to_string(file.path()).unwrap();
|
|
assert!(content.contains("srv.port=8080"));
|
|
assert!(!content.contains("server.port=8080"));
|
|
}
|
|
}
|