stabilize live speed indicator and normalize hi-res wheel handling
This commit is contained in:
@@ -8,8 +8,6 @@ use std::{
|
|||||||
time::Duration,
|
time::Duration,
|
||||||
};
|
};
|
||||||
|
|
||||||
use anyhow::Context;
|
|
||||||
|
|
||||||
static INPUT_SPEED_BITS: AtomicU64 = AtomicU64::new(0);
|
static INPUT_SPEED_BITS: AtomicU64 = AtomicU64::new(0);
|
||||||
|
|
||||||
pub fn read_input_speed() -> f64 {
|
pub fn read_input_speed() -> f64 {
|
||||||
@@ -21,7 +19,16 @@ pub fn setup_input_speed_reader() -> JoinHandle<anyhow::Result<()>> {
|
|||||||
let path = stats_path();
|
let path = stats_path();
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
if let Ok(content) = std::fs::read_to_string(&path)
|
let stale = std::fs::metadata(&path)
|
||||||
|
.and_then(|m| m.modified())
|
||||||
|
.ok()
|
||||||
|
.and_then(|t| t.elapsed().ok())
|
||||||
|
.map(|age| age > Duration::from_millis(300))
|
||||||
|
.unwrap_or(true);
|
||||||
|
|
||||||
|
if stale {
|
||||||
|
INPUT_SPEED_BITS.store(0.0f64.to_bits(), Ordering::Relaxed);
|
||||||
|
} else if let Ok(content) = std::fs::read_to_string(&path)
|
||||||
&& let Ok(parsed) = content.trim().parse::<f64>()
|
&& let Ok(parsed) = content.trim().parse::<f64>()
|
||||||
{
|
{
|
||||||
INPUT_SPEED_BITS.store(parsed.to_bits(), Ordering::Relaxed);
|
INPUT_SPEED_BITS.store(parsed.to_bits(), Ordering::Relaxed);
|
||||||
@@ -37,18 +44,6 @@ fn stats_path() -> PathBuf {
|
|||||||
return PathBuf::from(path);
|
return PathBuf::from(path);
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Ok(runtime_dir) = std::env::var("XDG_RUNTIME_DIR") {
|
// Keep default shared across sudo/non-sudo runs.
|
||||||
return PathBuf::from(runtime_dir).join("gsf-speed.txt");
|
|
||||||
}
|
|
||||||
|
|
||||||
let home = std::env::var("HOME").context("HOME missing").ok();
|
|
||||||
if let Some(home) = home {
|
|
||||||
return PathBuf::from(home)
|
|
||||||
.join(".local")
|
|
||||||
.join("state")
|
|
||||||
.join("gsf")
|
|
||||||
.join("speed.txt");
|
|
||||||
}
|
|
||||||
|
|
||||||
PathBuf::from("/tmp/gsf-speed.txt")
|
PathBuf::from("/tmp/gsf-speed.txt")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,9 +51,9 @@ impl Default for StoredConfig {
|
|||||||
yx_ratio: 1.0,
|
yx_ratio: 1.0,
|
||||||
input_dpi: 1000.0,
|
input_dpi: 1000.0,
|
||||||
angle_rotation: 0.0,
|
angle_rotation: 0.0,
|
||||||
accel: 0.0,
|
accel: 0.3,
|
||||||
offset_linear: 0.0,
|
offset_linear: 2.0,
|
||||||
output_cap: 0.0,
|
output_cap: 2.0,
|
||||||
decay_rate: 0.1,
|
decay_rate: 0.1,
|
||||||
offset_natural: 0.0,
|
offset_natural: 0.0,
|
||||||
limit: 1.5,
|
limit: 1.5,
|
||||||
@@ -164,6 +164,19 @@ fn config_path() -> anyhow::Result<PathBuf> {
|
|||||||
return Ok(PathBuf::from(p));
|
return Ok(PathBuf::from(p));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If daemon is launched with sudo, prefer caller's home so CLI/TUI and daemon share config.
|
||||||
|
if let Ok(sudo_user) = std::env::var("SUDO_USER")
|
||||||
|
&& !sudo_user.is_empty()
|
||||||
|
&& sudo_user != "root"
|
||||||
|
{
|
||||||
|
let candidate = PathBuf::from("/home")
|
||||||
|
.join(sudo_user)
|
||||||
|
.join(".config")
|
||||||
|
.join("gsf")
|
||||||
|
.join("config.json");
|
||||||
|
return Ok(candidate);
|
||||||
|
}
|
||||||
|
|
||||||
let home = std::env::var("HOME").context("HOME is not set")?;
|
let home = std::env::var("HOME").context("HOME is not set")?;
|
||||||
Ok(PathBuf::from(home)
|
Ok(PathBuf::from(home)
|
||||||
.join(".config")
|
.join(".config")
|
||||||
|
|||||||
+13
-13
@@ -277,7 +277,18 @@ fn transform_event(
|
|||||||
.max(0.001);
|
.max(0.001);
|
||||||
state.last_scroll_event_at = Some(now);
|
state.last_scroll_event_at = Some(now);
|
||||||
|
|
||||||
let speed = (value.abs() as f64) / dt;
|
let axis_unit = if axis == RelativeAxisCode::REL_WHEEL_HI_RES
|
||||||
|
|| axis == RelativeAxisCode::REL_HWHEEL_HI_RES
|
||||||
|
{
|
||||||
|
120.0
|
||||||
|
} else {
|
||||||
|
1.0
|
||||||
|
};
|
||||||
|
|
||||||
|
// Normalize hi-res wheel units (120 per detent) to "detent-equivalent"
|
||||||
|
// speed so tuning behaves similarly across REL_WHEEL and REL_WHEEL_HI_RES.
|
||||||
|
let normalized_value = (value as f64) / axis_unit;
|
||||||
|
let speed = normalized_value.abs() / dt;
|
||||||
let (gain_x, gain_y) = sensitivity(speed, *mode, params);
|
let (gain_x, gain_y) = sensitivity(speed, *mode, params);
|
||||||
let gain = if axis == RelativeAxisCode::REL_HWHEEL
|
let gain = if axis == RelativeAxisCode::REL_HWHEEL
|
||||||
|| axis == RelativeAxisCode::REL_HWHEEL_HI_RES
|
|| axis == RelativeAxisCode::REL_HWHEEL_HI_RES
|
||||||
@@ -335,17 +346,6 @@ fn stats_path() -> PathBuf {
|
|||||||
return PathBuf::from(path);
|
return PathBuf::from(path);
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Ok(runtime_dir) = std::env::var("XDG_RUNTIME_DIR") {
|
// Keep default shared across sudo/non-sudo runs.
|
||||||
return PathBuf::from(runtime_dir).join("gsf-speed.txt");
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Ok(home) = std::env::var("HOME") {
|
|
||||||
return PathBuf::from(home)
|
|
||||||
.join(".local")
|
|
||||||
.join("state")
|
|
||||||
.join("gsf")
|
|
||||||
.join("speed.txt");
|
|
||||||
}
|
|
||||||
|
|
||||||
PathBuf::from("/tmp/gsf-speed.txt")
|
PathBuf::from("/tmp/gsf-speed.txt")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,205 @@
|
|||||||
|
use std::fmt::Debug;
|
||||||
|
|
||||||
|
use gsf_core::{ContextRef, SensXY, get_param_value_from_ctx, persist::ParamStore, sensitivity};
|
||||||
|
|
||||||
|
use crate::{action, component::TuiComponent};
|
||||||
|
|
||||||
|
use crossterm::event::KeyEvent;
|
||||||
|
use ratatui::{prelude::*, widgets::*};
|
||||||
|
use tracing::debug;
|
||||||
|
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
struct LastMouseMove {
|
||||||
|
in_speed: f64,
|
||||||
|
out_sens_x: f64,
|
||||||
|
out_sens_y: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type GetNewYAxisBounds<PS> = dyn FnMut(ContextRef<PS>) -> [f64; 2];
|
||||||
|
|
||||||
|
pub struct SensitivityGraph<PS: ParamStore> {
|
||||||
|
last_mouse_move: LastMouseMove,
|
||||||
|
pub y_bounds: [f64; 2],
|
||||||
|
data: Vec<(f64, f64)>,
|
||||||
|
data_alt: Vec<(f64, f64)>,
|
||||||
|
title: &'static str,
|
||||||
|
data_name: String,
|
||||||
|
data_alt_name: String,
|
||||||
|
context: ContextRef<PS>,
|
||||||
|
on_y_axis_update_fn: Box<GetNewYAxisBounds<PS>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<PS: ParamStore> SensitivityGraph<PS> {
|
||||||
|
pub fn new(context: ContextRef<PS>) -> Self {
|
||||||
|
Self {
|
||||||
|
context: context.clone(),
|
||||||
|
last_mouse_move: Default::default(),
|
||||||
|
y_bounds: [0.0, 0.0],
|
||||||
|
data: vec![],
|
||||||
|
data_alt: vec![],
|
||||||
|
title: "Sensitivity Graph (Ratio = Speed_out / Speed_in)",
|
||||||
|
data_name: "🠠🠢 Sens".to_string(),
|
||||||
|
data_alt_name: "🠡🠣 Sens".to_string(),
|
||||||
|
on_y_axis_update_fn: Box::new(|ctx| {
|
||||||
|
[
|
||||||
|
0.0,
|
||||||
|
f64::from(get_param_value_from_ctx!(ctx, SensMult)) * 2.0,
|
||||||
|
]
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn on_y_axix_bounds_update(
|
||||||
|
mut self,
|
||||||
|
updater: impl FnMut(ContextRef<PS>) -> [f64; 2] + 'static,
|
||||||
|
) -> Self {
|
||||||
|
self.on_y_axis_update_fn = Box::new(updater);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_data(&mut self) {
|
||||||
|
self.data.clear();
|
||||||
|
self.data_alt.clear();
|
||||||
|
|
||||||
|
let params = self.context.get().params_snapshot();
|
||||||
|
for x in (0..128).map(|x| (x as f64) * 1.0 /* step size */) {
|
||||||
|
let (sens_x, sens_y) =
|
||||||
|
gsf_core::sensitivity(x, self.context.get().current_mode, ¶ms);
|
||||||
|
self.data.push((x, sens_x));
|
||||||
|
if sens_x != sens_y {
|
||||||
|
self.data_alt.push((x, sens_y));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_input_speed_and_resolved_sens(&self) -> (f64, SensXY) {
|
||||||
|
let input_speed = gsf_core::inputspeed::read_input_speed();
|
||||||
|
let params = self.context.get().params_snapshot();
|
||||||
|
debug!("last mouse move read at {} counts/ms", input_speed);
|
||||||
|
(
|
||||||
|
input_speed,
|
||||||
|
sensitivity(input_speed, self.context.get().current_mode, ¶ms),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_last_move(&mut self) {
|
||||||
|
let (in_speed, out_sens) = self.read_input_speed_and_resolved_sens();
|
||||||
|
self.last_mouse_move = LastMouseMove {
|
||||||
|
in_speed,
|
||||||
|
out_sens_x: out_sens.0,
|
||||||
|
out_sens_y: out_sens.1,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn formatted_current_point(&self, x: f64, y: f64) -> String {
|
||||||
|
format!("• ({:0<6.3}, {:0<6.3})", x, y)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<PS: ParamStore> TuiComponent for SensitivityGraph<PS> {
|
||||||
|
fn handle_key_event(&mut self, _event: &KeyEvent, _actions: &mut action::Actions) {}
|
||||||
|
|
||||||
|
fn handle_mouse_event(
|
||||||
|
&mut self,
|
||||||
|
_event: &crossterm::event::MouseEvent,
|
||||||
|
_actions: &mut action::Actions,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update(&mut self, action: &action::Action) {
|
||||||
|
if let action::Action::Tick = action {
|
||||||
|
debug!("updating graph on tick");
|
||||||
|
self.update_last_move();
|
||||||
|
self.update_data();
|
||||||
|
self.y_bounds = (self.on_y_axis_update_fn)(self.context.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw(&self, frame: &mut ratatui::Frame, area: ratatui::prelude::Rect) {
|
||||||
|
let (bounds, labels) = bounds_and_labels([0.0, 128.0], 16);
|
||||||
|
let x_axis = Axis::default()
|
||||||
|
.title("Speed_in".magenta())
|
||||||
|
.style(Style::default().white())
|
||||||
|
.bounds(bounds)
|
||||||
|
.labels(labels);
|
||||||
|
|
||||||
|
let (bounds, labels) = bounds_and_labels(self.y_bounds, 10);
|
||||||
|
let y_axis = Axis::default()
|
||||||
|
.title("Ratio".magenta())
|
||||||
|
.style(Style::default().white())
|
||||||
|
.bounds(bounds)
|
||||||
|
.labels(labels);
|
||||||
|
|
||||||
|
let clamped_x = self.last_mouse_move.in_speed.clamp(0.0, 128.0);
|
||||||
|
let highlight_point_x = &[(clamped_x, self.last_mouse_move.out_sens_x)];
|
||||||
|
let highlight_point_y = &[(clamped_x, self.last_mouse_move.out_sens_y)];
|
||||||
|
let mut chart_plots = vec![
|
||||||
|
Dataset::default()
|
||||||
|
.name(if self.data_alt.is_empty() {
|
||||||
|
"🠠🠢 🠡🠣Sens".to_owned()
|
||||||
|
} else {
|
||||||
|
self.data_name.clone()
|
||||||
|
})
|
||||||
|
.marker(symbols::Marker::Braille)
|
||||||
|
.graph_type(GraphType::Line)
|
||||||
|
.style(Style::default().green())
|
||||||
|
.data(&self.data),
|
||||||
|
// current instance of user input speed and output sensitivity
|
||||||
|
Dataset::default()
|
||||||
|
.name(self.formatted_current_point(
|
||||||
|
self.last_mouse_move.in_speed,
|
||||||
|
self.last_mouse_move.out_sens_x,
|
||||||
|
))
|
||||||
|
.marker(symbols::Marker::Braille)
|
||||||
|
.style(Style::default().red())
|
||||||
|
.data(highlight_point_x),
|
||||||
|
];
|
||||||
|
|
||||||
|
if !self.data_alt.is_empty() {
|
||||||
|
chart_plots.push(
|
||||||
|
Dataset::default()
|
||||||
|
.name(self.data_alt_name.clone())
|
||||||
|
.marker(symbols::Marker::Braille)
|
||||||
|
.graph_type(GraphType::Line)
|
||||||
|
.style(Style::default().yellow())
|
||||||
|
.data(&self.data_alt),
|
||||||
|
);
|
||||||
|
|
||||||
|
chart_plots.push(
|
||||||
|
Dataset::default()
|
||||||
|
.name(self.formatted_current_point(
|
||||||
|
self.last_mouse_move.in_speed,
|
||||||
|
self.last_mouse_move.out_sens_y,
|
||||||
|
))
|
||||||
|
.marker(symbols::Marker::Braille)
|
||||||
|
.style(Style::default().blue().bold())
|
||||||
|
.data(highlight_point_y),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let chart = Chart::new(chart_plots).x_axis(x_axis).y_axis(y_axis);
|
||||||
|
|
||||||
|
frame.render_widget(
|
||||||
|
chart.block(
|
||||||
|
Block::default()
|
||||||
|
.borders(Borders::NONE)
|
||||||
|
.title(self.title)
|
||||||
|
.bold(),
|
||||||
|
),
|
||||||
|
area,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn bounds_and_labels(bounds: [f64; 2], div: usize) -> ([f64; 2], Vec<Span<'static>>) {
|
||||||
|
let [o, f] = bounds;
|
||||||
|
let d = f - o;
|
||||||
|
let step = d / (div as f64);
|
||||||
|
|
||||||
|
let labels = (0..=div)
|
||||||
|
.map(|i| o + i as f64 * step)
|
||||||
|
.map(|label| format!("{:.2}", label).into())
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
(bounds, labels)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user