Define the Mutt-like TUI keymap
This commit is contained in:
@@ -22,7 +22,7 @@ use crossterm::event::{self, Event, KeyEventKind};
|
||||
use ratatui::DefaultTerminal;
|
||||
|
||||
use crate::{
|
||||
action::resolve_key,
|
||||
action::{KeyResolution, KeyResolver},
|
||||
app::{App, AppEffect, AsyncPayload, EditorSaveFailure, EditorSaveFailureKind, StartupData},
|
||||
runtime::{AsyncExecutor, AuthenticationCoordinator, AuthenticationEvent},
|
||||
};
|
||||
@@ -54,6 +54,7 @@ pub fn run(terminal: &mut DefaultTerminal) -> io::Result<()> {
|
||||
let mut clipboard_cancellations = ClipboardCancellations::default();
|
||||
let mut authentication = None;
|
||||
let mut authentication_initialized = false;
|
||||
let mut key_resolver = KeyResolver::default();
|
||||
let startup = app.begin_latest_request();
|
||||
executor.submit(startup, || {
|
||||
load_startup().map(|startup| AsyncPayload::Startup(Box::new(startup)))
|
||||
@@ -93,7 +94,7 @@ pub fn run(terminal: &mut DefaultTerminal) -> io::Result<()> {
|
||||
}
|
||||
|
||||
match event::read()? {
|
||||
Event::Key(key) if key.kind == KeyEventKind::Press => {
|
||||
Event::Key(key) if is_dispatchable_key_kind(key.kind) => {
|
||||
if let Some(coordinator) = authentication.as_mut()
|
||||
&& let Some(event) = coordinator.touch_user_activity()
|
||||
{
|
||||
@@ -105,37 +106,40 @@ pub fn run(terminal: &mut DefaultTerminal) -> io::Result<()> {
|
||||
| crossterm::event::KeyModifiers::SUPER,
|
||||
) && app.handle_editor_input(key.code)
|
||||
{
|
||||
key_resolver.reset();
|
||||
continue;
|
||||
}
|
||||
if app.sidebar().is_editing_filter() {
|
||||
key_resolver.reset();
|
||||
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) {
|
||||
match app.dispatch(action) {
|
||||
AppEffect::RefreshTree => {
|
||||
if let Some(config) = app.config().cloned() {
|
||||
let token = app.begin_latest_request();
|
||||
executor.submit(token, move || {
|
||||
load_tree(&config).map(AsyncPayload::Refreshed)
|
||||
});
|
||||
} else {
|
||||
match key_resolver.feed(app.mode(), key.code, key.modifiers) {
|
||||
KeyResolution::Action(action) => match app.dispatch(action) {
|
||||
AppEffect::RefreshTree => {
|
||||
if let Some(config) = app.config().cloned() {
|
||||
let token = app.begin_latest_request();
|
||||
executor.submit(token, move || {
|
||||
load_tree(&config).map(AsyncPayload::Refreshed)
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
AppEffect::AuthenticateEntry(entry) => {
|
||||
if let Some(coordinator) = authentication.as_mut() {
|
||||
coordinator.request(entry);
|
||||
} else {
|
||||
app.authentication_failed(
|
||||
"operating-system secure storage is unavailable".to_owned(),
|
||||
);
|
||||
AppEffect::AuthenticateEntry(entry) => {
|
||||
if let Some(coordinator) = authentication.as_mut() {
|
||||
coordinator.request(entry);
|
||||
} else {
|
||||
app.authentication_failed(
|
||||
"operating-system secure storage is unavailable".to_owned(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
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 || {
|
||||
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(),
|
||||
@@ -155,40 +159,49 @@ pub fn run(terminal: &mut DefaultTerminal) -> io::Result<()> {
|
||||
.map(AsyncPayload::ClipboardFinished)
|
||||
.map_err(|error| error.to_string())
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
AppEffect::GenerateField(target) => {
|
||||
let token = app.begin_request();
|
||||
executor.submit(token, move || {
|
||||
ironstorage::generate::GeneratorConfig::pass_defaults()
|
||||
.generate_secret(None, false)
|
||||
.map(|password| AsyncPayload::GeneratedField {
|
||||
target,
|
||||
password,
|
||||
})
|
||||
.map_err(|error| error.to_string())
|
||||
});
|
||||
}
|
||||
AppEffect::SaveDocument {
|
||||
config,
|
||||
entry,
|
||||
editor,
|
||||
} => {
|
||||
let token = app.begin_request();
|
||||
executor
|
||||
.submit(token, move || Ok(save_document(&config, entry, editor)));
|
||||
}
|
||||
AppEffect::ManualLock => {
|
||||
if let Some(coordinator) = authentication.as_mut()
|
||||
&& let Err(error) = coordinator.lock()
|
||||
{
|
||||
app.forced_relock("manual lock requested");
|
||||
app.report_status(format!(
|
||||
"locked after secure-store cleanup failed: {error}"
|
||||
));
|
||||
AppEffect::GenerateField(target) => {
|
||||
let token = app.begin_request();
|
||||
executor.submit(token, move || {
|
||||
ironstorage::generate::GeneratorConfig::pass_defaults()
|
||||
.generate_secret(None, false)
|
||||
.map(|password| AsyncPayload::GeneratedField {
|
||||
target,
|
||||
password,
|
||||
})
|
||||
.map_err(|error| error.to_string())
|
||||
});
|
||||
}
|
||||
AppEffect::SaveDocument {
|
||||
config,
|
||||
entry,
|
||||
editor,
|
||||
} => {
|
||||
let token = app.begin_request();
|
||||
executor.submit(token, move || {
|
||||
Ok(save_document(&config, entry, editor))
|
||||
});
|
||||
}
|
||||
AppEffect::ManualLock => {
|
||||
if let Some(coordinator) = authentication.as_mut()
|
||||
&& let Err(error) = coordinator.lock()
|
||||
{
|
||||
app.forced_relock("manual lock requested");
|
||||
app.report_status(format!(
|
||||
"locked after secure-store cleanup failed: {error}"
|
||||
));
|
||||
}
|
||||
}
|
||||
AppEffect::OpenWorkflow(_) => {}
|
||||
AppEffect::None => {}
|
||||
},
|
||||
KeyResolution::Pending => {
|
||||
app.report_status("Key sequence pending; Esc cancels".to_owned());
|
||||
}
|
||||
KeyResolution::Unavailable => {
|
||||
app.report_status("Key is unavailable in the current mode".to_owned());
|
||||
}
|
||||
AppEffect::None => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -205,6 +218,10 @@ pub fn run(terminal: &mut DefaultTerminal) -> io::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn is_dispatchable_key_kind(kind: KeyEventKind) -> bool {
|
||||
matches!(kind, KeyEventKind::Press | KeyEventKind::Repeat)
|
||||
}
|
||||
|
||||
fn load_startup() -> Result<StartupData, String> {
|
||||
let config = ironstorage::config::Config::load(None).map_err(|error| error.to_string())?;
|
||||
let repository = ironstorage::repository::Repository::open(config.vault())
|
||||
@@ -381,6 +398,13 @@ mod tests {
|
||||
use super::*;
|
||||
use crate::{editor::EntryEditor, viewer::test_support::fixture_document_from};
|
||||
|
||||
#[test]
|
||||
fn press_and_terminal_repeat_events_dispatch_but_release_does_not() {
|
||||
assert!(is_dispatchable_key_kind(KeyEventKind::Press));
|
||||
assert!(is_dispatchable_key_kind(KeyEventKind::Repeat));
|
||||
assert!(!is_dispatchable_key_kind(KeyEventKind::Release));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn editor_save_encrypts_and_automatically_commits_entirely_in_storage() {
|
||||
let temporary = tempfile::tempdir().expect("temporary editor store");
|
||||
|
||||
Reference in New Issue
Block a user