From ef654f442060c3a4feae6d89f8b9ddf300d54ecc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=89=E5=92=B2=E9=9B=85=20misaki=20masa?= Date: Wed, 24 Jun 2026 23:14:16 +0800 Subject: [PATCH] feat: context-aware icons for inputs (#4080) --- CHANGELOG.md | 2 ++ yazi-actor/src/input/show.rs | 3 +- yazi-actor/src/mgr/rename.rs | 3 +- yazi-config/src/popup/input.rs | 10 ++++++- yazi-core/src/input/input.rs | 3 +- yazi-fm/src/input/input.rs | 47 +++++++++++++++++++++++++++----- yazi-widgets/src/input/option.rs | 2 ++ 7 files changed, 59 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e55d5a50..41888030 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/): - Bulk create ([#3793]) - Make help menu a command palette ([#4074]) - H/M/L Vim-like motion for moving cursor relative to viewport ([#3970]) +- Context-aware icons for inputs ([#4080]) - Dynamic keymap Lua API ([#4031]) - New `ui.Input` element ([#4040]) - Image preview with Überzug++ on Niri ([#3990]) @@ -1763,3 +1764,4 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/): [#4067]: https://github.com/sxyazi/yazi/pull/4067 [#4068]: https://github.com/sxyazi/yazi/pull/4068 [#4074]: https://github.com/sxyazi/yazi/pull/4074 +[#4080]: https://github.com/sxyazi/yazi/pull/4080 diff --git a/yazi-actor/src/input/show.rs b/yazi-actor/src/input/show.rs index 3c3272c3..d0babeef 100644 --- a/yazi-actor/src/input/show.rs +++ b/yazi-actor/src/input/show.rs @@ -21,9 +21,10 @@ impl Actor for Show { let area = cx.mgr.area(opt.position).padding(cx.input.padding()); let input = &mut cx.input; - input.main.visible = true; + input.main.id = opt.id.clone(); input.main.title = opt.title.clone(); input.main.position = opt.position; + input.main.visible = true; opt.styles = (&THEME.input).into(); opt.blinking = YAZI.input.cursor_blink; diff --git a/yazi-actor/src/mgr/rename.rs b/yazi-actor/src/mgr/rename.rs index 22d54467..92202b2b 100644 --- a/yazi-actor/src/mgr/rename.rs +++ b/yazi-actor/src/mgr/rename.rs @@ -42,7 +42,8 @@ impl Actor for Rename { }; let (tab, old) = (cx.tab().id, hovered.url_owned()); - let mut input = input!(cx, YAZI.input.rename().with_value(name).with_cursor(cursor))?; + let mut input = + input!(cx, YAZI.input.rename(hovered.is_dir()).with_value(name).with_cursor(cursor))?; tokio::spawn(async move { let Some(InputEvent::Submit(name)) = input.recv().await else { return }; diff --git a/yazi-config/src/popup/input.rs b/yazi-config/src/popup/input.rs index e28608ac..348e35e5 100644 --- a/yazi-config/src/popup/input.rs +++ b/yazi-config/src/popup/input.rs @@ -47,6 +47,7 @@ pub struct Input { impl Input { pub fn cd(&self, cwd: Url) -> InputOpt { InputOpt { + id: "cd".to_owned(), title: self.cd_title.clone(), value: if cwd.kind().is_local() { String::new() } else { EncodeScheme(cwd).to_string() }, position: Position::new(self.cd_origin, self.cd_offset), @@ -57,14 +58,16 @@ impl Input { pub fn create(&self, dir: bool) -> InputOpt { InputOpt { + id: "create".to_owned(), title: self.create_title[dir as usize].clone(), position: Position::new(self.create_origin, self.create_offset), ..Default::default() } } - pub fn rename(&self) -> InputOpt { + pub fn rename(&self, is_dir: bool) -> InputOpt { InputOpt { + id: format!("rename-{}", if is_dir { "dir" } else { "file" }), title: self.rename_title.clone(), position: Position::new(self.rename_origin, self.rename_offset), ..Default::default() @@ -73,6 +76,7 @@ impl Input { pub fn filter(&self) -> InputOpt { InputOpt { + id: "filter".to_owned(), title: self.filter_title.clone(), position: Position::new(self.filter_origin, self.filter_offset), realtime: true, @@ -82,6 +86,7 @@ impl Input { pub fn find(&self, prev: bool) -> InputOpt { InputOpt { + id: "find".to_owned(), title: self.find_title[prev as usize].clone(), position: Position::new(self.find_origin, self.find_offset), realtime: true, @@ -91,6 +96,7 @@ impl Input { pub fn search(&self, name: &str) -> InputOpt { InputOpt { + id: "search".to_owned(), title: self.search_title.replace("{n}", name), position: Position::new(self.search_origin, self.search_offset), ..Default::default() @@ -99,6 +105,7 @@ impl Input { pub fn shell(&self, block: bool) -> InputOpt { InputOpt { + id: "shell".to_owned(), title: self.shell_title[block as usize].clone(), position: Position::new(self.shell_origin, self.shell_offset), ..Default::default() @@ -107,6 +114,7 @@ impl Input { pub fn tab_rename(&self) -> InputOpt { InputOpt { + id: "tab-rename".to_owned(), title: "Rename tab:".to_owned(), position: Position::new(Origin::TopCenter, Offset { x: 0, diff --git a/yazi-core/src/input/input.rs b/yazi-core/src/input/input.rs index 4ee51058..127556ce 100644 --- a/yazi-core/src/input/input.rs +++ b/yazi-core/src/input/input.rs @@ -52,9 +52,10 @@ impl Input { #[derive(Default)] pub struct InputMain { inner: yazi_widgets::input::Input, - pub visible: bool, + pub id: String, pub title: String, pub position: Position, + pub visible: bool, } impl Deref for InputMain { diff --git a/yazi-fm/src/input/input.rs b/yazi-fm/src/input/input.rs index c2edf160..84962b41 100644 --- a/yazi-fm/src/input/input.rs +++ b/yazi-fm/src/input/input.rs @@ -1,7 +1,11 @@ +use std::path::Path; + use ratatui_core::{buffer::Buffer, layout::{Margin, Rect}, text::Line, widgets::Widget}; use ratatui_widgets::{block::Block, borders::BorderType}; -use yazi_config::THEME; +use yazi_config::{Icon, THEME}; use yazi_core::Core; +use yazi_fs::{cha::{Cha, ChaKind}, file::File}; +use yazi_shim::path::CROSS_SEPARATOR; pub(crate) struct Input<'a> { core: &'a Core, @@ -9,21 +13,50 @@ pub(crate) struct Input<'a> { impl<'a> Input<'a> { pub(crate) fn new(core: &'a Core) -> Self { Self { core } } + + fn icon(&self) -> Option { + let input = &self.core.input.main; + + let is_dir = input.value().ends_with(CROSS_SEPARATOR); + let is_hovered = input.id.starts_with("rename-"); + + let (path, mode): (&str, u16) = match input.id.as_str() { + "cd" => (input.value(), if is_dir { 0o40755 } else { 0o100644 }), + "create" => (input.value(), if is_dir { 0o40755 } else { 0o100644 }), + "rename-file" => (input.value(), 0o100644), + "rename-dir" => (input.value(), 0o40755), + "shell" => ("icon.sh", 0o100644), + _ => return None, + }; + + THEME.icon.matches( + &File { + url: Path::new(path).into(), + cha: Cha { kind: ChaKind::empty(), mode: mode.try_into().ok()?, ..Default::default() }, + link_to: None, + }, + is_hovered, + ) + } } impl Widget for Input<'_> { fn render(self, _: Rect, buf: &mut Buffer) { - let input = &self.core.input; + let input = &self.core.input.main; - let outer = self.core.mgr.area(input.main.position); + let outer = self.core.mgr.area(input.position); yazi_widgets::clear::Clear::default().render(outer, buf); - Block::bordered() + let mut block = Block::bordered() .border_type(BorderType::Rounded) .border_style(THEME.input.border.get()) - .title(Line::styled(&input.main.title, THEME.input.title.get())) - .render(outer, buf); + .title(Line::styled(&input.title, THEME.input.title.get())); - input.main.render(outer.inner(Margin::new(1, 1)), buf); + if let Some(i) = self.icon() { + block = block.title_bottom(Line::raw(format!("{} ", i.text)).style(i.style).right_aligned()); + } + + block.render(outer, buf); + input.render(outer.inner(Margin::new(1, 1)), buf); } } diff --git a/yazi-widgets/src/input/option.rs b/yazi-widgets/src/input/option.rs index 3cb3c1c5..023f1f99 100644 --- a/yazi-widgets/src/input/option.rs +++ b/yazi-widgets/src/input/option.rs @@ -6,6 +6,7 @@ use crate::input::{InputCallback, InputStyles}; #[derive(Clone, Debug, Default)] pub struct InputOpt { + pub id: String, pub title: String, pub value: String, pub styles: InputStyles, @@ -42,6 +43,7 @@ impl TryFrom<&Table> for InputOpt { fn try_from(t: &Table) -> Result { Ok(Self { + id: t.raw_get("id").unwrap_or_default(), title: t.raw_get("title").unwrap_or_default(), value: t.raw_get("value").unwrap_or_default(), styles: t.raw_get("styles")?,