diff --git a/assets/default-keybind.toml b/assets/default-keybind.toml index 979c4cc..f382b87 100644 --- a/assets/default-keybind.toml +++ b/assets/default-keybind.toml @@ -1,31 +1,32 @@ force_quit = ["ctrl-c"] quit = ["q"] +help_toggle = ["?"] +# close widget or cancel progress but not quit app +close_or_cancel = ["esc"] + 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"] +go_to_bottom = ["shift-g"] 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"] +select_top = ["shift-h"] +select_middle = ["shift-m"] +select_bottom = ["shift-l"] + +go_to_next = ["n"] +go_to_previous = ["shift-n"] confirm = ["enter"] +ref_list_toggle = ["tab"] search = ["/"] # copy part of information, ex: copy the short commit hash not all short_copy = ["c"] -full_copy = ["shift-C"] - -ref_list_toggle = ["tab"] +full_copy = ["shift-c"] diff --git a/src/app.rs b/src/app.rs index 5d34341..173a2b7 100644 --- a/src/app.rs +++ b/src/app.rs @@ -11,7 +11,7 @@ use ratatui::{ use crate::{ color::ColorSet, - config::Config, + config::UiConfig, event::{AppEvent, Receiver, Sender, UserEvent}, external::copy_to_clipboard, git::Repository, @@ -47,7 +47,7 @@ pub struct App<'a> { status_line: StatusLine, keybind: &'a KeyBind, - config: &'a Config, + ui_config: &'a UiConfig, image_protocol: ImageProtocol, tx: Sender, } @@ -59,7 +59,7 @@ impl<'a> App<'a> { graph: &'a Graph, graph_image: &'a GraphImage, keybind: &'a KeyBind, - config: &'a Config, + ui_config: &'a UiConfig, color_set: &'a ColorSet, image_protocol: ImageProtocol, tx: Sender, @@ -91,14 +91,14 @@ impl<'a> App<'a> { head, ref_name_to_commit_index_map, ); - let view = View::of_list(commit_list_state, config, tx.clone()); + let view = View::of_list(commit_list_state, ui_config, tx.clone()); Self { repository, status_line: StatusLine::None, view, keybind, - config, + ui_config, image_protocol, tx, } @@ -257,7 +257,7 @@ impl App<'_> { commit, changes, refs, - self.config, + self.ui_config, self.image_protocol, self.tx.clone(), ); @@ -267,7 +267,7 @@ impl App<'_> { fn close_detail(&mut self) { if let View::Detail(ref mut view) = self.view { let commit_list_state = view.take_list_state(); - self.view = View::of_list(commit_list_state, self.config, self.tx.clone()); + self.view = View::of_list(commit_list_state, self.ui_config, self.tx.clone()); } } @@ -281,14 +281,14 @@ impl App<'_> { if let View::List(ref mut view) = self.view { let commit_list_state = view.take_list_state(); let refs = self.repository.all_refs().into_iter().cloned().collect(); - self.view = View::of_refs(commit_list_state, refs, self.config, self.tx.clone()); + self.view = View::of_refs(commit_list_state, refs, self.ui_config, self.tx.clone()); } } fn close_refs(&mut self) { if let View::Refs(ref mut view) = self.view { let commit_list_state = view.take_list_state(); - self.view = View::of_list(commit_list_state, self.config, self.tx.clone()); + self.view = View::of_list(commit_list_state, self.ui_config, self.tx.clone()); } } diff --git a/src/config.rs b/src/config.rs index abd5ab1..b46af36 100644 --- a/src/config.rs +++ b/src/config.rs @@ -13,12 +13,25 @@ const DEFAULT_LIST_NAME_WIDTH: u16 = 20; const DEFAULT_DETAIL_DATE_FORMAT: &str = "%Y-%m-%d %H:%M:%S %z"; const DEFAULT_DETAIL_DATE_LOCAL: bool = true; +pub fn load() -> (UiConfig, Option) { + let path = xdg::BaseDirectories::with_prefix(APP_DIR_NAME) + .unwrap() + .get_config_file(CONFIG_FILE_NAME); + let config = if path.exists() { + let content = std::fs::read_to_string(path).unwrap(); + toml::from_str(&content).unwrap() + } else { + Config::default() + }; + (config.ui, config.keybind) +} + #[derive(Debug, Default, Clone, PartialEq, Eq, Deserialize)] -pub struct Config { +struct Config { #[serde(default)] - pub ui: UiConfig, + ui: UiConfig, /// The user customed keybinds, please ref `assets/default-keybind.toml` - pub keybind: Option, + keybind: Option, } #[derive(Debug, Default, Clone, PartialEq, Eq, Deserialize)] @@ -100,20 +113,6 @@ fn ui_detail_date_local_default() -> bool { DEFAULT_DETAIL_DATE_LOCAL } -impl Config { - pub fn load() -> Config { - let path = xdg::BaseDirectories::with_prefix(APP_DIR_NAME) - .unwrap() - .get_config_file(CONFIG_FILE_NAME); - if path.exists() { - let content = std::fs::read_to_string(path).unwrap(); - toml::from_str(&content).unwrap() - } else { - Config::default() - } - } -} - #[cfg(test)] mod tests { use super::*; diff --git a/src/event.rs b/src/event.rs index 00942a8..93805e5 100644 --- a/src/event.rs +++ b/src/event.rs @@ -87,6 +87,14 @@ pub fn init() -> (Sender, Receiver) { #[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 + /// Force Quit app without passing input into widges or views + ForceQuit, + /// Quit app + Quit, + /// Toggle Help page + HelpToggle, + /// Close widget or cancel current progress + CloseOrCancel, /// Navigate up NavigateUp, /// Navigate down @@ -95,22 +103,10 @@ pub enum UserEvent { 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 @@ -129,15 +125,19 @@ pub enum UserEvent { SelectMiddle, /// Select bottom part SelectBottom, + /// Go to next item + GoToNext, + /// Go to previous item + GoToPrevious, /// Confirm Confirm, + /// Toggle for Reference List + RefListToggle, /// 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 index c1b924d..b677d7b 100644 --- a/src/keybind.rs +++ b/src/keybind.rs @@ -8,7 +8,7 @@ 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); +pub struct KeyBind(HashMap); impl Deref for KeyBind { type Target = HashMap; @@ -31,9 +31,7 @@ impl KeyBind { 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.insert(key_event, user_event); } } @@ -41,10 +39,16 @@ impl KeyBind { } pub fn keys_for_event(&self, user_event: &UserEvent) -> Vec { - self.0 + let mut key_events: Vec<&KeyEvent> = self + .0 .iter() .filter(|(_, ue)| *ue == user_event) - .map(|(ke, _)| key_event_to_string(ke)) + .map(|(ke, _)| ke) + .collect(); + key_events.sort_by(|a, b| a.partial_cmp(b).unwrap()); // At least when used for key bindings, it doesn't seem to be a problem... + key_events + .iter() + .map(|ke| key_event_to_string(ke)) .collect() } } @@ -54,11 +58,11 @@ impl<'de> Deserialize<'de> for KeyBind { where D: Deserializer<'de>, { - let mut parsed_map = HashMap::>::deserialize(deserializer)?; + let 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) { + for (user_event, key_events) in parsed_map { + for key_event_str in key_events { + 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:}"); @@ -158,33 +162,39 @@ fn parse_key_code_with_modifiers( Ok(KeyEvent::new(c, modifiers)) } -pub fn key_event_to_string(key_event: &KeyEvent) -> String { +fn key_event_to_string(key_event: &KeyEvent) -> String { + if let KeyCode::Char(c) = key_event.code { + if key_event.modifiers == KeyModifiers::SHIFT { + return format!("<{}>", c.to_ascii_uppercase()); + } + } + 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})"); + 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(n) => { + char = format!("F{n}"); &char } - KeyCode::Char(' ') => "space", + KeyCode::Char(' ') => "Space", KeyCode::Char(c) => { char = c.to_string(); &char } - KeyCode::Esc => "esc", + KeyCode::Esc => "Esc", KeyCode::Null => "", KeyCode::CapsLock => "", KeyCode::Menu => "", @@ -200,15 +210,15 @@ pub fn key_event_to_string(key_event: &KeyEvent) -> String { let mut modifiers = Vec::with_capacity(3); if key_event.modifiers.intersects(KeyModifiers::CONTROL) { - modifiers.push("ctrl"); + modifiers.push("Ctrl"); } if key_event.modifiers.intersects(KeyModifiers::SHIFT) { - modifiers.push("shift"); + modifiers.push("Shift"); } if key_event.modifiers.intersects(KeyModifiers::ALT) { - modifiers.push("alt"); + modifiers.push("Alt"); } let mut key = modifiers.join("-"); @@ -220,3 +230,111 @@ pub fn key_event_to_string(key_event: &KeyEvent) -> String { format!("<{key}>") } + +#[cfg(test)] +mod tests { + use super::*; + + #[rustfmt::skip] + #[test] + fn test_deserialize_keybind() { + let toml = r#" + navigate_up = ["k"] + navigate_down = ["j", "down"] + navigate_left = ["ctrl-h", "shift-h", "alt-h"] + navigate_right = ["ctrl-shift-l", "alt-shift-ctrl-l"] + quit = ["esc", "f12"] + "#; + + let expected = KeyBind( + [ + ( + KeyEvent::new(KeyCode::Char('k'), KeyModifiers::empty()), + UserEvent::NavigateUp, + ), + ( + KeyEvent::new(KeyCode::Char('j'), KeyModifiers::empty()), + UserEvent::NavigateDown, + ), + ( + KeyEvent::new(KeyCode::Down, KeyModifiers::empty()), + UserEvent::NavigateDown, + ), + ( + KeyEvent::new(KeyCode::Char('h'), KeyModifiers::CONTROL), + UserEvent::NavigateLeft, + ), + ( + KeyEvent::new(KeyCode::Char('h'), KeyModifiers::SHIFT), + UserEvent::NavigateLeft, + ), + ( + KeyEvent::new(KeyCode::Char('h'), KeyModifiers::ALT), + UserEvent::NavigateLeft, + ), + ( + KeyEvent::new(KeyCode::Char('l'), KeyModifiers::CONTROL | KeyModifiers::SHIFT), + UserEvent::NavigateRight, + ), + ( + KeyEvent::new(KeyCode::Char('l'), KeyModifiers::CONTROL | KeyModifiers::SHIFT | KeyModifiers::ALT), + UserEvent::NavigateRight, + ), + ( + KeyEvent::new(KeyCode::Esc, KeyModifiers::empty()), + UserEvent::Quit, + ), + ( + KeyEvent::new(KeyCode::F(12), KeyModifiers::empty()), + UserEvent::Quit, + ), + ] + .into_iter() + .collect(), + ); + + let actual: KeyBind = toml::from_str(toml).unwrap(); + + assert_eq!(actual, expected); + } + + #[rustfmt::skip] + #[test] + fn test_key_event_to_string() { + let key_event = KeyEvent::new(KeyCode::Char('k'), KeyModifiers::empty()); + assert_eq!(key_event_to_string(&key_event), ""); + + let key_event = KeyEvent::new(KeyCode::Char('j'), KeyModifiers::empty()); + assert_eq!(key_event_to_string(&key_event), ""); + + let key_event = KeyEvent::new(KeyCode::Down, KeyModifiers::empty()); + assert_eq!(key_event_to_string(&key_event), ""); + + let key_event = KeyEvent::new(KeyCode::Char('h'), KeyModifiers::CONTROL); + assert_eq!(key_event_to_string(&key_event), ""); + + let key_event = KeyEvent::new(KeyCode::Char('h'), KeyModifiers::SHIFT); + assert_eq!(key_event_to_string(&key_event), ""); + + let key_event = KeyEvent::new(KeyCode::Char('H'), KeyModifiers::SHIFT); + assert_eq!(key_event_to_string(&key_event), ""); + + let key_event = KeyEvent::new(KeyCode::Left, KeyModifiers::SHIFT); + assert_eq!(key_event_to_string(&key_event), ""); + + let key_event = KeyEvent::new(KeyCode::Char('h'), KeyModifiers::ALT); + assert_eq!(key_event_to_string(&key_event), ""); + + let key_event = KeyEvent::new(KeyCode::Char('l'), KeyModifiers::CONTROL | KeyModifiers::SHIFT); + assert_eq!(key_event_to_string(&key_event), ""); + + let key_event = KeyEvent::new(KeyCode::Char('l'), KeyModifiers::CONTROL | KeyModifiers::SHIFT | KeyModifiers::ALT); + assert_eq!(key_event_to_string(&key_event), ""); + + let key_event = KeyEvent::new(KeyCode::Esc, KeyModifiers::empty()); + assert_eq!(key_event_to_string(&key_event), ""); + + let key_event = KeyEvent::new(KeyCode::F(12), KeyModifiers::empty()); + assert_eq!(key_event_to_string(&key_event), ""); + } +} diff --git a/src/lib.rs b/src/lib.rs index 47f1e65..dafc4f9 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -113,8 +113,8 @@ fn auto_detect_best_protocol() -> protocol::ImageProtocol { pub fn run() -> std::io::Result<()> { color_eyre::install().unwrap(); let args = Args::parse(); - let mut config = config::Config::load(); - let key_bind = keybind::KeyBind::new(config.keybind.take()); + let (ui_config, key_bind_patch) = config::load(); + let key_bind = keybind::KeyBind::new(key_bind_patch); let color_set = color::ColorSet::default(); let image_protocol = args.protocol.into(); @@ -136,7 +136,7 @@ pub fn run() -> std::io::Result<()> { &graph, &graph_image, &key_bind, - &config, + &ui_config, &color_set, image_protocol, tx, diff --git a/src/view/detail.rs b/src/view/detail.rs index a94d57a..3e99f48 100644 --- a/src/view/detail.rs +++ b/src/view/detail.rs @@ -6,7 +6,7 @@ use ratatui::{ }; use crate::{ - config::Config, + config::UiConfig, event::{AppEvent, Sender, UserEvent}, git::{Commit, FileChange, Ref}, protocol::ImageProtocol, @@ -25,7 +25,7 @@ pub struct DetailView<'a> { changes: Vec, refs: Vec, - config: &'a Config, + ui_config: &'a UiConfig, image_protocol: ImageProtocol, tx: Sender, clear: bool, @@ -37,7 +37,7 @@ impl<'a> DetailView<'a> { commit: Commit, changes: Vec, refs: Vec, - config: &'a Config, + ui_config: &'a UiConfig, image_protocol: ImageProtocol, tx: Sender, ) -> DetailView<'a> { @@ -47,7 +47,7 @@ impl<'a> DetailView<'a> { commit, changes, refs, - config, + ui_config, image_protocol, tx, clear: false, @@ -84,7 +84,7 @@ impl<'a> DetailView<'a> { let [list_area, detail_area] = Layout::vertical([Constraint::Min(0), Constraint::Length(detail_height)]).areas(area); - let commit_list = CommitList::new(&self.config.ui.list); + let commit_list = CommitList::new(&self.ui_config.list); f.render_stateful_widget(commit_list, list_area, self.as_mut_list_state()); if self.clear { @@ -96,7 +96,7 @@ impl<'a> DetailView<'a> { &self.commit, &self.changes, &self.refs, - &self.config.ui.detail, + &self.ui_config.detail, ); f.render_stateful_widget(commit_detail, detail_area, &mut self.commit_detail_state); diff --git a/src/view/list.rs b/src/view/list.rs index eaecd08..efeba26 100644 --- a/src/view/list.rs +++ b/src/view/list.rs @@ -1,7 +1,7 @@ use ratatui::{crossterm::event::KeyEvent, layout::Rect, Frame}; use crate::{ - config::Config, + config::UiConfig, event::{AppEvent, Sender, UserEvent}, widget::commit_list::{CommitList, CommitListState, SearchState}, }; @@ -10,19 +10,19 @@ use crate::{ pub struct ListView<'a> { commit_list_state: Option>, - config: &'a Config, + ui_config: &'a UiConfig, tx: Sender, } impl<'a> ListView<'a> { pub fn new( commit_list_state: CommitListState<'a>, - config: &'a Config, + ui_config: &'a UiConfig, tx: Sender, ) -> ListView<'a> { ListView { commit_list_state: Some(commit_list_state), - config, + ui_config, tx, } } @@ -132,7 +132,7 @@ impl<'a> ListView<'a> { } pub fn render(&mut self, f: &mut Frame, area: Rect) { - let commit_list = CommitList::new(&self.config.ui.list); + let commit_list = CommitList::new(&self.ui_config.list); f.render_stateful_widget(commit_list, area, self.as_mut_list_state()); } } diff --git a/src/view/refs.rs b/src/view/refs.rs index a0950c6..d320151 100644 --- a/src/view/refs.rs +++ b/src/view/refs.rs @@ -5,7 +5,7 @@ use ratatui::{ }; use crate::{ - config::Config, + config::UiConfig, event::{AppEvent, Sender, UserEvent}, git::Ref, widget::{ @@ -21,7 +21,7 @@ pub struct RefsView<'a> { refs: Vec, - config: &'a Config, + ui_config: &'a UiConfig, tx: Sender, } @@ -29,14 +29,14 @@ impl<'a> RefsView<'a> { pub fn new( commit_list_state: CommitListState<'a>, refs: Vec, - config: &'a Config, + ui_config: &'a UiConfig, tx: Sender, ) -> RefsView<'a> { RefsView { commit_list_state: Some(commit_list_state), ref_list_state: RefListState::new(), refs, - config, + ui_config, tx, } } @@ -87,7 +87,7 @@ impl<'a> RefsView<'a> { let [list_area, refs_area] = Layout::horizontal([Constraint::Min(0), Constraint::Length(26)]).areas(area); - let commit_list = CommitList::new(&self.config.ui.list); + let commit_list = CommitList::new(&self.ui_config.list); f.render_stateful_widget(commit_list, list_area, self.as_mut_list_state()); let ref_list = RefList::new(&self.refs); diff --git a/src/view/views.rs b/src/view/views.rs index 27f20ab..8052086 100644 --- a/src/view/views.rs +++ b/src/view/views.rs @@ -1,7 +1,7 @@ use ratatui::{crossterm::event::KeyEvent, layout::Rect, Frame}; use crate::{ - config::Config, + config::UiConfig, event::{Sender, UserEvent}, git::{Commit, FileChange, Ref}, keybind::KeyBind, @@ -41,8 +41,12 @@ impl<'a> View<'a> { } } - pub fn of_list(commit_list_state: CommitListState<'a>, config: &'a Config, tx: Sender) -> Self { - View::List(Box::new(ListView::new(commit_list_state, config, tx))) + pub fn of_list( + commit_list_state: CommitListState<'a>, + ui_config: &'a UiConfig, + tx: Sender, + ) -> Self { + View::List(Box::new(ListView::new(commit_list_state, ui_config, tx))) } pub fn of_detail( @@ -50,7 +54,7 @@ impl<'a> View<'a> { commit: Commit, changes: Vec, refs: Vec, - config: &'a Config, + ui_config: &'a UiConfig, image_protocol: ImageProtocol, tx: Sender, ) -> Self { @@ -59,7 +63,7 @@ impl<'a> View<'a> { commit, changes, refs, - config, + ui_config, image_protocol, tx, ))) @@ -68,10 +72,15 @@ impl<'a> View<'a> { pub fn of_refs( commit_list_state: CommitListState<'a>, refs: Vec, - config: &'a Config, + ui_config: &'a UiConfig, tx: Sender, ) -> Self { - View::Refs(Box::new(RefsView::new(commit_list_state, refs, config, tx))) + View::Refs(Box::new(RefsView::new( + commit_list_state, + refs, + ui_config, + tx, + ))) } pub fn of_help(