mirror of
https://github.com/sxyazi/yazi.git
synced 2024-12-24 09:12:43 +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 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)) => {
|
||||||
|
@ -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 {
|
||||||
|
@ -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
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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 {
|
||||||
|
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;
|
mod peek;
|
||||||
|
|
||||||
|
pub use input::*;
|
||||||
pub use peek::*;
|
pub use peek::*;
|
||||||
|
Loading…
Reference in New Issue
Block a user