gitui 0.28.1

blazing fast terminal-ui for git
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use serde::{Deserialize, Serialize};
use std::{fs::File, path::PathBuf};
use struct_patch::traits::Patch as PatchTrait;
use struct_patch::Patch;

#[derive(Debug, PartialOrd, Clone, Copy, Serialize, Deserialize)]
pub struct GituiKeyEvent {
	pub code: KeyCode,
	pub modifiers: KeyModifiers,
}

impl GituiKeyEvent {
	pub const fn new(code: KeyCode, modifiers: KeyModifiers) -> Self {
		Self { code, modifiers }
	}
}

pub fn key_match(ev: &KeyEvent, binding: GituiKeyEvent) -> bool {
	ev.code == binding.code && ev.modifiers == binding.modifiers
}

impl PartialEq for GituiKeyEvent {
	fn eq(&self, other: &Self) -> bool {
		let ev: KeyEvent = self.into();
		let other: KeyEvent = other.into();
		ev == other
	}
}

impl From<&GituiKeyEvent> for KeyEvent {
	fn from(other: &GituiKeyEvent) -> Self {
		Self::new(other.code, other.modifiers)
	}
}

#[derive(Debug, Clone, Patch)]
#[patch(attribute(derive(Deserialize, Debug)))]
pub struct KeysList {
	pub tab_status: GituiKeyEvent,
	pub tab_log: GituiKeyEvent,
	pub tab_files: GituiKeyEvent,
	pub tab_stashing: GituiKeyEvent,
	pub tab_stashes: GituiKeyEvent,
	pub tab_toggle: GituiKeyEvent,
	pub tab_toggle_reverse: GituiKeyEvent,
	pub toggle_workarea: GituiKeyEvent,
	pub exit: GituiKeyEvent,
	pub quit: GituiKeyEvent,
	pub exit_popup: GituiKeyEvent,
	pub open_commit: GituiKeyEvent,
	pub open_commit_editor: GituiKeyEvent,
	pub open_help: GituiKeyEvent,
	pub open_options: GituiKeyEvent,
	pub move_left: GituiKeyEvent,
	pub move_right: GituiKeyEvent,
	pub move_up: GituiKeyEvent,
	pub move_down: GituiKeyEvent,
	pub tree_collapse_recursive: GituiKeyEvent,
	pub tree_expand_recursive: GituiKeyEvent,
	pub home: GituiKeyEvent,
	pub end: GituiKeyEvent,
	pub popup_up: GituiKeyEvent,
	pub popup_down: GituiKeyEvent,
	pub page_down: GituiKeyEvent,
	pub page_up: GituiKeyEvent,
	pub shift_up: GituiKeyEvent,
	pub shift_down: GituiKeyEvent,
	pub enter: GituiKeyEvent,
	pub blame: GituiKeyEvent,
	pub file_history: GituiKeyEvent,
	pub edit_file: GituiKeyEvent,
	pub status_stage_all: GituiKeyEvent,
	pub status_reset_item: GituiKeyEvent,
	pub status_ignore_file: GituiKeyEvent,
	pub diff_stage_lines: GituiKeyEvent,
	pub diff_reset_lines: GituiKeyEvent,
	pub stashing_save: GituiKeyEvent,
	pub stashing_toggle_untracked: GituiKeyEvent,
	pub stashing_toggle_index: GituiKeyEvent,
	pub stash_apply: GituiKeyEvent,
	pub stash_open: GituiKeyEvent,
	pub stash_drop: GituiKeyEvent,
	pub cmd_bar_toggle: GituiKeyEvent,
	pub log_tag_commit: GituiKeyEvent,
	pub log_mark_commit: GituiKeyEvent,
	pub log_checkout_commit: GituiKeyEvent,
	pub log_reset_commit: GituiKeyEvent,
	pub log_reword_commit: GituiKeyEvent,
	pub log_find: GituiKeyEvent,
	pub find_commit_sha: GituiKeyEvent,
	pub commit_amend: GituiKeyEvent,
	pub toggle_signoff: GituiKeyEvent,
	pub toggle_verify: GituiKeyEvent,
	pub copy: GituiKeyEvent,
	pub create_branch: GituiKeyEvent,
	pub rename_branch: GituiKeyEvent,
	pub select_branch: GituiKeyEvent,
	pub delete_branch: GituiKeyEvent,
	pub merge_branch: GituiKeyEvent,
	pub rebase_branch: GituiKeyEvent,
	pub reset_branch: GituiKeyEvent,
	pub compare_commits: GituiKeyEvent,
	pub tags: GituiKeyEvent,
	pub delete_tag: GituiKeyEvent,
	pub select_tag: GituiKeyEvent,
	pub push: GituiKeyEvent,
	pub open_file_tree: GituiKeyEvent,
	pub file_find: GituiKeyEvent,
	pub branch_find: GituiKeyEvent,
	pub force_push: GituiKeyEvent,
	pub fetch: GituiKeyEvent,
	pub pull: GituiKeyEvent,
	pub abort_merge: GituiKeyEvent,
	pub undo_commit: GituiKeyEvent,
	pub diff_hunk_next: GituiKeyEvent,
	pub diff_hunk_prev: GituiKeyEvent,
	pub stage_unstage_item: GituiKeyEvent,
	pub tag_annotate: GituiKeyEvent,
	pub view_submodules: GituiKeyEvent,
	pub view_remotes: GituiKeyEvent,
	pub update_remote_name: GituiKeyEvent,
	pub update_remote_url: GituiKeyEvent,
	pub add_remote: GituiKeyEvent,
	pub delete_remote: GituiKeyEvent,
	pub view_submodule_parent: GituiKeyEvent,
	pub update_submodule: GituiKeyEvent,
	pub commit_history_next: GituiKeyEvent,
	pub commit: GituiKeyEvent,
	pub newline: GituiKeyEvent,
	pub goto_line: GituiKeyEvent,
}

#[rustfmt::skip]
impl Default for KeysList {
	fn default() -> Self {
		Self {
			tab_status: GituiKeyEvent::new(KeyCode::Char('1'), KeyModifiers::empty()),
			tab_log: GituiKeyEvent::new(KeyCode::Char('2'),  KeyModifiers::empty()),
			tab_files: GituiKeyEvent::new(KeyCode::Char('3'),  KeyModifiers::empty()),
			tab_stashing: GituiKeyEvent::new(KeyCode::Char('4'),  KeyModifiers::empty()),
			tab_stashes: GituiKeyEvent::new(KeyCode::Char('5'),  KeyModifiers::empty()),
			tab_toggle: GituiKeyEvent::new(KeyCode::Tab,  KeyModifiers::empty()),
			tab_toggle_reverse: GituiKeyEvent::new(KeyCode::BackTab,  KeyModifiers::SHIFT),
			toggle_workarea: GituiKeyEvent::new(KeyCode::Char('w'),  KeyModifiers::empty()),
			exit: GituiKeyEvent::new(KeyCode::Char('c'),  KeyModifiers::CONTROL),
			quit: GituiKeyEvent::new(KeyCode::Char('q'),  KeyModifiers::empty()),
			exit_popup: GituiKeyEvent::new(KeyCode::Esc,  KeyModifiers::empty()),
			open_commit: GituiKeyEvent::new(KeyCode::Char('c'),  KeyModifiers::empty()),
			open_commit_editor: GituiKeyEvent::new(KeyCode::Char('e'), KeyModifiers::CONTROL),
			open_help: GituiKeyEvent::new(KeyCode::Char('h'),  KeyModifiers::empty()),
			open_options: GituiKeyEvent::new(KeyCode::Char('o'),  KeyModifiers::empty()),
			move_left: GituiKeyEvent::new(KeyCode::Left,  KeyModifiers::empty()),
			move_right: GituiKeyEvent::new(KeyCode::Right,  KeyModifiers::empty()),
			tree_collapse_recursive: GituiKeyEvent::new(KeyCode::Left,  KeyModifiers::SHIFT),
			tree_expand_recursive: GituiKeyEvent::new(KeyCode::Right,  KeyModifiers::SHIFT),
			home: GituiKeyEvent::new(KeyCode::Home,  KeyModifiers::empty()),
			end: GituiKeyEvent::new(KeyCode::End,  KeyModifiers::empty()),
			move_up: GituiKeyEvent::new(KeyCode::Up,  KeyModifiers::empty()),
			move_down: GituiKeyEvent::new(KeyCode::Down,  KeyModifiers::empty()),
			popup_up: GituiKeyEvent::new(KeyCode::Up,  KeyModifiers::empty()),
			popup_down: GituiKeyEvent::new(KeyCode::Down,  KeyModifiers::empty()),
			page_down: GituiKeyEvent::new(KeyCode::PageDown,  KeyModifiers::empty()),
			page_up: GituiKeyEvent::new(KeyCode::PageUp,  KeyModifiers::empty()),
			shift_up: GituiKeyEvent::new(KeyCode::Up,  KeyModifiers::SHIFT),
			shift_down: GituiKeyEvent::new(KeyCode::Down,  KeyModifiers::SHIFT),
			enter: GituiKeyEvent::new(KeyCode::Enter,  KeyModifiers::empty()),
			blame: GituiKeyEvent::new(KeyCode::Char('B'),  KeyModifiers::SHIFT),
			file_history: GituiKeyEvent::new(KeyCode::Char('H'),  KeyModifiers::SHIFT),
			edit_file: GituiKeyEvent::new(KeyCode::Char('e'),  KeyModifiers::empty()),
			status_stage_all: GituiKeyEvent::new(KeyCode::Char('a'),  KeyModifiers::empty()),
			status_reset_item: GituiKeyEvent::new(KeyCode::Char('D'),  KeyModifiers::SHIFT),
			diff_reset_lines: GituiKeyEvent::new(KeyCode::Char('d'),  KeyModifiers::empty()),
			status_ignore_file: GituiKeyEvent::new(KeyCode::Char('i'),  KeyModifiers::empty()),
			diff_stage_lines: GituiKeyEvent::new(KeyCode::Char('s'),  KeyModifiers::empty()),
			stashing_save: GituiKeyEvent::new(KeyCode::Char('s'),  KeyModifiers::empty()),
			stashing_toggle_untracked: GituiKeyEvent::new(KeyCode::Char('u'),  KeyModifiers::empty()),
			stashing_toggle_index: GituiKeyEvent::new(KeyCode::Char('i'),  KeyModifiers::empty()),
			stash_apply: GituiKeyEvent::new(KeyCode::Char('a'),  KeyModifiers::empty()),
			stash_open: GituiKeyEvent::new(KeyCode::Right,  KeyModifiers::empty()),
			stash_drop: GituiKeyEvent::new(KeyCode::Char('D'),  KeyModifiers::SHIFT),
			cmd_bar_toggle: GituiKeyEvent::new(KeyCode::Char('.'),  KeyModifiers::empty()),
			log_tag_commit: GituiKeyEvent::new(KeyCode::Char('t'),  KeyModifiers::empty()),
			log_mark_commit: GituiKeyEvent::new(KeyCode::Char(' '),  KeyModifiers::empty()),
			log_checkout_commit: GituiKeyEvent { code: KeyCode::Char('S'), modifiers: KeyModifiers::SHIFT },
			log_reset_commit: GituiKeyEvent { code: KeyCode::Char('R'), modifiers: KeyModifiers::SHIFT },
			log_reword_commit: GituiKeyEvent { code: KeyCode::Char('r'), modifiers: KeyModifiers::empty() },
			log_find: GituiKeyEvent { code: KeyCode::Char('f'), modifiers: KeyModifiers::empty() },
			find_commit_sha: GituiKeyEvent::new(KeyCode::Char('j'), KeyModifiers::CONTROL),
			commit_amend: GituiKeyEvent::new(KeyCode::Char('a'),  KeyModifiers::CONTROL),
			toggle_signoff: GituiKeyEvent::new(KeyCode::Char('s'),  KeyModifiers::CONTROL),
			toggle_verify: GituiKeyEvent::new(KeyCode::Char('f'),  KeyModifiers::CONTROL),
			copy: GituiKeyEvent::new(KeyCode::Char('y'),  KeyModifiers::empty()),
			create_branch: GituiKeyEvent::new(KeyCode::Char('c'),  KeyModifiers::empty()),
			rename_branch: GituiKeyEvent::new(KeyCode::Char('r'),  KeyModifiers::empty()),
			select_branch: GituiKeyEvent::new(KeyCode::Char('b'),  KeyModifiers::empty()),
			delete_branch: GituiKeyEvent::new(KeyCode::Char('D'),  KeyModifiers::SHIFT),
			merge_branch: GituiKeyEvent::new(KeyCode::Char('m'),  KeyModifiers::empty()),
			rebase_branch: GituiKeyEvent::new(KeyCode::Char('R'),  KeyModifiers::SHIFT),
			reset_branch: GituiKeyEvent::new(KeyCode::Char('s'),  KeyModifiers::empty()),
			compare_commits: GituiKeyEvent::new(KeyCode::Char('C'),  KeyModifiers::SHIFT),
			tags: GituiKeyEvent::new(KeyCode::Char('T'),  KeyModifiers::SHIFT),
			delete_tag: GituiKeyEvent::new(KeyCode::Char('D'),  KeyModifiers::SHIFT),
			select_tag: GituiKeyEvent::new(KeyCode::Enter,  KeyModifiers::empty()),
			push: GituiKeyEvent::new(KeyCode::Char('p'),  KeyModifiers::empty()),
			force_push: GituiKeyEvent::new(KeyCode::Char('P'),  KeyModifiers::SHIFT),
			undo_commit: GituiKeyEvent::new(KeyCode::Char('U'),  KeyModifiers::SHIFT),
			fetch: GituiKeyEvent::new(KeyCode::Char('F'),  KeyModifiers::SHIFT),
			pull: GituiKeyEvent::new(KeyCode::Char('f'),  KeyModifiers::empty()),
			abort_merge: GituiKeyEvent::new(KeyCode::Char('A'),  KeyModifiers::SHIFT),
			open_file_tree: GituiKeyEvent::new(KeyCode::Char('F'),  KeyModifiers::SHIFT),
			file_find: GituiKeyEvent::new(KeyCode::Char('f'),  KeyModifiers::empty()),
			branch_find: GituiKeyEvent::new(KeyCode::Char('f'),  KeyModifiers::empty()),
			diff_hunk_next: GituiKeyEvent::new(KeyCode::Char('n'),  KeyModifiers::empty()),
			diff_hunk_prev: GituiKeyEvent::new(KeyCode::Char('p'),  KeyModifiers::empty()),
			stage_unstage_item: GituiKeyEvent::new(KeyCode::Enter,  KeyModifiers::empty()),
			tag_annotate: GituiKeyEvent::new(KeyCode::Char('a'),  KeyModifiers::CONTROL),
			view_submodules: GituiKeyEvent::new(KeyCode::Char('S'),  KeyModifiers::SHIFT),
			view_remotes: GituiKeyEvent::new(KeyCode::Char('r'), KeyModifiers::CONTROL),
			update_remote_name: GituiKeyEvent::new(KeyCode::Char('n'),KeyModifiers::NONE),
			update_remote_url: GituiKeyEvent::new(KeyCode::Char('u'),KeyModifiers::NONE),
			add_remote: GituiKeyEvent::new(KeyCode::Char('a'), KeyModifiers::NONE),
			delete_remote: GituiKeyEvent::new(KeyCode::Char('r'), KeyModifiers::NONE),
			view_submodule_parent: GituiKeyEvent::new(KeyCode::Char('p'),  KeyModifiers::empty()),
			update_submodule: GituiKeyEvent::new(KeyCode::Char('u'),  KeyModifiers::empty()),
			commit_history_next: GituiKeyEvent::new(KeyCode::Char('n'),  KeyModifiers::CONTROL),
			commit: GituiKeyEvent::new(KeyCode::Char('d'),  KeyModifiers::CONTROL),
			newline: GituiKeyEvent::new(KeyCode::Enter,  KeyModifiers::empty()),
			goto_line: GituiKeyEvent::new(KeyCode::Char('L'),  KeyModifiers::SHIFT),
		}
	}
}

impl KeysList {
	pub fn init(file: PathBuf) -> Self {
		let mut keys_list = Self::default();
		if let Ok(f) = File::open(file) {
			match ron::de::from_reader(f) {
				Ok(patch) => keys_list.apply(patch),
				Err(e) => {
					log::error!("KeysList parse error: {e}");
				}
			}
		}
		keys_list
	}
}

#[cfg(test)]
mod tests {
	use super::*;
	use pretty_assertions::assert_eq;
	use std::io::Write;
	use tempfile::NamedTempFile;

	#[test]
	fn test_apply_vim_style_example() {
		let mut keys_list = KeysList::default();
		let f = File::open("vim_style_key_config.ron")
			.expect("vim style config should exist");
		let patch = ron::de::from_reader(f)
			.expect("vim style config format incorrect");
		keys_list.apply(patch);
	}

	#[test]
	fn test_smoke() {
		let mut file = NamedTempFile::new().unwrap();

		writeln!(
			file,
			r#"
(
	move_down: Some(( code: Char('j'), modifiers: "CONTROL")),
	move_up: Some((code: Char('h'), modifiers: ""))
)
"#
		)
		.unwrap();

		let keys = KeysList::init(file.path().to_path_buf());

		assert_eq!(keys.move_right, KeysList::default().move_right);
		assert_eq!(
			keys.move_down,
			GituiKeyEvent::new(
				KeyCode::Char('j'),
				KeyModifiers::CONTROL
			)
		);
		assert_eq!(
			keys.move_up,
			GituiKeyEvent::new(
				KeyCode::Char('h'),
				KeyModifiers::NONE
			)
		);
	}
}