better prefs and more themes
This commit is contained in:
162
src/app.rs
162
src/app.rs
@@ -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() {
|
||||
|
||||
Reference in New Issue
Block a user