mirror of
https://github.com/gitui-org/gitui
synced 2026-05-23 08:58:21 +00:00
Merge d7dddc7b64 into 8619c07f3f
This commit is contained in:
commit
712055650c
4 changed files with 158 additions and 6 deletions
|
|
@ -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,
|
||||
|
|
|
|||
90
src/keys/key_repeat_guard.rs
Normal file
90
src/keys/key_repeat_guard.rs
Normal file
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
@ -81,6 +81,7 @@ pub struct Status {
|
|||
git_action_executed: bool,
|
||||
options: SharedOptions,
|
||||
key_config: SharedKeyConfig,
|
||||
key_repeat_guard: KeyRepeatGuard,
|
||||
}
|
||||
|
||||
impl DrawableComponent for Status {
|
||||
|
|
@ -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(),
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Reference in a new issue