diff --git a/src/app.rs b/src/app.rs index 2d7d201..229023c 100644 --- a/src/app.rs +++ b/src/app.rs @@ -270,7 +270,6 @@ impl<'a> App<'a> { { return Ok(true); } - // // Tab switching else if key.code == KeyCode::Char('l') { self.set_next_tab_with_offset(commander, 1)?; diff --git a/src/keybinds/config.rs b/src/keybinds/config.rs index 38f4329..a594ff2 100644 --- a/src/keybinds/config.rs +++ b/src/keybinds/config.rs @@ -3,6 +3,7 @@ use super::Shortcut; #[derive(Debug, Clone, serde::Deserialize)] pub struct KeybindsConfig { pub log_tab: Option, + pub files_tab: Option, } #[derive(Debug, Clone, serde::Deserialize)] @@ -51,3 +52,18 @@ pub struct LogTabKeybindsConfig { pub open_help: Option, } + +#[derive(Debug, Clone, serde::Deserialize)] +#[serde(rename_all = "kebab-case")] +pub struct FilesTabKeybindsConfig { + pub scroll_down: Option, + pub scroll_up: Option, + pub scroll_down_half: Option, + pub scroll_up_half: Option, + + pub focus_current: Option, + pub toggle_diff_format: Option, + + pub refresh: Option, + pub open_help: Option, +} diff --git a/src/keybinds/files_tab.rs b/src/keybinds/files_tab.rs new file mode 100644 index 0000000..3a9ff4b --- /dev/null +++ b/src/keybinds/files_tab.rs @@ -0,0 +1,91 @@ +use std::str::FromStr; + +use ratatui::crossterm::event::KeyEvent; + +use crate::{make_keybinds_help, set_keybinds, update_keybinds}; + +use super::{config::FilesTabKeybindsConfig, keybinds_store::KeybindsStore, Shortcut}; + +#[derive(Debug)] +pub struct FilesTabKeybinds { + // todo: probably split keys for different contexts, e.g when describe_textarea is opened + keys: KeybindsStore, +} + +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +pub enum FilesTabEvent { + ScrollDown, + ScrollUp, + ScrollDownHalf, + ScrollUpHalf, + + FocusCurrent, + ToggleDiffFormat, + + Refresh, + OpenHelp, + + Unbound, +} + +impl Default for FilesTabKeybinds { + fn default() -> Self { + let mut keys = KeybindsStore::::default(); + set_keybinds!( + keys, + FilesTabEvent::ScrollDown => "j", + FilesTabEvent::ScrollDown => "down", + FilesTabEvent::ScrollUp => "k", + FilesTabEvent::ScrollUp => "up", + FilesTabEvent::ScrollDownHalf => "shift+j", + FilesTabEvent::ScrollUpHalf => "shift+k", + FilesTabEvent::FocusCurrent => "@", + // todo: move to DetailsKeybindings + FilesTabEvent::ToggleDiffFormat => "w", + FilesTabEvent::Refresh => "shift+r", + FilesTabEvent::Refresh => "f5", + FilesTabEvent::OpenHelp => "?", + ); + + Self { keys } + } +} + +impl FilesTabKeybinds { + pub fn match_event(&self, event: KeyEvent) -> FilesTabEvent { + if let Some(action) = self.keys.match_event(event) { + action + } else { + FilesTabEvent::Unbound + } + } + pub fn extend_from_config(&mut self, config: &FilesTabKeybindsConfig) { + update_keybinds!( + self.keys, + FilesTabEvent::ScrollDown => config.scroll_down, + FilesTabEvent::ScrollUp => config.scroll_up, + FilesTabEvent::ScrollDownHalf => config.scroll_down_half, + FilesTabEvent::ScrollUpHalf => config.scroll_up_half, + FilesTabEvent::FocusCurrent => config.focus_current, + FilesTabEvent::ToggleDiffFormat => config.toggle_diff_format, + FilesTabEvent::Refresh => config.refresh, + FilesTabEvent::OpenHelp => config.open_help, + ); + } + pub fn make_main_panel_help(&self) -> Vec<(String, String)> { + make_keybinds_help!( + self.keys, + FilesTabEvent::ScrollDown => "scroll down", + FilesTabEvent::ScrollUp => "scroll up", + FilesTabEvent::ScrollDownHalf => "scroll down by ½ page", + FilesTabEvent::ScrollUpHalf => "scroll up by ½ page", + FilesTabEvent::FocusCurrent => "view files from current change", + FilesTabEvent::Refresh => "refresh", + ) + } +} + +#[test] +fn test_files_tab_keybinds_default() { + let _ = FilesTabKeybinds::default(); +} diff --git a/src/keybinds/log_tab.rs b/src/keybinds/log_tab.rs index 83137a1..a23466a 100644 --- a/src/keybinds/log_tab.rs +++ b/src/keybinds/log_tab.rs @@ -149,6 +149,7 @@ impl LogTabKeybinds { LogTabEvent::ScrollUpHalf => "scroll up by ½ page", LogTabEvent::OpenFiles => "see files", LogTabEvent::FocusCurrent => "current change", + LogTabEvent::Refresh => "refresh", LogTabEvent::EditRevset => "set revset", LogTabEvent::Describe => "describe change", LogTabEvent::EditChange { ignore_immutable: false } => "edit change", diff --git a/src/keybinds/mod.rs b/src/keybinds/mod.rs index e8c5f9a..704be94 100644 --- a/src/keybinds/mod.rs +++ b/src/keybinds/mod.rs @@ -3,9 +3,11 @@ use std::{fmt::Display, str::FromStr}; use ratatui::crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; pub use config::{Keybind, KeybindsConfig}; +pub use files_tab::{FilesTabEvent, FilesTabKeybinds}; pub use log_tab::{LogTabEvent, LogTabKeybinds}; mod config; +mod files_tab; mod keybinds_store; mod log_tab; diff --git a/src/ui/details_panel.rs b/src/ui/details_panel.rs index bc10a5c..29e64a1 100644 --- a/src/ui/details_panel.rs +++ b/src/ui/details_panel.rs @@ -1,5 +1,5 @@ use ratatui::{ - crossterm::event::{KeyCode, KeyEvent, KeyModifiers}, + crossterm::event::{KeyCode, KeyEvent, KeyModifiers, MouseEvent, MouseEventKind}, layout::Rect, text::Text, widgets::{Paragraph, Wrap}, @@ -18,10 +18,16 @@ pub struct DetailsPanel { pub enum DetailsPanelEvent { ScrollDown, ScrollUp, + + ScrollDownRows(isize), + ScrollUpRows(isize), + ScrollDownHalfPage, ScrollUpHalfPage, + ScrollDownPage, ScrollUpPage, + ToggleWrap, } @@ -62,6 +68,8 @@ impl DetailsPanel { match details_panel_event { DetailsPanelEvent::ScrollDown => self.scroll(1), DetailsPanelEvent::ScrollUp => self.scroll(-1), + DetailsPanelEvent::ScrollDownRows(i) => self.scroll(i), + DetailsPanelEvent::ScrollUpRows(i) => self.scroll(-i), DetailsPanelEvent::ScrollDownHalfPage => self.scroll(self.height as isize / 2), DetailsPanelEvent::ScrollUpHalfPage => { self.scroll((self.height as isize / 2).saturating_neg()) @@ -99,4 +107,22 @@ impl DetailsPanel { true } + + pub fn match_mouse_event( + &self, + event: MouseEvent, + is_big_scroll: bool, + ) -> Option { + const DETAILS_SCROLL: isize = 3; + + match event.kind { + MouseEventKind::ScrollUp if is_big_scroll => Some(DetailsPanelEvent::ScrollUpHalfPage), + MouseEventKind::ScrollUp => Some(DetailsPanelEvent::ScrollUpRows(DETAILS_SCROLL)), + MouseEventKind::ScrollDown if is_big_scroll => { + Some(DetailsPanelEvent::ScrollDownHalfPage) + } + MouseEventKind::ScrollDown => Some(DetailsPanelEvent::ScrollDownRows(DETAILS_SCROLL)), + _ => None, + } + } } diff --git a/src/ui/files_tab.rs b/src/ui/files_tab.rs index 987fb0e..5f6d6b5 100644 --- a/src/ui/files_tab.rs +++ b/src/ui/files_tab.rs @@ -8,6 +8,7 @@ use crate::{ CommandError, Commander, }, env::{Config, DiffFormat}, + keybinds::{FilesTabEvent, FilesTabKeybinds}, ui::{ details_panel::DetailsPanel, help_popup::HelpPopup, utils::tabs_to_spaces, Component, ComponentAction, @@ -17,7 +18,7 @@ use crate::{ use ansi_to_tui::IntoText; use ratatui::{ - crossterm::event::{Event, KeyCode, KeyEventKind}, + crossterm::event::{Event, KeyEventKind, KeyModifiers, MouseEventKind}, prelude::*, widgets::*, }; @@ -37,7 +38,11 @@ pub struct FilesTab { diff_output: Result, CommandError>, diff_format: DiffFormat, + // Rect of panels [0] = files, [1] = details + panel_rect: [Rect; 2], + config: Config, + keybinds: FilesTabKeybinds, } fn get_current_file_index( @@ -84,6 +89,16 @@ impl FilesTab { files_output.as_ref(), )); + let mut keybinds = FilesTabKeybinds::default(); + if let Some(new_keybinds) = commander + .env + .config + .keybinds() + .and_then(|k| k.files_tab.clone()) + { + keybinds.extend_from_config(&new_keybinds); + } + Ok(Self { head, is_current_head, @@ -99,7 +114,10 @@ impl FilesTab { diff_format, diff_panel: DetailsPanel::new(), + panel_rect: [Rect::ZERO, Rect::ZERO], + config: commander.env.config.clone(), + keybinds, }) } @@ -162,6 +180,58 @@ impl FilesTab { } Ok(()) } + + fn handle_event( + &mut self, + commander: &mut Commander, + event: FilesTabEvent, + ) -> Result { + match event { + FilesTabEvent::ScrollDown => self.scroll_files(commander, 1)?, + FilesTabEvent::ScrollUp => self.scroll_files(commander, -1)?, + FilesTabEvent::ScrollDownHalf => { + self.scroll_files(commander, self.files_height as isize / 2)?; + } + FilesTabEvent::ScrollUpHalf => { + self.scroll_files(commander, (self.files_height as isize / 2).saturating_neg())?; + } + FilesTabEvent::ToggleDiffFormat => { + self.diff_format = self.diff_format.get_next(self.config.diff_tool()); + self.refresh_diff(commander)?; + } + FilesTabEvent::Refresh => { + self.head = commander.get_head_latest(&self.head)?; + self.refresh_files(commander)?; + self.refresh_diff(commander)?; + } + FilesTabEvent::FocusCurrent => { + let head = &commander.get_current_head()?; + self.set_head(commander, head)?; + } + FilesTabEvent::OpenHelp => { + return Ok(ComponentInputResult::HandledAction( + ComponentAction::SetPopup(Some(Box::new(HelpPopup::new( + self.keybinds.make_main_panel_help(), + vec![ + ("Ctrl+e/Ctrl+y".to_owned(), "scroll down/up".to_owned()), + ( + "Ctrl+d/Ctrl+u".to_owned(), + "scroll down/up by ½ page".to_owned(), + ), + ( + "Ctrl+f/Ctrl+b".to_owned(), + "scroll down/up by page".to_owned(), + ), + ("w".to_owned(), "toggle diff format".to_owned()), + ("W".to_owned(), "toggle wrapping".to_owned()), + ], + )))), + )) + } + FilesTabEvent::Unbound => return Ok(ComponentInputResult::NotHandled), + }; + Ok(ComponentInputResult::Handled) + } } impl Component for FilesTab { @@ -185,6 +255,7 @@ impl Component for FilesTab { Constraint::Percentage(100 - self.config.layout_percent()), ]) .split(area); + self.panel_rect = [chunks[0], chunks[1]]; // Draw files { @@ -300,57 +371,51 @@ impl Component for FilesTab { return Ok(ComponentInputResult::Handled); } - match key.code { - KeyCode::Char('j') | KeyCode::Down => self.scroll_files(commander, 1)?, - KeyCode::Char('k') | KeyCode::Up => self.scroll_files(commander, -1)?, - KeyCode::Char('J') => { - self.scroll_files(commander, self.files_height as isize / 2)?; - } - KeyCode::Char('K') => { - self.scroll_files( - commander, - (self.files_height as isize / 2).saturating_neg(), - )?; - } - KeyCode::Char('w') => { - self.diff_format = self.diff_format.get_next(self.config.diff_tool()); - self.refresh_diff(commander)?; - } - KeyCode::Char('R') | KeyCode::F(5) => { - self.head = commander.get_head_latest(&self.head)?; - self.refresh_files(commander)?; - self.refresh_diff(commander)?; - } - KeyCode::Char('@') => { - let head = &commander.get_current_head()?; - self.set_head(commander, head)?; + return self.handle_event(commander, self.keybinds.match_event(key)); + } + + // todo: this code is the same as in other components + if let Event::Mouse(mouse_event) = event { + let is_big_scroll = mouse_event.modifiers.contains(KeyModifiers::SHIFT); + + // Determine if mouse event is inside files-view or details-view + let panel = self + .panel_rect + .iter() + .enumerate() + .find(|(_, rect)| rect.contains(Position::new(mouse_event.column, mouse_event.row))) + .map(|(i, _)| i); + // Execute command dependent on panel and event kind + const FILES_PANEL: Option = Some(0); + const DETAILS_PANEL: Option = Some(1); + + match panel { + FILES_PANEL => { + let action = match mouse_event.kind { + MouseEventKind::ScrollUp if is_big_scroll => { + Some(FilesTabEvent::ScrollUpHalf) + } + MouseEventKind::ScrollUp => Some(FilesTabEvent::ScrollUp), + MouseEventKind::ScrollDown if is_big_scroll => { + Some(FilesTabEvent::ScrollDownHalf) + } + MouseEventKind::ScrollDown => Some(FilesTabEvent::ScrollDown), + _ => None, + }; + if let Some(action) = action { + self.handle_event(commander, action)?; + } } - KeyCode::Char('?') => { - return Ok(ComponentInputResult::HandledAction( - ComponentAction::SetPopup(Some(Box::new(HelpPopup::new( - vec![ - ("j/k".to_owned(), "scroll down/up".to_owned()), - ("J/K".to_owned(), "scroll down by ½ page".to_owned()), - ("@".to_owned(), "view current change files".to_owned()), - ], - vec![ - ("Ctrl+e/Ctrl+y".to_owned(), "scroll down/up".to_owned()), - ( - "Ctrl+d/Ctrl+u".to_owned(), - "scroll down/up by ½ page".to_owned(), - ), - ( - "Ctrl+f/Ctrl+b".to_owned(), - "scroll down/up by page".to_owned(), - ), - ("w".to_owned(), "toggle diff format".to_owned()), - ("W".to_owned(), "toggle wrapping".to_owned()), - ], - )))), - )) + DETAILS_PANEL => { + if let Some(action) = self + .diff_panel + .match_mouse_event(mouse_event, is_big_scroll) + { + self.diff_panel.handle_event(action); + } } - _ => return Ok(ComponentInputResult::NotHandled), - }; + _ => {} + } } Ok(ComponentInputResult::Handled) diff --git a/src/ui/log_tab.rs b/src/ui/log_tab.rs index af379ae..797ba2a 100644 --- a/src/ui/log_tab.rs +++ b/src/ui/log_tab.rs @@ -3,7 +3,7 @@ use ansi_to_tui::IntoText; use anyhow::Result; use ratatui::{ - crossterm::event::{Event, KeyEventKind, MouseEvent, MouseEventKind}, + crossterm::event::{Event, KeyEventKind, KeyModifiers, MouseEventKind}, layout::Rect, prelude::*, widgets::*, @@ -22,7 +22,6 @@ use crate::{ ui::{ bookmark_set_popup::BookmarkSetPopup, details_panel::DetailsPanel, - details_panel::DetailsPanelEvent, help_popup::HelpPopup, message_popup::MessagePopup, utils::{centered_rect, centered_rect_line_height, tabs_to_spaces}, @@ -221,13 +220,14 @@ impl LogTab<'_> { self.scroll_log(commander, -1); } LogTabEvent::ScrollDownHalf => { - self.scroll_log(commander, self.log_height as isize / 2 / 2); + // calculate number of items + let log_height_in_items = self.log_height as isize / 2; + self.scroll_log(commander, log_height_in_items / 2); } LogTabEvent::ScrollUpHalf => { - self.scroll_log( - commander, - (self.log_height as isize / 2 / 2).saturating_neg(), - ); + // calculate number of items + let log_height_in_items = self.log_height as isize / 2; + self.scroll_log(commander, (log_height_in_items / 2).saturating_neg()); } LogTabEvent::FocusCurrent => { self.head = commander.get_current_head()?; @@ -645,9 +645,9 @@ impl Component for LogTab<'_> { let log_block = Block::bordered() .title(title) .border_type(BorderType::Rounded); - self.log_height = log_block.inner(chunks[0]).height; + self.log_height = log_block.inner(self.panel_rect[0]).height; let log = List::new(log_lines).block(log_block).scroll_padding(7); - f.render_stateful_widget(log, chunks[0], &mut self.log_list_state); + f.render_stateful_widget(log, self.panel_rect[0], &mut self.log_list_state); } // Draw change details @@ -662,10 +662,10 @@ impl Component for LogTab<'_> { .padding(Padding::horizontal(1)); let head = self .head_panel - .render(head_content, head_block.inner(chunks[1])) + .render(head_content, head_block.inner(self.panel_rect[1])) .block(head_block); - f.render_widget(head, chunks[1]); + f.render_widget(head, self.panel_rect[1]); } // Draw popup @@ -824,48 +824,50 @@ impl Component for LogTab<'_> { return Ok(ComponentInputResult::Handled); } - let log_tab_event = self.keybinds.match_event(key); - return self.handle_event(commander, log_tab_event); + return self.handle_event(commander, self.keybinds.match_event(key)); } + // todo: this code is the same as in other components if let Event::Mouse(mouse_event) = event { + let is_big_scroll = mouse_event.modifiers.contains(KeyModifiers::SHIFT); + // Determine if mouse event is inside log-view or details-view - fn contains(rect: &Rect, mouse_event: &MouseEvent) -> bool { - rect.x <= mouse_event.column - && mouse_event.column < rect.x + rect.width - && rect.y <= mouse_event.row - && mouse_event.row < rect.y + rect.height - } - let find_panel = || -> Option { - for (i, rect) in self.panel_rect.iter().enumerate() { - if contains(rect, &mouse_event) { - return Some(i); - } - } - return None; - }; - let panel = find_panel(); + let panel = self + .panel_rect + .iter() + .enumerate() + .find(|(_, rect)| rect.contains(Position::new(mouse_event.column, mouse_event.row))) + .map(|(i, _)| i); // Execute command dependent on panel and event kind const LOG_PANEL: Option = Some(0); const DETAILS_PANEL: Option = Some(1); - match (panel, mouse_event.kind) { - (LOG_PANEL, MouseEventKind::ScrollUp) => { - self.handle_event(commander, LogTabEvent::ScrollUp)?; - } - (LOG_PANEL, MouseEventKind::ScrollDown) => { - self.handle_event(commander, LogTabEvent::ScrollDown)?; - } - (DETAILS_PANEL, MouseEventKind::ScrollUp) => { - self.head_panel.handle_event(DetailsPanelEvent::ScrollUp); - self.head_panel.handle_event(DetailsPanelEvent::ScrollUp); - self.head_panel.handle_event(DetailsPanelEvent::ScrollUp); + + match panel { + LOG_PANEL => { + let action = match mouse_event.kind { + MouseEventKind::ScrollUp if is_big_scroll => { + Some(LogTabEvent::ScrollUpHalf) + } + MouseEventKind::ScrollUp => Some(LogTabEvent::ScrollUp), + MouseEventKind::ScrollDown if is_big_scroll => { + Some(LogTabEvent::ScrollDownHalf) + } + MouseEventKind::ScrollDown => Some(LogTabEvent::ScrollDown), + _ => None, + }; + if let Some(action) = action { + self.handle_event(commander, action)?; + } } - (DETAILS_PANEL, MouseEventKind::ScrollDown) => { - self.head_panel.handle_event(DetailsPanelEvent::ScrollDown); - self.head_panel.handle_event(DetailsPanelEvent::ScrollDown); - self.head_panel.handle_event(DetailsPanelEvent::ScrollDown); + DETAILS_PANEL => { + if let Some(action) = self + .head_panel + .match_mouse_event(mouse_event, is_big_scroll) + { + self.head_panel.handle_event(action); + } } - _ => {} // Handle other mouse events if necessary + _ => {} } }