use std::collections::VecDeque;
use std::time::Instant;
#[derive(Debug, Clone)]
struct KeySequence {
key: String,
count: usize,
_first_press: Instant,
last_press: Instant,
}
pub struct KeySequenceRenderer {
sequences: VecDeque<KeySequence>,
max_display: usize,
collapse_window_ms: u64,
chord_mode: Option<String>,
fade_duration_ms: u64,
enabled: bool,
}
impl KeySequenceRenderer {
#[must_use]
pub fn new() -> Self {
Self {
sequences: VecDeque::with_capacity(10),
max_display: 5,
collapse_window_ms: 500, chord_mode: None,
fade_duration_ms: 2000,
enabled: true, }
}
pub fn set_enabled(&mut self, enabled: bool) {
self.enabled = enabled;
if !enabled {
self.sequences.clear();
self.chord_mode = None;
}
}
pub fn record_key(&mut self, key: String) {
if !self.enabled {
return;
}
let now = Instant::now();
if let Some(last) = self.sequences.back_mut() {
if last.key == key
&& last.last_press.elapsed().as_millis() < u128::from(self.collapse_window_ms)
{
last.count += 1;
last.last_press = now;
return;
}
}
self.sequences.push_back(KeySequence {
key,
count: 1,
_first_press: now,
last_press: now,
});
self.cleanup_sequences();
}
pub fn set_chord_mode(&mut self, description: Option<String>) {
self.chord_mode = description;
}
pub fn clear_chord_mode(&mut self) {
self.chord_mode = None;
}
#[must_use]
pub fn get_display(&self) -> String {
if !self.enabled {
return String::new();
}
if let Some(ref chord_desc) = self.chord_mode {
return self.format_chord_display(chord_desc);
}
self.format_sequence_display()
}
fn format_chord_display(&self, description: &str) -> String {
if description.starts_with("Yank mode:") {
if let Some(options) = description.strip_prefix("Yank mode: ") {
let keys: Vec<&str> = options
.split(", ")
.filter_map(|part| {
let key = part.split('=').next()?;
if key == "ESC" {
None } else {
Some(key)
}
})
.collect();
if !keys.is_empty() {
return format!("y({})", keys.join(","));
}
}
}
if description.contains("Waiting for:") {
if let Some(waiting) = description.strip_prefix("Waiting for: ") {
let parts: Vec<&str> = waiting
.split(", ")
.map(|p| p.split(" → ").next().unwrap_or(p))
.collect();
if !parts.is_empty() && self.sequences.back().is_some() {
if let Some(last) = self.sequences.back() {
return format!("{}({})", last.key, parts.join(","));
}
}
}
}
if description.len() > 20 {
format!("{}...", &description[..17])
} else {
description.to_string()
}
}
fn format_sequence_display(&self) -> String {
let now = Instant::now();
let mut display_sequences = Vec::new();
for seq in self.sequences.iter().rev().take(self.max_display) {
let age_ms = now.duration_since(seq.last_press).as_millis() as u64;
if age_ms > self.fade_duration_ms {
continue;
}
let formatted = if seq.count > 1 {
format!("{}{}", seq.count, seq.key)
} else {
seq.key.clone()
};
display_sequences.push(formatted);
}
display_sequences.reverse();
display_sequences.join(" ")
}
fn cleanup_sequences(&mut self) {
let now = Instant::now();
self.sequences.retain(|seq| {
now.duration_since(seq.last_press).as_millis() < u128::from(self.fade_duration_ms)
});
while self.sequences.len() > self.max_display * 2 {
self.sequences.pop_front();
}
}
#[must_use]
pub fn has_content(&self) -> bool {
self.enabled && (!self.sequences.is_empty() || self.chord_mode.is_some())
}
pub fn clear(&mut self) {
self.sequences.clear();
self.chord_mode = None;
}
pub fn configure(
&mut self,
max_display: Option<usize>,
collapse_window_ms: Option<u64>,
fade_duration_ms: Option<u64>,
) {
if let Some(max) = max_display {
self.max_display = max;
}
if let Some(window) = collapse_window_ms {
self.collapse_window_ms = window;
}
if let Some(fade) = fade_duration_ms {
self.fade_duration_ms = fade;
}
}
#[must_use]
pub fn is_enabled(&self) -> bool {
self.enabled
}
#[must_use]
pub fn get_chord_mode(&self) -> &Option<String> {
&self.chord_mode
}
#[must_use]
pub fn sequence_count(&self) -> usize {
self.sequences.len()
}
#[must_use]
pub fn get_sequences(&self) -> Vec<(String, usize)> {
self.sequences
.iter()
.map(|seq| (seq.key.clone(), seq.count))
.collect()
}
}
impl Default for KeySequenceRenderer {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::thread::sleep;
use std::time::Duration;
#[test]
fn test_collapse_repeated_keys() {
let mut renderer = KeySequenceRenderer::new();
renderer.set_enabled(true);
renderer.record_key("j".to_string());
sleep(Duration::from_millis(50));
renderer.record_key("j".to_string());
sleep(Duration::from_millis(50));
renderer.record_key("j".to_string());
let display = renderer.get_display();
assert_eq!(display, "3j");
}
#[test]
fn test_separate_sequences() {
let mut renderer = KeySequenceRenderer::new();
renderer.set_enabled(true);
renderer.record_key("j".to_string());
sleep(Duration::from_millis(600)); renderer.record_key("k".to_string());
sleep(Duration::from_millis(600));
renderer.record_key("h".to_string());
let display = renderer.get_display();
assert_eq!(display, "j k h");
}
#[test]
fn test_chord_mode_display() {
let mut renderer = KeySequenceRenderer::new();
renderer.set_enabled(true);
renderer.record_key("y".to_string());
renderer.set_chord_mode(Some(
"Yank mode: y=row, c=column, a=all, ESC=cancel".to_string(),
));
let display = renderer.get_display();
assert_eq!(display, "y(y,c,a)");
}
#[test]
fn test_max_display_limit() {
let mut renderer = KeySequenceRenderer::new();
renderer.set_enabled(true);
renderer.configure(Some(3), None, None);
for i in 1..=10 {
renderer.record_key(format!("{i}"));
sleep(Duration::from_millis(600));
}
let display = renderer.get_display();
let parts: Vec<&str> = display.split(' ').collect();
assert!(parts.len() <= 3);
}
#[test]
fn test_mixed_repeated_and_single() {
let mut renderer = KeySequenceRenderer::new();
renderer.set_enabled(true);
renderer.record_key("j".to_string());
sleep(Duration::from_millis(50));
renderer.record_key("j".to_string());
sleep(Duration::from_millis(50));
renderer.record_key("j".to_string());
sleep(Duration::from_millis(600)); renderer.record_key("g".to_string());
sleep(Duration::from_millis(600));
renderer.record_key("k".to_string());
sleep(Duration::from_millis(50));
renderer.record_key("k".to_string());
let display = renderer.get_display();
assert_eq!(display, "3j g 2k");
}
}