Implement the password-store tree sidebar

This commit is contained in:
Hermes Agent
2026-08-10 06:07:05 +00:00
parent 42e6a82b2d
commit 91f58bae3a
7 changed files with 1097 additions and 114 deletions

View File

@@ -6,6 +6,7 @@
pub mod action;
pub mod app;
pub mod runtime;
pub mod sidebar;
pub mod terminal;
pub mod ui;
@@ -16,7 +17,7 @@ use ratatui::DefaultTerminal;
use crate::{
action::resolve_key,
app::{App, AsyncPayload, StartupSummary},
app::{App, AppEffect, AsyncPayload, StartupData},
runtime::AsyncExecutor,
};
@@ -27,23 +28,16 @@ const TICK_INTERVAL: Duration = Duration::from_millis(250);
pub fn run(terminal: &mut DefaultTerminal) -> io::Result<()> {
let mut app = App::new();
let executor = AsyncExecutor::new();
let startup = app.begin_request();
executor.submit(startup, || {
ironstorage::config::Config::load(None)
.map(|config| {
AsyncPayload::Startup(StartupSummary {
configuration: config.source().display().to_string(),
vault: config.vault().display().to_string(),
})
})
.map_err(|error| error.to_string())
});
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();
@@ -52,8 +46,18 @@ pub fn run(terminal: &mut DefaultTerminal) -> io::Result<()> {
match event::read()? {
Event::Key(key) if key.kind == KeyEventKind::Press => {
if let Some(action) = resolve_key(app.mode(), key.code, key.modifiers) {
app.dispatch(action);
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),
@@ -62,3 +66,58 @@ pub fn run(terminal: &mut DefaultTerminal) -> io::Result<()> {
}
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,
}
}