diff options
40 files changed, 732 insertions, 685 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index 7a084d7..f08a605 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +<a name="v0.8.0"></a> +### v0.8.0 - 2019-06-07 +Half broot has been rewritten to allow Windows compatibility. Termion has been replaced with crossterm. + <a name="v0.7.5"></a> ### v0.7.5 - 2019-04-03 - try to give arguments to verbs executed with --cmd @@ -86,7 +86,7 @@ dependencies = [ [[package]] name = "broot" -version = "0.7.5" +version = "0.8.0" dependencies = [ "clap 2.33.0 (registry+https://github.com/rust-lang/crates.io-index)", "crossbeam 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", @@ -100,7 +100,7 @@ dependencies = [ "opener 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", "regex 1.1.5 (registry+https://github.com/rust-lang/crates.io-index)", "simplelog 0.5.3 (registry+https://github.com/rust-lang/crates.io-index)", - "termimad 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", + "termimad 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)", "toml 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", "users 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -617,7 +617,7 @@ dependencies = [ [[package]] name = "termimad" -version = "0.3.4" +version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "crossterm 0.9.5 (registry+https://github.com/rust-lang/crates.io-index)", @@ -795,7 +795,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum syn 0.15.30 (registry+https://github.com/rust-lang/crates.io-index)" = "66c8865bf5a7cbb662d8b011950060b3c8743dca141b054bf7195b20d314d8e2" "checksum synstructure 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)" = "73687139bf99285483c96ac0add482c3776528beac1d97d444f6e91f203a2015" "checksum term 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)" = "edd106a334b7657c10b7c540a0106114feadeb4dc314513e97df481d5d966f42" -"checksum termimad 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "a8326aabc4d9ab6d6410c99fbd1a3bff0f89f2a7d315bbf765e2ea754e41eb45" +"checksum termimad 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)" = "2c335380ef344bd1d9f20866ea45ba96319ef3191a43d38210c12a565a547848" "checksum termion 1.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "689a3bdfaab439fd92bc87df5c4c78417d3cbe537487274e9b0b2dce76e92096" "checksum textwrap 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" "checksum thread_local 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)" = "c6b53e329000edc2b34dbe8545fd20e55a333362d0a321909685a19bd28c3f1b" @@ -1,6 +1,6 @@ [package] name = "broot" -version = "0.7.5" +version = "0.8.0" authors = ["dystroy <denys.seguret@gmail.com>"] repository = "https://github.com/Canop/broot" description = "Fuzzy Search + tree + cd" @@ -23,7 +23,7 @@ glob = "0.3" crossbeam = "0.7" opener = "0.4" crossterm = "0.9.5" -termimad = "0.3.4" +termimad = "0.3.5" #termimad = { path = "../termimad" } [target.'cfg(unix)'.dependencies] @@ -7,17 +7,17 @@ //! - an operation which keeps the state //! - a request to quit broot //! - a request to launch an executable (thus leaving broot) +use crossterm::{InputEvent, TerminalInput}; use std::io::{self, Write}; use std::result::Result; use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::{mpsc, Arc}; use std::thread; -use crossterm::{TerminalInput, InputEvent}; use crate::app_context::AppContext; use crate::browser_states::BrowserState; -use crate::commands::Command; use crate::command_parsing::parse_command_sequence; +use crate::commands::Command; use crate::errors::ProgramError; use crate::errors::TreeBuildError; use crate::external::Launchable; @@ -65,11 +65,7 @@ pub trait AppState { screen: &mut Screen, con: &AppContext, ) -> io::Result<AppStateCmdResult>; - fn refresh( - &mut self, - screen: &Screen, - con: &AppContext, - ) -> Command; + fn refresh(&mut self, screen: &Screen, con: &AppContext) -> Command; fn has_pending_tasks(&self) -> bool; fn do_pending_task(&mut self, screen: &mut Screen, tl: &TaskLifetime); fn display(&mut self, screen: &mut Screen, con: &AppContext) -> io::Result<()>; @@ -125,9 +121,9 @@ impl App { let has_task = self.state().has_pending_tasks(); if has_task { loop { + self.mut_state().display(screen, con)?; self.state().write_status(screen, &cmd, con)?; screen.write_spinner(true)?; - self.mut_state().display(screen, con)?; if tl.is_expired() { break; } @@ -139,6 +135,7 @@ impl App { screen.write_spinner(false)?; } self.mut_state().display(screen, con)?; + self.mut_state().write_status(screen, &cmd, con)?; Ok(()) } diff --git a/src/browser_states.rs b/src/browser_states.rs index 2eb0b29..928be19 100644 --- a/src/browser_states.rs +++ b/src/browser_states.rs @@ -1,4 +1,3 @@ - use std::io; use std::path::PathBuf; use std::result::Result; @@ -18,8 +17,8 @@ use crate::status::Status; use crate::task_sync::TaskLifetime; use crate::tree_build::TreeBuilder; use crate::tree_options::{OptionBool, TreeOptions}; -use crate::verbs::{VerbExecutor}; -use crate::verb_store::{PrefixSearchResult}; +use crate::verb_store::PrefixSearchResult; +use crate::verbs::VerbExecutor; /// An application state dedicated to displaying a tree. /// It's the first and main screen of broot. @@ -30,7 +29,6 @@ pub struct BrowserState { } impl BrowserState { - pub fn new( path: PathBuf, mut options: TreeOptions, @@ -82,11 +80,9 @@ impl BrowserState { None => &self.tree, } } - } impl AppState for BrowserState { - fn apply( &mut self, cmd: &mut Command, @@ -178,7 +174,9 @@ impl AppState for BrowserState { con.verb_store.verbs[cd_idx].to_cmd_result(&line.target(), &None, screen, con)? } Action::Verb(invocation) => match con.verb_store.search(&invocation.key) { - PrefixSearchResult::Match(verb) => self.execute_verb(verb, &invocation, screen, con)?, + PrefixSearchResult::Match(verb) => { + self.execute_verb(verb, &invocation, screen, con)? + } _ => AppStateCmdResult::verb_not_found(&invocation.key), }, Action::FuzzyPatternEdit(pat) => match pat.len() { @@ -204,10 +202,7 @@ impl AppState for BrowserState { } } Action::Help => { - AppStateCmdResult::NewState( - Box::new(HelpState::new(screen, con)), - Command::new() - ) + AppStateCmdResult::NewState(Box::new(HelpState::new(screen, con)), Command::new()) } Action::Refresh => AppStateCmdResult::RefreshState, Action::Quit => AppStateCmdResult::Quit, @@ -276,7 +271,7 @@ impl AppState for BrowserState { left: 0, top: 0, width: screen.w, - height: screen.h-2, + height: screen.h - 2, }, in_app: true, }; @@ -340,11 +335,7 @@ impl AppState for BrowserState { } } - fn refresh( - &mut self, - screen: &Screen, - _con: &AppContext, - ) -> Command { + fn refresh(&mut self, screen: &Screen, _con: &AppContext) -> Command { let page_height = BrowserState::page_height(screen) as usize; // refresh the base tree if let Err(e) = self.tree.refresh(page_height) { @@ -370,21 +361,23 @@ impl AppState for BrowserState { screen.goto(screen.w - total_char_size, screen.h); let terminal = crossterm::Terminal::new(); terminal.clear(crossterm::ClearType::UntilNewLine)?; - screen.write(&format!( + print!( "{}{} {}{}", screen.skin.flag_label.apply_to(" h:"), - screen.skin.flag_value.apply_to( - if tree.options.show_hidden { 'y' } else { 'n' } - ), + screen + .skin + .flag_value + .apply_to(if tree.options.show_hidden { 'y' } else { 'n' }), screen.skin.flag_label.apply_to(" gi:"), - screen.skin.flag_value.apply_to( - match tree.options.respect_git_ignore { + screen + .skin + .flag_value + .apply_to(match tree.options.respect_git_ignore { OptionBool::Auto => 'a', OptionBool::Yes => 'y', OptionBool::No => 'n', - } - ), - )); + }), + ); Ok(()) } } @@ -1,6 +1,5 @@ /// this module manages reading and translating /// the arguments passed on launch of the application. - use crate::errors::{ProgramError, TreeBuildError}; use crate::tree_options::TreeOptions; use clap; @@ -9,15 +8,18 @@ use std::io; use std::path::PathBuf; use std::result::Result; +use crossterm::Color::*; +use termimad::{rgb, Alignment, MadSkin}; + pub struct AppLaunchArgs { pub root: PathBuf, // what should be the initial root pub file_export_path: Option<String>, // where to write the produced path (if required with --out) - pub cmd_export_path: Option<String>, // where to write the produced command (if required with --outcmd, or -oc) - pub tree_options: TreeOptions, // initial tree options - pub commands: Option<String>, // commands passed as cli argument, still unparsed - pub install: bool, // installation is required - pub height: Option<u16>, // an optional height to replace the screen's one - pub no_style: bool, // whether to remove all styles (including colors) + pub cmd_export_path: Option<String>, // where to write the produced command (if required with --outcmd, or -oc) + pub tree_options: TreeOptions, // initial tree options + pub commands: Option<String>, // commands passed as cli argument, still unparsed + pub install: bool, // installation is required + pub height: Option<u16>, // an optional height to replace the screen's one + pub no_style: bool, // whether to remove all styles (including colors) } /// declare the possible CLI arguments, and gets the values @@ -64,7 +66,7 @@ fn get_cli_args<'a>() -> clap::ArgMatches<'a> { clap::Arg::with_name("height") .long("height") .help("height (if you don't want to fill the screen or for file export)") - .takes_value(true) + .takes_value(true), ) .arg( clap::Arg::with_name("install") @@ -141,9 +143,7 @@ pub fn read_lauch_args() -> Result<AppLaunchArgs, ProgramError> { .value_of("commands") .and_then(|s| Some(s.to_owned())); let no_style = cli_args.is_present("no-style"); - let height = cli_args - .value_of("height") - .and_then(|s| s.parse().ok()); + let height = cli_args.value_of("height").and_then(|s| s.parse().ok()); Ok(AppLaunchArgs { root, file_export_path, @@ -156,11 +156,27 @@ pub fn read_lauch_args() -> Result<AppLaunchArgs, ProgramError> { }) } -pub fn ask_authorization(question: &str) -> io::Result<bool> { - println!("{}", question); +/// wait for user input, return `true` if she +/// didn't answer 'n' +pub fn ask_authorization() -> io::Result<bool> { let answer = crossterm::input().read_line()?; Ok(match answer.as_ref() { - "n" => false, + "n" | "N" => false, _ => true, }) } + +pub fn mad_skin() -> MadSkin { + let mut skin = MadSkin::default(); + skin.set_headers_fg(rgb!(255, 187, 0)); + skin.bold.set_fg(Yellow); + skin.italic.set_fgbg(Magenta, rgb!(30, 30, 40)); + skin.scrollbar.set_track_fg(Rgb { + r: 30, + g: 30, + b: 40, + }); + skin.scrollbar.set_thumb_fg(Rgb { r: 67, g: 51, b: 0 }); + skin.code.align = Alignment::Center; + skin +} diff --git a/src/command_parsing.rs b/src/command_parsing.rs index 280442a..8014ee7 100644 --- a/src/command_parsing.rs +++ b/src/command_parsing.rs @@ -11,7 +11,7 @@ use crate::verb_store::PrefixSearchResult; #[derive(Debug)] enum CommandSequenceToken { Standard(String), // one or several words, not starting with a ':'. May be a filter or a verb argument - VerbKey(String), // a verb (the ':' isn't given) + VerbKey(String), // a verb (the ':' isn't given) } struct CommandSequenceTokenizer { chars: Vec<char>, @@ -21,7 +21,7 @@ impl CommandSequenceTokenizer { pub fn from(sequence: &str) -> CommandSequenceTokenizer { CommandSequenceTokenizer { chars: sequence.chars().collect(), - pos: 0 + pos: 0, } } } @@ -49,13 +49,11 @@ impl Iterator for CommandSequenceTokenizer { } let token: String = self.chars[self.pos..end].iter().collect(); self.pos = end + 1; - Some( - if is_verb { - CommandSequenceToken::VerbKey(token) - } else { - CommandSequenceToken::Standard(token) - } - ) + Some(if is_verb { + CommandSequenceToken::VerbKey(token) + } else { + CommandSequenceToken::Standard(token) + }) } } @@ -70,7 +68,10 @@ impl Iterator for CommandSequenceTokenizer { /// The current parsing try to be the least possible flawed by /// giving verbs the biggest sequence of tokens accepted by their /// execution pattern. -pub fn parse_command_sequence(sequence: &str, con: &AppContext) -> Result<Vec<Command>, ProgramError> { +pub fn parse_command_sequence( + sequence: &str, + con: &AppContext, +) -> Result<Vec<Command>, ProgramError> { let mut tokenizer = CommandSequenceTokenizer::from(sequence); let mut commands: Vec<Command> = Vec::new(); let mut leftover: Option<CommandSequenceToken> = None; @@ -84,12 +85,12 @@ pub fn parse_command_sequence(sequence: &str, con: &AppContext) -> Result<Vec<Co CommandSequenceToken::VerbKey(key) => { let verb = match con.verb_store.search(&key) { PrefixSearchResult::NoMatch => { - return Err(ProgramError::UnknownVerb{key}); + return Err(ProgramError::UnknownVerb { key }); } PrefixSearchResult::TooManyMatches => { - return Err(ProgramError::AmbiguousVerbKey{key}); + return Err(ProgramError::AmbiguousVerbKey { key }); } - PrefixSearchResult::Match(verb) => verb + PrefixSearchResult::Match(verb) => verb, }; let mut raw = format!(":{}", key); if let Some(args_regex) = &verb.args_parser { @@ -111,7 +112,7 @@ pub fn parse_command_sequence(sequence: &str, con: &AppContext) -> Result<Vec<Co } } if nb_valid_args == 0 && !args_regex.is_match("") { - return Err(ProgramError::UnmatchingVerbArgs{key}); + return Err(ProgramError::UnmatchingVerbArgs { key }); } for (i, arg) in args.drain(..).enumerate() { if i < nb_valid_args { @@ -124,16 +125,15 @@ pub fn parse_command_sequence(sequence: &str, con: &AppContext) -> Result<Vec<Co } raw } - CommandSequenceToken::Standard(raw) => raw + CommandSequenceToken::Standard(raw) => raw, }; commands.push(Command::from(raw)); } if let Some(token) = leftover.take() { - commands.push(Command::from(match token{ + commands.push(Command::from(match token { CommandSequenceToken::Standard(raw) => raw, CommandSequenceToken::VerbKey(raw) => format!(":{}", raw), })); } Ok(commands) } - diff --git a/src/commands.rs b/src/commands.rs index bafd62d..967da1e 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -2,15 +2,15 @@ //! in the input. It's independant of the state of the application //! (verbs arent checked at this point) -use regex::Regex; -use crossterm::{KeyEvent}; use crate::verb_invocation::VerbInvocation; +use crossterm::KeyEvent; +use regex::Regex; #[derive(Debug, Clone)] pub struct Command { pub raw: String, // what's visible in the input parts: CommandParts, // the parsed parts of the visible input - pub action: Action, // what's required, based on the last key (which may be not visible, like esc) + pub action: Action, // what's required, based on the last key (which may be not visible, like esc) } /// An intermediate parsed representation of the raw string diff --git a/src/conf.rs b/src/conf.rs index e3cd1cd..5f9b939 100644 --- a/src/conf.rs +++ b/src/conf.rs @@ -1,7 +1,8 @@ //! manage reading the verb shortcuts from the configuration file, //! initializing if if it doesn't yet exist -use crossterm::{Attribute}; +use crossterm::Attribute; +use crossterm::ObjectStyle; use directories::ProjectDirs; use std::collections::HashMap; use std::fs; @@ -9,7 +10,6 @@ use std::io; use std::path::{Path, PathBuf}; use std::result::Result; use toml::{self, Value}; -use crossterm::ObjectStyle; use crate::errors::ConfError; use crate::skin_conf; @@ -60,7 +60,6 @@ pub fn dir() -> PathBuf { } impl Conf { - pub fn default_location() -> PathBuf { dir().join("conf.toml") } @@ -118,9 +117,11 @@ impl Conf { let from_shell = bool_field(verb_value, "from_shell"); let leave_broot = bool_field(verb_value, "leave_broot"); if leave_broot == Some(false) && from_shell == Some(true) { - eprintln!("Invalid [[verbs]] entry in configuration"); - eprintln!("You can't simultaneously have leave_broot=false and from_shell=true"); - continue; + eprintln!("Invalid [[verbs]] entry in configuration"); + eprintln!( + "You can't simultaneously have leave_broot=false and from_shell=true" + ); + continue; } verbs.push(VerbConf { invocation, @@ -139,17 +140,18 @@ impl Conf { for (k, v) in entries_tbl.iter() { if let Some(s) = v.as_str() { match skin_conf::parse_object_style(s) { - Ok(ske) => { skin.insert(k.to_string(), ske); }, - Err(e) => { eprintln!("{}", e); } + Ok(ske) => { + skin.insert(k.to_string(), ske); + } + Err(e) => { + eprintln!("{}", e); + } } } } } - Ok(Conf { - verbs, - skin, - }) + Ok(Conf { verbs, skin }) } } @@ -190,4 +192,39 @@ name = "view" invocation = "view" execution = "less {file}" +##################### +# Skin + +# If you want to change the colors of broot, +# uncomment the following bloc and start messing +# with the various values +# +# [skin] +# tree = "rgb(89, 73, 101) none" +# file = "gray(21) none" +# directory = "rgb(255, 152, 0) none bold" +# exe = "rgb(17, 164, 181) none" +# link = "Magenta none" +# pruning = "rgb(89, 73, 101) none Italic" +# permissions = "gray(12) none " +# selected_line = "none gray(3)" +# size_bar = "black rgb(255, 152, 0)" +# size_no_bar = "gray(15) gray(2)" +# char_match = "yellow none" +# file_error = "Red none" +# flag_label = "gray(16) none" +# flag_value = "rgb(255, 152, 0) none bold" +# input = "White none" +# spinner = "gray(10) gray(2)" +# status_error = "Red gray(2)" +# status_normal = "gray(20) gray(2)" +# scrollbar_track = "rgb(80, 50, 0) none" +# scrollbar_thumb = "rgb(255, 187, 0) none" +# help_paragraph = "gray(20) none" +# help_bold = "rgb(255, 187, 0) none bold" +# help_italic = "Magenta rgb(30, 30, 40) italic" +# help_code = "gray(21) gray(3)" +# help_headers = "rgb(255, 187, 0) none" +# help_table_border = "rgb(170, 136, 0) none" +# "#; diff --git a/src/displayable_tree.rs b/src/displayable_tree.rs index 20c215a..710f09a 100644 --- a/src/displayable_tree.rs +++ b/src/displayable_tree.rs @@ -1,5 +1,5 @@ -use std::fmt; use crossterm::{ClearType, Terminal}; +use std::fmt; use crate::file_sizes::Size; use crate::flat_tree::{LineType, Tree, TreeLine}; @@ -26,9 +26,27 @@ pub struct DisplayableTree<'s, 't> { pub in_app: bool, // if true we show the selection and scrollbar } -impl DisplayableTree<'_, '_> { +impl<'s, 't> DisplayableTree<'s, 't> { + pub fn out_of_app(tree: &'t Tree, skin: &'s Skin, width: u16) -> DisplayableTree<'s, 't> { + DisplayableTree { + tree, + skin, + area: termimad::Area { + left: 0, + top: 0, + width, + height: tree.lines.len() as u16, + }, + in_app: false, + } + } - fn write_line_size(&self, f: &mut fmt::Formatter<'_>, line: &TreeLine, total_size: Size) -> fmt::Result { + fn write_line_size( + &self, + f: &mut fmt::Formatter<'_>, + line: &TreeLine, + total_size: Size, + ) -> fmt::Result { if let Some(s) = line.size { let dr: usize = s.discrete_ratio(total_size, 8) as usize; let s: Vec<char> = s.to_string().chars().collect(); @@ -49,8 +67,10 @@ impl DisplayableTree<'_, '_> { } fn write_mode(&self, f: &mut fmt::Formatter<'_>, mode: u32) -> fmt::Result { - write!(f, "{}", self.skin.permissions.apply_to( - format!( + write!( + f, + "{}", + self.skin.permissions.apply_to(format!( "{}{}{}{}{}{}{}{}{}", if (mode & (1 << 8)) != 0 { 'r' } else { '-' }, if (mode & (1 << 7)) != 0 { 'w' } else { '-' }, @@ -61,8 +81,8 @@ impl DisplayableTree<'_, '_> { if (mode & (1 << 2)) != 0 { 'r' } else { '-' }, if (mode & (1 << 1)) != 0 { 'w' } else { '-' }, if (mode & 1) != 0 { 'x' } else { '-' }, - ) - )) + )) + ) } fn write_line_name( @@ -76,7 +96,11 @@ impl DisplayableTree<'_, '_> { let style = match &line.line_type { LineType::Dir => &self.skin.directory, LineType::File => { - if line.is_exe() { &self.skin.exe } else { &self.skin.file } + if line.is_exe() { + &self.skin.exe + } else { + &self.skin.file + } } LineType::SymLinkToFile(_) | LineType::SymLinkToDir(_) => &self.skin.link, LineType::Pruning => &self.skin.pruning, @@ -93,11 +117,7 @@ impl DisplayableTree<'_, '_> { write!( f, "{}", - pattern.style( - &line.name, - &style, - &self.skin.char_match, - ), + pattern.style(&line.name, &style, &self.skin.char_match,), )?; } match &line.line_type { @@ -111,7 +131,11 @@ impl DisplayableTree<'_, '_> { if line.has_error { self.skin.file_error.write(f, &target)?; } else { - let target_style = if line.is_dir() { &self.skin.directory } else { &self.skin.file }; + let target_style = if line.is_dir() { + &self.skin.directory + } else { + &self.skin.file + }; let mut target_style = target_style.clone(); if selected { if let Some(c) = self.skin.selected_line.bg_color { @@ -121,11 +145,10 @@ impl DisplayableTree<'_, '_> { target_style.write(f, &target)?; } } - _ => { } + _ => {} } Ok(()) } - } impl fmt::Display for DisplayableTree<'_, '_> { @@ -152,6 +175,9 @@ impl fmt::Display for DisplayableTree<'_, '_> { None }; for y in 0..self.area.height { + if self.in_app { + cursor.goto(0, y).unwrap(); + } let mut line_index = y as usize; if line_index > 0 { line_index += tree.scroll as usize; @@ -173,7 +199,7 @@ impl fmt::Display for DisplayableTree<'_, '_> { } } else { " " - } + }, )?; } if tree.op |