TODO app
A compact TODO app with:
TextInputfor adding itemsSelectableListwith keyboard navigationCommand.Asyncpersistence to diskDialogconfirm on clear-all
mount core.term.prelude.*;
mount core.term.widget.textarea.*;
// --------------------------------------------------------------------------
// Domain
// --------------------------------------------------------------------------
type Item is { text: Text, done: Bool };
type Mode is Browsing | Adding | Confirming;
type Model is {
items: List<Item>,
input: TextInputState,
list: ListState,
confirm: DialogState,
mode: Mode,
};
type Msg is
| StartAdd
| FinishAdd
| CancelAdd
| InputKey(KeyEvent)
| Toggle
| Delete
| Up
| Down
| AskClearAll
| ConfirmClearAll
| CancelClearAll
| Saved(Bool)
| Quit;
// --------------------------------------------------------------------------
// Update
// --------------------------------------------------------------------------
implement Model for Model {
type Msg = Msg;
fn init(&self) -> Command<Msg> {
Command.task(async { Msg.Saved(load_from_disk().await) })
}
fn update(&mut self, msg: Msg) -> Command<Msg> {
match msg {
StartAdd => { self.mode = Mode.Adding; self.input.clear(); Command.none() }
CancelAdd => { self.mode = Mode.Browsing; Command.none() }
InputKey(ke) => {
let _ = self.input.handle_key(ke);
Command.none()
}
FinishAdd => {
if self.input.value.len() > 0 {
self.items.push(Item { text: self.input.value.clone(), done: false });
}
self.mode = Mode.Browsing;
self.input.clear();
self.persist()
}
Toggle => {
match self.list.get_selected() {
Some(i) => { self.items[i].done = !self.items[i].done; self.persist() }
None => Command.none(),
}
}
Delete => {
match self.list.get_selected() {
Some(i) => { self.items.remove(i); self.persist() }
None => Command.none(),
}
}
Up => { self.list.select_previous(); Command.none() }
Down => { self.list.select_next(); Command.none() }
AskClearAll => { self.mode = Mode.Confirming; Command.none() }
CancelClearAll => { self.mode = Mode.Browsing; Command.none() }
ConfirmClearAll => {
self.items.clear();
self.mode = Mode.Browsing;
self.persist()
}
Saved(_) => Command.none(),
Quit => Command.quit(),
}
}
fn handle_event(&self, event: Event) -> Maybe<Msg> {
let Event.Key(ke) = event else { return None; };
match self.mode {
Browsing => match ke.code {
KeyCode.Char('a') => Some(Msg.StartAdd),
KeyCode.Char(' ') | KeyCode.Enter => Some(Msg.Toggle),
KeyCode.Char('d') | KeyCode.Delete => Some(Msg.Delete),
KeyCode.Char('c') => Some(Msg.AskClearAll),
KeyCode.Up => Some(Msg.Up),
KeyCode.Down => Some(Msg.Down),
KeyCode.Char('q') | KeyCode.Esc => Some(Msg.Quit),
_ => None,
},
Adding => match ke.code {
KeyCode.Esc => Some(Msg.CancelAdd),
KeyCode.Enter => Some(Msg.FinishAdd),
_ => Some(Msg.InputKey(ke)),
},
Confirming => match ke.code {
KeyCode.Char('y') | KeyCode.Enter => Some(Msg.ConfirmClearAll),
_ => Some(Msg.CancelClearAll),
},
}
}
fn view(&self, f: &mut Frame) {
let chunks = Layout.new()
.direction(Direction.Vertical)
.constraints([Constraint.Length(3), Constraint.Min(1), Constraint.Length(1)])
.split(f.size());
// Header
Block.new()
.title(" TODO — a:add space:toggle d:delete c:clear q:quit ")
.borders(Borders.ALL)
.render(chunks[0], f.buffer);
// Body: list or input
match self.mode {
Adding => {
TextInput.new()
.block(Block.new().title("Add item").borders(Borders.ALL))
.placeholder("type and press Enter…")
.render_stateful(f, chunks[1], &mut self.input.clone());
}
_ => {
let lines: List<Text> = self.items.iter().map(|it| {
let mark = if it.done { "☑" } else { "☐" };
f"{mark} {it.text}"
}).collect();
SelectableList.new(&lines)
.highlight_style(Style.new().reversed())
.highlight_symbol(&"▸ ")
.render_stateful(f, chunks[1], &mut self.list.clone());
}
}
// Status line
let status = f"{self.items.len()} items";
f.buffer.set_string(chunks[2].x, chunks[2].y, &status, Style.new().dim());
// Confirm modal
if self.mode is Mode.Confirming {
let area = centered(f.size(), 40, 6);
Dialog.new("Clear all items? [y/N]").render(area, f.buffer);
}
}
}
// --------------------------------------------------------------------------
// Side effects
// --------------------------------------------------------------------------
impl Model {
fn persist(&self) -> Command<Msg> {
let snapshot = self.items.clone();
Command.task(async {
let ok = save_to_disk(&snapshot).await;
Msg.Saved(ok)
})
}
}
async fn load_from_disk() -> Bool { /* … */ true }
async fn save_to_disk(items: &List<Item>) -> Bool { /* … */ true }
// --------------------------------------------------------------------------
// main
// --------------------------------------------------------------------------
fn main() -> IoResult<()> {
run(Model {
items: List.new(),
input: TextInputState.new(),
list: ListState.new(),
confirm: DialogState.new(),
mode: Mode.Browsing,
})
}
Why this is a good reference
- Three modes, one state machine.
Browsing/Adding/Confirmingdrive both event routing and view rendering — a common pattern. - Input re-uses
TextInput.handle_key. No custom keybindings needed. - Persistence is reified.
self.persist()returns aCommand.task(...)so the caller can decide whether to chain, batch, or ignore it. - Async doesn't leak into
update. Saving is a future; its result (Msg.Saved) is just another message.