Implement the authenticated structured entry viewer
This commit is contained in:
@@ -9,8 +9,13 @@ pub mod runtime;
|
||||
pub mod sidebar;
|
||||
pub mod terminal;
|
||||
pub mod ui;
|
||||
pub mod viewer;
|
||||
|
||||
use std::{io, time::Duration};
|
||||
use std::{
|
||||
io,
|
||||
sync::mpsc::{self, Sender},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use crossterm::event::{self, Event, KeyEventKind};
|
||||
use ratatui::DefaultTerminal;
|
||||
@@ -23,11 +28,29 @@ use crate::{
|
||||
|
||||
const TICK_INTERVAL: Duration = Duration::from_millis(250);
|
||||
|
||||
#[derive(Default)]
|
||||
struct ClipboardCancellations(Vec<Sender<()>>);
|
||||
|
||||
impl ClipboardCancellations {
|
||||
fn register(&mut self, cancellation: Sender<()>) {
|
||||
self.0.push(cancellation);
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for ClipboardCancellations {
|
||||
fn drop(&mut self) {
|
||||
for cancellation in self.0.drain(..) {
|
||||
let _ignored = cancellation.send(());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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 mut clipboard_cancellations = ClipboardCancellations::default();
|
||||
let mut authentication = None;
|
||||
let mut authentication_initialized = false;
|
||||
let startup = app.begin_latest_request();
|
||||
@@ -51,7 +74,7 @@ pub fn run(terminal: &mut DefaultTerminal) -> io::Result<()> {
|
||||
if let Some(coordinator) = authentication.as_mut()
|
||||
&& let Some(event) = coordinator.completion()
|
||||
{
|
||||
apply_authentication_event(&mut app, event);
|
||||
apply_authentication_event(&mut app, coordinator, &executor, event);
|
||||
}
|
||||
|
||||
let size = terminal.size()?;
|
||||
@@ -61,7 +84,7 @@ pub fn run(terminal: &mut DefaultTerminal) -> io::Result<()> {
|
||||
app.tick();
|
||||
if let Some(coordinator) = authentication.as_mut() {
|
||||
if let Some(event) = coordinator.poll_lease() {
|
||||
apply_authentication_event(&mut app, event);
|
||||
apply_authentication_event(&mut app, coordinator, &executor, event);
|
||||
}
|
||||
app.update_remaining_lease(coordinator.remaining_time());
|
||||
}
|
||||
@@ -73,7 +96,7 @@ pub fn run(terminal: &mut DefaultTerminal) -> io::Result<()> {
|
||||
if let Some(coordinator) = authentication.as_mut()
|
||||
&& let Some(event) = coordinator.touch_user_activity()
|
||||
{
|
||||
apply_authentication_event(&mut app, event);
|
||||
apply_authentication_event(&mut app, coordinator, &executor, event);
|
||||
}
|
||||
if app.sidebar().is_editing_filter() {
|
||||
if handle_filter_key(&mut app, key.code) {
|
||||
@@ -98,6 +121,33 @@ pub fn run(terminal: &mut DefaultTerminal) -> io::Result<()> {
|
||||
);
|
||||
}
|
||||
}
|
||||
AppEffect::CopyFocused(value) => {
|
||||
if let Some(config) = app.config().cloned() {
|
||||
let (cancel, cancellation) = mpsc::channel();
|
||||
clipboard_cancellations.register(cancel);
|
||||
let token = app.begin_request();
|
||||
executor.submit(token, move || {
|
||||
let mut clipboard =
|
||||
ironstorage::presentation::NativeClipboardManager::system(
|
||||
config.clipboard_timeout(),
|
||||
)
|
||||
.map_err(|error| error.to_string())?;
|
||||
clipboard
|
||||
.copy_with(&value, |duration| {
|
||||
match cancellation.recv_timeout(duration) {
|
||||
Err(mpsc::RecvTimeoutError::Timeout) => {
|
||||
ironstorage::presentation::ClipboardWait::Elapsed
|
||||
}
|
||||
Ok(()) | Err(mpsc::RecvTimeoutError::Disconnected) => {
|
||||
ironstorage::presentation::ClipboardWait::Cancelled
|
||||
}
|
||||
}
|
||||
})
|
||||
.map(AsyncPayload::ClipboardFinished)
|
||||
.map_err(|error| error.to_string())
|
||||
});
|
||||
}
|
||||
}
|
||||
AppEffect::ManualLock => {
|
||||
if let Some(coordinator) = authentication.as_mut()
|
||||
&& let Err(error) = coordinator.lock()
|
||||
@@ -144,16 +194,52 @@ fn load_startup() -> Result<StartupData, String> {
|
||||
Ok(StartupData { config, tree, key })
|
||||
}
|
||||
|
||||
fn apply_authentication_event(app: &mut App, event: AuthenticationEvent) {
|
||||
fn apply_authentication_event(
|
||||
app: &mut App,
|
||||
coordinator: &AuthenticationCoordinator,
|
||||
executor: &AsyncExecutor,
|
||||
event: AuthenticationEvent,
|
||||
) {
|
||||
match event {
|
||||
AuthenticationEvent::Granted(entry) => {
|
||||
app.authentication_granted(entry);
|
||||
if !app.authentication_granted(entry.clone()) {
|
||||
return;
|
||||
}
|
||||
let (Some(config), Some(handle)) = (app.config().cloned(), coordinator.handle()) else {
|
||||
app.authentication_failed(
|
||||
"authentication completed without an active secure-store lease".to_owned(),
|
||||
);
|
||||
return;
|
||||
};
|
||||
let token = app.begin_latest_request();
|
||||
executor.submit(token, move || {
|
||||
load_document(&config, &entry, handle).map(|document| {
|
||||
AsyncPayload::DocumentLoaded {
|
||||
entry,
|
||||
document: Box::new(document),
|
||||
}
|
||||
})
|
||||
});
|
||||
}
|
||||
AuthenticationEvent::Failed(error) => app.authentication_failed(error),
|
||||
AuthenticationEvent::Expired => app.forced_relock("authentication lease expired"),
|
||||
}
|
||||
}
|
||||
|
||||
fn load_document(
|
||||
config: &ironstorage::config::Config,
|
||||
entry: &str,
|
||||
mut handle: ironstorage::authentication::NativeAuthenticationHandle,
|
||||
) -> Result<ironstorage::document::EntryDocument, 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::document::EntryDocumentService::new(&repository, &keys)
|
||||
.open(entry, &mut handle)
|
||||
.map_err(|error| error.to_string())
|
||||
}
|
||||
|
||||
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())?;
|
||||
|
||||
Reference in New Issue
Block a user