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 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<Result<usize>>),
Input(InputOpt, oneshot::Sender<Result<String>>),
Input(InputOpt, mpsc::UnboundedSender<Result<String, InputError>>),
// Tasks
Open(Vec<(OsString, String)>, Option<Opener>),
@ -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)) => {

View File

@ -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<Sender<Result<String>>>,
// Typing
callback: Option<UnboundedSender<Result<String, InputError>>>,
realtime: bool,
// Shell
pub(super) highlight: bool,
}
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.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 {

View File

@ -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<str>) -> 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

View File

@ -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::<PathBuf>();
@ -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();
}

View File

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

View File

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

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;
pub use input::*;
pub use peek::*;