From a246e9c7c06de188341aa7945077aac1255f3e03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=89=E5=92=B2=E9=9B=85=20misaki=20masa?= Date: Thu, 19 Mar 2026 21:19:10 +0800 Subject: [PATCH] feat: make cmp component follow input cursor (#3784) --- yazi-actor/src/cmp/close.rs | 1 + yazi-actor/src/cmp/show.rs | 4 +- yazi-actor/src/cmp/trigger.rs | 26 +++---- yazi-actor/src/input/close.rs | 4 +- yazi-actor/src/input/show.rs | 35 +-------- yazi-actor/src/mgr/cd.rs | 6 +- yazi-actor/src/mgr/create.rs | 3 +- yazi-actor/src/mgr/filter.rs | 8 +- yazi-actor/src/mgr/find.rs | 4 +- yazi-actor/src/mgr/rename.rs | 3 +- yazi-actor/src/mgr/search.rs | 3 +- yazi-actor/src/mgr/shell.rs | 3 +- yazi-actor/src/mgr/tab_rename.rs | 3 +- yazi-binding/src/input.rs | 16 ++-- yazi-config/preset/theme-dark.toml | 2 +- yazi-config/preset/theme-light.toml | 2 +- yazi-core/src/cmp/cmp.rs | 24 +++--- yazi-core/src/input/input.rs | 9 +-- yazi-dds/src/spark/spark.rs | 4 +- yazi-parser/src/input/mod.rs | 2 +- yazi-proxy/src/cmp.rs | 4 +- yazi-proxy/src/input.rs | 4 +- yazi-shared/src/event/action.rs | 7 ++ yazi-widgets/src/input/actor/backspace.rs | 2 +- yazi-widgets/src/input/actor/casefy.rs | 2 +- yazi-widgets/src/input/actor/complete.rs | 10 +-- yazi-widgets/src/input/actor/kill.rs | 2 +- yazi-widgets/src/input/actor/move.rs | 4 + yazi-widgets/src/input/actor/replace.rs | 2 +- yazi-widgets/src/input/actor/type.rs | 2 +- yazi-widgets/src/input/error.rs | 22 ------ yazi-widgets/src/input/event.rs | 14 ++++ yazi-widgets/src/input/input.rs | 77 +++++++++++++------ yazi-widgets/src/input/mod.rs | 2 +- .../show.rs => yazi-widgets/src/input/opt.rs | 17 ++-- yazi-widgets/src/input/separator.rs | 5 ++ yazi-widgets/src/input/snap.rs | 6 -- 37 files changed, 174 insertions(+), 170 deletions(-) delete mode 100644 yazi-widgets/src/input/error.rs create mode 100644 yazi-widgets/src/input/event.rs rename yazi-parser/src/input/show.rs => yazi-widgets/src/input/opt.rs (68%) create mode 100644 yazi-widgets/src/input/separator.rs diff --git a/yazi-actor/src/cmp/close.rs b/yazi-actor/src/cmp/close.rs index c8b16368..a762106a 100644 --- a/yazi-actor/src/cmp/close.rs +++ b/yazi-actor/src/cmp/close.rs @@ -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))); } } diff --git a/yazi-actor/src/cmp/show.rs b/yazi-actor/src/cmp/show.rs index 9c64677c..e9c17593 100644 --- a/yazi-actor/src/cmp/show.rs +++ b/yazi-actor/src/cmp/show.rs @@ -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))); } diff --git a/yazi-actor/src/cmp/trigger.rs b/yazi-actor/src/cmp/trigger.rs index bbe88617..d79d036b 100644 --- a/yazi-actor/src/cmp/trigger.rs +++ b/yazi-actor/src/cmp/trigger.rs @@ -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 { - 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))); } } diff --git a/yazi-actor/src/input/close.rs b/yazi-actor/src/input/close.rs index 705f2bbf..5532109d 100644 --- a/yazi-actor/src/input/close.rs +++ b/yazi-actor/src/input/close.rs @@ -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)?; diff --git a/yazi-actor/src/input/show.rs b/yazi-actor/src/input/show.rs index 06494aff..cab85543 100644 --- a/yazi-actor/src/input/show.rs +++ b/yazi-actor/src/input/show.rs @@ -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!()); } diff --git a/yazi-actor/src/mgr/cd.rs b/yazi-actor/src/mgr/cd.rs index a5aabeb3..28341406 100644 --- a/yazi-actor/src/mgr/cd.rs +++ b/yazi-actor/src/mgr/cd.rs @@ -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, diff --git a/yazi-actor/src/mgr/create.rs b/yazi-actor/src/mgr/create.rs index 05e278a9..5a2b44e1 100644 --- a/yazi-actor/src/mgr/create.rs +++ b/yazi-actor/src/mgr/create.rs @@ -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; } diff --git a/yazi-actor/src/mgr/filter.rs b/yazi-actor/src/mgr/filter.rs index b76098dd..fd4b3eed 100644 --- a/yazi-actor/src/mgr/filter.rs +++ b/yazi-actor/src/mgr/filter.rs @@ -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 }); } diff --git a/yazi-actor/src/mgr/find.rs b/yazi-actor/src/mgr/find.rs index b375c56c..49c8f72a 100644 --- a/yazi-actor/src/mgr/find.rs +++ b/yazi-actor/src/mgr/find.rs @@ -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 }); } }); diff --git a/yazi-actor/src/mgr/rename.rs b/yazi-actor/src/mgr/rename.rs index 0c857d73..b809102d 100644 --- a/yazi-actor/src/mgr/rename.rs +++ b/yazi-actor/src/mgr/rename.rs @@ -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; } diff --git a/yazi-actor/src/mgr/search.rs b/yazi-actor/src/mgr/search.rs index acaab7a7..a6252a5d 100644 --- a/yazi-actor/src/mgr/search.rs +++ b/yazi-actor/src/mgr/search.rs @@ -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); } diff --git a/yazi-actor/src/mgr/shell.rs b/yazi-actor/src/mgr/shell.rs index bc5d7164..c7967ed7 100644 --- a/yazi-actor/src/mgr/shell.rs +++ b/yazi-actor/src/mgr/shell.rs @@ -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, } } diff --git a/yazi-actor/src/mgr/tab_rename.rs b/yazi-actor/src/mgr/tab_rename.rs index 12fb75b2..8a973a28 100644 --- a/yazi-actor/src/mgr/tab_rename.rs +++ b/yazi-actor/src/mgr/tab_rename.rs @@ -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); } }); diff --git a/yazi-binding/src/input.rs b/yazi-binding/src/input.rs index 5c98078f..860c2697 100644 --- a/yazi-binding/src/input.rs +++ b/yazi-binding/src/input.rs @@ -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>> { +pub struct InputRx> { inner: T, } -impl>> InputRx { +impl> InputRx { pub fn new(inner: T) -> Self { Self { inner } } pub async fn consume(inner: T) -> (Option, u8) { @@ -17,17 +17,17 @@ impl>> InputRx { inner.next().await.map(Self::parse).unwrap_or((None, 0)) } - fn parse(res: Result) -> (Option, u8) { + fn parse(res: InputEvent) -> (Option, 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> + 'static> UserData for InputRx { +impl + 'static> UserData for InputRx { fn add_methods>(methods: &mut M) { methods.add_async_method_mut("recv", |_, mut me, ()| async move { let mut inner = unsafe { Pin::new_unchecked(&mut me.inner) }; diff --git a/yazi-config/preset/theme-dark.toml b/yazi-config/preset/theme-dark.toml index 372e5f9c..6cb2a61d 100644 --- a/yazi-config/preset/theme-dark.toml +++ b/yazi-config/preset/theme-dark.toml @@ -207,7 +207,7 @@ inactive = {} # Icons icon_file = "" -icon_folder = "" +icon_folder = "" icon_command = "" # : }}} diff --git a/yazi-config/preset/theme-light.toml b/yazi-config/preset/theme-light.toml index e68d8309..b2ee831d 100644 --- a/yazi-config/preset/theme-light.toml +++ b/yazi-config/preset/theme-light.toml @@ -207,7 +207,7 @@ inactive = {} # Icons icon_file = "" -icon_folder = "" +icon_folder = "" icon_command = "" # : }}} diff --git a/yazi-core/src/cmp/cmp.rs b/yazi-core/src/cmp/cmp.rs index 89616057..1ff188f4 100644 --- a/yazi-core/src/cmp/cmp.rs +++ b/yazi-core/src/cmp/cmp.rs @@ -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>, - pub cands: Vec, - pub offset: usize, - pub cursor: usize, + pub caches: HashMap>, + pub matches: Vec, + pub offset: usize, + pub cursor: usize, pub ticket: Id, + pub handle: Option>>, 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 } diff --git a/yazi-core/src/input/input.rs b/yazi-core/src/input/input.rs index 6edf52c0..130c4e5a 100644 --- a/yazi-core/src/input/input.rs +++ b/yazi-core/src/input/input.rs @@ -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>>, - pub ticket: Rc, } impl Deref for Input { diff --git a/yazi-dds/src/spark/spark.rs b/yazi-dds/src/spark/spark.rs index 19184f76..2a0441d0 100644 --- a/yazi-dds/src/spark/spark.rs +++ b/yazi-dds/src/spark/spark.rs @@ -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); diff --git a/yazi-parser/src/input/mod.rs b/yazi-parser/src/input/mod.rs index 37e61514..727e6aa5 100644 --- a/yazi-parser/src/input/mod.rs +++ b/yazi-parser/src/input/mod.rs @@ -1 +1 @@ -yazi_macro::mod_flat!(close show); +yazi_macro::mod_flat!(close); diff --git a/yazi-proxy/src/cmp.rs b/yazi-proxy/src/cmp.rs index f77a166e..b297f554 100644 --- a/yazi-proxy/src/cmp.rs +++ b/yazi-proxy/src/cmp.rs @@ -9,7 +9,7 @@ impl CmpProxy { emit!(Call(relay!(cmp:show).with_any("opt", opt))); } - pub fn trigger(word: impl Into, ticket: Id) { - emit!(Call(relay!(cmp:trigger, [word.into()]).with("ticket", ticket))); + pub fn trigger(word: impl Into, ticket: Option) { + emit!(Call(relay!(cmp:trigger, [word.into()]).with_opt("ticket", ticket))); } } diff --git a/yazi-proxy/src/input.rs b/yazi-proxy/src/input.rs index cb5ccc53..3e0b7270 100644 --- a/yazi-proxy/src/input.rs +++ b/yazi-proxy/src/input.rs @@ -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> { + pub fn show(cfg: InputCfg) -> mpsc::UnboundedReceiver { let (tx, rx) = mpsc::unbounded_channel(); emit!(Call(relay!(input:show).with_any("tx", tx).with_any("cfg", cfg))); rx diff --git a/yazi-shared/src/event/action.rs b/yazi-shared/src/event/action.rs index 78df5796..cb6e9c6d 100644 --- a/yazi-shared/src/event/action.rs +++ b/yazi-shared/src/event/action.rs @@ -65,6 +65,13 @@ impl Action { self } + pub fn with_opt(mut self, name: impl Into, value: Option>) -> Self { + if let Some(value) = value { + self.args.insert(name.into(), value.into()); + } + self + } + pub fn with_seq(mut self, values: I) -> Self where I: IntoIterator, diff --git a/yazi-widgets/src/input/actor/backspace.rs b/yazi-widgets/src/input/actor/backspace.rs index baf42b5f..11407702 100644 --- a/yazi-widgets/src/input/actor/backspace.rs +++ b/yazi-widgets/src/input/actor/backspace.rs @@ -21,7 +21,7 @@ impl Input { act!(r#move, self, -1)?; } - self.flush_value(); + self.flush_type(); succ!(render!()); } } diff --git a/yazi-widgets/src/input/actor/casefy.rs b/yazi-widgets/src/input/actor/casefy.rs index 2d45119e..317162f6 100644 --- a/yazi-widgets/src/input/actor/casefy.rs +++ b/yazi-widgets/src/input/actor/casefy.rs @@ -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!()); diff --git a/yazi-widgets/src/input/actor/complete.rs b/yazi-widgets/src/input/actor/complete.rs index d2986542..f07c8d0b 100644 --- a/yazi-widgets/src/input/actor/complete.rs +++ b/yazi-widgets/src/input/actor/complete.rs @@ -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 { @@ -30,7 +24,7 @@ impl Input { snap.value = new; act!(r#move, self, delta)?; - self.flush_value(); + self.flush_type(); succ!(render!()); } } diff --git a/yazi-widgets/src/input/actor/kill.rs b/yazi-widgets/src/input/actor/kill.rs index 08d068e9..36bd1b2d 100644 --- a/yazi-widgets/src/input/actor/kill.rs +++ b/yazi-widgets/src/input/actor/kill.rs @@ -45,7 +45,7 @@ impl Input { } act!(r#move, self)?; - self.flush_value(); + self.flush_type(); succ!(render!()); } diff --git a/yazi-widgets/src/input/actor/move.rs b/yazi-widgets/src/input/actor/move.rs index 0db30d71..3b817006 100644 --- a/yazi-widgets/src/input/actor/move.rs +++ b/yazi-widgets/src/input/actor/move.rs @@ -41,6 +41,10 @@ impl Input { n_cur + pad - max } }; + + if n_cur != o_cur { + self.flush_trigger(false); + } succ!(); } } diff --git a/yazi-widgets/src/input/actor/replace.rs b/yazi-widgets/src/input/actor/replace.rs index 04c4be4c..652b7563 100644 --- a/yazi-widgets/src/input/actor/replace.rs +++ b/yazi-widgets/src/input/actor/replace.rs @@ -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!()); } } diff --git a/yazi-widgets/src/input/actor/type.rs b/yazi-widgets/src/input/actor/type.rs index 2685a750..93006a27 100644 --- a/yazi-widgets/src/input/actor/type.rs +++ b/yazi-widgets/src/input/actor/type.rs @@ -31,7 +31,7 @@ impl Input { } act!(r#move, self, s.chars().count() as isize)?; - self.flush_value(); + self.flush_type(); succ!(render!()); } } diff --git a/yazi-widgets/src/input/error.rs b/yazi-widgets/src/input/error.rs deleted file mode 100644 index ad96e5e7..00000000 --- a/yazi-widgets/src/input/error.rs +++ /dev/null @@ -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 {} diff --git a/yazi-widgets/src/input/event.rs b/yazi-widgets/src/input/event.rs new file mode 100644 index 00000000..fc1fefb1 --- /dev/null +++ b/yazi-widgets/src/input/event.rs @@ -0,0 +1,14 @@ +use yazi_shared::Id; + +#[derive(Debug)] +pub enum InputEvent { + Submit(String), + Cancel(String), + + Type(String), + Trigger(String, Option), +} + +impl InputEvent { + pub fn is_submit(&self) -> bool { matches!(self, Self::Submit(_)) } +} diff --git a/yazi-widgets/src/input/input.rs b/yazi-widgets/src/input/input.rs index 8c636592..d62edd0f 100644 --- a/yazi-widgets/src/input/input.rs +++ b/yazi-widgets/src/input/input.rs @@ -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; +use crate::{CLIPBOARD, input::{InputEvent, InputOpt, SEPARATOR}}; #[derive(Default)] pub struct Input { - pub snaps: InputSnaps, - pub limit: usize, - pub obscure: bool, - pub callback: Option, + pub snaps: InputSnaps, + pub limit: usize, + pub obscure: bool, + pub realtime: bool, + pub completion: bool, + + pub tx: Option>, + 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 { + 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() } } diff --git a/yazi-widgets/src/input/mod.rs b/yazi-widgets/src/input/mod.rs index a2c53152..9938b24f 100644 --- a/yazi-widgets/src/input/mod.rs +++ b/yazi-widgets/src/input/mod.rs @@ -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); diff --git a/yazi-parser/src/input/show.rs b/yazi-widgets/src/input/opt.rs similarity index 68% rename from yazi-parser/src/input/show.rs rename to yazi-widgets/src/input/opt.rs index a7e31f28..4e6f1dd7 100644 --- a/yazi-parser/src/input/show.rs +++ b/yazi-widgets/src/input/opt.rs @@ -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>, + pub tx: mpsc::UnboundedSender, } -impl TryFrom for ShowOpt { +impl TryFrom for InputOpt { type Error = anyhow::Error; fn try_from(mut a: ActionCow) -> Result { 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 { Err("unsupported".into_lua_err()) } } -impl IntoLua for ShowOpt { +impl IntoLua for InputOpt { fn into_lua(self, _: &Lua) -> mlua::Result { Err("unsupported".into_lua_err()) } } diff --git a/yazi-widgets/src/input/separator.rs b/yazi-widgets/src/input/separator.rs new file mode 100644 index 00000000..07e410a1 --- /dev/null +++ b/yazi-widgets/src/input/separator.rs @@ -0,0 +1,5 @@ +#[cfg(windows)] +pub(super) const SEPARATOR: [char; 2] = ['/', '\\']; + +#[cfg(not(windows))] +pub(super) const SEPARATOR: char = std::path::MAIN_SEPARATOR; diff --git a/yazi-widgets/src/input/snap.rs b/yazi-widgets/src/input/snap.rs index b3584cc0..0ae7228f 100644 --- a/yazi-widgets/src/input/snap.rs +++ b/yazi-widgets/src/input/snap.rs @@ -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 { 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) -> &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) -> u16 { if self.obscure { range.len() as u16 } else { self.slice(range).width() as u16 } } - #[inline] pub(super) fn window(&self, limit: usize) -> Range { Self::find_window( self.value.chars().map(|c| if self.obscure { '•' } else { c }),