From d933c0e6f11fbde6b5afc896620bed2ff5bd76a3 Mon Sep 17 00:00:00 2001 From: stickyburn <37672190+stickyburn@users.noreply.github.com> Date: Mon, 2 Mar 2026 17:03:56 -0500 Subject: [PATCH] chore: use if state for hovered icon --- yazi-binding/src/icon.rs | 13 +-- yazi-binding/src/macros.rs | 4 +- yazi-config/preset/theme-dark.toml | 3 +- yazi-config/src/icon.rs | 5 +- yazi-config/src/theme/icon.rs | 142 +++++++++++++++-------- yazi-plugin/preset/components/entity.lua | 5 +- 6 files changed, 107 insertions(+), 65 deletions(-) diff --git a/yazi-binding/src/icon.rs b/yazi-binding/src/icon.rs index 1a8486d1..d08f0410 100644 --- a/yazi-binding/src/icon.rs +++ b/yazi-binding/src/icon.rs @@ -5,11 +5,9 @@ use mlua::{UserData, UserDataFields, Value}; use crate::{Style, cached_field}; pub struct Icon { - inner: &'static yazi_config::Icon, - - v_text: Option, - v_style: Option, - v_hovered_text: Option, + inner: &'static yazi_config::Icon, + v_text: Option, + v_style: Option, } impl Deref for Icon { @@ -20,7 +18,7 @@ impl Deref for Icon { impl From<&'static yazi_config::Icon> for Icon { fn from(icon: &'static yazi_config::Icon) -> Self { - Self { inner: icon, v_text: None, v_style: None, v_hovered_text: None } + Self { inner: icon, v_text: None, v_style: None } } } @@ -28,8 +26,5 @@ impl UserData for Icon { fn add_fields>(fields: &mut F) { cached_field!(fields, text, |lua, me| lua.create_string(&me.text)); cached_field!(fields, style, |_, me| Ok(Style::from(me.style))); - cached_field!(fields, hovered_text, |lua, me| { - me.hovered_text.as_ref().map(|s| lua.create_string(s)).transpose() - }); } } diff --git a/yazi-binding/src/macros.rs b/yazi-binding/src/macros.rs index ac7eb742..53351f87 100644 --- a/yazi-binding/src/macros.rs +++ b/yazi-binding/src/macros.rs @@ -268,10 +268,10 @@ macro_rules! impl_file_methods { Ok(me.hash_u64()) }); - $methods.add_method("icon", |_, me, ()| { + $methods.add_method("icon", |_, me, hovered: Option| { use $crate::Icon; // TODO: use a cache - Ok(yazi_config::THEME.icon.matches(me).map(Icon::from)) + Ok(yazi_config::THEME.icon.matches(me, hovered.unwrap_or(false)).map(Icon::from)) }); }; } diff --git a/yazi-config/preset/theme-dark.toml b/yazi-config/preset/theme-dark.toml index 89c7b8bb..dfc33def 100644 --- a/yazi-config/preset/theme-dark.toml +++ b/yazi-config/preset/theme-dark.toml @@ -997,7 +997,8 @@ conds = [ { if = "dummy", text = "", fg = "#f44336" }, # Fallback - { if = "dir", text = "", fg = "#03a9f4", hovered_text = "" }, + { if = "dir & hovered", text = "" }, + { if = "dir", text = "", fg = "#03a9f4" }, { if = "exec", text = "", fg = "#8bc34a" }, { if = "!dir", text = "", fg = "#ffffff" }, ] diff --git a/yazi-config/src/icon.rs b/yazi-config/src/icon.rs index fe418474..d19cdc21 100644 --- a/yazi-config/src/icon.rs +++ b/yazi-config/src/icon.rs @@ -2,7 +2,6 @@ use crate::Style; #[derive(Clone, Debug)] pub struct Icon { - pub text: String, - pub style: Style, - pub hovered_text: Option, + pub text: String, + pub style: Style, } diff --git a/yazi-config/src/theme/icon.rs b/yazi-config/src/theme/icon.rs index 5522b544..ca6a8955 100644 --- a/yazi-config/src/theme/icon.rs +++ b/yazi-config/src/theme/icon.rs @@ -44,7 +44,7 @@ pub struct Icon { } impl Icon { - pub fn matches(&self, file: &File) -> Option<&I> { + pub fn matches(&self, file: &File, hovered: bool) -> Option<&I> { if let Some(i) = self.match_by_glob(file) { return Some(i); } @@ -65,6 +65,7 @@ impl Icon { "sock" => file.is_sock(), "exec" => file.is_exec(), "sticky" => file.is_sticky(), + "hovered" => hovered, _ => false, }; self.conds.iter().find(|(c, _)| c.eval(f) == Some(true)).map(|(_, i)| i) @@ -132,22 +133,15 @@ impl<'de> Deserialize<'de> for PatIcons { { #[derive(Deserialize)] struct Shadow { - url: Pattern, - text: String, - fg: Option, - hovered_text: Option, + url: Pattern, + text: String, + fg: Option, } Ok(Self( >::deserialize(deserializer)? .into_iter() - .map(|s| { - (s.url, I { - text: s.text, - style: Style { fg: s.fg, ..Default::default() }, - hovered_text: s.hovered_text, - }) - }) + .map(|s| (s.url, I { text: s.text, style: Style { fg: s.fg, ..Default::default() } })) .collect(), )) } @@ -169,22 +163,15 @@ impl<'de> Deserialize<'de> for StrIcons { { #[derive(Deserialize)] struct Shadow { - name: String, - text: String, - fg: Option, - hovered_text: Option, + name: String, + text: String, + fg: Option, } Ok(Self( >::deserialize(deserializer)? .into_iter() - .map(|s| { - (s.name, I { - text: s.text, - style: Style { fg: s.fg, ..Default::default() }, - hovered_text: s.hovered_text, - }) - }) + .map(|s| (s.name, I { text: s.text, style: Style { fg: s.fg, ..Default::default() } })) .collect(), )) } @@ -206,22 +193,15 @@ impl<'de> Deserialize<'de> for CondIcons { { #[derive(Deserialize)] struct Shadow { - r#if: Condition, - text: String, - fg: Option, - hovered_text: Option, + r#if: Condition, + text: String, + fg: Option, } Ok(Self( >::deserialize(deserializer)? .into_iter() - .map(|s| { - (s.r#if, I { - text: s.text, - style: Style { fg: s.fg, ..Default::default() }, - hovered_text: s.hovered_text, - }) - }) + .map(|s| (s.r#if, I { text: s.text, style: Style { fg: s.fg, ..Default::default() } })) .collect(), )) } @@ -229,24 +209,94 @@ impl<'de> Deserialize<'de> for CondIcons { #[cfg(test)] mod tests { + use std::str::FromStr; + + use yazi_fs::{File, cha::{Cha, ChaType}}; + use yazi_shared::url::Url; + use super::*; + + fn create_test_file(name: &str, cha_type: ChaType) -> File { + let url = Url::regular(name).to_owned(); + File::from_dummy(url, Some(cha_type)) + } + #[test] - fn test_icon_struct_with_hovered_text() { - let icon = I { - text: "normal_icon".to_string(), - style: Style::default(), - hovered_text: Some("hovered_icon".to_string()), + fn test_matches_hovered() { + // icon with hovered only + let icon = Icon { + conds: CondIcons(vec![(Condition::from_str("hovered").unwrap(), I { + text: "hovered_icon".to_string(), + style: Style::default(), + })]), + ..Default::default() }; - assert_eq!(icon.text, "normal_icon"); - assert_eq!(icon.hovered_text, Some("hovered_icon".to_string())); + let file = create_test_file("test.txt", ChaType::File); + + // should match when hovered + let result = icon.matches(&file, true); + assert!(result.is_some()); + assert_eq!(result.unwrap().text, "hovered_icon"); + + // should not match when not hovered + let result = icon.matches(&file, false); + assert!(result.is_none()); } #[test] - fn test_icon_struct_without_hovered_text() { - let icon = - I { text: "normal_icon".to_string(), style: Style::default(), hovered_text: None }; + fn test_matches_dir_and_hovered_condition() { + // icon with dir and hovered + let icon = Icon { + conds: CondIcons(vec![(Condition::from_str("dir & hovered").unwrap(), I { + text: "dir_hovered".to_string(), + style: Style::default(), + })]), + ..Default::default() + }; - assert_eq!(icon.text, "normal_icon"); - assert_eq!(icon.hovered_text, None); + let dir_file = create_test_file("test_dir", ChaType::Dir); + let file = create_test_file("test.txt", ChaType::File); + + // directory + hovered + let result = icon.matches(&dir_file, true); + assert!(result.is_some()); + assert_eq!(result.unwrap().text, "dir_hovered"); + + // directory + not hovered + let result = icon.matches(&dir_file, false); + assert!(result.is_none()); + + // file + hovered isnt present + let result = icon.matches(&file, true); + assert!(result.is_none()); + } + #[test] + fn test_matches_hovered_before_dir() { + // icon with hovered before dir + let icon = Icon { + conds: CondIcons(vec![ + (Condition::from_str("dir & hovered").unwrap(), I { + text: "dir_hovered".to_string(), + style: Style::default(), + }), + (Condition::from_str("dir").unwrap(), I { + text: "dir_normal".to_string(), + style: Style::default(), + }), + ]), + ..Default::default() + }; + + let dir_file = create_test_file("test_dir", ChaType::Dir); + + // hovered + let result = icon.matches(&dir_file, true); + assert!(result.is_some()); + assert_eq!(result.unwrap().text, "dir_hovered"); + + // non-hovered + let result = icon.matches(&dir_file, false); + assert!(result.is_some()); + assert_eq!(result.unwrap().text, "dir_normal"); } } diff --git a/yazi-plugin/preset/components/entity.lua b/yazi-plugin/preset/components/entity.lua index 86e38a71..328451ff 100644 --- a/yazi-plugin/preset/components/entity.lua +++ b/yazi-plugin/preset/components/entity.lua @@ -26,12 +26,9 @@ function Entity:padding() end function Entity:icon() - local icon = self._file:icon() + local icon = self._file:icon(self._file.is_hovered) if not icon then return "" - elseif self._file.is_hovered then - local text = icon.hovered_text or icon.text - return text .. " " else return ui.Line(icon.text .. " "):style(icon.style) end