Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 8 additions & 3 deletions src/components/diff.rs
Original file line number Diff line number Diff line change
Expand Up @@ -870,9 +870,14 @@ impl Component for DiffComponent {
Ok(EventState::Consumed)
} else if key_match(e, self.key_config.keys.move_left)
{
self.horizontal_scroll
.move_right(HorizontalScrollType::Left);
Ok(EventState::Consumed)
if self
.horizontal_scroll
.move_right(HorizontalScrollType::Left)
{
Ok(EventState::Consumed)
} else {
Ok(EventState::NotConsumed)
}
} else if key_match(
e,
self.key_config.keys.diff_hunk_next,
Expand Down
90 changes: 90 additions & 0 deletions src/keys/key_repeat_guard.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
//! Debounce arrow-key edge navigation against key autorepeat.

use super::key_list::GituiKeyEvent;
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use std::{
collections::HashMap,
time::{Duration, Instant},
};

const DEFAULT_COOLDOWN: Duration = Duration::from_millis(500);

#[derive(Hash, Eq, PartialEq, Clone, Copy)]
struct KeyId {
code: KeyCode,
modifiers: KeyModifiers,
}

impl From<GituiKeyEvent> for KeyId {
fn from(key: GituiKeyEvent) -> Self {
Self {
code: key.code,
modifiers: key.modifiers,
}
}
}

///
pub struct KeyRepeatGuard {
last: HashMap<KeyId, Instant>,
cooldown: Duration,
}

impl KeyRepeatGuard {
///
pub fn new() -> Self {
Self {
last: HashMap::new(),
cooldown: DEFAULT_COOLDOWN,
}
}

#[cfg(test)]
pub fn with_cooldown(cooldown: Duration) -> Self {
Self {
last: HashMap::new(),
cooldown,
}
}

///
pub fn record(&mut self, key: GituiKeyEvent) {
self.last.insert(key.into(), Instant::now());
}

///
pub fn record_key_event(&mut self, key: &KeyEvent) {
self.record(GituiKeyEvent {
code: key.code,
modifiers: key.modifiers,
});
}

/// Whether edge navigation (leaving a scrollable view) should run now.
pub fn allow_edge_navigation(&self, key: GituiKeyEvent) -> bool {
self.last
.get(&key.into())
.is_none_or(|t| Instant::now().duration_since(*t) >= self.cooldown)
}
}

#[cfg(test)]
mod tests {
use super::*;
use crossterm::event::{KeyCode, KeyModifiers};
use std::thread::sleep;

#[test]
fn test_blocks_rapid_repeats() {
let mut guard = KeyRepeatGuard::with_cooldown(Duration::from_millis(50));
let key =
GituiKeyEvent::new(KeyCode::Up, KeyModifiers::empty());

assert!(guard.allow_edge_navigation(key));
guard.record(key);
assert!(!guard.allow_edge_navigation(key));

sleep(Duration::from_millis(60));
assert!(guard.allow_edge_navigation(key));
}
}
2 changes: 2 additions & 0 deletions src/keys/mod.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
mod key_config;
mod key_list;
mod key_repeat_guard;
mod symbols;

pub use key_config::{KeyConfig, SharedKeyConfig};
pub use key_list::key_match;
pub use key_repeat_guard::KeyRepeatGuard;
61 changes: 58 additions & 3 deletions src/tabs/status.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use crate::{
DiffComponent, DrawableComponent, EventState,
FileTreeItemKind,
},
keys::{key_match, SharedKeyConfig},
keys::{key_match, KeyRepeatGuard, SharedKeyConfig},
options::SharedOptions,
queue::{Action, InternalEvent, NeedsUpdate, Queue, ResetItem},
strings, try_or_popup,
Expand Down Expand Up @@ -81,6 +81,7 @@ pub struct Status {
git_action_executed: bool,
options: SharedOptions,
key_config: SharedKeyConfig,
key_repeat_guard: KeyRepeatGuard,
}

impl DrawableComponent for Status {
Expand Down Expand Up @@ -198,6 +199,7 @@ impl Status {
env.repo.clone(),
),
key_config: env.key_config.clone(),
key_repeat_guard: KeyRepeatGuard::new(),
options: env.options.clone(),
repo: env.repo.clone(),
}
Expand Down Expand Up @@ -581,6 +583,17 @@ impl Status {
}
}

fn record_navigation_key(&mut self, key: &crossterm::event::KeyEvent) {
let keys = &self.key_config.keys;
if key_match(key, keys.move_up)
|| key_match(key, keys.move_down)
|| key_match(key, keys.move_left)
|| key_match(key, keys.move_right)
{
self.key_repeat_guard.record_key_event(key);
}
}

fn fetch(&self) {
if self.can_fetch() {
self.queue.push(InternalEvent::FetchRemotes);
Expand Down Expand Up @@ -820,6 +833,9 @@ impl Component for Status {
if event_pump(ev, self.components_mut().as_mut_slice())?
.is_consumed()
{
if let Event::Key(k) = ev {
self.record_navigation_key(k);
}
self.git_action_executed = true;
return Ok(EventState::Consumed);
}
Expand All @@ -845,6 +861,25 @@ impl Component for Status {
) && self.can_focus_diff()
{
self.switch_focus(Focus::Diff).map(Into::into)
} else if key_match(
k,
self.key_config.keys.move_left,
) && self.is_focus_on_diff()
{
let binding = self.key_config.keys.move_left;
let allow = self
.key_repeat_guard
.allow_edge_navigation(binding);
self.key_repeat_guard.record(binding);
if allow {
return self
.switch_focus(match self.diff_target {
DiffTarget::Stage => Focus::Stage,
DiffTarget::WorkingDir => Focus::WorkDir,
})
.map(Into::into);
}
return Ok(EventState::Consumed);
} else if key_match(
k,
self.key_config.keys.exit_popup,
Expand All @@ -858,12 +893,32 @@ impl Component for Status {
&& self.focus == Focus::WorkDir
&& !self.index.is_empty()
{
self.switch_focus(Focus::Stage).map(Into::into)
let binding = self.key_config.keys.move_down;
let allow = self
.key_repeat_guard
.allow_edge_navigation(binding);
self.key_repeat_guard.record(binding);
if allow {
return self
.switch_focus(Focus::Stage)
.map(Into::into);
}
return Ok(EventState::Consumed);
} else if key_match(k, self.key_config.keys.move_up)
&& self.focus == Focus::Stage
&& !self.index_wd.is_empty()
{
self.switch_focus(Focus::WorkDir).map(Into::into)
let binding = self.key_config.keys.move_up;
let allow = self
.key_repeat_guard
.allow_edge_navigation(binding);
self.key_repeat_guard.record(binding);
if allow {
return self
.switch_focus(Focus::WorkDir)
.map(Into::into);
}
return Ok(EventState::Consumed);
} else if key_match(
k,
self.key_config.keys.select_branch,
Expand Down