Skip to main content

cui/
lib.rs

1//! cui — TUI browser for cuj vaults.
2//!
3//! Evernote-shaped, keyboard-only: a jot list on the left, the
4//! selected jot (metadata strip + highlighted content) on the
5//! right, global commands in the top bar, context commands below
6//! it, and an ex-style ':' command line at the bottom. Browsing
7//! commits nothing and never reindexes an extension chain; the one
8//! write path is `e`, which hands the selected jot to $EDITOR via
9//! the cuj library's edit flow.
10
11pub mod cmdline;
12pub mod error;
13pub mod highlight;
14pub mod keymap;
15pub mod query;
16pub mod snapshot;
17pub mod state;
18pub mod ui;
19
20pub use error::{Error, Result};
21
22use ratatui::DefaultTerminal;
23use ratatui::crossterm::event::{self, Event, KeyEventKind};
24
25use keymap::Command;
26use state::{AppState, Level, Mode};
27
28/// The event loop: blocking reads (the browser has no background
29/// activity), one redraw per event.
30pub fn run(terminal: &mut DefaultTerminal, mut state: AppState) -> Result<()> {
31    loop {
32        state.ensure_content();
33        terminal.draw(|f| ui::draw(f, &mut state))?;
34        match event::read()? {
35            Event::Key(key) if key.kind != KeyEventKind::Release => match state.mode {
36                Mode::Command => cmdline::handle_key(&mut state, &key),
37                Mode::Help => state.mode = Mode::Normal,
38                Mode::Normal => match keymap::lookup(&state, &key) {
39                    Some(Command::Edit) => run_editor(terminal, &mut state)?,
40                    Some(cmd) => state::apply(&mut state, cmd),
41                    None => {}
42                },
43            },
44            _ => {}
45        }
46        if state.quit {
47            return Ok(());
48        }
49    }
50}
51
52/// Suspend the TUI, run `cuj edit` on the selected jot (export to
53/// a temp file, $EDITOR, commit if changed), resume, and reload
54/// the snapshot when an edit landed.
55fn run_editor(terminal: &mut DefaultTerminal, state: &mut AppState) -> Result<()> {
56    let Some((r, label)) = state::edit_target(state) else {
57        state.set_message(Level::Error, "no jot selected");
58        return Ok(());
59    };
60    ratatui::restore();
61    let result = state.app.edit(&cuj::Opts::default(), &r);
62    *terminal = ratatui::init();
63    terminal.clear()?;
64    match result {
65        Ok(true) => {
66            state::apply(state, Command::Reload);
67            state.set_message(Level::Info, format!("edited {label}"));
68        }
69        Ok(false) => state.set_message(Level::Info, format!("{label} unchanged")),
70        Err(e) => state.set_message(Level::Error, e.to_string()),
71    }
72    Ok(())
73}