diff options
author | Canop <cano.petrole@gmail.com> | 2020-03-30 22:22:51 +0200 |
---|---|---|
committer | Canop <cano.petrole@gmail.com> | 2020-03-30 22:22:51 +0200 |
commit | 8015fa9ab19539ccb33e2bf69aee58c9827f9128 (patch) | |
tree | 50995bad0333cf6e1c86b8d657ab22ea7e2e0f42 /src/browser/browser_state.rs | |
parent | 452dd38cc22aa66f11c2377eaafca521871d1184 (diff) |
moved files around
refactored, added a few mods into a less flat source structure.
Diffstat (limited to 'src/browser/browser_state.rs')
-rw-r--r-- | src/browser/browser_state.rs | 516 |
1 files changed, 516 insertions, 0 deletions
diff --git a/src/browser/browser_state.rs b/src/browser/browser_state.rs new file mode 100644 index 0000000..249ac20 --- /dev/null +++ b/src/browser/browser_state.rs @@ -0,0 +1,516 @@ +use { + crate::{ + app::{ + AppContext, + AppState, + AppStateCmdResult, + }, + command::{Action, Command}, + displayable_tree::DisplayableTree, + errors::{ProgramError, TreeBuildError}, + external::Launchable, + flat_tree::{LineType, Tree}, + git, + help::HelpState, + pattern::Pattern, + screens::{self, Screen}, + status::Status, + task_sync::Dam, + tree_build::TreeBuilder, + tree_options::TreeOptions, + verb_store::PrefixSearchResult, + verbs::VerbExecutor, + }, + minimad::Composite, + open, + std::{ + fs::OpenOptions, + io::Write, + path::PathBuf, + }, +}; + +/// An application state dedicated to displaying a tree. +/// It's the first and main screen of broot. +pub struct BrowserState { + pub tree: Tree, + pub filtered_tree: Option<Tree>, + pub pending_pattern: Pattern, // a pattern (or not) which has not yet be applied + pub total_search_required: bool, // whether the pending pattern should be in total search mode +} + +impl BrowserState { + pub fn new( + path: PathBuf, + mut options: TreeOptions, + screen: &Screen, + dam: &Dam, + ) -> Result<Option<BrowserState>, TreeBuildError> { + let pending_pattern = options.pattern.take(); + let builder = TreeBuilder::from(path, options, BrowserState::page_height(screen) as usize)?; + Ok(builder.build(false, dam).map(move |tree| BrowserState { + tree, + filtered_tree: None, + pending_pattern, + total_search_required: false, + })) + } + + pub fn with_new_options( + &self, + screen: &Screen, + change_options: &dyn Fn(&mut TreeOptions), + ) -> AppStateCmdResult { + let tree = self.displayed_tree(); + let mut options = tree.options.clone(); + change_options(&mut options); + AppStateCmdResult::from_optional_state( + BrowserState::new( + tree.root().clone(), + options, + screen, + &Dam::unlimited(), + ), + Command::from_pattern(&tree.options.pattern), + ) + } + + pub fn page_height(screen: &Screen) -> i32 { + i32::from(screen.height) - 2 + } + + /// return a reference to the currently displayed tree, which + /// is the filtered tree if there's one, the base tree if not. + pub fn displayed_tree(&self) -> &Tree { + self.filtered_tree.as_ref().unwrap_or(&self.tree) + } + + /// return a mutable reference to the currently displayed tree, which + /// is the filtered tree if there's one, the base tree if not. + pub fn displayed_tree_mut(&mut self) -> &mut Tree { + self.filtered_tree.as_mut().unwrap_or(&mut self.tree) + } + + pub fn open_selection_stay_in_broot( + &mut self, + screen: &mut Screen, + _con: &AppContext, + ) -> Result<AppStateCmdResult, ProgramError> { + let tree = self.displayed_tree(); + let line = tree.selected_line(); + match &line.line_type { + LineType::File => match open::that(&line.path) { + Ok(exit_status) => { + info!("open returned with exit_status {:?}", exit_status); + Ok(AppStateCmdResult::Keep) + } + Err(e) => Ok(AppStateCmdResult::DisplayError(format!("{:?}", e))), + }, + LineType::Dir | LineType::SymLinkToDir(_) => { + let mut target = line.target(); + if tree.selection == 0 { + // opening the root would be going to where we already are. + // We go up one level instead + if let Some(parent) = target.parent() { + target = PathBuf::from(parent); + } + } + let dam = Dam::unlimited(); + Ok(AppStateCmdResult::from_optional_state( + BrowserState::new(target, tree.options.without_pattern(), screen, &dam), + Command::new(), + )) + } + LineType::SymLinkToFile(target) => { + let path = PathBuf::from(target); + open::that(&path)?; + Ok(AppStateCmdResult::Keep) + } + _ => { + unreachable!(); + } + } + } + + pub fn open_selection_quit_broot( + &mut self, + screen: &mut Screen, + con: &AppContext, + ) -> Result<AppStateCmdResult, ProgramError> { + let tree = self.displayed_tree(); + let line = tree.selected_line(); + match &line.line_type { + LineType::File => make_opener(line.path.clone(), line.is_exe(), con), + LineType::Dir | LineType::SymLinkToDir(_) => { + Ok(if con.launch_args.cmd_export_path.is_some() { + let cd_idx = con.verb_store.index_of("cd"); + con.verb_store.verbs[cd_idx].to_cmd_result( + &line.target(), + &None, + screen, + con, + )? + } else { + AppStateCmdResult::DisplayError( + "This feature needs broot to be launched with the `br` script".to_owned(), + ) + }) + } + LineType::SymLinkToFile(target) => { + make_opener( + PathBuf::from(target), + line.is_exe(), // today this always return false + con, + ) + } + _ => { + unreachable!(); + } + } + } + + pub fn go_to_parent( + &mut self, + screen: &mut Screen, + ) -> AppStateCmdResult { + match &self.displayed_tree().selected_line().path.parent() { + Some(path) => AppStateCmdResult::from_optional_state( + BrowserState::new( + path.to_path_buf(), + self.displayed_tree().options.without_pattern(), + screen, + &Dam::unlimited(), + ), + Command::new(), + ), + None => AppStateCmdResult::DisplayError("no parent found".to_string()), + } + } + + fn normal_status_message(&self, has_pattern: bool) -> Composite<'static> { + let tree = self.displayed_tree(); + if tree.selection == 0 { + if has_pattern { + mad_inline!("Hit *esc* to remove the filter, *enter* to go up, '?' for help") + } else { + mad_inline!("Hit *esc* to go back, *enter* to go up, *?* for help, or a few letters to search") + } + } else { + let line = &tree.lines[tree.selection]; + if has_pattern { + if line.is_dir() { + mad_inline!("Hit *enter* to focus, *alt*-*enter* to cd, *esc* to clear filter, or a space then a verb") + } else { + mad_inline!("Hit *enter* to open, *alt*-*enter* to open and quit, *esc* to clear filter, or *:* + verb") + } + } else { + if line.is_dir() { + mad_inline!("Hit *enter* to focus, *alt*-*enter* to cd, or a space then a verb") + } else { + mad_inline!("Hit *enter* to open the file, *alt*-*enter* to open and quit, or a space then a verb") + } + } + } + } +} + +/// build a AppStateCmdResult with a launchable which will be used to +/// 1/ quit broot +/// 2/ open the relevant file the best possible way +fn make_opener( + path: PathBuf, + is_exe: bool, + con: &AppContext, +) -> Result<AppStateCmdResult, ProgramError> { + Ok(if is_exe { + let path = path.to_string_lossy().to_string(); + if let Some(export_path) = &con.launch_args.cmd_export_path { + // broot was launched as br, we can launch the executable from the shell + let f = OpenOptions::new().append(true).open(export_path)?; + writeln!(&f, "{}", path)?; + AppStateCmdResult::Quit + } else { + AppStateCmdResult::from(Launchable::program(vec![path])?) + } + } else { + AppStateCmdResult::from(Launchable::opener(path)) + }) +} + +impl AppState for BrowserState { + fn has_pending_task(&self) -> bool { + self.pending_pattern.is_some() + || self.displayed_tree().has_dir_missing_size() + || self.displayed_tree().is_missing_git_status_computation() + } + + fn write_status( + &self, + mut w: &mut dyn Write, + cmd: &Command, + screen: &Screen, + con: &AppContext, + ) -> Result<(), ProgramError> { + let task = if self.pending_pattern.is_some() { + Some("searching") + } else if self.displayed_tree().has_dir_missing_size() { + Some("computing sizes") + } else { + None + }; + match &cmd.action { + Action::FuzzyPatternEdit(s) if !s.is_empty() => { + Status::new(task, self.normal_status_message(true), false).display(&mut w, screen) + } + Action::RegexEdit(s, _) if !s.is_empty() => { + Status::new(task, self.normal_status_message(true), false).display(&mut w, screen) + } + Action::VerbEdit(invocation) => { + if invocation.name.is_empty() { + Status::new( + task, + mad_inline!( + "Type a verb then *enter* to execute it (*?* for the list of verbs)" + ), + false, + ) + .display(&mut w, screen) + } else { + match con.verb_store.search(&invocation.name) { + PrefixSearchResult::NoMatch => Status::new( + task, + mad_inline!("No matching verb (*?* for the list of verbs)"), + true, + ) + .display(&mut w, screen), + PrefixSearchResult::Match(verb) => { + let line = self.displayed_tree().selected_line(); + verb.write_status(&mut w, task, line.path.clone(), invocation, screen) + } + PrefixSearchResult::TooManyMatches(completions) => Status::new( + task, + Composite::from_inline(&format!( + "Possible verbs: {}", + completions + .iter() + .map(|c| format!("*{}*", c)) + .collect::<Vec<String>>() + .join(", "), + )), + false, + ) + .display(&mut w, screen), + } + } + } + _ => { + Status::new(task, self.normal_status_message(false), false) + .display(&mut w, screen) + } + } + } + + fn can_execute(&self, verb_index: usize, con: &AppContext) -> bool { + self.displayed_tree() + .selected_line() + .is_of(con.verb_store.verbs[verb_index].selection_condition) + } + + fn apply( + &mut self, + cmd: &mut Command, + screen: &mut Screen, + con: &AppContext, + ) -> Result<AppStateCmdResult, ProgramError> { + self.pending_pattern = Pattern::None; + let page_height = BrowserState::page_height(screen); + match &cmd.action { + Action::Back => { + if self.filtered_tree.is_some() { + self.filtered_tree = None; + cmd.raw.clear(); + Ok(AppStateCmdResult::Keep) + } else if self.tree.selection > 0 { + self.tree.selection = 0; + cmd.raw.clear(); + Ok(AppStateCmdResult::Keep) + } else { + Ok(AppStateCmdResult::PopState) + } + } + Action::MoveSelection(dy) => { + self.displayed_tree_mut().move_selection(*dy, page_height); + Ok(AppStateCmdResult::Keep) + } + Action::Click(_, y) => { + let y = *y as i32; + self.displayed_tree_mut().try_select_y(y); + Ok(AppStateCmdResult::Keep) + } + Action::DoubleClick(_, y) => { + if self.displayed_tree().selection == *y as usize { + self.open_selection_stay_in_broot(screen, con) + } else { + // A double click always come after a simple click at + // same position. If it's not the selected line, it means + // the click wasn't on a selectable/openable tree line + Ok(AppStateCmdResult::Keep) + } + } + Action::OpenSelection => self.open_selection_stay_in_broot(screen, con), + Action::AltOpenSelection => self.open_selection_quit_broot(screen, con), + Action::FuzzyPatternEdit(pat) => { + match pat.len() { + 0 => { + self.filtered_tree = None; + } + _ => { + self.pending_pattern = Pattern::fuzzy(pat); + } + } + Ok(AppStateCmdResult::Keep) + } + Action::Help => Ok(AppStateCmdResult::NewState( + Box::new(HelpState::new(screen, con)), + Command::new(), + )), + Action::Next => { + self.displayed_tree_mut().try_select_next_match(); + self.displayed_tree_mut().make_selection_visible(page_height); + Ok(AppStateCmdResult::Keep) + } + Action::Previous => { + if let Some(tree) = &mut self.filtered_tree { + tree.try_select_previous_match(); + tree.make_selection_visible(page_height); + } + Ok(AppStateCmdResult::Keep) + } + Action::RegexEdit(pat, flags) => Ok(match Pattern::regex(pat, flags) { + Ok(regex_pattern) => { + self.pending_pattern = regex_pattern; + AppStateCmdResult::Keep + } + Err(e) => { + // FIXME details + AppStateCmdResult::DisplayError(format!("{}", e)) + } + }), + Action::Resize(w, h) => { + screen.set_terminal_size(*w, *h, con); + Ok(AppStateCmdResult::RefreshState { clear_cache: false }) + } + Action::VerbIndex(index) => { + let verb = &con.verb_store.verbs[*index]; + self.execute_verb(verb, &verb.invocation, screen, con) + } + Action::VerbInvocate(invocation) => match con.verb_store.search(&invocation.name) { + PrefixSearchResult::Match(verb) => { + self.execute_verb(verb, &invocation, screen, con) + } + _ => Ok(AppStateCmdResult::verb_not_found(&invocation.name)), + }, + _ => Ok(AppStateCmdResult::Keep), + } + } + + /// do some work, totally or partially, if there's some to do. + /// Stop as soon as the dam asks for interruption + fn do_pending_task(&mut self, screen: &mut Screen, dam: &mut Dam) { + if self.pending_pattern.is_some() { + let pattern_str = self.pending_pattern.to_string(); + let mut options = self.tree.options.clone(); + options.pattern = self.pending_pattern.take(); + let root = self.tree.root().clone(); + let len = self.tree.lines.len() as u16; + let builder = match TreeBuilder::from(root, options, len as usize) { + Ok(builder) => builder, + Err(e) => { + warn!("Error while preparing tree builder: {:?}", e); + return; + } + }; + let mut filtered_tree = time!( + Info, + "tree filtering", + pattern_str, + builder.build(self.total_search_required, dam), + ); // can be None if a cancellation was required + self.total_search_required = false; + if let Some(ref mut ft) = filtered_tree { + ft.try_select_best_match(); + ft.make_selection_visible(BrowserState::page_height(screen)); + self.filtered_tree = filtered_tree; + } + + } else if self.displayed_tree().is_missing_git_status_computation() { + let root_path = self.displayed_tree().root(); + let git_status = git::get_tree_status(root_path, dam); + self.displayed_tree_mut().git_status = git_status; + } else { + self.displayed_tree_mut().fetch_some_missing_dir_size(dam); + } + } + + fn display( + &mut self, + mut w: &mut dyn Write, + screen: &Screen, + _con: &AppContext, + ) -> Result<(), ProgramError> { + screen.goto(&mut w, 0, 0)?; + let dp = DisplayableTree { + tree: &self.displayed_tree(), + skin: &screen.skin, + area: termimad::Area { + left: 0, + top: 0, + width: screen.width, + height: screen.height - 2, + }, + in_app: true, + }; + dp.write_on(&mut w) + } + + 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) { + warn!("refreshing base tree failed : {:?}", e); + } + // refresh the filtered tree, if any + Command::from_pattern(match self.filtered_tree { + Some(ref mut tree) => { + if let Err(e) = tree.refresh(page_height) { + warn!("refreshing filtered tree failed : {:?}", e); + } + &tree.options.pattern + } + None => &self.tree.options.pattern, + }) + } + + /// draw the flags at the bottom right of the screen + fn write_flags( + &self, + mut w: &mut dyn Write, + screen: &mut Screen, + _con: &AppContext, + ) -> Result<(), ProgramError> { + let tree = self.displayed_tree(); + let total_char_size = screens::FLAGS_AREA_WIDTH; + screen.goto_clear( + &mut w, + screen.width - total_char_size - 1, + screen.height - 1, + )?; + let h_value = if tree.options.show_hidden { 'y' } else { 'n' }; + let gi_value = if tree.options.respect_git_ignore { 'y' } else { 'n' }; + screen.skin.flag_label.queue_str(&mut w, " h:")?; + screen.skin.flag_value.queue(&mut w, h_value)?; + screen.skin.flag_label.queue_str(&mut w, " gi:")?; + screen.skin.flag_value.queue(&mut w, gi_value)?; + Ok(()) + } +} |