feat: make Input streamable (#127)

This commit is contained in:
三咲雅 · Misaki Masa 2023-09-09 22:58:20 +08:00 committed by GitHub
parent ff42685ec9
commit a90adf5bc5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 88 additions and 33 deletions

View File

@ -3,8 +3,8 @@ use std::{collections::BTreeMap, ffi::OsString};
use anyhow::Result; use anyhow::Result;
use config::{keymap::{Control, KeymapLayer}, open::Opener}; use config::{keymap::{Control, KeymapLayer}, open::Opener};
use crossterm::event::KeyEvent; use crossterm::event::KeyEvent;
use shared::{RoCell, Url}; use shared::{InputError, RoCell, Url};
use tokio::sync::{mpsc::UnboundedSender, oneshot}; use tokio::sync::{mpsc::{self, UnboundedSender}, oneshot};
use super::{files::{File, FilesOp}, input::InputOpt, select::SelectOpt}; use super::{files::{File, FilesOp}, input::InputOpt, select::SelectOpt};
use crate::manager::PreviewLock; use crate::manager::PreviewLock;
@ -32,7 +32,7 @@ pub enum Event {
// Input // Input
Select(SelectOpt, oneshot::Sender<Result<usize>>), Select(SelectOpt, oneshot::Sender<Result<usize>>),
Input(InputOpt, oneshot::Sender<Result<String>>), Input(InputOpt, mpsc::UnboundedSender<Result<String, InputError>>),
// Tasks // Tasks
Open(Vec<(OsString, String)>, Option<Opener>), Open(Vec<(OsString, String)>, Option<Opener>),
@ -104,8 +104,9 @@ macro_rules! emit {
$crate::Event::Select($opt, tx).wait(rx) $crate::Event::Select($opt, tx).wait(rx)
}}; }};
(Input($opt:expr)) => {{ (Input($opt:expr)) => {{
let (tx, rx) = tokio::sync::oneshot::channel(); let (tx, rx) = tokio::sync::mpsc::unbounded_channel();
$crate::Event::Input($opt, tx).wait(rx) $crate::Event::Input($opt, tx).emit();
rx
}}; }};
(Open($targets:expr, $opener:expr)) => { (Open($targets:expr, $opener:expr)) => {

View File

@ -1,10 +1,9 @@
use std::ops::Range; use std::ops::Range;
use anyhow::{anyhow, Result};
use config::keymap::Key; use config::keymap::Key;
use crossterm::event::KeyCode; use crossterm::event::KeyCode;
use shared::CharKind; use shared::{CharKind, InputError};
use tokio::sync::oneshot::Sender; use tokio::sync::mpsc::UnboundedSender;
use unicode_width::UnicodeWidthStr; use unicode_width::UnicodeWidthStr;
use super::{mode::InputMode, op::InputOp, InputOpt, InputSnap, InputSnaps}; use super::{mode::InputMode, op::InputOp, InputOpt, InputSnap, InputSnaps};
@ -17,21 +16,27 @@ pub struct Input {
title: String, title: String,
pub position: Position, pub position: Position,
callback: Option<Sender<Result<String>>>,
// Typing
callback: Option<UnboundedSender<Result<String, InputError>>>,
realtime: bool,
// Shell // Shell
pub(super) highlight: bool, pub(super) highlight: bool,
} }
impl Input { impl Input {
pub fn show(&mut self, opt: InputOpt, tx: Sender<Result<String>>) { pub fn show(&mut self, opt: InputOpt, tx: UnboundedSender<Result<String, InputError>>) {
self.close(false); self.close(false);
self.snaps.reset(opt.value); self.snaps.reset(opt.value);
self.visible = true; self.visible = true;
self.title = opt.title; self.title = opt.title;
self.position = opt.position; self.position = opt.position;
// Typing
self.callback = Some(tx); self.callback = Some(tx);
self.realtime = opt.realtime;
// Shell // Shell
self.highlight = opt.highlight; self.highlight = opt.highlight;
@ -39,8 +44,8 @@ impl Input {
pub fn close(&mut self, submit: bool) -> bool { pub fn close(&mut self, submit: bool) -> bool {
if let Some(cb) = self.callback.take() { if let Some(cb) = self.callback.take() {
let _ = let value = self.snap_mut().value.clone();
cb.send(if submit { Ok(self.snap_mut().value.clone()) } else { Err(anyhow!("canceled")) }); let _ = cb.send(if submit { Ok(value) } else { Err(InputError::Canceled(value)) });
} }
self.visible = false; self.visible = false;
@ -201,22 +206,26 @@ impl Input {
} }
pub fn type_str(&mut self, s: &str) -> bool { pub fn type_str(&mut self, s: &str) -> bool {
let snap = self.snap_mut(); let snap = self.snaps.current_mut();
if snap.cursor < 1 { if snap.cursor < 1 {
snap.value.insert_str(0, s); snap.value.insert_str(0, s);
} else { } else {
snap.value.insert_str(snap.idx(snap.cursor).unwrap(), s); snap.value.insert_str(snap.idx(snap.cursor).unwrap(), s);
} }
self.flush_value();
self.move_(s.chars().count() as isize) self.move_(s.chars().count() as isize)
} }
pub fn backspace(&mut self) -> bool { pub fn backspace(&mut self) -> bool {
let snap = self.snap_mut(); let snap = self.snaps.current_mut();
if snap.cursor < 1 { if snap.cursor < 1 {
return false; return false;
} else { } else {
snap.value.remove(snap.idx(snap.cursor - 1).unwrap()); snap.value.remove(snap.idx(snap.cursor - 1).unwrap());
} }
self.flush_value();
self.move_(-1) self.move_(-1)
} }
@ -278,7 +287,7 @@ impl Input {
fn handle_op(&mut self, cursor: usize, include: bool) -> bool { fn handle_op(&mut self, cursor: usize, include: bool) -> bool {
let old = self.snap().clone(); let old = self.snap().clone();
let snap = self.snap_mut(); let snap = self.snaps.current_mut();
match snap.op { match snap.op {
InputOp::None | InputOp::Select(_) => { InputOp::None | InputOp::Select(_) => {
@ -296,6 +305,10 @@ impl Input {
snap.op = InputOp::None; snap.op = InputOp::None;
snap.mode = if insert { InputMode::Insert } else { InputMode::Normal }; snap.mode = if insert { InputMode::Insert } else { InputMode::Normal };
snap.cursor = range.start; snap.cursor = range.start;
if self.realtime {
self.callback.as_ref().unwrap().send(Err(InputError::Typed(snap.value.clone()))).ok();
}
} }
InputOp::Yank(_) => { InputOp::Yank(_) => {
let range = snap.op.range(cursor, include).unwrap(); let range = snap.op.range(cursor, include).unwrap();
@ -316,6 +329,14 @@ impl Input {
} }
true 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 { impl Input {

View File

@ -6,6 +6,7 @@ pub struct InputOpt {
pub title: String, pub title: String,
pub value: String, pub value: String,
pub position: Position, pub position: Position,
pub realtime: bool,
pub highlight: bool, pub highlight: bool,
} }
@ -15,6 +16,7 @@ impl InputOpt {
title: title.as_ref().to_owned(), title: title.as_ref().to_owned(),
value: String::new(), value: String::new(),
position: Position::Top(/* TODO: hardcode */ Rect { x: 0, y: 2, width: 50, height: 3 }), position: Position::Top(/* TODO: hardcode */ Rect { x: 0, y: 2, width: 50, height: 3 }),
realtime: false,
highlight: false, highlight: false,
} }
} }
@ -27,15 +29,24 @@ impl InputOpt {
// TODO: hardcode // TODO: hardcode
Rect { x: 0, y: 1, width: 50, height: 3 }, Rect { x: 0, y: 1, width: 50, height: 3 },
), ),
realtime: false,
highlight: false, highlight: false,
} }
} }
#[inline]
pub fn with_value(mut self, value: impl AsRef<str>) -> Self { pub fn with_value(mut self, value: impl AsRef<str>) -> Self {
self.value = value.as_ref().to_owned(); self.value = value.as_ref().to_owned();
self self
} }
#[inline]
pub fn with_realtime(mut self) -> Self {
self.realtime = true;
self
}
#[inline]
pub fn with_highlight(mut self) -> Self { pub fn with_highlight(mut self) -> Self {
self.highlight = true; self.highlight = true;
self self

View File

@ -96,12 +96,12 @@ impl Manager {
} }
tokio::spawn(async move { 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)" "There are {tasks} tasks running, sure to quit? (y/N)"
)))); ))));
if let Ok(choice) = result.await { if let Some(Ok(choice)) = result.recv().await {
if choice.to_lowercase() == "y" { if choice == "y" || choice == "Y" {
emit!(Quit); emit!(Quit);
} }
} }
@ -179,9 +179,9 @@ impl Manager {
pub fn create(&self) -> bool { pub fn create(&self) -> bool {
let cwd = self.cwd().to_owned(); let cwd = self.cwd().to_owned();
tokio::spawn(async move { 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 path = cwd.join(&name);
let hovered = path.components().take(cwd.components().count() + 1).collect::<PathBuf>(); let hovered = path.components().take(cwd.components().count() + 1).collect::<PathBuf>();
@ -212,11 +212,11 @@ impl Manager {
}; };
tokio::spawn(async move { 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()) 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); let to = hovered.parent().unwrap().join(new);
fs::rename(&hovered, to).await.ok(); fs::rename(&hovered, to).await.ok();
} }

View File

@ -1,6 +1,6 @@
use std::{borrow::Cow, collections::{BTreeMap, BTreeSet}, ffi::{OsStr, OsString}, mem, time::Duration}; 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 config::open::Opener;
use shared::{Defer, Url}; use shared::{Defer, Url};
use tokio::{pin, task::JoinHandle}; use tokio::{pin, task::JoinHandle};
@ -123,10 +123,10 @@ impl Tab {
pub fn cd_interactive(&mut self, target: Url) -> bool { pub fn cd_interactive(&mut self, target: Url) -> bool {
tokio::spawn(async move { tokio::spawn(async move {
let result = let mut result =
emit!(Input(InputOpt::top("Change directory:").with_value(target.to_string_lossy()))); 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))); emit!(Cd(Url::from(s)));
} }
}); });
@ -241,7 +241,9 @@ impl Tab {
let hidden = self.show_hidden; let hidden = self.show_hidden;
self.search = Some(tokio::spawn(async move { 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 { let rx = if grep {
external::rg(external::RgOpt { cwd: cwd.clone(), hidden, subject }) external::rg(external::RgOpt { cwd: cwd.clone(), hidden, subject })
@ -309,10 +311,10 @@ impl Tab {
let mut exec = exec.to_owned(); let mut exec = exec.to_owned();
tokio::spawn(async move { tokio::spawn(async move {
if !confirm || exec.is_empty() { if !confirm || exec.is_empty() {
let result = emit!(Input(InputOpt::top("Shell:").with_value(&exec).with_highlight())); let mut result = emit!(Input(InputOpt::top("Shell:").with_value(&exec).with_highlight()));
match result.await { match result.recv().await {
Ok(e) => exec = e, Some(Ok(e)) => exec = e,
Err(_) => return, _ => return,
} }
} }

View File

@ -183,14 +183,14 @@ impl Tasks {
let scheduler = self.scheduler.clone(); let scheduler = self.scheduler.clone();
tokio::spawn(async move { tokio::spawn(async move {
let s = if targets.len() > 1 { "s" } else { "" }; 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)") format!("Delete selected file{s} permanently? (y/N)")
} else { } else {
format!("Move selected file{s} to trash? (y/N)") format!("Move selected file{s} to trash? (y/N)")
}))); })));
if let Ok(choice) = result.await { if let Some(Ok(choice)) = result.recv().await {
if choice.to_lowercase() != "y" { if choice != "y" && choice != "Y" {
return; return;
} }
for p in targets { for p in targets {

View File

@ -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 {}

View File

@ -1,3 +1,5 @@
mod input;
mod peek; mod peek;
pub use input::*;
pub use peek::*; pub use peek::*;