Repository: Canop/broot (language: rust)

Issue:
Show all folders in --whale-spotting mode
I've defined several folders as "no-enter", e.g. `~/.local`.

However, when launching broot in `--whale-spotting` mode, i would like to see all folders. I'm not aware of a way to do this and didn't find a flag for it.

Is this already possible and, if not, could this be added as an additional flag? Something like `--ignore-special-paths` (just an example, i hope you understand what i mean).


Discussion / hints:
I understand what you mean.

It will be hard to satisfy all needs.

Some folders are really not explorable, because they're *way* too slow.
Some other ones are just not entered because they're simply full of trash.

The real solution might be to add more nuances in the special paths directive, to have a _"no enter but of course you enter when it's for computing sizes"_ and a _"no enter, especially not for computing sizes"_.

> The real solution might be to add more nuances in the special paths directive, to have a _"no enter but of course you enter when it's for computing sizes"_ and a _"no enter, especially not for computing sizes"_.

That would be even better. Thank you for considering it!

Fix patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index bfed8635..2bd6d70a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,9 @@
+### next
+- new syntax for special paths - Fix #687, #669
+
 ### v1.32.0 - 2024-01-02
 <a name="v1.32.0"></a>
-- whith "modal" enabled, `initial_mode` setting lets you choose whether to start in `input` mode or `command` mode (default) - Fix #708
+- with "modal" enabled, `initial_mode` setting lets you choose whether to start in `input` mode or `command` mode (default) - Fix #708
 
 ### v1.31.0 - 2023-12-30
 <a name="v1.31.0"></a>
diff --git a/Cargo.lock b/Cargo.lock
index 0202dad1..23d24a66 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -217,7 +217,7 @@ checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a"
 
 [[package]]
 name = "broot"
-version = "1.32.0"
+version = "1.32.1-dev"
 dependencies = [
  "ahash 0.8.6",
  "ansi_colours",
@@ -261,7 +261,7 @@ dependencies = [
  "strict",
  "syntect-no-panic",
  "tempfile",
- "termimad 0.26.1",
+ "termimad 0.27.0",
  "terminal-clipboard",
  "terminal-light",
  "toml",
@@ -2144,9 +2144,9 @@ dependencies = [
 
 [[package]]
 name = "termimad"
-version = "0.26.1"
+version = "0.27.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7c3d19056c90eb12f23da61808c9f8c22b7d031bb5d50400b726ecf2d9718e73"
+checksum = "3d8fd6df3360f61fd890859b94d4a009c2e8dc2d50a6453ad3669fbb0df345f2"
 dependencies = [
  "coolor 0.8.0",
  "crossbeam",
diff --git a/Cargo.toml b/Cargo.toml
index c8b3aa2b..9a99454c 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "broot"
-version = "1.32.0"
+version = "1.32.1-dev"
 authors = ["dystroy <denys.seguret@gmail.com>"]
 repository = "https://github.com/Canop/broot"
 homepage = "https://dystroy.org/broot"
@@ -58,7 +58,7 @@ splitty = "1.0"
 strict = "0.1.4"
 syntect = { package = "syntect-no-panic", version = "4.6.1" } # see issue #485
 tempfile = "3.2"
-termimad = "0.26.1"
+termimad = "0.27.0"
 terminal-clipboard = { version = "0.4.1", optional = true }
 terminal-light = "1.1.1"
 toml = "0.8"
diff --git a/resources/default-conf/conf.hjson b/resources/default-conf/conf.hjson
index 67aa8772..c87523b8 100644
--- a/resources/default-conf/conf.hjson
+++ b/resources/default-conf/conf.hjson
@@ -99,12 +99,28 @@ show_selection_mark: true
 # Special paths
 # If some paths must be handled specially, uncomment (and change
 # this section as per the examples)
+# Setting "list":"never" on a dir prevents broot from looking at its 
+#  children when searching, unless the dir is the selected root.
+# Setting "sum":"never" on a dir prevents broot from looking at its
+#  children when computing the total size and count of files.
+# Setting "show":"always" makes a file visible even if its name
+#  starts with a dot.
+# Setting "list":"always" may be useful on a link to a directory
+#  (they're otherwise not entered by broot unless selected)
 #
 special_paths: {
-    "/media" : "no-enter" // comment it if desired
-    # "/media/slow-backup-disk"    : no-enter
-    # "/home/dys/useless"    : hide
-    # "/home/dys/my-link-I-want-to-explore"    : enter
+    "/media" : {
+        list: "never"
+        sum: "never"
+    }
+    "~/.config": { "show": "always" }
+    "trav": {
+        show: always
+        list: "always",
+        sum: "never"
+    }
+    # "~/useless": { "show": "never" }
+    # "~/my-link-I-want-to-explore": { "list": "always" }
 }
 
 ###############################################################
diff --git a/src/app/app_context.rs b/src/app/app_context.rs
index 2f813041..86a08872 100644
--- a/src/app/app_context.rs
+++ b/src/app/app_context.rs
@@ -8,8 +8,8 @@ use {
         errors::*,
         file_sum,
         icon::*,
-        path,
         pattern::SearchModeMap,
+        path::SpecialPaths,
         skin::ExtColorMap,
         syntactic::SyntaxTheme,
         tree::TreeOptions,
@@ -50,7 +50,7 @@ pub struct AppContext {
     pub verb_store: VerbStore,
 
     /// the paths for which there's a special behavior to follow (comes from conf)
-    pub special_paths: Vec<path::SpecialPath>,
+    pub special_paths: SpecialPaths,
 
     /// the map between search prefixes and the search mode to apply
     pub search_modes: SearchModeMap,
@@ -130,11 +130,8 @@ impl AppContext {
         };
         let icons = config.icon_theme.as_ref()
             .and_then(|itn| icon_plugin(itn));
-        let mut special_paths = config.special_paths
-            .iter()
-            .map(|(k, v)| path::SpecialPath::new(k.clone(), *v))
-            .collect();
-        path::add_defaults(&mut special_paths);
+        let mut special_paths: SpecialPaths = (&config.special_paths).try_into()?;
+        special_paths.add_defaults();
         let search_modes = config
             .search_modes
             .as_ref()
diff --git a/src/conf/conf.rs b/src/conf/conf.rs
index e68d8c7f..7abfeb6b 100644
--- a/src/conf/conf.rs
+++ b/src/conf/conf.rs
@@ -8,8 +8,6 @@ use {
         display::ColsConf,
         errors::{ConfError, ProgramError},
         path::{
-            Glob,
-            SpecialHandling,
             path_from,
             PathAnchor,
         },
@@ -21,6 +19,7 @@ use {
     crokey::crossterm::style::Attribute,
     fnv::FnvHashMap,
     serde::Deserialize,
+    std::collections::HashMap,
     std::path::PathBuf,
 };
 
@@ -59,7 +58,7 @@ pub struct Conf {
     pub skin: Option<AHashMap<String, SkinEntry>>,
 
     #[serde(default, alias="special-paths")]
-    pub special_paths: AHashMap<Glob, SpecialHandling>,
+    pub special_paths: HashMap<GlobConf, SpecialHandlingConf>,
 
     #[serde(alias="search-modes")]
     pub search_modes: Option<FnvHashMap<String, String>>,
diff --git a/src/conf/mod.rs b/src/conf/mod.rs
index c144e3b7..f34d2185 100644
--- a/src/conf/mod.rs
+++ b/src/conf/mod.rs
@@ -11,6 +11,7 @@ mod default_flags;
 mod format;
 pub mod file_size;
 mod import;
+mod special_handling_conf;
 mod verb_conf;
 
 pub use {
@@ -19,6 +20,7 @@ pub use {
     default_flags::*,
     format::*,
     import::*,
+    special_handling_conf::*,
     verb_conf::VerbConf,
 };
 
diff --git a/src/conf/special_handling_conf.rs b/src/conf/special_handling_conf.rs
new file mode 100644
index 00000000..350f8958
--- /dev/null
+++ b/src/conf/special_handling_conf.rs
@@ -0,0 +1,101 @@
+use {
+    crate::{
+        errors::ConfError,
+        path::*,
+    },
+    directories::UserDirs,
+    lazy_regex::*,
+    serde::Deserialize,
+    std::collections::HashMap,
+};
+
+
+type SpecialPathsConf = HashMap<GlobConf, SpecialHandlingConf>;
+
+#[derive(Clone, Debug, Deserialize, Hash, PartialEq, Eq)]
+#[serde(transparent)]
+pub struct GlobConf {
+    pub pattern: String,
+}
+
+#[derive(Clone, Copy, Debug, Deserialize)]
+#[serde(untagged)]
+pub enum SpecialHandlingConf {
+    Shortcut(SpecialHandlingShortcut),
+    Detailed(SpecialHandling),
+}
+
+#[derive(Clone, Debug, Copy, Deserialize, PartialEq, Eq)]
+#[serde(rename_all = "snake_case")]
+pub enum SpecialHandlingShortcut {
+    None,
+    Enter,
+    #[serde(alias = "no-enter")]
+    NoEnter,
+    Hide,
+    #[serde(alias = "no-hide")]
+    NoHide,
+}
+
+impl From<SpecialHandlingShortcut> for SpecialHandling {
+    fn from(shortcut: SpecialHandlingShortcut) -> Self {
+        use Directive::*;
+        match shortcut {
+            SpecialHandlingShortcut::None => SpecialHandling {
+                show: Default, list: Default, sum: Default,
+            },
+            SpecialHandlingShortcut::Enter => SpecialHandling {
+                show: Always, list: Always, sum: Always,
+            },
+            SpecialHandlingShortcut::NoEnter => SpecialHandling {
+                show: Default, list: Never, sum: Never,
+            },
+            SpecialHandlingShortcut::Hide => SpecialHandling {
+                show: Never, list: Default, sum: Never,
+            },
+            SpecialHandlingShortcut::NoHide => SpecialHandling {
+                show: Always, list: Default, sum: Default,
+            },
+        }
+    }
+}
+
+impl From<SpecialHandlingConf> for SpecialHandling {
+    fn from(conf: SpecialHandlingConf) -> Self {
+        match conf {
+            SpecialHandlingConf::Shortcut(shortcut) => shortcut.into(),
+            SpecialHandlingConf::Detailed(handling) => handling,
+        }
+    }
+}
+
+impl TryFrom<&SpecialPathsConf> for SpecialPaths {
+    type Error = ConfError;
+    fn try_from(map: &SpecialPathsConf) -> Result<Self, ConfError> {
+        let mut entries = Vec::new();
+        for (k, v) in map {
+            entries.push(SpecialPath::new(k.to_glob()?, (*v).into()));
+        }
+        Ok(Self { entries })
+    }
+}
+
+impl GlobConf {
+    pub fn to_glob(&self) -> Result<glob::Pattern, ConfError> {
+        let s = regex_replace!(r"^~(/|$)", &self.pattern, |_, sep| {
+            match UserDirs::new() {
+                Some(dirs) => format!("{}{}", dirs.home_dir().to_string_lossy(), sep),
+                None => "~/".to_string(),
+            }
+        });
+        let glob = if s.starts_with('/') || s.starts_with('~') {
+            glob::Pattern::new(&s)
+        } else {
+            let pattern = format!("**/{}", &s);
+            glob::Pattern::new(&pattern)
+        };
+        glob.map_err(|_| ConfError::InvalidGlobPattern {
+            pattern: self.pattern.to_string(),
+        })
+    }
+}
diff --git a/src/errors.rs b/src/errors.rs
index b5e35c60..8047fe69 100644
--- a/src/errors.rs
+++ b/src/errors.rs
@@ -87,6 +87,7 @@ custom_error! {pub ConfError
     InvalidThreadsCount { count: usize }            = "invalid threads count: {count}",
     InvalidDefaultFlags { flags: String }           = "invalid default flags: {flags:?}",
     InvalidSyntaxTheme { name: String }             = "invalid syntax theme: {name:?}",
+    InvalidGlobPattern { pattern: String }          = "invalid glob pattern: {pattern:?}",
 }
 
 // error which can be raised when parsing a pattern the user typed
diff --git a/src/file_sum/sum_computation.rs b/src/file_sum/sum_computation.rs
index 6d93a08b..a67ffb4a 100644
--- a/src/file_sum/sum_computation.rs
+++ b/src/file_sum/sum_computation.rs
@@ -43,14 +43,6 @@ struct NodeId {
     dev: u64,
 }
 
-#[inline(always)]
-fn is_ignored(path: &Path, special_paths: &[SpecialPath]) -> bool {
-    match special_paths.find(path) {
-        SpecialHandling::NoEnter | SpecialHandling::Hide => true,
-        SpecialHandling::None | SpecialHandling::Enter | SpecialHandling::NoHide => false,
-    }
-}
-
 impl DirSummer {
     pub fn new(thread_count: usize) -> Self {
         let thread_pool = ThreadPoolBuilder::new()
@@ -75,7 +67,7 @@ impl DirSummer {
     ) -> Option<FileSum> {
         let threads_count = self.thread_count;
 
-        if is_ignored(path, &con.special_paths) {
+        if con.special_paths.sum(path) == Directive::Never {
             return Some(FileSum::zero());
         }
 
@@ -102,10 +94,7 @@ impl DirSummer {
         // A None means there's nothing left and the thread may send its result and stop
         let (dirs_sender, dirs_receiver) = channel::unbounded();
 
-        let special_paths: Vec<SpecialPath> = con.special_paths.iter()
-            .filter(|sp| sp.can_have_matches_in(path))
-            .cloned()
-            .collect();
+        let special_paths = con.special_paths.reduce(path);
 
         // the first level is managed a little differently: we look at the cache
         // before adding. This enables faster computations in two cases:
@@ -117,7 +106,7 @@ impl DirSummer {
                     if md.is_dir() {
                         let entry_path = e.path();
 
-                        if is_ignored(&entry_path, &special_paths) {
+                        if con.special_paths.sum(&entry_path) == Directive::Never {
                             debug!("not summing special path {:?}", entry_path);
                             continue;
                         }
@@ -188,7 +177,7 @@ impl DirSummer {
 
                                         let path = e.path();
 
-                                        if is_ignored(&path, &special_paths) {
+                                        if special_paths.sum(&path) == Directive::Never {
                                             debug!("not summing (deep) special path {:?}", path);
                                             continue;
                                         }
diff --git a/src/path/special_path.rs b/src/path/special_path.rs
index cee517d2..f7a4e745 100644
--- a/src/path/special_path.rs
+++ b/src/path/special_path.rs
@@ -1,23 +1,32 @@
 use {
-    directories::UserDirs,
     glob,
-    lazy_regex::*,
-    serde::{de::Error, Deserialize, Deserializer},
-    std::path::{Path, PathBuf},
+    serde::Deserialize,
+    std::path::Path,
 };
 
-#[derive(Debug, Clone, PartialEq, Hash, Eq)]
-pub struct Glob {
-    pattern: glob::Pattern,
+///// Wrap a glob pattern to add the Deserialize trait
+//#[derive(Debug, Clone, PartialEq, Hash, Eq)]
+//pub struct Glob {
+//    pattern: glob::Pattern,
+//}
+
+#[derive(Clone, Copy, Debug, Deserialize, Default, PartialEq)]
+pub struct SpecialHandling {
+    #[serde(default)]
+    pub show: Directive,
+    #[serde(default)]
+    pub list: Directive,
+    #[serde(default)]
+    pub sum: Directive,
 }
 
-#[derive(Debug, Clone, Copy, PartialEq)]
-pub enum SpecialHandling {
-    None,
-    Enter,
-    NoEnter,
-    Hide,
-    NoHide,
+#[derive(Clone, Debug, Copy, Deserialize, PartialEq, Eq, Default)]
+#[serde(rename_all = "snake_case")]
+pub enum Directive {
+    #[default]
+    Default,
+    Never,
+    Always,
 }
 
 #[derive(Debug, Clone)]
@@ -26,52 +35,67 @@ pub struct SpecialPath {
     pub handling: SpecialHandling,
 }
 
-pub trait SpecialPathList {
-    fn find(self, path: &Path) -> SpecialHandling;
+#[derive(Debug, Clone)]
+pub struct SpecialPaths {
+    pub entries: Vec<SpecialPath>,
 }
 
-impl<'de> Deserialize<'de> for SpecialHandling {
-    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
-        where D: Deserializer<'de>
-    {
-        let s = String::deserialize(deserializer)?;
-        let s = s.to_lowercase();
-        // we remove non letters so to accept eg "no-enter"
-        let s = regex!(r"\W+").replace_all(&s, "");
-        match s.as_ref() {
-            "none" => Ok(SpecialHandling::None),
-            "enter" => Ok(SpecialHandling::Enter),
-            "noenter" => Ok(SpecialHandling::NoEnter), // noenter or no-enter
-            "hide" => Ok(SpecialHandling::Hide),
-            "nohide" => Ok(SpecialHandling::NoHide),   // nohide or no-hide
-            _ => Err(D::Error::custom(format!(
-                "unrecognized special handling: {s:?}"
-            ))),
-        }
+impl SpecialPaths {
+    pub fn find<P: AsRef<Path>>(&self, path: P) -> SpecialHandling {
+        self
+            .entries.iter()
+            .find(|sp| sp.pattern.matches_path(path.as_ref()))
+            .map(|sp| sp.handling)
+            .unwrap_or_default()
     }
-}
-
-impl<'de> Deserialize<'de> for Glob {
-    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
-        where D: Deserializer<'de>
-    {
-        let s = String::deserialize(deserializer)?;
-        let s = regex_replace!(r"^~(/|$)", &s, |_, sep| {
-            match UserDirs::new() {
-                Some(dirs) => format!("{}{}", dirs.home_dir().to_string_lossy(), sep),
-                None => "~/".to_string(),
+    pub fn show(&self, path: &Path) -> Directive {
+        self.find(path).show
+    }
+    pub fn list(&self, path: &Path) -> Directive {
+        self.find(path).list
+    }
+    pub fn sum(&self, path: &Path) -> Directive {
+        self.find(path).sum
+    }
+    /// Add a special handling, if none was previously defined for that path
+    pub fn add_default(&mut self, path: &str, handling: SpecialHandling) {
+        if self.find(path) != Default::default() {
+            return;
+        }
+        match glob::Pattern::new("/proc") {
+            Ok(pattern) => {
+                self.entries.push(SpecialPath { pattern, handling });
+            }
+            Err(e) => {
+                warn!("Invalid glob pattern: {path:?} : {e}");
             }
+        }
+    }
+    pub fn add_defaults(&mut self) {
+        // see https://github.com/Canop/broot/issues/639
+        self.add_default("/proc", SpecialHandling {
+            show: Directive::Default,
+            list: Directive::Never,
+            sum: Directive::Never,
         });
-        glob::Pattern::new(&s)
-            .map_err(|e| D::Error::custom(format!("invalid glob pattern {s:?} : {e:?}")))
-            .map(|pattern| Glob { pattern })
+    }
+    /// Return a potentially smaller set of special paths, reduced
+    /// to what can be in path
+    pub fn reduce(&self, path: &Path) -> Self {
+        let entries = self
+            .entries
+            .iter()
+            .filter(|sp| sp.can_have_matches_in(path))
+            .cloned()
+            .collect();
+        Self { entries }
     }
 }
 
 impl SpecialPath {
-    pub fn new(glob: Glob, handling: SpecialHandling) -> Self {
+    pub fn new(pattern: glob::Pattern, handling: SpecialHandling) -> Self {
         Self {
-            pattern: glob.pattern,
+            pattern,
             handling,
         }
     }
@@ -81,31 +105,4 @@ impl SpecialPath {
     }
 }
 
-impl SpecialPathList for &[SpecialPath] {
-    fn find(self, path: &Path) -> SpecialHandling {
-        for sp in self {
-            if sp.pattern.matches_path(path) {
-                return sp.handling;
-            }
-        }
-        SpecialHandling::None
-    }
-}
 
-/// Add a special handling, if none was previously defined for that path
-fn add_default(list: &mut Vec<SpecialPath>, path: &str, handling: SpecialHandling) {
-    if list.find(&PathBuf::from("/proc")) == SpecialHandling::None {
-        match glob::Pattern::new("/proc") {
-            Ok(pattern) => {
-                list.push(SpecialPath { pattern, handling });
-            }
-            Err(e) => {
-                warn!("Invalid glob pattern: {path:?} : {e}");
-            }
-        }
-    }
-}
-pub fn add_defaults(list: &mut Vec<SpecialPath>) {
-    // see https://github.com/Canop/broot/issues/639
-    add_default(list, "/proc", SpecialHandling::NoEnter);
-}
diff --git a/src/skin/help_mad_skin.rs b/src/skin/help_mad_skin.rs
index 3e2d38b9..eae2b0db 100644
--- a/src/skin/help_mad_skin.rs
+++ b/src/skin/help_mad_skin.rs
@@ -14,10 +14,10 @@ pub fn make_help_mad_skin(skin: &StyleMap) -> MadSkin {
     ms.code_block.compound_style = ms.inline_code.clone();
     ms.bold = skin.help_bold.clone();
     ms.italic = skin.help_italic.clone();
-    ms.table = LineStyle {
-        compound_style: skin.help_table_border.clone(),
-        align: Alignment::Center,
-    };
+    ms.table = LineStyle::new(
+        skin.help_table_border.clone(),
+        Alignment::Center,
+    );
     if let Some(c) = skin.help_headers.get_fg() {
         ms.set_headers_fg(c);
     }
diff --git a/src/skin/purpose_mad_skin.rs b/src/skin/purpose_mad_skin.rs
index 58ca86f0..fcebacf8 100644
--- a/src/skin/purpose_mad_skin.rs
+++ b/src/skin/purpose_mad_skin.rs
@@ -8,10 +8,10 @@ use {
 /// when there's no error
 pub fn make_purpose_mad_skin(skin: &StyleMap) -> MadSkin {
     MadSkin {
-        paragraph: LineStyle {
-            compound_style: skin.purpose_normal.clone(),
-            align: Alignment::Left,
-        },
+        paragraph: LineStyle::new(
+            skin.purpose_normal.clone(),
+            Alignment::Left,
+        ),
         italic: skin.purpose_italic.clone(),
         bold: skin.purpose_bold.clone(),
         ellipsis: skin.purpose_ellipsis.clone(),
diff --git a/src/skin/status_mad_skin.rs b/src/skin/status_mad_skin.rs
index b0732f71..6dc9fc74 100644
--- a/src/skin/status_mad_skin.rs
+++ b/src/skin/status_mad_skin.rs
@@ -14,10 +14,10 @@ pub struct StatusMadSkinSet {
 /// when there's no error
 fn make_normal_status_mad_skin(skin: &StyleMap) -> MadSkin {
     MadSkin {
-        paragraph: LineStyle {
-            compound_style: skin.status_normal.clone(),
-            align: Alignment::Left,
-        },
+        paragraph: LineStyle::new(
+            skin.status_normal.clone(),
+            Alignment::Left,
+        ),
         italic: skin.status_italic.clone(),
         bold: skin.status_bold.clone(),
         inline_code: skin.status_code.clone(),
@@ -30,10 +30,10 @@ fn make_normal_status_mad_skin(skin: &StyleMap) -> MadSkin {
 /// when there's a error
 fn make_error_status_mad_skin(skin: &StyleMap) -> MadSkin {
     MadSkin {
-        paragraph: LineStyle {
-            compound_style: skin.status_error.clone(),
-            align: Alignment::Left,
-        },
+        paragraph: LineStyle::new(
+            skin.status_error.clone(),
+            Alignment::Left,
+        ),
         ellipsis: skin.status_ellipsis.clone(),
         ..Default::default()
     }
diff --git a/src/tree_build/bline.rs b/src/tree_build/bline.rs
index 92ac29aa..d53a4b60 100644
--- a/src/tree_build/bline.rs
+++ b/src/tree_build/bline.rs
@@ -4,7 +4,7 @@ use {
         app::AppContext,
         errors::TreeBuildError,
         git::GitIgnoreChain,
-        path::{normalize_path, SpecialHandling},
+        path::{normalize_path, Directive, SpecialHandling},
         tree::*,
     },
     id_arena::Arena,
@@ -65,7 +65,7 @@ impl BLine {
                 score: 0,
                 nb_kept_children: 0,
                 git_ignore_chain,
-                special_handling: SpecialHandling::None,
+                special_handling: Default::default(),
             }))
         } else {
             Err(TreeBuildError::FileNotFound {
@@ -92,10 +92,10 @@ impl BLine {
     }
     /// tell whether we should list the children of the present line
     pub fn can_enter(&self) -> bool {
-        if self.file_type.is_dir() && self.special_handling != SpecialHandling::NoEnter {
+        if self.file_type.is_dir() && self.special_handling.list != Directive::Never {
             return true;
         }
-        if self.special_handling == SpecialHandling::Enter {
+        if self.special_handling.list == Directive::Always {
             // we must check we're a link to a directory
             if self.file_type.is_symlink() {
                 if let Ok(target) = fs::read_link(&self.path) {
diff --git a/src/tree_build/builder.rs b/src/tree_build/builder.rs
index 27b58cb4..50019024 100644
--- a/src/tree_build/builder.rs
+++ b/src/tree_build/builder.rs
@@ -9,7 +9,7 @@ use {
         errors::TreeBuildError,
         git::{GitIgnoreChain, GitIgnorer, LineStatusComputer},
         pattern::Candidate,
-        path::{SpecialHandling, SpecialPathList},
+        path::Directive,
         task_sync::ComputationResult,
         task_sync::Dam,
         tree::*,
@@ -128,13 +128,13 @@ impl<'c> TreeBuilder<'c> {
             return None;
         }
         let path = e.path();
+        let special_handling = self.con.special_paths.find(&path);
+        if special_handling.show == Directive::Never {
+            return None;
+        }
         if !self.options.show_hidden
             && name.as_bytes()[0] == b'.'
-            // if not matches any SpecialHandling::NoHide pattern
-            && !self.con.special_paths
-            .iter()
-            .filter(|sp| sp.handling == SpecialHandling::NoHide)
-            .any(|sp| sp.pattern.matches_path(&path))
+            && special_handling.show != Directive::Always
         {
             self.report.hidden_count += 1;
             return None;
@@ -193,17 +193,15 @@ impl<'c> TreeBuilder<'c> {
                 return None;
             }
         }
-        let special_handling = self.con.special_paths.find(&path);
-        if special_handling == SpecialHandling::Hide {
-            return None;
-        }
         if self.options.respect_git_ignore {
             let parent_chain = &self.blines[parent_id].git_ignore_chain;
             if !self
                 .git_ignorer
                 .accepts(parent_chain, &path, &name, file_type.is_dir())
             {
-                return None;
+                if special_handling.show != Directive::Always {
+                    return None;
+                }
             }
         };
         Some(BLine {
diff --git a/website/docs/conf_file.md b/website/docs/conf_file.md
index 1b451fef..0af8dbd6 100644
--- a/website/docs/conf_file.md
+++ b/website/docs/conf_file.md
@@ -100,27 +100,40 @@ Those flags can still be overridden at launch with the negating ones. For exampl
 
 # Special Paths
 
-You may map special paths to specific behaviors. You may especially want
+You may map special paths to specific behaviors. You may for example
 
-- to have some link to a directory to always automatically be handled as a normal directory
-- to exclude some path because it's on a slow device or non relevant
+- have some link to a directory to always automatically be handled as a normal directory
+- exclude some path because it's on a slow device or non relevant
+- have a file be visible even while it's normally ignored
+
+A special paths entry is defined by 3 parameters: `show`, `list`, and `sum`.
+Each of them can be `default`, `always`, or `never`.
 
 Example configuration:
 
 ```Hjson
 special_paths: {
-    "/media/slow-backup-disk"             : no-enter
-    "/home/dys/useless"                   : hide
-    "/home/dys/my-link-I-want-to-explore" : enter
-    "/home/dys/.config"                   : no-hide
+    "/media" : {
+        list: "never"
+        sum: "never"
+    }
+    "~/.config": { "show": "always" }
+    "trav": {
+        show: always
+        list: "always",
+        sum: "never"
+    }
+    "~/useless": { "show": "never" }
+    "~/my-link-I-want-to-explore": { "list": "always" }
 }
 ```
 ```TOML
 [special-paths]
-"/media/slow-backup-disk" = "no-enter"
-"/home/dys/useless" = "hide"
-"/home/dys/my-link-I-want-to-explore" = "enter"
-"/home/dys/.config" = "no-hide"
+"/media" = { list = "never", sum = "never" }
+"~/.config" = { show = "always" }
+"trav" = { show = "always", list = "always", sum = "never" }
+"~/useless" = { show = "never" }
+"~/my-link-I-want-to-explore" = { list = "always" }
 ```
 
 Be careful that those paths (globs, in fact) are checked a lot when broot builds trees and that defining a lot of paths will impact the overall speed.


Test patch: