diff --git a/yazi-actor/src/mgr/excluded.rs b/yazi-actor/src/mgr/excluded.rs new file mode 100644 index 00000000..2da95a98 --- /dev/null +++ b/yazi-actor/src/mgr/excluded.rs @@ -0,0 +1,52 @@ +use anyhow::Result; +use yazi_core::tab::Folder; +use yazi_fs::FolderStage; +use yazi_macro::{act, render, render_and, succ}; +use yazi_parser::mgr::ExcludedOpt; +use yazi_shared::data::Data; + +use crate::{Actor, Ctx}; + +pub struct Excluded; + +impl Actor for Excluded { + type Options = ExcludedOpt; + + const NAME: &str = "excluded"; + + fn act(cx: &mut Ctx, opt: Self::Options) -> Result { + let state = opt.state.bool(cx.tab().current.files.show_excluded()); + let hovered = cx.hovered().map(|f| f.urn().to_owned()); + + let apply = |f: &mut Folder| { + if f.stage == FolderStage::Loading { + render!(); + false + } else { + f.files.set_show_excluded(state); + render_and!(f.files.catchup_revision()) + } + }; + + // Apply to CWD and parent + if let (a, Some(b)) = (apply(cx.current_mut()), cx.parent_mut().map(apply)) + && (a | b) + { + act!(mgr:hover, cx)?; + act!(mgr:update_paged, cx)?; + } + + // Apply to hovered + if let Some(h) = cx.hovered_folder_mut() + && apply(h) + { + render!(h.repos(None)); + act!(mgr:peek, cx, true)?; + } else if hovered.as_deref() != cx.hovered().map(|f| f.urn()) { + act!(mgr:peek, cx)?; + act!(mgr:watch, cx)?; + } + + succ!() + } +} diff --git a/yazi-actor/src/mgr/mod.rs b/yazi-actor/src/mgr/mod.rs index ddb3bce8..6fb6b65f 100644 --- a/yazi-actor/src/mgr/mod.rs +++ b/yazi-actor/src/mgr/mod.rs @@ -10,6 +10,7 @@ yazi_macro::mod_flat!( enter escape exclude_add + excluded filter filter_do find diff --git a/yazi-dds/src/spark/spark.rs b/yazi-dds/src/spark/spark.rs index 1a76412d..db6b3d38 100644 --- a/yazi-dds/src/spark/spark.rs +++ b/yazi-dds/src/spark/spark.rs @@ -24,6 +24,7 @@ pub enum Spark<'a> { EscapeSelect(yazi_parser::VoidOpt), EscapeVisual(yazi_parser::VoidOpt), ExcludeAdd(yazi_parser::mgr::ExcludeAddOpt), + Excluded(yazi_parser::mgr::ExcludedOpt), Filter(yazi_parser::mgr::FilterOpt), FilterDo(yazi_parser::mgr::FilterOpt), Find(yazi_parser::mgr::FindOpt), @@ -149,6 +150,7 @@ impl<'a> IntoLua for Spark<'a> { Self::EscapeSelect(b) => b.into_lua(lua), Self::EscapeVisual(b) => b.into_lua(lua), Self::ExcludeAdd(b) => b.into_lua(lua), + Self::Excluded(b) => b.into_lua(lua), Self::Filter(b) => b.into_lua(lua), Self::FilterDo(b) => b.into_lua(lua), Self::Find(b) => b.into_lua(lua), @@ -310,6 +312,7 @@ try_from_spark!(mgr::RevealOpt, mgr:reveal); try_from_spark!(mgr::SearchOpt, mgr:search, mgr:search_do); try_from_spark!(mgr::SeekOpt, mgr:seek); try_from_spark!(mgr::ShellOpt, mgr:shell); +try_from_spark!(mgr::ExcludedOpt, mgr:excluded); try_from_spark!(mgr::SortOpt, mgr:sort); try_from_spark!(mgr::SpotOpt, mgr:spot); try_from_spark!(mgr::TabCloseOpt, mgr:tab_close); diff --git a/yazi-fm/src/executor.rs b/yazi-fm/src/executor.rs index 15e97118..885dc375 100644 --- a/yazi-fm/src/executor.rs +++ b/yazi-fm/src/executor.rs @@ -111,6 +111,7 @@ impl<'a> Executor<'a> { on!(copy); on!(shell); on!(hidden); + on!(excluded); on!(ignore); on!(linemode); on!(search); diff --git a/yazi-fs/src/files.rs b/yazi-fs/src/files.rs index 53d8c85c..c277056c 100644 --- a/yazi-fs/src/files.rs +++ b/yazi-fs/src/files.rs @@ -19,6 +19,7 @@ pub struct Files { sorter: FilesSorter, filter: Option, show_hidden: bool, + show_excluded: bool, ignore_filter: Option, } @@ -261,12 +262,23 @@ impl Files { files.into_iter().partition(|f| { (f.is_hidden() && !self.show_hidden) || !filter.matches(f.urn()) - || self.ignore_filter.as_ref().is_some_and(|ig| ig.matches_url(&f.url)) + || (!self.show_excluded + && self.ignore_filter.as_ref().is_some_and(|ig| ig.matches_url(&f.url))) }) } else if let Some(ignore_filter) = &self.ignore_filter { - files - .into_iter() - .partition(|f| (f.is_hidden() && !self.show_hidden) || ignore_filter.matches_url(&f.url)) + if self.show_excluded { + // Show excluded files - only hide based on hidden status + if self.show_hidden { + (vec![], files.into_iter().collect()) + } else { + files.into_iter().partition(|f| f.is_hidden()) + } + } else { + // Hide excluded files - apply ignore filter + files + .into_iter() + .partition(|f| (f.is_hidden() && !self.show_hidden) || ignore_filter.matches_url(&f.url)) + } } else if self.show_hidden { (vec![], files.into_iter().collect()) } else { @@ -341,6 +353,26 @@ impl Files { true } + // --- Show excluded + #[inline] + pub fn show_excluded(&self) -> bool { self.show_excluded } + + pub fn set_show_excluded(&mut self, state: bool) -> bool { + if self.show_excluded == state { + return false; + } + + self.show_excluded = state; + + // Re-split files with the new state + let it = mem::take(&mut self.items).into_iter().chain(mem::take(&mut self.hidden)); + (self.hidden, self.items) = self.split_files(it); + self.sorter.sort(&mut self.items, &self.sizes); + self.revision += 1; + + true + } + // --- Show hidden pub fn set_show_hidden(&mut self, state: bool) { if self.show_hidden == state { diff --git a/yazi-parser/src/mgr/excluded.rs b/yazi-parser/src/mgr/excluded.rs new file mode 100644 index 00000000..5b784b89 --- /dev/null +++ b/yazi-parser/src/mgr/excluded.rs @@ -0,0 +1,56 @@ +use std::str::FromStr; + +use mlua::{ExternalError, FromLua, IntoLua, Lua, Value}; +use serde::{Deserialize, Serialize}; +use yazi_shared::event::CmdCow; + +#[derive(Debug, Default)] +pub struct ExcludedOpt { + pub state: ExcludedOptState, +} + +impl TryFrom for ExcludedOpt { + type Error = anyhow::Error; + + fn try_from(c: CmdCow) -> Result { + Ok(Self { state: c.str(0).parse().unwrap_or_default() }) + } +} + +impl FromLua for ExcludedOpt { + fn from_lua(_: Value, _: &Lua) -> mlua::Result { Err("unsupported".into_lua_err()) } +} + +impl IntoLua for ExcludedOpt { + fn into_lua(self, _: &Lua) -> mlua::Result { Err("unsupported".into_lua_err()) } +} + +// --- State +#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "kebab-case")] +pub enum ExcludedOptState { + #[default] + None, + Show, + Hide, + Toggle, +} + +impl FromStr for ExcludedOptState { + type Err = serde::de::value::Error; + + fn from_str(s: &str) -> Result { + Self::deserialize(serde::de::value::StrDeserializer::new(s)) + } +} + +impl ExcludedOptState { + pub fn bool(self, old: bool) -> bool { + match self { + Self::None => old, + Self::Show => true, + Self::Hide => false, + Self::Toggle => !old, + } + } +} diff --git a/yazi-parser/src/mgr/mod.rs b/yazi-parser/src/mgr/mod.rs index 7d309192..3157926b 100644 --- a/yazi-parser/src/mgr/mod.rs +++ b/yazi-parser/src/mgr/mod.rs @@ -6,6 +6,7 @@ yazi_macro::mod_flat!( download escape exclude_add + excluded filter find find_arrow