mirror of
https://github.com/sxyazi/yazi.git
synced 2026-07-25 08:41:05 +00:00
refactor: new yazi-widgets crate (#2498)
This commit is contained in:
parent
7632163678
commit
570c6d308b
50 changed files with 450 additions and 354 deletions
37
Cargo.lock
generated
37
Cargo.lock
generated
|
|
@ -620,9 +620,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "deranged"
|
name = "deranged"
|
||||||
version = "0.3.11"
|
version = "0.4.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4"
|
checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"powerfmt",
|
"powerfmt",
|
||||||
]
|
]
|
||||||
|
|
@ -2589,9 +2589,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "time"
|
name = "time"
|
||||||
version = "0.3.39"
|
version = "0.3.40"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "dad298b01a40a23aac4580b67e3dbedb7cc8402f3592d7f49469de2ea4aecdd8"
|
checksum = "9d9c75b47bdff86fa3334a3db91356b8d7d86a9b839dab7d0bdc5c3d3a077618"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"deranged",
|
"deranged",
|
||||||
"itoa",
|
"itoa",
|
||||||
|
|
@ -2606,15 +2606,15 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "time-core"
|
name = "time-core"
|
||||||
version = "0.1.3"
|
version = "0.1.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "765c97a5b985b7c11d7bc27fa927dc4fe6af3a6dfb021d28deb60d3bf51e76ef"
|
checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "time-macros"
|
name = "time-macros"
|
||||||
version = "0.2.20"
|
version = "0.2.21"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e8093bc3e81c3bc5f7879de09619d06c9a5a5e45ca44dfeeb7225bae38005c5c"
|
checksum = "29aa485584182073ed57fd5004aa09c371f021325014694e432313345865fd04"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"num-conv",
|
"num-conv",
|
||||||
"time-core",
|
"time-core",
|
||||||
|
|
@ -3191,9 +3191,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-link"
|
name = "windows-link"
|
||||||
version = "0.1.0"
|
version = "0.1.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6dccfd733ce2b1753b03b6d3c65edf020262ea35e20ccdf3e288043e6dd620e3"
|
checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-result"
|
name = "windows-result"
|
||||||
|
|
@ -3433,6 +3433,7 @@ dependencies = [
|
||||||
"yazi-proxy",
|
"yazi-proxy",
|
||||||
"yazi-scheduler",
|
"yazi-scheduler",
|
||||||
"yazi-shared",
|
"yazi-shared",
|
||||||
|
"yazi-widgets",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -3482,7 +3483,6 @@ dependencies = [
|
||||||
"ratatui",
|
"ratatui",
|
||||||
"scopeguard",
|
"scopeguard",
|
||||||
"signal-hook-tokio",
|
"signal-hook-tokio",
|
||||||
"syntect",
|
|
||||||
"textwrap",
|
"textwrap",
|
||||||
"tikv-jemallocator",
|
"tikv-jemallocator",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
|
@ -3501,6 +3501,7 @@ dependencies = [
|
||||||
"yazi-plugin",
|
"yazi-plugin",
|
||||||
"yazi-proxy",
|
"yazi-proxy",
|
||||||
"yazi-shared",
|
"yazi-shared",
|
||||||
|
"yazi-widgets",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -3630,6 +3631,20 @@ dependencies = [
|
||||||
"yazi-macro",
|
"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]]
|
[[package]]
|
||||||
name = "yoke"
|
name = "yoke"
|
||||||
version = "0.7.5"
|
version = "0.7.5"
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,7 @@ regex = "1.11.1"
|
||||||
scopeguard = "1.2.0"
|
scopeguard = "1.2.0"
|
||||||
serde = { version = "1.0.219", features = [ "derive" ] }
|
serde = { version = "1.0.219", features = [ "derive" ] }
|
||||||
serde_json = "1.0.140"
|
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 = { version = "1.44.1", features = [ "full" ] }
|
||||||
tokio-stream = "0.1.17"
|
tokio-stream = "0.1.17"
|
||||||
tokio-util = "0.7.14"
|
tokio-util = "0.7.14"
|
||||||
|
|
|
||||||
|
|
@ -46,7 +46,7 @@ pub struct Input {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Input {
|
impl Input {
|
||||||
pub const fn border(&self) -> u16 { 2 }
|
pub const fn border(&self) -> u16 { 1 }
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FromStr for Input {
|
impl FromStr for Input {
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ yazi-plugin = { path = "../yazi-plugin", version = "25.3.7" }
|
||||||
yazi-proxy = { path = "../yazi-proxy", version = "25.3.7" }
|
yazi-proxy = { path = "../yazi-proxy", version = "25.3.7" }
|
||||||
yazi-scheduler = { path = "../yazi-scheduler", version = "25.3.7" }
|
yazi-scheduler = { path = "../yazi-scheduler", version = "25.3.7" }
|
||||||
yazi-shared = { path = "../yazi-shared", version = "25.3.7" }
|
yazi-shared = { path = "../yazi-shared", version = "25.3.7" }
|
||||||
|
yazi-widgets = { path = "../yazi-widgets", version = "25.3.7" }
|
||||||
|
|
||||||
# External dependencies
|
# External dependencies
|
||||||
anyhow = { workspace = true }
|
anyhow = { workspace = true }
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
use std::mem;
|
||||||
|
|
||||||
use yazi_macro::render;
|
use yazi_macro::render;
|
||||||
use yazi_proxy::InputProxy;
|
use yazi_proxy::InputProxy;
|
||||||
use yazi_shared::event::CmdCow;
|
use yazi_shared::event::CmdCow;
|
||||||
|
|
@ -23,7 +25,6 @@ impl Cmp {
|
||||||
}
|
}
|
||||||
|
|
||||||
self.caches.clear();
|
self.caches.clear();
|
||||||
self.visible = false;
|
render!(mem::replace(&mut self.visible, false));
|
||||||
render!();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,11 @@
|
||||||
use yazi_config::popup::{Offset, Origin, Position};
|
|
||||||
use yazi_macro::render;
|
use yazi_macro::render;
|
||||||
use yazi_shared::event::CmdCow;
|
use yazi_shared::event::CmdCow;
|
||||||
|
|
||||||
use crate::{help::Help, input::Input};
|
use crate::help::Help;
|
||||||
|
|
||||||
impl Help {
|
impl Help {
|
||||||
pub fn filter(&mut self, _: CmdCow) {
|
pub fn filter(&mut self, _: CmdCow) {
|
||||||
let mut input = Input::default();
|
self.in_filter = Some(Default::default());
|
||||||
input.position = Position::new(Origin::BottomLeft, Offset::line());
|
|
||||||
|
|
||||||
self.in_filter = Some(input);
|
|
||||||
self.filter_apply();
|
self.filter_apply();
|
||||||
render!();
|
render!();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,11 @@
|
||||||
use crossterm::event::KeyCode;
|
use crossterm::{cursor::SetCursorStyle, event::KeyCode};
|
||||||
use unicode_width::UnicodeWidthStr;
|
use unicode_width::UnicodeWidthStr;
|
||||||
use yazi_adapter::Dimension;
|
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_macro::{render, render_and};
|
||||||
use yazi_shared::Layer;
|
use yazi_shared::Layer;
|
||||||
|
|
||||||
use super::HELP_MARGIN;
|
use super::HELP_MARGIN;
|
||||||
use crate::input::Input;
|
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct Help {
|
pub struct Help {
|
||||||
|
|
@ -16,7 +15,7 @@ pub struct Help {
|
||||||
|
|
||||||
// Filter
|
// Filter
|
||||||
pub(super) keyword: String,
|
pub(super) keyword: String,
|
||||||
pub(super) in_filter: Option<Input>,
|
pub(super) in_filter: Option<yazi_widgets::input::Input>,
|
||||||
|
|
||||||
pub(super) offset: usize,
|
pub(super) offset: usize,
|
||||||
pub(super) cursor: usize,
|
pub(super) cursor: usize,
|
||||||
|
|
@ -89,7 +88,7 @@ impl Help {
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map(|i| i.value())
|
.map(|i| i.value())
|
||||||
.or(Some(self.keyword.as_str()).filter(|&s| !s.is_empty()))
|
.or(Some(self.keyword.as_str()).filter(|&s| !s.is_empty()))
|
||||||
.map(|s| format!("Filter: {}", s))
|
.map(|s| format!("Filter: {s}"))
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Bindings
|
// --- Bindings
|
||||||
|
|
@ -113,4 +112,9 @@ impl Help {
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn rel_cursor(&self) -> usize { self.cursor - self.offset }
|
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 }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,17 +18,15 @@ impl From<bool> for Opt {
|
||||||
impl Input {
|
impl Input {
|
||||||
#[yazi_codegen::command]
|
#[yazi_codegen::command]
|
||||||
pub fn close(&mut self, opt: Opt) {
|
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.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!();
|
render!();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,9 @@
|
||||||
use yazi_macro::render;
|
use yazi_macro::render;
|
||||||
use yazi_proxy::CmpProxy;
|
use yazi_proxy::CmpProxy;
|
||||||
use yazi_shared::event::CmdCow;
|
use yazi_shared::event::CmdCow;
|
||||||
|
use yazi_widgets::input::InputOp;
|
||||||
|
|
||||||
use crate::input::{Input, InputMode, op::InputOp};
|
use crate::input::Input;
|
||||||
|
|
||||||
struct Opt;
|
struct Opt;
|
||||||
|
|
||||||
|
|
@ -16,28 +17,16 @@ impl From<()> for Opt {
|
||||||
impl Input {
|
impl Input {
|
||||||
#[yazi_codegen::command]
|
#[yazi_codegen::command]
|
||||||
pub fn escape(&mut self, _: Opt) {
|
pub fn escape(&mut self, _: Opt) {
|
||||||
let snap = self.snap_mut();
|
use yazi_widgets::input::InputMode as M;
|
||||||
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);
|
|
||||||
|
|
||||||
if self.completion {
|
let mode = self.snap().mode;
|
||||||
CmpProxy::close();
|
match mode {
|
||||||
}
|
M::Normal if self.snap_mut().op == InputOp::None => self.close(false),
|
||||||
}
|
M::Insert => CmpProxy::close(),
|
||||||
InputMode::Replace => {
|
M::Normal | M::Replace => {}
|
||||||
snap.mode = InputMode::Normal;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
self.snaps.tag(self.limit());
|
self.inner.escape(());
|
||||||
render!();
|
render!();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
use tokio::sync::mpsc;
|
use tokio::sync::mpsc;
|
||||||
use yazi_config::popup::InputCfg;
|
use yazi_config::{INPUT, popup::InputCfg};
|
||||||
use yazi_macro::render;
|
use yazi_macro::render;
|
||||||
use yazi_shared::{errors::InputError, event::CmdCow};
|
use yazi_shared::{errors::InputError, event::CmdCow};
|
||||||
|
use yazi_widgets::input::InputCallback;
|
||||||
|
|
||||||
use crate::input::Input;
|
use crate::input::Input;
|
||||||
|
|
||||||
|
|
@ -20,7 +21,7 @@ impl TryFrom<CmdCow> for Opt {
|
||||||
|
|
||||||
impl Input {
|
impl Input {
|
||||||
pub fn show(&mut self, opt: impl TryInto<Opt>) {
|
pub fn show(&mut self, opt: impl TryInto<Opt>) {
|
||||||
let Ok(opt) = opt.try_into() else { return };
|
let Ok(opt): Result<Opt, _> = opt.try_into() else { return };
|
||||||
|
|
||||||
self.close(false);
|
self.close(false);
|
||||||
self.visible = true;
|
self.visible = true;
|
||||||
|
|
@ -28,17 +29,28 @@ impl Input {
|
||||||
self.position = opt.cfg.position;
|
self.position = opt.cfg.position;
|
||||||
|
|
||||||
// Typing
|
// Typing
|
||||||
self.callback = Some(opt.tx);
|
self.tx = Some(opt.tx.clone());
|
||||||
self.realtime = opt.cfg.realtime;
|
let ticket = self.ticket.clone();
|
||||||
self.completion = opt.cfg.completion;
|
|
||||||
|
|
||||||
// Shell
|
// Shell
|
||||||
self.highlight = opt.cfg.highlight;
|
self.highlight = opt.cfg.highlight;
|
||||||
|
|
||||||
// Reset snaps
|
// Reset input
|
||||||
self.snaps.reset(opt.cfg.value, self.limit());
|
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
|
// Set cursor after reset
|
||||||
|
// TODO: remove this
|
||||||
if let Some(cursor) = opt.cfg.cursor {
|
if let Some(cursor) = opt.cfg.cursor {
|
||||||
self.snap_mut().cursor = cursor;
|
self.snap_mut().cursor = cursor;
|
||||||
self.move_(0);
|
self.move_(0);
|
||||||
|
|
|
||||||
|
|
@ -1,131 +1,31 @@
|
||||||
use std::ops::Range;
|
use std::{ops::{Deref, DerefMut}, rc::Rc};
|
||||||
|
|
||||||
use tokio::sync::mpsc::UnboundedSender;
|
use tokio::sync::mpsc::UnboundedSender;
|
||||||
use unicode_width::UnicodeWidthStr;
|
use yazi_config::popup::Position;
|
||||||
use yazi_config::{INPUT, popup::Position};
|
use yazi_shared::{Ids, errors::InputError};
|
||||||
use yazi_plugin::CLIPBOARD;
|
|
||||||
use yazi_shared::errors::InputError;
|
|
||||||
|
|
||||||
use super::{InputSnap, InputSnaps, mode::InputMode, op::InputOp};
|
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct Input {
|
pub struct Input {
|
||||||
pub(super) snaps: InputSnaps,
|
pub(super) inner: yazi_widgets::input::Input,
|
||||||
pub ticket: usize,
|
|
||||||
pub visible: bool,
|
|
||||||
|
|
||||||
|
pub visible: bool,
|
||||||
pub title: String,
|
pub title: String,
|
||||||
pub position: Position,
|
pub position: Position,
|
||||||
|
|
||||||
// Typing
|
// Typing
|
||||||
pub(super) callback: Option<UnboundedSender<Result<String, InputError>>>,
|
pub(super) tx: Option<UnboundedSender<Result<String, InputError>>>,
|
||||||
pub(super) realtime: bool,
|
pub(super) ticket: Rc<Ids>,
|
||||||
pub(super) completion: bool,
|
|
||||||
|
|
||||||
// Shell
|
// Shell
|
||||||
pub highlight: bool,
|
pub highlight: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Input {
|
impl Deref for Input {
|
||||||
#[inline]
|
type Target = yazi_widgets::input::Input;
|
||||||
pub(super) fn limit(&self) -> usize {
|
|
||||||
self.position.offset.width.saturating_sub(INPUT.border()) as usize
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(super) fn handle_op(&mut self, cursor: usize, include: bool) -> bool {
|
fn deref(&self) -> &Self::Target { &self.inner }
|
||||||
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::<String>();
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Input {
|
impl DerefMut for Input {
|
||||||
#[inline]
|
fn deref_mut(&mut self) -> &mut Self::Target { &mut self.inner }
|
||||||
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<Range<u16>> {
|
|
||||||
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() }
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
yazi_macro::mod_pub!(commands);
|
yazi_macro::mod_pub!(commands);
|
||||||
|
|
||||||
yazi_macro::mod_flat!(input mode op snap snaps);
|
yazi_macro::mod_flat!(input);
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ yazi-macro = { path = "../yazi-macro", version = "25.3.7" }
|
||||||
yazi-plugin = { path = "../yazi-plugin", version = "25.3.7" }
|
yazi-plugin = { path = "../yazi-plugin", version = "25.3.7" }
|
||||||
yazi-proxy = { path = "../yazi-proxy", version = "25.3.7" }
|
yazi-proxy = { path = "../yazi-proxy", version = "25.3.7" }
|
||||||
yazi-shared = { path = "../yazi-shared", version = "25.3.7" }
|
yazi-shared = { path = "../yazi-shared", version = "25.3.7" }
|
||||||
|
yazi-widgets = { path = "../yazi-widgets", version = "25.3.7" }
|
||||||
|
|
||||||
# External dependencies
|
# External dependencies
|
||||||
anyhow = { workspace = true }
|
anyhow = { workspace = true }
|
||||||
|
|
@ -35,7 +36,6 @@ indexmap = { workspace = true }
|
||||||
mlua = { workspace = true }
|
mlua = { workspace = true }
|
||||||
ratatui = { workspace = true }
|
ratatui = { workspace = true }
|
||||||
scopeguard = { workspace = true }
|
scopeguard = { workspace = true }
|
||||||
syntect = { version = "5.2.0", default-features = false, features = [ "parsing", "plist-load", "regex-onig" ] }
|
|
||||||
tokio = { workspace = true }
|
tokio = { workspace = true }
|
||||||
tokio-stream = { workspace = true }
|
tokio-stream = { workspace = true }
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,9 +3,9 @@ use std::sync::atomic::Ordering;
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use crossterm::event::KeyEvent;
|
use crossterm::event::KeyEvent;
|
||||||
use yazi_config::keymap::Key;
|
use yazi_config::keymap::Key;
|
||||||
use yazi_core::input::InputMode;
|
|
||||||
use yazi_macro::emit;
|
use yazi_macro::emit;
|
||||||
use yazi_shared::event::{CmdCow, Event, NEED_RENDER};
|
use yazi_shared::event::{CmdCow, Event, NEED_RENDER};
|
||||||
|
use yazi_widgets::input::InputMode;
|
||||||
|
|
||||||
use crate::{Ctx, Executor, Router, Signals, Term, lives::Lives};
|
use crate::{Ctx, Executor, Router, Signals, Term, lives::Lives};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,7 @@
|
||||||
use std::sync::atomic::Ordering;
|
use std::sync::atomic::{AtomicU8, Ordering};
|
||||||
|
|
||||||
use crossterm::{execute, queue, terminal::{BeginSynchronizedUpdate, EndSynchronizedUpdate}};
|
use crossterm::{cursor::{MoveTo, SetCursorStyle, Show}, execute, queue, terminal::{BeginSynchronizedUpdate, EndSynchronizedUpdate}};
|
||||||
use ratatui::{CompletedFrame, backend::{Backend, CrosstermBackend}, buffer::Buffer};
|
use ratatui::{CompletedFrame, backend::{Backend, CrosstermBackend}, buffer::Buffer, layout::Position};
|
||||||
use scopeguard::defer;
|
|
||||||
use yazi_plugin::elements::COLLISION;
|
use yazi_plugin::elements::COLLISION;
|
||||||
use yazi_shared::{event::NEED_RENDER, tty::TTY};
|
use yazi_shared::{event::NEED_RENDER, tty::TTY};
|
||||||
|
|
||||||
|
|
@ -13,22 +12,18 @@ impl App {
|
||||||
NEED_RENDER.store(false, Ordering::Relaxed);
|
NEED_RENDER.store(false, Ordering::Relaxed);
|
||||||
let Some(term) = &mut self.term else { return };
|
let Some(term) = &mut self.term else { return };
|
||||||
|
|
||||||
queue!(TTY.writer(), BeginSynchronizedUpdate).ok();
|
Self::routine(true, None);
|
||||||
defer! { execute!(TTY.writer(), EndSynchronizedUpdate).ok(); }
|
let _guard = scopeguard::guard(self.cx.cursor(), |c| Self::routine(false, c));
|
||||||
|
|
||||||
let collision = COLLISION.swap(false, Ordering::Relaxed);
|
let collision = COLLISION.swap(false, Ordering::Relaxed);
|
||||||
let frame = term
|
let frame = term
|
||||||
.draw(|f| {
|
.draw(|f| {
|
||||||
_ = Lives::scope(&self.cx, || Ok(f.render_widget(Root::new(&self.cx), f.area())));
|
_ = 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();
|
.unwrap();
|
||||||
|
|
||||||
if COLLISION.load(Ordering::Relaxed) {
|
if COLLISION.load(Ordering::Relaxed) {
|
||||||
Self::patch(frame, self.cx.cursor());
|
Self::patch(frame);
|
||||||
}
|
}
|
||||||
if !self.cx.notify.messages.is_empty() {
|
if !self.cx.notify.messages.is_empty() {
|
||||||
self.render_partially();
|
self.render_partially();
|
||||||
|
|
@ -46,6 +41,9 @@ impl App {
|
||||||
return self.render();
|
return self.render();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Self::routine(true, None);
|
||||||
|
let _guard = scopeguard::guard(self.cx.cursor(), |c| Self::routine(false, c));
|
||||||
|
|
||||||
let frame = term
|
let frame = term
|
||||||
.draw_partial(|f| {
|
.draw_partial(|f| {
|
||||||
_ = Lives::scope(&self.cx, || {
|
_ = Lives::scope(&self.cx, || {
|
||||||
|
|
@ -53,20 +51,16 @@ impl App {
|
||||||
f.render_widget(crate::notify::Notify::new(&self.cx), f.area());
|
f.render_widget(crate::notify::Notify::new(&self.cx), f.area());
|
||||||
Ok(())
|
Ok(())
|
||||||
});
|
});
|
||||||
|
|
||||||
if let Some(pos) = self.cx.cursor() {
|
|
||||||
f.set_cursor_position(pos);
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
if COLLISION.load(Ordering::Relaxed) {
|
if COLLISION.load(Ordering::Relaxed) {
|
||||||
Self::patch(frame, self.cx.cursor());
|
Self::patch(frame);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
fn patch(frame: CompletedFrame, cursor: Option<(u16, u16)>) {
|
fn patch(frame: CompletedFrame) {
|
||||||
let mut new = Buffer::empty(frame.area);
|
let mut new = Buffer::empty(frame.area);
|
||||||
for y in new.area.top()..new.area.bottom() {
|
for y in new.area.top()..new.area.bottom() {
|
||||||
for x in new.area.left()..new.area.right() {
|
for x in new.area.left()..new.area.right() {
|
||||||
|
|
@ -79,14 +73,23 @@ impl App {
|
||||||
}
|
}
|
||||||
|
|
||||||
let patches = frame.buffer.diff(&new);
|
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);
|
fn routine(push: bool, cursor: Option<(Position, SetCursorStyle)>) {
|
||||||
backend.draw(patches.into_iter()).ok();
|
static COUNT: AtomicU8 = AtomicU8::new(0);
|
||||||
if let Some(pos) = cursor {
|
if push && COUNT.fetch_add(1, Ordering::Relaxed) != 0 {
|
||||||
backend.show_cursor().ok();
|
return;
|
||||||
backend.set_cursor_position(pos).ok();
|
} 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)
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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_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;
|
use yazi_shared::Layer;
|
||||||
|
|
||||||
|
|
@ -30,13 +31,16 @@ impl Ctx {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn cursor(&self) -> Option<(u16, u16)> {
|
pub fn cursor(&self) -> Option<(Position, SetCursorStyle)> {
|
||||||
if self.input.visible {
|
if self.input.visible {
|
||||||
let Rect { x, y, .. } = self.mgr.area(self.input.position);
|
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() {
|
if let Some((x, y)) = self.help.cursor() {
|
||||||
return Some((x, y));
|
return Some((Position { x, y }, self.help.cursor_shape()));
|
||||||
}
|
}
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
use yazi_core::input::InputMode;
|
|
||||||
use yazi_shared::{Layer, event::CmdCow};
|
use yazi_shared::{Layer, event::CmdCow};
|
||||||
|
use yazi_widgets::input::InputMode;
|
||||||
|
|
||||||
use crate::app::App;
|
use crate::app::App;
|
||||||
|
|
||||||
|
|
@ -233,57 +233,30 @@ impl<'a> Executor<'a> {
|
||||||
return self.app.cx.input.$name(cmd);
|
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!(show);
|
||||||
on!(close);
|
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() {
|
match self.app.cx.input.mode() {
|
||||||
InputMode::Normal => {
|
InputMode::Normal => {
|
||||||
on!(insert);
|
|
||||||
on!(visual);
|
|
||||||
on!(replace);
|
|
||||||
|
|
||||||
on!(delete);
|
|
||||||
on!(yank);
|
|
||||||
on!(paste);
|
|
||||||
|
|
||||||
on!(undo);
|
|
||||||
on!(redo);
|
|
||||||
|
|
||||||
match cmd.name.as_str() {
|
match cmd.name.as_str() {
|
||||||
// Help
|
// Help
|
||||||
"help" => self.app.cx.help.toggle(Layer::Input),
|
"help" => return self.app.cx.help.toggle(Layer::Input),
|
||||||
// Plugin
|
// Plugin
|
||||||
"plugin" => self.app.plugin(cmd),
|
"plugin" => return self.app.plugin(cmd),
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
InputMode::Insert => {
|
InputMode::Insert => match cmd.name.as_str() {
|
||||||
on!(visual);
|
"complete" if cmd.bool("trigger") => return self.app.cx.cmp.trigger(cmd),
|
||||||
|
_ => {}
|
||||||
on!(backspace);
|
},
|
||||||
on!(kill);
|
|
||||||
}
|
|
||||||
InputMode::Replace => {}
|
InputMode::Replace => {}
|
||||||
}
|
};
|
||||||
|
|
||||||
|
self.app.cx.input.execute(cmd)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn confirm(&mut self, cmd: CmdCow) {
|
fn confirm(&mut self, cmd: CmdCow) {
|
||||||
|
|
|
||||||
|
|
@ -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 crate::Ctx;
|
||||||
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};
|
|
||||||
|
|
||||||
pub(crate) struct Input<'a> {
|
pub(crate) struct Input<'a> {
|
||||||
cx: &'a Ctx,
|
cx: &'a Ctx,
|
||||||
|
|
@ -15,52 +9,21 @@ pub(crate) struct Input<'a> {
|
||||||
|
|
||||||
impl<'a> Input<'a> {
|
impl<'a> Input<'a> {
|
||||||
pub(crate) fn new(cx: &'a Ctx) -> Self { Self { cx } }
|
pub(crate) fn new(cx: &'a Ctx) -> Self { Self { cx } }
|
||||||
|
|
||||||
fn highlighted_value(&self) -> Result<Line<'static>> {
|
|
||||||
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<'_> {
|
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 input = &self.cx.input;
|
||||||
let area = self.cx.mgr.area(input.position);
|
let area = self.cx.mgr.area(input.position);
|
||||||
|
|
||||||
yazi_plugin::elements::Clear::default().render(area, buf);
|
yazi_plugin::elements::Clear::default().render(area, buf);
|
||||||
Paragraph::new(self.highlighted_value().unwrap_or_else(|_| Line::from(input.value())))
|
|
||||||
.block(
|
Block::bordered()
|
||||||
Block::bordered()
|
.border_type(BorderType::Rounded)
|
||||||
.border_type(BorderType::Rounded)
|
.border_style(THEME.input.border)
|
||||||
.border_style(THEME.input.border)
|
.title(Line::styled(&input.title, THEME.input.title))
|
||||||
.title(Line::styled(&input.title, THEME.input.title)),
|
|
||||||
)
|
|
||||||
.style(THEME.input.value)
|
|
||||||
.render(area, buf);
|
.render(area, buf);
|
||||||
|
|
||||||
if let Some(Range { start, end }) = input.selected() {
|
input.render(area.inner(Margin::new(1, 1)), buf);
|
||||||
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(),
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
use std::{io, ops::{Deref, DerefMut}, sync::atomic::{AtomicBool, AtomicU8, Ordering}};
|
use std::{io, ops::{Deref, DerefMut}, sync::atomic::{AtomicBool, AtomicU8, Ordering}};
|
||||||
|
|
||||||
use anyhow::Result;
|
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 cursor::RestoreCursor;
|
||||||
use ratatui::{CompletedFrame, Frame, Terminal, backend::CrosstermBackend, buffer::Buffer, layout::Rect};
|
use ratatui::{CompletedFrame, Frame, Terminal, backend::CrosstermBackend, buffer::Buffer, layout::Rect};
|
||||||
use yazi_adapter::{Emulator, Mux};
|
use yazi_adapter::{Emulator, Mux};
|
||||||
use yazi_config::{INPUT, MGR};
|
use yazi_config::MGR;
|
||||||
use yazi_shared::{SyncCell, tty::{TTY, TtyWriter}};
|
use yazi_shared::{SyncCell, tty::{TTY, TtyWriter}};
|
||||||
|
|
||||||
static CSI_U: AtomicBool = AtomicBool::new(false);
|
static CSI_U: AtomicBool = AtomicBool::new(false);
|
||||||
|
|
@ -141,36 +141,6 @@ impl Term {
|
||||||
pub(super) fn can_partial(&mut self) -> bool {
|
pub(super) fn can_partial(&mut self) -> bool {
|
||||||
self.inner.autoresize().is_ok() && self.last_area == self.inner.get_frame().area()
|
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 {
|
impl Drop for Term {
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,7 @@ mlua = { workspace = true }
|
||||||
parking_lot = { workspace = true }
|
parking_lot = { workspace = true }
|
||||||
ratatui = { workspace = true }
|
ratatui = { workspace = true }
|
||||||
serde_json = { 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 = { workspace = true }
|
||||||
tokio-stream = { workspace = true }
|
tokio-stream = { workspace = true }
|
||||||
tokio-util = { workspace = true }
|
tokio-util = { workspace = true }
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
use yazi_macro::emit;
|
use yazi_macro::emit;
|
||||||
use yazi_shared::event::Cmd;
|
use yazi_shared::{Id, event::Cmd};
|
||||||
|
|
||||||
pub struct CmpProxy;
|
pub struct CmpProxy;
|
||||||
|
|
||||||
|
|
@ -10,7 +10,7 @@ impl CmpProxy {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[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)));
|
emit!(Call(Cmd::args("cmp:trigger", &[word]).with("ticket", ticket)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,11 @@
|
||||||
use std::{error::Error, fmt::{self, Display}};
|
use std::{error::Error, fmt::{self, Display}};
|
||||||
|
|
||||||
|
use crate::Id;
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum InputError {
|
pub enum InputError {
|
||||||
Typed(String),
|
Typed(String),
|
||||||
Completed(String, usize),
|
Completed(String, Id),
|
||||||
Canceled(String),
|
Canceled(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
22
yazi-widgets/Cargo.toml
Normal file
22
yazi-widgets/Cargo.toml
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
[package]
|
||||||
|
name = "yazi-widgets"
|
||||||
|
version = "25.3.7"
|
||||||
|
edition = "2021"
|
||||||
|
license = "MIT"
|
||||||
|
authors = [ "sxyazi <sxyazi@gmail.com>" ]
|
||||||
|
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 }
|
||||||
48
yazi-widgets/src/input/commands/commands.rs
Normal file
48
yazi-widgets/src/input/commands/commands.rs
Normal file
|
|
@ -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 => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -12,15 +12,15 @@ const SEPARATOR: [char; 2] = ['/', '\\'];
|
||||||
const SEPARATOR: char = std::path::MAIN_SEPARATOR;
|
const SEPARATOR: char = std::path::MAIN_SEPARATOR;
|
||||||
|
|
||||||
struct Opt {
|
struct Opt {
|
||||||
word: Cow<'static, str>,
|
word: Cow<'static, str>,
|
||||||
ticket: usize,
|
_ticket: usize, // FIXME: not used
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<CmdCow> for Opt {
|
impl From<CmdCow> for Opt {
|
||||||
fn from(mut c: CmdCow) -> Self {
|
fn from(mut c: CmdCow) -> Self {
|
||||||
Self {
|
Self {
|
||||||
word: c.take_first_str().unwrap_or_default(),
|
word: c.take_first_str().unwrap_or_default(),
|
||||||
ticket: c.get("ticket").and_then(Data::as_usize).unwrap_or(0),
|
_ticket: c.get("ticket").and_then(Data::as_usize).unwrap_or(0),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -28,11 +28,7 @@ impl From<CmdCow> for Opt {
|
||||||
impl Input {
|
impl Input {
|
||||||
#[yazi_codegen::command]
|
#[yazi_codegen::command]
|
||||||
pub fn complete(&mut self, opt: Opt) {
|
pub fn complete(&mut self, opt: Opt) {
|
||||||
if self.ticket != opt.ticket {
|
let (before, after) = self.partition();
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let [before, after] = self.partition();
|
|
||||||
let new = if let Some((prefix, _)) = before.rsplit_once(SEPARATOR) {
|
let new = if let Some((prefix, _)) = before.rsplit_once(SEPARATOR) {
|
||||||
format!("{prefix}/{}{after}", opt.word).replace(SEPARATOR, MAIN_SEPARATOR_STR)
|
format!("{prefix}/{}{after}", opt.word).replace(SEPARATOR, MAIN_SEPARATOR_STR)
|
||||||
} else {
|
} else {
|
||||||
27
yazi-widgets/src/input/commands/escape.rs
Normal file
27
yazi-widgets/src/input/commands/escape.rs
Normal file
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
1
yazi-widgets/src/input/commands/mod.rs
Normal file
1
yazi-widgets/src/input/commands/mod.rs
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
yazi_macro::mod_flat!(backspace backward commands complete delete escape forward insert kill move_ paste redo replace type_ undo visual yank);
|
||||||
|
|
@ -33,7 +33,7 @@ impl Input {
|
||||||
|
|
||||||
render!(self.handle_op(opt.step.cursor(snap), false));
|
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 {
|
if snap.offset > snap.cursor {
|
||||||
snap.offset = snap.cursor;
|
snap.offset = snap.cursor;
|
||||||
} else if snap.value.is_empty() {
|
} else if snap.value.is_empty() {
|
||||||
|
|
@ -27,6 +27,6 @@ impl Input {
|
||||||
}
|
}
|
||||||
|
|
||||||
render!();
|
render!();
|
||||||
self.snaps.tag(self.limit()).then(|| self.flush_value());
|
self.snaps.tag(self.limit).then(|| self.flush_value());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
129
yazi-widgets/src/input/input.rs
Normal file
129
yazi-widgets/src/input/input.rs
Normal file
|
|
@ -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<dyn Fn(&str, &str)>;
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct Input {
|
||||||
|
pub snaps: InputSnaps,
|
||||||
|
pub limit: usize,
|
||||||
|
pub callback: Option<InputCallback>,
|
||||||
|
}
|
||||||
|
|
||||||
|
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::<String>();
|
||||||
|
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<Range<u16>> {
|
||||||
|
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() }
|
||||||
|
}
|
||||||
3
yazi-widgets/src/input/mod.rs
Normal file
3
yazi-widgets/src/input/mod.rs
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
yazi_macro::mod_pub!(commands);
|
||||||
|
|
||||||
|
yazi_macro::mod_flat!(input mode op snap snaps widget);
|
||||||
|
|
@ -5,14 +5,14 @@ use unicode_width::UnicodeWidthChar;
|
||||||
use super::{InputMode, InputOp};
|
use super::{InputMode, InputOp};
|
||||||
|
|
||||||
#[derive(Clone, Debug, Default, PartialEq, Eq)]
|
#[derive(Clone, Debug, Default, PartialEq, Eq)]
|
||||||
pub(super) struct InputSnap {
|
pub struct InputSnap {
|
||||||
pub(super) value: String,
|
pub value: String,
|
||||||
|
|
||||||
pub(super) op: InputOp,
|
pub op: InputOp,
|
||||||
|
|
||||||
pub(super) mode: InputMode,
|
pub mode: InputMode,
|
||||||
pub(super) offset: usize,
|
pub offset: usize,
|
||||||
pub(super) cursor: usize,
|
pub cursor: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl InputSnap {
|
impl InputSnap {
|
||||||
|
|
@ -22,7 +22,7 @@ impl InputSnap {
|
||||||
|
|
||||||
op: Default::default(),
|
op: Default::default(),
|
||||||
|
|
||||||
mode: Default::default(),
|
mode: Default::default(),
|
||||||
offset: usize::MAX,
|
offset: usize::MAX,
|
||||||
cursor: usize::MAX,
|
cursor: usize::MAX,
|
||||||
};
|
};
|
||||||
|
|
@ -2,20 +2,27 @@ use std::mem;
|
||||||
|
|
||||||
use super::InputSnap;
|
use super::InputSnap;
|
||||||
|
|
||||||
#[derive(Default, PartialEq, Eq)]
|
#[derive(PartialEq, Eq)]
|
||||||
pub(super) struct InputSnaps {
|
pub struct InputSnaps {
|
||||||
idx: usize,
|
idx: usize,
|
||||||
versions: Vec<InputSnap>,
|
versions: Vec<InputSnap>,
|
||||||
current: InputSnap,
|
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 {
|
impl InputSnaps {
|
||||||
#[inline]
|
pub fn new(value: String, limit: usize) -> Self {
|
||||||
pub(super) fn reset(&mut self, value: String, limit: usize) {
|
let current = InputSnap::new(value, limit);
|
||||||
self.idx = 0;
|
Self { idx: 0, versions: vec![current.clone()], current }
|
||||||
self.versions.clear();
|
|
||||||
self.versions.push(InputSnap::new(value, limit));
|
|
||||||
self.current = self.versions[0].clone();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn tag(&mut self, limit: usize) -> bool {
|
pub(super) fn tag(&mut self, limit: usize) -> bool {
|
||||||
|
|
@ -65,7 +72,7 @@ impl InputSnaps {
|
||||||
|
|
||||||
impl InputSnaps {
|
impl InputSnaps {
|
||||||
#[inline]
|
#[inline]
|
||||||
pub(super) fn current(&self) -> &InputSnap { &self.current }
|
pub fn current(&self) -> &InputSnap { &self.current }
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
pub(super) fn current_mut(&mut self) -> &mut InputSnap { &mut self.current }
|
pub(super) fn current_mut(&mut self) -> &mut InputSnap { &mut self.current }
|
||||||
30
yazi-widgets/src/input/widget.rs
Normal file
30
yazi-widgets/src/input/widget.rs
Normal file
|
|
@ -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,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
1
yazi-widgets/src/lib.rs
Normal file
1
yazi-widgets/src/lib.rs
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
yazi_macro::mod_pub!(input);
|
||||||
Loading…
Add table
Reference in a new issue