add host-side daemon and config-backed parameter store
This commit is contained in:
@@ -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")
|
||||
}
|
||||
Reference in New Issue
Block a user