Finished the driver and installed it. Saving for archive.

This commit is contained in:
2026-07-15 11:15:33 +02:00
parent b56dd2a34e
commit e6e1e9a6d6
5 changed files with 215 additions and 32 deletions
+6
View File
@@ -24,6 +24,12 @@ Regular wheel mode:
cargo run --manifest-path renewed_driver/Cargo.toml -- --mode wheel
```
Multitouch timing can be tuned without recompiling:
```bash
cargo run --manifest-path renewed_driver/Cargo.toml -- --mode multitouch --mt-idle-ms 100 --mt-idle-max-ms 1500 --mt-settle-ms 80
```
The process still needs access to the Surface Dial event device and
`/dev/uinput`, just like the original daemon.
+28 -7
View File
@@ -19,14 +19,35 @@ surface-dial-renewed --mode wheel
Normal Dial rotation emits a synthetic two-finger touchpad scroll through
`Surface Dial Renewed Touchpad`.
The driver starts a virtual two-finger contact on the first rotation event,
moves both virtual fingers vertically, and ends the touch gesture after no
rotation has arrived for 2 seconds.
The driver starts a virtual two-finger contact on the first rotation event and
moves both virtual fingers vertically. Gesture release is dynamic:
The 2 second hold is intentional. Very slow physical Dial movement can emit
sparse `REL_DIAL` events. If the virtual fingers are lifted too quickly, libinput
can treat those sparse movements as many tiny gestures and ignore them. Holding
the contact makes slow and precise movement accumulate into one gesture.
- normal and fast rotation releases after about `100ms`
- sparse precision movement can stretch release up to `1500ms`
The `100ms` minimum matches the old driver's multitouch control. The timeout is
based on the time gap between rotation events, not the raw delta size. That is
important because the Dial can emit `delta=1` even during normal fast scrolling.
If the virtual fingers are lifted too quickly during sparse movement, libinput
can treat those sparse events as many tiny gestures and ignore them. If the hold
is too long, page-edge overscroll animations can remain stretched for too long
before bouncing back.
The driver keeps the last rotation timestamp even after a virtual touch gesture
ends. This matters because sparse precision events can arrive after the 100ms
gesture has already been released. The next event still sees the previous event
gap and can stretch the new gesture timeout. A gap of `2500ms` or more is
treated as a real pause and resets back to the `100ms` minimum.
Timing is configurable:
```bash
surface-dial-renewed --mt-idle-ms 100 --mt-idle-max-ms 1500 --mt-settle-ms 80
```
`--mt-idle-ms` controls the fast-scroll minimum release timeout.
`--mt-idle-max-ms` controls the slow/precision maximum release timeout.
`--mt-settle-ms` controls the short stationary frame before the fingers lift.
## Sensitivity Selector
+19
View File
@@ -52,6 +52,25 @@ The service runs:
It restarts on failure with a 2 second delay.
To tune multitouch timing in the service, edit:
```text
~/.config/systemd/user/surface-dial-renewed.service
```
Example:
```text
ExecStart=%h/.cargo/bin/surface-dial-renewed --mode multitouch --mt-idle-ms 100 --mt-idle-max-ms 1500 --mt-settle-ms 80
```
Then reload and restart:
```bash
systemctl --user daemon-reload
systemctl --user restart surface-dial-renewed.service
```
Useful commands:
```bash
+21
View File
@@ -56,6 +56,27 @@ systemctl --user status surface-dial-renewed.service
journalctl --user -u surface-dial-renewed.service -f
```
## Tune Multitouch Timing
Edit:
```bash
~/.config/systemd/user/surface-dial-renewed.service
```
Example:
```text
ExecStart=%h/.cargo/bin/surface-dial-renewed --mode multitouch --mt-idle-ms 100 --mt-idle-max-ms 1500 --mt-settle-ms 80
```
Apply changes:
```bash
systemctl --user daemon-reload
systemctl --user restart surface-dial-renewed.service
```
## Uninstall
Automatic uninstall from the repository root:
+141 -25
View File
@@ -5,7 +5,7 @@ mod notify;
mod sensitivity;
mod virtual_input;
use std::time::Duration;
use std::time::{Duration, Instant};
use dial::{DialEvent, DialEventKind, DialReader};
use error::Result;
@@ -14,8 +14,12 @@ use notify::SensitivityNotifier;
use sensitivity::Sensitivity;
use virtual_input::{VirtualInput, WheelDirection};
const MULTITOUCH_IDLE_TIMEOUT: Duration = Duration::from_millis(2_000);
const MULTITOUCH_SETTLE_TIMEOUT: Duration = Duration::from_millis(80);
const DEFAULT_MULTITOUCH_IDLE_TIMEOUT: Duration = Duration::from_millis(100);
const DEFAULT_MULTITOUCH_MAX_IDLE_TIMEOUT: Duration = Duration::from_millis(1_500);
const DEFAULT_MULTITOUCH_SETTLE_TIMEOUT: Duration = Duration::from_millis(80);
const DYNAMIC_IDLE_FAST_EVENT_MS: u128 = 80;
const DYNAMIC_IDLE_SLOW_EVENT_MS: u128 = 450;
const DYNAMIC_IDLE_RESET_MS: u128 = 2_500;
const SENSITIVITY_STEP_UNITS: i32 = 48;
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
@@ -24,6 +28,25 @@ enum OutputMode {
Wheel,
}
#[derive(Debug, Clone, Copy)]
struct DriverConfig {
mode: OutputMode,
multitouch_idle_timeout: Duration,
multitouch_max_idle_timeout: Duration,
multitouch_settle_timeout: Duration,
}
impl Default for DriverConfig {
fn default() -> Self {
Self {
mode: OutputMode::Multitouch,
multitouch_idle_timeout: DEFAULT_MULTITOUCH_IDLE_TIMEOUT,
multitouch_max_idle_timeout: DEFAULT_MULTITOUCH_MAX_IDLE_TIMEOUT,
multitouch_settle_timeout: DEFAULT_MULTITOUCH_SETTLE_TIMEOUT,
}
}
}
#[derive(Debug)]
struct DriverState {
mode: OutputMode,
@@ -36,6 +59,8 @@ struct DriverState {
mt_active: bool,
mt_position: i32,
mt_remainder_milli: i32,
mt_current_idle_timeout: Duration,
mt_last_rotation_at: Option<Instant>,
}
impl DriverState {
@@ -49,6 +74,8 @@ impl DriverState {
mt_active: false,
mt_position: 0,
mt_remainder_milli: 0,
mt_current_idle_timeout: DEFAULT_MULTITOUCH_IDLE_TIMEOUT,
mt_last_rotation_at: None,
}
}
}
@@ -61,8 +88,8 @@ fn main() {
}
fn run() -> Result<()> {
let mode = parse_mode(std::env::args().skip(1));
let mut state = DriverState::new(mode);
let config = parse_config(std::env::args().skip(1));
let mut state = DriverState::new(config.mode);
// Input reading runs on a worker thread; output devices stay owned by the
// main thread to avoid sharing uinput handles across threads.
let mut dial = DialReader::spawn(Duration::from_millis(750))?;
@@ -70,25 +97,38 @@ fn run() -> Result<()> {
let mut notifier = SensitivityNotifier::new();
let mut haptics = Haptics::new();
eprintln!("surface-dial-renewed: running in {mode:?} mode");
eprintln!(
"surface-dial-renewed: running in {:?} mode, mt_idle={}ms, mt_idle_max={}ms, mt_settle={}ms",
config.mode,
config.multitouch_idle_timeout.as_millis(),
config.multitouch_max_idle_timeout.as_millis(),
config.multitouch_settle_timeout.as_millis()
);
loop {
// While a synthetic two-finger gesture is active, recv with a timeout.
// Timeout means "the user stopped rotating", so the virtual fingers can
// be lifted. The 2s delay is deliberate: very slow Dial movement emits
// sparse REL_DIAL events, and libinput needs one continuous gesture to
// recognize those as precise scroll movement.
// be lifted. The timeout is dynamic: normal/fast scrolling uses the old
// driver's 100ms release behavior, while sparse precision movement can
// stretch toward the configured maximum. The last rotation timestamp is
// kept across gesture endings so slow events are not all treated as
// unrelated first events.
let timeout = if state.mt_active {
Some(MULTITOUCH_IDLE_TIMEOUT)
Some(state.mt_current_idle_timeout)
} else {
None
};
match dial.next_event(timeout)? {
Some(event) => {
handle_event(event, &mut state, &mut output, &mut notifier, &mut haptics)?
}
None => end_multitouch_if_active(&mut state, &mut output)?,
Some(event) => handle_event(
event,
&mut state,
&mut output,
&mut notifier,
&mut haptics,
&config,
)?,
None => end_multitouch_if_active(&mut state, &mut output, &config)?,
}
}
}
@@ -99,6 +139,7 @@ fn handle_event(
output: &mut VirtualInput,
notifier: &mut SensitivityNotifier,
haptics: &mut Haptics,
config: &DriverConfig,
) -> Result<()> {
match event.kind {
DialEventKind::Connected => {
@@ -108,7 +149,7 @@ fn handle_event(
haptics.reconnect();
}
DialEventKind::Disconnected => {
end_multitouch_if_active(state, output)?;
end_multitouch_if_active(state, output, config)?;
haptics.disconnect();
eprintln!("surface-dial-renewed: dial disconnected");
}
@@ -133,13 +174,13 @@ fn handle_event(
// Never scroll while the sensitivity selector is active. End a
// pending touchpad gesture before showing the selector so the
// desktop does not see overlapping scroll and adjustment input.
end_multitouch_if_active(state, output)?;
end_multitouch_if_active(state, output, config)?;
adjust_sensitivity(delta, state, notifier, haptics)?;
return Ok(());
}
match state.mode {
OutputMode::Multitouch => multitouch_scroll(delta, state, output)?,
OutputMode::Multitouch => multitouch_scroll(delta, state, output, config)?,
OutputMode::Wheel => wheel_scroll(delta, state, output)?,
}
}
@@ -178,7 +219,14 @@ fn adjust_sensitivity(
Ok(())
}
fn multitouch_scroll(delta: i32, state: &mut DriverState, output: &mut VirtualInput) -> Result<()> {
fn multitouch_scroll(
delta: i32,
state: &mut DriverState,
output: &mut VirtualInput,
config: &DriverConfig,
) -> Result<()> {
update_multitouch_idle_timeout(delta, state, config);
if !state.mt_active {
state.mt_position = 0;
output.multitouch_start()?;
@@ -216,6 +264,38 @@ fn multitouch_scroll(delta: i32, state: &mut DriverState, output: &mut VirtualIn
Ok(())
}
fn update_multitouch_idle_timeout(_delta: i32, state: &mut DriverState, config: &DriverConfig) {
let now = Instant::now();
let event_gap = state
.mt_last_rotation_at
.map(|last| now.saturating_duration_since(last));
state.mt_last_rotation_at = Some(now);
let Some(event_gap) = event_gap else {
state.mt_current_idle_timeout = config.multitouch_idle_timeout;
return;
};
let gap_ms = event_gap.as_millis();
state.mt_current_idle_timeout = if gap_ms >= DYNAMIC_IDLE_RESET_MS {
// A genuine pause should not make the first new scroll linger.
config.multitouch_idle_timeout
} else if gap_ms >= DYNAMIC_IDLE_SLOW_EVENT_MS {
config.multitouch_max_idle_timeout
} else if gap_ms <= DYNAMIC_IDLE_FAST_EVENT_MS {
config.multitouch_idle_timeout
} else {
let fast = DYNAMIC_IDLE_FAST_EVENT_MS;
let slow = DYNAMIC_IDLE_SLOW_EVENT_MS;
let span = slow.saturating_sub(fast).max(1);
let progress = gap_ms.saturating_sub(fast).min(span);
let min_ms = config.multitouch_idle_timeout.as_millis();
let max_ms = config.multitouch_max_idle_timeout.as_millis().max(min_ms);
let dynamic_ms = min_ms + ((max_ms - min_ms) * progress) / span;
Duration::from_millis(dynamic_ms as u64)
};
}
fn wheel_scroll(delta: i32, state: &DriverState, output: &mut VirtualInput) -> Result<()> {
let ticks = delta.abs().saturating_mul(state.sensitivity.wheel_ticks());
let direction = if delta > 0 {
@@ -231,36 +311,72 @@ fn wheel_scroll(delta: i32, state: &DriverState, output: &mut VirtualInput) -> R
Ok(())
}
fn end_multitouch_if_active(state: &mut DriverState, output: &mut VirtualInput) -> Result<()> {
fn end_multitouch_if_active(
state: &mut DriverState,
output: &mut VirtualInput,
config: &DriverConfig,
) -> Result<()> {
if state.mt_active {
// Emit one stationary frame and wait briefly before lifting fingers.
// This made libinput less likely to interpret the final delta as a
// touchpad flick/coast.
output.multitouch_settle(state.mt_position)?;
std::thread::sleep(MULTITOUCH_SETTLE_TIMEOUT);
std::thread::sleep(config.multitouch_settle_timeout);
output.multitouch_end()?;
state.mt_active = false;
state.mt_position = 0;
state.mt_remainder_milli = 0;
state.mt_current_idle_timeout = config.multitouch_idle_timeout;
}
Ok(())
}
fn parse_mode(args: impl Iterator<Item = String>) -> OutputMode {
let mut mode = OutputMode::Multitouch;
fn parse_config(args: impl Iterator<Item = String>) -> DriverConfig {
let mut config = DriverConfig::default();
let mut previous = String::new();
for arg in args {
if previous == "--mode" {
mode = parse_mode_value(&arg);
config.mode = parse_mode_value(&arg);
} else if previous == "--mt-idle-ms" {
config.multitouch_idle_timeout = Duration::from_millis(parse_ms(&arg, "mt-idle-ms"));
} else if previous == "--mt-idle-max-ms" {
config.multitouch_max_idle_timeout =
Duration::from_millis(parse_ms(&arg, "mt-idle-max-ms"));
} else if previous == "--mt-settle-ms" {
config.multitouch_settle_timeout =
Duration::from_millis(parse_ms(&arg, "mt-settle-ms"));
} else if let Some(value) = arg.strip_prefix("--mode=") {
mode = parse_mode_value(value);
config.mode = parse_mode_value(value);
} else if let Some(value) = arg.strip_prefix("--mt-idle-ms=") {
config.multitouch_idle_timeout = Duration::from_millis(parse_ms(value, "mt-idle-ms"));
} else if let Some(value) = arg.strip_prefix("--mt-idle-max-ms=") {
config.multitouch_max_idle_timeout =
Duration::from_millis(parse_ms(value, "mt-idle-max-ms"));
} else if let Some(value) = arg.strip_prefix("--mt-settle-ms=") {
config.multitouch_settle_timeout =
Duration::from_millis(parse_ms(value, "mt-settle-ms"));
}
previous = arg;
}
mode
config
}
fn parse_ms(value: &str, name: &str) -> u64 {
match value.parse::<u64>() {
Ok(ms) => ms,
Err(_) => {
eprintln!("surface-dial-renewed: invalid --{name} '{value}', using default");
match name {
"mt-idle-ms" => DEFAULT_MULTITOUCH_IDLE_TIMEOUT.as_millis() as u64,
"mt-idle-max-ms" => DEFAULT_MULTITOUCH_MAX_IDLE_TIMEOUT.as_millis() as u64,
"mt-settle-ms" => DEFAULT_MULTITOUCH_SETTLE_TIMEOUT.as_millis() as u64,
_ => 0,
}
}
}
}
fn parse_mode_value(value: &str) -> OutputMode {