use serde_json::Value;
const BURST_GAP_MS: f64 = 5.0;
const GESTURE_GAP_MS: f64 = 200.0;
const REFERENCE_GAP_MS: f64 = 100.0;
const MAX_AUTO_LINES: f64 = 10.0;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum WheelScrollLines {
Auto,
Lines(usize),
}
impl WheelScrollLines {
pub const MAX: usize = 100;
pub fn from_json(value: Option<&Value>) -> Self {
match value.and_then(|v| v.as_f64()) {
Some(n) if n.is_finite() => Self::Lines((n.floor().max(1.0) as usize).min(Self::MAX)),
_ => Self::Auto,
}
}
pub fn as_str(&self) -> String {
match self {
Self::Auto => "auto".to_string(),
Self::Lines(n) => (*n).clamp(1, Self::MAX).to_string(),
}
}
}
fn terminal_accelerates_wheel() -> bool {
cfg!(target_os = "macos")
&& std::env::var_os("SSH_CONNECTION").is_none()
&& std::env::var_os("SSH_CLIENT").is_none()
&& std::env::var_os("SSH_TTY").is_none()
}
#[derive(Debug, Clone)]
pub struct WheelScrollAccelerator {
lines: WheelScrollLines,
accelerate: bool,
last_time_ms: f64,
last_direction: i32,
average_gap_ms: Option<f64>,
carry: f64,
}
impl Default for WheelScrollAccelerator {
fn default() -> Self {
Self::new(WheelScrollLines::Auto, !terminal_accelerates_wheel())
}
}
impl WheelScrollAccelerator {
pub fn new(lines: WheelScrollLines, accelerate: bool) -> Self {
WheelScrollAccelerator {
lines,
accelerate,
last_time_ms: f64::NEG_INFINITY,
last_direction: 0,
average_gap_ms: None,
carry: 0.0,
}
}
pub fn for_terminal(lines: WheelScrollLines) -> Self {
Self::new(lines, !terminal_accelerates_wheel())
}
pub fn set_lines(&mut self, lines: WheelScrollLines) {
self.lines = lines;
self.reset();
}
pub fn lines(&self) -> WheelScrollLines {
self.lines
}
pub fn next(&mut self, direction: i32, now_ms: u64) -> usize {
if let WheelScrollLines::Lines(n) = self.lines {
return n.clamp(1, WheelScrollLines::MAX);
}
if !self.accelerate {
return 1;
}
let now = now_ms as f64;
let gap = now - self.last_time_ms;
let same_gesture = direction == self.last_direction && gap <= GESTURE_GAP_MS;
self.last_time_ms = now;
self.last_direction = direction;
if !same_gesture {
self.average_gap_ms = None;
self.carry = 0.0;
return 1;
}
if gap < BURST_GAP_MS {
return 1;
}
self.average_gap_ms = Some(match self.average_gap_ms {
None => gap,
Some(avg) => (avg + gap) / 2.0,
});
let base =
(REFERENCE_GAP_MS / self.average_gap_ms.unwrap_or(gap)).clamp(1.0, MAX_AUTO_LINES);
let lines = base + self.carry;
let whole = lines.floor();
self.carry = lines - whole;
whole as usize
}
pub fn reset(&mut self) {
self.last_time_ms = f64::NEG_INFINITY;
self.last_direction = 0;
self.average_gap_ms = None;
self.carry = 0.0;
}
}
#[derive(Debug, Clone)]
pub struct KeyRepeatAccelerator {
accel: WheelScrollAccelerator,
}
impl Default for KeyRepeatAccelerator {
fn default() -> Self {
Self::new()
}
}
impl KeyRepeatAccelerator {
pub fn new() -> Self {
Self {
accel: WheelScrollAccelerator::new(WheelScrollLines::Auto, true),
}
}
pub fn next(&mut self, direction: i32, now_ms: u64) -> usize {
self.accel.next(direction, now_ms)
}
pub fn reset(&mut self) {
self.accel.reset();
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn key_repeat_accelerates_while_held() {
let mut acc = KeyRepeatAccelerator::new();
assert_eq!(acc.next(1, 0), 1, "孤立一次按 1 行");
assert_eq!(acc.next(1, 33), 3, "33ms 间隔(约 30 次/秒)加速到 3 行");
assert_eq!(acc.next(1, 66), 3);
assert_eq!(acc.next(-1, 70), 1, "换方向即新手势");
assert_eq!(acc.next(1, 800), 1, "停顿超过手势上限后重新从 1 行起算");
acc.reset();
assert_eq!(acc.next(1, 810), 1, "reset 后回到 1 行");
}
#[test]
fn key_repeat_ignores_terminal_and_fixed_lines() {
let mut acc = KeyRepeatAccelerator::new();
acc.next(1, 0);
assert!(acc.next(1, 20) > 1, "极快连按应加速");
}
#[test]
fn fixed_lines_ignores_velocity() {
let mut acc = WheelScrollAccelerator::new(WheelScrollLines::Lines(3), true);
assert_eq!(acc.next(1, 0), 3);
assert_eq!(acc.next(1, 10), 3);
let mut acc = WheelScrollAccelerator::new(WheelScrollLines::Lines(0), true);
assert_eq!(acc.next(1, 0), 1);
}
#[test]
fn isolated_notch_moves_one_line() {
let mut acc = WheelScrollAccelerator::new(WheelScrollLines::Auto, true);
assert_eq!(acc.next(1, 1_000), 1);
assert_eq!(acc.next(1, 1_500), 1);
assert_eq!(acc.next(-1, 1_510), 1);
}
#[test]
fn auto_accelerates_fast_spins() {
let mut acc = WheelScrollAccelerator::new(WheelScrollLines::Auto, true);
assert_eq!(acc.next(1, 0), 1, "首格恒 1 行");
assert_eq!(acc.next(1, 100), 1);
assert_eq!(acc.next(1, 200), 1);
let mut acc = WheelScrollAccelerator::new(WheelScrollLines::Auto, true);
assert_eq!(acc.next(1, 0), 1);
assert_eq!(acc.next(1, 50), 2);
assert_eq!(acc.next(1, 100), 2);
let mut acc = WheelScrollAccelerator::new(WheelScrollLines::Auto, true);
assert_eq!(acc.next(1, 0), 1);
assert_eq!(acc.next(1, 20), 5);
assert_eq!(acc.next(1, 22), 1);
assert_eq!(acc.next(1, 24), 1);
}
#[test]
fn auto_lines_capped_at_six() {
let mut acc = WheelScrollAccelerator::new(WheelScrollLines::Auto, true);
acc.next(1, 0);
for i in 1..20 {
assert!(acc.next(1, i * 100) <= 6);
}
let mut acc = WheelScrollAccelerator::new(WheelScrollLines::Auto, true);
acc.next(1, 0);
let mut max = 0;
for i in 1..50 {
max = max.max(acc.next(1, i * 100_000 + 6 * i));
}
assert!(max <= 6, "max = {max}");
}
#[test]
fn non_accelerating_terminal_stays_one_line() {
let mut acc = WheelScrollAccelerator::new(WheelScrollLines::Auto, false);
for i in 0..10 {
assert_eq!(acc.next(1, i * 10), 1);
}
}
#[test]
fn parses_setting_values() {
use serde_json::json;
assert_eq!(
WheelScrollLines::from_json(Some(&json!("auto"))),
WheelScrollLines::Auto
);
assert_eq!(WheelScrollLines::from_json(None), WheelScrollLines::Auto);
assert_eq!(
WheelScrollLines::from_json(Some(&json!(5))),
WheelScrollLines::Lines(5)
);
assert_eq!(
WheelScrollLines::from_json(Some(&json!(3.7))),
WheelScrollLines::Lines(3)
);
assert_eq!(
WheelScrollLines::from_json(Some(&json!(0))),
WheelScrollLines::Lines(1)
);
assert_eq!(
WheelScrollLines::from_json(Some(&json!(9999))),
WheelScrollLines::Lines(WheelScrollLines::MAX)
);
assert_eq!(WheelScrollLines::Auto.as_str(), "auto");
assert_eq!(WheelScrollLines::Lines(7).as_str(), "7");
}
}