feat: context-aware icons for inputs (#4080)

This commit is contained in:
三咲雅 misaki masa 2026-06-24 23:14:16 +08:00 committed by GitHub
parent 2a106aa231
commit ef654f4420
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 59 additions and 11 deletions

View file

@ -18,6 +18,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/):
- Bulk create ([#3793]) - Bulk create ([#3793])
- Make help menu a command palette ([#4074]) - Make help menu a command palette ([#4074])
- H/M/L Vim-like motion for moving cursor relative to viewport ([#3970]) - H/M/L Vim-like motion for moving cursor relative to viewport ([#3970])
- Context-aware icons for inputs ([#4080])
- Dynamic keymap Lua API ([#4031]) - Dynamic keymap Lua API ([#4031])
- New `ui.Input` element ([#4040]) - New `ui.Input` element ([#4040])
- Image preview with Überzug++ on Niri ([#3990]) - 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 [#4067]: https://github.com/sxyazi/yazi/pull/4067
[#4068]: https://github.com/sxyazi/yazi/pull/4068 [#4068]: https://github.com/sxyazi/yazi/pull/4068
[#4074]: https://github.com/sxyazi/yazi/pull/4074 [#4074]: https://github.com/sxyazi/yazi/pull/4074
[#4080]: https://github.com/sxyazi/yazi/pull/4080

View file

@ -21,9 +21,10 @@ impl Actor for Show {
let area = cx.mgr.area(opt.position).padding(cx.input.padding()); let area = cx.mgr.area(opt.position).padding(cx.input.padding());
let input = &mut cx.input; let input = &mut cx.input;
input.main.visible = true; input.main.id = opt.id.clone();
input.main.title = opt.title.clone(); input.main.title = opt.title.clone();
input.main.position = opt.position; input.main.position = opt.position;
input.main.visible = true;
opt.styles = (&THEME.input).into(); opt.styles = (&THEME.input).into();
opt.blinking = YAZI.input.cursor_blink; opt.blinking = YAZI.input.cursor_blink;

View file

@ -42,7 +42,8 @@ impl Actor for Rename {
}; };
let (tab, old) = (cx.tab().id, hovered.url_owned()); 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 { tokio::spawn(async move {
let Some(InputEvent::Submit(name)) = input.recv().await else { return }; let Some(InputEvent::Submit(name)) = input.recv().await else { return };

View file

@ -47,6 +47,7 @@ pub struct Input {
impl Input { impl Input {
pub fn cd(&self, cwd: Url) -> InputOpt { pub fn cd(&self, cwd: Url) -> InputOpt {
InputOpt { InputOpt {
id: "cd".to_owned(),
title: self.cd_title.clone(), title: self.cd_title.clone(),
value: if cwd.kind().is_local() { String::new() } else { EncodeScheme(cwd).to_string() }, value: if cwd.kind().is_local() { String::new() } else { EncodeScheme(cwd).to_string() },
position: Position::new(self.cd_origin, self.cd_offset), position: Position::new(self.cd_origin, self.cd_offset),
@ -57,14 +58,16 @@ impl Input {
pub fn create(&self, dir: bool) -> InputOpt { pub fn create(&self, dir: bool) -> InputOpt {
InputOpt { InputOpt {
id: "create".to_owned(),
title: self.create_title[dir as usize].clone(), title: self.create_title[dir as usize].clone(),
position: Position::new(self.create_origin, self.create_offset), position: Position::new(self.create_origin, self.create_offset),
..Default::default() ..Default::default()
} }
} }
pub fn rename(&self) -> InputOpt { pub fn rename(&self, is_dir: bool) -> InputOpt {
InputOpt { InputOpt {
id: format!("rename-{}", if is_dir { "dir" } else { "file" }),
title: self.rename_title.clone(), title: self.rename_title.clone(),
position: Position::new(self.rename_origin, self.rename_offset), position: Position::new(self.rename_origin, self.rename_offset),
..Default::default() ..Default::default()
@ -73,6 +76,7 @@ impl Input {
pub fn filter(&self) -> InputOpt { pub fn filter(&self) -> InputOpt {
InputOpt { InputOpt {
id: "filter".to_owned(),
title: self.filter_title.clone(), title: self.filter_title.clone(),
position: Position::new(self.filter_origin, self.filter_offset), position: Position::new(self.filter_origin, self.filter_offset),
realtime: true, realtime: true,
@ -82,6 +86,7 @@ impl Input {
pub fn find(&self, prev: bool) -> InputOpt { pub fn find(&self, prev: bool) -> InputOpt {
InputOpt { InputOpt {
id: "find".to_owned(),
title: self.find_title[prev as usize].clone(), title: self.find_title[prev as usize].clone(),
position: Position::new(self.find_origin, self.find_offset), position: Position::new(self.find_origin, self.find_offset),
realtime: true, realtime: true,
@ -91,6 +96,7 @@ impl Input {
pub fn search(&self, name: &str) -> InputOpt { pub fn search(&self, name: &str) -> InputOpt {
InputOpt { InputOpt {
id: "search".to_owned(),
title: self.search_title.replace("{n}", name), title: self.search_title.replace("{n}", name),
position: Position::new(self.search_origin, self.search_offset), position: Position::new(self.search_origin, self.search_offset),
..Default::default() ..Default::default()
@ -99,6 +105,7 @@ impl Input {
pub fn shell(&self, block: bool) -> InputOpt { pub fn shell(&self, block: bool) -> InputOpt {
InputOpt { InputOpt {
id: "shell".to_owned(),
title: self.shell_title[block as usize].clone(), title: self.shell_title[block as usize].clone(),
position: Position::new(self.shell_origin, self.shell_offset), position: Position::new(self.shell_origin, self.shell_offset),
..Default::default() ..Default::default()
@ -107,6 +114,7 @@ impl Input {
pub fn tab_rename(&self) -> InputOpt { pub fn tab_rename(&self) -> InputOpt {
InputOpt { InputOpt {
id: "tab-rename".to_owned(),
title: "Rename tab:".to_owned(), title: "Rename tab:".to_owned(),
position: Position::new(Origin::TopCenter, Offset { position: Position::new(Origin::TopCenter, Offset {
x: 0, x: 0,

View file

@ -52,9 +52,10 @@ impl Input {
#[derive(Default)] #[derive(Default)]
pub struct InputMain { pub struct InputMain {
inner: yazi_widgets::input::Input, inner: yazi_widgets::input::Input,
pub visible: bool, pub id: String,
pub title: String, pub title: String,
pub position: Position, pub position: Position,
pub visible: bool,
} }
impl Deref for InputMain { impl Deref for InputMain {

View file

@ -1,7 +1,11 @@
use std::path::Path;
use ratatui_core::{buffer::Buffer, layout::{Margin, Rect}, text::Line, widgets::Widget}; use ratatui_core::{buffer::Buffer, layout::{Margin, Rect}, text::Line, widgets::Widget};
use ratatui_widgets::{block::Block, borders::BorderType}; use ratatui_widgets::{block::Block, borders::BorderType};
use yazi_config::THEME; use yazi_config::{Icon, THEME};
use yazi_core::Core; use yazi_core::Core;
use yazi_fs::{cha::{Cha, ChaKind}, file::File};
use yazi_shim::path::CROSS_SEPARATOR;
pub(crate) struct Input<'a> { pub(crate) struct Input<'a> {
core: &'a Core, core: &'a Core,
@ -9,21 +13,50 @@ pub(crate) struct Input<'a> {
impl<'a> Input<'a> { impl<'a> Input<'a> {
pub(crate) fn new(core: &'a Core) -> Self { Self { core } } pub(crate) fn new(core: &'a Core) -> Self { Self { core } }
fn icon(&self) -> Option<Icon> {
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<'_> { impl Widget for Input<'_> {
fn render(self, _: Rect, buf: &mut Buffer) { 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); yazi_widgets::clear::Clear::default().render(outer, buf);
Block::bordered() let mut block = Block::bordered()
.border_type(BorderType::Rounded) .border_type(BorderType::Rounded)
.border_style(THEME.input.border.get()) .border_style(THEME.input.border.get())
.title(Line::styled(&input.main.title, THEME.input.title.get())) .title(Line::styled(&input.title, THEME.input.title.get()));
.render(outer, buf);
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);
} }
} }

View file

@ -6,6 +6,7 @@ use crate::input::{InputCallback, InputStyles};
#[derive(Clone, Debug, Default)] #[derive(Clone, Debug, Default)]
pub struct InputOpt { pub struct InputOpt {
pub id: String,
pub title: String, pub title: String,
pub value: String, pub value: String,
pub styles: InputStyles, pub styles: InputStyles,
@ -42,6 +43,7 @@ impl TryFrom<&Table> for InputOpt {
fn try_from(t: &Table) -> Result<Self, Self::Error> { fn try_from(t: &Table) -> Result<Self, Self::Error> {
Ok(Self { Ok(Self {
id: t.raw_get("id").unwrap_or_default(),
title: t.raw_get("title").unwrap_or_default(), title: t.raw_get("title").unwrap_or_default(),
value: t.raw_get("value").unwrap_or_default(), value: t.raw_get("value").unwrap_or_default(),
styles: t.raw_get("styles")?, styles: t.raw_get("styles")?,