diff --git a/man/page b/man/page index 71e79331..7ca858c6 100644 --- a/man/page +++ b/man/page @@ -37,6 +37,9 @@ Show folders and files alike \fB\-\-show\-root\-fs\fR Show filesystem info on top .TP +\fB\-\-root\-relative\-path\fR +Show the root path relative to the launch directory +.TP \fB\-g\fR, \fB\-\-show\-git\-info\fR Show git statuses on files and stats on repo .TP diff --git a/src/app/app_context.rs b/src/app/app_context.rs index be492280..b5d0cd5a 100644 --- a/src/app/app_context.rs +++ b/src/app/app_context.rs @@ -34,6 +34,9 @@ pub struct AppContext { /// Initial tree options pub initial_tree_options: TreeOptions, + /// Initial working directory. This is where `broot` is launched, not the root path! + pub initial_working_dir: PathBuf, + /// where's the config file we're using /// This vec can't be empty pub config_paths: Vec, @@ -159,6 +162,7 @@ impl AppContext { initial_root, initial_file, initial_tree_options, + initial_working_dir: std::env::current_dir()?, config_paths, launch_args, verb_store, diff --git a/src/app/panel_state.rs b/src/app/panel_state.rs index a7430716..73614084 100644 --- a/src/app/panel_state.rs +++ b/src/app/panel_state.rs @@ -390,6 +390,21 @@ pub trait PanelState { con, ) } + Internal::toggle_root_relative => { + self.with_new_options( + screen, + &|o| { + o.relative_root ^= true; + if o.relative_root { + "*displaying root relative to launch dir*" + } else { + "*displaying root as absolute path*" + } + }, + bang, + con, + ) + } Internal::toggle_git_ignore => { self.with_new_options( screen, diff --git a/src/browser/browser_state.rs b/src/browser/browser_state.rs index a6470d34..5abd3f90 100644 --- a/src/browser/browser_state.rs +++ b/src/browser/browser_state.rs @@ -708,7 +708,7 @@ impl PanelState for BrowserState { disc: &DisplayContext, ) -> Result<(), ProgramError> { let dp = DisplayableTree { - app_state: Some(disc.app_state), + display_context: Some(disc), tree: self.displayed_tree(), skin: &disc.panel_skin.styles, ext_colors: &disc.con.ext_colors, diff --git a/src/cli/args.rs b/src/cli/args.rs index 16ffb213..59d6124a 100644 --- a/src/cli/args.rs +++ b/src/cli/args.rs @@ -44,6 +44,10 @@ pub struct Args { #[arg(long)] pub show_root_fs: bool, + /// Show the root path relative to the launch directory. + #[arg(long)] + pub root_relative_path: bool, + /// Show git statuses on files and stats on repo #[arg(short='g', long)] pub show_git_info: bool, diff --git a/src/conf/conf.rs b/src/conf/conf.rs index e6b80c21..df46d66b 100644 --- a/src/conf/conf.rs +++ b/src/conf/conf.rs @@ -107,6 +107,9 @@ pub struct Conf { #[serde(alias="content-search-max-file-size", deserialize_with="file_size::deserialize", default)] pub content_search_max_file_size: Option, + + #[serde(alias="root-relative-path")] + pub root_relative_path: Option, } impl Conf { @@ -187,6 +190,7 @@ impl Conf { overwrite!(self, max_staged_count, conf); overwrite!(self, show_matching_characters_on_path_searches, conf); overwrite!(self, content_search_max_file_size, conf); + overwrite!(self, root_relative_path, conf); self.verbs.append(&mut conf.verbs); // the following maps are "additive": we can add entries from several // config files and they still make sense diff --git a/src/display/displayable_tree.rs b/src/display/displayable_tree.rs index b4eb9498..514b4a5d 100644 --- a/src/display/displayable_tree.rs +++ b/src/display/displayable_tree.rs @@ -1,3 +1,6 @@ +use crate::app::DisplayContext; +use crate::path::relativize_path; + use { super::{ cond_bg, @@ -9,7 +12,6 @@ use { SPACE_FILLING, BRANCH_FILLING, }, crate::{ - app::AppState, content_search::ContentMatch, errors::ProgramError, file_sum::FileSum, @@ -37,7 +39,7 @@ use { /// - a scrollbar may be drawn /// - the empty lines will be erased pub struct DisplayableTree<'a, 's, 't> { - pub app_state: Option<&'a AppState>, + pub display_context: Option<&'a DisplayContext<'a>>, pub tree: &'t Tree, pub skin: &'s StyleMap, pub area: termimad::Area, @@ -55,7 +57,7 @@ impl<'a, 's, 't> DisplayableTree<'a, 's, 't> { height: u16, ) -> DisplayableTree<'a, 's, 't> { DisplayableTree { - app_state: None, + display_context: None, tree, skin, ext_colors, @@ -413,8 +415,15 @@ impl<'a, 's, 't> DisplayableTree<'a, 's, 't> { )?; } } - let title = line.path.to_string_lossy(); - cw.queue_str(style, &title)?; + match self.display_context { + Some(context) if self.tree.options.relative_root => { + cw.queue_str(style, &relativize_path(&line.path, context.con)?)?; + }, + _ => { + cw.queue_str(style, &line.path.to_string_lossy())?; + }, + } + if self.in_app && !cw.is_full() { if let ComputationResult::Done(git_status) = &self.tree.git_status { let git_status_display = GitStatusDisplay::from( @@ -486,7 +495,7 @@ impl<'a, 's, 't> DisplayableTree<'a, 's, 't> { .options .cols_order .iter() - .filter(|col| col.is_visible(tree, self.app_state)) + .filter(|col| col.is_visible(tree, self.display_context.map(|con| con.app_state))) .cloned() .collect(); @@ -536,8 +545,8 @@ impl<'a, 's, 't> DisplayableTree<'a, 's, 't> { if visible_cols[0].needs_left_margin() { cw.queue_char(space_style, ' ')?; } - let staged = self.app_state - .map_or(false, |a| a.stage.contains(&line.path)); + let staged = self.display_context + .map_or(false, |a| a.app_state.stage.contains(&line.path)); for col in &visible_cols { let void_len = match col { diff --git a/src/path/mod.rs b/src/path/mod.rs index 5deba90b..5d34909e 100644 --- a/src/path/mod.rs +++ b/src/path/mod.rs @@ -4,6 +4,7 @@ mod closest; mod from; mod normalize; mod special_path; +mod relative; pub use { anchor::*, @@ -12,4 +13,5 @@ pub use { from::*, normalize::*, special_path::*, + relative::*, }; diff --git a/src/path/relative.rs b/src/path/relative.rs new file mode 100644 index 00000000..0afc9738 --- /dev/null +++ b/src/path/relative.rs @@ -0,0 +1,23 @@ +use std::io; +use std::path::Path; + +use crate::app::AppContext; + +pub fn relativize_path(path: &Path, con: &AppContext) -> io::Result { + let relative_path = match pathdiff::diff_paths(path, &con.initial_working_dir) { + None => { + return Err(io::Error::new( + io::ErrorKind::Other, + format!("Cannot relativize {path:?}"), // does this happen ? how ? + )); + } + Some(p) => p, + }; + Ok( + if relative_path.components().next().is_some() { + relative_path.to_string_lossy().to_string() + } else { + ".".to_string() + } + ) +} diff --git a/src/preview/dir_view.rs b/src/preview/dir_view.rs index 4fd79482..fd0cd117 100644 --- a/src/preview/dir_view.rs +++ b/src/preview/dir_view.rs @@ -67,7 +67,7 @@ impl DirView { self.page_height = Some(page_height); } let dp = DisplayableTree { - app_state: None, + display_context: None, tree: &self.tree, skin: &disc.panel_skin.styles, ext_colors: &disc.con.ext_colors, diff --git a/src/print.rs b/src/print.rs index 67754019..cbc13d5d 100644 --- a/src/print.rs +++ b/src/print.rs @@ -1,5 +1,7 @@ //! functions printing a tree or a path +use crate::path::relativize_path; + use { crate::{ app::*, @@ -10,11 +12,7 @@ use { tree::Tree, }, crokey::crossterm::tty::IsTty, - pathdiff, - std::{ - io::{self, stdout}, - path::Path, - }, + std::io::{self, stdout}, }; fn print_string(string: String, _con: &AppContext) -> io::Result { @@ -42,25 +40,6 @@ pub fn print_paths(sel_info: SelInfo, con: &AppContext) -> io::Result print_string(string, con) } -fn relativize_path(path: &Path, con: &AppContext) -> io::Result { - let relative_path = match pathdiff::diff_paths(path, &con.initial_root) { - None => { - return Err(io::Error::new( - io::ErrorKind::Other, - format!("Cannot relativize {path:?}"), // does this happen ? how ? - )); - } - Some(p) => p, - }; - Ok( - if relative_path.components().next().is_some() { - relative_path.to_string_lossy().to_string() - } else { - ".".to_string() - } - ) -} - pub fn print_relative_paths(sel_info: SelInfo, con: &AppContext) -> io::Result { let string = match sel_info { SelInfo::None => "".to_string(), diff --git a/src/tree/tree_options.rs b/src/tree/tree_options.rs index f838939d..4d41e09d 100644 --- a/src/tree/tree_options.rs +++ b/src/tree/tree_options.rs @@ -24,6 +24,8 @@ pub struct TreeOptions { pub show_device_id: bool, pub show_root_fs: bool, // show information relative to the fs of the root pub trim_root: bool, // whether to cut out direct children of root + /// Show root directory relative to the program's current working directory. + pub relative_root: bool, pub show_permissions: bool, // show classic rwx unix permissions (only on unix) pub respect_git_ignore: bool, // hide files as requested by .gitignore ? pub filter_by_git_status: bool, // only show files whose git status is not nul @@ -51,6 +53,7 @@ impl TreeOptions { show_device_id: self.show_device_id, show_root_fs: self.show_root_fs, trim_root: self.trim_root, + relative_root: self.relative_root, pattern: InputPattern::none(), date_time_format: self.date_time_format, sort: self.sort, @@ -85,7 +88,7 @@ impl TreeOptions { let conf_matches = Args::try_parse_from(vec!["broot", &flags_args]) .map_err(|_| ConfError::InvalidDefaultFlags { flags: default_flags.to_string() - })?; + })?; self.apply_launch_args(&conf_matches); } if let Some(b) = config.show_selection_mark { @@ -97,6 +100,9 @@ impl TreeOptions { if let Some(b) = config.show_matching_characters_on_path_searches { self.show_matching_characters_on_path_searches = b; } + if let Some(b) = config.root_relative_path { + self.relative_root = b; + } self.cols_order = config .cols_order .as_ref() @@ -183,6 +189,9 @@ impl TreeOptions { } else if cli_args.no_trim_root { self.trim_root = false; } + if cli_args.root_relative_path { + self.relative_root = true; + } } } @@ -199,6 +208,7 @@ impl Default for TreeOptions { show_device_id: false, show_root_fs: false, trim_root: false, + relative_root: false, show_permissions: false, respect_git_ignore: true, filter_by_git_status: false, diff --git a/src/verb/internal.rs b/src/verb/internal.rs index 33f905d1..baab0346 100644 --- a/src/verb/internal.rs +++ b/src/verb/internal.rs @@ -140,6 +140,7 @@ Internals! { toggle_git_file_info: "toggle display of git file information" false, toggle_git_status: "toggle showing only files relevant for git status" false, toggle_root_fs: "toggle showing filesystem info on top" false, + toggle_root_relative: "toggle displaying root path relative to launch dir" false, toggle_hidden: "toggle showing hidden files" false, toggle_perm: "toggle showing file permissions" false, toggle_sizes: "toggle showing sizes" false, diff --git a/src/verb/verb_store.rs b/src/verb/verb_store.rs index 3ff9c639..23934c35 100644 --- a/src/verb/verb_store.rs +++ b/src/verb/verb_store.rs @@ -290,6 +290,7 @@ impl VerbStore { self.add_internal(toggle_git_file_info).with_shortcut("gf"); self.add_internal(toggle_git_status).with_shortcut("gs"); self.add_internal(toggle_root_fs).with_shortcut("rfs"); + self.add_internal(toggle_root_relative); self.add_internal(toggle_hidden) .with_key(key!(alt-h)) .with_shortcut("h"); diff --git a/website/docs/conf_file.md b/website/docs/conf_file.md index 96efbe80..843e9985 100644 --- a/website/docs/conf_file.md +++ b/website/docs/conf_file.md @@ -307,3 +307,16 @@ show_matching_characters_on_path_searches = false which gives this: ![not shown](img/subpath-match-not-shown.png) + +## Show the root path relative to the launch directory + +If you'd prefer the root path (the top line in the tree display) to be +displayed relative to the directory `broot` is launched in (not the root +directory passed on the command line), you can use this option: + +```Hjson +root-relative-path: true +``` +```TOML +root_relative_path = true +``` diff --git a/website/docs/conf_verbs.md b/website/docs/conf_verbs.md index b4a2ea02..f3667f88 100644 --- a/website/docs/conf_verbs.md +++ b/website/docs/conf_verbs.md @@ -443,6 +443,7 @@ invocation | default key | default shortcut | behavior / details :toggle_stage | ctrlg | - | add or remove selection to staging area :toggle_staging_area | - | tsa | open/close the staging area panel :toggle_trim_root | - | - | toggle trimming of top level files in tree display +:toggle_root_relative | - | - | toggle display of root path relative to launch dir or absolute :unstage | - | - | remove selection from staging area :up_tree | - | - | focus the parent of the current root