diff --git a/Cargo.lock b/Cargo.lock index f5bba1b6..513ff303 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -31,7 +31,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] name = "broot" -version = "0.4.6" +version = "0.4.7" dependencies = [ "clap 2.32.0 (registry+https://github.com/rust-lang/crates.io-index)", "custom_error 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)", diff --git a/Cargo.toml b/Cargo.toml index aa5774ec..287e5304 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "broot" -version = "0.4.6" +version = "0.4.7" authors = ["dystroy "] repository = "https://github.com/Canop/broot" description = "Fuzzy Search + tree + cd" diff --git a/README.md b/README.md index a29710aa..2d152f21 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,6 @@ An interactive tree view, a fuzzy search, a balanced BFS descent and customizable commands. -[Documentation](documentation.md) ### Get an overview of a directory, even a big one: @@ -50,6 +49,10 @@ broot tries to select the most relevant file. You can still go from one match to Just find the file you want to edit with a few keystrokes, type `:e`, then `` (you should define your prefered editor, see [documentation](documentation.md#verbs)). +### More... + +See the complete [Documentation](documentation.md). + ## Installation ### From Source diff --git a/documentation.md b/documentation.md index 912a9ea4..6008bd77 100644 --- a/documentation.md +++ b/documentation.md @@ -135,3 +135,30 @@ In the default configuration, it's mapped to `s` and can be activated at launch When broot starts, it checks for a configuration file in the standard location defined by your OS and creates one if there's none. You can see this location by opening the help with ̀`?`. You can also open it directly from the help screen by typing `:o`. + +## Passing commands as program argument + +*Note: this feature is experimental and will probably change.* + +Commands to be executed can be passed using the `--cmd` argument, separated with a space. + +### Direcly search + + broot --cmd miaou / + +This opens broot and immediately search for "miaou" in `/` as if it were typed in broot's input. + +### Go to the most relevant directory + + broot --cmd ":p miaou :g" + +This opens broot, goes to the parent directory, searches for "miaou", then opens the selected directory (staying in broot). + +### cd to a directory + + br --cmd "roulette :c" ~ + +This launches broot using the `br` shell function in your home directory, searches for "roulette", then cd to the relevant directory (leaving broot). + + + diff --git a/src/app.rs b/src/app.rs index 281ec108..b93ba7e7 100644 --- a/src/app.rs +++ b/src/app.rs @@ -55,19 +55,25 @@ pub trait AppState { } pub struct App { - pub states: Vec>, // stack: the last one is current + states: Vec>, // stack: the last one is current + quitting: bool, + launch_at_end: Option, // what must be launched after end } impl App { pub fn new() -> App { - App { states: Vec::new() } + App { + states: Vec::new(), + quitting: false, + launch_at_end: None, + } } pub fn push(&mut self, new_state: Box) { self.states.push(new_state); } - pub fn mut_state(&mut self) -> &mut Box { + fn mut_state(&mut self) -> &mut Box { match self.states.last_mut() { Some(s) => s, None => { @@ -75,7 +81,7 @@ impl App { } } } - pub fn state(&self) -> &Box { + fn state(&self) -> &Box { match self.states.last() { Some(s) => s, None => { @@ -84,6 +90,8 @@ impl App { } } + /// execute all the pending tasks until there's none remaining or + /// the allowed lifetime is expired (usually when the user typed a new key) fn do_pending_tasks(&mut self, cmd: &Command, screen: &mut Screen, con: &AppContext, tl: TaskLifetime) -> io::Result<()> { let has_task = self.state().has_pending_tasks(); if has_task { @@ -105,8 +113,52 @@ impl App { Ok(()) } + /// apply a command, and returns a command, which may be the same (modified or not) + /// or a new one. + /// This normally mutates self + fn apply_command(&mut self, cmd: Command, screen: &mut Screen, con: &AppContext) -> io::Result { + let mut cmd = cmd; + debug!("action: {:?}", &cmd.action); + screen.write_input(&cmd)?; + self.state().write_flags(screen, con)?; + match self.mut_state().apply(&mut cmd, con)? { + AppStateCmdResult::Quit => { + debug!("cmd result quit"); + self.quitting = true; + } + AppStateCmdResult::Launch(launchable) => { + self.launch_at_end = Some(launchable); + self.quitting = true; + } + AppStateCmdResult::NewState(boxed_state) => { + self.push(boxed_state); + cmd = cmd.pop_verb(); + self.state().write_status(screen, &cmd, con)?; + } + AppStateCmdResult::PopState => { + if self.states.len() == 1 { + debug!("quitting on last pop state"); + self.quitting = true; + } else { + self.states.pop(); + cmd = Command::new(); + self.state().write_status(screen, &cmd, con)?; + } + } + AppStateCmdResult::DisplayError(txt) => { + screen.write_status_err(&txt)?; + } + AppStateCmdResult::Keep => { + self.state().write_status(screen, &cmd, con)?; + } + } + screen.write_input(&cmd)?; + self.state().write_flags(screen, con)?; + Ok(cmd) + } + /// This is the main loop of the application - pub fn run(mut self, con: &AppContext) -> io::Result> { + pub fn run(mut self, con: &AppContext, input_commands: Vec) -> io::Result> { let (w, h) = termion::terminal_size()?; let mut screen = Screen::new(w, h)?; write!( @@ -115,15 +167,30 @@ impl App { termion::clear::All, termion::cursor::Hide )?; - let stdin = stdin(); - let keys = stdin.keys(); + + // if some commands were passed to the application + // we execute them before even starting listening for keys + for cmd in input_commands { + let cmd = self.apply_command(cmd, &mut screen, con)?; + self.do_pending_tasks( + &cmd, + &mut screen, + con, + TaskLifetime::unlimited(), + )?; + if self.quitting { + return Ok(self.launch_at_end); + } + } + + // we listen for keys in a separate thread so that we can go on listening + // when a long search is running, and interrupt it if needed + let keys = stdin().keys(); let (tx_keys, rx_keys) = mpsc::channel(); let (tx_quit, rx_quit) = mpsc::channel(); let cmd_count = Arc::new(AtomicUsize::new(0)); let key_count = Arc::clone(&cmd_count); thread::spawn(move || { - // we listen for keys in a separate thread so that we can go on listening - // when a long search is running, and interrupt it if needed for c in keys { key_count.fetch_add(1, Ordering::SeqCst); // we send the command to the receiver in the @@ -139,21 +206,12 @@ impl App { } } }); + let mut cmd = Command::new(); screen.write_input(&cmd)?; screen.write_status_text("Hit to quit, '?' for help, or type some letters to search")?; self.state().write_flags(&mut screen, con)?; - let mut quit = false; - let mut to_launch: Option = None; loop { - if !quit { - self.do_pending_tasks( - &cmd, - &mut screen, - con, - TaskLifetime::new(&cmd_count), - )?; - } let c = match rx_keys.recv() { Ok(c) => c, Err(_) => { @@ -163,44 +221,17 @@ impl App { } }; cmd.add_key(c?); - debug!("action: {:?}", &cmd.action); - screen.write_input(&cmd)?; - self.state().write_flags(&mut screen, con)?; - match self.mut_state().apply(&mut cmd, con)? { - AppStateCmdResult::Quit => { - debug!("cmd result quit"); - quit = true; - } - AppStateCmdResult::Launch(launchable) => { - to_launch = Some(launchable); - quit = true; - } - AppStateCmdResult::NewState(boxed_state) => { - self.push(boxed_state); - cmd = cmd.pop_verb(); - self.state().write_status(&mut screen, &cmd, con)?; - } - AppStateCmdResult::PopState => { - if self.states.len() == 1 { - debug!("quitting on last pop state"); - quit = true; - } else { - self.states.pop(); - cmd = Command::new(); - self.state().write_status(&mut screen, &cmd, con)?; - } - } - AppStateCmdResult::DisplayError(txt) => { - screen.write_status_err(&txt)?; - } - AppStateCmdResult::Keep => { - self.state().write_status(&mut screen, &cmd, con)?; - } + cmd = self.apply_command(cmd, &mut screen, con)?; + tx_quit.send(self.quitting).unwrap(); + if !self.quitting { + self.do_pending_tasks( + &cmd, + &mut screen, + con, + TaskLifetime::new(&cmd_count), + )?; } - screen.write_input(&cmd)?; - self.state().write_flags(&mut screen, con)?; - tx_quit.send(quit).unwrap(); } - Ok(to_launch) + Ok(self.launch_at_end) } } diff --git a/src/commands.rs b/src/commands.rs index 38167c91..935de531 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -101,6 +101,24 @@ impl Command { } c } + // build a command from a string + // Note that this isn't used (or usable) for interpretation + // of the in-app user input. It's meant for interpretation + // of a file or from a sequence of commands passed as argument + // of the program. + // A ':', even if at the end, is assumed to mean that the + // command must be executed (it's equivalent to the user + // typing `enter` in the app + // This specific syntax isn't definitive + pub fn from(raw: String) -> Command { + let parts = CommandParts::from(&raw); + let action = Action::from(&parts, raw.contains(":")); + Command { + raw, + parts, + action, + } + } pub fn add_key(&mut self, key: Key) { match key { Key::Char('\t') => { diff --git a/src/main.rs b/src/main.rs index 14ff5fea..2646a225 100644 --- a/src/main.rs +++ b/src/main.rs @@ -38,6 +38,7 @@ use toml; use crate::app::App; use crate::app_context::AppContext; use crate::browser_states::BrowserState; +use crate::commands::Command; use crate::conf::Conf; use crate::errors::ProgramError; use crate::external::Launchable; @@ -45,7 +46,7 @@ use crate::task_sync::TaskLifetime; use crate::tree_options::TreeOptions; use crate::verbs::VerbStore; -const VERSION: &str = "0.4.6"; +const VERSION: &str = "0.4.7"; // declare the possible CLI arguments, and gets the values fn get_cli_args<'a>() -> clap::ArgMatches<'a> { @@ -53,7 +54,17 @@ fn get_cli_args<'a>() -> clap::ArgMatches<'a> { .version(VERSION) .author("dystroy ") .about("Balanced tree view + fuzzy search + BFS + customizable launcher") - .arg(clap::Arg::with_name("root").help("sets the root directory")) + .arg( + clap::Arg::with_name("root") + .help("sets the root directory") + ) + .arg( + clap::Arg::with_name("commands") + .short("c") + .long("cmd") + .takes_value(true) + .help("commands to execute (space separated, experimental)"), + ) .arg( clap::Arg::with_name("only-folders") .short("f") @@ -160,15 +171,19 @@ fn run() -> Result, ProgramError> { .value_of("output_path") .and_then(|s| Some(s.to_owned())), }; - debug!("output path: {:?}", &con.output_path); + let input_commands: Vec = match cli_args.value_of("commands") { + Some(str) => str.split(' ').map(|s| Command::from(s.to_string())).collect(), + None => Vec::new(), + }; + Ok( match BrowserState::new(path.clone(), tree_options, &TaskLifetime::unlimited()) { Some(bs) => { let mut app = App::new(); app.push(Box::new(bs)); - app.run(&con)? + app.run(&con, input_commands)? } _ => None, // should not happen, as the lifetime is "unlimited" },