Implement TUI colon command mode

This commit is contained in:
Hermes Agent
2026-08-10 08:28:55 +00:00
parent 639ae0d08e
commit d66d9b0f08
7 changed files with 1502 additions and 89 deletions

View File

@@ -5,6 +5,7 @@
pub mod action;
pub mod app;
pub mod command;
pub mod editor;
pub mod runtime;
pub mod sidebar;
@@ -100,6 +101,17 @@ pub fn run(terminal: &mut DefaultTerminal) -> io::Result<()> {
{
apply_authentication_event(&mut app, coordinator, &executor, event);
}
if let Some(effect) = app.handle_command_input(key.code, key.modifiers) {
key_resolver.reset();
apply_app_effect(
&mut app,
effect,
&mut authentication,
&executor,
&mut clipboard_cancellations,
);
continue;
}
if !key.modifiers.intersects(
crossterm::event::KeyModifiers::CONTROL
| crossterm::event::KeyModifiers::ALT
@@ -116,86 +128,16 @@ pub fn run(terminal: &mut DefaultTerminal) -> io::Result<()> {
}
} 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::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::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::Action(action) => {
let effect = app.dispatch(action);
apply_app_effect(
&mut app,
effect,
&mut authentication,
&executor,
&mut clipboard_cancellations,
);
}
KeyResolution::Pending => {
app.report_status("Key sequence pending; Esc cancels".to_owned());
}
@@ -218,6 +160,86 @@ pub fn run(terminal: &mut DefaultTerminal) -> io::Result<()> {
Ok(())
}
fn apply_app_effect(
app: &mut App,
effect: AppEffect,
authentication: &mut Option<AuthenticationCoordinator>,
executor: &AsyncExecutor,
clipboard_cancellations: &mut ClipboardCancellations,
) {
match effect {
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::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::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::RunCommand(_) | AppEffect::OpenWorkflow(_) | AppEffect::None => {}
}
}
fn is_dispatchable_key_kind(kind: KeyEventKind) -> bool {
matches!(kind, KeyEventKind::Press | KeyEventKind::Repeat)
}