use {
super::*,
crate::{
command::*,
display::{Screen, W},
errors::ProgramError,
flag::Flag,
help::HelpState,
pattern::*,
preview::{PreviewMode, PreviewState},
print,
stage::*,
task_sync::Dam,
tree::*,
verb::*,
},
std::{
path::{Path, PathBuf},
str::FromStr,
},
};
pub trait PanelState {
fn get_type(&self) -> PanelStateType;
fn set_mode(&mut self, mode: Mode);
fn get_mode(&self) -> Mode;
fn clear_pending(&mut self) {}
fn on_click(
&mut self,
_x: u16,
_y: u16,
_screen: Screen,
_con: &AppContext,
) -> Result<CmdResult, ProgramError> {
Ok(CmdResult::Keep)
}
fn on_double_click(
&mut self,
_x: u16,
_y: u16,
_screen: Screen,
_con: &AppContext,
) -> Result<CmdResult, ProgramError> {
Ok(CmdResult::Keep)
}
fn on_pattern(
&mut self,
_pat: InputPattern,
_app_state: &AppState,
_con: &AppContext,
) -> Result<CmdResult, ProgramError> {
Ok(CmdResult::Keep)
}
fn on_mode_verb(
&mut self,
mode: Mode,
con: &AppContext,
) -> CmdResult {
if con.modal {
self.set_mode(mode);
CmdResult::Keep
} else {
CmdResult::error("modal mode not enabled in configuration")
}
}
fn on_internal(
&mut self,
w: &mut W,
internal_exec: &InternalExecution,
input_invocation: Option<&VerbInvocation>,
trigger_type: TriggerType,
app_state: &mut AppState,
cc: &CmdContext,
) -> Result<CmdResult, ProgramError>;
fn on_internal_generic(
&mut self,
_w: &mut W,
internal_exec: &InternalExecution,
input_invocation: Option<&VerbInvocation>,
_trigger_type: TriggerType,
app_state: &mut AppState,
cc: &CmdContext,
) -> Result<CmdResult, ProgramError> {
let con = &cc.app.con;
let screen = cc.app.screen;
let bang = input_invocation
.map(|inv| inv.bang)
.unwrap_or(internal_exec.bang);
Ok(match internal_exec.internal {
Internal::back => CmdResult::PopState,
Internal::copy_line | Internal::copy_path => {
#[cfg(not(feature = "clipboard"))]
{
CmdResult::error("Clipboard feature not enabled at compilation")
}
#[cfg(feature = "clipboard")]
{
if let Some(path) = self.selected_path() {
let path = path.to_string_lossy().to_string();
match terminal_clipboard::set_string(path) {
Ok(()) => CmdResult::Keep,
Err(_) => CmdResult::error("Clipboard error while copying path"),
}
} else {
CmdResult::error("Nothing to copy")
}
}
}
Internal::close_panel_ok => CmdResult::ClosePanel {
validate_purpose: true,
panel_ref: PanelReference::Active,
},
Internal::close_panel_cancel => CmdResult::ClosePanel {
validate_purpose: false,
panel_ref: PanelReference::Active,
},
#[cfg(unix)]
Internal::filesystems => {
let fs_state = crate::filesystems::FilesystemState::new(
self.selected_path(),
self.tree_options(),
con,
);
match fs_state {
Ok(state) => {
let bang = input_invocation
.map(|inv| inv.bang)
.unwrap_or(internal_exec.bang);
if bang && cc.app.preview_panel.is_none() {
CmdResult::NewPanel {
state: Box::new(state),
purpose: PanelPurpose::None,
direction: HDir::Right,
}
} else {
CmdResult::NewState(Box::new(state))
}
}
Err(e) => CmdResult::DisplayError(format!("{}", e)),
}
}
Internal::help => {
let bang = input_invocation
.map(|inv| inv.bang)
.unwrap_or(internal_exec.bang);
if bang && cc.app.preview_panel.is_none() {
CmdResult::NewPanel {
state: Box::new(HelpState::new(self.tree_options(), screen, con)),
purpose: PanelPurpose::None,
direction: HDir::Right,
}
} else {
CmdResult::NewState(Box::new(
HelpState::new(self.tree_options(), screen, con)
))
}
}
Internal::mode_input => self.on_mode_verb(Mode::Input, con),
Internal::mode_command => self.on_mode_verb(Mode::Command, con),
Internal::open_leave => {
if let Some(selection) = self.selection() {
selection.to_opener(con)?
} else {
CmdResult::error("no selection to open")
}
}
Internal::open_preview => self.open_preview(None, false, cc),
Internal::preview_image => self.open_preview(Some(PreviewMode::Image), false, cc),
Internal::preview_text => self.open_preview(Some(PreviewMode::Text), false, cc),
Internal::preview_binary => self.open_preview(Some(PreviewMode::Hex), false, cc),
Internal::toggle_preview => self.open_preview(None, true, cc),
Internal::sort_by_count => self.with_new_options(
screen,
&|o| {
if o.sort == Sort::Count {
o.sort = Sort::None;
o.show_counts = false;
} else {
o.sort = Sort::Count;
o.show_counts = true;
}
},
bang,
con,
),
Internal::sort_by_date => self.with_new_options(
screen,
&|o| {
if o.sort == Sort::Date {
o.sort = Sort::None;
o.show_dates = false;
} else {
o.sort = Sort::Date;
o.show_dates = true;
}
},
bang,
con,
),
Internal::sort_by_size => self.with_new_options(
screen,
&|o| {
if o.sort == Sort::Size {
o.sort = Sort::None;
o.show_sizes = false;
} else {
o.sort = Sort::Size;
o.show_sizes = true;
o.show_root_fs = true;
}
},
bang,
con,
),
Internal::no_sort => self.with_new_options(screen, &|o| o.sort = Sort::None, bang, con),
Internal::toggle_counts => {
self.with_new_options(screen, &|o| o.show_counts ^= true, bang, con)
}
Internal::toggle_dates => {
self.with_new_options(screen, &|o| o.show_dates ^= true, bang, con)
}
Internal::toggle_device_id => {
self.with_new_options(screen, &|o| o.show_device_id ^= true, bang, con)
}
Internal::toggle_files => {
self.with_new_options(screen, &|o: &mut TreeOptions| o.only_folders ^= true, bang, con)
}
Internal::toggle_hidden => {
self.with_new_options(screen, &|o| o.show_hidden ^= true, bang, con)
}
Internal::toggle_root_fs => {
self.with_new_options(screen, &|o| o.show_root_fs ^= true, bang, con)
}
Internal::toggle_git_ignore => {
self.with_new_options(screen, &|o| o.respect_git_ignore ^= true, bang, con)
}
Internal::toggle_git_file_info => {
self.with_new_options(screen, &|o| o.show_git_file_info ^= true, bang, con)
}
Internal::toggle_git_status => {
self.with_new_options(
screen, &|o| {
if o.filter_by_git_status {
o.filter_by_git_status = false;
} else {
o.filter_by_git_status = true;
o.show_hidden = true;
}
}, bang, con
)
}
Internal::toggle_perm => {
self.with_new_options(screen, &|o| o.show_permissions ^= true, bang, con)
}
Internal::toggle_sizes => self.with_new_options(
screen,
&|o| {
if o.show_sizes {
o.show_sizes = false;
o.show_root_fs = false;
} else {
o.show_sizes = true;
o.show_root_fs = true;
}
},
bang,
con,
),
Internal::toggle_trim_root => {
self.with_new_options(screen, &|o| o.trim_root ^= true, bang, con)
}
Internal::close_preview => {
if let Some(id) = cc.app.preview_panel {
CmdResult::ClosePanel {
validate_purpose: false,
panel_ref: PanelReference::Id(id),
}
} else {
CmdResult::Keep
}
}
Internal::panel_left => {
CmdResult::HandleInApp(Internal::panel_left)
}
Internal::panel_right => {
CmdResult::HandleInApp(Internal::panel_right)
}
Internal::toggle_second_tree => {
CmdResult::HandleInApp(Internal::toggle_second_tree)
}
Internal::clear_stage => {
app_state.stage.clear();
if let Some(panel_id) = cc.app.stage_panel {
CmdResult::ClosePanel {
validate_purpose: false,
panel_ref: PanelReference::Id(panel_id),
}
} else {
CmdResult::Keep
}
}
Internal::stage => self.stage(app_state, cc, con),
Internal::unstage => self.unstage(app_state, cc, con),
Internal::toggle_stage => self.toggle_stage(app_state, cc, con),
Internal::close_staging_area => {
if let Some(id) = cc.app.stage_panel {
CmdResult::ClosePanel {
validate_purpose: false,
panel_ref: PanelReference::Id(id),
}
} else {
CmdResult::Keep
}
}
Internal::open_staging_area => {
if cc.app.stage_panel.is_none() {
CmdResult::NewPanel {
state: Box::new(StageState::new(app_state, self.tree_options(), con)),
purpose: PanelPurpose::None,
direction: HDir::Right,
}
} else {
CmdResult::Keep
}
}
Internal::toggle_staging_area => {
if let Some(id) = cc.app.stage_panel {
CmdResult::ClosePanel {
validate_purpose: false,
panel_ref: PanelReference::Id(id),
}
} else {
CmdResult::NewPanel {
state: Box::new(StageState::new(app_state, self.tree_options(), con)),
purpose: PanelPurpose::None,
direction: HDir::Right,
}
}
}
Internal::print_path => print::print_paths(&self.sel_info(app_state), con)?,
Internal::print_relative_path => print::print_relative_paths(&self.sel_info(app_state), con)?,
Internal::refresh => CmdResult::RefreshState { clear_cache: true },
Internal::quit => CmdResult::Quit,
_ => CmdResult::Keep,
})
}
fn stage(
&self,
app_state: &mut AppState,
cc: &CmdContext,
con: &AppContext,
) -> CmdResult {
if let Some(path) = self.selected_path() {
let path = path.to_path_buf();
app_state.stage.add(path);
if cc.app.stage_panel.is_none() {
return CmdResult::NewPanel {
state: Box::new(StageState::new(app_state, self.tree_options(), con)),
purpose: PanelPurpose::None,
direction: HDir::Right,
};
}
} else {
warn!("no path in state");
}
CmdResult::Keep
}
fn unstage(
&self,
app_state: &mut AppState,
cc: &CmdContext,
_con: &AppContext,
) -> CmdResult {
if let Some(path) = self.selected_path() {
if app_state.stage.remove(path) && app_state.stage.is_empty() {
if let Some(panel_id) = cc.app.stage_panel {
return CmdResult::ClosePanel {
validate_purpose: false,
panel_ref: PanelReference::Id(panel_id),
};
}
}
}
CmdResult::Keep
}
fn toggle_stage(
&self,
app_state: &mut AppState,
cc: &CmdContext,
con: &AppContext,
) -> CmdResult {
if let Some(path) = self.selected_path() {
if app_state.stage.contains(path) {
self.unstage(app_state, cc, con)
} else {
self.stage(app_state, cc, con)
}
} else {
CmdResult::error("no selection")
}
}
fn execute_verb(
&mut self,
w: &mut W, verb: &Verb,
invocation: Option<&VerbInvocation>,
trigger_type: TriggerType,
app_state: &mut AppState,
cc: &CmdContext,
) -> Result<CmdResult, ProgramError> {
if verb.needs_selection && !self.has_at_least_one_selection(app_state) {
return Ok(CmdResult::error("This verb needs a selection"));
}
if verb.needs_another_panel && app_state.other_panel_path.is_none() {
return Ok(CmdResult::error("This verb needs another panel"));
}
match &verb.execution {
VerbExecution::Internal(internal_exec) => {
self.on_internal(w, internal_exec, invocation, trigger_type, app_state, cc)
}
VerbExecution::External(external) => {
self.execute_external(w, verb, external, invocation, app_state, cc)
}
VerbExecution::Sequence(seq_ex) => {
self.execute_sequence(w, verb, seq_ex, invocation, app_state, cc)
}
}
}
fn execute_external(
&mut self,
w: &mut W,
verb: &Verb,
external_execution: &ExternalExecution,
invocation: Option<&VerbInvocation>,
app_state: &mut AppState,
cc: &CmdContext,
) -> Result<CmdResult, ProgramError> {
let exec_builder = ExecutionStringBuilder::with_invocation(
&verb.invocation_parser,
self.sel_info(app_state),
app_state,
if let Some(inv) = invocation {
&inv.args
} else {
&None
},
);
external_execution.to_cmd_result(w, exec_builder, &cc.app.con)
}
fn execute_sequence(
&mut self,
_w: &mut W,
verb: &Verb,
seq_ex: &SequenceExecution,
invocation: Option<&VerbInvocation>,
app_state: &mut AppState,
_cc: &CmdContext,
) -> Result<CmdResult, ProgramError> {
let sel_info = self.sel_info(app_state);
if matches!(sel_info, SelInfo::More(_)) {
return Ok(CmdResult::error("sequences can't be executed on multiple selections"));
}
let exec_builder = ExecutionStringBuilder::with_invocation(
&verb.invocation_parser,
sel_info,
app_state,
if let Some(inv) = invocation {
&inv.args
} else {
&None
},
);
let sequence = Sequence {
raw: exec_builder.shell_exec_string(&ExecPattern::from_string(&seq_ex.sequence.raw)),
separator: seq_ex.sequence.separator.clone(),
};
Ok(CmdResult::ExecuteSequence { sequence })
}
fn on_command(
&mut self,
w: &mut W,
app_state: &mut AppState,
cc: &CmdContext,
) -> Result<CmdResult, ProgramError> {
self.clear_pending();
let con = &cc.app.con;
let screen = cc.app.screen;
match &cc.cmd {
Command::Click(x, y) => self.on_click(*x, *y, screen, con),
Command::DoubleClick(x, y) => self.on_double_click(*x, *y, screen, con),
Command::PatternEdit { raw, expr } => {
match InputPattern::new(raw.clone(), expr, con) {
Ok(pattern) => self.on_pattern(pattern, app_state, con),
Err(e) => Ok(CmdResult::DisplayError(format!("{}", e))),
}
}
Command::VerbTrigger {
index,
input_invocation,
} => self.execute_verb(
w,
&con.verb_store.verbs[*index],
input_invocation.as_ref(),
TriggerType::Other,
app_state,
cc,
),
Command::Internal {
internal,
input_invocation,
} => self.on_internal(
w,
&InternalExecution::from_internal(*internal),
input_invocation.as_ref(),
TriggerType::Other,
app_state,
cc,
),
Command::VerbInvocate(invocation) => {
let sel_info = self.sel_info(app_state);
match con.verb_store.search_sel_info(
&invocation.name,
&sel_info,
) {
PrefixSearchResult::Match(_, verb) => {
self.execute_verb(
w,
verb,
Some(invocation),
TriggerType::Input,
app_state,
cc,
)
}
_ => Ok(CmdResult::verb_not_found(&invocation.name)),
}
}
Command::None | Command::VerbEdit(_) => {
Ok(CmdResult::Keep)
}
}
}
fn open_preview(
&mut self,
prefered_mode: Option<PreviewMode>,
close_if_open: bool,
cc: &CmdContext,
) -> CmdResult {
if let Some(id) = cc.app.preview_panel {
if close_if_open {
CmdResult::ClosePanel {
validate_purpose: false,
panel_ref: PanelReference::Id(id),
}
} else {
if prefered_mode.is_some() {
CmdResult::ApplyOnPanel { id }
} else {
CmdResult::Keep
}
}
} else {
if let Some(path) = self.selected_path() {
if path.is_file() {
CmdResult::NewPanel {
state: Box::new(PreviewState::new(
path.to_path_buf(),
InputPattern::none(),
prefered_mode,
self.tree_options(),
&cc.app.con,
)),
purpose: PanelPurpose::Preview,
direction: HDir::Right,
}
} else {
CmdResult::error("only regular files can be previewed")
}
} else {
CmdResult::error("no selected file")
}
}
}
fn tree_root(&self) -> Option<&Path> {
None
}
fn selected_path(&self) -> Option<&Path>;
fn selection(&self) -> Option<Selection<'_>>;
fn sel_info<'c>(&'c self, _app_state: &'c AppState) -> SelInfo<'c> {
match self.selection() {
None => SelInfo::None,
Some(selection) => SelInfo::One(selection),
}
}
fn has_at_least_one_selection(&self, _app_state: &AppState) -> bool {
true }
fn refresh(&mut self, screen: Screen, con: &AppContext) -> Command;
fn tree_options(&self) -> TreeOptions;
fn with_new_options(
&mut self,
screen: Screen,
change_options: &dyn Fn(&mut TreeOptions),
in_new_panel: bool,
con: &AppContext,
) -> CmdResult;
fn do_pending_task(
&mut self,
_stage: &Stage,
_screen: Screen,
_con: &AppContext,
_dam: &mut Dam,
) {
unreachable!();
}
fn get_pending_task(
&self,
) -> Option<&'static str> {
None
}
fn display(
&mut self,
w: &mut W,
disc: &DisplayContext,
) -> Result<(), ProgramError>;
fn get_flags(&self) -> Vec<Flag> {
vec![]
}
fn get_starting_input(&self) -> String {
String::new()
}
fn set_selected_path(&mut self, _path: PathBuf, _con: &AppContext) {
}
fn no_verb_status(
&self,
_has_previous_state: bool,
_con: &AppContext,
) -> Status {
Status::from_message(
"Hit *esc* to get back, or a space to start a verb"
)
}
fn get_status(
&self,
app_state: &AppState,
cc: &CmdContext,
has_previous_state: bool,
) -> Status {
match &cc.cmd {
Command::PatternEdit { .. } => self.no_verb_status(has_previous_state, cc.app.con),
Command::VerbEdit(invocation) => {
if invocation.name.is_empty() {
Status::new(
"Type a verb then *enter* to execute it (*?* for the list of verbs)",
false,
)
} else {
let sel_info = self.sel_info(app_state);
match cc.app.con.verb_store.search_sel_info(
&invocation.name,
&sel_info,
) {
PrefixSearchResult::NoMatch => {
Status::new("No matching verb (*?* for the list of verbs)", true)
}
PrefixSearchResult::Match(_, verb) => {
self.get_verb_status(verb, invocation, sel_info, cc, app_state)
}
PrefixSearchResult::Matches(completions) => Status::new(
format!(
"Possible verbs: {}",
completions
.iter()
.map(|c| format!("*{}*", c))
.collect::<Vec<String>>()
.join(", "),
),
false,
),
}
}
}
_ => self.no_verb_status(has_previous_state, cc.app.con),
}
}
fn get_verb_status(
&self,
verb: &Verb,
invocation: &VerbInvocation,
sel_info: SelInfo<'_>,
_cc: &CmdContext,
app_state: &AppState,
) -> Status {
if sel_info.count_paths() > 1 {
if let VerbExecution::External(external) = &verb.execution {
if external.exec_mode != ExternalExecutionMode::StayInBroot {
return Status::new(
"only verbs returning to broot on end can be executed on a multi-selection".to_owned(),
true,
);
}
}
}
if let Some(err) = verb.check_args(&sel_info, invocation, &app_state.other_panel_path) {
Status::new(err, true)
} else {
Status::new(
verb.get_status_markdown(
sel_info,
app_state,
invocation,
),
false,
)
}
}
}
pub fn get_arg<T: Copy + FromStr>(
verb_invocation: Option<&VerbInvocation>,
internal_exec: &InternalExecution,
default: T,
) -> T {
verb_invocation
.and_then(|vi| vi.args.as_ref())
.or_else(|| internal_exec.arg.as_ref())
.and_then(|s| s.parse::<T>().ok())
.unwrap_or(default)
}
pub fn initial_mode(con: &AppContext) -> Mode {
if con.modal {
Mode::Command
} else {
Mode::Input
}
}