mirror of
https://github.com/sxyazi/yazi.git
synced 2024-12-18 22:31:35 +03:00
feat: make Input
streamable (#127)
This commit is contained in:
parent
ff42685ec9
commit
a90adf5bc5
@ -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)) => {
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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 {
|
||||
|
18
shared/src/errors/input.rs
Normal file
18
shared/src/errors/input.rs
Normal 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 {}
|
@ -1,3 +1,5 @@
|
||||
mod input;
|
||||
mod peek;
|
||||
|
||||
pub use input::*;
|
||||
pub use peek::*;
|
||||
|
Loading…
Reference in New Issue
Block a user