add host-side daemon and config-backed parameter store

This commit is contained in:
2026-03-24 12:10:31 +00:00
parent 8894880c1d
commit 6ef326188a
5 changed files with 685 additions and 0 deletions
+54
View File
@@ -0,0 +1,54 @@
//! Provides functions to get the latest observed scroll speed.
//! For host-side mode, the daemon writes a single numeric value to a stats file.
use std::{
path::PathBuf,
sync::atomic::{AtomicU64, Ordering},
thread::{self, JoinHandle},
time::Duration,
};
use anyhow::Context;
static INPUT_SPEED_BITS: AtomicU64 = AtomicU64::new(0);
pub fn read_input_speed() -> f64 {
f64::from_bits(INPUT_SPEED_BITS.load(Ordering::Relaxed))
}
pub fn setup_input_speed_reader() -> JoinHandle<anyhow::Result<()>> {
thread::spawn(|| {
let path = stats_path();
loop {
if let Ok(content) = std::fs::read_to_string(&path)
&& let Ok(parsed) = content.trim().parse::<f64>()
{
INPUT_SPEED_BITS.store(parsed.to_bits(), Ordering::Relaxed);
}
thread::sleep(Duration::from_millis(75));
}
})
}
fn stats_path() -> PathBuf {
if let Ok(path) = std::env::var("GSF_STATS_PATH") {
return PathBuf::from(path);
}
if let Ok(runtime_dir) = std::env::var("XDG_RUNTIME_DIR") {
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")
}
+250
View File
@@ -0,0 +1,250 @@
use std::{
fmt::{Debug, Display},
path::PathBuf,
};
use anyhow::{Context, anyhow};
use serde::{Deserialize, Serialize};
use crate::{
fixedptc::Fpt,
params::{
ALL_MODES, AccelMode, CommonParamArgs, LinearParamArgs, NaturalParamArgs, Param,
SynchronousParamArgs, format_param_value, validate_param_value,
},
};
pub trait ParamStore: Debug {
fn set(&mut self, param: Param, value: f64) -> anyhow::Result<()>;
fn get(&self, param: Param) -> anyhow::Result<Fpt>;
fn set_current_accel_mode(&mut self, mode: AccelMode) -> anyhow::Result<()>;
fn get_current_accel_mode(&self) -> anyhow::Result<AccelMode>;
}
const CONFIG_ENV_VAR: &str = "GSF_CONFIG_PATH";
#[derive(Debug, Clone, Serialize, Deserialize)]
struct StoredConfig {
mode: u8,
sens_mult: f64,
yx_ratio: f64,
input_dpi: f64,
angle_rotation: f64,
accel: f64,
offset_linear: f64,
output_cap: f64,
decay_rate: f64,
offset_natural: f64,
limit: f64,
gamma: f64,
smooth: f64,
motivity: f64,
sync_speed: f64,
}
impl Default for StoredConfig {
fn default() -> Self {
Self {
mode: AccelMode::Linear as u8,
sens_mult: 1.0,
yx_ratio: 1.0,
input_dpi: 1000.0,
angle_rotation: 0.0,
accel: 0.0,
offset_linear: 0.0,
output_cap: 0.0,
decay_rate: 0.1,
offset_natural: 0.0,
limit: 1.5,
gamma: 1.0,
smooth: 0.5,
motivity: 1.5,
sync_speed: 5.0,
}
}
}
#[derive(Debug)]
pub struct SysFsStore;
impl ParamStore for SysFsStore {
fn set(&mut self, param: Param, value: f64) -> anyhow::Result<()> {
validate_param_value(param, value)?;
let mut cfg = load_or_init_config()?;
set_param_on_config(&mut cfg, param, value);
save_config(&cfg)?;
Ok(())
}
fn get(&self, param: Param) -> anyhow::Result<Fpt> {
let cfg = load_or_init_config()?;
Ok(get_param_from_config(&cfg, param).into())
}
fn set_current_accel_mode(&mut self, mode: AccelMode) -> anyhow::Result<()> {
let mut cfg = load_or_init_config()?;
cfg.mode = mode as u8;
save_config(&cfg)?;
Ok(())
}
fn get_current_accel_mode(&self) -> anyhow::Result<AccelMode> {
let cfg = load_or_init_config()?;
let idx = (cfg.mode as usize) % ALL_MODES.len();
Ok(ALL_MODES[idx])
}
}
impl SysFsStore {
pub fn set_all_common(&mut self, args: CommonParamArgs) -> anyhow::Result<()> {
let CommonParamArgs {
sens_mult,
yx_ratio,
input_dpi,
angle_rotation,
} = args;
self.set(Param::SensMult, sens_mult)?;
self.set(Param::YxRatio, yx_ratio)?;
self.set(Param::InputDpi, input_dpi)?;
self.set(Param::AngleRotation, angle_rotation)?;
Ok(())
}
pub fn set_all_linear(&mut self, args: LinearParamArgs) -> anyhow::Result<()> {
let LinearParamArgs {
accel,
offset_linear,
output_cap,
} = args;
self.set(Param::Accel, accel)?;
self.set(Param::OffsetLinear, offset_linear)?;
self.set(Param::OutputCap, output_cap)?;
Ok(())
}
pub fn set_all_natural(&mut self, args: NaturalParamArgs) -> anyhow::Result<()> {
let NaturalParamArgs {
decay_rate,
limit,
offset_natural,
} = args;
self.set(Param::DecayRate, decay_rate)?;
self.set(Param::OffsetNatural, offset_natural)?;
self.set(Param::Limit, limit)?;
Ok(())
}
pub fn set_all_synchronous(&mut self, args: SynchronousParamArgs) -> anyhow::Result<()> {
let SynchronousParamArgs {
gamma,
smooth,
motivity,
sync_speed,
} = args;
self.set(Param::Gamma, gamma)?;
self.set(Param::Smooth, smooth)?;
self.set(Param::Motivity, motivity)?;
self.set(Param::SyncSpeed, sync_speed)?;
Ok(())
}
}
fn config_path() -> anyhow::Result<PathBuf> {
if let Ok(p) = std::env::var(CONFIG_ENV_VAR) {
return Ok(PathBuf::from(p));
}
let home = std::env::var("HOME").context("HOME is not set")?;
Ok(PathBuf::from(home)
.join(".config")
.join("gsf")
.join("config.json"))
}
fn load_or_init_config() -> anyhow::Result<StoredConfig> {
let path = config_path()?;
if !path.exists() {
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)
.with_context(|| anyhow!("failed to create config directory {}", parent.display()))?;
}
let cfg = StoredConfig::default();
save_config(&cfg)?;
return Ok(cfg);
}
let content = std::fs::read_to_string(&path)
.with_context(|| anyhow!("failed to read config file {}", path.display()))?;
let cfg: StoredConfig =
serde_json::from_str(&content).context("failed to parse gsf config JSON")?;
Ok(cfg)
}
fn save_config(cfg: &StoredConfig) -> anyhow::Result<()> {
let path = config_path()?;
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)
.with_context(|| anyhow!("failed to create config directory {}", parent.display()))?;
}
let content = serde_json::to_string_pretty(cfg).context("failed to serialize config")?;
std::fs::write(&path, content)
.with_context(|| anyhow!("failed to write config {}", path.display()))?;
Ok(())
}
fn get_param_from_config(cfg: &StoredConfig, param: Param) -> f64 {
match param {
Param::SensMult => cfg.sens_mult,
Param::YxRatio => cfg.yx_ratio,
Param::InputDpi => cfg.input_dpi,
Param::AngleRotation => cfg.angle_rotation,
Param::Accel => cfg.accel,
Param::OffsetLinear => cfg.offset_linear,
Param::OutputCap => cfg.output_cap,
Param::DecayRate => cfg.decay_rate,
Param::OffsetNatural => cfg.offset_natural,
Param::Limit => cfg.limit,
Param::Gamma => cfg.gamma,
Param::Smooth => cfg.smooth,
Param::Motivity => cfg.motivity,
Param::SyncSpeed => cfg.sync_speed,
}
}
fn set_param_on_config(cfg: &mut StoredConfig, param: Param, value: f64) {
match param {
Param::SensMult => cfg.sens_mult = value,
Param::YxRatio => cfg.yx_ratio = value,
Param::InputDpi => cfg.input_dpi = value,
Param::AngleRotation => cfg.angle_rotation = value,
Param::Accel => cfg.accel = value,
Param::OffsetLinear => cfg.offset_linear = value,
Param::OutputCap => cfg.output_cap = value,
Param::DecayRate => cfg.decay_rate = value,
Param::OffsetNatural => cfg.offset_natural = value,
Param::Limit => cfg.limit = value,
Param::Gamma => cfg.gamma = value,
Param::Smooth => cfg.smooth = value,
Param::Motivity => cfg.motivity = value,
Param::SyncSpeed => cfg.sync_speed = value,
}
}
impl Display for Fpt {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&format_param_value(f64::from(*self)))
}
}