better prefs and more themes

This commit is contained in:
Hermes Agent
2026-08-16 20:33:02 +00:00
parent f53ed36f31
commit 93fb3fcdb1
5 changed files with 486 additions and 33 deletions

View File

@@ -55,6 +55,19 @@ pub struct FormState {
pub field: usize,
}
#[derive(Debug, Clone)]
pub struct ChoiceOption {
pub label: String,
pub value: String,
}
#[derive(Debug, Clone)]
pub struct ChoiceState {
pub form: FormState,
pub options: Vec<ChoiceOption>,
pub selected: usize,
}
#[derive(Debug, Clone)]
pub enum Mode {
Normal,
@@ -75,6 +88,7 @@ pub enum Mode {
prompt: String,
action: ConfirmAction,
},
Choice(ChoiceState),
}
#[derive(Debug, Clone)]
@@ -107,6 +121,7 @@ pub struct App {
pub item_rows: Vec<(Rect, i64)>,
pub command_regions: Vec<Rect>,
pub view_regions: Vec<Rect>,
pub choice_regions: Vec<Rect>,
last_autosave: Instant,
current_day: NaiveDate,
}
@@ -158,6 +173,7 @@ impl App {
item_rows: vec![],
command_regions: vec![],
view_regions: vec![],
choice_regions: vec![],
last_autosave: Instant::now(),
current_day: Local::now().date_naive(),
};
@@ -211,6 +227,7 @@ impl App {
Mode::CategoryManager => self.handle_category_manager(key),
Mode::Menu => self.handle_menu(key),
Mode::Confirm { .. } => self.handle_confirm(key),
Mode::Choice(_) => self.handle_choice(key),
}
}
@@ -750,13 +767,13 @@ impl App {
yes_no(self.preferences.show_command_bar),
yes_no(self.preferences.show_rule_info),
yes_no(self.preferences.show_carriage_returns),
self.preferences.item_marker.clone(),
display_space(&self.preferences.item_marker),
self.preferences.autosave_minutes.to_string(),
yes_no(self.preferences.confirm_destructive),
self.preferences.date_format.clone(),
yes_no(self.preferences.clock_24h),
self.preferences.decimal_separator.clone(),
self.preferences.thousands_separator.clone(),
display_space(&self.preferences.thousands_separator),
],
field: 0,
});
@@ -866,6 +883,7 @@ impl App {
let Mode::Form(mut form) = self.mode.clone() else {
return Ok(());
};
let choices = form_choices(&form.kind, form.field);
match key.code {
KeyCode::Esc => {
self.mode = match form.kind {
@@ -880,7 +898,22 @@ impl App {
form.field = (form.field + form.values.len() - 1) % form.values.len()
}
KeyCode::Backspace => {
form.values[form.field].pop();
if choices.is_none() {
form.values[form.field].pop();
}
}
KeyCode::Char(' ') if choices.is_some() => {
let options = choices.unwrap_or_default();
let selected = options
.iter()
.position(|option| option.value == form.values[form.field])
.unwrap_or(0);
self.mode = Mode::Choice(ChoiceState {
form,
options,
selected,
});
return Ok(());
}
KeyCode::Enter => {
match form.kind {
@@ -932,7 +965,7 @@ impl App {
show_command_bar: parse_yes_no(&form.values[1])?,
show_rule_info: parse_yes_no(&form.values[2])?,
show_carriage_returns: parse_yes_no(&form.values[3])?,
item_marker: form.values[4].clone(),
item_marker: decode_space(&form.values[4]),
autosave_minutes: form.values[5]
.parse()
.context("autosave interval must be a number")?,
@@ -940,7 +973,7 @@ impl App {
date_format: form.values[7].trim().to_lowercase(),
clock_24h: parse_yes_no(&form.values[8])?,
decimal_separator: form.values[9].clone(),
thousands_separator: form.values[10].clone(),
thousands_separator: decode_space(&form.values[10]),
};
preferences.save_to(&self.preferences_path)?;
self.preferences = preferences;
@@ -975,7 +1008,9 @@ impl App {
}
return Ok(());
}
KeyCode::Char(c) if !key.modifiers.contains(KeyModifiers::CONTROL) => {
KeyCode::Char(c)
if choices.is_none() && !key.modifiers.contains(KeyModifiers::CONTROL) =>
{
form.values[form.field].push(c)
}
_ => {}
@@ -984,6 +1019,40 @@ impl App {
Ok(())
}
fn handle_choice(&mut self, key: KeyEvent) -> Result<()> {
let Mode::Choice(mut choice) = self.mode.clone() else {
return Ok(());
};
match key.code {
KeyCode::Esc => self.mode = Mode::Form(choice.form),
KeyCode::Up | KeyCode::Char('k') => {
choice.selected = choice.selected.saturating_sub(1);
self.mode = Mode::Choice(choice);
}
KeyCode::Down | KeyCode::Char('j') => {
choice.selected = (choice.selected + 1).min(choice.options.len().saturating_sub(1));
self.mode = Mode::Choice(choice);
}
KeyCode::Home => {
choice.selected = 0;
self.mode = Mode::Choice(choice);
}
KeyCode::End => {
choice.selected = choice.options.len().saturating_sub(1);
self.mode = Mode::Choice(choice);
}
KeyCode::Enter | KeyCode::Char(' ') => {
if let Some(option) = choice.options.get(choice.selected) {
let field = choice.form.field;
choice.form.values[field] = option.value.clone();
}
self.mode = Mode::Form(choice.form);
}
_ => self.mode = Mode::Choice(choice),
}
Ok(())
}
fn open_dependencies(&mut self) -> Result<()> {
let Some(item_id) = self.selected_item().map(|i| i.id) else {
return Ok(());
@@ -1044,6 +1113,19 @@ impl App {
return Ok(());
}
let (x, y) = (event.column, event.row);
if let Mode::Choice(mut choice) = self.mode.clone() {
if let Some(index) = self
.choice_regions
.iter()
.position(|region| inside(*region, x, y))
&& let Some(option) = choice.options.get(index)
{
let field = choice.form.field;
choice.form.values[field] = option.value.clone();
self.mode = Mode::Form(choice.form);
}
return Ok(());
}
if let Some((_, id)) = self.item_rows.iter().find(|(r, _)| inside(*r, x, y)) {
if let Some(pos) = self.items.iter().position(|i| i.id == *id) {
self.selected = pos;
@@ -1082,6 +1164,74 @@ fn parse_yes_no(value: &str) -> Result<bool> {
}
}
fn display_space(value: &str) -> String {
if value == " " { "space" } else { value }.into()
}
fn decode_space(value: &str) -> String {
if value.eq_ignore_ascii_case("space") {
" ".into()
} else {
value.into()
}
}
pub(crate) fn form_choices(kind: &FormKind, field: usize) -> Option<Vec<ChoiceOption>> {
let entries: &[(&str, &str)] = match kind {
FormKind::Preferences => match field {
0 => &[
("Classic — Lotus-inspired blue", "classic"),
("Mono — grayscale", "mono"),
("Amber — warm dark", "amber"),
("Nord — arctic blue dark", "nord"),
("Solarized Dark — balanced low contrast", "solarized-dark"),
("Solarized Light — warm paper light", "solarized-light"),
("Catppuccin Mocha — cozy pastel dark", "catppuccin-mocha"),
],
1..=3 | 6 | 8 => YES_NO_CHOICES,
7 => &[
("ISO — 2026-08-16", "iso"),
("US — 08/16/2026", "us"),
("European — 16/08/2026", "european"),
("Long — 16 Aug 2026", "long"),
],
_ => return None,
},
FormKind::DocumentSettings => match field {
1 | 4 => YES_NO_CHOICES,
2 => &[
("On demand", "on-demand"),
("On close", "on-close"),
("End of day", "end-of-day"),
("Immediately", "immediate"),
],
3 => &[
("Keep completed items", "keep"),
("Move them to Trash", "trash"),
],
5 => &[
("Year / month / day", "ymd"),
("Month / day / year", "mdy"),
("Day / month / year", "dmy"),
],
6 => &[("Monday", "monday"), ("Sunday", "sunday")],
_ => return None,
},
FormKind::Category(_) | FormKind::View(_) => return None,
};
Some(
entries
.iter()
.map(|(label, value)| ChoiceOption {
label: (*label).into(),
value: (*value).into(),
})
.collect(),
)
}
const YES_NO_CHOICES: &[(&str, &str)] = &[("Yes", "yes"), ("No", "no")];
fn normalize_date(db: &Database, value: &str) -> Result<Option<String>> {
let s = value.trim();
if s.is_empty() {