Skip to content

Commit 0ca15c9

Browse files
committed
Refactor into lib + add basic tests
1 parent e892004 commit 0ca15c9

2 files changed

Lines changed: 231 additions & 63 deletions

File tree

src/lib.rs

Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
1+
#![feature(default_field_values)]
2+
3+
use evdev::{EventType, InputEvent, RelativeAxisCode};
4+
use std::time::SystemTime;
5+
6+
/// Parameters for the anxious scroll algorithm
7+
#[derive(Debug, Clone)]
8+
pub struct AnxiousParams {
9+
/// Base sensitivity to start at
10+
pub base_sens: f32,
11+
/// Max sensitivity to taper off towards
12+
pub max_sens: f32,
13+
/// How fast to ramp up the logistic function
14+
pub ramp_up_rate: f32,
15+
}
16+
17+
impl Default for AnxiousParams {
18+
fn default() -> Self {
19+
Self {
20+
base_sens: 1.0,
21+
max_sens: 15.0,
22+
ramp_up_rate: 0.3,
23+
}
24+
}
25+
}
26+
27+
/// State for tracking scroll velocity over time
28+
#[derive(Debug)]
29+
#[repr(transparent)]
30+
pub struct AnxiousState {
31+
pub prev_time: SystemTime,
32+
}
33+
34+
impl AnxiousState {
35+
pub fn new() -> Self {
36+
Self {
37+
prev_time: SystemTime::now(),
38+
}
39+
}
40+
}
41+
42+
#[inline(always)]
43+
/// We use a logistic function as the transformation function.
44+
/// f(vel) = max_sens / (1 + C * e^(-ramp_up_rate * vel)), where
45+
/// C = (max_sens / (base_sens) - 1
46+
/// Visualisation: https://www.desmos.com/calculator/grsgyudrch
47+
pub fn apply_anxious_scroll(
48+
value: f32,
49+
timestamp: SystemTime,
50+
anxious_params: &AnxiousParams,
51+
anxious_state: &mut AnxiousState,
52+
) -> i32 {
53+
let elapsed_time = timestamp.duration_since(anxious_state.prev_time).unwrap();
54+
anxious_state.prev_time = timestamp;
55+
56+
let vel = value.abs() / elapsed_time.as_millis() as f32;
57+
let c = (anxious_params.max_sens / anxious_params.base_sens) - 1.0;
58+
// TODO: Use fast approximation for the calculation
59+
let sens = anxious_params.max_sens
60+
/ (1.0 + c * (-1.0 * vel as f32 * anxious_params.ramp_up_rate).exp());
61+
return (value * sens) as i32;
62+
}
63+
64+
#[inline(always)]
65+
/// Process a batch of input events, applying anxious scroll transformation to wheel events
66+
/// This is a pure function with no I/O dependencies, making it easily testable and benchmarkable
67+
pub fn process_events(
68+
events: impl Iterator<Item = InputEvent>,
69+
anxious_params: &AnxiousParams,
70+
anxious_state: &mut AnxiousState,
71+
) -> Vec<InputEvent> {
72+
let mut event_batch = Vec::new();
73+
74+
for event in events {
75+
if event.event_type() == EventType::RELATIVE
76+
&& event.code() == RelativeAxisCode::REL_WHEEL_HI_RES.0
77+
{
78+
// Create a new event with modified value
79+
let modified_value = apply_anxious_scroll(
80+
event.value() as f32,
81+
event.timestamp(),
82+
anxious_params,
83+
anxious_state,
84+
);
85+
// new_now() is not necessary here as the kernel will update the time field
86+
// when it emits the events to any programs reading the event "file".
87+
let modified_event = InputEvent::new(event.event_type().0, event.code(), modified_value);
88+
event_batch.push(modified_event);
89+
} else if event.event_type() == EventType::RELATIVE
90+
&& event.code() == RelativeAxisCode::REL_WHEEL.0
91+
{
92+
// Drop event
93+
continue;
94+
} else {
95+
// Pass through all other events unchanged
96+
event_batch.push(event);
97+
}
98+
}
99+
100+
event_batch
101+
}
102+
103+
#[cfg(test)]
104+
mod tests {
105+
use super::*;
106+
use std::time::{Duration, UNIX_EPOCH};
107+
108+
fn create_test_state_with_time(prev_time: SystemTime) -> AnxiousState {
109+
AnxiousState { prev_time }
110+
}
111+
112+
#[test]
113+
fn test_zero_value() {
114+
let params = AnxiousParams::default();
115+
let base_time = UNIX_EPOCH + Duration::from_secs(1000000000);
116+
let mut state = create_test_state_with_time(base_time);
117+
118+
let result = apply_anxious_scroll(0.0, base_time + Duration::from_millis(10), &params, &mut state);
119+
assert_eq!(result, 0);
120+
}
121+
122+
#[test]
123+
fn test_large_value() {
124+
let params = AnxiousParams::default();
125+
let base_time = UNIX_EPOCH + Duration::from_secs(1000000000);
126+
let mut state = create_test_state_with_time(base_time);
127+
128+
let result = apply_anxious_scroll(1000.0, base_time + Duration::from_millis(1), &params, &mut state);
129+
// Should not panic and should return a reasonable value
130+
assert!(result > 0);
131+
}
132+
133+
#[test]
134+
fn test_negative_value() {
135+
let params = AnxiousParams::default();
136+
let base_time = UNIX_EPOCH + Duration::from_secs(1000000000);
137+
let mut state = create_test_state_with_time(base_time);
138+
139+
let result = apply_anxious_scroll(-10.0, base_time + Duration::from_millis(10), &params, &mut state);
140+
assert!(result < 0);
141+
}
142+
143+
#[test]
144+
fn test_very_small_elapsed_time() {
145+
let params = AnxiousParams::default();
146+
let base_time = UNIX_EPOCH + Duration::from_secs(1000000000);
147+
let mut state = create_test_state_with_time(base_time);
148+
149+
// Test with very small elapsed time (1 microsecond)
150+
let result = apply_anxious_scroll(10.0, base_time + Duration::from_micros(1), &params, &mut state);
151+
// Should not panic and should return a reasonable value
152+
assert!(result > 0);
153+
}
154+
155+
#[test]
156+
fn test_parameter_configurations() {
157+
let base_time = UNIX_EPOCH + Duration::from_secs(1000000000);
158+
let mut state = create_test_state_with_time(base_time);
159+
160+
// Test default parameters
161+
let default_params = AnxiousParams::default();
162+
let result1 = apply_anxious_scroll(10.0, base_time + Duration::from_millis(10), &default_params, &mut state);
163+
164+
// Test high sensitivity
165+
let high_sens_params = AnxiousParams {
166+
base_sens: 1.0,
167+
max_sens: 30.0,
168+
ramp_up_rate: 0.5,
169+
};
170+
let result2 = apply_anxious_scroll(10.0, base_time + Duration::from_millis(10), &high_sens_params, &mut state);
171+
172+
// Test low sensitivity
173+
let low_sens_params = AnxiousParams {
174+
base_sens: 0.5,
175+
max_sens: 5.0,
176+
ramp_up_rate: 0.1,
177+
};
178+
let result3 = apply_anxious_scroll(10.0, base_time + Duration::from_millis(10), &low_sens_params, &mut state);
179+
180+
// All should return reasonable values
181+
assert!(result1 > 0);
182+
assert!(result2 > 0);
183+
assert!(result3 > 0);
184+
185+
// High sensitivity should generally produce higher values than low sensitivity
186+
assert!(result2 > result3);
187+
}
188+
189+
#[test]
190+
fn test_process_events_basic() {
191+
use evdev::{EventType, InputEvent, RelativeAxisCode};
192+
193+
// Create events with proper timestamps to avoid SystemTime issues
194+
let base_time = UNIX_EPOCH + Duration::from_secs(1000000000);
195+
let events = vec![
196+
InputEvent::new_now(EventType::RELATIVE.0, RelativeAxisCode::REL_WHEEL_HI_RES.0, 120),
197+
InputEvent::new_now(EventType::RELATIVE.0, RelativeAxisCode::REL_WHEEL.0, 1), // Should be dropped
198+
InputEvent::new_now(EventType::RELATIVE.0, RelativeAxisCode::REL_X.0, 10), // Should pass through
199+
];
200+
201+
let params = AnxiousParams::default();
202+
let mut state = create_test_state_with_time(base_time);
203+
204+
let result = process_events(events.iter().cloned(), &params, &mut state);
205+
206+
// Should have 2 events: one processed wheel event and one pass-through event
207+
assert_eq!(result.len(), 2);
208+
209+
// First event should be the processed wheel event
210+
assert_eq!(result[0].event_type(), EventType::RELATIVE);
211+
assert_eq!(result[0].code(), RelativeAxisCode::REL_WHEEL_HI_RES.0);
212+
213+
// Second event should be the pass-through event
214+
assert_eq!(result[1].event_type(), EventType::RELATIVE);
215+
assert_eq!(result[1].code(), RelativeAxisCode::REL_X.0);
216+
assert_eq!(result[1].value(), 10);
217+
}
218+
}

src/main.rs

Lines changed: 13 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,9 @@
1-
#![feature(default_field_values)]
2-
31
use anyhow::{Context, Result};
42
use clap::Parser;
5-
use evdev::{
6-
uinput::VirtualDevice, Device, EventType, RelativeAxisCode,
7-
};
8-
use log::{debug, error, info};
3+
use evdev::{uinput::VirtualDevice, Device, EventType, RelativeAxisCode};
4+
use log::{error, info};
5+
use mouse_scroll_daemon::{AnxiousParams, AnxiousState, process_events};
96
use std::path::PathBuf;
10-
use std::time::SystemTime;
117

128
#[derive(Parser, Debug)]
139
#[command(author, version, about, long_about = None)]
@@ -21,20 +17,6 @@ struct Args {
2117
debug: bool,
2218
}
2319

24-
// TODO: Add ability to load params
25-
struct AnxiousParams {
26-
/// Base sensitivity to start at
27-
base_sens: f32 = 1.0,
28-
/// Max sensitivity to taper off towards
29-
max_sens: f32 = 15.0,
30-
/// How fast to ramp up the logistic function
31-
ramp_up_rate: f32 = 0.3,
32-
}
33-
34-
struct AnxiousState {
35-
prev_time: SystemTime,
36-
}
37-
3820
fn main() -> Result<()> {
3921
let args = Args::parse();
4022

@@ -45,9 +27,9 @@ fn main() -> Result<()> {
4527
info!("Starting anxious scroll daemon");
4628

4729
// Initialize anxious parameters and state
48-
let anxious_params = AnxiousParams{ .. };
30+
let anxious_params = AnxiousParams::default();
4931
// TODO: analyse initial jitter?
50-
let mut anxious_state = AnxiousState{ prev_time: SystemTime::now() };
32+
let mut anxious_state = AnxiousState::new();
5133

5234
// Find the physical mouse device
5335
let mut physical_device = find_mouse_device(args.device)?;
@@ -125,50 +107,18 @@ fn create_virtual_mouse(physical_device: &Device) -> Result<VirtualDevice> {
125107
Ok(builder.build()?)
126108
}
127109

128-
#[inline(always)]
129-
/// We use a logistic function as the transformation function.
130-
/// f(vel) = max_sens / (1 + C * e^(-ramp_up_rate * vel)), where
131-
/// C = (max_sens / (base_sens) - 1
132-
/// Visualisation: https://www.desmos.com/calculator/grsgyudrch
133-
fn apply_anxious_scroll(value: f32, timestamp: SystemTime, anxious_params: &AnxiousParams, anxious_state: &mut AnxiousState) -> i32 {
134-
let elapsed_time = timestamp.duration_since(anxious_state.prev_time).unwrap();
135-
anxious_state.prev_time = timestamp;
136-
137-
let vel = value.abs() / elapsed_time.as_millis() as f32;
138-
let C = (anxious_params.max_sens / anxious_params.base_sens) - 1.0;
139-
// TODO: Use fast approximation for the calculation
140-
let sens = anxious_params.max_sens / (1.0 + C * (-1.0 * vel as f32 * anxious_params.ramp_up_rate).exp());
141-
return (value * sens) as i32;
142-
}
143110

144-
fn run_pass_through_loop(physical_device: &mut Device, virtual_device: &mut VirtualDevice, anxious_params: &AnxiousParams, anxious_state: &mut AnxiousState) -> Result<()> {
111+
fn run_pass_through_loop(
112+
physical_device: &mut Device,
113+
virtual_device: &mut VirtualDevice,
114+
anxious_params: &AnxiousParams,
115+
anxious_state: &mut AnxiousState,
116+
) -> Result<()> {
145117
loop {
146118
match physical_device.fetch_events() {
147119
Ok(events) => {
148-
// Process events in batches to handle high-resolution scroll coordination
149-
let mut event_batch = Vec::new();
150-
151-
for event in events {
152-
if event.event_type() == EventType::RELATIVE && event.code() == RelativeAxisCode::REL_WHEEL_HI_RES.0 {
153-
// Create a new event with modified value
154-
let modified_value = apply_anxious_scroll(event.value() as f32, event.timestamp(),anxious_params, anxious_state);
155-
// new_now() is not necessary here as the kernel will update the time field
156-
// when it emits the events to any programs reading the event "file".
157-
let modified_event = evdev::InputEvent::new(
158-
event.event_type().0,
159-
event.code(),
160-
modified_value
161-
);
162-
event_batch.push(modified_event);
163-
}
164-
else if event.event_type() == EventType::RELATIVE && event.code() == RelativeAxisCode::REL_WHEEL.0 {
165-
// Drop event
166-
continue;
167-
} else {
168-
// Pass through all other events unchanged
169-
event_batch.push(event);
170-
}
171-
}
120+
// Process events using the pure function from lib
121+
let event_batch = process_events(events, anxious_params, anxious_state);
172122

173123
// Emit all events in the batch together
174124
if !event_batch.is_empty() {

0 commit comments

Comments
 (0)