diff --git a/README.md b/README.md index 6e701038..d709c0f8 100644 --- a/README.md +++ b/README.md @@ -149,9 +149,11 @@ Add files to the [staging area](staging-area) then execute any command on all of If you want to display *sizes*, *dates* and *permissions*, do `br -sdp` which gets you this: -![replace ls](website/docs/img/20230930-sdp.png) +![replace ls](website/docs/img/20240501-sdp.png) -You may also toggle options with a few keystrokes while inside broot. For example hitting a space, a d then enter shows you the dates. Or hit alth and you see hidden files. +You may also toggle options with a few keystrokes while inside broot. +For example you could have typed this `-sdp` while in broot. +Or hit alth and you see hidden files. ## Sort, see what takes space: diff --git a/src/app/app.rs b/src/app/app.rs index b9a6cd9a..9ccde9e8 100644 --- a/src/app/app.rs +++ b/src/app/app.rs @@ -596,7 +596,7 @@ impl App { } if let Some(shared_root) = &mut self.shared_root { if let Ok(mut root) = shared_root.lock() { - *root = app_state.root.clone(); + root.clone_from(&app_state.root); } } } diff --git a/src/app/panel_state.rs b/src/app/panel_state.rs index fd00aef0..a593edf3 100644 --- a/src/app/panel_state.rs +++ b/src/app/panel_state.rs @@ -111,6 +111,25 @@ pub trait PanelState { .map(|inv| inv.bang) .unwrap_or(internal_exec.bang); Ok(match internal_exec.internal { + Internal::apply_flags => { + info!("applying flags input_invocation: {:#?}", input_invocation); + let flags = input_invocation.and_then(|inv| inv.args.as_ref()); + if let Some(flags) = flags { + self.with_new_options( + screen, + &|o| { + match o.apply_flags(flags) { + Ok(()) => "*flags applied*", + Err(e) => e, + } + }, + bang, + con, + ) + } else { + CmdResult::error(":apply_flags needs flags as arguments") + } + } Internal::back => CmdResult::PopState, Internal::copy_line | Internal::copy_path => { #[cfg(not(feature = "clipboard"))] @@ -986,6 +1005,7 @@ pub trait PanelState { ) } else { let sel_info = self.sel_info(app_state); + info!("invocation: {:#?}", invocation); match cc.app.con.verb_store.search_sel_info( &invocation.name, sel_info, diff --git a/src/cli/args.rs b/src/cli/args.rs index 3e9ccefd..177eef7b 100644 --- a/src/cli/args.rs +++ b/src/cli/args.rs @@ -128,6 +128,10 @@ pub struct Args { #[arg(short, long)] pub whale_spotting: bool, + /// No sort, no show hidden, no show git ignored + #[arg(short='W', long)] + pub no_whale_spotting: bool, + /// Trim the root too and don't show a scrollbar #[arg(short='t', long)] pub trim_root: bool, diff --git a/src/command/panel_input.rs b/src/command/panel_input.rs index ca310532..e7384938 100644 --- a/src/command/panel_input.rs +++ b/src/command/panel_input.rs @@ -377,6 +377,8 @@ impl PanelInput { let raw = self.input_field.get_content(); let parts = CommandParts::from(raw.clone()); + info!("parts: {:#?}", parts); + let verb = if self.is_key_allowed_for_verb(key, mode) { self.find_key_verb( key, diff --git a/src/errors.rs b/src/errors.rs index 8047fe69..86e0d880 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -34,7 +34,7 @@ custom_error! {pub ProgramError ZeroLenFile = "File seems empty", } -custom_error!{pub ShellInstallError +custom_error! {pub ShellInstallError Io {source: io::Error, when: String} = "IO Error {source} on {when}", } impl ShellInstallError { @@ -43,19 +43,25 @@ impl ShellInstallError { Self::Io { source, .. } => { if source.kind() == io::ErrorKind::PermissionDenied { true - } else { cfg!(windows) && source.raw_os_error().unwrap_or(0) == 1314 } + } else { + cfg!(windows) && source.raw_os_error().unwrap_or(0) == 1314 + } } } } } pub trait IoToShellInstallError { - fn context(self, f: &dyn Fn() -> String) -> Result; + fn context( + self, + f: &dyn Fn() -> String, + ) -> Result; } impl IoToShellInstallError for Result { - fn context(self, f: &dyn Fn() -> String) -> Result { - self.map_err(|source| ShellInstallError::Io { - source, when: f() - }) + fn context( + self, + f: &dyn Fn() -> String, + ) -> Result { + self.map_err(|source| ShellInstallError::Io { source, when: f() }) } } @@ -88,6 +94,7 @@ custom_error! {pub ConfError InvalidDefaultFlags { flags: String } = "invalid default flags: {flags:?}", InvalidSyntaxTheme { name: String } = "invalid syntax theme: {name:?}", InvalidGlobPattern { pattern: String } = "invalid glob pattern: {pattern:?}", + InvalidVerbName { name: String } = "invalid verb name: {name:?} (must either not start with a special character or be only made of special characters)", } // error which can be raised when parsing a pattern the user typed diff --git a/src/filesystems/mod.rs b/src/filesystems/mod.rs index 9708451d..6ddbbc8e 100644 --- a/src/filesystems/mod.rs +++ b/src/filesystems/mod.rs @@ -15,7 +15,7 @@ use { std::sync::Mutex, }; -pub static MOUNTS: Lazy> = Lazy::new(|| Mutex::new(MountList::new())); +pub static MOUNTS: Lazy> = Lazy::new(|| Mutex::new(MountList::default())); pub fn clear_cache() { let mut mount_list = MOUNTS.lock().unwrap(); diff --git a/src/filesystems/mount_list.rs b/src/filesystems/mount_list.rs index 03e044e4..8963b2fa 100644 --- a/src/filesystems/mount_list.rs +++ b/src/filesystems/mount_list.rs @@ -11,14 +11,12 @@ use { }, }; +#[derive(Default)] pub struct MountList { mounts: Option>, } impl MountList { - pub const fn new() -> Self { - Self { mounts: None } - } pub fn clear_cache(&mut self) { self.mounts = None; } diff --git a/src/git/status.rs b/src/git/status.rs index 0e9e8667..04bc415a 100644 --- a/src/git/status.rs +++ b/src/git/status.rs @@ -59,7 +59,6 @@ impl LineStatusComputer { } } -/// #[derive(Debug, Clone)] pub struct TreeGitStatus { pub current_branch_name: Option, diff --git a/src/tree/tree_options.rs b/src/tree/tree_options.rs index 14911608..36f95f76 100644 --- a/src/tree/tree_options.rs +++ b/src/tree/tree_options.rs @@ -7,6 +7,8 @@ use { errors::ConfError, pattern::*, }, + clap::Parser, + lazy_regex::regex_is_match, std::convert::TryFrom, }; @@ -99,6 +101,21 @@ impl TreeOptions { .unwrap_or(DEFAULT_COLS); Ok(()) } + /// apply flags like "sdp" + pub fn apply_flags(&mut self, flags: &str) -> Result<(), &'static str> { + if !regex_is_match!("^[a-zA-Z]+$", flags) { + return Err("Flags must be a sequence of letters"); + } + let prefixed = format!("-{flags}"); + let tokens = vec!["broot", &prefixed]; + let args = Args::try_parse_from(tokens) + .map_err(|_| { + warn!("invalid flags: {:?}", flags); + "invalid flag (valid flags are -dDfFgGhHiIpPsSwWtT)" + })?; + self.apply_launch_args(&args); + Ok(()) + } /// change tree options according to broot launch arguments pub fn apply_launch_args(&mut self, cli_args: &Args) { if cli_args.sizes { @@ -114,6 +131,13 @@ impl TreeOptions { self.show_sizes = true; self.show_root_fs = true; } + if cli_args.no_whale_spotting { + self.show_hidden = false; + self.respect_git_ignore = true; + self.sort = Sort::None; + self.show_sizes = false; + self.show_root_fs = false; + } if cli_args.only_folders { self.only_folders = true; } else if cli_args.no_only_folders { diff --git a/src/tree_build/builder.rs b/src/tree_build/builder.rs index a153ae08..d1c85b21 100644 --- a/src/tree_build/builder.rs +++ b/src/tree_build/builder.rs @@ -486,7 +486,6 @@ impl<'c> TreeBuilder<'c> { }) } - /// pub fn build_paths( mut self, total_search: bool, diff --git a/src/verb/internal.rs b/src/verb/internal.rs index 744bcd9e..47231163 100644 --- a/src/verb/internal.rs +++ b/src/verb/internal.rs @@ -53,7 +53,9 @@ macro_rules! Internals { // internals: // name: "description" needs_a_path Internals! { + apply_flags: "apply flags (eg `-sd` to show sizes and dates)" false, back: "revert to the previous state (mapped to *esc*)" false, + clear_output: "clear the --verb-output file" false, clear_stage: "empty the staging area" false, close_panel_cancel: "close the panel, not using the selected path" false, close_panel_ok: "close the panel, validating the selected path" false, @@ -150,13 +152,13 @@ Internals! { unstage: "remove selection from staging area" true, up_tree: "focus the parent of the current root" true, write_output: "write the argument to the --verb-output file" false, - clear_output: "clear the --verb-output file" false, //restore_pattern: "restore a pattern which was just removed" false, } impl Internal { pub fn invocation_pattern(self) -> &'static str { match self { + Internal::apply_flags => r"-(?P\w+)?", Internal::focus => r"focus (?P.*)?", Internal::select => r"select (?P.*)?", Internal::line_down => r"line_down (?P\d*)?", @@ -170,6 +172,7 @@ impl Internal { } pub fn exec_pattern(self) -> &'static str { match self { + Internal::apply_flags => r"apply_flags {flags}", Internal::focus => r"focus {path}", Internal::line_down => r"line_down {count}", Internal::line_up => r"line_up {count}", diff --git a/src/verb/verb.rs b/src/verb/verb.rs index b3161613..fa673899 100644 --- a/src/verb/verb.rs +++ b/src/verb/verb.rs @@ -95,7 +95,9 @@ impl Verb { let invocation_parser = invocation_str.map(InvocationParser::new).transpose()?; let mut names = Vec::new(); if let Some(ref invocation_parser) = invocation_parser { - names.push(invocation_parser.name().to_string()); + let name = invocation_parser.name().to_string(); + check_verb_name(&name)?; + names.push(name); } let ( needs_selection, @@ -143,6 +145,11 @@ impl Verb { self.show_in_doc = false; self } + pub fn with_name(&mut self, name: &str) -> Result<&mut Self, ConfError> { + check_verb_name(name)?; + self.names.insert(0, name.to_string()); + Ok(self) + } pub fn with_description(&mut self, description: &str) -> &mut Self { self.description = VerbDescription::from_text(description.to_string()); self @@ -300,3 +307,11 @@ impl Verb { } } } + +pub fn check_verb_name(name: &str) -> Result<(), ConfError> { + if regex_is_match!(r"^([@,#~&'%$\dù_-]+|[\w][\w_@,#~&'%$\dù_-]*)+$", name) { + Ok(()) + } else { + Err(ConfError::InvalidVerbName{ name: name.to_string() }) + } +} diff --git a/src/verb/verb_invocation.rs b/src/verb/verb_invocation.rs index 26b2ea39..3831787f 100644 --- a/src/verb/verb_invocation.rs +++ b/src/verb/verb_invocation.rs @@ -1,6 +1,5 @@ use { std::fmt, - lazy_regex::regex, }; /// the verb and its arguments, making the invocation. @@ -63,34 +62,80 @@ impl VerbInvocation { } impl From<&str> for VerbInvocation { - /// parse a string being or describing the invocation of a verb with its + /// Parse a string being or describing the invocation of a verb with its /// arguments and optional bang. The leading space or colon must /// have been stripped before. + /// + /// Examples: + /// "mv" -> name: "mv" + /// "!mv" -> name: "mv", bang + /// "mv a b" -> name: "mv", args: "a b" + /// "mv!a b" -> name: "mv", args: "a b", bang + /// "a-b c" -> name: "a-b", args: "c", bang + /// "-sp" -> name: "-", args: "sp" + /// "-a b" -> name: "-", args: "a b" + /// "-a b" -> name: "-", args: "a b" + /// "--a" -> name: "--", args: "a" + /// + /// Notes: + /// 1. A name is either "special" (only made of non alpha characters) + /// or normal (starting with an alpha character). Special names don't + /// need a space afterwards, as the first alpha character will start + /// the args. + /// 2. The space or colon after the name is optional if there's a bang + /// after the name: the bang is the separator. + /// 3. Duplicate separators before args are ignored (they're usually typos) + /// 4. An opening parenthesis starts args fn from(invocation: &str) -> Self { - let caps = regex!( - r"(?x) - ^ - (?P!)? - (?P[^!\s]*) - (?P!(?P[^\s:]+)?)? - (?:[\s:]+(?P.*))? - \s* - $ - " - ) - .captures(invocation) - .unwrap(); - let bang_before = caps.name("bang_before").is_some(); - let bang_after = caps.name("bang_after").is_some(); - let bang = bang_before || bang_after; - if let Some(post_bang) = caps.name("post_bang") { - // If there's a non space character just after the "bang_after" - // (a bang which isn't the first character of the invocation) - // it falls into a kind of void, having no meaning. - info!("ignored post_bang: {:?}", post_bang); + let mut bang_before = false; + let mut name = String::new(); + let mut bang_after = false; + let mut args: Option = None; + let mut name_is_special = false; + for c in invocation.chars() { + if let Some(args) = args.as_mut() { + if args.is_empty() && (c == ' ' || c == ':') { + // we don't want args starting with a space just because + // they're doubled or are optional after a special name + } else { + args.push(c); + } + continue; + } + if c == ' ' || c == ':' { + args = Some(String::new()); + continue; + } + if c == '(' { + args = Some(c.to_string()); + continue; + } + if c == '!' { + if !name.is_empty() { + bang_after = true; + args = Some(String::new()); + } else { + bang_before = true; + } + continue; + } + if name.is_empty() { + if c.is_alphabetic() { + name.push(c); + } else { + name.push(c); + name_is_special = true; + } + continue; + } + if c.is_alphabetic() && name_is_special { + // this isn't part of the name anymore, it's part of the args + args = Some(c.to_string()); + continue; + } + name.push(c); } - let name = caps.name("name").unwrap().as_str().to_string(); - let args = caps.name("args").map(|c| c.as_str().to_string()); + let bang = bang_before || bang_after; VerbInvocation { name, args, bang } } } @@ -99,6 +144,54 @@ impl From<&str> for VerbInvocation { mod verb_invocation_tests { use super::*; + #[test] + fn check_special_chars() { + assert_eq!( + VerbInvocation::from("-sdp"), + VerbInvocation::new("-", Some("sdp"), false), + ); + assert_eq!( + VerbInvocation::from("!-sdp"), + VerbInvocation::new("-", Some("sdp"), true), + ); + assert_eq!( + VerbInvocation::from("-!sdp"), + VerbInvocation::new("-", Some("sdp"), true), + ); + assert_eq!( + VerbInvocation::from("-! sdp"), + VerbInvocation::new("-", Some("sdp"), true), + ); + assert_eq!( + VerbInvocation::from("!@a b"), + VerbInvocation::new("@", Some("a b"), true), + ); + assert_eq!( + VerbInvocation::from("!@%a b"), + VerbInvocation::new("@%", Some("a b"), true), + ); + assert_eq!( + VerbInvocation::from("22a b"), + VerbInvocation::new("22", Some("a b"), false), + ); + assert_eq!( + VerbInvocation::from("22!a b"), + VerbInvocation::new("22", Some("a b"), true), + ); + assert_eq!( + VerbInvocation::from("22 !a b"), + VerbInvocation::new("22", Some("!a b"), false), + ); + assert_eq!( + VerbInvocation::from("a$b4!r"), + VerbInvocation::new("a$b4", Some("r"), true), + ); + assert_eq!( + VerbInvocation::from("a-b c"), + VerbInvocation::new("a-b", Some("c"), false), + ); + } + #[test] fn check_verb_invocation_parsing_empty_arg() { // those tests focus mainly on the distinction between @@ -110,7 +203,7 @@ mod verb_invocation_tests { ); assert_eq!( VerbInvocation::from("mva!"), - VerbInvocation::new("mva", None, true), + VerbInvocation::new("mva", Some(""), true), ); assert_eq!( VerbInvocation::from("cp "), @@ -127,7 +220,7 @@ mod verb_invocation_tests { // ignoring post_bang (see issue #326) assert_eq!( VerbInvocation::from("mva!a"), - VerbInvocation::new("mva", None, true), + VerbInvocation::new("mva", Some("a"), true), ); assert_eq!( VerbInvocation::from("!!!"), @@ -148,14 +241,6 @@ mod verb_invocation_tests { VerbInvocation::from("!"), VerbInvocation::new("", None, true), ); - assert_eq!( - VerbInvocation::from("!!"), - VerbInvocation::new("", None, true), - ); - assert_eq!( - VerbInvocation::from("!!a"), // case of post_bang - VerbInvocation::new("", None, true), - ); assert_eq!( VerbInvocation::from("!! "), VerbInvocation::new("", Some(""), true), @@ -169,6 +254,14 @@ mod verb_invocation_tests { #[test] fn check_verb_invocation_parsing_oddities() { // checking some corner cases + assert_eq!( + VerbInvocation::from("!!a"), // the second bang is ignored + VerbInvocation::new("a", None, true), + ); + assert_eq!( + VerbInvocation::from("!!"), // the second bang is ignored + VerbInvocation::new("", None, true), + ); assert_eq!( VerbInvocation::from("a ! !"), VerbInvocation::new("a", Some("! !"), false), diff --git a/src/verb/verb_store.rs b/src/verb/verb_store.rs index a599c8f7..e6019126 100644 --- a/src/verb/verb_store.rs +++ b/src/verb/verb_store.rs @@ -50,13 +50,13 @@ impl VerbStore { } } } - store.add_builtin_verbs(); // at the end so that we can override them + store.add_builtin_verbs()?; // at the end so that we can override them Ok(store) } fn add_builtin_verbs( &mut self, - ) { + ) -> Result<(), ConfError> { use super::{ExternalExecutionMode::*, Internal::*}; self.add_internal(escape).with_key(key!(esc)); @@ -80,7 +80,9 @@ impl VerbStore { self.add_internal(line_down).with_key(key!(down)).with_key(key!('j')); self.add_internal(line_up).with_key(key!(up)).with_key(key!('k')); + // changing display self.add_internal(set_syntax_theme); + self.add_internal(apply_flags).with_name("apply_flags")?; // those two operations are mapped on ALT-ENTER, one // for directories and the other one for the other files @@ -315,6 +317,7 @@ impl VerbStore { self.add_internal(clear_output); self.add_internal(write_output); + Ok(()) } fn build_add_internal( @@ -483,7 +486,7 @@ impl VerbStore { verb.auto_exec = false; } if !vc.panels.is_empty() { - verb.panels = vc.panels.clone(); + verb.panels.clone_from(&vc.panels); } verb.selection_condition = vc.apply_to; Ok(()) @@ -597,3 +600,9 @@ impl VerbStore { } } + +#[test] +fn check_builtin_verbs() { + let mut conf = Conf::default(); + let _store = VerbStore::new(&mut conf).unwrap(); +} diff --git a/website/docs/img/20240501-sdp.png b/website/docs/img/20240501-sdp.png new file mode 100644 index 00000000..dede6255 Binary files /dev/null and b/website/docs/img/20240501-sdp.png differ diff --git a/website/docs/index.md b/website/docs/index.md index f718db71..8e3e1645 100644 --- a/website/docs/index.md +++ b/website/docs/index.md @@ -131,9 +131,11 @@ Add files to the [staging area](staging-area) then execute any command on all of If you want to display *sizes*, *dates* and *permissions*, do `br -sdp` which gets you this: -![replace ls](img/20230930-sdp.png) +![replace ls](img/20240501-sdp.png) -You may also toggle options with a few keystrokes while inside broot. For example hitting a space, a d then enter shows you the dates. Or hit alth and you see hidden files. +You may also toggle options with a few keystrokes while inside broot. +For example you could have typed this `-sdp` while in broot. +Or hit alth and you see hidden files. # See what takes space: