Skip to content

Commit

Permalink
--cmd program argument to pass a sequence of commands
Browse files Browse the repository at this point in the history
Right not I use it for tests & benchmarks
  • Loading branch information
Canop committed Jan 21, 2019
1 parent 8aa96fb commit 3e5faa6
Show file tree
Hide file tree
Showing 7 changed files with 157 additions and 63 deletions.
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "broot"
version = "0.4.6"
version = "0.4.7"
authors = ["dystroy <[email protected]>"]
repository = "https://github.com/Canop/broot"
description = "Fuzzy Search + tree + cd"
Expand Down
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down Expand Up @@ -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 `<enter>` (you should define your prefered editor, see [documentation](documentation.md#verbs)).

### More...

See the complete [Documentation](documentation.md).

## Installation

### From Source
Expand Down
27 changes: 27 additions & 0 deletions documentation.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).



143 changes: 87 additions & 56 deletions src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,27 +55,33 @@ pub trait AppState {
}

pub struct App {
pub states: Vec<Box<dyn AppState>>, // stack: the last one is current
states: Vec<Box<dyn AppState>>, // stack: the last one is current
quitting: bool,
launch_at_end: Option<Launchable>, // 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<dyn AppState>) {
self.states.push(new_state);
}

pub fn mut_state(&mut self) -> &mut Box<dyn AppState> {
fn mut_state(&mut self) -> &mut Box<dyn AppState> {
match self.states.last_mut() {
Some(s) => s,
None => {
panic!("No path has been pushed");
}
}
}
pub fn state(&self) -> &Box<dyn AppState> {
fn state(&self) -> &Box<dyn AppState> {
match self.states.last() {
Some(s) => s,
None => {
Expand All @@ -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 {
Expand All @@ -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<Command> {
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<Option<Launchable>> {
pub fn run(mut self, con: &AppContext, input_commands: Vec<Command>) -> io::Result<Option<Launchable>> {
let (w, h) = termion::terminal_size()?;
let mut screen = Screen::new(w, h)?;
write!(
Expand All @@ -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
Expand All @@ -139,21 +206,12 @@ impl App {
}
}
});

let mut cmd = Command::new();
screen.write_input(&cmd)?;
screen.write_status_text("Hit <esc> 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<Launchable> = 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(_) => {
Expand All @@ -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)
}
}
18 changes: 18 additions & 0 deletions src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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') => {
Expand Down
23 changes: 19 additions & 4 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,22 +38,33 @@ 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;
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> {
clap::App::new("broot")
.version(VERSION)
.author("dystroy <[email protected]>")
.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")
Expand Down Expand Up @@ -160,15 +171,19 @@ fn run() -> Result<Option<Launchable>, ProgramError> {
.value_of("output_path")
.and_then(|s| Some(s.to_owned())),
};

debug!("output path: {:?}", &con.output_path);

let input_commands: Vec<Command> = 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"
},
Expand Down

0 comments on commit 3e5faa6

Please sign in to comment.