From 33018f4b7a885d08f4b18cc184d36e81099a20cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Denys=20S=C3=A9guret?= Date: Sat, 3 Aug 2024 06:02:50 +0100 Subject: [PATCH] Internals for changing the widths of panels (#921) 3 new internals: * `set_panel_width`, taking as parameter the index of the panel and the desired width * `move_panel_divider`, taking as parameter the index of the divider and the desired change * `default_layout`, restores the default panel sizes `ctrl-<` is bound by default to `:move_panel_divider 0 -1` `ctrl->` is bound by default to `:move_panel_divider 0 1` It's also possible to define the layout in conf, eg ``` layout_instructions: [ { panel: 0, width: 18 } ] ``` --- CHANGELOG.md | 6 ++ src/app/app.rs | 39 ++++++---- src/app/app_context.rs | 7 +- src/app/cmd_result.rs | 3 + src/app/panel_state.rs | 24 +++++- src/browser/browser_state.rs | 2 +- src/command/panel_input.rs | 2 +- src/command/sequence.rs | 11 ++- src/conf/conf.rs | 6 +- src/display/areas.rs | 113 ++++++++++++++++++++++++----- src/display/displayable_tree.rs | 1 + src/display/layout_instructions.rs | 108 +++++++++++++++++++++++++++ src/display/mod.rs | 2 + src/pattern/input_pattern.rs | 2 +- src/shell_install/bash.rs | 1 + src/shell_install/fish.rs | 1 + src/shell_install/nushell.rs | 1 + src/verb/internal.rs | 7 ++ src/verb/verb.rs | 5 +- src/verb/verb_store.rs | 25 +++++++ website/docs/conf_verbs.md | 3 + website/docs/panels.md | 39 ++++++++++ 22 files changed, 364 insertions(+), 44 deletions(-) create mode 100644 src/display/layout_instructions.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index cc2aa36b..9a36e001 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,12 @@ ctrl-s now triggers `:search_again` which either - brings back the last used search pattern, when no filtering pattern is active - does a "total search" if a filtering pattern is active and the search wasn't complete +#### Major Feature: internals changing panel widths +* `set_panel_width`, taking as parameter the index of the panel and the desired width +* `move_panel_divider`, taking as parameter the index of the divider and the desired change +`ctrl-<` is bound by default to `:move_panel_divider 0 -1` +`ctrl->` is bound by default to `:move_panel_divider 0 1` +See http://dystroy.org/broot/panels/#resize-panels #### Minor Changes: - when git file infos are shown, and git ignored files aren't hidden, those files are flagged with a 'I' - Fix #916 - Remove .bak extension from content search exclusion list - Fix #915 diff --git a/src/app/app.rs b/src/app/app.rs index 50185c67..18914a85 100644 --- a/src/app/app.rs +++ b/src/app/app.rs @@ -8,11 +8,7 @@ use { Sequence, }, conf::Conf, - display::{ - Areas, - Screen, - W, - }, + display::*, errors::ProgramError, file_sum, git, @@ -104,7 +100,7 @@ impl App { let panel = Panel::new( PanelId::from(0), browser_state, - Areas::create(&mut Vec::new(), 0, screen, false), + Areas::create(&mut Vec::new(), &con.layout_instructions, 0, screen, false), con, ); let (tx_seqs, rx_seqs) = unbounded::(); @@ -191,6 +187,7 @@ impl App { fn close_panel( &mut self, panel_idx: usize, + con: &AppContext, ) -> bool { let active_panel_id = self.panels[self.active_panel_idx].id; if let Some(preview_id) = self.preview_panel { @@ -214,6 +211,7 @@ impl App { } Areas::resize_all( self.panels.as_mut_slice(), + &con.layout_instructions, self.screen, self.preview_panel.is_some(), ); @@ -233,9 +231,9 @@ impl App { /// Close the panel too if that was its only state. /// Close nothing and return false if there's not /// at least two states in the app. - fn remove_state(&mut self) -> bool { + fn remove_state(&mut self, con: &AppContext) -> bool { self.panels[self.active_panel_idx].remove_state() - || self.close_panel(self.active_panel_idx) + || self.close_panel(self.active_panel_idx, con) } /// redraw the whole screen. All drawing @@ -363,7 +361,7 @@ impl App { .map(|p| p.to_string_lossy().to_string()); } } - if self.close_panel(close_idx) { + if self.close_panel(close_idx, con) { let screen = self.screen; self.mut_state().refresh(screen, con); if let Some(new_arg) = new_arg { @@ -384,6 +382,15 @@ impl App { self.quitting = true; } } + ChangeLayout(instruction) => { + con.layout_instructions.push(instruction); + Areas::resize_all( + self.panels.as_mut_slice(), + &con.layout_instructions, + self.screen, + self.preview_panel.is_some(), + ); + } DisplayError(txt) => { error = Some(txt); } @@ -404,7 +411,7 @@ impl App { // we're here because the state wants us to either move to the panel // to the left, or close the rightest one if self.active_panel_idx == 0 { - self.close_panel(self.panels.len().get() - 1); + self.close_panel(self.panels.len().get() - 1, con); None } else { Some(self.active_panel_idx - 1) @@ -413,7 +420,7 @@ impl App { // panel_right // we either move to the right or close the leftest panel if self.active_panel_idx + 1 == self.panels.len().get() { - self.close_panel(0); + self.close_panel(0, con); None } else { Some(self.active_panel_idx + 1) @@ -470,7 +477,7 @@ impl App { if panels_count >= con.max_panels_count { for i in (0..panels_count).rev() { if self.panels[i].state().get_type() != PanelStateType::Tree { - self.close_panel(i); + self.close_panel(i, con); break; } } @@ -502,7 +509,7 @@ impl App { if i == self.active_panel_idx { continue; } - self.close_panel(i); + self.close_panel(i, con); break; } } @@ -551,7 +558,7 @@ impl App { if is_input_invocation { self.mut_panel().clear_input(); } - if self.remove_state() { + if self.remove_state(con) { self.mut_state().refresh(app_cmd_context.screen, con); self.mut_panel() .refresh_input_status(app_state, &app_cmd_context); @@ -563,7 +570,7 @@ impl App { if is_input_invocation { self.mut_panel().clear_input(); } - if self.remove_state() { + if self.remove_state(con) { let app_cmd_context = AppCmdContext { panel_skin, preview_panel: self.preview_panel, @@ -681,6 +688,7 @@ impl App { let with_preview = purpose.is_preview() || self.preview_panel.is_some(); let areas = Areas::create( self.panels.as_mut_slice(), + &con.layout_instructions, insertion_idx, self.screen, with_preview, @@ -873,6 +881,7 @@ impl App { self.screen.set_terminal_size(width, height, con); Areas::resize_all( self.panels.as_mut_slice(), + &con.layout_instructions, self.screen, self.preview_panel.is_some(), ); diff --git a/src/app/app_context.rs b/src/app/app_context.rs index b9e717db..01ae46e6 100644 --- a/src/app/app_context.rs +++ b/src/app/app_context.rs @@ -5,6 +5,7 @@ use { cli::{Args, TriBool}, conf::*, content_search, + display::LayoutInstructions, errors::*, file_sum, icon::*, @@ -131,6 +132,9 @@ pub struct AppContext { /// The set of transformers called before previewing a file pub preview_transformers: PreviewTransformers, + + /// layout modifiers, like divider moves + pub layout_instructions: LayoutInstructions, } impl AppContext { @@ -200,8 +204,8 @@ impl AppContext { .unwrap_or(content_search::DEFAULT_MAX_FILE_SIZE); let terminal_title_pattern = config.terminal_title.clone(); - let preview_transformers = PreviewTransformers::new(&config.preview_transformers)?; + let layout_instructions = config.layout_instructions.clone().unwrap_or_default(); Ok(Self { is_tty, @@ -236,6 +240,7 @@ impl AppContext { lines_after_match_in_preview: config.lines_after_match_in_preview.unwrap_or(0), lines_before_match_in_preview: config.lines_before_match_in_preview.unwrap_or(0), preview_transformers, + layout_instructions, }) } /// Return the --cmd argument, coming from the launch arguments (prefered) diff --git a/src/app/cmd_result.rs b/src/app/cmd_result.rs index 91e38c05..37ef297a 100644 --- a/src/app/cmd_result.rs +++ b/src/app/cmd_result.rs @@ -3,6 +3,7 @@ use { crate::{ browser::BrowserState, command::Sequence, + display::LayoutInstruction, errors::TreeBuildError, launchable::Launchable, verb::Internal, @@ -36,6 +37,7 @@ pub enum CmdResult { validate_purpose: bool, panel_ref: PanelReference, }, + ChangeLayout(LayoutInstruction), DisplayError(String), ExecuteSequence { sequence: Sequence, @@ -110,6 +112,7 @@ impl fmt::Debug for CmdResult { "{}", match self { CmdResult::ApplyOnPanel { .. } => "ApplyOnPanel", + CmdResult::ChangeLayout(_) => "ChangeLayout", CmdResult::ClosePanel { validate_purpose: false, .. } => "CancelPanel", diff --git a/src/app/panel_state.rs b/src/app/panel_state.rs index 094c3219..2c90a9a7 100644 --- a/src/app/panel_state.rs +++ b/src/app/panel_state.rs @@ -2,12 +2,12 @@ use { super::*, crate::{ command::*, - display::{Screen, W}, + display::*, errors::ProgramError, flag::Flag, help::HelpState, pattern::*, - preview::{PreviewMode, PreviewState}, + preview::*, print, stage::*, task_sync::Dam, @@ -157,6 +157,25 @@ pub trait PanelState { validate_purpose: false, panel_ref: PanelReference::Active, }, + Internal::move_panel_divider => { + let MoveDividerArgs { divider, dx } = get_arg( + input_invocation, + internal_exec, + MoveDividerArgs { divider: 0, dx: 1 }, + ); + CmdResult::ChangeLayout(LayoutInstruction::MoveDivider { divider, dx }) + } + Internal::default_layout => { + CmdResult::ChangeLayout(LayoutInstruction::Clear) + } + Internal::set_panel_width => { + let SetPanelWidthArgs { panel, width } = get_arg( + input_invocation, + internal_exec, + SetPanelWidthArgs { panel: 0, width: 100 }, + ); + CmdResult::ChangeLayout(LayoutInstruction::SetPanelWidth { panel, width }) + } #[cfg(feature = "trash")] Internal::purge_trash => { let res = trash::os_limited::list() @@ -1115,3 +1134,4 @@ pub fn get_arg( .unwrap_or(default) } + diff --git a/src/browser/browser_state.rs b/src/browser/browser_state.rs index d8a81af7..bf82d7b5 100644 --- a/src/browser/browser_state.rs +++ b/src/browser/browser_state.rs @@ -2,7 +2,7 @@ use { crate::{ app::*, command::*, - display::{DisplayableTree, Screen, W}, + display::*, errors::{ProgramError, TreeBuildError}, flag::Flag, git, diff --git a/src/command/panel_input.rs b/src/command/panel_input.rs index ca310532..d08cd6a7 100644 --- a/src/command/panel_input.rs +++ b/src/command/panel_input.rs @@ -72,7 +72,7 @@ impl PanelInput { /// consume the event to /// - maybe change the input /// - build a command - /// then redraw the input field + /// then redraw the input field #[allow(clippy::too_many_arguments)] pub fn on_event( &mut self, diff --git a/src/command/sequence.rs b/src/command/sequence.rs index ae319c75..1a8bb832 100644 --- a/src/command/sequence.rs +++ b/src/command/sequence.rs @@ -67,12 +67,15 @@ impl Sequence { } } -/// an input may be made of two parts: +/// Add commands to a sequence. +/// +/// An input may be made of two parts: /// - a search pattern /// - a verb followed by its arguments -/// we need to build a command for each part so -/// that the search is effectively done before -/// the verb invocation +/// +/// We need to build a command for each part so +/// that the search is effectively done before +/// the verb invocation fn add_commands( input: &str, commands: &mut Vec<(String, Command)>, diff --git a/src/conf/conf.rs b/src/conf/conf.rs index 83e059ee..7cc9095a 100644 --- a/src/conf/conf.rs +++ b/src/conf/conf.rs @@ -5,7 +5,7 @@ use { super::*, crate::{ app::Mode, - display::ColsConf, + display::{ColsConf, LayoutInstructions}, errors::{ConfError, ProgramError}, kitty::TransmissionMedium, path::{ @@ -143,6 +143,9 @@ pub struct Conf { #[serde(default)] pub verbs: Vec, + #[serde(alias="layout-instructions")] + pub layout_instructions: Option, + // BEWARE: entries added here won't be usable unless also // added in read_file! } @@ -232,6 +235,7 @@ impl Conf { overwrite!(self, kitty_graphics_transmission, conf); overwrite!(self, lines_after_match_in_preview, conf); overwrite!(self, lines_before_match_in_preview, conf); + overwrite!(self, layout_instructions, conf); self.verbs.append(&mut conf.verbs); // the following prefs are "additive": we can add entries from several // config files and they still make sense diff --git a/src/display/areas.rs b/src/display/areas.rs index 0e6161e7..e6f858d0 100644 --- a/src/display/areas.rs +++ b/src/display/areas.rs @@ -1,11 +1,6 @@ use { - super::{ - Screen, - WIDE_STATUS, - }, - crate::{ - app::Panel, - }, + super::*, + crate::app::Panel, termimad::Area, }; @@ -23,8 +18,8 @@ pub struct Areas { } const MINIMAL_PANEL_HEIGHT: u16 = 4; -const MINIMAL_PANEL_WIDTH: u16 = 4; -const MINIMAL_SCREEN_WIDTH: u16 = 8; +const MINIMAL_PANEL_WIDTH: u16 = 8; +const MINIMAL_SCREEN_WIDTH: u16 = 16; enum Slot<'a> { Panel(usize), @@ -32,10 +27,10 @@ enum Slot<'a> { } impl Areas { - /// compute an area for a new panel which will be inserted pub fn create( present_panels: &mut [Panel], + layout_instructions: &LayoutInstructions, mut insertion_idx: usize, screen: Screen, with_preview: bool, // slightly larger last panel @@ -59,12 +54,19 @@ impl Areas { for i in insertion_idx..present_panels.len() { slots.push(Slot::Panel(i)); } - Self::compute_areas(present_panels, &mut slots, screen, with_preview); + Self::compute_areas( + present_panels, + layout_instructions, + &mut slots, + screen, + with_preview, + ); areas } pub fn resize_all( panels: &mut [Panel], + layout_instructions: &LayoutInstructions, screen: Screen, with_preview: bool, // slightly larger last panel ) { @@ -72,11 +74,18 @@ impl Areas { for i in 0..panels.len() { slots.push(Slot::Panel(i)); } - Self::compute_areas(panels, &mut slots, screen, with_preview) + Self::compute_areas( + panels, + layout_instructions, + &mut slots, + screen, + with_preview, + ) } fn compute_areas( panels: &mut [Panel], + layout_instructions: &LayoutInstructions, slots: &mut [Slot], screen: Screen, with_preview: bool, // slightly larger last panel @@ -84,6 +93,8 @@ impl Areas { let screen_height = screen.height.max(MINIMAL_PANEL_HEIGHT); let screen_width = screen.width.max(MINIMAL_SCREEN_WIDTH); let n = slots.len() as u16; + + // compute auto/default panel widths let mut panel_width = if with_preview { 3 * screen_width / (3 * n + 1) } else { @@ -92,13 +103,80 @@ impl Areas { if panel_width < MINIMAL_PANEL_WIDTH { panel_width = panel_width.max(MINIMAL_PANEL_WIDTH); } - let mut x = 0; let nb_pos = slots.len(); + let mut panel_widths = vec![panel_width; nb_pos]; + panel_widths[nb_pos - 1] = screen_width - (nb_pos as u16 - 1) * panel_width; + + // adjust panel widths with layout instructions + if nb_pos > 1 { + for instruction in &layout_instructions.instructions { + debug!("Applying {:?}", instruction); + debug!("panel_widths before: {:?}", &panel_widths); + match *instruction { + LayoutInstruction::Clear => {} // not supposed to happen + LayoutInstruction::MoveDivider { divider, dx } => { + if divider + 1 >= nb_pos { + continue; + } + let (decr, incr, diff) = if dx < 0 { + (divider, divider + 1, (-dx) as u16) + } else { + (divider + 1, divider, dx as u16) + }; + let diff = diff.min(panel_widths[decr] - MINIMAL_PANEL_WIDTH); + panel_widths[decr] -= diff; + panel_widths[incr] += diff; + } + LayoutInstruction::SetPanelWidth { panel, width } => { + if panel >= nb_pos { continue; } + let width = width.max(MINIMAL_PANEL_WIDTH); + if width > panel_widths[panel] { + let mut diff = width - panel_widths[panel]; + // as we try to increase the width of 'panel' we have to decrease the + // widths of the other ones + while diff > 0 { + let mut freed = 0; + let step = diff / (nb_pos as u16 - 1); + for i in 0..nb_pos { + if i != panel { + let step = step.min(panel_widths[i] - MINIMAL_PANEL_WIDTH); + panel_widths[i] -= step; + freed += step; + } + } + if freed == 0 { break; } + diff -= freed; + panel_widths[panel] += freed; + } + } else { + // we distribute the freed width among other panels + let freed = panel_widths[panel] - width; + panel_widths[panel] = width; + let step = freed / (nb_pos as u16 - 1); + for i in 0..nb_pos { + if i != panel { + panel_widths[i] += step; + } + } + let rem = freed - (nb_pos as u16 - 1) * freed; + for i in 0..nb_pos { + if i != panel { + panel_widths[i] += rem; + break; + } + } + } + } + } + debug!("panel_widths after: {:?}", &panel_widths); + } + } + + // compute the areas of each slot, and give it to their panels + let mut x = 0; #[allow(clippy::needless_range_loop)] for slot_idx in 0..nb_pos { - if slot_idx == nb_pos - 1 { - panel_width = screen_width - x; - } + let panel_width = panel_widths[slot_idx]; let areas: &mut Areas = match &mut slots[slot_idx] { Slot::Panel(panel_idx) => &mut panels[*panel_idx].areas, Slot::New(areas) => areas, @@ -118,7 +196,8 @@ impl Areas { areas.input.width -= 1; } areas.purpose = if slot_idx > 0 { - let area_width = panel_width / 2; + // the purpose area is over the panel at left + let area_width = panel_widths[slot_idx - 1] / 2; Some(Area::new(x - area_width, y, area_width, 1)) } else { None diff --git a/src/display/displayable_tree.rs b/src/display/displayable_tree.rs index b4eb9498..495a9ac0 100644 --- a/src/display/displayable_tree.rs +++ b/src/display/displayable_tree.rs @@ -32,6 +32,7 @@ use { /// A tree wrapper which can be used either /// - to write on the screen in the application, /// - or to write in a file or an exported string. +/// /// Using it in the application (with in_app true) means that /// - the selection is drawn /// - a scrollbar may be drawn diff --git a/src/display/layout_instructions.rs b/src/display/layout_instructions.rs new file mode 100644 index 00000000..8b0d8bce --- /dev/null +++ b/src/display/layout_instructions.rs @@ -0,0 +1,108 @@ +use { + lazy_regex::*, + serde::Deserialize, + std::str::FromStr, +}; + +#[derive(Debug, Clone, Default, Deserialize)] +#[serde(transparent)] +pub struct LayoutInstructions { + pub instructions: Vec, +} + +#[derive(Debug, Clone, Copy, Deserialize)] +#[serde(untagged)] +pub enum LayoutInstruction { + Clear, // clear all instructions + MoveDivider { divider: usize, dx: i16 }, + SetPanelWidth { panel: usize, width: u16 }, +} + +/// arguments for moving a divider, read from a string eg "0 -5" +/// (move the first divider 5 cells to the left) +#[derive(Debug, Clone, Copy)] +pub struct MoveDividerArgs { + pub divider: usize, + pub dx: i16, +} +impl FromStr for MoveDividerArgs { + type Err = &'static str; + fn from_str(s: &str) -> Result { + if let Some((_, divider, dx)) = regex_captures!(r"^\s*(\d)\s+(-?\d{1,3})\s*$", s) { + Ok(Self { + divider: divider.parse().unwrap(), + dx: dx.parse().unwrap(), + }) + } else { + Err("not the expected move_divider args") + } + } +} + +/// arguments for setting the width of a panel, read from a string eg "1 150" +#[derive(Debug, Clone, Copy)] +pub struct SetPanelWidthArgs { + pub panel: usize, + pub width: u16, +} +impl FromStr for SetPanelWidthArgs { + type Err = &'static str; + fn from_str(s: &str) -> Result { + if let Some((_, panel, width)) = regex_captures!(r"^\s*(\d)\s+(\d{1,4})\s*$", s) { + Ok(Self { + panel: panel.parse().unwrap(), + width: width.parse().unwrap(), + }) + } else { + Err("not the expected set_panel_width args") + } + } +} + +impl LayoutInstruction { + pub fn is_moving_divider( + self, + idx: usize, + ) -> bool { + match self { + Self::MoveDivider { divider, .. } => divider == idx, + _ => false, + } + } +} + +impl LayoutInstructions { + pub fn push( + &mut self, + new_instruction: LayoutInstruction, + ) { + use LayoutInstruction::*; + match new_instruction { + Clear => { + self.instructions.clear(); + } + SetPanelWidth { + panel: new_panel, .. + } => { + // all previous SetPanelWidth for the same panel are now irrelevant + self.instructions.retain(|i| match i { + SetPanelWidth { panel, .. } => *panel != new_panel, + _ => true, + }); + } + MoveDivider { + divider: new_divider, + dx: new_dx, + } => { + // if the last instruction is a move of the same divider, we adjust it + if let Some(MoveDivider { divider, dx }) = self.instructions.last_mut() { + if *divider == new_divider { + *dx += new_dx; + return; + } + } + } + } + self.instructions.push(new_instruction); + } +} diff --git a/src/display/mod.rs b/src/display/mod.rs index 76a9cfd3..3c829a0b 100644 --- a/src/display/mod.rs +++ b/src/display/mod.rs @@ -27,6 +27,7 @@ mod col; mod displayable_tree; pub mod flags_display; mod git_status_display; +mod layout_instructions; mod luma; mod matched_string; mod num_format; @@ -42,6 +43,7 @@ pub use { cond_bg, displayable_tree::DisplayableTree, git_status_display::GitStatusDisplay, + layout_instructions::*, luma::LumaCondition, matched_string::MatchedString, screen::Screen, diff --git a/src/pattern/input_pattern.rs b/src/pattern/input_pattern.rs index 89fb41ac..b273cb35 100644 --- a/src/pattern/input_pattern.rs +++ b/src/pattern/input_pattern.rs @@ -15,7 +15,7 @@ use { /// wraps both /// - the "pattern" (which may be used to filter and rank file entries) /// - the source raw string which was used to build it and which may -/// be put back in the input. +/// be put back in the input. #[derive(Debug, Clone)] pub struct InputPattern { pub raw: String, diff --git a/src/shell_install/bash.rs b/src/shell_install/bash.rs index e730e035..22041790 100644 --- a/src/shell_install/bash.rs +++ b/src/shell_install/bash.rs @@ -7,6 +7,7 @@ //! - a function declaration script in ~/.local/share/broot/launcher/bash/br/1 //! - a link to that script in ~/.config/broot/launcher/bash/br/1 //! - a line to source the link in ~/.bashrc and ~/.zshrc +//! //! (exact paths depend on XDG variables) use { diff --git a/src/shell_install/fish.rs b/src/shell_install/fish.rs index 720db204..c5773f37 100644 --- a/src/shell_install/fish.rs +++ b/src/shell_install/fish.rs @@ -7,6 +7,7 @@ //! In a correct installation, we have: //! - a function declaration script in ~/.local/share/broot/launcher/fish/br.fish //! - a link to that script in ~/.config/fish/functions/br.fish +//! //! (exact paths depend on XDG variables) //! //! fish stores functions in FISH_CONFIG_DIR/functions (for example, diff --git a/src/shell_install/nushell.rs b/src/shell_install/nushell.rs index 13bce656..a201fffa 100644 --- a/src/shell_install/nushell.rs +++ b/src/shell_install/nushell.rs @@ -7,6 +7,7 @@ //! - a function declaration script in ~/.local/share/broot/launcher/nushell/br/1 //! - a link to that script in ~/.config/broot/launcher/nushell/br/1 //! - a line to use the link in ~/.config/nushell/config.nu +//! //! (exact paths depend on XDG variables) //! //! Please note that this function doesn't allow other commands than cd, diff --git a/src/verb/internal.rs b/src/verb/internal.rs index eef565e0..c25dd52a 100644 --- a/src/verb/internal.rs +++ b/src/verb/internal.rs @@ -55,6 +55,7 @@ macro_rules! Internals { Internals! { apply_flags: "apply flags (eg `-sd` to show sizes and dates)" false, back: "revert to the previous state (mapped to *esc*)" false, + default_layout: "restore default panel sizes" false, clear_output: "clear the --verb-output file" false, clear_stage: "empty the staging area" false, close_panel_cancel: "close the panel, not using the selected path" false, @@ -87,6 +88,7 @@ Internals! { line_up_no_cycle: "move one line up" false, mode_command: "enter the command mode" false, mode_input: "enter the input mode" false, + move_panel_divider: "move a panel divider" false, next_dir: "select the next directory" false, next_match: "select the next match" false, next_same_depth: "select the next file at the same depth" false, @@ -123,6 +125,7 @@ Internals! { select: "select a file by path" true, select_first: "select the first item" false, select_last: "select the last item" false, + set_panel_width: "set the width of a panel" false, set_syntax_theme: "set the theme of code preview" false, sort_by_count: "sort by count" false, sort_by_date: "sort by date" false, @@ -170,6 +173,8 @@ impl Internal { Internal::line_up => r"line_up (?P\d*)?", Internal::line_down_no_cycle => r"line_down_no_cycle (?P\d*)?", Internal::line_up_no_cycle => r"line_up_no_cycle (?P\d*)?", + Internal::move_panel_divider => r"move_panel_divider (?P\d+) (?P-?\d+)", + Internal::set_panel_width => r"set_panel_width (?P\d+) (?P\d+)", Internal::set_syntax_theme => r"set_syntax_theme {theme:theme}", Internal::write_output => r"write_output (?P.*)", _ => self.name(), @@ -183,6 +188,8 @@ impl Internal { Internal::line_up => r"line_up {count}", Internal::line_down_no_cycle => r"line_down_no_cycle {count}", Internal::line_up_no_cycle => r"line_up_no_cycle {count}", + Internal::move_panel_divider => r"move_panel_divider {idx} {dx}", + Internal::set_panel_width => r"set_panel_width {idx} {width}", Internal::write_output => r"write_output {line}", _ => self.name(), } diff --git a/src/verb/verb.rs b/src/verb/verb.rs index fa673899..c3d6597c 100644 --- a/src/verb/verb.rs +++ b/src/verb/verb.rs @@ -21,14 +21,17 @@ use { /// Verbs are the engines of broot commands, and apply /// - to the selected file (if user-defined, then must contain {file}, {parent} or {directory}) /// - to the current app state +/// /// There are two types of verbs executions: /// - external programs or commands (cd, mkdir, user defined commands, etc.) /// - internal behaviors (focusing a path, going back, showing the help, etc.) +/// /// Some verbs are builtins, some other ones are created by configuration. +/// /// Both builtins and configured vers can be internal or external based. /// /// Verbs can't be cloned. Two verbs are equal if they have the same address -/// in memory. +/// in memory. #[derive(Debug)] pub struct Verb { diff --git a/src/verb/verb_store.rs b/src/verb/verb_store.rs index 8caa0014..fe83425f 100644 --- a/src/verb/verb_store.rs +++ b/src/verb/verb_store.rs @@ -19,7 +19,9 @@ use { /// Provide access to the verbs: /// - the built-in ones /// - the user defined ones +/// /// A user defined verb can replace a built-in. +/// /// When the user types some keys, we select a verb /// - if the input exactly matches a shortcut or the name /// - if only one verb name starts with the input @@ -87,6 +89,8 @@ impl VerbStore { // changing display self.add_internal(set_syntax_theme); self.add_internal(apply_flags).with_name("apply_flags")?; + self.add_internal(set_panel_width); + self.add_internal(default_layout); // those two operations are mapped on ALT-ENTER, one // for directories and the other one for the other files @@ -333,6 +337,9 @@ impl VerbStore { self.add_internal(search_again).with_key(key!(ctrl-s)); self.add_internal(up_tree).with_shortcut("up"); + self.add_internal_with_args(move_panel_divider, "0 1").with_key(key!(alt-'>')); + self.add_internal_with_args(move_panel_divider, "0 -1").with_key(key!(alt-'<')); + self.add_internal(clear_output); self.add_internal(write_output); Ok(()) @@ -358,6 +365,24 @@ impl VerbStore { self.build_add_internal(internal, false) } + fn add_internal_with_args( + &mut self, + internal: Internal, + args: &str, + ) -> &mut Verb { + let command = + format!("{} {}", internal.name(), args); + let execution = VerbExecution::Internal( + InternalExecution { + internal, + bang: false, + arg: Some(args.to_string()), + } + ); + let description = VerbDescription::from_text(command.clone()); + self.add_verb(Some(&command), execution, description).unwrap() + } + fn add_internal_bang( &mut self, internal: Internal, diff --git a/website/docs/conf_verbs.md b/website/docs/conf_verbs.md index 69f944d1..c69dbe05 100644 --- a/website/docs/conf_verbs.md +++ b/website/docs/conf_verbs.md @@ -385,6 +385,7 @@ Here's a list of internals: builtin actions you can add an alternate shortcut or invocation | default key | default shortcut | behavior / details -|-|-|- :back | left | - | back to previous app state | +:default_layout | - | - | restore the default panel sizes :clear_stage | - | cls | empty the staging area :close_panel_cancel | - | - | close the panel, not using the selected path :close_panel_ok | - | - | close the panel, validating the selected path @@ -400,6 +401,7 @@ invocation | default key | default shortcut | behavior / details :line_down_no_cycle | - | - | same as line_down, but doesn't cycle :line_up | | - | scroll one line up or select the previous line :line_up_no_cycle | - | - | same as line_up, but doesn't cycle +:move_panel_divider | - | - | ex: `:move_panel_divider 0 -5` reduces the size of the left panel by 5 "characters" (while growing the right panel by 5) :next_dir | - | - | select the next directory :next_match | tab | - | select the next matching file, or matching verb or path in auto-completion :next_same_depth | - | - | select the next file at the same depth @@ -433,6 +435,7 @@ invocation | default key | default shortcut | behavior / details :select | - | - | select a path given as argument, if it's in the visible tree :select_first | - | - | select the first line :select_last | - | - | select the last line +:set_panel_width | - | - | ex: `:set_panel_width 1 150` sets the width of the second panel to 150 "characters" :set_syntax_theme | - | - | set the [syntect theme](../conf_file/#syntax-theme) of code preview, eg `:set SolarizedDark` :sort_by_count | - | sc | sort by count (only one level of the tree is displayed) :sort_by_date | - | sd | sort by date diff --git a/website/docs/panels.md b/website/docs/panels.md index 66307fd8..1742a7de 100644 --- a/website/docs/panels.md +++ b/website/docs/panels.md @@ -110,4 +110,43 @@ If your terminal is wide enough, you may then open more panels: ![image](img/20200526-3-panels.png) +# Resize panels + +3 verbs are at your disposal if you ever need to change the width of panels: + +* `move_panel_divider`: eg `move_panel_divider 0 -4` moves the first divider 4 cells to the left, making the second panel wider +* `set_panel_width`: eg `set_panel_width 1 50` sets to 50 cells the width of the second panel +* `default_layout` removes the previous resizing instructions + +You may bind keyboard shortcuts, eg (in the `verbs` array of verbs.hjson) + +```hjson +{ + invocation: move_divider_left + key: alt-< + execution: ":move_panel_divider 0 -1" + leave_broot: false +} +{ + invocation: move_divider_right + key: alt-> + execution: ":move_panel_divider 0 1" + leave_broot: false +} +``` + +Resizing instructions can also be provided in a configuration file, eg + +```hjson +layout_instructions: [ + { panel: 1, width: 80 } +] +``` + +or +```hjson +layout_instructions: [ + { divider: 0, dx: 5 } +] +```