mirror of
https://github.com/sxyazi/yazi.git
synced 2026-07-21 23:01:05 +00:00
feat: make cmp component follow input cursor (#3784)
This commit is contained in:
parent
cc2414728c
commit
a246e9c7c0
37 changed files with 174 additions and 170 deletions
|
|
@ -23,6 +23,7 @@ impl Actor for Close {
|
|||
|
||||
cmp.caches.clear();
|
||||
cmp.ticket = Default::default();
|
||||
cmp.handle.take().map(|h| h.abort());
|
||||
succ!(render!(mem::replace(&mut cmp.visible, false)));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,8 +29,8 @@ impl Actor for Show {
|
|||
succ!();
|
||||
};
|
||||
|
||||
cmp.cands = Self::match_candidates(opt.word.as_path(), cache);
|
||||
if cmp.cands.is_empty() {
|
||||
cmp.matches = Self::match_candidates(opt.word.as_path(), cache);
|
||||
if cmp.matches.is_empty() {
|
||||
succ!(render!(mem::replace(&mut cmp.visible, false)));
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
use std::mem;
|
||||
use std::{io, mem};
|
||||
|
||||
use anyhow::Result;
|
||||
use yazi_fs::{path::clean_url, provider::{DirReader, FileHolder}};
|
||||
|
|
@ -18,25 +18,23 @@ impl Actor for Trigger {
|
|||
const NAME: &str = "trigger";
|
||||
|
||||
fn act(cx: &mut Ctx, opt: Self::Options) -> Result<Data> {
|
||||
let cmp = &mut cx.cmp;
|
||||
if let Some(t) = opt.ticket {
|
||||
if t < cmp.ticket {
|
||||
succ!();
|
||||
}
|
||||
cmp.ticket = t;
|
||||
if opt.ticket.is_some_and(|t| t != cx.cmp.ticket) {
|
||||
succ!();
|
||||
} else if opt.ticket.is_none() {
|
||||
cx.cmp.ticket = cx.input.ticket.current();
|
||||
}
|
||||
|
||||
cx.cmp.handle.take().map(|h| h.abort());
|
||||
let Some((parent, word)) = Self::split_url(&opt.word) else {
|
||||
return act!(cmp:close, cx, false);
|
||||
};
|
||||
|
||||
if cmp.caches.contains_key(&parent) {
|
||||
let ticket = cmp.ticket;
|
||||
let ticket = cx.cmp.ticket;
|
||||
if cx.cmp.caches.contains_key(&parent) {
|
||||
return act!(cmp:show, cx, ShowOpt { cache: vec![], cache_name: parent, word, ticket });
|
||||
}
|
||||
|
||||
let ticket = cmp.ticket;
|
||||
tokio::spawn(async move {
|
||||
cx.cmp.handle = Some(tokio::spawn(async move {
|
||||
let mut dir = provider::read_dir(&parent).await?;
|
||||
let mut cache = vec![];
|
||||
|
||||
|
|
@ -58,10 +56,10 @@ impl Actor for Trigger {
|
|||
CmpProxy::show(ShowOpt { cache, cache_name: parent, word, ticket });
|
||||
}
|
||||
|
||||
Ok::<_, anyhow::Error>(())
|
||||
});
|
||||
Ok::<_, io::Error>(())
|
||||
}));
|
||||
|
||||
succ!(render!(mem::replace(&mut cmp.visible, false)));
|
||||
succ!(render!(mem::replace(&mut cx.cmp.visible, false)));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ use anyhow::Result;
|
|||
use yazi_macro::{act, render, succ};
|
||||
use yazi_parser::input::CloseOpt;
|
||||
use yazi_shared::data::Data;
|
||||
use yazi_widgets::input::InputError;
|
||||
use yazi_widgets::input::InputEvent;
|
||||
|
||||
use crate::{Actor, Ctx};
|
||||
|
||||
|
|
@ -20,7 +20,7 @@ impl Actor for Close {
|
|||
|
||||
if let Some(tx) = input.tx.take() {
|
||||
let value = input.snap().value.clone();
|
||||
_ = tx.send(if opt.submit { Ok(value) } else { Err(InputError::Canceled(value)) });
|
||||
_ = tx.send(if opt.submit { InputEvent::Submit(value) } else { InputEvent::Cancel(value) });
|
||||
}
|
||||
|
||||
act!(cmp:close, cx)?;
|
||||
|
|
|
|||
|
|
@ -1,18 +1,16 @@
|
|||
use std::ops::DerefMut;
|
||||
|
||||
use anyhow::Result;
|
||||
use yazi_config::YAZI;
|
||||
use yazi_macro::{act, render, succ};
|
||||
use yazi_parser::input::ShowOpt;
|
||||
use yazi_shared::data::Data;
|
||||
use yazi_widgets::input::{InputCallback, InputError};
|
||||
use yazi_widgets::input::InputOpt;
|
||||
|
||||
use crate::{Actor, Ctx};
|
||||
|
||||
pub struct Show;
|
||||
|
||||
impl Actor for Show {
|
||||
type Options = ShowOpt;
|
||||
type Options = InputOpt;
|
||||
|
||||
const NAME: &str = "show";
|
||||
|
||||
|
|
@ -21,34 +19,9 @@ impl Actor for Show {
|
|||
|
||||
let input = &mut cx.input;
|
||||
input.visible = true;
|
||||
input.title = opt.cfg.title;
|
||||
input.title = opt.cfg.title.clone();
|
||||
input.position = opt.cfg.position;
|
||||
|
||||
// Typing
|
||||
input.tx = Some(opt.tx.clone());
|
||||
let ticket = input.ticket.clone();
|
||||
|
||||
// Reset input
|
||||
let cb: InputCallback = Box::new(move |before, after| {
|
||||
if opt.cfg.realtime {
|
||||
opt.tx.send(Err(InputError::Typed(format!("{before}{after}")))).ok();
|
||||
} else if opt.cfg.completion {
|
||||
opt.tx.send(Err(InputError::Completed(before.to_owned(), ticket.current()))).ok();
|
||||
}
|
||||
});
|
||||
*input.deref_mut() = yazi_widgets::input::Input::new(
|
||||
opt.cfg.value,
|
||||
opt.cfg.position.offset.width.saturating_sub(YAZI.input.border()) as usize,
|
||||
opt.cfg.obscure,
|
||||
cb,
|
||||
);
|
||||
|
||||
// Set cursor after reset
|
||||
// TODO: remove this
|
||||
if let Some(cursor) = opt.cfg.cursor {
|
||||
input.snap_mut().cursor = cursor;
|
||||
act!(r#move, input)?;
|
||||
}
|
||||
*input.deref_mut() = yazi_widgets::input::Input::new(opt)?;
|
||||
|
||||
succ!(render!());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ use yazi_parser::mgr::CdOpt;
|
|||
use yazi_proxy::{CmpProxy, InputProxy, MgrProxy};
|
||||
use yazi_shared::{Debounce, data::Data, url::{AsUrl, UrlBuf, UrlLike}};
|
||||
use yazi_vfs::{VfsFile, provider};
|
||||
use yazi_widgets::input::InputError;
|
||||
use yazi_widgets::input::InputEvent;
|
||||
|
||||
use crate::{Actor, Ctx};
|
||||
|
||||
|
|
@ -70,7 +70,7 @@ impl Cd {
|
|||
|
||||
while let Some(result) = rx.next().await {
|
||||
match result {
|
||||
Ok(s) => {
|
||||
InputEvent::Submit(s) => {
|
||||
let Ok(url) = UrlBuf::try_from(s).map(expand_url) else { return };
|
||||
let Ok(url) = provider::absolute(&url).await else { return };
|
||||
let url = clean_url(url);
|
||||
|
|
@ -85,7 +85,7 @@ impl Cd {
|
|||
}
|
||||
MgrProxy::reveal(url);
|
||||
}
|
||||
Err(InputError::Completed(before, ticket)) => {
|
||||
InputEvent::Trigger(before, ticket) => {
|
||||
CmpProxy::trigger(before, ticket);
|
||||
}
|
||||
_ => break,
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ use yazi_proxy::{ConfirmProxy, InputProxy, MgrProxy};
|
|||
use yazi_shared::{data::Data, url::{UrlBuf, UrlLike}};
|
||||
use yazi_vfs::{VfsFile, maybe_exists, provider};
|
||||
use yazi_watcher::WATCHER;
|
||||
use yazi_widgets::input::InputEvent;
|
||||
|
||||
use crate::{Actor, Ctx};
|
||||
|
||||
|
|
@ -22,7 +23,7 @@ impl Actor for Create {
|
|||
let mut input = InputProxy::show(InputCfg::create(opt.dir));
|
||||
|
||||
tokio::spawn(async move {
|
||||
let Some(Ok(name)) = input.recv().await else { return };
|
||||
let Some(InputEvent::Submit(name)) = input.recv().await else { return };
|
||||
if name.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ use yazi_macro::succ;
|
|||
use yazi_parser::mgr::FilterOpt;
|
||||
use yazi_proxy::{InputProxy, MgrProxy};
|
||||
use yazi_shared::{Debounce, data::Data};
|
||||
use yazi_widgets::input::InputError;
|
||||
use yazi_widgets::input::InputEvent;
|
||||
|
||||
use crate::{Actor, Ctx};
|
||||
|
||||
|
|
@ -26,9 +26,9 @@ impl Actor for Filter {
|
|||
let rx = Debounce::new(UnboundedReceiverStream::new(input), Duration::from_millis(50));
|
||||
pin!(rx);
|
||||
|
||||
while let Some(result) = rx.next().await {
|
||||
let done = result.is_ok();
|
||||
let (Ok(s) | Err(InputError::Typed(s))) = result else { continue };
|
||||
while let Some(event) = rx.next().await {
|
||||
let done = event.is_submit();
|
||||
let (InputEvent::Submit(s) | InputEvent::Type(s)) = event else { continue };
|
||||
|
||||
MgrProxy::filter_do(FilterOpt { query: s.into(), case: opt.case, done });
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ use yazi_macro::succ;
|
|||
use yazi_parser::mgr::{FindDoOpt, FindOpt};
|
||||
use yazi_proxy::{InputProxy, MgrProxy};
|
||||
use yazi_shared::{Debounce, data::Data};
|
||||
use yazi_widgets::input::InputError;
|
||||
use yazi_widgets::input::InputEvent;
|
||||
|
||||
use crate::{Actor, Ctx};
|
||||
|
||||
|
|
@ -26,7 +26,7 @@ impl Actor for Find {
|
|||
let rx = Debounce::new(UnboundedReceiverStream::new(input), Duration::from_millis(50));
|
||||
pin!(rx);
|
||||
|
||||
while let Some(Ok(s)) | Some(Err(InputError::Typed(s))) = rx.next().await {
|
||||
while let Some(InputEvent::Submit(s) | InputEvent::Type(s)) = rx.next().await {
|
||||
MgrProxy::find_do(FindDoOpt { query: s.into(), prev: opt.prev, case: opt.case });
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ use yazi_proxy::{ConfirmProxy, InputProxy, MgrProxy};
|
|||
use yazi_shared::{Id, data::Data, url::{UrlBuf, UrlLike}};
|
||||
use yazi_vfs::{VfsFile, maybe_exists, provider};
|
||||
use yazi_watcher::WATCHER;
|
||||
use yazi_widgets::input::InputEvent;
|
||||
|
||||
use crate::{Actor, Ctx};
|
||||
|
||||
|
|
@ -44,7 +45,7 @@ impl Actor for Rename {
|
|||
let mut input = InputProxy::show(InputCfg::rename().with_value(name).with_cursor(cursor));
|
||||
|
||||
tokio::spawn(async move {
|
||||
let Some(Ok(name)) = input.recv().await else { return };
|
||||
let Some(InputEvent::Submit(name)) = input.recv().await else { return };
|
||||
if name.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ use yazi_parser::{VoidOpt, mgr::{CdSource, SearchOpt, SearchOptVia}};
|
|||
use yazi_plugin::external;
|
||||
use yazi_proxy::{InputProxy, MgrProxy, NotifyProxy};
|
||||
use yazi_shared::{data::Data, url::{AsUrl, UrlLike}};
|
||||
use yazi_widgets::input::InputEvent;
|
||||
|
||||
use crate::{Actor, Ctx};
|
||||
|
||||
|
|
@ -29,7 +30,7 @@ impl Actor for Search {
|
|||
InputProxy::show(InputCfg::search(opt.via.into_str()).with_value(&*opt.subject));
|
||||
|
||||
tokio::spawn(async move {
|
||||
if let Some(Ok(subject)) = input.recv().await {
|
||||
if let Some(InputEvent::Submit(subject)) = input.recv().await {
|
||||
opt.subject = Cow::Owned(subject);
|
||||
MgrProxy::search_do(opt);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ use yazi_macro::{act, succ};
|
|||
use yazi_parser::{mgr::ShellOpt, tasks::ProcessOpenOpt};
|
||||
use yazi_proxy::{InputProxy, TasksProxy};
|
||||
use yazi_shared::data::Data;
|
||||
use yazi_widgets::input::InputEvent;
|
||||
|
||||
use crate::{Actor, Ctx};
|
||||
|
||||
|
|
@ -29,7 +30,7 @@ impl Actor for Shell {
|
|||
tokio::spawn(async move {
|
||||
if let Some(mut rx) = input {
|
||||
match rx.recv().await {
|
||||
Some(Ok(e)) => opt.run = Cow::Owned(e),
|
||||
Some(InputEvent::Submit(e)) => opt.run = Cow::Owned(e),
|
||||
_ => return,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ use yazi_macro::{act, render, succ};
|
|||
use yazi_parser::mgr::TabRenameOpt;
|
||||
use yazi_proxy::{InputProxy, MgrProxy};
|
||||
use yazi_shared::data::Data;
|
||||
use yazi_widgets::input::InputEvent;
|
||||
|
||||
use crate::{Actor, Ctx};
|
||||
|
||||
|
|
@ -30,7 +31,7 @@ impl Actor for TabRename {
|
|||
InputCfg::tab_rename().with_value(opt.name.unwrap_or(Cow::Borrowed(&pref.name))),
|
||||
);
|
||||
tokio::spawn(async move {
|
||||
if let Some(Ok(name)) = input.recv().await {
|
||||
if let Some(InputEvent::Submit(name)) = input.recv().await {
|
||||
MgrProxy::tab_rename(tab, name);
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -3,13 +3,13 @@ use std::pin::Pin;
|
|||
use mlua::{UserData, UserDataMethods};
|
||||
use tokio::pin;
|
||||
use tokio_stream::StreamExt;
|
||||
use yazi_widgets::input::InputError;
|
||||
use yazi_widgets::input::InputEvent;
|
||||
|
||||
pub struct InputRx<T: StreamExt<Item = Result<String, InputError>>> {
|
||||
pub struct InputRx<T: StreamExt<Item = InputEvent>> {
|
||||
inner: T,
|
||||
}
|
||||
|
||||
impl<T: StreamExt<Item = Result<String, InputError>>> InputRx<T> {
|
||||
impl<T: StreamExt<Item = InputEvent>> InputRx<T> {
|
||||
pub fn new(inner: T) -> Self { Self { inner } }
|
||||
|
||||
pub async fn consume(inner: T) -> (Option<String>, u8) {
|
||||
|
|
@ -17,17 +17,17 @@ impl<T: StreamExt<Item = Result<String, InputError>>> InputRx<T> {
|
|||
inner.next().await.map(Self::parse).unwrap_or((None, 0))
|
||||
}
|
||||
|
||||
fn parse(res: Result<String, InputError>) -> (Option<String>, u8) {
|
||||
fn parse(res: InputEvent) -> (Option<String>, u8) {
|
||||
match res {
|
||||
Ok(s) => (Some(s), 1),
|
||||
Err(InputError::Canceled(s)) => (Some(s), 2),
|
||||
Err(InputError::Typed(s)) => (Some(s), 3),
|
||||
InputEvent::Submit(s) => (Some(s), 1),
|
||||
InputEvent::Cancel(s) => (Some(s), 2),
|
||||
InputEvent::Type(s) => (Some(s), 3),
|
||||
_ => (None, 0),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: StreamExt<Item = Result<String, InputError>> + 'static> UserData for InputRx<T> {
|
||||
impl<T: StreamExt<Item = InputEvent> + 'static> UserData for InputRx<T> {
|
||||
fn add_methods<M: UserDataMethods<Self>>(methods: &mut M) {
|
||||
methods.add_async_method_mut("recv", |_, mut me, ()| async move {
|
||||
let mut inner = unsafe { Pin::new_unchecked(&mut me.inner) };
|
||||
|
|
|
|||
|
|
@ -207,7 +207,7 @@ inactive = {}
|
|||
|
||||
# Icons
|
||||
icon_file = ""
|
||||
icon_folder = ""
|
||||
icon_folder = ""
|
||||
icon_command = ""
|
||||
|
||||
# : }}}
|
||||
|
|
|
|||
|
|
@ -207,7 +207,7 @@ inactive = {}
|
|||
|
||||
# Icons
|
||||
icon_file = ""
|
||||
icon_folder = ""
|
||||
icon_folder = ""
|
||||
icon_command = ""
|
||||
|
||||
# : }}}
|
||||
|
|
|
|||
|
|
@ -1,36 +1,40 @@
|
|||
use std::io;
|
||||
|
||||
use hashbrown::HashMap;
|
||||
use tokio::task::JoinHandle;
|
||||
use yazi_parser::cmp::CmpItem;
|
||||
use yazi_shared::{Id, url::UrlBuf};
|
||||
use yazi_widgets::Scrollable;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Cmp {
|
||||
pub caches: HashMap<UrlBuf, Vec<CmpItem>>,
|
||||
pub cands: Vec<CmpItem>,
|
||||
pub offset: usize,
|
||||
pub cursor: usize,
|
||||
pub caches: HashMap<UrlBuf, Vec<CmpItem>>,
|
||||
pub matches: Vec<CmpItem>,
|
||||
pub offset: usize,
|
||||
pub cursor: usize,
|
||||
|
||||
pub ticket: Id,
|
||||
pub handle: Option<JoinHandle<io::Result<()>>>,
|
||||
pub visible: bool,
|
||||
}
|
||||
|
||||
impl Cmp {
|
||||
// --- Cands
|
||||
// --- Matches
|
||||
pub fn window(&self) -> &[CmpItem] {
|
||||
let end = (self.offset + self.limit()).min(self.cands.len());
|
||||
&self.cands[self.offset..end]
|
||||
let end = (self.offset + self.limit()).min(self.matches.len());
|
||||
&self.matches[self.offset..end]
|
||||
}
|
||||
|
||||
pub fn selected(&self) -> Option<&CmpItem> { self.cands.get(self.cursor) }
|
||||
pub fn selected(&self) -> Option<&CmpItem> { self.matches.get(self.cursor) }
|
||||
|
||||
// --- Cursor
|
||||
pub fn rel_cursor(&self) -> usize { self.cursor - self.offset }
|
||||
}
|
||||
|
||||
impl Scrollable for Cmp {
|
||||
fn total(&self) -> usize { self.cands.len() }
|
||||
fn total(&self) -> usize { self.matches.len() }
|
||||
|
||||
fn limit(&self) -> usize { self.cands.len().min(10) }
|
||||
fn limit(&self) -> usize { self.matches.len().min(10) }
|
||||
|
||||
fn cursor_mut(&mut self) -> &mut usize { &mut self.cursor }
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,6 @@
|
|||
use std::{ops::{Deref, DerefMut}, rc::Rc};
|
||||
use std::ops::{Deref, DerefMut};
|
||||
|
||||
use tokio::sync::mpsc::UnboundedSender;
|
||||
use yazi_config::popup::Position;
|
||||
use yazi_shared::Ids;
|
||||
use yazi_widgets::input::InputError;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Input {
|
||||
|
|
@ -12,10 +9,6 @@ pub struct Input {
|
|||
pub visible: bool,
|
||||
pub title: String,
|
||||
pub position: Position,
|
||||
|
||||
// Typing
|
||||
pub tx: Option<UnboundedSender<Result<String, InputError>>>,
|
||||
pub ticket: Rc<Ids>,
|
||||
}
|
||||
|
||||
impl Deref for Input {
|
||||
|
|
|
|||
|
|
@ -121,7 +121,7 @@ pub enum Spark<'a> {
|
|||
InputKill(yazi_widgets::input::parser::KillOpt),
|
||||
InputMove(yazi_widgets::input::parser::MoveOpt),
|
||||
InputPaste(yazi_widgets::input::parser::PasteOpt),
|
||||
InputShow(yazi_parser::input::ShowOpt),
|
||||
InputShow(yazi_widgets::input::InputOpt),
|
||||
|
||||
// Notify
|
||||
NotifyPush(yazi_parser::notify::PushOpt),
|
||||
|
|
@ -377,7 +377,7 @@ try_from_spark!(yazi_parser::confirm::CloseOpt, confirm:close);
|
|||
try_from_spark!(yazi_parser::confirm::ShowOpt, confirm:show);
|
||||
try_from_spark!(yazi_parser::help::ToggleOpt, help:toggle);
|
||||
try_from_spark!(yazi_parser::input::CloseOpt, input:close);
|
||||
try_from_spark!(yazi_parser::input::ShowOpt, input:show);
|
||||
try_from_spark!(yazi_widgets::input::InputOpt, input:show);
|
||||
try_from_spark!(yazi_parser::mgr::CdOpt, mgr:cd);
|
||||
try_from_spark!(yazi_parser::mgr::CloseOpt, mgr:close);
|
||||
try_from_spark!(yazi_parser::mgr::CopyOpt, mgr:copy);
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
yazi_macro::mod_flat!(close show);
|
||||
yazi_macro::mod_flat!(close);
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ impl CmpProxy {
|
|||
emit!(Call(relay!(cmp:show).with_any("opt", opt)));
|
||||
}
|
||||
|
||||
pub fn trigger(word: impl Into<String>, ticket: Id) {
|
||||
emit!(Call(relay!(cmp:trigger, [word.into()]).with("ticket", ticket)));
|
||||
pub fn trigger(word: impl Into<String>, ticket: Option<Id>) {
|
||||
emit!(Call(relay!(cmp:trigger, [word.into()]).with_opt("ticket", ticket)));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
use tokio::sync::mpsc;
|
||||
use yazi_config::popup::InputCfg;
|
||||
use yazi_macro::{emit, relay};
|
||||
use yazi_widgets::input::InputError;
|
||||
use yazi_widgets::input::InputEvent;
|
||||
|
||||
pub struct InputProxy;
|
||||
|
||||
impl InputProxy {
|
||||
pub fn show(cfg: InputCfg) -> mpsc::UnboundedReceiver<Result<String, InputError>> {
|
||||
pub fn show(cfg: InputCfg) -> mpsc::UnboundedReceiver<InputEvent> {
|
||||
let (tx, rx) = mpsc::unbounded_channel();
|
||||
emit!(Call(relay!(input:show).with_any("tx", tx).with_any("cfg", cfg)));
|
||||
rx
|
||||
|
|
|
|||
|
|
@ -65,6 +65,13 @@ impl Action {
|
|||
self
|
||||
}
|
||||
|
||||
pub fn with_opt(mut self, name: impl Into<DataKey>, value: Option<impl Into<Data>>) -> Self {
|
||||
if let Some(value) = value {
|
||||
self.args.insert(name.into(), value.into());
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_seq<I>(mut self, values: I) -> Self
|
||||
where
|
||||
I: IntoIterator,
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ impl Input {
|
|||
act!(r#move, self, -1)?;
|
||||
}
|
||||
|
||||
self.flush_value();
|
||||
self.flush_type();
|
||||
succ!(render!());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ impl Input {
|
|||
snap.value.replace_range(start..end, &casefied);
|
||||
snap.op = InputOp::None;
|
||||
snap.cursor = range.start;
|
||||
self.snaps.tag(self.limit).then(|| self.flush_value());
|
||||
self.snaps.tag(self.limit).then(|| self.flush_type());
|
||||
|
||||
act!(r#move, self)?;
|
||||
succ!(render!());
|
||||
|
|
|
|||
|
|
@ -4,13 +4,7 @@ use anyhow::Result;
|
|||
use yazi_macro::{act, render, succ};
|
||||
use yazi_shared::data::Data;
|
||||
|
||||
use crate::input::{Input, parser::CompleteOpt};
|
||||
|
||||
#[cfg(windows)]
|
||||
const SEPARATOR: [char; 2] = ['/', '\\'];
|
||||
|
||||
#[cfg(not(windows))]
|
||||
const SEPARATOR: char = std::path::MAIN_SEPARATOR;
|
||||
use crate::input::{Input, SEPARATOR, parser::CompleteOpt};
|
||||
|
||||
impl Input {
|
||||
pub fn complete(&mut self, opt: CompleteOpt) -> Result<Data> {
|
||||
|
|
@ -30,7 +24,7 @@ impl Input {
|
|||
snap.value = new;
|
||||
|
||||
act!(r#move, self, delta)?;
|
||||
self.flush_value();
|
||||
self.flush_type();
|
||||
succ!(render!());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ impl Input {
|
|||
}
|
||||
|
||||
act!(r#move, self)?;
|
||||
self.flush_value();
|
||||
self.flush_type();
|
||||
succ!(render!());
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -41,6 +41,10 @@ impl Input {
|
|||
n_cur + pad - max
|
||||
}
|
||||
};
|
||||
|
||||
if n_cur != o_cur {
|
||||
self.flush_trigger(false);
|
||||
}
|
||||
succ!();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ impl Input {
|
|||
(Some(_), Some((len, _))) => snap.value.replace_range(start..start + len, &s),
|
||||
}
|
||||
|
||||
self.snaps.tag(self.limit).then(|| self.flush_value());
|
||||
self.snaps.tag(self.limit).then(|| self.flush_type());
|
||||
succ!(render!());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ impl Input {
|
|||
}
|
||||
|
||||
act!(r#move, self, s.chars().count() as isize)?;
|
||||
self.flush_value();
|
||||
self.flush_type();
|
||||
succ!(render!());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,22 +0,0 @@
|
|||
use std::{error::Error, fmt::{self, Display}};
|
||||
|
||||
use yazi_shared::Id;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum InputError {
|
||||
Typed(String),
|
||||
Completed(String, Id),
|
||||
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::Completed(text, _) => write!(f, "Completed error: {text}"),
|
||||
Self::Canceled(text) => write!(f, "Canceled error: {text}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Error for InputError {}
|
||||
14
yazi-widgets/src/input/event.rs
Normal file
14
yazi-widgets/src/input/event.rs
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
use yazi_shared::Id;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum InputEvent {
|
||||
Submit(String),
|
||||
Cancel(String),
|
||||
|
||||
Type(String),
|
||||
Trigger(String, Option<Id>),
|
||||
}
|
||||
|
||||
impl InputEvent {
|
||||
pub fn is_submit(&self) -> bool { matches!(self, Self::Submit(_)) }
|
||||
}
|
||||
|
|
@ -1,24 +1,47 @@
|
|||
use std::{borrow::Cow, ops::Range};
|
||||
|
||||
use anyhow::Result;
|
||||
use crossterm::cursor::SetCursorStyle;
|
||||
use tokio::sync::mpsc;
|
||||
use yazi_config::YAZI;
|
||||
use yazi_macro::act;
|
||||
use yazi_shared::Ids;
|
||||
|
||||
use super::{InputSnap, InputSnaps, mode::InputMode, op::InputOp};
|
||||
use crate::CLIPBOARD;
|
||||
|
||||
pub type InputCallback = Box<dyn Fn(&str, &str)>;
|
||||
use crate::{CLIPBOARD, input::{InputEvent, InputOpt, SEPARATOR}};
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Input {
|
||||
pub snaps: InputSnaps,
|
||||
pub limit: usize,
|
||||
pub obscure: bool,
|
||||
pub callback: Option<InputCallback>,
|
||||
pub snaps: InputSnaps,
|
||||
pub limit: usize,
|
||||
pub obscure: bool,
|
||||
pub realtime: bool,
|
||||
pub completion: bool,
|
||||
|
||||
pub tx: Option<mpsc::UnboundedSender<InputEvent>>,
|
||||
pub ticket: Ids,
|
||||
}
|
||||
|
||||
impl Input {
|
||||
pub fn new(value: String, limit: usize, obscure: bool, callback: InputCallback) -> Self {
|
||||
Self { snaps: InputSnaps::new(value, obscure, limit), limit, obscure, callback: Some(callback) }
|
||||
pub fn new(opt: InputOpt) -> Result<Self> {
|
||||
let limit = opt.cfg.position.offset.width.saturating_sub(YAZI.input.border()) as usize;
|
||||
let mut input = Self {
|
||||
snaps: InputSnaps::new(opt.cfg.value, opt.cfg.obscure, limit),
|
||||
limit,
|
||||
obscure: opt.cfg.obscure,
|
||||
realtime: opt.cfg.realtime,
|
||||
completion: opt.cfg.completion,
|
||||
|
||||
tx: Some(opt.tx),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
if let Some(cursor) = opt.cfg.cursor {
|
||||
input.snap_mut().cursor = cursor;
|
||||
act!(r#move, input)?;
|
||||
}
|
||||
|
||||
Ok(input)
|
||||
}
|
||||
|
||||
pub(super) fn handle_op(&mut self, cursor: usize, include: bool) -> bool {
|
||||
|
|
@ -57,24 +80,34 @@ impl Input {
|
|||
return false;
|
||||
}
|
||||
if !matches!(old.op, InputOp::None | InputOp::Select(_)) {
|
||||
self.snaps.tag(self.limit).then(|| self.flush_value());
|
||||
self.snaps.tag(self.limit).then(|| self.flush_type());
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
pub(super) fn flush_value(&mut self) {
|
||||
if let Some(cb) = &self.callback {
|
||||
let (before, after) = self.partition();
|
||||
cb(before, after);
|
||||
pub(super) fn flush_type(&mut self) {
|
||||
self.ticket.next();
|
||||
if let Some(tx) = self.tx.as_ref().filter(|_| self.realtime) {
|
||||
tx.send(InputEvent::Type(self.value().to_owned())).ok();
|
||||
}
|
||||
|
||||
self.flush_trigger(true);
|
||||
}
|
||||
|
||||
pub(super) fn flush_trigger(&self, force: bool) {
|
||||
if let Some(tx) = self.tx.as_ref().filter(|_| self.completion) {
|
||||
tx.send(InputEvent::Trigger(
|
||||
self.partition().0.to_owned(),
|
||||
Some(self.ticket.current()).filter(|_| !force),
|
||||
))
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Input {
|
||||
#[inline]
|
||||
pub fn value(&self) -> &str { &self.snap().value }
|
||||
|
||||
#[inline]
|
||||
pub fn display(&self) -> Cow<'_, str> {
|
||||
if self.obscure {
|
||||
"•".repeat(self.snap().window(self.limit).len()).into()
|
||||
|
|
@ -83,10 +116,8 @@ impl Input {
|
|||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn mode(&self) -> InputMode { self.snap().mode }
|
||||
|
||||
#[inline]
|
||||
pub fn cursor(&self) -> u16 { self.snap().width(self.snap().offset..self.snap().cursor) }
|
||||
|
||||
pub fn cursor_shape(&self) -> SetCursorStyle {
|
||||
|
|
@ -117,16 +148,18 @@ impl Input {
|
|||
Some(s..s + snap.width(start..end))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn partition(&self) -> (&str, &str) {
|
||||
let snap = self.snap();
|
||||
let idx = snap.idx(snap.cursor).unwrap();
|
||||
(&snap.value[..idx], &snap.value[idx..])
|
||||
|
||||
if let Some(sep) = snap.value[idx..].find(SEPARATOR).map(|i| idx + i) {
|
||||
(&snap.value[..sep], &snap.value[sep + 1..])
|
||||
} else {
|
||||
(&snap.value, "")
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn snap(&self) -> &InputSnap { self.snaps.current() }
|
||||
|
||||
#[inline]
|
||||
pub fn snap_mut(&mut self) -> &mut InputSnap { self.snaps.current_mut() }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
yazi_macro::mod_pub!(actor parser);
|
||||
|
||||
yazi_macro::mod_flat!(error input mode op snap snaps widget);
|
||||
yazi_macro::mod_flat!(event input mode op opt separator snap snaps widget);
|
||||
|
|
|
|||
|
|
@ -3,34 +3,35 @@ use mlua::{ExternalError, FromLua, IntoLua, Lua, Value};
|
|||
use tokio::sync::mpsc;
|
||||
use yazi_config::popup::InputCfg;
|
||||
use yazi_shared::event::ActionCow;
|
||||
use yazi_widgets::input::InputError;
|
||||
|
||||
use crate::input::InputEvent;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ShowOpt {
|
||||
pub struct InputOpt {
|
||||
pub cfg: InputCfg,
|
||||
pub tx: mpsc::UnboundedSender<Result<String, InputError>>,
|
||||
pub tx: mpsc::UnboundedSender<InputEvent>,
|
||||
}
|
||||
|
||||
impl TryFrom<ActionCow> for ShowOpt {
|
||||
impl TryFrom<ActionCow> for InputOpt {
|
||||
type Error = anyhow::Error;
|
||||
|
||||
fn try_from(mut a: ActionCow) -> Result<Self, Self::Error> {
|
||||
let Some(cfg) = a.take_any("cfg") else {
|
||||
bail!("Invalid 'cfg' in ShowOpt");
|
||||
bail!("Invalid 'cfg' in InputOpt");
|
||||
};
|
||||
|
||||
let Some(tx) = a.take_any("tx") else {
|
||||
bail!("Invalid 'tx' in ShowOpt");
|
||||
bail!("Invalid 'tx' in InputOpt");
|
||||
};
|
||||
|
||||
Ok(Self { cfg, tx })
|
||||
}
|
||||
}
|
||||
|
||||
impl FromLua for ShowOpt {
|
||||
impl FromLua for InputOpt {
|
||||
fn from_lua(_: Value, _: &Lua) -> mlua::Result<Self> { Err("unsupported".into_lua_err()) }
|
||||
}
|
||||
|
||||
impl IntoLua for ShowOpt {
|
||||
impl IntoLua for InputOpt {
|
||||
fn into_lua(self, _: &Lua) -> mlua::Result<Value> { Err("unsupported".into_lua_err()) }
|
||||
}
|
||||
5
yazi-widgets/src/input/separator.rs
Normal file
5
yazi-widgets/src/input/separator.rs
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
#[cfg(windows)]
|
||||
pub(super) const SEPARATOR: [char; 2] = ['/', '\\'];
|
||||
|
||||
#[cfg(not(windows))]
|
||||
pub(super) const SEPARATOR: char = std::path::MAIN_SEPARATOR;
|
||||
|
|
@ -53,13 +53,10 @@ impl InputSnap {
|
|||
}
|
||||
|
||||
impl InputSnap {
|
||||
#[inline]
|
||||
pub(super) fn len(&self) -> usize { self.value.len() }
|
||||
|
||||
#[inline]
|
||||
pub(super) fn count(&self) -> usize { self.value.chars().count() }
|
||||
|
||||
#[inline]
|
||||
pub(super) fn idx(&self, n: usize) -> Option<usize> {
|
||||
self
|
||||
.value
|
||||
|
|
@ -69,18 +66,15 @@ impl InputSnap {
|
|||
.or_else(|| if n == self.count() { Some(self.len()) } else { None })
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(super) fn slice(&self, range: Range<usize>) -> &str {
|
||||
let (s, e) = (self.idx(range.start), self.idx(range.end));
|
||||
&self.value[s.unwrap()..e.unwrap()]
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(super) fn width(&self, range: Range<usize>) -> u16 {
|
||||
if self.obscure { range.len() as u16 } else { self.slice(range).width() as u16 }
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(super) fn window(&self, limit: usize) -> Range<usize> {
|
||||
Self::find_window(
|
||||
self.value.chars().map(|c| if self.obscure { '•' } else { c }),
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue