diff --git a/core/src/event.rs b/core/src/event.rs index 1b7236c7..3460d203 100644 --- a/core/src/event.rs +++ b/core/src/event.rs @@ -3,8 +3,8 @@ use std::{collections::BTreeMap, ffi::OsString}; use anyhow::Result; use config::{keymap::{Control, KeymapLayer}, open::Opener}; use crossterm::event::KeyEvent; -use shared::{RoCell, Url}; -use tokio::sync::{mpsc::UnboundedSender, oneshot}; +use shared::{InputError, RoCell, Url}; +use tokio::sync::{mpsc::{self, UnboundedSender}, oneshot}; use super::{files::{File, FilesOp}, input::InputOpt, select::SelectOpt}; use crate::manager::PreviewLock; @@ -32,7 +32,7 @@ pub enum Event { // Input Select(SelectOpt, oneshot::Sender>), - Input(InputOpt, oneshot::Sender>), + Input(InputOpt, mpsc::UnboundedSender>), // Tasks Open(Vec<(OsString, String)>, Option), @@ -104,8 +104,9 @@ macro_rules! emit { $crate::Event::Select($opt, tx).wait(rx) }}; (Input($opt:expr)) => {{ - let (tx, rx) = tokio::sync::oneshot::channel(); - $crate::Event::Input($opt, tx).wait(rx) + let (tx, rx) = tokio::sync::mpsc::unbounded_channel(); + $crate::Event::Input($opt, tx).emit(); + rx }}; (Open($targets:expr, $opener:expr)) => { diff --git a/core/src/input/input.rs b/core/src/input/input.rs index ca71e08d..58964e62 100644 --- a/core/src/input/input.rs +++ b/core/src/input/input.rs @@ -1,10 +1,9 @@ use std::ops::Range; -use anyhow::{anyhow, Result}; use config::keymap::Key; use crossterm::event::KeyCode; -use shared::CharKind; -use tokio::sync::oneshot::Sender; +use shared::{CharKind, InputError}; +use tokio::sync::mpsc::UnboundedSender; use unicode_width::UnicodeWidthStr; use super::{mode::InputMode, op::InputOp, InputOpt, InputSnap, InputSnaps}; @@ -17,21 +16,27 @@ pub struct Input { title: String, pub position: Position, - callback: Option>>, + + // Typing + callback: Option>>, + realtime: bool, // Shell pub(super) highlight: bool, } impl Input { - pub fn show(&mut self, opt: InputOpt, tx: Sender>) { + pub fn show(&mut self, opt: InputOpt, tx: UnboundedSender>) { self.close(false); self.snaps.reset(opt.value); self.visible = true; self.title = opt.title; self.position = opt.position; + + // Typing self.callback = Some(tx); + self.realtime = opt.realtime; // Shell self.highlight = opt.highlight; @@ -39,8 +44,8 @@ impl Input { pub fn close(&mut self, submit: bool) -> bool { if let Some(cb) = self.callback.take() { - let _ = - cb.send(if submit { Ok(self.snap_mut().value.clone()) } else { Err(anyhow!("canceled")) }); + let value = self.snap_mut().value.clone(); + let _ = cb.send(if submit { Ok(value) } else { Err(InputError::Canceled(value)) }); } self.visible = false; @@ -201,22 +206,26 @@ impl Input { } pub fn type_str(&mut self, s: &str) -> bool { - let snap = self.snap_mut(); + let snap = self.snaps.current_mut(); if snap.cursor < 1 { snap.value.insert_str(0, s); } else { snap.value.insert_str(snap.idx(snap.cursor).unwrap(), s); } + + self.flush_value(); self.move_(s.chars().count() as isize) } pub fn backspace(&mut self) -> bool { - let snap = self.snap_mut(); + let snap = self.snaps.current_mut(); if snap.cursor < 1 { return false; } else { snap.value.remove(snap.idx(snap.cursor - 1).unwrap()); } + + self.flush_value(); self.move_(-1) } @@ -278,7 +287,7 @@ impl Input { fn handle_op(&mut self, cursor: usize, include: bool) -> bool { let old = self.snap().clone(); - let snap = self.snap_mut(); + let snap = self.snaps.current_mut(); match snap.op { InputOp::None | InputOp::Select(_) => { @@ -296,6 +305,10 @@ impl Input { snap.op = InputOp::None; snap.mode = if insert { InputMode::Insert } else { InputMode::Normal }; snap.cursor = range.start; + + if self.realtime { + self.callback.as_ref().unwrap().send(Err(InputError::Typed(snap.value.clone()))).ok(); + } } InputOp::Yank(_) => { let range = snap.op.range(cursor, include).unwrap(); @@ -316,6 +329,14 @@ impl Input { } true } + + #[inline] + fn flush_value(&self) { + if self.realtime { + let value = self.snap().value.clone(); + self.callback.as_ref().unwrap().send(Err(InputError::Typed(value))).ok(); + } + } } impl Input { diff --git a/core/src/input/option.rs b/core/src/input/option.rs index ae970858..dd96c068 100644 --- a/core/src/input/option.rs +++ b/core/src/input/option.rs @@ -6,6 +6,7 @@ pub struct InputOpt { pub title: String, pub value: String, pub position: Position, + pub realtime: bool, pub highlight: bool, } @@ -15,6 +16,7 @@ impl InputOpt { title: title.as_ref().to_owned(), value: String::new(), position: Position::Top(/* TODO: hardcode */ Rect { x: 0, y: 2, width: 50, height: 3 }), + realtime: false, highlight: false, } } @@ -27,15 +29,24 @@ impl InputOpt { // TODO: hardcode Rect { x: 0, y: 1, width: 50, height: 3 }, ), + realtime: false, highlight: false, } } + #[inline] pub fn with_value(mut self, value: impl AsRef) -> Self { self.value = value.as_ref().to_owned(); self } + #[inline] + pub fn with_realtime(mut self) -> Self { + self.realtime = true; + self + } + + #[inline] pub fn with_highlight(mut self) -> Self { self.highlight = true; self diff --git a/core/src/manager/manager.rs b/core/src/manager/manager.rs index 2e83fc52..56292825 100644 --- a/core/src/manager/manager.rs +++ b/core/src/manager/manager.rs @@ -96,12 +96,12 @@ impl Manager { } tokio::spawn(async move { - let result = emit!(Input(InputOpt::top(format!( + let mut result = emit!(Input(InputOpt::top(format!( "There are {tasks} tasks running, sure to quit? (y/N)" )))); - if let Ok(choice) = result.await { - if choice.to_lowercase() == "y" { + if let Some(Ok(choice)) = result.recv().await { + if choice == "y" || choice == "Y" { emit!(Quit); } } @@ -179,9 +179,9 @@ impl Manager { pub fn create(&self) -> bool { let cwd = self.cwd().to_owned(); tokio::spawn(async move { - let result = emit!(Input(InputOpt::top("Create:"))); + let mut result = emit!(Input(InputOpt::top("Create:"))); - if let Ok(name) = result.await { + if let Some(Ok(name)) = result.recv().await { let path = cwd.join(&name); let hovered = path.components().take(cwd.components().count() + 1).collect::(); @@ -212,11 +212,11 @@ impl Manager { }; tokio::spawn(async move { - let result = emit!(Input( + let mut result = emit!(Input( InputOpt::hovered("Rename:").with_value(hovered.file_name().unwrap().to_string_lossy()) )); - if let Ok(new) = result.await { + if let Some(Ok(new)) = result.recv().await { let to = hovered.parent().unwrap().join(new); fs::rename(&hovered, to).await.ok(); } diff --git a/core/src/manager/tab.rs b/core/src/manager/tab.rs index facd9d65..cdf30220 100644 --- a/core/src/manager/tab.rs +++ b/core/src/manager/tab.rs @@ -1,6 +1,6 @@ use std::{borrow::Cow, collections::{BTreeMap, BTreeSet}, ffi::{OsStr, OsString}, mem, time::Duration}; -use anyhow::{Error, Result}; +use anyhow::{bail, Error, Result}; use config::open::Opener; use shared::{Defer, Url}; use tokio::{pin, task::JoinHandle}; @@ -123,10 +123,10 @@ impl Tab { pub fn cd_interactive(&mut self, target: Url) -> bool { tokio::spawn(async move { - let result = + let mut result = emit!(Input(InputOpt::top("Change directory:").with_value(target.to_string_lossy()))); - if let Ok(s) = result.await { + if let Some(Ok(s)) = result.recv().await { emit!(Cd(Url::from(s))); } }); @@ -241,7 +241,9 @@ impl Tab { let hidden = self.show_hidden; self.search = Some(tokio::spawn(async move { - let subject = emit!(Input(InputOpt::top("Search:"))).await?; + let Some(Ok(subject)) = emit!(Input(InputOpt::top("Search:"))).recv().await else { + bail!("canceled") + }; let rx = if grep { external::rg(external::RgOpt { cwd: cwd.clone(), hidden, subject }) @@ -309,10 +311,10 @@ impl Tab { let mut exec = exec.to_owned(); tokio::spawn(async move { if !confirm || exec.is_empty() { - let result = emit!(Input(InputOpt::top("Shell:").with_value(&exec).with_highlight())); - match result.await { - Ok(e) => exec = e, - Err(_) => return, + let mut result = emit!(Input(InputOpt::top("Shell:").with_value(&exec).with_highlight())); + match result.recv().await { + Some(Ok(e)) => exec = e, + _ => return, } } diff --git a/core/src/tasks/tasks.rs b/core/src/tasks/tasks.rs index bb5fe04c..c75ec048 100644 --- a/core/src/tasks/tasks.rs +++ b/core/src/tasks/tasks.rs @@ -183,14 +183,14 @@ impl Tasks { let scheduler = self.scheduler.clone(); tokio::spawn(async move { let s = if targets.len() > 1 { "s" } else { "" }; - let result = emit!(Input(InputOpt::hovered(if permanently { + let mut result = emit!(Input(InputOpt::hovered(if permanently { format!("Delete selected file{s} permanently? (y/N)") } else { format!("Move selected file{s} to trash? (y/N)") }))); - if let Ok(choice) = result.await { - if choice.to_lowercase() != "y" { + if let Some(Ok(choice)) = result.recv().await { + if choice != "y" && choice != "Y" { return; } for p in targets { diff --git a/shared/src/errors/input.rs b/shared/src/errors/input.rs new file mode 100644 index 00000000..792dc7ae --- /dev/null +++ b/shared/src/errors/input.rs @@ -0,0 +1,18 @@ +use std::{error::Error, fmt::{self, Display}}; + +#[derive(Debug)] +pub enum InputError { + Typed(String), + Canceled(String), +} + +impl Display for InputError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Typed(text) => write!(f, "Typed error: {text}"), + Self::Canceled(text) => write!(f, "Canceled error: {text}"), + } + } +} + +impl Error for InputError {} diff --git a/shared/src/errors/mod.rs b/shared/src/errors/mod.rs index b70c7117..9d67f513 100644 --- a/shared/src/errors/mod.rs +++ b/shared/src/errors/mod.rs @@ -1,3 +1,5 @@ +mod input; mod peek; +pub use input::*; pub use peek::*;