feat: make cmp component follow input cursor (#3784)

This commit is contained in:
三咲雅 misaki masa 2026-03-19 21:19:10 +08:00 committed by GitHub
parent cc2414728c
commit a246e9c7c0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
37 changed files with 174 additions and 170 deletions

View file

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

View file

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

View file

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

View file

@ -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)?;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -207,7 +207,7 @@ inactive = {}
# Icons
icon_file = ""
icon_folder = ""
icon_folder = ""
icon_command = ""
# : }}}

View file

@ -207,7 +207,7 @@ inactive = {}
# Icons
icon_file = ""
icon_folder = ""
icon_folder = ""
icon_command = ""
# : }}}

View file

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

View file

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

View file

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

View file

@ -1 +1 @@
yazi_macro::mod_flat!(close show);
yazi_macro::mod_flat!(close);

View file

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

View file

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

View file

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

View file

@ -21,7 +21,7 @@ impl Input {
act!(r#move, self, -1)?;
}
self.flush_value();
self.flush_type();
succ!(render!());
}
}

View file

@ -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!());

View file

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

View file

@ -45,7 +45,7 @@ impl Input {
}
act!(r#move, self)?;
self.flush_value();
self.flush_type();
succ!(render!());
}

View file

@ -41,6 +41,10 @@ impl Input {
n_cur + pad - max
}
};
if n_cur != o_cur {
self.flush_trigger(false);
}
succ!();
}
}

View file

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

View file

@ -31,7 +31,7 @@ impl Input {
}
act!(r#move, self, s.chars().count() as isize)?;
self.flush_value();
self.flush_type();
succ!(render!());
}
}

View file

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

View 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(_)) }
}

View file

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

View file

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

View file

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

View 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;

View file

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