replace string settings with typed enums

- preserve local wall-time handling and import completion timestamps
- validate invalid properties without overwriting items
This commit is contained in:
Chili Palmer
2026-08-19 21:44:10 +02:00
parent 1f9f00ae7a
commit a6b38af396
7 changed files with 494 additions and 239 deletions

View File

@@ -15,7 +15,10 @@ use crate::{
MacroAction, MacroContext, MacroRuntime, MenuChoice, PromptKind, key_to_source, macro_name,
parse_key_binding,
},
model::{Category, DocumentSettings, Item, ItemChanges, MacroDef, ViewDef},
model::{
Category, DateOrder, DocumentSettings, DonePolicy, Item, ItemChanges, MacroDef,
TrashPolicy, ViewDef, ViewKind, WeekStart,
},
preferences::AppPreferences,
};
@@ -397,7 +400,7 @@ impl App {
let today = Local::now().date_naive();
if today != self.current_day {
self.current_day = today;
if self.document_settings.trash_policy == "end-of-day" {
if self.document_settings.trash_policy == TrashPolicy::EndOfDay {
let count = self.db.empty_trash()?;
if count > 0 {
self.status = format!("Emptied {count} Trash item(s) at end of day");
@@ -985,9 +988,9 @@ impl App {
if ids.is_empty() {
return Ok(());
}
let restore = self.current_view().kind == "trash";
let restore = self.current_view().kind == ViewKind::Trash;
if !restore
&& self.document_settings.trash_policy == "immediate"
&& self.document_settings.trash_policy == TrashPolicy::Immediate
&& self.preferences.confirm_destructive
{
self.mode = Mode::Confirm {
@@ -1000,7 +1003,7 @@ impl App {
}
fn apply_discard(&mut self, ids: Vec<i64>, restore: bool) -> Result<()> {
let permanent = !restore && self.document_settings.trash_policy == "immediate";
let permanent = !restore && self.document_settings.trash_policy == TrashPolicy::Immediate;
self.db.discard(&ids, !restore)?;
self.marked.clear();
self.refresh()?;
@@ -1174,13 +1177,33 @@ impl App {
props.values[props.field].pop();
}
KeyCode::Enter => {
let priority = props.values[0].parse().unwrap_or(3);
let when_at = normalize_date(&self.db, &props.values[1])?;
let alarm_at = normalize_date(&self.db, &props.values[2])?;
let numeric_value = if props.values[3].trim().is_empty() {
None
} else {
props.values[3].parse().ok()
let parsed = (|| -> Result<_> {
let priority = props.values[0]
.parse::<i64>()
.context("priority must be a number from 1 to 5")?;
if !(1..=5).contains(&priority) {
bail!("priority must be a number from 1 to 5");
}
let when_at = normalize_date(&self.db, &props.values[1])?;
let alarm_at = normalize_date(&self.db, &props.values[2])?;
let numeric_value = if props.values[3].trim().is_empty() {
None
} else {
Some(
props.values[3]
.parse::<f64>()
.context("numeric value must be a number")?,
)
};
Ok((priority, when_at, alarm_at, numeric_value))
})();
let (priority, when_at, alarm_at, numeric_value) = match parsed {
Ok(values) => values,
Err(error) => {
self.status = format!("Cannot save properties: {error:#}");
self.mode = Mode::Properties(props);
return Ok(());
}
};
self.db.update_item(
props.item_id,
@@ -1436,11 +1459,11 @@ impl App {
values: vec![
settings.description.clone(),
yes_no(settings.backup_on_open),
settings.trash_policy.clone(),
settings.done_policy.clone(),
settings.trash_policy.to_string(),
settings.done_policy.to_string(),
yes_no(settings.automatic_filing),
settings.date_order.clone(),
settings.week_start.clone(),
settings.date_order.to_string(),
settings.week_start.to_string(),
settings.default_time.clone(),
settings.morning_time.clone(),
settings.afternoon_time.clone(),
@@ -1471,7 +1494,7 @@ impl App {
vec![
c.name,
parent,
c.kind,
c.kind.to_string(),
c.match_text,
yes_no(c.exclusive),
condition,
@@ -1502,10 +1525,10 @@ impl App {
let sections = v.sections_spec();
vec![
v.name,
v.kind,
v.kind.to_string(),
v.filter_value,
v.filter_expr,
v.sort_key,
v.sort_key.to_string(),
yes_no(v.show_done),
columns,
sections,
@@ -1636,11 +1659,27 @@ impl App {
let settings = DocumentSettings {
description: form.values[0].trim().into(),
backup_on_open: parse_yes_no(&form.values[1])?,
trash_policy: form.values[2].trim().to_lowercase(),
done_policy: form.values[3].trim().to_lowercase(),
trash_policy: form.values[2]
.trim()
.to_lowercase()
.parse::<TrashPolicy>()
.map_err(anyhow::Error::msg)?,
done_policy: form.values[3]
.trim()
.to_lowercase()
.parse::<DonePolicy>()
.map_err(anyhow::Error::msg)?,
automatic_filing: parse_yes_no(&form.values[4])?,
date_order: form.values[5].trim().to_lowercase(),
week_start: form.values[6].trim().to_lowercase(),
date_order: form.values[5]
.trim()
.to_lowercase()
.parse::<DateOrder>()
.map_err(anyhow::Error::msg)?,
week_start: form.values[6]
.trim()
.to_lowercase()
.parse::<WeekStart>()
.map_err(anyhow::Error::msg)?,
default_time: form.values[7].trim().into(),
morning_time: form.values[8].trim().into(),
afternoon_time: form.values[9].trim().into(),
@@ -1707,7 +1746,7 @@ impl App {
let Some(item_id) = self.selected_item().map(|i| i.id) else {
return Ok(());
};
let Some(all) = self.views.iter().find(|v| v.kind == "list") else {
let Some(all) = self.views.iter().find(|v| v.kind == ViewKind::List) else {
return Ok(());
};
let choices = self
@@ -1963,7 +2002,9 @@ fn normalize_date(db: &Database, value: &str) -> Result<Option<String>> {
} else if chrono::DateTime::parse_from_rfc3339(s).is_ok() {
Ok(Some(s.into()))
} else {
db.interpret_date(&format!("on {s}"))
Ok(Some(db.interpret_date(&format!("on {s}"))?.with_context(
|| format!("unrecognized date or time {s:?}"),
)?))
}
}
fn inside(r: Rect, x: u16, y: u16) -> bool {
@@ -2235,4 +2276,38 @@ mod tests {
assert!(matches!(input.kind, InputKind::Note(_)));
assert_eq!(input.value, "x");
}
#[test]
fn invalid_properties_remain_open_without_overwriting_the_item() {
let directory = tempdir().unwrap();
let path = directory.path().join("invalid-properties.agnd");
let mut db = Database::open(&path).unwrap();
let item_id = db.add_item("Keep my properties").unwrap();
let mut app = App::new_with_preferences(
db,
path,
AppPreferences::default(),
directory.path().join("preferences.toml"),
)
.unwrap();
app.mode = Mode::Properties(PropsState {
item_id,
values: [
"not-a-priority".into(),
"not-a-date".into(),
String::new(),
"not-a-number".into(),
String::new(),
],
field: 0,
});
app.handle_key(key(KeyCode::Enter)).unwrap();
assert!(matches!(app.mode, Mode::Properties(_)));
assert!(app.status.starts_with("Cannot save properties:"));
assert_eq!(app.items[0].priority, 3);
assert!(app.items[0].when_at.is_none());
assert!(app.items[0].numeric_value.is_none());
}
}