summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDenys Séguret <cano.petrole@gmail.com>2024-05-04 14:24:01 +0200
committerGitHub <noreply@github.com>2024-05-04 14:24:01 +0200
commit3c3f4e62dd9a3252f7dababe01aef3b8d5f5e4d0 (patch)
tree9a9a1b6ac4494b2a3a4ee8ae0b089b1937e1a754
parentf0db2f4797d91e86aa148ea9fc195d87601af211 (diff)
make it possible to use flags like '-sdp' as verb (#874)
* make it possible to use flags like '-sdp' as verb * Modify verbinvocation syntax parser * introduce the dash-flags way * check verb names verify new naming constraints
-rw-r--r--README.md6
-rw-r--r--src/app/app.rs2
-rw-r--r--src/app/panel_state.rs20
-rw-r--r--src/cli/args.rs4
-rw-r--r--src/command/panel_input.rs2
-rw-r--r--src/errors.rs21
-rw-r--r--src/filesystems/mod.rs2
-rw-r--r--src/filesystems/mount_list.rs4
-rw-r--r--src/git/status.rs1
-rw-r--r--src/tree/tree_options.rs24
-rw-r--r--src/tree_build/builder.rs1
-rw-r--r--src/verb/internal.rs5
-rw-r--r--src/verb/verb.rs17
-rw-r--r--src/verb/verb_invocation.rs163
-rw-r--r--src/verb/verb_store.rs15
-rw-r--r--website/docs/img/20240501-sdp.pngbin0 -> 139216 bytes
-rw-r--r--website/docs/index.md6
17 files changed, 235 insertions, 58 deletions
diff --git a/README.md b/README.md
index 6e70103..d709c0f 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 <kbd>d</kbd> then <kbd>enter</kbd> shows you the dates. Or hit <kbd>alt</kbd><kbd>h</kbd> 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 <kbd>alt</kbd><kbd>h</kbd> and you see hidden files.
## Sort, see what takes space:
diff --git a/src/app/app.rs b/src/app/app.rs
index b9a6cd9..9ccde9e 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 fd00aef..a593edf 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 3e9ccef..177eef7 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 ca31053..e738493 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 8047fe6..86e0d88 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<Ok> {
- fn context(self, f: &dyn Fn() -> String) -> Result<Ok, ShellInstallError>;
+ fn context(
+ self,
+ f: &dyn Fn() -> String,
+ ) -> Result<Ok, ShellInstallError>;
}
impl<Ok> IoToShellInstallError<Ok> for Result<Ok, io::Error> {
- fn context(self, f: &dyn Fn() -> String) -> Result<Ok, ShellInstallError> {
- self.map_err(|source| ShellInstallError::Io {
- source, when: f()
- })
+ fn context(
+ self,
+ f: &dyn Fn() -> String,
+ ) -> Result<Ok, ShellInstallError> {
+ 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 9708451..6ddbbc8 100644
--- a/src/filesystems/mod.rs
+++ b/src/filesystems/mod.rs
@@ -15,7 +15,7 @@ use {
std::sync::Mutex,
};
-pub static MOUNTS: Lazy<Mutex<MountList>> = Lazy::new(|| Mutex::new(MountList::new()));
+pub static MOUNTS: Lazy<Mutex<MountList>> = 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 03e044e..8963b2f 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<Vec<Mount>>,
}
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 0e9e866..04bc415 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<String>,
diff --git a/src/tree/tree_options.rs b/src/tree/tree_options.rs
index 1491160..36f95f7 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 a153ae0..d1c85b2 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<F>(
mut self,
total_search: bool,
diff --git a/src/verb/internal.rs b/src/verb/internal.rs
index 744bcd9..4723116 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<flags>\w+)?",
Internal::focus => r"focus (?P<path>.*)?",
Internal::select => r"select (?P<path>.*)?",
Internal::line_down => r"line_down (?P<count>\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 b316161..fa67389 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 26b2ea3..3831787 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<bang_before>!)?
- (?P<name>[^!\s]*)
- (?P<bang_after>!(?P<post_bang>[^\s:]+)?)?
- (?:[\s:]+(?P<args>.*))?
- \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<String> = 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 }
}
}
@@ -100,6 +145,54 @@ 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
// None and Some("") for the args, distinction which matters
@@ -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("!!!"),
@@ -149,14 +242,6 @@ mod verb_invocation_tests {
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),
);
@@ -170,6 +255,14 @@ mod verb_invocation_tests {
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 a599c8f..e601912 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 0000000..dede625
--- /dev/null
+++ b/website/docs/img/20240501-sdp.png
Binary files differ
diff --git a/website/docs/index.md b/website/docs/index.md
index f718db7..8e3e164 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 <kbd>d</kbd> then <kbd>enter</kbd> shows you the dates. Or hit <kbd>alt</kbd><kbd>h</kbd> 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 <kbd>alt</kbd><kbd>h</kbd> and you see hidden files.
# See what takes space: