use std::sync::OnceLock;
use std::time::Duration;
const DEFAULT_DOUBLE_CLICK_MS: u32 = 400;
pub fn double_click_interval() -> Duration {
static CACHE: OnceLock<Duration> = OnceLock::new();
*CACHE.get_or_init(|| {
let ms = query_double_click_ms().unwrap_or(DEFAULT_DOUBLE_CLICK_MS);
Duration::from_millis(ms as u64)
})
}
#[cfg(target_os = "linux")]
fn query_double_click_ms() -> Option<u32> {
if let Some(ms) = linux::xsettings_double_click_ms() {
return Some(ms);
}
linux::gsettings_double_click_ms()
}
#[cfg(not(target_os = "linux"))]
fn query_double_click_ms() -> Option<u32> {
None
}
#[cfg(target_os = "linux")]
mod linux {
use std::process::Command;
use std::sync::OnceLock;
use x11rb::protocol::xproto::{AtomEnum, ConnectionExt};
use x11rb::rust_connection::RustConnection;
fn conn() -> Option<(&'static RustConnection, usize)> {
static CONN: OnceLock<Option<(RustConnection, usize)>> = OnceLock::new();
CONN.get_or_init(|| x11rb::connect(None).ok().map(|(c, s)| (c, s)))
.as_ref()
.map(|(c, s)| (c, *s))
}
pub fn xsettings_double_click_ms() -> Option<u32> {
let (conn, screen_num) = conn()?;
let selection_name = format!("_XSETTINGS_S{}", screen_num);
let sel_atom = conn
.intern_atom(false, selection_name.as_bytes())
.ok()?
.reply()
.ok()?
.atom;
let owner = conn.get_selection_owner(sel_atom).ok()?.reply().ok()?.owner;
if owner == 0 {
return None;
}
let prop_atom = conn
.intern_atom(false, b"_XSETTINGS_SETTINGS")
.ok()?
.reply()
.ok()?
.atom;
let reply = conn
.get_property(false, owner, prop_atom, AtomEnum::ANY, 0, 8192)
.ok()?
.reply()
.ok()?;
parse_xsettings_int(&reply.value, "Net/DoubleClickTime")
}
fn parse_xsettings_int(data: &[u8], name: &str) -> Option<u32> {
if data.len() < 12 {
return None;
}
let big_endian = match data[0] {
0 => true,
1 => false,
_ => return None,
};
let read_u16 = |off: usize| -> Option<u16> {
let s = data.get(off..off + 2)?;
Some(if big_endian {
u16::from_be_bytes([s[0], s[1]])
} else {
u16::from_le_bytes([s[0], s[1]])
})
};
let read_u32 = |off: usize| -> Option<u32> {
let s = data.get(off..off + 4)?;
Some(if big_endian {
u32::from_be_bytes([s[0], s[1], s[2], s[3]])
} else {
u32::from_le_bytes([s[0], s[1], s[2], s[3]])
})
};
let n_settings = read_u32(8)?;
let mut off = 12;
for _ in 0..n_settings {
let ty = *data.get(off)?;
let name_len = read_u16(off + 2)? as usize;
off += 4;
let raw_name = data.get(off..off + name_len)?;
let setting_name = core::str::from_utf8(raw_name).ok()?;
let name_pad = (4 - (name_len % 4)) % 4;
off += name_len + name_pad;
off += 4;
let value: Option<u32> = match ty {
0 => {
let v = read_u32(off)?;
off += 4;
Some(v)
}
1 => {
let slen = read_u32(off)? as usize;
off += 4 + slen;
let spad = (4 - (slen % 4)) % 4;
off += spad;
None
}
2 => {
off += 8;
None
}
_ => return None,
};
if setting_name == name {
return value;
}
}
None
}
pub fn gsettings_double_click_ms() -> Option<u32> {
let out = Command::new("gsettings")
.args(["get", "org.gnome.desktop.peripherals.mouse", "double-click"])
.output()
.ok()?;
if !out.status.success() {
return None;
}
let s = core::str::from_utf8(&out.stdout).ok()?.trim();
s.parse::<u32>().ok()
}
}