use std::io::{self, Write};
use std::path::PathBuf;
use std::result::Result;
use std::time::Instant;
use crate::app::{AppState, AppStateCmdResult};
use crate::app_context::AppContext;
use crate::commands::{Action, Command};
use crate::errors::TreeBuildError;
use crate::external::Launchable;
use crate::flat_tree::{LineType, Tree};
use crate::help_states::HelpState;
use crate::patterns::Pattern;
use crate::screens::Screen;
use crate::status::Status;
use crate::task_sync::TaskLifetime;
use crate::tree_build::TreeBuilder;
use crate::tree_options::{OptionBool, TreeOptions};
use crate::tree_views::TreeView;
use crate::verbs::{VerbExecutor};
use crate::verb_store::{PrefixSearchResult};
pub struct BrowserState {
pub tree: Tree,
pub filtered_tree: Option<Tree>,
pending_pattern: Pattern, }
impl BrowserState {
pub fn new(
path: PathBuf,
mut options: TreeOptions,
screen: &Screen,
tl: &TaskLifetime,
) -> Result<Option<BrowserState>, TreeBuildError> {
let pending_pattern = options.pattern;
options.pattern = Pattern::None;
let builder = TreeBuilder::from(path, options, BrowserState::page_height(screen) as usize)?;
Ok(match builder.build(tl) {
Some(tree) => Some(BrowserState {
tree,
filtered_tree: None,
pending_pattern,
}),
None => None, })
}
pub fn with_new_options(
&self,
screen: &Screen,
change_options: &Fn(&mut TreeOptions),
) -> AppStateCmdResult {
let tree = match &self.filtered_tree {
Some(tree) => &tree,
None => &self.tree,
};
let mut options = tree.options.clone();
change_options(&mut options);
AppStateCmdResult::from_optional_state(
BrowserState::new(
tree.root().clone(),
options,
screen,
&TaskLifetime::unlimited(),
),
tree.options.pattern.to_command(),
)
}
fn page_height(screen: &Screen) -> i32 {
i32::from(screen.h) - 2
}
pub fn displayed_tree(&self) -> &Tree {
match &self.filtered_tree {
Some(tree) => &tree,
None => &self.tree,
}
}
}
impl AppState for BrowserState {
fn apply(
&mut self,
cmd: &mut Command,
screen: &mut Screen,
con: &AppContext,
) -> io::Result<AppStateCmdResult> {
self.pending_pattern = Pattern::None;
let page_height = BrowserState::page_height(screen);
Ok(match &cmd.action {
Action::Back => {
if self.filtered_tree.is_some() {
self.filtered_tree = None;
cmd.raw.clear();
AppStateCmdResult::Keep
} else if self.tree.selection > 0 {
self.tree.selection = 0;
cmd.raw.clear();
AppStateCmdResult::Keep
} else {
AppStateCmdResult::PopState
}
}
Action::MoveSelection(dy) => {
match self.filtered_tree {
Some(ref mut tree) => {
tree.move_selection(*dy, page_height);
}
None => {
self.tree.move_selection(*dy, page_height);
}
};
AppStateCmdResult::Keep
}
Action::ScrollPage(dp) => {
if page_height < self.displayed_tree().lines.len() as i32 {
let dy = dp * page_height;
match self.filtered_tree {
Some(ref mut tree) => {
tree.try_scroll(dy, page_height);
}
None => {
self.tree.try_scroll(dy, page_height);
}
};
}
AppStateCmdResult::Keep
}
Action::OpenSelection => {
let tree = match &self.filtered_tree {
Some(tree) => tree,
None => &self.tree,
};
if tree.selection == 0 {
AppStateCmdResult::Quit
} else {
let line = tree.selected_line();
let tl = TaskLifetime::unlimited();
match &line.line_type {
LineType::File => {
AppStateCmdResult::Launch(Launchable::opener(line.path.clone()))
}
LineType::Dir | LineType::SymLinkToDir(_) => {
AppStateCmdResult::from_optional_state(
BrowserState::new(
line.target(),
tree.options.without_pattern(),
screen,
&tl,
),
Command::new(),
)
}
LineType::SymLinkToFile(target) => {
AppStateCmdResult::Launch(Launchable::opener(PathBuf::from(target)))
}
_ => {
unreachable!();
}
}
}
}
Action::AltOpenSelection => {
let tree = match &self.filtered_tree {
Some(tree) => tree,
None => &self.tree,
};
let line = tree.selected_line();
let cd_idx = con.verb_store.index_of("cd");
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)?,
_ => AppStateCmdResult::verb_not_found(&invocation.key),
},
Action::FuzzyPatternEdit(pat) => match pat.len() {
0 => {
self.filtered_tree = None;
AppStateCmdResult::Keep
}
_ => {
self.pending_pattern = Pattern::fuzzy(pat);
AppStateCmdResult::Keep
}
},
Action::RegexEdit(pat, flags) => {
match Pattern::regex(pat, flags) {
Ok(regex_pattern) => {
self.pending_pattern = regex_pattern;
AppStateCmdResult::Keep
}
Err(e) => {
AppStateCmdResult::DisplayError(format!("{}", e))
}
}
}
Action::Help => {
AppStateCmdResult::NewState(Box::new(HelpState::new(screen)), Command::new())
}
Action::Refresh => AppStateCmdResult::RefreshState,
Action::Quit => AppStateCmdResult::Quit,
Action::Next => {
if let Some(ref mut tree) = self.filtered_tree {
tree.try_select_next_match();
tree.make_selection_visible(page_height);
}
AppStateCmdResult::Keep
}
_ => AppStateCmdResult::Keep,
})
}
fn has_pending_tasks(&self) -> bool {
if self.pending_pattern.is_some() {
return true;
}
if self.displayed_tree().has_dir_missing_size() {
return true;
}
false
}
fn do_pending_task(&mut self, screen: &mut Screen, tl: &TaskLifetime) {
if self.pending_pattern.is_some() {
let start = Instant::now();
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 mut filtered_tree = match TreeBuilder::from(root, options, len as usize) {
Ok(builder) => builder.build(tl),
Err(e) => {
let _ = screen.write_status_err(&e.to_string());
warn!("Error while building tree: {:?}", e);
return;
}
};
if let Some(ref mut filtered_tree) = filtered_tree {
info!(
"Tree search with pattern {} took {:?}",
&filtered_tree.options.pattern,
start.elapsed()
);
filtered_tree.try_select_best_match();
filtered_tree.make_selection_visible(BrowserState::page_height(screen));
} self.filtered_tree = filtered_tree;
return;
}
if let Some(ref mut tree) = self.filtered_tree {
tree.fetch_some_missing_dir_size(tl);
} else {
self.tree.fetch_some_missing_dir_size(tl);
}
}
fn display(&mut self, screen: &mut Screen, _con: &AppContext) -> io::Result<()> {
let mut tree_view = TreeView::from_screen(screen);
tree_view.write_tree(&self.displayed_tree())
}
fn write_status(&self, screen: &mut Screen, cmd: &Command, con: &AppContext) -> io::Result<()> {
match &cmd.action {
Action::FuzzyPatternEdit(_) => {
screen.write_status_text("Hit <enter> to select, <esc> to remove the filter")
}
Action::RegexEdit(_, _) => {
screen.write_status_text("Hit <enter> to select, <esc> to remove the filter")
}
Action::VerbEdit(invocation) => {
match con.verb_store.search(&invocation.key) {
PrefixSearchResult::NoMatch => {
screen.write_status_err("No matching verb (':?' for the list of verbs)")
}
PrefixSearchResult::Match(verb) => {
if let Some(err) = verb.match_error(invocation) {
screen.write_status_err(&err)
} else {
let line = match &self.filtered_tree {
Some(tree) => tree.selected_line(),
None => self.tree.selected_line(),
};
screen.write_status_text(
&format!(
"Hit <enter> to {} : {}",
&verb.invocation.key,
verb.description_for(line.target(), &invocation.args)
)
.to_string(),
)
}
}
PrefixSearchResult::TooManyMatches => screen.write_status_text(
"Type a verb then <enter> to execute it (':?' for the list of verbs)",
),
}
}
_ => {
let tree = self.displayed_tree();
if tree.selection == 0 {
screen.write_status_text(
"Hit <enter> to quit, '?' for help, or a few letters to search",
)
} else {
let line = &tree.lines[tree.selection];
screen.write_status_text(if line.is_dir() {
"Hit <enter> to focus, <alt><enter> to cd, or a space then a verb"
} else {
"Hit <enter> to open the file, or type a space then a verb"
})
}
}
}
}
fn refresh(
&mut self,
screen: &Screen,
_con: &AppContext,
) -> Command {
let page_height = BrowserState::page_height(screen) as usize;
if let Err(e) = self.tree.refresh(page_height) {
warn!("refreshing base tree failed : {:?}", e);
}
if let Some(ref mut tree) = self.filtered_tree {
if let Err(e) = tree.refresh(page_height) {
warn!("refreshing filtered tree failed : {:?}", e);
}
tree.options.pattern.to_command()
} else {
self.tree.options.pattern.to_command()
}
}
fn write_flags(&self, screen: &mut Screen, _con: &AppContext) -> io::Result<()> {
let tree = match &self.filtered_tree {
Some(tree) => &tree,
None => &self.tree,
};
let total_char_size = 9;
write!(
screen.stdout,
"{}{}{}{} h:{}{}{}{}{} gi:{}{}{}",
termion::cursor::Goto(screen.w - total_char_size, screen.h),
screen.skin.flag_label.fg,
screen.skin.flag_label.bg,
termion::clear::UntilNewline,
screen.skin.flag_value.fg,
screen.skin.flag_value.bg,
if tree.options.show_hidden { 'y' } else { 'n' },
screen.skin.flag_label.fg,
screen.skin.flag_label.bg,
screen.skin.flag_value.fg,
screen.skin.flag_value.bg,
match tree.options.respect_git_ignore {
OptionBool::Auto => 'a',
OptionBool::Yes => 'y',
OptionBool::No => 'n',
},
)?;
Ok(())
}
}