From 9e9ebf0745eacbda368b64af45cce34d79fe2011 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=89=E5=92=B2=E9=9B=85=20=C2=B7=20Misaki=20Masa?= Date: Fri, 25 Apr 2025 07:15:51 +0800 Subject: [PATCH] feat: obscure input component for inputting passwords (#2675) --- yazi-adapter/src/brand.rs | 1 + yazi-config/src/popup/options.rs | 3 +- yazi-core/src/input/commands/show.rs | 4 +-- yazi-core/src/input/input.rs | 3 -- yazi-core/src/mgr/mgr.rs | 16 +--------- yazi-core/src/mgr/tabs.rs | 14 --------- yazi-plugin/preset/plugins/extract.lua | 1 + yazi-plugin/src/utils/layer.rs | 2 +- yazi-widgets/src/input/commands/move_.rs | 9 +++--- yazi-widgets/src/input/input.rs | 25 +++++++++------- yazi-widgets/src/input/snap.rs | 38 +++++++++++++++++++----- yazi-widgets/src/input/snaps.rs | 8 ++--- yazi-widgets/src/input/widget.rs | 2 +- 13 files changed, 59 insertions(+), 67 deletions(-) diff --git a/yazi-adapter/src/brand.rs b/yazi-adapter/src/brand.rs index cbf82cbd..c9cebc75 100644 --- a/yazi-adapter/src/brand.rs +++ b/yazi-adapter/src/brand.rs @@ -53,6 +53,7 @@ impl Brand { ("WEZTERM_EXECUTABLE", B::WezTerm), ("GHOSTTY_RESOURCES_DIR", B::Ghostty), ("WT_Session", B::Microsoft), + ("WARP_HONOR_PS1", B::Warp), ("VSCODE_INJECTION", B::VSCode), ("TABBY_CONFIG_DIRECTORY", B::Tabby), ]; diff --git a/yazi-config/src/popup/options.rs b/yazi-config/src/popup/options.rs index 2663cabc..b27d0c65 100644 --- a/yazi-config/src/popup/options.rs +++ b/yazi-config/src/popup/options.rs @@ -9,10 +9,10 @@ pub struct InputCfg { pub title: String, pub value: String, pub cursor: Option, + pub obscure: bool, pub position: Position, pub realtime: bool, pub completion: bool, - pub highlight: bool, } #[derive(Default)] @@ -86,7 +86,6 @@ impl InputCfg { Self { title: YAZI.input.shell_title[block as usize].to_owned(), position: Position::new(YAZI.input.shell_origin, YAZI.input.shell_offset), - highlight: true, ..Default::default() } } diff --git a/yazi-core/src/input/commands/show.rs b/yazi-core/src/input/commands/show.rs index d17ef3e5..13a4b55c 100644 --- a/yazi-core/src/input/commands/show.rs +++ b/yazi-core/src/input/commands/show.rs @@ -32,9 +32,6 @@ impl Input { self.tx = Some(opt.tx.clone()); let ticket = self.ticket.clone(); - // Shell - self.highlight = opt.cfg.highlight; - // Reset input let cb: InputCallback = Box::new(move |before, after| { if opt.cfg.realtime { @@ -46,6 +43,7 @@ impl Input { self.inner = yazi_widgets::input::Input::new( opt.cfg.value, opt.cfg.position.offset.width.saturating_sub(YAZI.input.border()) as usize, + opt.cfg.obscure, cb, ); diff --git a/yazi-core/src/input/input.rs b/yazi-core/src/input/input.rs index 879a0463..b68b0e0d 100644 --- a/yazi-core/src/input/input.rs +++ b/yazi-core/src/input/input.rs @@ -15,9 +15,6 @@ pub struct Input { // Typing pub(super) tx: Option>>, pub(super) ticket: Rc, - - // Shell - pub highlight: bool, } impl Deref for Input { diff --git a/yazi-core/src/mgr/mgr.rs b/yazi-core/src/mgr/mgr.rs index 7771fc2b..6a7a06cd 100644 --- a/yazi-core/src/mgr/mgr.rs +++ b/yazi-core/src/mgr/mgr.rs @@ -2,7 +2,7 @@ use ratatui::layout::Rect; use yazi_adapter::Dimension; use yazi_config::popup::{Origin, Position}; use yazi_fs::File; -use yazi_shared::{Id, url::Url}; +use yazi_shared::url::Url; use super::{Mimetype, Tabs, Watcher, Yanked}; use crate::tab::{Folder, Tab}; @@ -47,26 +47,12 @@ impl Mgr { #[inline] pub fn active_mut(&mut self) -> &mut Tab { self.tabs.active_mut() } - #[inline] - pub fn active_or(&self, id: Option) -> &Tab { self.tabs.active_or(id) } - - #[inline] - pub fn active_or_mut(&mut self, id: Option) -> &mut Tab { self.tabs.active_or_mut(id) } - #[inline] pub fn current(&self) -> &Folder { &self.active().current } #[inline] pub fn current_mut(&mut self) -> &mut Folder { &mut self.active_mut().current } - #[inline] - pub fn current_or(&self, id: Option) -> &Folder { &self.active_or(id).current } - - #[inline] - pub fn current_or_mut(&mut self, id: Option) -> &mut Folder { - &mut self.active_or_mut(id).current - } - #[inline] pub fn parent(&self) -> Option<&Folder> { self.active().parent.as_ref() } diff --git a/yazi-core/src/mgr/tabs.rs b/yazi-core/src/mgr/tabs.rs index 9c53e657..556a4f1d 100644 --- a/yazi-core/src/mgr/tabs.rs +++ b/yazi-core/src/mgr/tabs.rs @@ -49,20 +49,6 @@ impl Tabs { #[inline] pub(super) fn active_mut(&mut self) -> &mut Tab { &mut self.items[self.cursor] } - #[inline] - pub fn active_or(&self, id: Option) -> &Tab { - id.and_then(|id| self.iter().find(|&t| t.id == id)).unwrap_or(self.active()) - } - - #[inline] - pub(super) fn active_or_mut(&mut self, id: Option) -> &mut Tab { - if let Some(i) = id.and_then(|id| self.iter().position(|t| t.id == id)) { - &mut self.items[i] - } else { - self.active_mut() - } - } - #[inline] pub fn current(&self) -> &Folder { &self.active().current } diff --git a/yazi-plugin/preset/plugins/extract.lua b/yazi-plugin/preset/plugins/extract.lua index 204079e8..e59b1994 100644 --- a/yazi-plugin/preset/plugins/extract.lua +++ b/yazi-plugin/preset/plugins/extract.lua @@ -28,6 +28,7 @@ function M:entry(job) local value, event = ya.input { title = string.format('Password for "%s":', from.name), + obscure = true, position = { "center", w = 50 }, } if event == 1 then diff --git a/yazi-plugin/src/utils/layer.rs b/yazi-plugin/src/utils/layer.rs index f33fff6c..797b5275 100644 --- a/yazi-plugin/src/utils/layer.rs +++ b/yazi-plugin/src/utils/layer.rs @@ -45,10 +45,10 @@ impl Utils { title: t.raw_get("title")?, value: t.raw_get("value").unwrap_or_default(), cursor: None, // TODO + obscure: t.raw_get("obscure").unwrap_or_default(), position: Pos::new_input(t.raw_get::("position")?)?.into(), realtime, completion: false, - highlight: false, })); if !realtime { diff --git a/yazi-widgets/src/input/commands/move_.rs b/yazi-widgets/src/input/commands/move_.rs index 17d00e64..0f48bf1a 100644 --- a/yazi-widgets/src/input/commands/move_.rs +++ b/yazi-widgets/src/input/commands/move_.rs @@ -1,6 +1,5 @@ use std::{num::ParseIntError, str::FromStr}; -use unicode_width::UnicodeWidthStr; use yazi_macro::render; use yazi_shared::event::{CmdCow, Data}; @@ -40,10 +39,10 @@ impl Input { snap.offset = 0; } else { let delta = snap.mode.delta(); - let s = snap.slice(snap.offset..snap.cursor + delta); - if s.width() >= limit { - let range = InputSnap::find_window(s.chars().rev(), 0, limit); - snap.offset = snap.cursor - range.end.saturating_sub(delta); + let range = snap.offset..snap.cursor + delta; + if snap.width(range.clone()) >= limit as u16 { + let it = snap.slice(range).chars().rev().map(|c| if snap.obscure { '•' } else { c }); + snap.offset = snap.cursor - InputSnap::find_window(it, 0, limit).end.saturating_sub(delta); } } } diff --git a/yazi-widgets/src/input/input.rs b/yazi-widgets/src/input/input.rs index c70f7863..7cf03da1 100644 --- a/yazi-widgets/src/input/input.rs +++ b/yazi-widgets/src/input/input.rs @@ -1,7 +1,6 @@ -use std::ops::Range; +use std::{borrow::Cow, ops::Range}; use crossterm::cursor::SetCursorStyle; -use unicode_width::UnicodeWidthStr; use yazi_config::YAZI; use yazi_plugin::CLIPBOARD; @@ -13,12 +12,13 @@ pub type InputCallback = Box; pub struct Input { pub snaps: InputSnaps, pub limit: usize, + pub obscure: bool, 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 fn new(value: String, limit: usize, obscure: bool, callback: InputCallback) -> Self { + Self { snaps: InputSnaps::new(value, obscure, limit), limit, obscure, callback: Some(callback) } } pub(super) fn handle_op(&mut self, cursor: usize, include: bool) -> bool { @@ -75,16 +75,19 @@ impl Input { pub fn value(&self) -> &str { &self.snap().value } #[inline] - pub fn visible_value(&self) -> &str { self.snap().slice(self.snap().window(self.limit)) } + pub fn display(&self) -> Cow { + if self.obscure { + "•".repeat(self.snap().window(self.limit).len()).into() + } else { + self.snap().slice(self.snap().window(self.limit)).into() + } + } #[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(&self) -> u16 { self.snap().width(self.snap().offset..self.snap().cursor) } pub fn cursor_shape(&self) -> SetCursorStyle { use InputMode as M; @@ -110,8 +113,8 @@ impl Input { 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) + let s = snap.width(snap.offset..start); + Some(s..s + snap.width(start..end)) } #[inline] diff --git a/yazi-widgets/src/input/snap.rs b/yazi-widgets/src/input/snap.rs index 11d39ba9..b3584cc0 100644 --- a/yazi-widgets/src/input/snap.rs +++ b/yazi-widgets/src/input/snap.rs @@ -1,6 +1,6 @@ use std::ops::Range; -use unicode_width::UnicodeWidthChar; +use unicode_width::{UnicodeWidthChar, UnicodeWidthStr}; use super::{InputMode, InputOp}; @@ -10,20 +10,24 @@ pub struct InputSnap { pub op: InputOp, - pub mode: InputMode, + pub mode: InputMode, + pub obscure: bool, + pub offset: usize, pub cursor: usize, } impl InputSnap { - pub(super) fn new(value: String, limit: usize) -> Self { + pub(super) fn new(value: String, obscure: bool, limit: usize) -> Self { let mut snap = Self { value, op: Default::default(), mode: Default::default(), - offset: usize::MAX, + obscure, + + offset: 0, cursor: usize::MAX, }; snap.resize(limit); @@ -32,9 +36,19 @@ impl InputSnap { #[inline] pub(super) fn resize(&mut self, limit: usize) { - let range = Self::find_window(self.value.chars().rev(), 0, limit); + let count = self.count(); + let limit = if self.obscure { + count.min(limit) + } else { + Self::find_window(self.value.chars().rev(), 0, limit).end + }; + self.cursor = self.cursor.min(self.count().saturating_sub(self.mode.delta())); - self.offset = self.offset.min(self.cursor.saturating_sub(range.end)); + self.offset = if self.cursor < (self.offset + limit).min(count) { + count.saturating_sub(limit).min(self.offset) + } else { + count.saturating_sub(limit).min(self.cursor.saturating_sub(limit) + 1) + }; } } @@ -62,11 +76,19 @@ impl InputSnap { } #[inline] - pub(super) fn window(&self, limit: usize) -> Range { - Self::find_window(self.value.chars(), self.offset, limit) + pub(super) fn width(&self, range: Range) -> u16 { + if self.obscure { range.len() as u16 } else { self.slice(range).width() as u16 } } #[inline] + pub(super) fn window(&self, limit: usize) -> Range { + Self::find_window( + self.value.chars().map(|c| if self.obscure { '•' } else { c }), + self.offset, + limit, + ) + } + pub(super) fn find_window(it: T, offset: usize, limit: usize) -> Range where T: Iterator, diff --git a/yazi-widgets/src/input/snaps.rs b/yazi-widgets/src/input/snaps.rs index 2d19078b..0bfbd539 100644 --- a/yazi-widgets/src/input/snaps.rs +++ b/yazi-widgets/src/input/snaps.rs @@ -13,15 +13,15 @@ impl Default for InputSnaps { fn default() -> Self { Self { idx: 0, - versions: vec![InputSnap::new(String::new(), 0)], - current: InputSnap::new(String::new(), 0), + versions: vec![InputSnap::new(String::new(), false, 0)], + current: InputSnap::new(String::new(), false, 0), } } } impl InputSnaps { - pub fn new(value: String, limit: usize) -> Self { - let current = InputSnap::new(value, limit); + pub fn new(value: String, obscure: bool, limit: usize) -> Self { + let current = InputSnap::new(value, obscure, limit); Self { idx: 0, versions: vec![current.clone()], current } } diff --git a/yazi-widgets/src/input/widget.rs b/yazi-widgets/src/input/widget.rs index 43df8740..fee55ac1 100644 --- a/yazi-widgets/src/input/widget.rs +++ b/yazi-widgets/src/input/widget.rs @@ -12,7 +12,7 @@ impl Widget for &Input { { yazi_plugin::elements::Clear::default().render(area, buf); - Line::styled(self.visible_value(), THEME.input.value).render(area, buf); + Line::styled(self.display(), THEME.input.value).render(area, buf); if let Some(Range { start, end }) = self.selected() { let s = start.min(area.width);