Repository: Canop/broot (language: rust)

Issue:
ctrl-esc/shift-esc to go back to the first match.
<kbd>ctrl</kbd>-<kbd>esc</kbd>/<kbd>shift</kbd>-<kbd>esc</kbd> to go back to the first match.

Is this possible already?


Discussion / hints:
By first match, you mean the first in the list, or the best match ?
> By first match, you mean the first in the list, or the best match ?

First in the list, my initial location. Where I started br.

Fix patch:
diff --git a/Cargo.lock b/Cargo.lock
index 8df44323..8ebf2f44 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -2018,9 +2018,9 @@ dependencies = [
 
 [[package]]
 name = "splitty"
-version = "1.0.1"
+version = "1.0.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8dae68aa5bd5dc2d3a2137b0f6bcdd8255dce1983dc155fe0246572e179c9c3a"
+checksum = "2db70a1e6827e4d71c655b606caf1346862c38ae52ab4f58c32635e7c7aedd67"
 
 [[package]]
 name = "str-buf"
diff --git a/Cargo.toml b/Cargo.toml
index d0ed3860..0d3b3e62 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -36,7 +36,7 @@ custom_error = "1.6"
 deser-hjson = "2.2.3"
 directories = "4.0"
 file-size = "1.0.3"
-git2 = { version = "0.14", default-features = false }
+git2 = { version = "0.14", default-features = false } # waiting for a good pure-rust alternative
 glob = "0.3"
 id-arena = "2.2.1"
 image = "0.24"
@@ -54,7 +54,7 @@ rustc-hash = "1"
 secular = { version = "1.0", features = ["normalization", "bmp"] }
 serde = { version = "1.0", features = ["derive"] }
 smallvec = "1.11" # version 2 is still alpha
-splitty = "1.0"
+splitty = "1.0.2"
 strict = "0.1.4"
 syntect = { package = "syntect-no-panic", version = "4.6.1" } # see issue #485
 tempfile = "3.2"
diff --git a/src/app/app_context.rs b/src/app/app_context.rs
index 01ae46e6..b27e5370 100644
--- a/src/app/app_context.rs
+++ b/src/app/app_context.rs
@@ -259,6 +259,17 @@ impl AppContext {
     }
 }
 
+/// An unsafe implementation of Default, for tests only
+#[cfg(test)]
+impl Default for AppContext {
+    fn default() -> Self {
+        let mut config = Conf::default();
+        let verb_store = VerbStore::new(&mut config).unwrap();
+        let launch_args = parse_default_flags("").unwrap();
+        Self::from(launch_args, verb_store, &config).unwrap()
+    }
+}
+
 /// try to determine whether the terminal supports true
 /// colors. This doesn't work well, hence the use of an
 /// optional config setting.
diff --git a/src/app/panel_state.rs b/src/app/panel_state.rs
index 2c90a9a7..20d870b5 100644
--- a/src/app/panel_state.rs
+++ b/src/app/panel_state.rs
@@ -668,7 +668,7 @@ pub trait PanelState {
                     }
                 };
                 if let Some(pattern) = internal_exec.arg.as_ref() {
-                    let line = exec_builder.string(pattern);
+                    let line = exec_builder.string(pattern, con);
                     verb_write(con, &line)?;
                 } else {
                     let line = input_invocation
@@ -847,7 +847,7 @@ pub trait PanelState {
                 None
             },
         );
-        let sequence = exec_builder.sequence(&seq_ex.sequence, &cc.app.con.verb_store);
+        let sequence = exec_builder.sequence(&seq_ex.sequence, &cc.app.con.verb_store, cc.app.con);
         Ok(CmdResult::ExecuteSequence { sequence })
     }
 
@@ -1092,7 +1092,7 @@ pub trait PanelState {
         verb: &Verb,
         invocation: &VerbInvocation,
         sel_info: SelInfo<'_>,
-        _cc: &CmdContext,
+        cc: &CmdContext,
         app_state: &AppState,
     ) -> Status {
         if sel_info.count_paths() > 1 {
@@ -1114,6 +1114,7 @@ pub trait PanelState {
                     sel_info,
                     app_state,
                     invocation,
+                    cc.app.con,
                 ),
                 false,
             )
diff --git a/src/command/panel_input.rs b/src/command/panel_input.rs
index d08cd6a7..8c903f04 100644
--- a/src/command/panel_input.rs
+++ b/src/command/panel_input.rs
@@ -449,7 +449,8 @@ impl PanelInput {
                     app_state,
                 );
                 let verb_invocation = exec_builder.invocation_with_default(
-                    &invocation_parser.invocation_pattern
+                    &invocation_parser.invocation_pattern,
+                    con,
                 );
                 let mut parts = parts;
                 parts.verb_invocation = Some(verb_invocation);
diff --git a/src/terminal.rs b/src/terminal.rs
index 50a0d55c..a3971041 100644
--- a/src/terminal.rs
+++ b/src/terminal.rs
@@ -16,7 +16,7 @@ pub fn update_title(
     con: &AppContext,
 ) {
     if let Some(pattern) = &con.terminal_title_pattern {
-        set_title(w, pattern, app_state);
+        set_title(w, pattern, app_state, con);
     }
 }
 
@@ -24,12 +24,13 @@ fn set_title(
     w: &mut W,
     pattern: &ExecPattern,
     app_state: &AppState,
+    con: &AppContext,
 ) {
     let builder = ExecutionStringBuilder::without_invocation(
         SelInfo::from_path(&app_state.root),
         app_state,
     );
-    let title = builder.shell_exec_string(pattern);
+    let title = builder.shell_exec_string(pattern, con);
     set_title_str(w, &title)
 }
 
diff --git a/src/verb/execution_builder.rs b/src/verb/execution_builder.rs
index 26da9cf8..656267a5 100644
--- a/src/verb/execution_builder.rs
+++ b/src/verb/execution_builder.rs
@@ -95,9 +95,13 @@ impl<'b> ExecutionStringBuilder<'b> {
             }
         }
     }
-    fn get_raw_capture_replacement(&self, ec: &Captures<'_>) -> Option<String> {
+    fn get_raw_capture_replacement(
+        &self,
+        ec: &Captures<'_>,
+        con: &AppContext,
+    ) -> Option<String> {
         self.get_raw_replacement(|sel| {
-            self.get_raw_sel_capture_replacement(ec, sel)
+            self.get_raw_sel_capture_replacement(ec, sel, con)
         })
     }
     /// return the standard replacement (ie not one from the invocation)
@@ -105,10 +109,12 @@ impl<'b> ExecutionStringBuilder<'b> {
         &self,
         name: &str,
         sel: Option<Selection<'_>>,
+        con: &AppContext,
     ) -> Option<String> {
         debug!("repl name : {:?}", name);
         match name {
             "root" => Some(path_to_string(self.root)),
+            "initial-root" => Some(path_to_string(&con.initial_root)),
             "line" => sel.map(|s| s.line.to_string()),
             "file" => sel.map(|s| s.path)
                 .map(path_to_string),
@@ -188,9 +194,10 @@ impl<'b> ExecutionStringBuilder<'b> {
         &self,
         ec: &Captures<'_>,
         sel: Option<Selection<'_>>,
+        con: &AppContext,
     ) -> Option<String> {
         let name = ec.get(1).unwrap().as_str();
-        self.get_raw_sel_name_standard_replacement(name, sel)
+        self.get_raw_sel_name_standard_replacement(name, sel, con)
             .or_else(||{
                 // it's not one of the standard group names, so we'll look
                 // into the ones provided by the invocation pattern
@@ -216,8 +223,12 @@ impl<'b> ExecutionStringBuilder<'b> {
             })
     }
     #[inline]
-    fn get_capture_replacement(&self, ec: &Captures<'_>) -> String {
-        self.get_raw_capture_replacement(ec)
+    fn get_capture_replacement(
+        &self,
+        ec: &Captures<'_>,
+        con: &AppContext,
+    ) -> String {
+        self.get_raw_capture_replacement(ec, con)
             .unwrap_or_else(||
                 if self.keep_groups { ec[0].to_string() } else { "".to_string() }
             )
@@ -226,8 +237,9 @@ impl<'b> ExecutionStringBuilder<'b> {
         &self,
         ec: &Captures<'_>,
         sel: Option<Selection<'_>>,
+        con: &AppContext,
     ) -> String {
-        self.get_raw_sel_capture_replacement(ec, sel)
+        self.get_raw_sel_capture_replacement(ec, sel, con)
             .unwrap_or_else(||
                 if self.keep_groups { ec[0].to_string() } else { "".to_string() }
             )
@@ -239,6 +251,7 @@ impl<'b> ExecutionStringBuilder<'b> {
     pub fn invocation_with_default(
         &self,
         verb_invocation: &VerbInvocation,
+        con: &AppContext,
     ) -> VerbInvocation {
         VerbInvocation {
             name: verb_invocation.name.clone(),
@@ -250,7 +263,7 @@ impl<'b> ExecutionStringBuilder<'b> {
                             .map(|default_name| default_name.as_str())
                             .and_then(|default_name|
                                 self.get_raw_replacement(|sel|
-                                    self.get_raw_sel_name_standard_replacement(default_name, sel)
+                                    self.get_raw_sel_name_standard_replacement(default_name, sel, con)
                                 )
                             )
                             .unwrap_or_default()
@@ -279,6 +292,7 @@ impl<'b> ExecutionStringBuilder<'b> {
         &self,
         sequence: &Sequence,
         verb_store: &VerbStore,
+        con: &AppContext,
     ) -> Sequence {
         let mut inputs = Vec::new();
         for input in sequence.raw.split(&sequence.separator) {
@@ -297,9 +311,9 @@ impl<'b> ExecutionStringBuilder<'b> {
                 })
                 .map_or(false, |verb| verb.get_internal().is_none());
             let input = if verb_is_external {
-                self.shell_exec_string(&ExecPattern::from_string(input))
+                self.shell_exec_string(&ExecPattern::from_string(input), con)
             } else {
-                self.string(input)
+                self.string(input, con)
             };
             inputs.push(input);
         }
@@ -312,11 +326,12 @@ impl<'b> ExecutionStringBuilder<'b> {
     pub fn string(
         &self,
         pattern: &str,
+        con: &AppContext,
     ) -> String {
         GROUP
             .replace_all(
                 pattern,
-                |ec: &Captures<'_>| self.get_capture_replacement(ec),
+                |ec: &Captures<'_>| self.get_capture_replacement(ec, con),
             )
             .to_string()
     }
@@ -324,13 +339,14 @@ impl<'b> ExecutionStringBuilder<'b> {
     pub fn path(
         &self,
         pattern: &str,
+        con: &AppContext,
     ) -> PathBuf {
         path::path_from(
             self.base_dir(),
             path::PathAnchor::Unspecified,
             &GROUP.replace_all(
                 pattern,
-                |ec: &Captures<'_>| self.get_capture_replacement(ec),
+                |ec: &Captures<'_>| self.get_capture_replacement(ec, con),
             )
         )
     }
@@ -338,12 +354,13 @@ impl<'b> ExecutionStringBuilder<'b> {
     pub fn shell_exec_string(
         &self,
         exec_pattern: &ExecPattern,
+        con: &AppContext,
     ) -> String {
         exec_pattern
             .apply(&|s| {
                 GROUP.replace_all(
                     s,
-                    |ec: &Captures<'_>| self.get_capture_replacement(ec),
+                    |ec: &Captures<'_>| self.get_capture_replacement(ec, con),
                 ).to_string()
             })
             .fix_paths()
@@ -356,12 +373,13 @@ impl<'b> ExecutionStringBuilder<'b> {
         &self,
         exec_pattern: &ExecPattern,
         sel: Option<Selection<'_>>,
+        con: &AppContext,
     ) -> String {
         exec_pattern
             .apply(&|s| {
                 GROUP.replace_all(
                     s,
-                    |ec: &Captures<'_>| self.get_sel_capture_replacement(ec, sel),
+                    |ec: &Captures<'_>| self.get_sel_capture_replacement(ec, sel, con),
                 ).to_string()
             })
             .fix_paths()
@@ -372,12 +390,13 @@ impl<'b> ExecutionStringBuilder<'b> {
     pub fn exec_token(
         &self,
         exec_pattern: &ExecPattern,
+        con: &AppContext,
     ) -> Vec<String> {
         exec_pattern
             .apply(&|s| {
                 GROUP.replace_all(
                     s,
-                    |ec: &Captures<'_>| self.get_capture_replacement(ec),
+                    |ec: &Captures<'_>| self.get_capture_replacement(ec, con),
                 ).to_string()
             })
             .fix_paths()
@@ -389,12 +408,13 @@ impl<'b> ExecutionStringBuilder<'b> {
         &self,
         exec_pattern: &ExecPattern,
         sel: Option<Selection<'_>>,
+        con: &AppContext,
     ) -> Vec<String> {
         exec_pattern
             .apply(&|s| {
                 GROUP.replace_all(
                     s,
-                    |ec: &Captures<'_>| self.get_sel_capture_replacement(ec, sel),
+                    |ec: &Captures<'_>| self.get_sel_capture_replacement(ec, sel, con),
                 ).to_string()
             })
             .fix_paths()
@@ -451,8 +471,9 @@ mod execution_builder_test {
             map.insert(k.to_owned(), v.to_owned());
         }
         builder.invocation_values = Some(map);
+        let con = AppContext::default();
         for exec_pattern in exec_patterns {
-            let exec_token = builder.exec_token(&exec_pattern);
+            let exec_token = builder.exec_token(&exec_pattern, &con);
             assert_eq!(exec_token, chk_exec_token);
         }
     }
diff --git a/src/verb/external_execution.rs b/src/verb/external_execution.rs
index 57b78c94..b855e2c6 100644
--- a/src/verb/external_execution.rs
+++ b/src/verb/external_execution.rs
@@ -89,10 +89,11 @@ impl ExternalExecution {
     fn working_dir_path(
         &self,
         builder: &ExecutionStringBuilder<'_>,
+        con: &AppContext,
     ) -> Option<PathBuf> {
         self.working_dir
             .as_ref()
-            .map(|pattern| builder.path(pattern))
+            .map(|pattern| builder.path(pattern, con))
             .filter(|pb| {
                 if pb.exists() {
                     true
@@ -119,7 +120,7 @@ impl ExternalExecution {
             // Broot was probably launched as br.
             // the whole command is exported in the passed file
             let f = OpenOptions::new().append(true).open(export_path)?;
-            writeln!(&f, "{}", builder.shell_exec_string(&self.exec_pattern))?;
+            writeln!(&f, "{}", builder.shell_exec_string(&self.exec_pattern, con))?;
             Ok(CmdResult::Quit)
         } else {
             Ok(CmdResult::error(
@@ -141,8 +142,8 @@ impl ExternalExecution {
             ));
         }
         let launchable = Launchable::program(
-            builder.exec_token(&self.exec_pattern),
-            self.working_dir_path(&builder),
+            builder.exec_token(&self.exec_pattern, con),
+            self.working_dir_path(&builder, con),
             self.switch_terminal,
             con,
         )?;
@@ -157,12 +158,12 @@ impl ExternalExecution {
         builder: ExecutionStringBuilder<'_>,
         con: &AppContext,
     ) -> Result<CmdResult, ProgramError> {
-        let working_dir_path = self.working_dir_path(&builder);
+        let working_dir_path = self.working_dir_path(&builder, con);
         match &builder.sel_info {
             SelInfo::None | SelInfo::One(_) => {
                 // zero or one selection -> only one execution
                 let launchable = Launchable::program(
-                    builder.exec_token(&self.exec_pattern),
+                    builder.exec_token(&self.exec_pattern, con),
                     working_dir_path,
                     self.switch_terminal,
                     con,
@@ -184,7 +185,7 @@ impl ExternalExecution {
                     });
                 for sel in sels {
                     let launchable = Launchable::program(
-                        builder.sel_exec_token(&self.exec_pattern, Some(sel)),
+                        builder.sel_exec_token(&self.exec_pattern, Some(sel), con),
                         working_dir_path.clone(),
                         self.switch_terminal,
                         con,
diff --git a/src/verb/internal_focus.rs b/src/verb/internal_focus.rs
index 95b8862b..0d91b05e 100644
--- a/src/verb/internal_focus.rs
+++ b/src/verb/internal_focus.rs
@@ -103,6 +103,7 @@ fn path_from_input(
     base_path: &Path, // either the selected path or the root path
     input_arg: Option<&String>,
     app_state: &AppState,
+    con: &AppContext,
 ) -> PathBuf {
     match (input_arg, internal_exec.arg.as_ref()) {
         (Some(input_arg), Some(verb_arg)) => {
@@ -119,7 +120,7 @@ fn path_from_input(
                 app_state,
                 Some(input_arg),
             );
-            path_builder.path(verb_arg)
+            path_builder.path(verb_arg, con)
         }
         (Some(input_arg), None) => {
             // the verb defines nothing
@@ -142,7 +143,7 @@ fn path_from_input(
                 app_state,
                 None,
             );
-            path_builder.path(verb_arg)
+            path_builder.path(verb_arg, con)
         }
         (None, None) => {
             // user only wants to open the selected path, either in the same panel or
@@ -158,6 +159,7 @@ pub fn get_status_markdown(
     sel_info: SelInfo<'_>,
     invocation: &VerbInvocation,
     app_state: &AppState,
+    con: &AppContext,
 ) -> String {
     let base_path = sel_info
         .one_path()
@@ -168,6 +170,7 @@ pub fn get_status_markdown(
         base_path,
         invocation.args.as_ref(),
         app_state,
+        con,
     );
     format!("Hit *enter* to focus `{}`", path.to_string_lossy())
 }
@@ -198,6 +201,7 @@ pub fn on_internal(
                 selected_path,
                 input_arg,
                 app_state,
+                cc.app.con,
             );
             on_path(path, screen, tree_options, bang, con)
         }
@@ -206,10 +210,14 @@ pub fn on_internal(
             if let Some(arg) = &internal_exec.arg {
                 // the internal_execution specifies the path to use
                 // (it may come from a configured verb whose execution is
-                //  `:focus some/path`).
+                //  `:focus some/path` or `:focus {initial-root}̀).
                 // The given path may be relative hence the need for the
                 // state's selection
-                let path = path::path_from(selected_path, PathAnchor::Unspecified, arg);
+                let path_builder = ExecutionStringBuilder::without_invocation(
+                    SelInfo::from_path(selected_path),
+                    app_state,
+                );
+                let path = path_builder.path(arg, con);
                 let bang = input_invocation
                     .map(|inv| inv.bang)
                     .unwrap_or(internal_exec.bang);
diff --git a/src/verb/internal_select.rs b/src/verb/internal_select.rs
index 383e2815..947d3fa4 100644
--- a/src/verb/internal_select.rs
+++ b/src/verb/internal_select.rs
@@ -44,6 +44,7 @@ pub fn on_internal(
                 &tree.selected_line().path,
                 input_arg,
                 app_state,
+                cc.app.con,
             );
             on_path(path, tree, screen, bang)
         }
@@ -84,6 +85,7 @@ fn path_from_input(
     base_path: &Path, // either the selected path or the root path
     input_arg: Option<&String>,
     app_state: &AppState,
+    con: &AppContext,
 ) -> PathBuf {
     match (input_arg, internal_exec.arg.as_ref()) {
         (Some(input_arg), Some(verb_arg)) => {
@@ -100,7 +102,7 @@ fn path_from_input(
                 app_state,
                 Some(input_arg),
             );
-            path_builder.path(verb_arg)
+            path_builder.path(verb_arg, con)
         }
         (Some(input_arg), None) => {
             // the verb defines nothing
diff --git a/src/verb/verb.rs b/src/verb/verb.rs
index c3d6597c..10517061 100644
--- a/src/verb/verb.rs
+++ b/src/verb/verb.rs
@@ -230,6 +230,7 @@ impl Verb {
         sel_info: SelInfo<'_>,
         app_state: &AppState,
         invocation: &VerbInvocation,
+        con: &AppContext,
     ) -> String {
         let name = self.names.first().unwrap_or(&invocation.name);
 
@@ -246,6 +247,7 @@ impl Verb {
                     sel_info,
                     invocation,
                     app_state,
+                    con,
                 );
             }
         }
@@ -260,11 +262,12 @@ impl Verb {
         };
         if let VerbExecution::Sequence(seq_ex) = &self.execution {
             let exec_desc = builder().shell_exec_string(
-                &ExecPattern::from_string(&seq_ex.sequence.raw)
+                &ExecPattern::from_string(&seq_ex.sequence.raw),
+                con,
             );
             format!("Hit *enter* to **{}**: `{}`", name, &exec_desc)
         } else if let VerbExecution::External(external_exec) = &self.execution {
-            let exec_desc = builder().shell_exec_string(&external_exec.exec_pattern);
+            let exec_desc = builder().shell_exec_string(&external_exec.exec_pattern, con);
             format!("Hit *enter* to **{}**: `{}`", name, &exec_desc)
         } else if self.description.code {
             format!("Hit *enter* to **{}**: `{}`", name, &self.description.content)
diff --git a/website/docs/conf_verbs.md b/website/docs/conf_verbs.md
index c69dbe05..deb85a48 100644
--- a/website/docs/conf_verbs.md
+++ b/website/docs/conf_verbs.md
@@ -302,6 +302,7 @@ name | expanded to
 `{other-panel-parent}` | complete path of the current selection's parent in the other panel
 `{other-panel-directory}` | closest directory, either `{file}` or `{parent}` in the other panel
 `{root}` | current tree root (top of the displayed files tree)
+`{initial-root}` | tree root at launch
 `{git-root}` | The working directory of the Git repository containing the current selection
 `{git-name}` | Name of the working directory of the current Git repository
 `{file-git-relative}` | path of the current selection relative to the working directory of the containing Git repository. If the selection is not in a Git repository then the absolute path.


Test patch: