summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDenys Séguret <cano.petrole@gmail.com>2024-05-29 20:05:26 +0200
committerGitHub <noreply@github.com>2024-05-29 20:05:26 +0200
commitb50161f7f6a2ee994b7f32cc797043c215da0875 (patch)
treeb2e9ebca415f73a42a3fdc0bd77785ca7f94269b
parent8f50c72f58fbfd48d686aeb6ed1b1e79b5069204 (diff)
Trash state (#882)
`:open_trash` shows the content of the trash. Other new internals & verbs: `:delete_trashed_file`, `:restore_trashed_file`, `:purge_trash`. All this is available only when broot is compiled with the `trash` feature. Fix #855
-rw-r--r--CHANGELOG.md1
-rw-r--r--bacon.toml2
-rw-r--r--src/app/app.rs6
-rw-r--r--src/app/cmd_result.rs2
-rw-r--r--src/app/panel_state.rs35
-rw-r--r--src/app/standard_status.rs7
-rw-r--r--src/app/state_type.rs9
-rw-r--r--src/command/panel_input.rs2
-rw-r--r--src/display/matched_string.rs2
-rw-r--r--src/errors.rs1
-rw-r--r--src/lib.rs1
-rw-r--r--src/trash/mod.rs6
-rw-r--r--src/trash/trash_state.rs583
-rw-r--r--src/verb/internal.rs4
-rw-r--r--src/verb/verb_store.rs15
-rw-r--r--website/docs/trash.md13
-rw-r--r--website/mkdocs.yml1
17 files changed, 680 insertions, 10 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index c5e4204..62154d0 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,6 @@
### next
- fix build on Android - thanks @dead10ck
+- `:open_trash` shows the content of the trash. Other new internals & verbs: `:delete_trashed_file`, `:restore_trashed_file`, `:purge_trash` - Fix #855
### v1.38.0 - 2024-05-04
<a name="v1.38.0"></a>
diff --git a/bacon.toml b/bacon.toml
index ee7b106..7105efb 100644
--- a/bacon.toml
+++ b/bacon.toml
@@ -14,7 +14,7 @@ watch = ["tests", "benches", "examples"]
command = [
"cargo", "check",
"--color", "always",
- "--features", "clipboard, kitty-csi-check",
+ "--features", "clipboard kitty-csi-check trash",
]
need_stdout = false
watch = ["benches"]
diff --git a/src/app/app.rs b/src/app/app.rs
index 9ccde9e..c5603a0 100644
--- a/src/app/app.rs
+++ b/src/app/app.rs
@@ -512,6 +512,12 @@ impl App {
self.mut_panel().clear_input_invocation(con);
}
}
+ Message(md) => {
+ if is_input_invocation {
+ self.mut_panel().clear_input_invocation(con);
+ }
+ self.mut_panel().set_message(md);
+ }
Launch(launchable) => {
self.launch_at_end = Some(*launchable);
self.quitting = true;
diff --git a/src/app/cmd_result.rs b/src/app/cmd_result.rs
index ff7b462..91e38c0 100644
--- a/src/app/cmd_result.rs
+++ b/src/app/cmd_result.rs
@@ -42,6 +42,7 @@ pub enum CmdResult {
},
HandleInApp(Internal), // command must be handled at the app level
Keep,
+ Message(String),
Launch(Box<Launchable>),
NewPanel {
state: Box<dyn PanelState>,
@@ -118,6 +119,7 @@ impl fmt::Debug for CmdResult {
CmdResult::DisplayError(_) => "DisplayError",
CmdResult::ExecuteSequence{ .. } => "ExecuteSequence",
CmdResult::Keep => "Keep",
+ CmdResult::Message { .. } => "Message",
CmdResult::Launch(_) => "Launch",
CmdResult::NewState { .. } => "NewState",
CmdResult::NewPanel { .. } => "NewPanel",
diff --git a/src/app/panel_state.rs b/src/app/panel_state.rs
index a593edf..b003dc2 100644
--- a/src/app/panel_state.rs
+++ b/src/app/panel_state.rs
@@ -157,6 +157,41 @@ pub trait PanelState {
validate_purpose: false,
panel_ref: PanelReference::Active,
},
+ #[cfg(feature = "trash")]
+ Internal::purge_trash => {
+ let res = trash::os_limited::list()
+ .and_then(|items| {
+ trash::os_limited::purge_all(items)
+ });
+ match res {
+ Ok(()) => CmdResult::RefreshState { clear_cache: false },
+ Err(e) => CmdResult::DisplayError(format!("{e}")),
+ }
+ }
+ #[cfg(feature = "trash")]
+ Internal::open_trash => {
+ let trash_state = crate::trash::TrashState::new(
+ self.tree_options(),
+ con,
+ );
+ match trash_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::new_state(Box::new(state))
+ }
+ }
+ Err(e) => CmdResult::DisplayError(format!("{e}")),
+ }
+ }
#[cfg(unix)]
Internal::filesystems => {
let fs_state = crate::filesystems::FilesystemState::new(
diff --git a/src/app/standard_status.rs b/src/app/standard_status.rs
index 17169a5..f682819 100644
--- a/src/app/standard_status.rs
+++ b/src/app/standard_status.rs
@@ -223,10 +223,13 @@ impl<'s> StandardStatusBuilder<'s> {
}
}
PanelStateType::Fs => {
- warn!("TODO fs status");
+ // TODO fs status
}
PanelStateType::Stage => {
- warn!("TODO stage status");
+ // TODO stage status
+ }
+ PanelStateType::Trash => {
+ // TODO stage status ? Maybe the shortcuts to restore or delete ?
}
}
parts.to_status(self.width)
diff --git a/src/app/state_type.rs b/src/app/state_type.rs
index 81a4961..b045482 100644
--- a/src/app/state_type.rs
+++ b/src/app/state_type.rs
@@ -8,9 +8,6 @@ use {
#[serde(rename_all = "snake_case")]
pub enum PanelStateType {
- /// standard browsing tree
- Tree,
-
/// filesystems
Fs,
@@ -22,4 +19,10 @@ pub enum PanelStateType {
/// stage panel, never alone on screen
Stage,
+
+ /// content of the trash
+ Trash,
+
+ /// standard browsing tree
+ Tree,
}
diff --git a/src/command/panel_input.rs b/src/command/panel_input.rs
index e738493..ca31053 100644
--- a/src/command/panel_input.rs
+++ b/src/command/panel_input.rs
@@ -377,8 +377,6 @@ impl PanelInput {
let raw = self.input_field.get_content();
let parts = CommandParts::from(raw.clone());
- info!("parts: {:#?}", parts);
-
let verb = if self.is_key_allowed_for_verb(key, mode) {
self.find_key_verb(
key,
diff --git a/src/display/matched_string.rs b/src/display/matched_string.rs
index db4920e..ea8610c 100644
--- a/src/display/matched_string.rs
+++ b/src/display/matched_string.rs
@@ -77,7 +77,7 @@ impl<'a, 'w> MatchedString<'a> {
let mut width = self.width();
for (idx, c) in self.string.char_indices() {
if width <= max_width { break; }
- break_idx = idx;
+ break_idx = idx + c.len_utf8();
let char_width = c.width().unwrap_or(0);
if char_width > width {
warn!("inconsistent char/str widths");
diff --git a/src/errors.rs b/src/errors.rs
index 86e0d88..bfdc8e5 100644
--- a/src/errors.rs
+++ b/src/errors.rs
@@ -32,6 +32,7 @@ custom_error! {pub ProgramError
UnprintableFile = "File can't be printed", // has characters that can't be printed without escaping
Unrecognized {token: String} = "Unrecognized: {token}",
ZeroLenFile = "File seems empty",
+ Trash {message: String} = "Trash error: {message}",
}
custom_error! {pub ShellInstallError
diff --git a/src/lib.rs b/src/lib.rs
index bb22ab8..4bd5199 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -30,6 +30,7 @@ pub mod skin;
pub mod syntactic;
pub mod task_sync;
pub mod terminal;
+pub mod trash;
pub mod tree;
pub mod tree_build;
pub mod verb;
diff --git a/src/trash/mod.rs b/src/trash/mod.rs
new file mode 100644
index 0000000..8757b62
--- /dev/null
+++ b/src/trash/mod.rs
@@ -0,0 +1,6 @@
+
+#[cfg(feature = "trash")]
+mod trash_state;
+
+#[cfg(feature = "trash")]
+pub use trash_state::*;
diff --git a/src/trash/trash_state.rs b/src/trash/trash_state.rs
new file mode 100644
index 0000000..c2e969e
--- /dev/null
+++ b/src/trash/trash_state.rs
@@ -0,0 +1,583 @@
+use {
+ crate::{
+ app::*,
+ command::*,
+ display::*,
+ errors::ProgramError,
+ pattern::*,
+ tree::TreeOptions,
+ verb::*,
+ },
+ crokey::crossterm::{
+ cursor,
+ style::Color,
+ QueueableCommand,
+ },
+ std::{
+ path::Path,
+ },
+ termimad::{
+ minimad::Alignment,
+ *,
+ },
+ trash::{
+ self as trash_crate,
+ TrashItem,
+ },
+ unicode_width::UnicodeWidthStr,
+};
+
+struct FilteredContent {
+ pattern: Pattern,
+ items: Vec<TrashItem>,
+ selection_idx: Option<usize>,
+}
+
+/// an application state showing the content of the trash
+pub struct TrashState {
+ items: Vec<TrashItem>,
+ selection_idx: Option<usize>,
+ scroll: usize,
+ page_height: usize,
+ tree_options: TreeOptions,
+ filtered: Option<FilteredContent>,
+ mode: Mode,
+}
+
+impl TrashState {
+ /// create a state listing the content of the system's trash
+ pub fn new(
+ tree_options: TreeOptions,
+ con: &AppContext,
+ ) -> Result<TrashState, ProgramError> {
+ let items = trash::os_limited::list()
+ .map_err(|e| ProgramError::Trash { message: e.to_string() })?;
+ let selection_idx = None;
+ Ok(TrashState {
+ items,
+ selection_idx,
+ scroll: 0,
+ page_height: 0,
+ tree_options,
+ filtered: None,
+ mode: con.initial_mode(),
+ })
+ }
+ pub fn count(&self) -> usize {
+ self.filtered
+ .as_ref()
+ .map(|f| f.items.len())
+ .unwrap_or_else(|| self.items.len().into())
+ }
+ pub fn try_scroll(
+ &mut self,
+ cmd: ScrollCommand,
+ ) -> bool {
+ let old_scroll = self.scroll;
+ self.scroll = cmd.apply(self.scroll, self.count(), self.page_height);
+ // move selection to an item in view
+ if let Some(f) = self.filtered.as_mut() {
+ if let Some(idx) = f.selection_idx {
+ if idx < self.scroll {
+ f.selection_idx = Some(self.scroll);
+ } else if idx >= self.scroll + self.page_height {
+ f.selection_idx = Some(self.scroll + self.page_height - 1);
+ }
+ }
+ } else {
+ if let Some(idx) = self.selection_idx {
+ if idx < self.scroll {
+ self.selection_idx = Some(self.scroll);
+ } else if idx >= self.scroll + self.page_height {
+ self.selection_idx = Some(self.scroll + self.page_height - 1);
+ }
+ }
+ }
+ self.scroll != old_scroll
+ }
+ /// If there's a selection, adjust the scroll to make it visible
+ pub fn show_selection(&mut self) {
+ let selection_idx = if let Some(f) = self.filtered.as_ref() {
+ f.selection_idx
+ } else {
+ self.selection_idx
+ };
+ if let Some(idx) = selection_idx {
+ if idx < self.scroll {
+ self.scroll = idx;
+ } else if idx >= self.scroll + self.page_height {
+ self.scroll = idx - self.page_height + 1;
+ }
+ }
+ }
+
+ /// change the selection
+ fn move_line(
+ &mut self,
+ internal_exec: &InternalExecution,
+ input_invocation: Option<&VerbInvocation>,
+ dir: i32, // -1 for up, 1 for down
+ cycle: bool,
+ ) -> CmdResult {
+ let count = get_arg(input_invocation, internal_exec, 1);
+ let dec = dir * count;
+ let selection_idx;
+ if let Some(f) = self.filtered.as_mut() {
+ selection_idx = if let Some(idx) = f.selection_idx {
+ Some(move_sel(idx, f.items.len(), dec, cycle))
+ } else if !f.items.is_empty() {
+ Some(if dec > 0 { 0 } else { f.items.len() - 1 })
+ } else {
+ None
+ };
+ f.selection_idx = selection_idx;
+ } else {
+ selection_idx = if let Some(idx) = self.selection_idx {
+ Some(move_sel(idx, self.items.len(), dec, cycle))
+ } else if !self.items.is_empty() {
+ Some(if dec > 0 { 0 } else { self.items.len() - 1 })
+ } else {
+ None
+ };
+ self.selection_idx = selection_idx;
+ }
+ if let Some(selection_idx) = selection_idx {
+ if selection_idx < self.scroll {
+ self.scroll = selection_idx;
+ } else if selection_idx >= self.scroll + self.page_height {
+ self.scroll = selection_idx + 1 - self.page_height;
+ }
+ }
+ CmdResult::Keep
+ }
+
+ fn selected_item(&self) -> Option<&TrashItem> {
+ if let Some(f) = self.filtered.as_ref() {
+ f.selection_idx.map(|idx| &f.items[idx])
+ } else {
+ self.selection_idx.map(|idx| &self.items[idx])
+ }
+ }
+
+ fn take_selected_item(&mut self) -> Option<TrashItem> {
+ if let Some(f) = self.filtered.as_mut() {
+ if let Some(idx) = f.selection_idx {
+ let item = f.items.remove(idx);
+ if f.items.is_empty() {
+ f.selection_idx = None;
+ } else if idx == f.items.len() {
+ f.selection_idx = Some(idx - 1);
+ }
+ Some(item)
+ } else {
+ None
+ }
+ } else {
+ if let Some(idx) = self.selection_idx {
+ let item = self.items.remove(idx);
+ if self.items.is_empty() {
+ self.selection_idx = None;
+ } else if idx == self.items.len() {
+ self.selection_idx = Some(idx - 1);
+ }
+ Some(item)
+ } else {
+ None
+ }
+ }
+ }
+}
+
+impl PanelState for TrashState {
+ fn get_type(&self) -> PanelStateType {
+ PanelStateType::Trash
+ }
+
+ fn set_mode(
+ &mut self,
+ mode: Mode,
+ ) {
+ self.mode = mode;
+ }
+
+ fn get_mode(&self) -> Mode {
+ self.mode
+ }
+
+ /// We don't want to expose path to verbs because you can't
+ /// normally access files in the trash
+ fn selected_path(&self) -> Option<&Path> {
+ None
+ }
+
+ fn tree_options(&self) -> TreeOptions {
+ self.tree_options.clone()
+ }
+
+ fn with_new_options(
+ &mut self,
+ _screen: Screen,
+ change_options: &dyn Fn(&mut TreeOptions) -> &'static str,
+ _in_new_panel: bool, // TODO open tree if true
+ _con: &AppContext,
+ ) -> CmdResult {
+ change_options(&mut self.tree_options);
+ CmdResult::Keep
+ }
+
+ /// We don't want to expose path to verbs because you can't
+ /// normally access files in the trash
+ fn selection(&self) -> Option<Selection<'_>> {
+ None
+ }
+
+ fn refresh(
+ &mut self,
+ _screen: Screen,
+ _con: &AppContext,
+ ) -> Command {
+ // minimal implementation. It would be better to keep filtering, and
+ // also selection & scroll whenever possible
+ if let Ok(items) = trash::os_limited::list() {
+ self.items = items;
+ self.selection_idx = None;
+ self.scroll = 0;
+ }
+ Command::empty()
+ }
+
+ fn on_pattern(
+ &mut self,
+ pattern: InputPattern,
+ _app_state: &AppState,
+ _con: &AppContext,
+ ) -> Result<CmdResult, ProgramError> {
+ if pattern.is_none() {
+ if let Some(f) = self.filtered.take() {
+ if let Some(idx) = f.selection_idx {
+ self.selection_idx = self.items
+ .iter()
+ .position(|m| m.id == f.items[idx].id);
+ }
+ }
+ } else {
+ let pattern = pattern.pattern;
+ let mut best_score = 0;
+ let mut selection_idx = None;
+ let mut items = Vec::new();
+ for item in &self.items {
+ let score = pattern.score_of_string(&item.name).unwrap_or(0)
+ + pattern
+ .score_of_string(&item.original_parent.to_string_lossy())
+ .unwrap_or(0);
+ if score > 0 {
+ items.push(item.clone());
+ if score > best_score {
+ best_score = score;
+ selection_idx = Some(items.len() - 1);
+ }
+ }
+ }
+ self.filtered = Some(FilteredContent {
+ pattern,
+ items,
+ selection_idx,
+ });
+ }
+ self.show_selection();
+ Ok(CmdResult::Keep)
+ }
+
+ fn display(
+ &mut self,
+ w: &mut W,
+ disc: &DisplayContext,
+ ) -> Result<(), ProgramError> {
+ let title_parent = "Original parent";
+ let title_name = "Deleted file name";
+ let area = &disc.state_area;
+ let con = &disc.con;
+ self.page_height = area.height as usize - 2;
+ let (items, selection_idx) = if let Some(filtered) = &self.filtered {
+ (filtered.items.as_slice(), filtered.selection_idx)
+ } else {
+ (self.items.as_slice(), self.selection_idx)
+ };
+ let scrollbar = area.scrollbar(self.scroll, items.len());
+ //- style preparation
+ let styles = &disc.panel_skin.styles;
+ let selection_bg = styles
+ .selected_line
+ .get_bg()
+ .unwrap_or(Color::AnsiValue(240));
+ let match_style = &styles.char_match;
+ let mut selected_match_style = styles.char_match.clone();
+ selected_match_style.set_bg(selection_bg);
+ let border_style = &styles.help_table_border;
+ let mut selected_border_style = styles.help_table_border.clone();
+ selected_border_style.set_bg(selection_bg);
+ //- width computations
+ let width = area.width as usize;
+ let optimal_parent_width = items
+ .iter()
+ .map(|m| m.original_parent.to_string_lossy().width())
+ .max()
+ .unwrap_or(0)
+ .max(title_parent.len());
+ let optimal_name_width = items
+ .iter()
+ .map(|m| m.name.width())
+ .max()
+ .unwrap_or(0)
+ .max(title_name.len());
+ let available_width = if con.show_selection_mark {
+ width - 1
+ } else {
+ width
+ };
+ let mut w_parent = optimal_parent_width;
+ let mut w_name = optimal_name_width;
+ if w_name + w_parent > available_width {
+ w_name = (width * 2 / 3).min(optimal_name_width);
+ w_parent = width - w_name;
+ }
+ info!("optimal_parent_width: {}, optimal_name_width: {}", optimal_parent_width, optimal_name_width);
+ info!("available_width: {}, w_parent: {}, w_name: {}", available_width, w_parent, w_name);
+ //- titles
+ w.queue(cursor::MoveTo(area.left, area.top))?;
+ let mut cw = CropWriter::new(w, width);
+ if con.show_selection_mark {
+ cw.queue_char(&styles.default, ' ')?;
+ }
+ let title = if title_parent.len() > w_parent {
+ &title_parent[..w_parent]
+ } else {
+ title_parent
+ };
+ cw.queue_g_string(&styles.default, format!("{:^w_parent$}", title))?;
+ cw.queue_char(border_style, '│')?;
+ let title = if title_name.len() > w_name {
+ &title_name[..w_name]
+ } else {
+ title_name
+ };
+ cw.queue_g_string(&styles.default, format!("{:^w_name$}", title))?;
+ cw.fill(border_style, &SPACE_FILLING)?;
+ //- horizontal line
+ w.queue(cursor::MoveTo(area.left, 1 + area.top))?;
+ let mut cw = CropWriter::new(w, width);
+ if con.show_selection_mark {
+ cw.queue_char(&styles.default, ' ')?;
+ }
+ cw.queue_g_string(border_style, format!("{:─>width$}", '┼', width = w_parent + 1))?;
+ cw.fill(border_style, &BRANCH_FILLING)?;
+ //- content
+ let mut idx = self.scroll;
+ for y in 2..area.height {
+ w.queue(cursor::MoveTo(area.left, y + area.top))?;
+ let selected = selection_idx == Some(idx);
+ let mut cw = CropWriter::new(w, width - 1); // -1 for scrollbar
+ let txt_style = if selected {
+ &styles.selected_line
+ } else {
+ &styles.default
+ };
+ if let Some(item) = items.get(idx) {
+ let match_style = if selected {
+ &selected_match_style
+ } else {
+ match_style
+ };
+ let border_style = if selected {
+ &selected_border_style
+ } else {
+ border_style
+ };
+ if con.show_selection_mark {
+ cw.queue_char(txt_style, if selected { '▶' } else { ' ' })?;
+ }
+ // parent
+ let s = item.original_parent.to_string_lossy();
+ let mut matched_string = MatchedString::new(
+ self.filtered
+ .as_ref()
+ .and_then(|f| f.pattern.search_string(&s)),
+ &s,
+ txt_style,
+ match_style,
+ );
+ if s.width() > w_parent {
+ //info!("CUT w_parent: {}, s.width(): {}", w_parent, s.width());
+ cw.queue_char(txt_style, '…')?;
+ matched_string.cut_left_to_fit(w_parent - 1);
+ //info!(" cut string width: {}", matched_string.string.width());
+ matched_string.queue_on(&mut cw)?;
+ } else {
+ matched_string.fill(w_parent, Alignment::Left);
+ matched_string.queue_on(&mut cw)?;
+ }
+ cw.queue_char(border_style, '│')?;
+ // name
+ let s = &item.name;
+ let mut matched_string = MatchedString::new(
+ self.filtered
+ .as_ref()
+ .and_then(|f| f.pattern.search_string(s)),
+ s,
+ txt_style,
+ match_style,
+ );
+ matched_string.fill(w_name, Alignment::Left);
+ matched_string.queue_on(&mut cw)?;
+ idx += 1;
+ } else {
+ if con.show_selection_mark {
+ cw.queue_char(&styles.default, ' ')?;
+ }
+ cw.queue_g_string(border_style, format!("{: >width$}", '│', width = w_parent + 1))?;
+ }
+ cw.fill(txt_style, &SPACE_FILLING)?;
+ let scrollbar_style = if ScrollCommand::is_thumb(y, scrollbar) {
+ &styles.scrollbar_thumb
+ } else {
+ &styles.scrollbar_track
+ };
+ scrollbar_style.queue_str(w, "▐")?;
+ }
+ Ok(())
+ }
+
+ 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> {
+ use Internal::*;
+ Ok(match internal_exec.internal {
+ Internal::restore_trashed_file => {
+ if let Some(item) = self.selected_item() {
+ match trash_crate::os_limited::restore_all([item.clone()]) {
+ Ok(_) => {
+ let path = item.original_path();
+ self.take_selected_item();
+ CmdResult::Message(format!(
+ "File *{}* restored",
+ path.to_string_lossy(),
+ ))
+ }
+ Err(trash_crate::Error::RestoreCollision { path, .. }) => {
+ CmdResult::DisplayError(format!(
+ "collision: *{}* already exists",
+ path.to_string_lossy(),
+ ))
+ }
+ Err(e) => {
+ CmdResult::DisplayError(format!(
+ "restore failed: {}",
+ e.to_string(),
+ ))
+ }
+ }
+ } else {
+ CmdResult::DisplayError(
+ "an item must be selected".to_string(),
+ )
+ }
+ }
+ Internal::delete_trashed_file => {
+ if let Some(item) = self.selected_item() {
+ match trash_crate::os_limited::purge_all([item.clone()]) {
+ Ok(_) => {
+ let path = item.original_path();
+ self.take_selected_item();
+ CmdResult::Message(format!(
+ "File *{}* restored",
+ path.to_string_lossy(),
+ ))
+ }
+ Err(e) => {
+ CmdResult::DisplayError(format!(
+ "deletion failed: {}",
+ e.to_string(),
+ ))
+ }
+ }
+ } else {
+ CmdResult::DisplayError(
+ "an item must be selected".to_string(),
+ )
+ }
+ }
+ Internal::back => {
+ if let Some(f) = self.filtered.take() {
+ if let Some(idx) = f.selection_idx {
+ self.selection_idx = self.items
+ .iter()
+ .position(|m| m.id == f.items[idx].id);
+ }
+ self.show_selection();
+ CmdResult::Keep
+ } else {
+ CmdResult::PopState
+ }
+ }
+ Internal::line_down => self.move_line(internal_exec, input_invocation, 1, true),
+ Internal::line_up => self.move_line(internal_exec, input_invocation, -1, true),
+ Internal::line_down_no_cycle => {
+ self.move_line(internal_exec, input_invocation, 1, false)
+ }
+ Internal::line_up_no_cycle => {
+ self.move_line(internal_exec, input_invocation, -1, false)
+ }
+ Internal::open_stay => {
+ // it would probably be a good idea to bind enter to restore_trash_file ?
+ CmdResult::DisplayError("can't open a file from the trash".to_string())
+ }
+ Internal::panel_left_no_open => CmdResult::HandleInApp(Internal::panel_left_no_open),
+ Internal::panel_right_no_open => CmdResult::HandleInApp(Internal::panel_right_no_open),
+ Internal::page_down => {
+ if !self.try_scroll(ScrollCommand::Pages(1)) {
+ self.selection_idx = Some(self.count() - 1);
+ }
+ CmdResult::Keep
+ }
+ Internal::page_up => {
+ if !self.try_scroll(ScrollCommand::Pages(-1)) {
+ self.selection_idx = Some(0);
+ }
+ CmdResult::Keep
+ }
+ open_leave => CmdResult::PopStateAndReapply,
+ _ => self.on_internal_generic(
+ w,
+ invocation_parser,
+ internal_exec,
+ input_invocation,
+ trigger_type,
+ app_state,
+ cc,
+ )?,
+ })
+ }
+
+ fn on_click(
+ &mut self,
+ _x: u16,
+ y: u16,
+ _screen: Screen,
+ _con: &AppContext,
+ ) -> Result<CmdResult, ProgramError> {
+ if y >= 2 {
+ let y = y as usize - 2 + self.scroll;
+ if y < self.items.len().into() {
+ self.selection_idx = Some(y);
+ }
+ }
+ Ok(CmdResult::Keep)
+ }
+}
diff --git a/src/verb/internal.rs b/src/verb/internal.rs
index 4723116..90f1486 100644
--- a/src/verb/internal.rs
+++ b/src/verb/internal.rs
@@ -96,6 +96,7 @@ Internals! {
open_staging_area: "open the staging area" false,
open_stay: "open file or directory according to OS (stay in broot)" true,
open_stay_filter: "display the directory, keeping the current pattern" true,
+ open_trash: "show the content of the trash" false,
page_down: "scroll one page down" false,
page_up: "scroll one page up" false,
panel_left: "focus or open panel on left" false,
@@ -114,6 +115,9 @@ Internals! {
print_tree: "print tree and leaves broot" true,
quit: "quit Broot" false,
refresh: "refresh tree and clear size cache" false,
+ delete_trashed_file: "irreversibly delete a file which is in the trash" false,
+ restore_trashed_file: "restore a file which is in the trash" false,
+ purge_trash: "irreversibly delete the trash's content" false,
root_down: "move tree root down" true,
root_up: "move tree root up" true,
select: "select a file by path" true,
diff --git a/src/verb/verb_store.rs b/src/verb/verb_store.rs
index e601912..10a6579 100644
--- a/src/verb/verb_store.rs
+++ b/src/verb/verb_store.rs
@@ -147,6 +147,20 @@ impl VerbStore {
StayInBroot,
)
.with_shortcut("cpp");