diff --git a/Cargo.lock b/Cargo.lock index eae4d7f..64497ee 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1786,6 +1786,7 @@ dependencies = [ "serde", "serde_json", "sha1", + "strum", "tempfile", "text-to-png", "toml", diff --git a/Cargo.toml b/Cargo.toml index 2f224e2..3e7f658 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,6 +28,7 @@ semver = "1.0.23" serde = { version = "1.0.204", features = ["derive"] } serde_json = "1.0.122" sha1 = "0.10.6" +strum = "0.26.3" toml = "0.8.19" tui-input = "0.9.0" tui-tree-widget = "0.21.0" diff --git a/assets/default-keybind.toml b/assets/default-keybind.toml new file mode 100644 index 0000000..979c4cc --- /dev/null +++ b/assets/default-keybind.toml @@ -0,0 +1,31 @@ +force_quit = ["ctrl-c"] +quit = ["q"] +navigate_up = ["k", "up"] +navigate_down = ["j", "down"] +navigate_right = ["l", "right"] +navigate_left = ["h", "left"] + +# close widget or cancel progress but not quit app +close_or_cancel = ["esc"] +help_toggle = ["?"] +go_to_top = ["g"] +go_to_bottom = ["shift-G"] +go_to_next = ["n"] +go_to_previous = ["shift-N"] +scroll_down = ["ctrl-e"] +scroll_up = ["ctrl-y"] +page_up = ["ctrl-b"] +page_down = ["ctrl-f"] +half_page_up = ["ctrl-u"] +half_page_down = ["ctrl-d"] +select_top = ["shift-H"] +select_middle = ["shift-M"] +select_bottom = ["shift-L"] +confirm = ["enter"] +search = ["/"] + +# copy part of information, ex: copy the short commit hash not all +short_copy = ["c"] +full_copy = ["shift-C"] + +ref_list_toggle = ["tab"] diff --git a/src/app.rs b/src/app.rs index 11fbd1b..5d34341 100644 --- a/src/app.rs +++ b/src/app.rs @@ -12,11 +12,11 @@ use ratatui::{ use crate::{ color::ColorSet, config::Config, - event::{AppEvent, Receiver, Sender}, + event::{AppEvent, Receiver, Sender, UserEvent}, external::copy_to_clipboard, git::Repository, graph::{Graph, GraphImage}, - key_code_char, + keybind::KeyBind, protocol::ImageProtocol, view::View, widget::commit_list::{CommitInfo, CommitListState}, @@ -46,16 +46,19 @@ pub struct App<'a> { view: View<'a>, status_line: StatusLine, + keybind: &'a KeyBind, config: &'a Config, image_protocol: ImageProtocol, tx: Sender, } impl<'a> App<'a> { + #[allow(clippy::too_many_arguments)] pub fn new( repository: &'a Repository, graph: &'a Graph, graph_image: &'a GraphImage, + keybind: &'a KeyBind, config: &'a Config, color_set: &'a ColorSet, image_protocol: ImageProtocol, @@ -94,6 +97,7 @@ impl<'a> App<'a> { repository, status_line: StatusLine::None, view, + keybind, config, image_protocol, tx, @@ -109,33 +113,37 @@ impl App<'_> { ) -> std::io::Result<()> { loop { terminal.draw(|f| self.render(f))?; - match rx.recv() { - AppEvent::Key(key) => match key { - key_code_char!('c', Ctrl) => { - return Ok(()); - } - _ => { - match self.status_line { - StatusLine::None | StatusLine::Input(_, _) => { - // do nothing - } - StatusLine::NotificationInfo(_) - | StatusLine::NotificationSuccess(_) - | StatusLine::NotificationWarn(_) => { - // Clear message and pass key input as is - self.clear_status_line(); - } - StatusLine::NotificationError(_) => { - // Clear message and cancel key input - self.clear_status_line(); - continue; - } + AppEvent::Key(key) => { + match self.status_line { + StatusLine::None | StatusLine::Input(_, _) => { + // do nothing + } + StatusLine::NotificationInfo(_) + | StatusLine::NotificationSuccess(_) + | StatusLine::NotificationWarn(_) => { + // Clear message and pass key input as is + self.clear_status_line(); } + StatusLine::NotificationError(_) => { + // Clear message and cancel key input + self.clear_status_line(); + continue; + } + } - self.view.handle_key(key); + match self.keybind.get(&key) { + Some(UserEvent::ForceQuit) => { + self.tx.send(AppEvent::Quit); + } + Some(ue) => { + self.view.handle_event(ue, key); + } + None => { + self.view.handle_event(&UserEvent::Unknown, key); + } } - }, + } AppEvent::Resize(w, h) => { let _ = (w, h); } @@ -286,7 +294,12 @@ impl App<'_> { fn open_help(&mut self) { let before_view = std::mem::take(&mut self.view); - self.view = View::of_help(before_view, self.image_protocol, self.tx.clone()); + self.view = View::of_help( + before_view, + self.image_protocol, + self.tx.clone(), + self.keybind, + ); } fn close_help(&mut self) { diff --git a/src/config.rs b/src/config.rs index ffd28c3..abd5ab1 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,5 +1,7 @@ use serde::Deserialize; +use crate::keybind::KeyBind; + const APP_DIR_NAME: &str = "serie"; const CONFIG_FILE_NAME: &str = "config.toml"; @@ -15,6 +17,8 @@ const DEFAULT_DETAIL_DATE_LOCAL: bool = true; pub struct Config { #[serde(default)] pub ui: UiConfig, + /// The user customed keybinds, please ref `assets/default-keybind.toml` + pub keybind: Option, } #[derive(Debug, Default, Clone, PartialEq, Eq, Deserialize)] @@ -131,6 +135,7 @@ mod tests { date_local: true, }, }, + keybind: None, }; assert_eq!(actual, expected); } @@ -163,6 +168,7 @@ mod tests { date_local: false, }, }, + keybind: None, }; assert_eq!(actual, expected); } @@ -188,6 +194,7 @@ mod tests { date_local: true, }, }, + keybind: None, }; assert_eq!(actual, expected); } diff --git a/src/event.rs b/src/event.rs index bb1a68b..00942a8 100644 --- a/src/event.rs +++ b/src/event.rs @@ -5,6 +5,8 @@ use std::{ }; use ratatui::crossterm::event::KeyEvent; +use serde::Deserialize; +use strum::{EnumIter, EnumMessage}; pub enum AppEvent { Key(KeyEvent), @@ -79,3 +81,63 @@ pub fn init() -> (Sender, Receiver) { (tx, rx) } + +/// The event triggered by user's key input +#[derive(Clone, Debug, strum::Display, Deserialize, EnumIter, Eq, EnumMessage, Hash, PartialEq)] +#[serde(rename_all = "snake_case")] +pub enum UserEvent { + // NOTE User Event should have document, else the enum item will be hidden in the help page + /// Navigate up + NavigateUp, + /// Navigate down + NavigateDown, + /// Navigate right + NavigateRight, + /// Navigate left + NavigateLeft, + /// Force Quit serie without passing input into widges or views + ForceQuit, + /// Quit serie + Quit, + /// Close widget or cancel current progress + CloseOrCancel, + /// Toggle Help page + HelpToggle, + /// Go to top + GoToTop, + /// Go to bottom + GoToBottom, + /// Go to next item + GoToNext, + /// Go to previous item + GoToPrevious, + /// Scroll one line up + ScrollUp, + /// Scroll one line down + ScrollDown, + /// Scroll one page up + PageUp, + /// Scroll one page down + PageDown, + /// Scroll half page up + HalfPageUp, + /// Scroll half page down + HalfPageDown, + /// Select top part + SelectTop, + /// Select middle part + SelectMiddle, + /// Select bottom part + SelectBottom, + /// Confirm + Confirm, + /// Search + Search, + /// Copy part of content + ShortCopy, + /// Copy + FullCopy, + /// Toggle for Reference List + RefListToggle, + Unknown, +} diff --git a/src/keybind.rs b/src/keybind.rs new file mode 100644 index 0000000..c1b924d --- /dev/null +++ b/src/keybind.rs @@ -0,0 +1,222 @@ +use crate::event::UserEvent; +use serde::{de::Deserializer, Deserialize}; +use std::collections::HashMap; +use std::ops::{Deref, DerefMut}; + +use ratatui::crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; + +const DEFAULT_KEY_BIND: &str = include_str!("../assets/default-keybind.toml"); + +#[derive(Clone, Debug, Default, Eq, PartialEq)] +pub struct KeyBind(pub HashMap); + +impl Deref for KeyBind { + type Target = HashMap; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for KeyBind { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +impl KeyBind { + pub fn new(custom_keybind_patch: Option) -> Self { + let mut keybind: KeyBind = + toml::from_str(DEFAULT_KEY_BIND).expect("default key bind should be correct"); + + if let Some(mut custom_keybind_patch) = custom_keybind_patch { + for (key_event, user_event) in custom_keybind_patch.drain() { + if let Some(_old_user_event) = keybind.insert(key_event, user_event) { + // log!("{key_event}: {_old_user_event} -> {user_event}") + } + } + } + + keybind + } + + pub fn keys_for_event(&self, user_event: &UserEvent) -> Vec { + self.0 + .iter() + .filter(|(_, ue)| *ue == user_event) + .map(|(ke, _)| key_event_to_string(ke)) + .collect() + } +} + +impl<'de> Deserialize<'de> for KeyBind { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let mut parsed_map = HashMap::>::deserialize(deserializer)?; + let mut key_map = HashMap::::new(); + for (user_event, key_events) in parsed_map.iter_mut() { + for key_event_str in key_events.iter_mut() { + let key_event = match parse_key_event(key_event_str) { + Ok(e) => e, + Err(s) => { + panic!("{key_event_str:?} is not a valid key event: {s:}"); + } + }; + if let Some(conflict_user_event) = key_map.insert(key_event, user_event.clone()) { + panic!( + "{:?} map to multiple events: {:?}, {:?}", + key_event, user_event, conflict_user_event + ); + } + } + } + + Ok(KeyBind(key_map)) + } +} + +fn parse_key_event(raw: &str) -> Result { + let raw_lower = raw.to_ascii_lowercase().replace(" ", ""); + let (remaining, modifiers) = extract_modifiers(&raw_lower); + parse_key_code_with_modifiers(remaining, modifiers) +} + +fn extract_modifiers(raw: &str) -> (&str, KeyModifiers) { + let mut modifiers = KeyModifiers::empty(); + let mut current = raw; + + loop { + match current { + rest if rest.starts_with("ctrl-") => { + modifiers.insert(KeyModifiers::CONTROL); + current = &rest[5..]; + } + rest if rest.starts_with("alt-") => { + modifiers.insert(KeyModifiers::ALT); + current = &rest[4..]; + } + rest if rest.starts_with("shift-") => { + modifiers.insert(KeyModifiers::SHIFT); + current = &rest[6..]; + } + _ => break, // break out of the loop if no known prefix is detected + }; + } + + (current, modifiers) +} + +fn parse_key_code_with_modifiers( + raw: &str, + mut modifiers: KeyModifiers, +) -> Result { + let c = match raw { + "esc" => KeyCode::Esc, + "enter" => KeyCode::Enter, + "left" => KeyCode::Left, + "right" => KeyCode::Right, + "up" => KeyCode::Up, + "down" => KeyCode::Down, + "home" => KeyCode::Home, + "end" => KeyCode::End, + "pageup" => KeyCode::PageUp, + "pagedown" => KeyCode::PageDown, + "backtab" => { + modifiers.insert(KeyModifiers::SHIFT); + KeyCode::BackTab + } + "backspace" => KeyCode::Backspace, + "delete" => KeyCode::Delete, + "insert" => KeyCode::Insert, + "f1" => KeyCode::F(1), + "f2" => KeyCode::F(2), + "f3" => KeyCode::F(3), + "f4" => KeyCode::F(4), + "f5" => KeyCode::F(5), + "f6" => KeyCode::F(6), + "f7" => KeyCode::F(7), + "f8" => KeyCode::F(8), + "f9" => KeyCode::F(9), + "f10" => KeyCode::F(10), + "f11" => KeyCode::F(11), + "f12" => KeyCode::F(12), + "space" => KeyCode::Char(' '), + "hyphen" => KeyCode::Char('-'), + "minus" => KeyCode::Char('-'), + "tab" => KeyCode::Tab, + c if c.len() == 1 => { + let mut c = c.chars().next().unwrap(); + if modifiers.contains(KeyModifiers::SHIFT) { + c = c.to_ascii_uppercase(); + } + KeyCode::Char(c) + } + _ => return Err(format!("Unable to parse {raw}")), + }; + Ok(KeyEvent::new(c, modifiers)) +} + +pub fn key_event_to_string(key_event: &KeyEvent) -> String { + let char; + let key_code = match key_event.code { + KeyCode::Backspace => "backspace", + KeyCode::Enter => "enter", + KeyCode::Left => "left", + KeyCode::Right => "right", + KeyCode::Up => "up", + KeyCode::Down => "down", + KeyCode::Home => "home", + KeyCode::End => "end", + KeyCode::PageUp => "pageup", + KeyCode::PageDown => "pagedown", + KeyCode::Tab => "tab", + KeyCode::BackTab => "backtab", + KeyCode::Delete => "delete", + KeyCode::Insert => "insert", + KeyCode::F(c) => { + char = format!("f({c})"); + &char + } + KeyCode::Char(' ') => "space", + KeyCode::Char(c) => { + char = c.to_string(); + &char + } + KeyCode::Esc => "esc", + KeyCode::Null => "", + KeyCode::CapsLock => "", + KeyCode::Menu => "", + KeyCode::ScrollLock => "", + KeyCode::Media(_) => "", + KeyCode::NumLock => "", + KeyCode::PrintScreen => "", + KeyCode::Pause => "", + KeyCode::KeypadBegin => "", + KeyCode::Modifier(_) => "", + }; + + let mut modifiers = Vec::with_capacity(3); + + if key_event.modifiers.intersects(KeyModifiers::CONTROL) { + modifiers.push("ctrl"); + } + + if key_event.modifiers.intersects(KeyModifiers::SHIFT) { + modifiers.push("shift"); + } + + if key_event.modifiers.intersects(KeyModifiers::ALT) { + modifiers.push("alt"); + } + + let mut key = modifiers.join("-"); + + if !key.is_empty() { + key.push('-'); + } + key.push_str(key_code); + + format!("<{key}>") +} diff --git a/src/lib.rs b/src/lib.rs index 9dfcaed..47f1e65 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -7,6 +7,7 @@ mod app; mod config; mod event; mod external; +mod keybind; mod macros; mod view; mod widget; @@ -112,7 +113,8 @@ fn auto_detect_best_protocol() -> protocol::ImageProtocol { pub fn run() -> std::io::Result<()> { color_eyre::install().unwrap(); let args = Args::parse(); - let config = config::Config::load(); + let mut config = config::Config::load(); + let key_bind = keybind::KeyBind::new(config.keybind.take()); let color_set = color::ColorSet::default(); let image_protocol = args.protocol.into(); @@ -133,6 +135,7 @@ pub fn run() -> std::io::Result<()> { &repository, &graph, &graph_image, + &key_bind, &config, &color_set, image_protocol, diff --git a/src/view/detail.rs b/src/view/detail.rs index 2614ede..a94d57a 100644 --- a/src/view/detail.rs +++ b/src/view/detail.rs @@ -1,5 +1,5 @@ use ratatui::{ - crossterm::event::{KeyCode, KeyEvent}, + crossterm::event::KeyEvent, layout::{Constraint, Layout, Rect}, widgets::Clear, Frame, @@ -7,9 +7,8 @@ use ratatui::{ use crate::{ config::Config, - event::{AppEvent, Sender}, + event::{AppEvent, Sender, UserEvent}, git::{Commit, FileChange, Ref}, - key_code, key_code_char, protocol::ImageProtocol, widget::{ commit_detail::{CommitDetail, CommitDetailState}, @@ -55,27 +54,24 @@ impl<'a> DetailView<'a> { } } - pub fn handle_key(&mut self, key: KeyEvent) { - match key { - key_code_char!('q') => { - self.tx.send(AppEvent::Quit); - } - key_code_char!('j') | key_code!(KeyCode::Down) => { + pub fn handle_event(&mut self, event: &UserEvent, _: KeyEvent) { + match event { + UserEvent::NavigateDown => { self.commit_detail_state.scroll_down(); } - key_code_char!('k') | key_code!(KeyCode::Up) => { + UserEvent::NavigateUp => { self.commit_detail_state.scroll_up(); } - key_code_char!('c') => { + UserEvent::ShortCopy => { self.copy_commit_short_hash(); } - key_code_char!('C') => { + UserEvent::FullCopy => { self.copy_commit_hash(); } - key_code_char!('?') => { + UserEvent::HelpToggle => { self.tx.send(AppEvent::OpenHelp); } - key_code!(KeyCode::Esc) | key_code!(KeyCode::Backspace) => { + UserEvent::CloseOrCancel => { self.tx.send(AppEvent::ClearDetail); // hack: reset the rendering of the image area self.tx.send(AppEvent::CloseDetail); } diff --git a/src/view/help.rs b/src/view/help.rs index 7005059..dad29fb 100644 --- a/src/view/help.rs +++ b/src/view/help.rs @@ -1,5 +1,5 @@ use ratatui::{ - crossterm::event::{KeyCode, KeyEvent}, + crossterm::event::KeyEvent, layout::{Constraint, Layout, Rect}, style::{Color, Modifier, Stylize}, text::{Line, Span}, @@ -8,12 +8,14 @@ use ratatui::{ }; use crate::{ - event::{AppEvent, Sender}, - key_code, key_code_char, + event::{AppEvent, Sender, UserEvent}, + keybind::KeyBind, protocol::ImageProtocol, view::View, }; +use strum::{EnumMessage, IntoEnumIterator}; + const BLOCK_TITLE_COLOR: Color = Color::Green; const KEY_COLOR: Color = Color::Yellow; @@ -32,8 +34,13 @@ pub struct HelpView<'a> { } impl HelpView<'_> { - pub fn new(before: View, image_protocol: ImageProtocol, tx: Sender) -> HelpView { - let (help_key_lines, help_value_lines) = build_lines(); + pub fn new<'a>( + before: View<'a>, + image_protocol: ImageProtocol, + tx: Sender, + keybind: &'a KeyBind, + ) -> HelpView<'a> { + let (help_key_lines, help_value_lines) = build_lines(keybind); HelpView { before, help_key_lines, @@ -45,19 +52,19 @@ impl HelpView<'_> { } } - pub fn handle_key(&mut self, key: KeyEvent) { - match key { - key_code_char!('q') => { + pub fn handle_event(&mut self, event: &UserEvent, _: KeyEvent) { + match event { + UserEvent::Quit => { self.tx.send(AppEvent::Quit); } - key_code_char!('?') | key_code!(KeyCode::Esc) | key_code!(KeyCode::Backspace) => { + UserEvent::HelpToggle | UserEvent::CloseOrCancel => { self.tx.send(AppEvent::ClearHelp); // hack: reset the rendering of the image area self.tx.send(AppEvent::CloseHelp); } - key_code_char!('j') | key_code!(KeyCode::Down) => { + UserEvent::NavigateDown => { self.scroll_down(); } - key_code_char!('k') | key_code!(KeyCode::Up) => { + UserEvent::NavigateUp => { self.scroll_up(); } _ => {} @@ -129,112 +136,28 @@ impl<'a> HelpView<'a> { } #[rustfmt::skip] -fn build_lines() -> (Vec>, Vec>) { - let (common_key_lines, common_value_lines) = build_block_lines( - "Common:", - &[ - (&["Ctrl-c", "q"], "Quit app"), - (&["?"], "Open help"), - ] - ); - let (help_key_lines, help_value_lines) = build_block_lines( - "Help:", - &[ - (&["Esc", "Backspace", "?"], "Close help"), - (&["Down", "j"], "Scroll down"), - (&["Up", "k"], "Scroll up"), - ] - ); - let (list_key_lines, list_value_lines) = build_block_lines( - "Commit List:", - &[ - (&["Down", "j"], "Move down"), - (&["Up", "k"], "Move up"), - (&["g"], "Go to top"), - (&["G"], "Go to bottom"), - (&["Ctrl-f"], "Scroll page down"), - (&["Ctrl-b"], "Scroll page up"), - (&["Ctrl-d"], "Scroll half page down"), - (&["Ctrl-u"], "Scroll half page up"), - (&["H"], "Select top of the screen"), - (&["M"], "Select middle of the screen"), - (&["L"], "Select bottom of the screen"), - (&["Enter"], "Show commit details"), - (&["Tab"], "Open refs list"), - (&["/"], "Start search"), - (&["Esc"], "Cancel search"), - (&["n"], "Go to next search match"), - (&["N"], "Go to previous search match"), - (&["c"], "Copy commit short hash"), - (&["C"], "Copy commit hash"), - ] - ); - let (detail_key_lines, detail_value_lines) = build_block_lines( - "Commit Detail:", - &[ - (&["Esc", "Backspace"], "Close commit details"), - (&["Down", "j"], "Scroll down"), - (&["Up", "k"], "Scroll up"), - (&["c"], "Copy commit short hash"), - (&["C"], "Copy commit hash"), - ] - ); - let (refs_key_lines, refs_value_lines) = build_block_lines( - "Refs List:", - &[ - (&["Esc", "Backspace", "Tab"], "Close refs list"), - (&["Down", "j"], "Move down"), - (&["Up", "k"], "Move up"), - (&["g"], "Go to top"), - (&["G"], "Go to bottom"), - (&["Right", "l"], "Open node"), - (&["Left", "h"], "Close node"), - (&["c"], "Copy ref name"), - ] - ); - - let key_lines = join_line_groups_with_empty(vec![ - common_key_lines, - help_key_lines, - list_key_lines, - detail_key_lines, - refs_key_lines, - ]); - let value_lines = join_line_groups_with_empty(vec![ - common_value_lines, - help_value_lines, - list_value_lines, - detail_value_lines, - refs_value_lines, - ]); - - (key_lines, value_lines) -} - -fn build_block_lines( - title: &'static str, - keybindings: &[(&[&'static str], &'static str)], -) -> (Vec>, Vec>) { +fn build_lines(keybind: &KeyBind) -> (Vec>, Vec>) { + let mut event_key_maps = Vec::new(); + for user_event in UserEvent::iter() { + let key_events: String = keybind.keys_for_event(&user_event).join(" "); + event_key_maps.push((user_event.get_documentation(), key_events)); + } let mut key_lines = Vec::new(); let mut value_lines = Vec::new(); - let key_title_lines = vec![Line::from(title) + let key_title_lines = vec![Line::from("Help") .fg(BLOCK_TITLE_COLOR) .add_modifier(Modifier::BOLD)]; let value_title_lines = vec![Line::from("")]; - let key_binding_lines: Vec = keybindings - .iter() - .map(|(keys, _)| { - join_span_groups_with_space( - keys.iter() - .map(|key| vec!["<".into(), key.fg(KEY_COLOR), ">".into()]) - .collect(), - ) + let key_binding_lines: Vec = event_key_maps.clone() + .into_iter() + .map(|(_, keys)| { + Line::from(Span::raw(keys)).fg(KEY_COLOR) }) .collect(); - let value_binding_lines: Vec = keybindings - .iter() - .map(|(_, value)| Line::from(*value)) + let value_binding_lines: Vec = event_key_maps + .into_iter() + .filter_map(|(user_event, _)| user_event.map(Line::from)) .collect(); key_lines.extend(key_title_lines); @@ -244,27 +167,3 @@ fn build_block_lines( (key_lines, value_lines) } - -fn join_line_groups_with_empty(line_groups: Vec>>) -> Vec> { - let mut result = Vec::new(); - let n = line_groups.len(); - for (i, lines) in line_groups.into_iter().enumerate() { - result.extend(lines); - if i < n - 1 { - result.push(Line::raw("")); - } - } - result -} - -fn join_span_groups_with_space(span_groups: Vec>>) -> Line<'static> { - let mut spans: Vec = Vec::new(); - let n = span_groups.len(); - for (i, ss) in span_groups.into_iter().enumerate() { - spans.extend(ss); - if i < n - 1 { - spans.push(Span::raw(" ")); - } - } - Line::from(spans) -} diff --git a/src/view/list.rs b/src/view/list.rs index 2c5ca86..eaecd08 100644 --- a/src/view/list.rs +++ b/src/view/list.rs @@ -1,13 +1,8 @@ -use ratatui::{ - crossterm::event::{KeyCode, KeyEvent}, - layout::Rect, - Frame, -}; +use ratatui::{crossterm::event::KeyEvent, layout::Rect, Frame}; use crate::{ config::Config, - event::{AppEvent, Sender}, - key_code, key_code_char, + event::{AppEvent, Sender, UserEvent}, widget::commit_list::{CommitList, CommitListState, SearchState}, }; @@ -32,14 +27,14 @@ impl<'a> ListView<'a> { } } - pub fn handle_key(&mut self, key: KeyEvent) { + pub fn handle_event(&mut self, event: &UserEvent, key: KeyEvent) { if let SearchState::Searching { .. } = self.as_list_state().search_state() { - match key { - key_code!(KeyCode::Enter) => { + match event { + UserEvent::Confirm => { self.as_mut_list_state().apply_search(); self.update_matched_message(); } - key_code!(KeyCode::Esc) => { + UserEvent::CloseOrCancel => { self.as_mut_list_state().cancel_search(); self.clear_search_query(); } @@ -49,15 +44,84 @@ impl<'a> ListView<'a> { } } return; + } else { + match event { + UserEvent::Quit => { + self.tx.send(AppEvent::Quit); + } + UserEvent::NavigateDown => { + self.as_mut_list_state().select_next(); + } + UserEvent::NavigateUp => { + self.as_mut_list_state().select_prev(); + } + UserEvent::GoToTop => { + self.as_mut_list_state().select_first(); + } + UserEvent::GoToBottom => { + self.as_mut_list_state().select_last(); + } + UserEvent::ScrollDown => { + self.as_mut_list_state().scroll_down(); + } + UserEvent::ScrollUp => { + self.as_mut_list_state().scroll_up(); + } + UserEvent::PageDown => { + self.as_mut_list_state().scroll_down_page(); + } + UserEvent::PageUp => { + self.as_mut_list_state().scroll_up_page(); + } + UserEvent::HalfPageDown => { + self.as_mut_list_state().scroll_down_half(); + } + UserEvent::HalfPageUp => { + self.as_mut_list_state().scroll_up_half(); + } + UserEvent::SelectTop => { + self.as_mut_list_state().select_high(); + } + UserEvent::SelectMiddle => { + self.as_mut_list_state().select_middle(); + } + UserEvent::SelectBottom => { + self.as_mut_list_state().select_low(); + } + UserEvent::ShortCopy => { + self.copy_commit_short_hash(); + } + UserEvent::FullCopy => { + self.copy_commit_hash(); + } + UserEvent::Search => { + self.as_mut_list_state().start_search(); + self.update_search_query(); + } + UserEvent::HelpToggle => { + self.tx.send(AppEvent::OpenHelp); + } + UserEvent::CloseOrCancel => { + self.as_mut_list_state().cancel_search(); + self.clear_search_query(); + } + UserEvent::Confirm => { + self.tx.send(AppEvent::OpenDetail); + } + UserEvent::RefListToggle => { + self.tx.send(AppEvent::OpenRefs); + } + _ => {} + } } if let SearchState::Applied { .. } = self.as_list_state().search_state() { - match key { - key_code_char!('n') => { + match event { + UserEvent::GoToNext => { self.as_mut_list_state().select_next_match(); self.update_matched_message(); } - key_code_char!('N') => { + UserEvent::GoToPrevious => { self.as_mut_list_state().select_prev_match(); self.update_matched_message(); } @@ -65,75 +129,6 @@ impl<'a> ListView<'a> { } // Do not return here } - - match key { - key_code_char!('q') => { - self.tx.send(AppEvent::Quit); - } - key_code_char!('j') | key_code!(KeyCode::Down) => { - self.as_mut_list_state().select_next(); - } - key_code_char!('k') | key_code!(KeyCode::Up) => { - self.as_mut_list_state().select_prev(); - } - key_code_char!('g') => { - self.as_mut_list_state().select_first(); - } - key_code_char!('G') => { - self.as_mut_list_state().select_last(); - } - key_code_char!('e', Ctrl) => { - self.as_mut_list_state().scroll_down(); - } - key_code_char!('y', Ctrl) => { - self.as_mut_list_state().scroll_up(); - } - key_code_char!('f', Ctrl) => { - self.as_mut_list_state().scroll_down_page(); - } - key_code_char!('b', Ctrl) => { - self.as_mut_list_state().scroll_up_page(); - } - key_code_char!('d', Ctrl) => { - self.as_mut_list_state().scroll_down_half(); - } - key_code_char!('u', Ctrl) => { - self.as_mut_list_state().scroll_up_half(); - } - key_code_char!('H') => { - self.as_mut_list_state().select_high(); - } - key_code_char!('M') => { - self.as_mut_list_state().select_middle(); - } - key_code_char!('L') => { - self.as_mut_list_state().select_low(); - } - key_code_char!('c') => { - self.copy_commit_short_hash(); - } - key_code_char!('C') => { - self.copy_commit_hash(); - } - key_code_char!('/') => { - self.as_mut_list_state().start_search(); - self.update_search_query(); - } - key_code_char!('?') => { - self.tx.send(AppEvent::OpenHelp); - } - key_code!(KeyCode::Esc) => { - self.as_mut_list_state().cancel_search(); - self.clear_search_query(); - } - key_code!(KeyCode::Enter) => { - self.tx.send(AppEvent::OpenDetail); - } - key_code!(KeyCode::Tab) => { - self.tx.send(AppEvent::OpenRefs); - } - _ => {} - } } pub fn render(&mut self, f: &mut Frame, area: Rect) { diff --git a/src/view/refs.rs b/src/view/refs.rs index 341a362..a0950c6 100644 --- a/src/view/refs.rs +++ b/src/view/refs.rs @@ -1,14 +1,13 @@ use ratatui::{ - crossterm::event::{KeyCode, KeyEvent}, + crossterm::event::KeyEvent, layout::{Constraint, Layout, Rect}, Frame, }; use crate::{ config::Config, - event::{AppEvent, Sender}, + event::{AppEvent, Sender, UserEvent}, git::Ref, - key_code, key_code_char, widget::{ commit_list::{CommitList, CommitListState}, ref_list::{RefList, RefListState}, @@ -42,42 +41,42 @@ impl<'a> RefsView<'a> { } } - pub fn handle_key(&mut self, key: KeyEvent) { - match key { - key_code_char!('q') => { + pub fn handle_event(&mut self, event: &UserEvent, _: KeyEvent) { + match event { + UserEvent::Quit => { self.tx.send(AppEvent::Quit); } - key_code!(KeyCode::Esc) | key_code!(KeyCode::Backspace) | key_code!(KeyCode::Tab) => { + UserEvent::CloseOrCancel | UserEvent::RefListToggle => { self.tx.send(AppEvent::CloseRefs); } - key_code_char!('j') | key_code!(KeyCode::Down) => { + UserEvent::NavigateDown => { self.ref_list_state.select_next(); self.update_commit_list_selected(); } - key_code_char!('k') | key_code!(KeyCode::Up) => { + UserEvent::NavigateUp => { self.ref_list_state.select_prev(); self.update_commit_list_selected(); } - key_code_char!('g') => { + UserEvent::GoToTop => { self.ref_list_state.select_first(); self.update_commit_list_selected(); } - key_code_char!('G') => { + UserEvent::GoToBottom => { self.ref_list_state.select_last(); self.update_commit_list_selected(); } - key_code_char!('l') | key_code!(KeyCode::Right) => { + UserEvent::NavigateRight => { self.ref_list_state.open_node(); self.update_commit_list_selected(); } - key_code_char!('h') | key_code!(KeyCode::Left) => { + UserEvent::NavigateLeft => { self.ref_list_state.close_node(); self.update_commit_list_selected(); } - key_code_char!('c') => { + UserEvent::ShortCopy | UserEvent::FullCopy => { self.copy_ref_name(); } - key_code_char!('?') => { + UserEvent::HelpToggle => { self.tx.send(AppEvent::OpenHelp); } _ => {} diff --git a/src/view/views.rs b/src/view/views.rs index ae7e8cf..27f20ab 100644 --- a/src/view/views.rs +++ b/src/view/views.rs @@ -2,8 +2,9 @@ use ratatui::{crossterm::event::KeyEvent, layout::Rect, Frame}; use crate::{ config::Config, - event::Sender, + event::{Sender, UserEvent}, git::{Commit, FileChange, Ref}, + keybind::KeyBind, protocol::ImageProtocol, view::{detail::DetailView, help::HelpView, list::ListView, refs::RefsView}, widget::commit_list::CommitListState, @@ -20,13 +21,13 @@ pub enum View<'a> { } impl<'a> View<'a> { - pub fn handle_key(&mut self, key: KeyEvent) { + pub fn handle_event(&mut self, user_event: &UserEvent, key_event: KeyEvent) { match self { View::Default => {} - View::List(view) => view.handle_key(key), - View::Detail(view) => view.handle_key(key), - View::Refs(view) => view.handle_key(key), - View::Help(view) => view.handle_key(key), + View::List(view) => view.handle_event(user_event, key_event), + View::Detail(view) => view.handle_event(user_event, key_event), + View::Refs(view) => view.handle_event(user_event, key_event), + View::Help(view) => view.handle_event(user_event, key_event), } } @@ -73,7 +74,12 @@ impl<'a> View<'a> { View::Refs(Box::new(RefsView::new(commit_list_state, refs, config, tx))) } - pub fn of_help(before: View<'a>, image_protocol: ImageProtocol, tx: Sender) -> Self { - View::Help(Box::new(HelpView::new(before, image_protocol, tx))) + pub fn of_help( + before: View<'a>, + image_protocol: ImageProtocol, + tx: Sender, + keybind: &'a KeyBind, + ) -> Self { + View::Help(Box::new(HelpView::new(before, image_protocol, tx, keybind))) } }