use {
crate::{
app::*,
command::*,
display::*,
errors::{ProgramError, TreeBuildError},
flag::Flag,
git,
path::{self, PathAnchor},
pattern::*,
print,
stage::*,
task_sync::Dam,
tree::*,
tree_build::TreeBuilder,
verb::*,
},
opener,
std::path::{Path, PathBuf},
};
pub struct BrowserState {
pub tree: Tree,
pub filtered_tree: Option<Tree>,
mode: Mode, pending_task: Option<BrowserTask>, }
#[derive(Debug)]
enum BrowserTask {
Search {
pattern: InputPattern,
total: bool,
},
StageAll {
pattern: InputPattern,
file_type_condition: FileTypeCondition,
},
}
impl BrowserState {
pub fn new(
path: PathBuf,
mut options: TreeOptions,
screen: Screen,
con: &AppContext,
dam: &Dam,
) -> Result<BrowserState, TreeBuildError> {
#[cfg(not(target_os = "windows"))]
let path = path.canonicalize().unwrap_or(path);
let pending_task = options
.pattern
.take()
.as_option()
.map(|pattern| BrowserTask::Search {
pattern,
total: false,
});
let builder = TreeBuilder::from(path, options, BrowserState::page_height(screen), con)?;
let tree = builder.build_tree(false, dam)?;
Ok(BrowserState {
tree,
filtered_tree: None,
mode: con.initial_mode(),
pending_task,
})
}
fn search(
&mut self,
pattern: InputPattern,
total: bool,
) {
self.pending_task = Some(BrowserTask::Search { pattern, total });
}
fn modified(
&self,
screen: Screen,
root: PathBuf,
options: TreeOptions,
message: Option<&'static str>,
in_new_panel: bool,
con: &AppContext,
) -> CmdResult {
let tree = self.displayed_tree();
let mut new_state = BrowserState::new(root, options, screen, con, &Dam::unlimited());
if let Ok(bs) = &mut new_state {
if tree.selection != 0 {
bs.displayed_tree_mut()
.try_select_path(&tree.selected_line().path);
}
}
CmdResult::from_optional_browser_state(new_state, message, in_new_panel)
}
pub fn root(&self) -> &Path {
self.tree.root()
}
pub fn page_height(screen: Screen) -> usize {
screen.height as usize - 2 }
pub fn displayed_tree(&self) -> &Tree {
self.filtered_tree.as_ref().unwrap_or(&self.tree)
}
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: Screen,
con: &AppContext,
in_new_panel: bool,
keep_pattern: bool,
) -> Result<CmdResult, ProgramError> {
let tree = self.displayed_tree();
let line = tree.selected_line();
let mut target = line.target().to_path_buf();
if line.is_dir() {
if tree.selection == 0 {
if let Some(parent) = target.parent() {
target = PathBuf::from(parent);
}
}
let dam = Dam::unlimited();
Ok(CmdResult::from_optional_browser_state(
BrowserState::new(
target,
if keep_pattern {
tree.options.clone()
} else {
tree.options.without_pattern()
},
screen,
con,
&dam,
),
None,
in_new_panel,
))
} else {
match opener::open(&target) {
Ok(exit_status) => {
info!("open returned with exit_status {exit_status:?}");
Ok(CmdResult::Keep)
}
Err(e) => Ok(CmdResult::error(format!("{e:?}"))),
}
}
}
pub fn go_to_parent(
&mut self,
screen: Screen,
con: &AppContext,
in_new_panel: bool,
) -> CmdResult {
match &self.displayed_tree().selected_line().path.parent() {
Some(path) => CmdResult::from_optional_browser_state(
BrowserState::new(
path.to_path_buf(),
self.displayed_tree().options.without_pattern(),
screen,
con,
&Dam::unlimited(),
),
None,
in_new_panel,
),
None => CmdResult::error("no parent found"),
}
}
}
impl PanelState for BrowserState {
fn tree_root(&self) -> Option<&Path> {
Some(self.root())
}
fn get_type(&self) -> PanelStateType {
PanelStateType::Tree
}
fn set_mode(
&mut self,
mode: Mode,
) {
self.mode = mode;
}
fn get_mode(&self) -> Mode {
self.mode
}
fn get_pending_task(&self) -> Option<&'static str> {
if self.displayed_tree().has_dir_missing_sum() {
Some("computing stats")
} else if self.displayed_tree().is_missing_git_status_computation() {
Some("computing git status")
} else {
self.pending_task.as_ref().map(|task| match task {
BrowserTask::Search { .. } => "searching",
BrowserTask::StageAll { .. } => "staging",
})
}
}
fn watchable_paths(&self) -> Vec<PathBuf> {
let mut paths = Vec::new();
for line in &self.tree.lines {
paths.push(line.path.clone());
}
paths
}
fn selected_path(&self) -> Option<&Path> {
Some(&self.displayed_tree().selected_line().path)
}
fn selection(&self) -> Option<Selection<'_>> {
let tree = self.displayed_tree();
let mut selection = tree.selected_line().as_selection();
selection.line = tree
.options
.pattern
.pattern
.get_match_line_count(selection.path)
.unwrap_or(0);
Some(selection)
}
fn tree_options(&self) -> TreeOptions {
self.displayed_tree().options.clone()
}
fn with_new_options(
&mut self,
screen: Screen,
change_options: &dyn Fn(&mut TreeOptions) -> &'static str,
in_new_panel: bool,
con: &AppContext,
) -> CmdResult {
let tree = self.displayed_tree();
let mut options = tree.options.clone();
let message = change_options(&mut options);
let message = Some(message);
self.modified(
screen,
tree.root().clone(),
options,
message,
in_new_panel,
con,
)
}
fn clear_pending(&mut self) {
self.pending_task = None;
}
fn on_click(
&mut self,
_x: u16,
y: u16,
_screen: Screen,
_con: &AppContext,
) -> Result<CmdResult, ProgramError> {
self.displayed_tree_mut().try_select_y(y as usize);
Ok(CmdResult::Keep)
}
fn on_double_click(
&mut self,
_x: u16,
y: u16,
screen: Screen,
con: &AppContext,
) -> Result<CmdResult, ProgramError> {
if self.displayed_tree().selection == y as usize {
self.open_selection_stay_in_broot(screen, con, false, false)
} else {
Ok(CmdResult::Keep)
}
}
fn on_pattern(
&mut self,
pat: InputPattern,
_app_state: &AppState,
_con: &AppContext,
) -> Result<CmdResult, ProgramError> {
if pat.is_none() {
self.filtered_tree = None;
}
if let Some(filtered_tree) = &self.filtered_tree {
if pat != filtered_tree.options.pattern {
self.search(pat, false);
}
} else {
self.search(pat, false);
}
Ok(CmdResult::Keep)
}
fn on_internal(
&mut self,
w: &mut W,
invocation_parser: Option<&InvocationParser>,
internal_exec: &InternalExecution,
input_invocation: Option<&VerbInvocation>,
trigger_type: TriggerType,
app_state: &mut AppState,
cc: &CmdContext,
) -> Result<CmdResult, ProgramError> {
debug!("browser_state on_internal {internal_exec:?}");
let con = &cc.app.con;
let screen = cc.app.screen;
let page_height = BrowserState::page_height(cc.app.screen);
let bang = input_invocation
.map(|inv| inv.bang)
.unwrap_or(internal_exec.bang);
Ok(match internal_exec.internal {
Internal::back => {
if let Some(filtered_tree) = &self.filtered_tree {
let filtered_selection = &filtered_tree.selected_line().path;
if self.tree.try_select_path(filtered_selection) {
self.tree.make_selection_visible(page_height);
}
self.filtered_tree = None;
CmdResult::Keep
} else if self.tree.selection > 0 {
self.tree.selection = 0;
CmdResult::Keep
} else {
CmdResult::PopState
}
}
Internal::focus => {
let tree = self.displayed_tree();
internal_focus::on_internal(
internal_exec,
input_invocation,
trigger_type,
&tree.selected_line().path,
tree.is_root_selected(),
tree.options.clone(),
app_state,
cc,
)
}
Internal::line_down => {
let count = get_arg(input_invocation, internal_exec, 1);
self.displayed_tree_mut()
.move_selection(count, page_height, true);
CmdResult::Keep
}
Internal::line_down_no_cycle => {
let count = get_arg(input_invocation, internal_exec, 1);
self.displayed_tree_mut()
.move_selection(count, page_height, false);
CmdResult::Keep
}
Internal::line_up => {
let count = get_arg(input_invocation, internal_exec, 1);
self.displayed_tree_mut()
.move_selection(-count, page_height, true);
CmdResult::Keep
}
Internal::line_up_no_cycle => {
let count = get_arg(input_invocation, internal_exec, 1);
self.displayed_tree_mut()
.move_selection(-count, page_height, false);
CmdResult::Keep
}
Internal::next_dir => {
self.displayed_tree_mut()
.try_select_next_filtered(TreeLine::is_dir, page_height);
CmdResult::Keep
}
Internal::next_match => {
self.displayed_tree_mut()
.try_select_next_filtered(|line| line.direct_match, page_height);
CmdResult::Keep
}
Internal::next_same_depth => {
self.displayed_tree_mut()
.try_select_next_same_depth(page_height);
CmdResult::Keep
}
Internal::open_stay => self.open_selection_stay_in_broot(screen, con, bang, false)?,
Internal::open_stay_filter => {
self.open_selection_stay_in_broot(screen, con, bang, true)?
}
Internal::page_down => {
let tree = self.displayed_tree_mut();
if !tree.try_scroll(page_height as i32, page_height) {
tree.try_select_last(page_height);
}
CmdResult::Keep
}
Internal::page_up => {
let tree = self.displayed_tree_mut();
if !tree.try_scroll(-(page_height as i32), page_height) {
tree.try_select_first();
}
CmdResult::Keep
}
Internal::panel_left => {
let areas = &cc.panel.areas;
if areas.is_first() && areas.nb_pos < con.max_panels_count {
internal_focus::new_panel_on_path(
self.displayed_tree().selected_line().path.clone(),
screen,
self.displayed_tree().options.clone(),
PanelPurpose::None,
con,
HDir::Left,
)
} else {
CmdResult::HandleInApp(Internal::panel_left_no_open)
}
}
Internal::panel_left_no_open => CmdResult::HandleInApp(Internal::panel_left_no_open),
Internal::panel_right => {
let areas = &cc.panel.areas;
let selected_path = &self.displayed_tree().selected_line().path;
if areas.is_last() && areas.nb_pos < con.max_panels_count {
let purpose = if selected_path.is_file() && cc.app.preview_panel.is_none() {
PanelPurpose::Preview
} else {
PanelPurpose::None
};
internal_focus::new_panel_on_path(
selected_path.clone(),
screen,
self.displayed_tree().options.clone(),
purpose,
con,
HDir::Right,
)
} else {
CmdResult::HandleInApp(Internal::panel_right_no_open)
}
}
Internal::panel_right_no_open => CmdResult::HandleInApp(Internal::panel_right_no_open),
Internal::parent => self.go_to_parent(screen, con, bang),
Internal::previous_dir => {
self.displayed_tree_mut()
.try_select_previous_filtered(TreeLine::is_dir, page_height);
CmdResult::Keep
}
Internal::previous_match => {
self.displayed_tree_mut()
.try_select_previous_filtered(|line| line.direct_match, page_height);
CmdResult::Keep
}
Internal::previous_same_depth => {
self.displayed_tree_mut()
.try_select_previous_same_depth(page_height);
CmdResult::Keep
}
Internal::print_tree => {
print::print_tree(self.displayed_tree(), cc.app.screen, cc.app.panel_skin, con)?
}
Internal::quit => CmdResult::Quit,
Internal::root_down => {
let tree = self.displayed_tree();
if tree.selection > 0 {
let root_len = tree.root().components().count();
let new_root = tree
.selected_line()
.path
.components()
.take(root_len + 1)
.collect();
self.modified(screen, new_root, tree.options.clone(), None, bang, con)
} else {
CmdResult::error("No selected line")
}
}
Internal::root_up => {
let tree = self.displayed_tree();
let root = tree.root();
if let Some(new_root) = root.parent() {
self.modified(
screen,
new_root.to_path_buf(),
tree.options.clone(),
None,
bang,
con,
)
} else {
CmdResult::error(format!("{root:?} has no parent"))
}
}
Internal::search_again => {
match self.filtered_tree.as_ref().map(|t| t.total_search) {
None => {
CmdResult::HandleInApp(Internal::search_again)
}
Some(true) => CmdResult::error(
"search was already total: all possible matches have been ranked",
),
Some(false) => {
self.search(self.displayed_tree().options.pattern.clone(), true);
CmdResult::Keep
}
}
}
Internal::select => internal_select::on_internal(
internal_exec,
input_invocation,
trigger_type,
self.displayed_tree_mut(),
app_state,
cc,
),
Internal::select_first => {
self.displayed_tree_mut().try_select_first();
CmdResult::Keep
}
Internal::select_last => {
let page_height = BrowserState::page_height(screen);
self.displayed_tree_mut().try_select_last(page_height);
CmdResult::Keep
}
Internal::show => {
let path = internal_path::determine_path(
internal_exec,
input_invocation,
trigger_type,
self.displayed_tree(),
app_state,
cc,
);
match path {
Some(path) => {
let res = self.displayed_tree_mut().show_path(&path, con);
match res {
Ok(()) => {
let page_height = BrowserState::page_height(screen);
self.displayed_tree_mut()
.make_selection_visible(page_height);
CmdResult::Keep
}
Err(e) => CmdResult::DisplayError(format!("{e}")),
}
}
None => CmdResult::Keep,
}
}
Internal::stage_all_directories => {
let pattern = self.displayed_tree().options.pattern.clone();
let file_type_condition = FileTypeCondition::Directory;
self.pending_task = Some(BrowserTask::StageAll {
pattern,
file_type_condition,
});
if cc.app.stage_panel.is_none() {
let stage_options = self.tree.options.without_pattern();
CmdResult::NewPanel {
state: Box::new(StageState::new(app_state, stage_options, con)),
purpose: PanelPurpose::None,
direction: HDir::Right,
}
} else {
CmdResult::Keep
}
}
Internal::stage_all_files => {
let pattern = self.displayed_tree().options.pattern.clone();
let file_type_condition = FileTypeCondition::File;
self.pending_task = Some(BrowserTask::StageAll {
pattern,
file_type_condition,
});
if cc.app.stage_panel.is_none() {
let stage_options = self.tree.options.without_pattern();
CmdResult::NewPanel {
state: Box::new(StageState::new(app_state, stage_options, con)),
purpose: PanelPurpose::None,
direction: HDir::Right,
}
} else {
CmdResult::Keep
}
}
Internal::start_end_panel => {
if cc.panel.purpose.is_arg_edition() {
debug!("start_end understood as end");
CmdResult::ClosePanel {
validate_purpose: true,
panel_ref: PanelReference::Active,
clear_cache: false,
}
} else {
debug!("start_end understood as start");
let tree_options = self.displayed_tree().options.clone();
if let Some(input_invocation) = input_invocation {
let path = if let Some(input_arg) = &input_invocation.args {
path::path_from(self.root(), PathAnchor::Unspecified, input_arg)
} else {
self.root().to_path_buf()
};
let arg_type = SelectionType::Any; let purpose = PanelPurpose::ArgEdition { arg_type };
internal_focus::new_panel_on_path(
path,
screen,
tree_options,
purpose,
con,
HDir::Right,
)
} else {
internal_focus::new_panel_on_path(
self.displayed_tree().selected_line().path.clone(),
screen,
tree_options,
PanelPurpose::None,
con,
HDir::Right,
)
}
}
}
Internal::total_search => match self.filtered_tree.as_ref().map(|t| t.total_search) {
None => CmdResult::error("this verb can be used only after a search"),
Some(true) => CmdResult::error(
"search was already total: all possible matches have been ranked",
),
Some(false) => {
self.search(self.displayed_tree().options.pattern.clone(), true);
CmdResult::Keep
}
},
Internal::trash => {
let path = self.displayed_tree().selected_line().path.clone();
info!("trash {:?}", &path);
#[cfg(any(target_os = "windows", all(unix, not(any(target_os = "ios", target_os = "android")))))]
match trash::delete(&path) {
Ok(()) => CmdResult::RefreshState { clear_cache: true },
Err(e) => {
warn!("trash error: {:?}", &e);
CmdResult::DisplayError(format!("trash error: {:?}", &e))
}
}
#[cfg(not(any(target_os = "windows", all(unix, not(any(target_os = "ios", target_os = "android"))))))]
CmdResult::DisplayError("trash not supported on this platform".into())
}
Internal::up_tree => match self.displayed_tree().root().parent() {
Some(path) => internal_focus::on_path(
path.to_path_buf(),
screen,
self.displayed_tree().options.clone(),
bang,
con,
),
None => CmdResult::error("no parent found"),
},
_ => self.on_internal_generic(
w,
invocation_parser,
internal_exec,
input_invocation,
trigger_type,
app_state,
cc,
)?,
})
}
fn no_verb_status(
&self,
has_previous_state: bool,
con: &AppContext,
width: usize,
) -> Status {
let tree = self.displayed_tree();
if tree.is_empty() && tree.build_report.hidden_count > 0 {
let mut parts = Vec::new();
if let Some(md) = con.standard_status.all_files_hidden.clone() {
parts.push(md);
}
if let Some(md) = con.standard_status.all_files_ignored.clone() {
parts.push(md);
}
if !parts.is_empty() {
return Status::from_error(parts.join(". "));
}
}
let mut ssb = con.standard_status.builder(
PanelStateType::Tree,
tree.selected_line().as_selection(),
width,
);
ssb.has_previous_state = has_previous_state;
ssb.is_filtered = self.filtered_tree.is_some();
ssb.has_removed_pattern = false;
ssb.on_tree_root = tree.selection == 0;
ssb.status()
}
fn do_pending_task(
&mut self,
app_state: &mut AppState,
screen: Screen,
con: &AppContext,
dam: &mut Dam,
) -> Result<(), ProgramError> {
if let Some(pending_task) = self.pending_task.take() {
match pending_task {
BrowserTask::Search { pattern, total } => {
let pattern_str = pattern.raw.clone();
let mut options = self.tree.options.clone();
options.pattern = pattern;
let root = self.tree.root().clone();
let page_height = BrowserState::page_height(screen);
let builder = TreeBuilder::from(root, options, page_height, con)?;
let filtered_tree = time!(
Info,
"tree filtering",
&pattern_str,
builder.build_tree(total, dam),
);
if let Ok(mut ft) = filtered_tree {
ft.try_select_best_match();
ft.make_selection_visible(BrowserState::page_height(screen));
self.filtered_tree = Some(ft);
}
}
BrowserTask::StageAll {
pattern,
file_type_condition,
} => {
debug!("stage all pattern: {pattern:?}");
let tree = self.displayed_tree();
let root = tree.root().clone();
let mut options = tree.options.clone();
let total_search = true;
options.pattern = pattern; let builder = TreeBuilder::from(root, options, con.max_staged_count, con);
let mut paths = builder.and_then(|mut builder| {
builder.matches_max = Some(con.max_staged_count);
time!(builder.build_paths(total_search, dam, |line| {
debug!("??staging {:?}", &line.path);
file_type_condition.accepts_path(&line.path)
}))
})?;
for path in paths.drain(..) {
app_state.stage.add(path);
}
}
}
} 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_sum(dam, con);
}
Ok(())
}
fn display(
&mut self,
w: &mut W,
disc: &DisplayContext,
) -> Result<(), ProgramError> {
let dp = DisplayableTree {
app_state: Some(disc.app_state),
tree: self.displayed_tree(),
skin: &disc.panel_skin.styles,
ext_colors: &disc.con.ext_colors,
area: disc.state_area.clone(),
in_app: true,
};
dp.write_on(w)
}
fn refresh(
&mut self,
screen: Screen,
con: &AppContext,
) -> Command {
let page_height = BrowserState::page_height(screen);
if let Err(e) = self.tree.refresh(page_height, con) {
warn!("refreshing base tree failed : {e:?}");
}
Command::from_pattern(match self.filtered_tree {
Some(ref mut tree) => {
if let Err(e) = tree.refresh(page_height, con) {
warn!("refreshing filtered tree failed : {e:?}");
}
&tree.options.pattern
}
None => &self.tree.options.pattern,
})
}
fn get_flags(&self) -> Vec<Flag> {
let options = &self.displayed_tree().options;
vec![
Flag {
name: "h",
value: if options.show_hidden { "y" } else { "n" },
},
Flag {
name: "gi",
value: if options.respect_git_ignore { "y" } else { "n" },
},
]
}
fn get_starting_input(&self) -> String {
if let Some(BrowserTask::Search { pattern, .. }) = self.pending_task.as_ref() {
pattern.raw.clone()
} else {
self.displayed_tree().options.pattern.raw.clone()
}
}
}