cuj-tui 0.1.0

cui — a read-only TUI browser for cuj vaults
Documentation
//! cui — TUI browser for cuj vaults.
//!
//! Evernote-shaped, keyboard-only: a jot list on the left, the
//! selected jot (metadata strip + highlighted content) on the
//! right, global commands in the top bar, context commands below
//! it, and an ex-style ':' command line at the bottom. Browsing
//! commits nothing and never reindexes an extension chain; the one
//! write path is `e`, which hands the selected jot to $EDITOR via
//! the cuj library's edit flow.

pub mod cmdline;
pub mod error;
pub mod highlight;
pub mod keymap;
pub mod query;
pub mod snapshot;
pub mod state;
pub mod ui;

pub use error::{Error, Result};

use ratatui::DefaultTerminal;
use ratatui::crossterm::event::{self, Event, KeyEventKind};

use keymap::Command;
use state::{AppState, Level, Mode};

/// The event loop: blocking reads (the browser has no background
/// activity), one redraw per event.
pub fn run(terminal: &mut DefaultTerminal, mut state: AppState) -> Result<()> {
    loop {
        state.ensure_content();
        terminal.draw(|f| ui::draw(f, &mut state))?;
        match event::read()? {
            Event::Key(key) if key.kind != KeyEventKind::Release => match state.mode {
                Mode::Command => cmdline::handle_key(&mut state, &key),
                Mode::Help => state.mode = Mode::Normal,
                Mode::Normal => match keymap::lookup(&state, &key) {
                    Some(Command::Edit) => run_editor(terminal, &mut state)?,
                    Some(cmd) => state::apply(&mut state, cmd),
                    None => {}
                },
            },
            _ => {}
        }
        if state.quit {
            return Ok(());
        }
    }
}

/// Suspend the TUI, run `cuj edit` on the selected jot (export to
/// a temp file, $EDITOR, commit if changed), resume, and reload
/// the snapshot when an edit landed.
fn run_editor(terminal: &mut DefaultTerminal, state: &mut AppState) -> Result<()> {
    let Some((r, label)) = state::edit_target(state) else {
        state.set_message(Level::Error, "no jot selected");
        return Ok(());
    };
    ratatui::restore();
    let result = state.app.edit(&cuj::Opts::default(), &r);
    *terminal = ratatui::init();
    terminal.clear()?;
    match result {
        Ok(true) => {
            state::apply(state, Command::Reload);
            state.set_message(Level::Info, format!("edited {label}"));
        }
        Ok(false) => state.set_message(Level::Info, format!("{label} unchanged")),
        Err(e) => state.set_message(Level::Error, e.to_string()),
    }
    Ok(())
}