diff --git a/CHANGELOG.md b/CHANGELOG.md index 3bdad9ba8f..dc41871cd7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +### Added +* vim-like navigation: `j`/`k` for up/down, `l` to open preview + ### Changed * use [tombi](https://github.com/tombi-toml/tombi) for all toml file formatting * open the external editor from the status diff view [[@WaterWhisperer](https://github.com/WaterWhisperer)] ([#2805](https://github.com/gitui-org/gitui/issues/2805)) diff --git a/src/components/commit_details/details.rs b/src/components/commit_details/details.rs index 42825900a0..1f3d7dfbf4 100644 --- a/src/components/commit_details/details.rs +++ b/src/components/commit_details/details.rs @@ -361,40 +361,26 @@ impl Component for DetailsComponent { fn event(&mut self, event: &Event) -> Result { if self.focused { if let Event::Key(e) = event { - return Ok( - if key_match(e, self.key_config.keys.move_up) { - self.move_scroll_top(ScrollType::Up).into() - } else if key_match( - e, - self.key_config.keys.move_down, - ) { - self.move_scroll_top(ScrollType::Down).into() - } else if key_match( - e, - self.key_config.keys.page_up, - ) { - self.move_scroll_top(ScrollType::PageUp) - .into() - } else if key_match( - e, - self.key_config.keys.page_down, - ) { - self.move_scroll_top(ScrollType::PageDown) - .into() - } else if key_match(e, self.key_config.keys.home) - || key_match(e, self.key_config.keys.shift_up) - { - self.move_scroll_top(ScrollType::Home).into() - } else if key_match(e, self.key_config.keys.end) - || key_match( - e, - self.key_config.keys.shift_down, - ) { - self.move_scroll_top(ScrollType::End).into() - } else { - EventState::NotConsumed - }, - ); + return Ok(if self.key_config.is_nav_up(e) { + self.move_scroll_top(ScrollType::Up).into() + } else if self.key_config.is_nav_down(e) { + self.move_scroll_top(ScrollType::Down).into() + } else if key_match(e, self.key_config.keys.page_up) { + self.move_scroll_top(ScrollType::PageUp).into() + } else if key_match(e, self.key_config.keys.page_down) + { + self.move_scroll_top(ScrollType::PageDown).into() + } else if key_match(e, self.key_config.keys.home) + || key_match(e, self.key_config.keys.shift_up) + { + self.move_scroll_top(ScrollType::Home).into() + } else if key_match(e, self.key_config.keys.end) + || key_match(e, self.key_config.keys.shift_down) + { + self.move_scroll_top(ScrollType::End).into() + } else { + EventState::NotConsumed + }); } } diff --git a/src/components/commit_details/mod.rs b/src/components/commit_details/mod.rs index b8872c396b..972443c59f 100644 --- a/src/components/commit_details/mod.rs +++ b/src/components/commit_details/mod.rs @@ -7,10 +7,7 @@ use super::{ Component, DrawableComponent, EventState, StatusTreeComponent, }; use crate::{ - accessors, - app::Environment, - keys::{key_match, SharedKeyConfig}, - strings, + accessors, app::Environment, keys::SharedKeyConfig, strings, }; use anyhow::Result; use asyncgit::{ @@ -208,13 +205,13 @@ impl Component for CommitDetailsComponent { if self.focused() { if let Event::Key(e) = ev { - return if key_match(e, self.key_config.keys.move_down) + return if self.key_config.is_nav_down(e) && self.details_focused() { self.set_details_focus(false); self.file_tree.focus(true); Ok(EventState::Consumed) - } else if key_match(e, self.key_config.keys.move_up) + } else if self.key_config.is_nav_up(e) && self.file_tree.focused() && !self.is_compare() { diff --git a/src/components/commitlist.rs b/src/components/commitlist.rs index a84060151d..7a55386ec5 100644 --- a/src/components/commitlist.rs +++ b/src/components/commitlist.rs @@ -830,42 +830,37 @@ impl DrawableComponent for CommitList { impl Component for CommitList { fn event(&mut self, ev: &Event) -> Result { if let Event::Key(k) = ev { - let selection_changed = - if key_match(k, self.key_config.keys.move_up) { - self.move_selection(ScrollType::Up)? - } else if key_match(k, self.key_config.keys.move_down) - { - self.move_selection(ScrollType::Down)? - } else if key_match(k, self.key_config.keys.shift_up) - || key_match(k, self.key_config.keys.home) - { - self.move_selection(ScrollType::Home)? - } else if key_match( - k, - self.key_config.keys.shift_down, - ) || key_match(k, self.key_config.keys.end) - { - self.move_selection(ScrollType::End)? - } else if key_match(k, self.key_config.keys.page_up) { - self.move_selection(ScrollType::PageUp)? - } else if key_match(k, self.key_config.keys.page_down) - { - self.move_selection(ScrollType::PageDown)? - } else if key_match( - k, - self.key_config.keys.log_mark_commit, - ) { - self.mark(); - true - } else if key_match( - k, - self.key_config.keys.log_checkout_commit, - ) { - self.checkout(); - true - } else { - false - }; + let selection_changed = if self.key_config.is_nav_up(k) { + self.move_selection(ScrollType::Up)? + } else if self.key_config.is_nav_down(k) { + self.move_selection(ScrollType::Down)? + } else if key_match(k, self.key_config.keys.shift_up) + || key_match(k, self.key_config.keys.home) + { + self.move_selection(ScrollType::Home)? + } else if key_match(k, self.key_config.keys.shift_down) + || key_match(k, self.key_config.keys.end) + { + self.move_selection(ScrollType::End)? + } else if key_match(k, self.key_config.keys.page_up) { + self.move_selection(ScrollType::PageUp)? + } else if key_match(k, self.key_config.keys.page_down) { + self.move_selection(ScrollType::PageDown)? + } else if key_match( + k, + self.key_config.keys.log_mark_commit, + ) { + self.mark(); + true + } else if key_match( + k, + self.key_config.keys.log_checkout_commit, + ) { + self.checkout(); + true + } else { + false + }; return Ok(selection_changed.into()); } diff --git a/src/components/diff.rs b/src/components/diff.rs index 04779caada..35a04a7787 100644 --- a/src/components/diff.rs +++ b/src/components/diff.rs @@ -831,8 +831,7 @@ impl Component for DiffComponent { fn event(&mut self, ev: &Event) -> Result { if self.focused() { if let Event::Key(e) = ev { - return if key_match(e, self.key_config.keys.move_down) - { + return if self.key_config.is_nav_down(e) { self.move_selection(ScrollType::Down); Ok(EventState::Consumed) } else if key_match( @@ -851,7 +850,7 @@ impl Component for DiffComponent { } else if key_match(e, self.key_config.keys.home) { self.move_selection(ScrollType::Home); Ok(EventState::Consumed) - } else if key_match(e, self.key_config.keys.move_up) { + } else if self.key_config.is_nav_up(e) { self.move_selection(ScrollType::Up); Ok(EventState::Consumed) } else if key_match(e, self.key_config.keys.page_up) { @@ -861,10 +860,7 @@ impl Component for DiffComponent { { self.move_selection(ScrollType::PageDown); Ok(EventState::Consumed) - } else if key_match( - e, - self.key_config.keys.move_right, - ) { + } else if self.key_config.is_nav_right(e) { self.horizontal_scroll .move_right(HorizontalScrollType::Right); Ok(EventState::Consumed) diff --git a/src/components/revision_files.rs b/src/components/revision_files.rs index 1e15ec086f..f239a09632 100644 --- a/src/components/revision_files.rs +++ b/src/components/revision_files.rs @@ -496,8 +496,7 @@ impl Component for RevisionFilesComponent { self.hide(); return Ok(EventState::Consumed); } - } else if key_match(key, self.key_config.keys.move_right) - { + } else if self.key_config.is_nav_right(key) { if is_tree_focused { self.focus = Focus::File; self.current_file.focus(true); diff --git a/src/components/status_tree.rs b/src/components/status_tree.rs index ac4fc9f6a8..eb15cacf43 100644 --- a/src/components/status_tree.rs +++ b/src/components/status_tree.rs @@ -518,12 +518,11 @@ impl Component for StatusTreeComponent { } else if key_match(e, self.key_config.keys.copy) { self.copy_file_path(); Ok(EventState::Consumed) - } else if key_match(e, self.key_config.keys.move_down) - { + } else if self.key_config.is_nav_down(e) { Ok(self .move_selection(MoveSelection::Down) .into()) - } else if key_match(e, self.key_config.keys.move_up) { + } else if self.key_config.is_nav_up(e) { Ok(self.move_selection(MoveSelection::Up).into()) } else if key_match(e, self.key_config.keys.home) || key_match(e, self.key_config.keys.shift_up) @@ -549,10 +548,7 @@ impl Component for StatusTreeComponent { Ok(self .move_selection(MoveSelection::Left) .into()) - } else if key_match( - e, - self.key_config.keys.move_right, - ) { + } else if self.key_config.is_nav_right(e) { Ok(self .move_selection(MoveSelection::Right) .into()) diff --git a/src/keys/key_config.rs b/src/keys/key_config.rs index 45a9ffdfb2..4f5ebe5533 100644 --- a/src/keys/key_config.rs +++ b/src/keys/key_config.rs @@ -1,11 +1,11 @@ use anyhow::Result; -use crossterm::event::{KeyCode, KeyModifiers}; +use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; use std::{fs::canonicalize, path::PathBuf, rc::Rc}; use crate::{args::get_app_config_path, strings::symbol}; use super::{ - key_list::{GituiKeyEvent, KeysList}, + key_list::{key_match, GituiKeyEvent, KeysList}, symbols::KeySymbols, }; @@ -52,6 +52,24 @@ impl KeyConfig { Ok(Self { keys, symbols }) } + pub fn is_nav_up(&self, key: &KeyEvent) -> bool { + key_match(key, self.keys.move_up) + || (key.code == KeyCode::Char('k') + && key.modifiers == KeyModifiers::NONE) + } + + pub fn is_nav_down(&self, key: &KeyEvent) -> bool { + key_match(key, self.keys.move_down) + || (key.code == KeyCode::Char('j') + && key.modifiers == KeyModifiers::NONE) + } + + pub fn is_nav_right(&self, key: &KeyEvent) -> bool { + key_match(key, self.keys.move_right) + || (key.code == KeyCode::Char('l') + && key.modifiers == KeyModifiers::NONE) + } + fn get_key_symbol(&self, k: KeyCode) -> &str { match k { KeyCode::Enter => &self.symbols.enter, @@ -135,6 +153,71 @@ mod tests { use std::io::Write; use tempfile::NamedTempFile; + fn key_event(code: KeyCode, modifiers: KeyModifiers) -> KeyEvent { + KeyEvent::new(code, modifiers) + } + + #[test] + fn test_vim_nav_up() { + let config = KeyConfig::default(); + assert!(config + .is_nav_up(&key_event(KeyCode::Up, KeyModifiers::NONE))); + assert!(config.is_nav_up(&key_event( + KeyCode::Char('k'), + KeyModifiers::NONE + ))); + assert!(!config.is_nav_up(&key_event( + KeyCode::Char('k'), + KeyModifiers::CONTROL + ))); + assert!(!config.is_nav_up(&key_event( + KeyCode::Char('j'), + KeyModifiers::NONE + ))); + } + + #[test] + fn test_vim_nav_down() { + let config = KeyConfig::default(); + assert!(config.is_nav_down(&key_event( + KeyCode::Down, + KeyModifiers::NONE + ))); + assert!(config.is_nav_down(&key_event( + KeyCode::Char('j'), + KeyModifiers::NONE + ))); + assert!(!config.is_nav_down(&key_event( + KeyCode::Char('j'), + KeyModifiers::CONTROL + ))); + assert!(!config.is_nav_down(&key_event( + KeyCode::Char('k'), + KeyModifiers::NONE + ))); + } + + #[test] + fn test_vim_nav_right() { + let config = KeyConfig::default(); + assert!(config.is_nav_right(&key_event( + KeyCode::Right, + KeyModifiers::NONE + ))); + assert!(config.is_nav_right(&key_event( + KeyCode::Char('l'), + KeyModifiers::NONE + ))); + assert!(!config.is_nav_right(&key_event( + KeyCode::Char('l'), + KeyModifiers::CONTROL + ))); + assert!(!config.is_nav_right(&key_event( + KeyCode::Char('j'), + KeyModifiers::NONE + ))); + } + #[test] fn test_get_hint() { let config = KeyConfig::default(); diff --git a/src/popups/blame_file.rs b/src/popups/blame_file.rs index b7d1304b33..d838742696 100644 --- a/src/popups/blame_file.rs +++ b/src/popups/blame_file.rs @@ -257,13 +257,9 @@ impl Component for BlameFilePopup { if let Event::Key(key) = event { if key_match(key, self.key_config.keys.exit_popup) { self.hide_stacked(false); - } else if key_match(key, self.key_config.keys.move_up) - { + } else if self.key_config.is_nav_up(key) { self.move_selection(ScrollType::Up); - } else if key_match( - key, - self.key_config.keys.move_down, - ) { + } else if self.key_config.is_nav_down(key) { self.move_selection(ScrollType::Down); } else if key_match( key, @@ -289,10 +285,7 @@ impl Component for BlameFilePopup { } else if key_match(key, self.key_config.keys.page_up) { self.move_selection(ScrollType::PageUp); - } else if key_match( - key, - self.key_config.keys.move_right, - ) { + } else if self.key_config.is_nav_right(key) { if let Some(commit_id) = self.selected_commit() { self.hide_stacked(true); self.queue.push(InternalEvent::OpenPopup( diff --git a/src/popups/branchlist.rs b/src/popups/branchlist.rs index fa66ffffad..66c704231f 100644 --- a/src/popups/branchlist.rs +++ b/src/popups/branchlist.rs @@ -170,7 +170,7 @@ impl Component for BranchListPopup { "rebase error:", self.rebase_branch() ); - } else if key_match(e, self.key_config.keys.move_right) + } else if self.key_config.is_nav_right(e) && self.valid_selection() { self.inspect_head_of_branch(); @@ -258,11 +258,11 @@ impl BranchListPopup { fn move_event(&mut self, e: &KeyEvent) -> Result { if key_match(e, self.key_config.keys.exit_popup) { self.hide(); - } else if key_match(e, self.key_config.keys.move_down) { + } else if self.key_config.is_nav_down(e) { return self .move_selection(ScrollType::Up) .map(Into::into); - } else if key_match(e, self.key_config.keys.move_up) { + } else if self.key_config.is_nav_up(e) { return self .move_selection(ScrollType::Down) .map(Into::into); diff --git a/src/popups/checkout_option.rs b/src/popups/checkout_option.rs index c2abdd0a84..18e4a0e69a 100644 --- a/src/popups/checkout_option.rs +++ b/src/popups/checkout_option.rs @@ -200,13 +200,9 @@ impl Component for CheckoutOptionPopup { if let Event::Key(key) = &event { if key_match(key, self.key_config.keys.exit_popup) { self.hide(); - } else if key_match( - key, - self.key_config.keys.move_down, - ) { + } else if self.key_config.is_nav_down(key) { self.change_kind(true); - } else if key_match(key, self.key_config.keys.move_up) - { + } else if self.key_config.is_nav_up(key) { self.change_kind(false); } else if key_match(key, self.key_config.keys.enter) { try_or_popup!( diff --git a/src/popups/compare_commits.rs b/src/popups/compare_commits.rs index 5ff6ef87aa..5694720b84 100644 --- a/src/popups/compare_commits.rs +++ b/src/popups/compare_commits.rs @@ -124,10 +124,8 @@ impl Component for CompareCommitsPopup { } else { self.hide_stacked(false); } - } else if key_match( - e, - self.key_config.keys.move_right, - ) && self.can_focus_diff() + } else if self.key_config.is_nav_right(e) + && self.can_focus_diff() { self.details.focus(false); self.diff.focus(true); diff --git a/src/popups/file_revlog.rs b/src/popups/file_revlog.rs index 771ae857fe..4d4098f38e 100644 --- a/src/popups/file_revlog.rs +++ b/src/popups/file_revlog.rs @@ -507,10 +507,8 @@ impl Component for FileRevlogPopup { } else { self.hide_stacked(false); } - } else if key_match( - key, - self.key_config.keys.move_right, - ) && self.can_focus_diff() + } else if self.key_config.is_nav_right(key) + && self.can_focus_diff() { self.diff.focus(true); } else if key_match(key, self.key_config.keys.enter) { @@ -537,13 +535,9 @@ impl Component for FileRevlogPopup { ), )); } - } else if key_match(key, self.key_config.keys.move_up) - { + } else if self.key_config.is_nav_up(key) { self.move_selection(ScrollType::Up)?; - } else if key_match( - key, - self.key_config.keys.move_down, - ) { + } else if self.key_config.is_nav_down(key) { self.move_selection(ScrollType::Down)?; } else if key_match( key, diff --git a/src/popups/help.rs b/src/popups/help.rs index 0ca3bb0e27..ef30dc7411 100644 --- a/src/popups/help.rs +++ b/src/popups/help.rs @@ -137,10 +137,9 @@ impl Component for HelpPopup { if let Event::Key(e) = ev { if key_match(e, self.key_config.keys.exit_popup) { self.hide(); - } else if key_match(e, self.key_config.keys.move_down) - { + } else if self.key_config.is_nav_down(e) { self.move_selection(true); - } else if key_match(e, self.key_config.keys.move_up) { + } else if self.key_config.is_nav_up(e) { self.move_selection(false); } } diff --git a/src/popups/inspect_commit.rs b/src/popups/inspect_commit.rs index 4acfe88e90..1396debdf8 100644 --- a/src/popups/inspect_commit.rs +++ b/src/popups/inspect_commit.rs @@ -161,10 +161,8 @@ impl Component for InspectCommitPopup { } else { self.hide_stacked(false); } - } else if key_match( - e, - self.key_config.keys.move_right, - ) && self.can_focus_diff() + } else if self.key_config.is_nav_right(e) + && self.can_focus_diff() { self.details.focus(false); self.diff.focus(true); diff --git a/src/popups/options.rs b/src/popups/options.rs index b365c64817..5ae1abaed1 100644 --- a/src/popups/options.rs +++ b/src/popups/options.rs @@ -322,18 +322,11 @@ impl Component for OptionsPopup { if let Event::Key(key) = &event { if key_match(key, self.key_config.keys.exit_popup) { self.hide(); - } else if key_match(key, self.key_config.keys.move_up) - { + } else if self.key_config.is_nav_up(key) { self.move_selection(true); - } else if key_match( - key, - self.key_config.keys.move_down, - ) { + } else if self.key_config.is_nav_down(key) { self.move_selection(false); - } else if key_match( - key, - self.key_config.keys.move_right, - ) { + } else if self.key_config.is_nav_right(key) { self.switch_option(true); } else if key_match( key, diff --git a/src/popups/remotelist.rs b/src/popups/remotelist.rs index c09986aef3..1813d7d1ac 100644 --- a/src/popups/remotelist.rs +++ b/src/popups/remotelist.rs @@ -192,11 +192,11 @@ impl RemoteListPopup { fn move_event(&mut self, e: &KeyEvent) -> Result { if key_match(e, self.key_config.keys.exit_popup) { self.hide(); - } else if key_match(e, self.key_config.keys.move_down) { + } else if self.key_config.is_nav_down(e) { return self .move_selection(ScrollType::Up) .map(Into::into); - } else if key_match(e, self.key_config.keys.move_up) { + } else if self.key_config.is_nav_up(e) { return self .move_selection(ScrollType::Down) .map(Into::into); diff --git a/src/popups/reset.rs b/src/popups/reset.rs index d40d2a6ffe..43907b875d 100644 --- a/src/popups/reset.rs +++ b/src/popups/reset.rs @@ -230,13 +230,9 @@ impl Component for ResetPopup { if let Event::Key(key) = &event { if key_match(key, self.key_config.keys.exit_popup) { self.hide(); - } else if key_match( - key, - self.key_config.keys.move_down, - ) { + } else if self.key_config.is_nav_down(key) { self.change_kind(true); - } else if key_match(key, self.key_config.keys.move_up) - { + } else if self.key_config.is_nav_up(key) { self.change_kind(false); } else if key_match(key, self.key_config.keys.enter) { self.reset(); diff --git a/src/popups/submodules.rs b/src/popups/submodules.rs index f40fe0ba5e..8771607600 100644 --- a/src/popups/submodules.rs +++ b/src/popups/submodules.rs @@ -151,11 +151,11 @@ impl Component for SubmodulesListPopup { if let Event::Key(e) = ev { if key_match(e, self.key_config.keys.exit_popup) { self.hide(); - } else if key_match(e, self.key_config.keys.move_down) { + } else if self.key_config.is_nav_down(e) { return self .move_selection(ScrollType::Up) .map(Into::into); - } else if key_match(e, self.key_config.keys.move_up) { + } else if self.key_config.is_nav_up(e) { return self .move_selection(ScrollType::Down) .map(Into::into); diff --git a/src/popups/taglist.rs b/src/popups/taglist.rs index 959251b2c0..90ec14d031 100644 --- a/src/popups/taglist.rs +++ b/src/popups/taglist.rs @@ -187,13 +187,9 @@ impl Component for TagListPopup { if let Event::Key(key) = event { if key_match(key, self.key_config.keys.exit_popup) { self.hide(); - } else if key_match(key, self.key_config.keys.move_up) - { + } else if self.key_config.is_nav_up(key) { self.move_selection(ScrollType::Up); - } else if key_match( - key, - self.key_config.keys.move_down, - ) { + } else if self.key_config.is_nav_down(key) { self.move_selection(ScrollType::Down); } else if key_match( key, @@ -219,10 +215,8 @@ impl Component for TagListPopup { } else if key_match(key, self.key_config.keys.page_up) { self.move_selection(ScrollType::PageUp); - } else if key_match( - key, - self.key_config.keys.move_right, - ) && self.can_show_annotation() + } else if self.key_config.is_nav_right(key) + && self.can_show_annotation() { self.show_annotation(); } else if key_match( diff --git a/src/tabs/revlog.rs b/src/tabs/revlog.rs index 9200c93f00..e17d3fe0b3 100644 --- a/src/tabs/revlog.rs +++ b/src/tabs/revlog.rs @@ -494,10 +494,8 @@ impl Component for Revlog { Ok(EventState::Consumed) }, ); - } else if key_match( - k, - self.key_config.keys.move_right, - ) && self.commit_details.is_visible() + } else if self.key_config.is_nav_right(k) + && self.commit_details.is_visible() { self.inspect_commit(); return Ok(EventState::Consumed); diff --git a/src/tabs/status.rs b/src/tabs/status.rs index 135cf18e4e..e08c601735 100644 --- a/src/tabs/status.rs +++ b/src/tabs/status.rs @@ -839,10 +839,8 @@ impl Component for Status { { self.switch_focus(self.focus.toggled_focus()) .map(Into::into) - } else if key_match( - k, - self.key_config.keys.move_right, - ) && self.can_focus_diff() + } else if self.key_config.is_nav_right(k) + && self.can_focus_diff() { self.switch_focus(Focus::Diff).map(Into::into) } else if key_match( @@ -854,12 +852,12 @@ impl Component for Status { DiffTarget::WorkingDir => Focus::WorkDir, }) .map(Into::into) - } else if key_match(k, self.key_config.keys.move_down) + } else if self.key_config.is_nav_down(k) && self.focus == Focus::WorkDir && !self.index.is_empty() { self.switch_focus(Focus::Stage).map(Into::into) - } else if key_match(k, self.key_config.keys.move_up) + } else if self.key_config.is_nav_up(k) && self.focus == Focus::Stage && !self.index_wd.is_empty() { diff --git a/src/ui/mod.rs b/src/ui/mod.rs index f0ee1539f9..88589056f5 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -128,15 +128,15 @@ pub fn common_nav( key: &crossterm::event::KeyEvent, key_config: &SharedKeyConfig, ) -> Option { - if key_match(key, key_config.keys.move_down) { + if key_config.is_nav_down(key) { Some(MoveSelection::Down) - } else if key_match(key, key_config.keys.move_up) { + } else if key_config.is_nav_up(key) { Some(MoveSelection::Up) } else if key_match(key, key_config.keys.page_up) { Some(MoveSelection::PageUp) } else if key_match(key, key_config.keys.page_down) { Some(MoveSelection::PageDown) - } else if key_match(key, key_config.keys.move_right) { + } else if key_config.is_nav_right(key) { Some(MoveSelection::Right) } else if key_match(key, key_config.keys.move_left) { Some(MoveSelection::Left)