add host-side daemon and config-backed parameter store
This commit is contained in:
@@ -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")
|
||||||
|
}
|
||||||
@@ -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)))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
[package]
|
||||||
|
name = "gsf-daemon"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "gsfd"
|
||||||
|
path = "src/main.rs"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
anyhow = { workspace = true }
|
||||||
|
clap = { workspace = true, features = ["derive"] }
|
||||||
|
evdev = "0.13"
|
||||||
|
gsf-core = { path = "../crates/core" }
|
||||||
@@ -0,0 +1,351 @@
|
|||||||
|
use std::{
|
||||||
|
fs,
|
||||||
|
path::PathBuf,
|
||||||
|
time::{Duration, Instant},
|
||||||
|
};
|
||||||
|
|
||||||
|
use anyhow::{Context, anyhow};
|
||||||
|
use clap::Parser;
|
||||||
|
use evdev::{
|
||||||
|
AttributeSet, AttributeSetRef, Device, EventSummary, EventType, InputEvent, RelativeAxisCode,
|
||||||
|
uinput::VirtualDevice,
|
||||||
|
};
|
||||||
|
use gsf_core::{
|
||||||
|
AccelMode, AllParamArgs, Param,
|
||||||
|
persist::{ParamStore, SysFsStore},
|
||||||
|
sensitivity,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Parser, Debug)]
|
||||||
|
#[command(author, version, about = "host-side scroll acceleration daemon")]
|
||||||
|
struct Args {
|
||||||
|
/// Path to source input device (/dev/input/eventX).
|
||||||
|
#[arg(long)]
|
||||||
|
device: Option<PathBuf>,
|
||||||
|
|
||||||
|
/// Auto-select device by case-insensitive name substring.
|
||||||
|
#[arg(long)]
|
||||||
|
match_name: Option<String>,
|
||||||
|
|
||||||
|
/// List candidate devices and exit.
|
||||||
|
#[arg(long)]
|
||||||
|
list_devices: bool,
|
||||||
|
|
||||||
|
/// Don't create virtual output device, just print transformations.
|
||||||
|
#[arg(long)]
|
||||||
|
dry_run: bool,
|
||||||
|
|
||||||
|
/// Do not EVIOCGRAB the source device.
|
||||||
|
#[arg(long)]
|
||||||
|
no_grab: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
struct ScrollState {
|
||||||
|
last_scroll_event_at: Option<Instant>,
|
||||||
|
remainder_v: f64,
|
||||||
|
remainder_h: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() -> anyhow::Result<()> {
|
||||||
|
let args = Args::parse();
|
||||||
|
|
||||||
|
if args.list_devices {
|
||||||
|
print_devices();
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let source_path = if let Some(path) = args.device {
|
||||||
|
path
|
||||||
|
} else {
|
||||||
|
find_default_device(args.match_name.as_deref())
|
||||||
|
.context("failed to auto-select a scroll-capable device")?
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut source = Device::open(&source_path)
|
||||||
|
.with_context(|| anyhow!("failed to open input device {}", source_path.display()))?;
|
||||||
|
|
||||||
|
let source_name = source.name().unwrap_or("unknown").to_string();
|
||||||
|
let has_scroll = source
|
||||||
|
.supported_relative_axes()
|
||||||
|
.map(has_scroll_axes)
|
||||||
|
.unwrap_or(false);
|
||||||
|
|
||||||
|
if !has_scroll {
|
||||||
|
anyhow::bail!(
|
||||||
|
"selected source device does not expose REL_WHEEL/REL_HWHEEL axes: {} ({})",
|
||||||
|
source_path.display(),
|
||||||
|
source_name
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if !args.no_grab {
|
||||||
|
source
|
||||||
|
.grab()
|
||||||
|
.with_context(|| anyhow!("failed to grab {}", source_path.display()))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
eprintln!(
|
||||||
|
"using source device: {} ({})",
|
||||||
|
source_path.display(),
|
||||||
|
source_name
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut virtual_dev = if args.dry_run {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(build_virtual_from_source(&source).context("failed to create uinput device")?)
|
||||||
|
};
|
||||||
|
|
||||||
|
let store = SysFsStore;
|
||||||
|
let mut state = ScrollState::default();
|
||||||
|
let mut last_cfg_reload = Instant::now();
|
||||||
|
let mut cfg = snapshot_config(&store)?;
|
||||||
|
|
||||||
|
loop {
|
||||||
|
if last_cfg_reload.elapsed() >= Duration::from_millis(100) {
|
||||||
|
cfg = snapshot_config(&store)?;
|
||||||
|
last_cfg_reload = Instant::now();
|
||||||
|
}
|
||||||
|
|
||||||
|
let events = source
|
||||||
|
.fetch_events()
|
||||||
|
.context("failed to read input events")?
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
let mut out_events = Vec::new();
|
||||||
|
|
||||||
|
let suppress_lowres_v = events
|
||||||
|
.iter()
|
||||||
|
.any(|ev| ev.code() == RelativeAxisCode::REL_WHEEL_HI_RES.0);
|
||||||
|
let suppress_lowres_h = events
|
||||||
|
.iter()
|
||||||
|
.any(|ev| ev.code() == RelativeAxisCode::REL_HWHEEL_HI_RES.0);
|
||||||
|
|
||||||
|
for ev in events {
|
||||||
|
if let Some(new_ev) = transform_event(
|
||||||
|
ev,
|
||||||
|
&cfg,
|
||||||
|
&mut state,
|
||||||
|
args.dry_run,
|
||||||
|
suppress_lowres_v,
|
||||||
|
suppress_lowres_h,
|
||||||
|
) {
|
||||||
|
out_events.push(new_ev);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(virtual_dev) = virtual_dev.as_mut()
|
||||||
|
&& !out_events.is_empty()
|
||||||
|
{
|
||||||
|
virtual_dev
|
||||||
|
.emit(&out_events)
|
||||||
|
.context("failed to emit event batch to uinput")?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn has_scroll_axes(axes: &AttributeSetRef<RelativeAxisCode>) -> bool {
|
||||||
|
axes.contains(RelativeAxisCode::REL_WHEEL)
|
||||||
|
|| axes.contains(RelativeAxisCode::REL_WHEEL_HI_RES)
|
||||||
|
|| axes.contains(RelativeAxisCode::REL_HWHEEL)
|
||||||
|
|| axes.contains(RelativeAxisCode::REL_HWHEEL_HI_RES)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn print_devices() {
|
||||||
|
for (path, dev) in evdev::enumerate() {
|
||||||
|
let has_scroll = dev
|
||||||
|
.supported_relative_axes()
|
||||||
|
.map(has_scroll_axes)
|
||||||
|
.unwrap_or(false);
|
||||||
|
|
||||||
|
eprintln!(
|
||||||
|
"{}\t{}\t{}",
|
||||||
|
path.display(),
|
||||||
|
dev.name().unwrap_or("unknown"),
|
||||||
|
if has_scroll {
|
||||||
|
"[scroll-capable]"
|
||||||
|
} else {
|
||||||
|
"[no-scroll]"
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn find_default_device(match_name: Option<&str>) -> anyhow::Result<PathBuf> {
|
||||||
|
let needle = match_name.map(|s| s.to_ascii_lowercase());
|
||||||
|
|
||||||
|
evdev::enumerate()
|
||||||
|
.find_map(|(path, dev)| {
|
||||||
|
let has_scroll = dev
|
||||||
|
.supported_relative_axes()
|
||||||
|
.map(has_scroll_axes)
|
||||||
|
.unwrap_or(false);
|
||||||
|
if !has_scroll {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(needle) = needle.as_ref() {
|
||||||
|
let name = dev.name().unwrap_or("unknown").to_ascii_lowercase();
|
||||||
|
if !name.contains(needle) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(path)
|
||||||
|
})
|
||||||
|
.ok_or_else(|| anyhow!("no matching scroll-capable device found"))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_virtual_from_source(source: &Device) -> anyhow::Result<VirtualDevice> {
|
||||||
|
let name = format!("gsf virtual: {}", source.name().unwrap_or("unknown"));
|
||||||
|
|
||||||
|
let mut rel_axes = AttributeSet::<RelativeAxisCode>::new();
|
||||||
|
if let Some(axes) = source.supported_relative_axes() {
|
||||||
|
for axis in axes.iter() {
|
||||||
|
rel_axes.insert(axis);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut builder = VirtualDevice::builder()?.name(name.as_bytes());
|
||||||
|
|
||||||
|
if let Some(keys) = source.supported_keys() {
|
||||||
|
builder = builder.with_keys(keys)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
builder = builder.with_relative_axes(&rel_axes)?;
|
||||||
|
|
||||||
|
let mut device = builder.build()?;
|
||||||
|
|
||||||
|
for path in device.enumerate_dev_nodes_blocking()? {
|
||||||
|
let path = path?;
|
||||||
|
eprintln!("virtual device node: {}", path.display());
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(device)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn snapshot_config(store: &SysFsStore) -> anyhow::Result<(AccelMode, AllParamArgs)> {
|
||||||
|
let mode = store.get_current_accel_mode()?;
|
||||||
|
|
||||||
|
let params = AllParamArgs {
|
||||||
|
sens_mult: store.get(Param::SensMult)?,
|
||||||
|
yx_ratio: store.get(Param::YxRatio)?,
|
||||||
|
input_dpi: store.get(Param::InputDpi)?,
|
||||||
|
accel: store.get(Param::Accel)?,
|
||||||
|
offset_linear: store.get(Param::OffsetLinear)?,
|
||||||
|
offset_natural: store.get(Param::OffsetNatural)?,
|
||||||
|
output_cap: store.get(Param::OutputCap)?,
|
||||||
|
decay_rate: store.get(Param::DecayRate)?,
|
||||||
|
limit: store.get(Param::Limit)?,
|
||||||
|
gamma: store.get(Param::Gamma)?,
|
||||||
|
smooth: store.get(Param::Smooth)?,
|
||||||
|
motivity: store.get(Param::Motivity)?,
|
||||||
|
sync_speed: store.get(Param::SyncSpeed)?,
|
||||||
|
angle_rotation: store.get(Param::AngleRotation)?,
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok((mode, params))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn transform_event(
|
||||||
|
ev: InputEvent,
|
||||||
|
cfg: &(AccelMode, AllParamArgs),
|
||||||
|
state: &mut ScrollState,
|
||||||
|
dry_run: bool,
|
||||||
|
suppress_lowres_v: bool,
|
||||||
|
suppress_lowres_h: bool,
|
||||||
|
) -> Option<InputEvent> {
|
||||||
|
let (mode, params) = cfg;
|
||||||
|
|
||||||
|
match ev.destructure() {
|
||||||
|
EventSummary::RelativeAxis(_, axis, value)
|
||||||
|
if axis == RelativeAxisCode::REL_WHEEL
|
||||||
|
|| axis == RelativeAxisCode::REL_WHEEL_HI_RES
|
||||||
|
|| axis == RelativeAxisCode::REL_HWHEEL
|
||||||
|
|| axis == RelativeAxisCode::REL_HWHEEL_HI_RES =>
|
||||||
|
{
|
||||||
|
if (axis == RelativeAxisCode::REL_WHEEL && suppress_lowres_v)
|
||||||
|
|| (axis == RelativeAxisCode::REL_HWHEEL && suppress_lowres_h)
|
||||||
|
{
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let now = Instant::now();
|
||||||
|
let dt = state
|
||||||
|
.last_scroll_event_at
|
||||||
|
.map(|t| now.saturating_duration_since(t).as_secs_f64())
|
||||||
|
.unwrap_or(0.016)
|
||||||
|
.max(0.001);
|
||||||
|
state.last_scroll_event_at = Some(now);
|
||||||
|
|
||||||
|
let speed = (value.abs() as f64) / dt;
|
||||||
|
let (gain_x, gain_y) = sensitivity(speed, *mode, params);
|
||||||
|
let gain = if axis == RelativeAxisCode::REL_HWHEEL
|
||||||
|
|| axis == RelativeAxisCode::REL_HWHEEL_HI_RES
|
||||||
|
{
|
||||||
|
gain_x
|
||||||
|
} else {
|
||||||
|
gain_y
|
||||||
|
};
|
||||||
|
|
||||||
|
let remainder = if axis == RelativeAxisCode::REL_HWHEEL
|
||||||
|
|| axis == RelativeAxisCode::REL_HWHEEL_HI_RES
|
||||||
|
{
|
||||||
|
&mut state.remainder_h
|
||||||
|
} else {
|
||||||
|
&mut state.remainder_v
|
||||||
|
};
|
||||||
|
|
||||||
|
let scaled = (value as f64) * gain + *remainder;
|
||||||
|
let quantized = scaled.trunc() as i32;
|
||||||
|
*remainder = scaled - (quantized as f64);
|
||||||
|
|
||||||
|
let _ = write_speed_stat(speed);
|
||||||
|
|
||||||
|
if dry_run {
|
||||||
|
eprintln!(
|
||||||
|
"scroll {:?}: in={} speed={:.2} gain={:.3} out={}",
|
||||||
|
axis, value, speed, gain, quantized
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if quantized == 0 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(InputEvent::new(EventType::RELATIVE.0, ev.code(), quantized))
|
||||||
|
}
|
||||||
|
_ => Some(ev),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_speed_stat(speed: f64) -> anyhow::Result<()> {
|
||||||
|
let path = stats_path();
|
||||||
|
if let Some(parent) = path.parent() {
|
||||||
|
fs::create_dir_all(parent)
|
||||||
|
.with_context(|| anyhow!("failed to create {}", parent.display()))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
fs::write(&path, format!("{speed:.6}"))
|
||||||
|
.with_context(|| anyhow!("failed to write {}", path.display()))?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
|
||||||
|
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")
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=gotta-scroll-fast daemon
|
||||||
|
After=graphical-session.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
ExecStart=%h/.cargo/bin/gsfd --match-name ploopy
|
||||||
|
Restart=on-failure
|
||||||
|
RestartSec=1
|
||||||
|
|
||||||
|
# Optional overrides:
|
||||||
|
# Environment=GSF_CONFIG_PATH=%h/.config/gsf/config.json
|
||||||
|
# Environment=GSF_STATS_PATH=%t/gsf-speed.txt
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=default.target
|
||||||
Reference in New Issue
Block a user