124 lines
4.2 KiB
Rust
124 lines
4.2 KiB
Rust
#![forbid(unsafe_code)]
|
|
#![deny(clippy::disallowed_types)]
|
|
|
|
//! Presentation-only state, rendering, and event plumbing for the terminal UI.
|
|
|
|
pub mod action;
|
|
pub mod app;
|
|
pub mod runtime;
|
|
pub mod sidebar;
|
|
pub mod terminal;
|
|
pub mod ui;
|
|
|
|
use std::{io, time::Duration};
|
|
|
|
use crossterm::event::{self, Event, KeyEventKind};
|
|
use ratatui::DefaultTerminal;
|
|
|
|
use crate::{
|
|
action::resolve_key,
|
|
app::{App, AppEffect, AsyncPayload, StartupData},
|
|
runtime::AsyncExecutor,
|
|
};
|
|
|
|
const TICK_INTERVAL: Duration = Duration::from_millis(250);
|
|
|
|
/// Run the interactive event loop. Polling keeps input responsive while storage
|
|
/// work runs on the executor and periodic repaints are pending.
|
|
pub fn run(terminal: &mut DefaultTerminal) -> io::Result<()> {
|
|
let mut app = App::new();
|
|
let executor = AsyncExecutor::new();
|
|
let startup = app.begin_latest_request();
|
|
executor.submit(startup, || load_startup().map(AsyncPayload::Startup));
|
|
|
|
while !app.should_quit() {
|
|
for result in executor.drain() {
|
|
app.apply_result(result);
|
|
}
|
|
|
|
let size = terminal.size()?;
|
|
app.resize(size.width, size.height);
|
|
terminal.draw(|frame| ui::draw(frame, &app))?;
|
|
if !event::poll(TICK_INTERVAL)? {
|
|
app.tick();
|
|
continue;
|
|
}
|
|
|
|
match event::read()? {
|
|
Event::Key(key) if key.kind == KeyEventKind::Press => {
|
|
if app.sidebar().is_editing_filter() {
|
|
if handle_filter_key(&mut app, key.code) {
|
|
submit_filter(&mut app, &executor);
|
|
}
|
|
} else if let Some(action) = resolve_key(app.mode(), key.code, key.modifiers)
|
|
&& app.dispatch(action) == AppEffect::RefreshTree
|
|
&& let Some(config) = app.config().cloned()
|
|
{
|
|
let token = app.begin_latest_request();
|
|
executor.submit(token, move || {
|
|
load_tree(&config).map(AsyncPayload::Refreshed)
|
|
});
|
|
}
|
|
}
|
|
Event::Resize(width, height) => app.resize(width, height),
|
|
_ => {}
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn load_startup() -> Result<StartupData, String> {
|
|
let config = ironstorage::config::Config::load(None).map_err(|error| error.to_string())?;
|
|
let tree = load_tree(&config)?;
|
|
Ok(StartupData { config, tree })
|
|
}
|
|
|
|
fn load_tree(config: &ironstorage::config::Config) -> Result<ironstorage::read::TreeModel, String> {
|
|
let repository = ironstorage::repository::Repository::open(config.vault())
|
|
.map_err(|error| error.to_string())?;
|
|
let keys = ironstorage::crypto::KeyStore::load(config.key_material())
|
|
.map_err(|error| error.to_string())?;
|
|
ironstorage::read::VaultReader::new(&repository, &keys)
|
|
.list(&ironstorage::repository::DirectoryPath::root())
|
|
.map_err(|error| error.to_string())
|
|
}
|
|
|
|
fn submit_filter(app: &mut App, executor: &AsyncExecutor) {
|
|
let query = app.sidebar().filter_query().to_owned();
|
|
if query.is_empty() {
|
|
app.sidebar_mut().reset_filter_results();
|
|
return;
|
|
}
|
|
let Some(config) = app.config().cloned() else {
|
|
return;
|
|
};
|
|
let token = app.begin_latest_request();
|
|
executor.submit(token, move || {
|
|
let repository = ironstorage::repository::Repository::open(config.vault())
|
|
.map_err(|error| error.to_string())?;
|
|
let keys = ironstorage::crypto::KeyStore::load(config.key_material())
|
|
.map_err(|error| error.to_string())?;
|
|
let results = ironstorage::read::VaultReader::new(&repository, &keys)
|
|
.find(std::slice::from_ref(&query))
|
|
.map_err(|error| error.to_string())?;
|
|
Ok(AsyncPayload::Filtered { query, results })
|
|
});
|
|
}
|
|
|
|
fn handle_filter_key(app: &mut App, code: crossterm::event::KeyCode) -> bool {
|
|
use crossterm::event::KeyCode;
|
|
match code {
|
|
KeyCode::Esc => {
|
|
app.sidebar_mut().clear_filter_results();
|
|
false
|
|
}
|
|
KeyCode::Enter => {
|
|
app.sidebar_mut().finish_filter();
|
|
false
|
|
}
|
|
KeyCode::Backspace => app.sidebar_mut().pop_filter_character(),
|
|
KeyCode::Char(character) => app.sidebar_mut().push_filter_character(character),
|
|
_ => false,
|
|
}
|
|
}
|