modularize app and database internals
- Extract editor, form, export, and schema modules - Add database workflow integration tests
This commit is contained in:
82
src/app/editor.rs
Normal file
82
src/app/editor.rs
Normal file
@@ -0,0 +1,82 @@
|
||||
use super::InputState;
|
||||
|
||||
pub(super) fn insert_at_cursor(input: &mut InputState, character: char) {
|
||||
input.value.insert(input.cursor, character);
|
||||
input.cursor += character.len_utf8();
|
||||
}
|
||||
|
||||
pub(super) fn insert_str_at_cursor(input: &mut InputState, value: &str) {
|
||||
input.value.insert_str(input.cursor, value);
|
||||
input.cursor += value.len();
|
||||
}
|
||||
|
||||
pub(super) fn delete_before_cursor(input: &mut InputState) {
|
||||
let previous = previous_boundary(&input.value, input.cursor);
|
||||
if previous != input.cursor {
|
||||
input.value.drain(previous..input.cursor);
|
||||
input.cursor = previous;
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn delete_at_cursor(input: &mut InputState) {
|
||||
let next = next_boundary(&input.value, input.cursor);
|
||||
if next != input.cursor {
|
||||
input.value.drain(input.cursor..next);
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn previous_boundary(value: &str, cursor: usize) -> usize {
|
||||
value[..cursor]
|
||||
.char_indices()
|
||||
.next_back()
|
||||
.map_or(0, |(index, _)| index)
|
||||
}
|
||||
|
||||
pub(super) fn next_boundary(value: &str, cursor: usize) -> usize {
|
||||
value[cursor..]
|
||||
.chars()
|
||||
.next()
|
||||
.map_or(cursor, |character| cursor + character.len_utf8())
|
||||
}
|
||||
|
||||
pub(super) fn line_start(value: &str, cursor: usize) -> usize {
|
||||
value[..cursor].rfind('\n').map_or(0, |index| index + 1)
|
||||
}
|
||||
|
||||
pub(super) fn line_end(value: &str, cursor: usize) -> usize {
|
||||
value[cursor..]
|
||||
.find('\n')
|
||||
.map_or(value.len(), |index| cursor + index)
|
||||
}
|
||||
|
||||
fn byte_at_character(value: &str, start: usize, end: usize, column: usize) -> usize {
|
||||
value[start..end]
|
||||
.char_indices()
|
||||
.nth(column)
|
||||
.map_or(end, |(offset, _)| start + offset)
|
||||
}
|
||||
|
||||
pub(super) fn move_cursor_vertically(input: &mut InputState, delta: isize) {
|
||||
let start = line_start(&input.value, input.cursor);
|
||||
let end = line_end(&input.value, input.cursor);
|
||||
let column = input.value[start..input.cursor].chars().count();
|
||||
input.cursor = if delta < 0 {
|
||||
if start == 0 {
|
||||
input.cursor
|
||||
} else {
|
||||
let target_end = start - 1;
|
||||
let target_start = input.value[..target_end]
|
||||
.rfind('\n')
|
||||
.map_or(0, |index| index + 1);
|
||||
byte_at_character(&input.value, target_start, target_end, column)
|
||||
}
|
||||
} else if end == input.value.len() {
|
||||
input.cursor
|
||||
} else {
|
||||
let target_start = end + 1;
|
||||
let target_end = input.value[target_start..]
|
||||
.find('\n')
|
||||
.map_or(input.value.len(), |index| target_start + index);
|
||||
byte_at_character(&input.value, target_start, target_end, column)
|
||||
};
|
||||
}
|
||||
141
src/app/forms.rs
Normal file
141
src/app/forms.rs
Normal file
@@ -0,0 +1,141 @@
|
||||
use anyhow::{Result, bail};
|
||||
|
||||
use super::{ChoiceOption, FormKind};
|
||||
|
||||
const YES_NO_CHOICES: &[(&str, &str)] = &[("Yes", "yes"), ("No", "no")];
|
||||
|
||||
pub(super) fn yes_no(value: bool) -> String {
|
||||
if value { "yes" } else { "no" }.into()
|
||||
}
|
||||
|
||||
pub(super) fn parse_yes(value: &str) -> bool {
|
||||
matches!(
|
||||
value.trim().to_lowercase().as_str(),
|
||||
"y" | "yes" | "true" | "1" | "on"
|
||||
)
|
||||
}
|
||||
|
||||
pub(super) fn parse_yes_no(value: &str) -> Result<bool> {
|
||||
match value.trim().to_lowercase().as_str() {
|
||||
"y" | "yes" | "true" | "1" | "on" => Ok(true),
|
||||
"n" | "no" | "false" | "0" | "off" => Ok(false),
|
||||
_ => bail!("expected yes or no, got {value}"),
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn display_space(value: &str) -> String {
|
||||
if value == " " { "space" } else { value }.into()
|
||||
}
|
||||
|
||||
pub(super) 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"),
|
||||
("Greenscreen — muted phosphor green", "greenscreen"),
|
||||
("Nord — arctic blue dark", "nord"),
|
||||
("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(),
|
||||
)
|
||||
}
|
||||
|
||||
pub(super) fn form_prompt(kind: &FormKind, field: usize) -> &'static str {
|
||||
let prompts: &[&str] = match kind {
|
||||
FormKind::Category(_) => &[
|
||||
"Name",
|
||||
"Parent category",
|
||||
"Kind",
|
||||
"Match phrases",
|
||||
"Mutually exclusive",
|
||||
"Rule condition",
|
||||
"Rule action",
|
||||
],
|
||||
FormKind::View(_) => &[
|
||||
"Name",
|
||||
"Kind",
|
||||
"Filter value",
|
||||
"Boolean filter",
|
||||
"Sort key",
|
||||
"Show done",
|
||||
"Columns",
|
||||
"Sections",
|
||||
],
|
||||
FormKind::Preferences => &[
|
||||
"Theme",
|
||||
"Show command bar",
|
||||
"Show rule info",
|
||||
"Show return markers",
|
||||
"Item marker",
|
||||
"Autosave minutes",
|
||||
"Confirm destructive actions",
|
||||
"Date format",
|
||||
"24-hour clock",
|
||||
"Decimal separator",
|
||||
"Thousands separator",
|
||||
],
|
||||
FormKind::DocumentSettings => &[
|
||||
"Description",
|
||||
"Backup on open",
|
||||
"Trash",
|
||||
"Completed items",
|
||||
"Automatic filing",
|
||||
"Numeric date order",
|
||||
"Week starts",
|
||||
"Default time",
|
||||
"Morning time",
|
||||
"Afternoon time",
|
||||
"Evening time",
|
||||
"Note tab width",
|
||||
],
|
||||
};
|
||||
prompts.get(field).copied().unwrap_or("")
|
||||
}
|
||||
Reference in New Issue
Block a user