diff --git a/Cargo.lock b/Cargo.lock index 99986894..7ae987c9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -620,9 +620,9 @@ dependencies = [ [[package]] name = "deranged" -version = "0.3.11" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" +checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" dependencies = [ "powerfmt", ] @@ -2589,9 +2589,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.39" +version = "0.3.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dad298b01a40a23aac4580b67e3dbedb7cc8402f3592d7f49469de2ea4aecdd8" +checksum = "9d9c75b47bdff86fa3334a3db91356b8d7d86a9b839dab7d0bdc5c3d3a077618" dependencies = [ "deranged", "itoa", @@ -2606,15 +2606,15 @@ dependencies = [ [[package]] name = "time-core" -version = "0.1.3" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "765c97a5b985b7c11d7bc27fa927dc4fe6af3a6dfb021d28deb60d3bf51e76ef" +checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c" [[package]] name = "time-macros" -version = "0.2.20" +version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8093bc3e81c3bc5f7879de09619d06c9a5a5e45ca44dfeeb7225bae38005c5c" +checksum = "29aa485584182073ed57fd5004aa09c371f021325014694e432313345865fd04" dependencies = [ "num-conv", "time-core", @@ -3191,9 +3191,9 @@ dependencies = [ [[package]] name = "windows-link" -version = "0.1.0" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6dccfd733ce2b1753b03b6d3c65edf020262ea35e20ccdf3e288043e6dd620e3" +checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38" [[package]] name = "windows-result" @@ -3433,6 +3433,7 @@ dependencies = [ "yazi-proxy", "yazi-scheduler", "yazi-shared", + "yazi-widgets", ] [[package]] @@ -3482,7 +3483,6 @@ dependencies = [ "ratatui", "scopeguard", "signal-hook-tokio", - "syntect", "textwrap", "tikv-jemallocator", "tokio", @@ -3501,6 +3501,7 @@ dependencies = [ "yazi-plugin", "yazi-proxy", "yazi-shared", + "yazi-widgets", ] [[package]] @@ -3630,6 +3631,20 @@ dependencies = [ "yazi-macro", ] +[[package]] +name = "yazi-widgets" +version = "25.3.7" +dependencies = [ + "futures", + "ratatui", + "unicode-width 0.2.0", + "yazi-codegen", + "yazi-config", + "yazi-macro", + "yazi-plugin", + "yazi-shared", +] + [[package]] name = "yoke" version = "0.7.5" diff --git a/Cargo.toml b/Cargo.toml index 5d82d069..80ebeda0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,6 +37,7 @@ regex = "1.11.1" scopeguard = "1.2.0" serde = { version = "1.0.219", features = [ "derive" ] } serde_json = "1.0.140" +syntect = { version = "5.2.0", default-features = false, features = [ "parsing", "plist-load", "regex-onig" ] } tokio = { version = "1.44.1", features = [ "full" ] } tokio-stream = "0.1.17" tokio-util = "0.7.14" diff --git a/yazi-config/src/popup/input.rs b/yazi-config/src/popup/input.rs index f2c05c37..71c87ba0 100644 --- a/yazi-config/src/popup/input.rs +++ b/yazi-config/src/popup/input.rs @@ -46,7 +46,7 @@ pub struct Input { } impl Input { - pub const fn border(&self) -> u16 { 2 } + pub const fn border(&self) -> u16 { 1 } } impl FromStr for Input { diff --git a/yazi-core/Cargo.toml b/yazi-core/Cargo.toml index d7ad6db4..2ad16e76 100644 --- a/yazi-core/Cargo.toml +++ b/yazi-core/Cargo.toml @@ -20,6 +20,7 @@ yazi-plugin = { path = "../yazi-plugin", version = "25.3.7" } yazi-proxy = { path = "../yazi-proxy", version = "25.3.7" } yazi-scheduler = { path = "../yazi-scheduler", version = "25.3.7" } yazi-shared = { path = "../yazi-shared", version = "25.3.7" } +yazi-widgets = { path = "../yazi-widgets", version = "25.3.7" } # External dependencies anyhow = { workspace = true } diff --git a/yazi-core/src/cmp/commands/close.rs b/yazi-core/src/cmp/commands/close.rs index 81e80634..6e1d3c46 100644 --- a/yazi-core/src/cmp/commands/close.rs +++ b/yazi-core/src/cmp/commands/close.rs @@ -1,3 +1,5 @@ +use std::mem; + use yazi_macro::render; use yazi_proxy::InputProxy; use yazi_shared::event::CmdCow; @@ -23,7 +25,6 @@ impl Cmp { } self.caches.clear(); - self.visible = false; - render!(); + render!(mem::replace(&mut self.visible, false)); } } diff --git a/yazi-core/src/help/commands/filter.rs b/yazi-core/src/help/commands/filter.rs index 29fcc540..0bfb54af 100644 --- a/yazi-core/src/help/commands/filter.rs +++ b/yazi-core/src/help/commands/filter.rs @@ -1,15 +1,11 @@ -use yazi_config::popup::{Offset, Origin, Position}; use yazi_macro::render; use yazi_shared::event::CmdCow; -use crate::{help::Help, input::Input}; +use crate::help::Help; impl Help { pub fn filter(&mut self, _: CmdCow) { - let mut input = Input::default(); - input.position = Position::new(Origin::BottomLeft, Offset::line()); - - self.in_filter = Some(input); + self.in_filter = Some(Default::default()); self.filter_apply(); render!(); } diff --git a/yazi-core/src/help/help.rs b/yazi-core/src/help/help.rs index 00670f8a..a9c12fc6 100644 --- a/yazi-core/src/help/help.rs +++ b/yazi-core/src/help/help.rs @@ -1,12 +1,11 @@ -use crossterm::event::KeyCode; +use crossterm::{cursor::SetCursorStyle, event::KeyCode}; use unicode_width::UnicodeWidthStr; use yazi_adapter::Dimension; -use yazi_config::{KEYMAP, keymap::{Chord, Key}}; +use yazi_config::{INPUT, KEYMAP, keymap::{Chord, Key}}; use yazi_macro::{render, render_and}; use yazi_shared::Layer; use super::HELP_MARGIN; -use crate::input::Input; #[derive(Default)] pub struct Help { @@ -16,7 +15,7 @@ pub struct Help { // Filter pub(super) keyword: String, - pub(super) in_filter: Option, + pub(super) in_filter: Option, pub(super) offset: usize, pub(super) cursor: usize, @@ -89,7 +88,7 @@ impl Help { .as_ref() .map(|i| i.value()) .or(Some(self.keyword.as_str()).filter(|&s| !s.is_empty())) - .map(|s| format!("Filter: {}", s)) + .map(|s| format!("Filter: {s}")) } // --- Bindings @@ -113,4 +112,9 @@ impl Help { #[inline] pub fn rel_cursor(&self) -> usize { self.cursor - self.offset } + + #[inline] + pub fn cursor_shape(&self) -> SetCursorStyle { + if INPUT.cursor_blink { SetCursorStyle::BlinkingBlock } else { SetCursorStyle::SteadyBlock } + } } diff --git a/yazi-core/src/input/commands/close.rs b/yazi-core/src/input/commands/close.rs index a3a5bc26..816c012f 100644 --- a/yazi-core/src/input/commands/close.rs +++ b/yazi-core/src/input/commands/close.rs @@ -18,17 +18,15 @@ impl From for Opt { impl Input { #[yazi_codegen::command] pub fn close(&mut self, opt: Opt) { - if self.completion { - CmpProxy::close(); - } - - if let Some(cb) = self.callback.take() { - let value = self.snap_mut().value.clone(); - _ = cb.send(if opt.submit { Ok(value) } else { Err(InputError::Canceled(value)) }); - } - - self.ticket = self.ticket.wrapping_add(1); self.visible = false; + self.ticket.next(); + + if let Some(tx) = self.tx.take() { + let value = self.snap().value.clone(); + _ = tx.send(if opt.submit { Ok(value) } else { Err(InputError::Canceled(value)) }); + } + + CmpProxy::close(); render!(); } } diff --git a/yazi-core/src/input/commands/escape.rs b/yazi-core/src/input/commands/escape.rs index f376a807..03811ddb 100644 --- a/yazi-core/src/input/commands/escape.rs +++ b/yazi-core/src/input/commands/escape.rs @@ -1,8 +1,9 @@ use yazi_macro::render; use yazi_proxy::CmpProxy; use yazi_shared::event::CmdCow; +use yazi_widgets::input::InputOp; -use crate::input::{Input, InputMode, op::InputOp}; +use crate::input::Input; struct Opt; @@ -16,28 +17,16 @@ impl From<()> for Opt { impl Input { #[yazi_codegen::command] pub fn escape(&mut self, _: Opt) { - let snap = self.snap_mut(); - match snap.mode { - InputMode::Normal if snap.op == InputOp::None => { - self.close(false); - } - InputMode::Normal => { - snap.op = InputOp::None; - } - InputMode::Insert => { - snap.mode = InputMode::Normal; - self.move_(-1); + use yazi_widgets::input::InputMode as M; - if self.completion { - CmpProxy::close(); - } - } - InputMode::Replace => { - snap.mode = InputMode::Normal; - } + let mode = self.snap().mode; + match mode { + M::Normal if self.snap_mut().op == InputOp::None => self.close(false), + M::Insert => CmpProxy::close(), + M::Normal | M::Replace => {} } - self.snaps.tag(self.limit()); + self.inner.escape(()); render!(); } } diff --git a/yazi-core/src/input/commands/mod.rs b/yazi-core/src/input/commands/mod.rs index 87017d3f..d23f3447 100644 --- a/yazi-core/src/input/commands/mod.rs +++ b/yazi-core/src/input/commands/mod.rs @@ -1 +1 @@ -yazi_macro::mod_flat!(backspace backward close complete delete escape forward insert kill move_ paste redo replace show type_ undo visual yank); +yazi_macro::mod_flat!(close escape show); diff --git a/yazi-core/src/input/commands/show.rs b/yazi-core/src/input/commands/show.rs index ac4e3879..0c3ea1f2 100644 --- a/yazi-core/src/input/commands/show.rs +++ b/yazi-core/src/input/commands/show.rs @@ -1,7 +1,8 @@ use tokio::sync::mpsc; -use yazi_config::popup::InputCfg; +use yazi_config::{INPUT, popup::InputCfg}; use yazi_macro::render; use yazi_shared::{errors::InputError, event::CmdCow}; +use yazi_widgets::input::InputCallback; use crate::input::Input; @@ -20,7 +21,7 @@ impl TryFrom for Opt { impl Input { pub fn show(&mut self, opt: impl TryInto) { - let Ok(opt) = opt.try_into() else { return }; + let Ok(opt): Result = opt.try_into() else { return }; self.close(false); self.visible = true; @@ -28,17 +29,28 @@ impl Input { self.position = opt.cfg.position; // Typing - self.callback = Some(opt.tx); - self.realtime = opt.cfg.realtime; - self.completion = opt.cfg.completion; + self.tx = Some(opt.tx.clone()); + let ticket = self.ticket.clone(); // Shell self.highlight = opt.cfg.highlight; - // Reset snaps - self.snaps.reset(opt.cfg.value, self.limit()); + // 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(); + } + }); + self.inner = yazi_widgets::input::Input::new( + opt.cfg.value, + opt.cfg.position.offset.width.saturating_sub(INPUT.border()) as usize, + cb, + ); // Set cursor after reset + // TODO: remove this if let Some(cursor) = opt.cfg.cursor { self.snap_mut().cursor = cursor; self.move_(0); diff --git a/yazi-core/src/input/input.rs b/yazi-core/src/input/input.rs index 27dff313..879a0463 100644 --- a/yazi-core/src/input/input.rs +++ b/yazi-core/src/input/input.rs @@ -1,131 +1,31 @@ -use std::ops::Range; +use std::{ops::{Deref, DerefMut}, rc::Rc}; use tokio::sync::mpsc::UnboundedSender; -use unicode_width::UnicodeWidthStr; -use yazi_config::{INPUT, popup::Position}; -use yazi_plugin::CLIPBOARD; -use yazi_shared::errors::InputError; - -use super::{InputSnap, InputSnaps, mode::InputMode, op::InputOp}; +use yazi_config::popup::Position; +use yazi_shared::{Ids, errors::InputError}; #[derive(Default)] pub struct Input { - pub(super) snaps: InputSnaps, - pub ticket: usize, - pub visible: bool, + pub(super) inner: yazi_widgets::input::Input, + pub visible: bool, pub title: String, pub position: Position, // Typing - pub(super) callback: Option>>, - pub(super) realtime: bool, - pub(super) completion: bool, + pub(super) tx: Option>>, + pub(super) ticket: Rc, // Shell pub highlight: bool, } -impl Input { - #[inline] - pub(super) fn limit(&self) -> usize { - self.position.offset.width.saturating_sub(INPUT.border()) as usize - } +impl Deref for Input { + type Target = yazi_widgets::input::Input; - pub(super) fn handle_op(&mut self, cursor: usize, include: bool) -> bool { - let old = self.snap().clone(); - let snap = self.snap_mut(); - - match snap.op { - InputOp::None | InputOp::Select(_) => { - snap.cursor = cursor; - } - InputOp::Delete(cut, insert, _) => { - let range = snap.op.range(cursor, include).unwrap(); - let Range { start, end } = snap.idx(range.start)..snap.idx(range.end); - - let drain = snap.value.drain(start.unwrap()..end.unwrap()).collect::(); - if cut { - futures::executor::block_on(CLIPBOARD.set(&drain)); - } - - snap.op = InputOp::None; - snap.mode = if insert { InputMode::Insert } else { InputMode::Normal }; - snap.cursor = range.start; - } - InputOp::Yank(_) => { - let range = snap.op.range(cursor, include).unwrap(); - let Range { start, end } = snap.idx(range.start)..snap.idx(range.end); - let yanked = &snap.value[start.unwrap()..end.unwrap()]; - - snap.op = InputOp::None; - futures::executor::block_on(CLIPBOARD.set(yanked)); - } - }; - - snap.cursor = snap.count().saturating_sub(snap.mode.delta()).min(snap.cursor); - if snap == &old { - return false; - } - if !matches!(old.op, InputOp::None | InputOp::Select(_)) { - self.snaps.tag(self.limit()).then(|| self.flush_value()); - } - true - } - - pub(super) fn flush_value(&mut self) { - let Some(tx) = &self.callback else { return }; - self.ticket = self.ticket.wrapping_add(1); - - if self.realtime { - let value = self.snap().value.clone(); - tx.send(Err(InputError::Typed(value))).ok(); - } - - if self.completion { - let before = self.partition()[0].to_owned(); - tx.send(Err(InputError::Completed(before, self.ticket))).ok(); - } - } + fn deref(&self) -> &Self::Target { &self.inner } } -impl Input { - #[inline] - pub fn value(&self) -> &str { self.snap().slice(self.snap().window(self.limit())) } - - #[inline] - pub fn mode(&self) -> InputMode { self.snap().mode } - - #[inline] - pub fn cursor(&self) -> u16 { - let snap = self.snap(); - snap.slice(snap.offset..snap.cursor).width() as u16 - } - - pub fn selected(&self) -> Option> { - let snap = self.snap(); - let start = snap.op.start()?; - - let (start, end) = - if start < snap.cursor { (start, snap.cursor) } else { (snap.cursor + 1, start + 1) }; - - let win = snap.window(self.limit()); - let Range { start, end } = start.max(win.start)..end.min(win.end); - - let s = snap.slice(snap.offset..start).width() as u16; - Some(s..s + snap.slice(start..end).width() as u16) - } - - #[inline] - pub fn partition(&self) -> [&str; 2] { - let snap = self.snap(); - let idx = snap.idx(snap.cursor).unwrap(); - [&snap.value[..idx], &snap.value[idx..]] - } - - #[inline] - pub(super) fn snap(&self) -> &InputSnap { self.snaps.current() } - - #[inline] - pub(super) fn snap_mut(&mut self) -> &mut InputSnap { self.snaps.current_mut() } +impl DerefMut for Input { + fn deref_mut(&mut self) -> &mut Self::Target { &mut self.inner } } diff --git a/yazi-core/src/input/mod.rs b/yazi-core/src/input/mod.rs index bc6b58ab..e77b2fe8 100644 --- a/yazi-core/src/input/mod.rs +++ b/yazi-core/src/input/mod.rs @@ -1,3 +1,3 @@ yazi_macro::mod_pub!(commands); -yazi_macro::mod_flat!(input mode op snap snaps); +yazi_macro::mod_flat!(input); diff --git a/yazi-fm/Cargo.toml b/yazi-fm/Cargo.toml index 4dba97a9..360e1e7b 100644 --- a/yazi-fm/Cargo.toml +++ b/yazi-fm/Cargo.toml @@ -24,6 +24,7 @@ yazi-macro = { path = "../yazi-macro", version = "25.3.7" } yazi-plugin = { path = "../yazi-plugin", version = "25.3.7" } yazi-proxy = { path = "../yazi-proxy", version = "25.3.7" } yazi-shared = { path = "../yazi-shared", version = "25.3.7" } +yazi-widgets = { path = "../yazi-widgets", version = "25.3.7" } # External dependencies anyhow = { workspace = true } @@ -35,7 +36,6 @@ indexmap = { workspace = true } mlua = { workspace = true } ratatui = { workspace = true } scopeguard = { workspace = true } -syntect = { version = "5.2.0", default-features = false, features = [ "parsing", "plist-load", "regex-onig" ] } tokio = { workspace = true } tokio-stream = { workspace = true } diff --git a/yazi-fm/src/app/app.rs b/yazi-fm/src/app/app.rs index 5d0253ea..2a16e14c 100644 --- a/yazi-fm/src/app/app.rs +++ b/yazi-fm/src/app/app.rs @@ -3,9 +3,9 @@ use std::sync::atomic::Ordering; use anyhow::Result; use crossterm::event::KeyEvent; use yazi_config::keymap::Key; -use yazi_core::input::InputMode; use yazi_macro::emit; use yazi_shared::event::{CmdCow, Event, NEED_RENDER}; +use yazi_widgets::input::InputMode; use crate::{Ctx, Executor, Router, Signals, Term, lives::Lives}; diff --git a/yazi-fm/src/app/commands/render.rs b/yazi-fm/src/app/commands/render.rs index 97a80c69..a7e2fadd 100644 --- a/yazi-fm/src/app/commands/render.rs +++ b/yazi-fm/src/app/commands/render.rs @@ -1,8 +1,7 @@ -use std::sync::atomic::Ordering; +use std::sync::atomic::{AtomicU8, Ordering}; -use crossterm::{execute, queue, terminal::{BeginSynchronizedUpdate, EndSynchronizedUpdate}}; -use ratatui::{CompletedFrame, backend::{Backend, CrosstermBackend}, buffer::Buffer}; -use scopeguard::defer; +use crossterm::{cursor::{MoveTo, SetCursorStyle, Show}, execute, queue, terminal::{BeginSynchronizedUpdate, EndSynchronizedUpdate}}; +use ratatui::{CompletedFrame, backend::{Backend, CrosstermBackend}, buffer::Buffer, layout::Position}; use yazi_plugin::elements::COLLISION; use yazi_shared::{event::NEED_RENDER, tty::TTY}; @@ -13,22 +12,18 @@ impl App { NEED_RENDER.store(false, Ordering::Relaxed); let Some(term) = &mut self.term else { return }; - queue!(TTY.writer(), BeginSynchronizedUpdate).ok(); - defer! { execute!(TTY.writer(), EndSynchronizedUpdate).ok(); } + Self::routine(true, None); + let _guard = scopeguard::guard(self.cx.cursor(), |c| Self::routine(false, c)); let collision = COLLISION.swap(false, Ordering::Relaxed); let frame = term .draw(|f| { _ = Lives::scope(&self.cx, || Ok(f.render_widget(Root::new(&self.cx), f.area()))); - - if let Some(pos) = self.cx.cursor() { - f.set_cursor_position(pos); - } }) .unwrap(); if COLLISION.load(Ordering::Relaxed) { - Self::patch(frame, self.cx.cursor()); + Self::patch(frame); } if !self.cx.notify.messages.is_empty() { self.render_partially(); @@ -46,6 +41,9 @@ impl App { return self.render(); } + Self::routine(true, None); + let _guard = scopeguard::guard(self.cx.cursor(), |c| Self::routine(false, c)); + let frame = term .draw_partial(|f| { _ = Lives::scope(&self.cx, || { @@ -53,20 +51,16 @@ impl App { f.render_widget(crate::notify::Notify::new(&self.cx), f.area()); Ok(()) }); - - if let Some(pos) = self.cx.cursor() { - f.set_cursor_position(pos); - } }) .unwrap(); if COLLISION.load(Ordering::Relaxed) { - Self::patch(frame, self.cx.cursor()); + Self::patch(frame); } } #[inline] - fn patch(frame: CompletedFrame, cursor: Option<(u16, u16)>) { + fn patch(frame: CompletedFrame) { let mut new = Buffer::empty(frame.area); for y in new.area.top()..new.area.bottom() { for x in new.area.left()..new.area.right() { @@ -79,14 +73,23 @@ impl App { } let patches = frame.buffer.diff(&new); - let stdout = &mut *TTY.lockout(); + CrosstermBackend::new(&mut *TTY.lockout()).draw(patches.into_iter()).ok(); + } - let mut backend = CrosstermBackend::new(stdout); - backend.draw(patches.into_iter()).ok(); - if let Some(pos) = cursor { - backend.show_cursor().ok(); - backend.set_cursor_position(pos).ok(); + fn routine(push: bool, cursor: Option<(Position, SetCursorStyle)>) { + static COUNT: AtomicU8 = AtomicU8::new(0); + if push && COUNT.fetch_add(1, Ordering::Relaxed) != 0 { + return; + } else if !push && COUNT.fetch_sub(1, Ordering::Relaxed) != 1 { + return; } - backend.flush().ok(); + + _ = if push { + queue!(TTY.writer(), BeginSynchronizedUpdate) + } else if let Some((Position { x, y }, shape)) = cursor { + execute!(TTY.writer(), shape, MoveTo(x, y), Show, EndSynchronizedUpdate) + } else { + execute!(TTY.writer(), EndSynchronizedUpdate) + }; } } diff --git a/yazi-fm/src/context.rs b/yazi-fm/src/context.rs index f8d5b1b7..cc1f012b 100644 --- a/yazi-fm/src/context.rs +++ b/yazi-fm/src/context.rs @@ -1,4 +1,5 @@ -use ratatui::layout::Rect; +use crossterm::cursor::SetCursorStyle; +use ratatui::layout::{Position, Rect}; use yazi_core::{cmp::Cmp, confirm::Confirm, help::Help, input::Input, mgr::Mgr, notify::Notify, pick::Pick, tab::{Folder, Tab}, tasks::Tasks, which::Which}; use yazi_shared::Layer; @@ -30,13 +31,16 @@ impl Ctx { } #[inline] - pub fn cursor(&self) -> Option<(u16, u16)> { + pub fn cursor(&self) -> Option<(Position, SetCursorStyle)> { if self.input.visible { let Rect { x, y, .. } = self.mgr.area(self.input.position); - return Some((x + 1 + self.input.cursor(), y + 1)); + return Some(( + Position { x: x + 1 + self.input.cursor(), y: y + 1 }, + self.input.cursor_shape(), + )); } if let Some((x, y)) = self.help.cursor() { - return Some((x, y)); + return Some((Position { x, y }, self.help.cursor_shape())); } None } diff --git a/yazi-fm/src/executor.rs b/yazi-fm/src/executor.rs index a5a21700..282c1ad6 100644 --- a/yazi-fm/src/executor.rs +++ b/yazi-fm/src/executor.rs @@ -1,5 +1,5 @@ -use yazi_core::input::InputMode; use yazi_shared::{Layer, event::CmdCow}; +use yazi_widgets::input::InputMode; use crate::app::App; @@ -233,57 +233,30 @@ impl<'a> Executor<'a> { return self.app.cx.input.$name(cmd); } }; - ($name:ident, $alias:literal) => { - if cmd.name == $alias { - return self.app.cx.input.$name(cmd); - } - }; } + on!(escape); on!(show); on!(close); - on!(escape); - on!(move_, "move"); - on!(backward); - on!(forward); - - if cmd.name.as_str() == "complete" { - return if cmd.bool("trigger") { - self.app.cx.cmp.trigger(cmd) - } else { - self.app.cx.input.complete(cmd) - }; - } match self.app.cx.input.mode() { InputMode::Normal => { - on!(insert); - on!(visual); - on!(replace); - - on!(delete); - on!(yank); - on!(paste); - - on!(undo); - on!(redo); - match cmd.name.as_str() { // Help - "help" => self.app.cx.help.toggle(Layer::Input), + "help" => return self.app.cx.help.toggle(Layer::Input), // Plugin - "plugin" => self.app.plugin(cmd), + "plugin" => return self.app.plugin(cmd), _ => {} } } - InputMode::Insert => { - on!(visual); - - on!(backspace); - on!(kill); - } + InputMode::Insert => match cmd.name.as_str() { + "complete" if cmd.bool("trigger") => return self.app.cx.cmp.trigger(cmd), + _ => {} + }, InputMode::Replace => {} - } + }; + + self.app.cx.input.execute(cmd) } fn confirm(&mut self, cmd: CmdCow) { diff --git a/yazi-fm/src/input/input.rs b/yazi-fm/src/input/input.rs index cb27e8f3..4c85dcb4 100644 --- a/yazi-fm/src/input/input.rs +++ b/yazi-fm/src/input/input.rs @@ -1,13 +1,7 @@ -use std::ops::Range; +use ratatui::{buffer::Buffer, layout::{Margin, Rect}, text::Line, widgets::{Block, BorderType, Widget}}; +use yazi_config::THEME; -use anyhow::{Result, bail}; -use ratatui::{buffer::Buffer, layout::Rect, text::Line, widgets::{Block, BorderType, Paragraph, Widget}}; -use syntect::easy::HighlightLines; -use yazi_config::{PREVIEW, THEME}; -use yazi_core::input::InputMode; -use yazi_plugin::external::Highlighter; - -use crate::{Ctx, Term}; +use crate::Ctx; pub(crate) struct Input<'a> { cx: &'a Ctx, @@ -15,52 +9,21 @@ pub(crate) struct Input<'a> { impl<'a> Input<'a> { pub(crate) fn new(cx: &'a Ctx) -> Self { Self { cx } } - - fn highlighted_value(&self) -> Result> { - if !self.cx.input.highlight { - bail!("Highlighting is disabled"); - } - - let (theme, syntaxes) = Highlighter::init(); - if let Some(syntax) = syntaxes.find_syntax_by_name("Bourne Again Shell (bash)") { - let mut h = HighlightLines::new(syntax, theme); - let regions = h.highlight_line(self.cx.input.value(), syntaxes)?; - return Ok(Highlighter::to_line_widget(regions, &PREVIEW.indent())); - } - bail!("Failed to find syntax") - } } impl Widget for Input<'_> { - fn render(self, win: Rect, buf: &mut Buffer) { + fn render(self, _: Rect, buf: &mut Buffer) { let input = &self.cx.input; let area = self.cx.mgr.area(input.position); yazi_plugin::elements::Clear::default().render(area, buf); - Paragraph::new(self.highlighted_value().unwrap_or_else(|_| Line::from(input.value()))) - .block( - Block::bordered() - .border_type(BorderType::Rounded) - .border_style(THEME.input.border) - .title(Line::styled(&input.title, THEME.input.title)), - ) - .style(THEME.input.value) + + Block::bordered() + .border_type(BorderType::Rounded) + .border_style(THEME.input.border) + .title(Line::styled(&input.title, THEME.input.title)) .render(area, buf); - if let Some(Range { start, end }) = input.selected() { - let x = win.width.min(area.x + 1 + start); - let y = win.height.min(area.y + 1); - - buf.set_style( - Rect { x, y, width: (end - start).min(win.width - x), height: 1.min(win.height - y) }, - THEME.input.selected, - ) - } - - _ = match input.mode() { - InputMode::Insert => Term::set_cursor_bar(), - InputMode::Replace => Term::set_cursor_underscore(), - _ => Term::set_cursor_block(), - }; + input.render(area.inner(Margin::new(1, 1)), buf); } } diff --git a/yazi-fm/src/term.rs b/yazi-fm/src/term.rs index 182ee1af..e0d22bf2 100644 --- a/yazi-fm/src/term.rs +++ b/yazi-fm/src/term.rs @@ -1,11 +1,11 @@ use std::{io, ops::{Deref, DerefMut}, sync::atomic::{AtomicBool, AtomicU8, Ordering}}; use anyhow::Result; -use crossterm::{Command, event::{DisableBracketedPaste, EnableBracketedPaste, KeyboardEnhancementFlags, PopKeyboardEnhancementFlags, PushKeyboardEnhancementFlags}, execute, queue, style::Print, terminal::{LeaveAlternateScreen, SetTitle, disable_raw_mode, enable_raw_mode}}; +use crossterm::{Command, event::{DisableBracketedPaste, EnableBracketedPaste, KeyboardEnhancementFlags, PopKeyboardEnhancementFlags, PushKeyboardEnhancementFlags}, execute, style::Print, terminal::{LeaveAlternateScreen, SetTitle, disable_raw_mode, enable_raw_mode}}; use cursor::RestoreCursor; use ratatui::{CompletedFrame, Frame, Terminal, backend::CrosstermBackend, buffer::Buffer, layout::Rect}; use yazi_adapter::{Emulator, Mux}; -use yazi_config::{INPUT, MGR}; +use yazi_config::MGR; use yazi_shared::{SyncCell, tty::{TTY, TtyWriter}}; static CSI_U: AtomicBool = AtomicBool::new(false); @@ -141,36 +141,6 @@ impl Term { pub(super) fn can_partial(&mut self) -> bool { self.inner.autoresize().is_ok() && self.last_area == self.inner.get_frame().area() } - - #[inline] - pub(super) fn set_cursor_block() -> Result<()> { - use crossterm::cursor::SetCursorStyle; - Ok(if INPUT.cursor_blink { - queue!(TTY.writer(), SetCursorStyle::BlinkingBlock)? - } else { - queue!(TTY.writer(), SetCursorStyle::SteadyBlock)? - }) - } - - #[inline] - pub(super) fn set_cursor_bar() -> Result<()> { - use crossterm::cursor::SetCursorStyle; - Ok(if INPUT.cursor_blink { - queue!(TTY.writer(), SetCursorStyle::BlinkingBar)? - } else { - queue!(TTY.writer(), SetCursorStyle::SteadyBar)? - }) - } - - #[inline] - pub(super) fn set_cursor_underscore() -> Result<()> { - use crossterm::cursor::SetCursorStyle; - Ok(if INPUT.cursor_blink { - queue!(TTY.writer(), SetCursorStyle::BlinkingUnderScore)? - } else { - queue!(TTY.writer(), SetCursorStyle::SteadyUnderScore)? - }) - } } impl Drop for Term { diff --git a/yazi-plugin/Cargo.toml b/yazi-plugin/Cargo.toml index 135d97f2..25eee456 100644 --- a/yazi-plugin/Cargo.toml +++ b/yazi-plugin/Cargo.toml @@ -34,7 +34,7 @@ mlua = { workspace = true } parking_lot = { workspace = true } ratatui = { workspace = true } serde_json = { workspace = true } -syntect = { version = "5.2.0", default-features = false, features = [ "parsing", "plist-load", "regex-onig" ] } +syntect = { workspace = true } tokio = { workspace = true } tokio-stream = { workspace = true } tokio-util = { workspace = true } diff --git a/yazi-proxy/src/cmp.rs b/yazi-proxy/src/cmp.rs index cb02e11c..f33a9b53 100644 --- a/yazi-proxy/src/cmp.rs +++ b/yazi-proxy/src/cmp.rs @@ -1,5 +1,5 @@ use yazi_macro::emit; -use yazi_shared::event::Cmd; +use yazi_shared::{Id, event::Cmd}; pub struct CmpProxy; @@ -10,7 +10,7 @@ impl CmpProxy { } #[inline] - pub fn trigger(word: &str, ticket: usize) { + pub fn trigger(word: &str, ticket: Id) { emit!(Call(Cmd::args("cmp:trigger", &[word]).with("ticket", ticket))); } } diff --git a/yazi-shared/src/errors/input.rs b/yazi-shared/src/errors/input.rs index 9c0c1c6f..93b53fe0 100644 --- a/yazi-shared/src/errors/input.rs +++ b/yazi-shared/src/errors/input.rs @@ -1,9 +1,11 @@ use std::{error::Error, fmt::{self, Display}}; +use crate::Id; + #[derive(Debug)] pub enum InputError { Typed(String), - Completed(String, usize), + Completed(String, Id), Canceled(String), } diff --git a/yazi-widgets/Cargo.toml b/yazi-widgets/Cargo.toml new file mode 100644 index 00000000..806ad867 --- /dev/null +++ b/yazi-widgets/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "yazi-widgets" +version = "25.3.7" +edition = "2021" +license = "MIT" +authors = [ "sxyazi " ] +description = "Yazi user interface widgets" +homepage = "https://yazi-rs.github.io" +repository = "https://github.com/sxyazi/yazi" +rust-version = "1.83.0" + +[dependencies] +yazi-codegen = { path = "../yazi-codegen", version = "25.3.7" } +yazi-config = { path = "../yazi-config", version = "25.3.7" } +yazi-macro = { path = "../yazi-macro", version = "25.3.7" } +yazi-plugin = { path = "../yazi-plugin", version = "25.3.7" } +yazi-shared = { path = "../yazi-shared", version = "25.3.7" } + +# External dependencies +futures = { workspace = true } +ratatui = { workspace = true } +unicode-width = { workspace = true } diff --git a/yazi-core/src/input/commands/backspace.rs b/yazi-widgets/src/input/commands/backspace.rs similarity index 100% rename from yazi-core/src/input/commands/backspace.rs rename to yazi-widgets/src/input/commands/backspace.rs diff --git a/yazi-core/src/input/commands/backward.rs b/yazi-widgets/src/input/commands/backward.rs similarity index 100% rename from yazi-core/src/input/commands/backward.rs rename to yazi-widgets/src/input/commands/backward.rs diff --git a/yazi-widgets/src/input/commands/commands.rs b/yazi-widgets/src/input/commands/commands.rs new file mode 100644 index 00000000..7ab08e12 --- /dev/null +++ b/yazi-widgets/src/input/commands/commands.rs @@ -0,0 +1,48 @@ +use yazi_shared::event::CmdCow; + +use crate::input::{Input, InputMode}; + +impl Input { + pub fn execute(&mut self, cmd: CmdCow) { + macro_rules! on { + ($name:ident) => { + if cmd.name == stringify!($name) { + return self.$name(cmd); + } + }; + ($name:ident, $alias:literal) => { + if cmd.name == $alias { + return self.$name(cmd); + } + }; + } + + on!(move_, "move"); + on!(backward); + on!(forward); + + match self.mode() { + InputMode::Normal => { + on!(insert); + on!(visual); + on!(replace); + + on!(delete); + on!(yank); + on!(paste); + + on!(undo); + on!(redo); + } + InputMode::Insert => { + on!(visual); + + on!(backspace); + on!(kill); + + on!(complete); + } + InputMode::Replace => {} + } + } +} diff --git a/yazi-core/src/input/commands/complete.rs b/yazi-widgets/src/input/commands/complete.rs similarity index 78% rename from yazi-core/src/input/commands/complete.rs rename to yazi-widgets/src/input/commands/complete.rs index 065557ab..241a3e03 100644 --- a/yazi-core/src/input/commands/complete.rs +++ b/yazi-widgets/src/input/commands/complete.rs @@ -12,15 +12,15 @@ const SEPARATOR: [char; 2] = ['/', '\\']; const SEPARATOR: char = std::path::MAIN_SEPARATOR; struct Opt { - word: Cow<'static, str>, - ticket: usize, + word: Cow<'static, str>, + _ticket: usize, // FIXME: not used } impl From for Opt { fn from(mut c: CmdCow) -> Self { Self { - word: c.take_first_str().unwrap_or_default(), - ticket: c.get("ticket").and_then(Data::as_usize).unwrap_or(0), + word: c.take_first_str().unwrap_or_default(), + _ticket: c.get("ticket").and_then(Data::as_usize).unwrap_or(0), } } } @@ -28,11 +28,7 @@ impl From for Opt { impl Input { #[yazi_codegen::command] pub fn complete(&mut self, opt: Opt) { - if self.ticket != opt.ticket { - return; - } - - let [before, after] = self.partition(); + let (before, after) = self.partition(); let new = if let Some((prefix, _)) = before.rsplit_once(SEPARATOR) { format!("{prefix}/{}{after}", opt.word).replace(SEPARATOR, MAIN_SEPARATOR_STR) } else { diff --git a/yazi-core/src/input/commands/delete.rs b/yazi-widgets/src/input/commands/delete.rs similarity index 100% rename from yazi-core/src/input/commands/delete.rs rename to yazi-widgets/src/input/commands/delete.rs diff --git a/yazi-widgets/src/input/commands/escape.rs b/yazi-widgets/src/input/commands/escape.rs new file mode 100644 index 00000000..9074b8b1 --- /dev/null +++ b/yazi-widgets/src/input/commands/escape.rs @@ -0,0 +1,27 @@ +use crate::input::{Input, InputMode, op::InputOp}; + +struct Opt; + +impl From<()> for Opt { + fn from(_: ()) -> Self { Self } +} + +impl Input { + #[yazi_codegen::command] + pub fn escape(&mut self, _: Opt) { + let snap = self.snap_mut(); + match snap.mode { + InputMode::Normal => { + snap.op = InputOp::None; + } + InputMode::Insert => { + snap.mode = InputMode::Normal; + self.move_(-1); + } + InputMode::Replace => { + snap.mode = InputMode::Normal; + } + } + self.snaps.tag(self.limit); + } +} diff --git a/yazi-core/src/input/commands/forward.rs b/yazi-widgets/src/input/commands/forward.rs similarity index 100% rename from yazi-core/src/input/commands/forward.rs rename to yazi-widgets/src/input/commands/forward.rs diff --git a/yazi-core/src/input/commands/insert.rs b/yazi-widgets/src/input/commands/insert.rs similarity index 100% rename from yazi-core/src/input/commands/insert.rs rename to yazi-widgets/src/input/commands/insert.rs diff --git a/yazi-core/src/input/commands/kill.rs b/yazi-widgets/src/input/commands/kill.rs similarity index 100% rename from yazi-core/src/input/commands/kill.rs rename to yazi-widgets/src/input/commands/kill.rs diff --git a/yazi-widgets/src/input/commands/mod.rs b/yazi-widgets/src/input/commands/mod.rs new file mode 100644 index 00000000..3fb65236 --- /dev/null +++ b/yazi-widgets/src/input/commands/mod.rs @@ -0,0 +1 @@ +yazi_macro::mod_flat!(backspace backward commands complete delete escape forward insert kill move_ paste redo replace type_ undo visual yank); diff --git a/yazi-core/src/input/commands/move_.rs b/yazi-widgets/src/input/commands/move_.rs similarity index 97% rename from yazi-core/src/input/commands/move_.rs rename to yazi-widgets/src/input/commands/move_.rs index 9edc5384..64ff0db2 100644 --- a/yazi-core/src/input/commands/move_.rs +++ b/yazi-widgets/src/input/commands/move_.rs @@ -33,7 +33,7 @@ impl Input { render!(self.handle_op(opt.step.cursor(snap), false)); - let (limit, snap) = (self.limit(), self.snap_mut()); + let (limit, snap) = (self.limit, self.snap_mut()); if snap.offset > snap.cursor { snap.offset = snap.cursor; } else if snap.value.is_empty() { diff --git a/yazi-core/src/input/commands/paste.rs b/yazi-widgets/src/input/commands/paste.rs similarity index 100% rename from yazi-core/src/input/commands/paste.rs rename to yazi-widgets/src/input/commands/paste.rs diff --git a/yazi-core/src/input/commands/redo.rs b/yazi-widgets/src/input/commands/redo.rs similarity index 100% rename from yazi-core/src/input/commands/redo.rs rename to yazi-widgets/src/input/commands/redo.rs diff --git a/yazi-core/src/input/commands/replace.rs b/yazi-widgets/src/input/commands/replace.rs similarity index 92% rename from yazi-core/src/input/commands/replace.rs rename to yazi-widgets/src/input/commands/replace.rs index f028ebb2..ab7046f3 100644 --- a/yazi-core/src/input/commands/replace.rs +++ b/yazi-widgets/src/input/commands/replace.rs @@ -27,6 +27,6 @@ impl Input { } render!(); - self.snaps.tag(self.limit()).then(|| self.flush_value()); + self.snaps.tag(self.limit).then(|| self.flush_value()); } } diff --git a/yazi-core/src/input/commands/type_.rs b/yazi-widgets/src/input/commands/type_.rs similarity index 100% rename from yazi-core/src/input/commands/type_.rs rename to yazi-widgets/src/input/commands/type_.rs diff --git a/yazi-core/src/input/commands/undo.rs b/yazi-widgets/src/input/commands/undo.rs similarity index 100% rename from yazi-core/src/input/commands/undo.rs rename to yazi-widgets/src/input/commands/undo.rs diff --git a/yazi-core/src/input/commands/visual.rs b/yazi-widgets/src/input/commands/visual.rs similarity index 100% rename from yazi-core/src/input/commands/visual.rs rename to yazi-widgets/src/input/commands/visual.rs diff --git a/yazi-core/src/input/commands/yank.rs b/yazi-widgets/src/input/commands/yank.rs similarity index 100% rename from yazi-core/src/input/commands/yank.rs rename to yazi-widgets/src/input/commands/yank.rs diff --git a/yazi-widgets/src/input/input.rs b/yazi-widgets/src/input/input.rs new file mode 100644 index 00000000..0fb0834b --- /dev/null +++ b/yazi-widgets/src/input/input.rs @@ -0,0 +1,129 @@ +use std::ops::Range; + +use ratatui::crossterm::cursor::SetCursorStyle; +use unicode_width::UnicodeWidthStr; +use yazi_config::INPUT; +use yazi_plugin::CLIPBOARD; + +use super::{InputSnap, InputSnaps, mode::InputMode, op::InputOp}; + +pub type InputCallback = Box; + +#[derive(Default)] +pub struct Input { + pub snaps: InputSnaps, + pub limit: usize, + pub callback: Option, +} + +impl Input { + pub fn new(value: String, limit: usize, callback: InputCallback) -> Self { + Self { snaps: InputSnaps::new(value, limit), limit, callback: Some(callback) } + } + + pub(super) fn handle_op(&mut self, cursor: usize, include: bool) -> bool { + let old = self.snap().clone(); + let snap = self.snap_mut(); + + match snap.op { + InputOp::None | InputOp::Select(_) => { + snap.cursor = cursor; + } + InputOp::Delete(cut, insert, _) => { + let range = snap.op.range(cursor, include).unwrap(); + let Range { start, end } = snap.idx(range.start)..snap.idx(range.end); + + let drain = snap.value.drain(start.unwrap()..end.unwrap()).collect::(); + if cut { + futures::executor::block_on(CLIPBOARD.set(&drain)); + } + + snap.op = InputOp::None; + snap.mode = if insert { InputMode::Insert } else { InputMode::Normal }; + snap.cursor = range.start; + } + InputOp::Yank(_) => { + let range = snap.op.range(cursor, include).unwrap(); + let Range { start, end } = snap.idx(range.start)..snap.idx(range.end); + let yanked = &snap.value[start.unwrap()..end.unwrap()]; + + snap.op = InputOp::None; + futures::executor::block_on(CLIPBOARD.set(yanked)); + } + }; + + snap.cursor = snap.count().saturating_sub(snap.mode.delta()).min(snap.cursor); + if snap == &old { + return false; + } + if !matches!(old.op, InputOp::None | InputOp::Select(_)) { + self.snaps.tag(self.limit).then(|| self.flush_value()); + } + true + } + + pub(super) fn flush_value(&mut self) { + if let Some(cb) = &self.callback { + let (before, after) = self.partition(); + cb(before, after); + } + } +} + +impl Input { + #[inline] + pub fn value(&self) -> &str { &self.snap().value } + + #[inline] + pub fn visible_value(&self) -> &str { self.snap().slice(self.snap().window(self.limit)) } + + #[inline] + pub fn mode(&self) -> InputMode { self.snap().mode } + + #[inline] + pub fn cursor(&self) -> u16 { + let snap = self.snap(); + snap.slice(snap.offset..snap.cursor).width() as u16 + } + + pub fn cursor_shape(&self) -> SetCursorStyle { + use InputMode as M; + + match self.mode() { + M::Normal if INPUT.cursor_blink => SetCursorStyle::BlinkingBlock, + M::Normal if !INPUT.cursor_blink => SetCursorStyle::SteadyBlock, + M::Insert if INPUT.cursor_blink => SetCursorStyle::BlinkingBar, + M::Insert if !INPUT.cursor_blink => SetCursorStyle::SteadyBar, + M::Replace if INPUT.cursor_blink => SetCursorStyle::BlinkingUnderScore, + M::Replace if !INPUT.cursor_blink => SetCursorStyle::SteadyUnderScore, + M::Normal | M::Insert | M::Replace => unreachable!(), + } + } + + pub fn selected(&self) -> Option> { + let snap = self.snap(); + let start = snap.op.start()?; + + let (start, end) = + if start < snap.cursor { (start, snap.cursor) } else { (snap.cursor + 1, start + 1) }; + + let win = snap.window(self.limit); + let Range { start, end } = start.max(win.start)..end.min(win.end); + + let s = snap.slice(snap.offset..start).width() as u16; + Some(s..s + snap.slice(start..end).width() as u16) + } + + #[inline] + pub fn partition(&self) -> (&str, &str) { + let snap = self.snap(); + let idx = snap.idx(snap.cursor).unwrap(); + (&snap.value[..idx], &snap.value[idx..]) + } + + #[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 new file mode 100644 index 00000000..1e6e60b0 --- /dev/null +++ b/yazi-widgets/src/input/mod.rs @@ -0,0 +1,3 @@ +yazi_macro::mod_pub!(commands); + +yazi_macro::mod_flat!(input mode op snap snaps widget); diff --git a/yazi-core/src/input/mode.rs b/yazi-widgets/src/input/mode.rs similarity index 100% rename from yazi-core/src/input/mode.rs rename to yazi-widgets/src/input/mode.rs diff --git a/yazi-core/src/input/op.rs b/yazi-widgets/src/input/op.rs similarity index 100% rename from yazi-core/src/input/op.rs rename to yazi-widgets/src/input/op.rs diff --git a/yazi-core/src/input/snap.rs b/yazi-widgets/src/input/snap.rs similarity index 90% rename from yazi-core/src/input/snap.rs rename to yazi-widgets/src/input/snap.rs index e2931220..6b0d09e2 100644 --- a/yazi-core/src/input/snap.rs +++ b/yazi-widgets/src/input/snap.rs @@ -5,14 +5,14 @@ use unicode_width::UnicodeWidthChar; use super::{InputMode, InputOp}; #[derive(Clone, Debug, Default, PartialEq, Eq)] -pub(super) struct InputSnap { - pub(super) value: String, +pub struct InputSnap { + pub value: String, - pub(super) op: InputOp, + pub op: InputOp, - pub(super) mode: InputMode, - pub(super) offset: usize, - pub(super) cursor: usize, + pub mode: InputMode, + pub offset: usize, + pub cursor: usize, } impl InputSnap { @@ -22,7 +22,7 @@ impl InputSnap { op: Default::default(), - mode: Default::default(), + mode: Default::default(), offset: usize::MAX, cursor: usize::MAX, }; diff --git a/yazi-core/src/input/snaps.rs b/yazi-widgets/src/input/snaps.rs similarity index 74% rename from yazi-core/src/input/snaps.rs rename to yazi-widgets/src/input/snaps.rs index b1960bcf..c0d6b1de 100644 --- a/yazi-core/src/input/snaps.rs +++ b/yazi-widgets/src/input/snaps.rs @@ -2,20 +2,27 @@ use std::mem; use super::InputSnap; -#[derive(Default, PartialEq, Eq)] -pub(super) struct InputSnaps { +#[derive(PartialEq, Eq)] +pub struct InputSnaps { idx: usize, versions: Vec, current: InputSnap, } +impl Default for InputSnaps { + fn default() -> Self { + Self { + idx: 0, + versions: vec![InputSnap::new(String::new(), 0)], + current: InputSnap::new(String::new(), 0), + } + } +} + impl InputSnaps { - #[inline] - pub(super) fn reset(&mut self, value: String, limit: usize) { - self.idx = 0; - self.versions.clear(); - self.versions.push(InputSnap::new(value, limit)); - self.current = self.versions[0].clone(); + pub fn new(value: String, limit: usize) -> Self { + let current = InputSnap::new(value, limit); + Self { idx: 0, versions: vec![current.clone()], current } } pub(super) fn tag(&mut self, limit: usize) -> bool { @@ -65,7 +72,7 @@ impl InputSnaps { impl InputSnaps { #[inline] - pub(super) fn current(&self) -> &InputSnap { &self.current } + pub fn current(&self) -> &InputSnap { &self.current } #[inline] pub(super) fn current_mut(&mut self) -> &mut InputSnap { &mut self.current } diff --git a/yazi-widgets/src/input/widget.rs b/yazi-widgets/src/input/widget.rs new file mode 100644 index 00000000..10e5ba91 --- /dev/null +++ b/yazi-widgets/src/input/widget.rs @@ -0,0 +1,30 @@ +use std::ops::Range; + +use ratatui::{layout::Rect, text::Line, widgets::Widget}; +use yazi_config::THEME; + +use super::Input; + +impl Widget for &Input { + fn render(self, area: ratatui::layout::Rect, buf: &mut ratatui::buffer::Buffer) + where + Self: Sized, + { + yazi_plugin::elements::Clear::default().render(area, buf); + + Line::styled(self.visible_value(), THEME.input.value).render(area, buf); + + if let Some(Range { start, end }) = self.selected() { + let s = start.min(area.width); + buf.set_style( + Rect { + x: area.x + s, + y: area.y, + width: (end - start).min(area.width - s), + height: area.height.min(1), + }, + THEME.input.selected, + ) + } + } +} diff --git a/yazi-widgets/src/lib.rs b/yazi-widgets/src/lib.rs new file mode 100644 index 00000000..6ce2b0ad --- /dev/null +++ b/yazi-widgets/src/lib.rs @@ -0,0 +1 @@ +yazi_macro::mod_pub!(input);