mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-17 21:15:32 +09:00
feat(desktop): portal-based Linux global shortcuts and PTT (#3185)
This commit is contained in:
1 parent
5799ef705d
commit
8f58fcc4c4
149 files changed
+26849
-8769
No files matched your search
@@ -185,14 +185,6 @@ pub fn keycode_to_name(code: u16) -> Option<&'static str> {
|
||||
.map(|(_, name)| *name)
|
||||
}
|
||||
|
||||
pub fn name_to_keycode(name: &str) -> u16 {
|
||||
KEY_MAP
|
||||
.iter()
|
||||
.find(|(_, entry_name)| *entry_name == name)
|
||||
.map(|(code, _)| *code)
|
||||
.unwrap_or(0)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -211,15 +203,6 @@ mod tests {
|
||||
assert_eq!(keycode_to_name(0xffff), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn name_to_keycode_round_trips_every_entry() {
|
||||
for (code, name) in KEY_MAP {
|
||||
assert_eq!(name_to_keycode(name), *code);
|
||||
}
|
||||
assert_eq!(name_to_keycode("NoSuchKey"), 0);
|
||||
assert_eq!(name_to_keycode(""), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn evdev_button_matches_dom_convention() {
|
||||
assert_eq!(evdev_button_to_browser_button(BTN_LEFT), Some(0));
|
||||
|
||||
@@ -8,10 +8,13 @@ mod keymap;
|
||||
mod linux;
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
pub use linux::{EvdevHook, name_to_evdev_keycode};
|
||||
mod logind;
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
pub use linux::{EvdevHook, is_keyboard_readable};
|
||||
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
#[napi_derive::napi(js_name = "nameToEvdevKeycode")]
|
||||
pub fn name_to_evdev_keycode(name: String) -> u32 {
|
||||
u32::from(crate::keymap::name_to_keycode(&name))
|
||||
#[napi_derive::napi(js_name = "isKeyboardReadable")]
|
||||
pub fn is_keyboard_readable() -> bool {
|
||||
false
|
||||
}
|
||||
@@ -2,16 +2,22 @@
|
||||
|
||||
use std::{
|
||||
collections::{HashMap, HashSet},
|
||||
os::fd::{AsFd, AsRawFd, BorrowedFd, RawFd},
|
||||
ffi::OsStr,
|
||||
ops::Range,
|
||||
os::{
|
||||
fd::{AsFd, AsRawFd, BorrowedFd, OwnedFd, RawFd},
|
||||
unix::fs::OpenOptionsExt,
|
||||
},
|
||||
path::{Path, PathBuf},
|
||||
sync::{
|
||||
Arc, Mutex,
|
||||
atomic::{AtomicBool, Ordering},
|
||||
},
|
||||
thread::{self, JoinHandle},
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use evdev::{Device, EventType, KeyCode};
|
||||
use evdev::{EventType, KeyCode, SynchronizationCode, raw_stream::RawDevice};
|
||||
use napi::{
|
||||
Env, Status,
|
||||
bindgen_prelude::{Function, Object, Result, ToNapiValue},
|
||||
@@ -19,16 +25,28 @@ use napi::{
|
||||
threadsafe_function::{ThreadsafeFunction, ThreadsafeFunctionCallMode, UnknownReturnValue},
|
||||
};
|
||||
use napi_derive::napi;
|
||||
use nix::sys::eventfd::{EfdFlags, EventFd};
|
||||
use nix::{
|
||||
fcntl::OFlag,
|
||||
sys::{
|
||||
eventfd::{EfdFlags, EventFd},
|
||||
inotify::{AddWatchFlags, InitFlags, Inotify},
|
||||
},
|
||||
};
|
||||
use polling::{Event as PollEvent, Events, Poller};
|
||||
|
||||
use crate::keymap;
|
||||
use crate::logind::{SessionGate, SessionWatch};
|
||||
|
||||
const EXIT_KEY: usize = 0;
|
||||
const MONITOR_KEY: usize = 1;
|
||||
const DEVICE_KEY_BASE: usize = 2;
|
||||
const INOTIFY_KEY: usize = 2;
|
||||
const DEVICE_KEY_BASE: usize = 3;
|
||||
const DEV_INPUT_DIR: &str = "/dev/input";
|
||||
const DEV_INPUT_PREFIX: &str = "/dev/input/event";
|
||||
const EVENT_QUEUE_LIMIT: usize = 1024;
|
||||
const EVENT_NODE_PREFIX: &str = "event";
|
||||
const FLATPAK_INFO_PATH: &str = "/.flatpak-info";
|
||||
const PERMISSION_RETRY_DELAY: Duration = Duration::from_millis(500);
|
||||
const FULL_KEYBOARD_KEY_CODES: std::ops::Range<u16> = 1..32;
|
||||
|
||||
fn poll_key_for_device_fd(fd: RawFd) -> Option<usize> {
|
||||
usize::try_from(fd).ok()?.checked_add(DEVICE_KEY_BASE)
|
||||
@@ -126,17 +144,8 @@ impl ToNapiValue for NativeEvent {
|
||||
}
|
||||
}
|
||||
|
||||
type EventTsfn = Arc<
|
||||
ThreadsafeFunction<
|
||||
NativeEvent,
|
||||
UnknownReturnValue,
|
||||
NativeEvent,
|
||||
Status,
|
||||
false,
|
||||
true,
|
||||
EVENT_QUEUE_LIMIT,
|
||||
>,
|
||||
>;
|
||||
type EventTsfn =
|
||||
Arc<ThreadsafeFunction<NativeEvent, UnknownReturnValue, NativeEvent, Status, false, true>>;
|
||||
|
||||
struct ExitFd {
|
||||
fd: EventFd,
|
||||
@@ -191,18 +200,79 @@ fn lookup_input_seat(sysname: &str) -> Option<String> {
|
||||
read_seat_from_device(&device)
|
||||
}
|
||||
|
||||
struct SeatFilter {
|
||||
udev_handle_available: bool,
|
||||
seat: String,
|
||||
}
|
||||
|
||||
impl SeatFilter {
|
||||
fn detect() -> Self {
|
||||
Self {
|
||||
udev_handle_available: udev::Enumerator::new().is_ok(),
|
||||
seat: resolve_seat(),
|
||||
}
|
||||
}
|
||||
|
||||
fn includes(&self, sysname: &str) -> bool {
|
||||
if !self.udev_handle_available || self.seat.is_empty() {
|
||||
return true;
|
||||
}
|
||||
match lookup_input_seat(sysname) {
|
||||
Some(found) => found == self.seat,
|
||||
None => true,
|
||||
}
|
||||
}
|
||||
|
||||
fn event_node_paths(&self) -> Vec<PathBuf> {
|
||||
let Ok(entries) = std::fs::read_dir(DEV_INPUT_DIR) else {
|
||||
return Vec::new();
|
||||
};
|
||||
entries
|
||||
.flatten()
|
||||
.filter(|entry| {
|
||||
entry
|
||||
.file_name()
|
||||
.to_str()
|
||||
.is_some_and(|name| is_event_node_name(name) && self.includes(name))
|
||||
})
|
||||
.map(|entry| entry.path())
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
fn is_event_node_name(name: &str) -> bool {
|
||||
name.starts_with(EVENT_NODE_PREFIX)
|
||||
}
|
||||
|
||||
fn is_sandboxed() -> bool {
|
||||
Path::new(FLATPAK_INFO_PATH).exists()
|
||||
}
|
||||
|
||||
struct OpenedDevice {
|
||||
device: Device,
|
||||
device: RawDevice,
|
||||
path: PathBuf,
|
||||
held: HashSet<u16>,
|
||||
dropped: bool,
|
||||
}
|
||||
|
||||
enum DeviceInput {
|
||||
Key(u16, i32),
|
||||
Resync,
|
||||
}
|
||||
|
||||
struct PermissionRetry {
|
||||
path: PathBuf,
|
||||
due: Instant,
|
||||
}
|
||||
|
||||
struct Reader {
|
||||
poller: Arc<Poller>,
|
||||
exit_fd: Arc<ExitFd>,
|
||||
udev_handle_available: bool,
|
||||
seat: String,
|
||||
gate: Arc<SessionGate>,
|
||||
seats: SeatFilter,
|
||||
devices: HashMap<RawFd, OpenedDevice>,
|
||||
held_keys: HashSet<u16>,
|
||||
retries: Vec<PermissionRetry>,
|
||||
delivering: bool,
|
||||
callback: EventTsfn,
|
||||
stop: Arc<AtomicBool>,
|
||||
}
|
||||
@@ -217,22 +287,31 @@ impl Reader {
|
||||
unsafe {
|
||||
poller.add(&exit_fd.as_borrowed(), PollEvent::readable(EXIT_KEY))?;
|
||||
}
|
||||
let seat = resolve_seat();
|
||||
let udev_handle_available = udev::Enumerator::new().is_ok();
|
||||
let gate = Arc::new(SessionGate::new(poller.clone()));
|
||||
|
||||
Ok(Self {
|
||||
poller,
|
||||
exit_fd,
|
||||
udev_handle_available,
|
||||
seat,
|
||||
gate,
|
||||
seats: SeatFilter::detect(),
|
||||
devices: HashMap::new(),
|
||||
held_keys: HashSet::new(),
|
||||
retries: Vec::new(),
|
||||
delivering: false,
|
||||
callback,
|
||||
stop,
|
||||
})
|
||||
}
|
||||
|
||||
fn has_full_keyboard(&self) -> bool {
|
||||
self.devices
|
||||
.values()
|
||||
.any(|opened| device_is_full_keyboard(&opened.device))
|
||||
}
|
||||
|
||||
fn try_attach_monitor(&self) -> Option<udev::MonitorSocket> {
|
||||
if is_sandboxed() {
|
||||
return None;
|
||||
}
|
||||
let socket = udev::MonitorBuilder::new()
|
||||
.and_then(|b| b.match_subsystem("input"))
|
||||
.and_then(|b| b.listen())
|
||||
@@ -246,38 +325,62 @@ impl Reader {
|
||||
}
|
||||
}
|
||||
|
||||
fn device_is_on_our_seat(&self, sysname: &str) -> bool {
|
||||
if !self.udev_handle_available || self.seat.is_empty() {
|
||||
return true;
|
||||
fn try_attach_inotify(&self) -> Option<Inotify> {
|
||||
let inotify = Inotify::init(InitFlags::IN_CLOEXEC | InitFlags::IN_NONBLOCK).ok()?;
|
||||
inotify
|
||||
.add_watch(
|
||||
DEV_INPUT_DIR,
|
||||
AddWatchFlags::IN_CREATE | AddWatchFlags::IN_ATTRIB,
|
||||
)
|
||||
.ok()?;
|
||||
if unsafe {
|
||||
self.poller
|
||||
.add(&inotify.as_fd(), PollEvent::readable(INOTIFY_KEY))
|
||||
}
|
||||
match lookup_input_seat(sysname) {
|
||||
Some(found) => found == self.seat,
|
||||
None => true,
|
||||
.is_ok()
|
||||
{
|
||||
Some(inotify)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn open_all_devices(&mut self) {
|
||||
let entries = match std::fs::read_dir("/dev/input") {
|
||||
Ok(entries) => entries,
|
||||
Err(_) => return,
|
||||
};
|
||||
for entry in entries.flatten() {
|
||||
let name = entry.file_name();
|
||||
let name_str = match name.to_str() {
|
||||
Some(s) => s,
|
||||
None => continue,
|
||||
};
|
||||
if !name_str.starts_with("event") {
|
||||
continue;
|
||||
}
|
||||
if !self.device_is_on_our_seat(name_str) {
|
||||
continue;
|
||||
}
|
||||
let path = entry.path();
|
||||
for path in self.seats.event_node_paths() {
|
||||
let _ = self.open_device(&path);
|
||||
}
|
||||
}
|
||||
|
||||
fn open_hotplugged_device(&mut self, path: &Path) {
|
||||
if let Err(err) = self.open_device(path)
|
||||
&& err.kind() == std::io::ErrorKind::PermissionDenied
|
||||
&& !self.retries.iter().any(|retry| retry.path == path)
|
||||
{
|
||||
self.retries.push(PermissionRetry {
|
||||
path: path.to_path_buf(),
|
||||
due: Instant::now() + PERMISSION_RETRY_DELAY,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn next_retry_timeout(&self) -> Option<Duration> {
|
||||
let now = Instant::now();
|
||||
self.retries
|
||||
.iter()
|
||||
.map(|retry| retry.due.saturating_duration_since(now))
|
||||
.min()
|
||||
}
|
||||
|
||||
fn run_due_retries(&mut self) {
|
||||
let now = Instant::now();
|
||||
let (due, pending): (Vec<PermissionRetry>, Vec<PermissionRetry>) =
|
||||
self.retries.drain(..).partition(|retry| retry.due <= now);
|
||||
self.retries = pending;
|
||||
for retry in due {
|
||||
let _ = self.open_device(&retry.path);
|
||||
}
|
||||
}
|
||||
|
||||
fn open_device(&mut self, path: &Path) -> std::io::Result<()> {
|
||||
if self
|
||||
.devices
|
||||
@@ -286,11 +389,10 @@ impl Reader {
|
||||
{
|
||||
return Ok(());
|
||||
}
|
||||
let device = Device::open(path)?;
|
||||
let device = open_read_only(path)?;
|
||||
if !device_has_routable_input(&device) {
|
||||
return Ok(());
|
||||
}
|
||||
let _ = device.set_nonblocking(true);
|
||||
let fd = device.as_raw_fd();
|
||||
let poll_key = poll_key_for_device_fd(fd).ok_or_else(|| {
|
||||
std::io::Error::new(
|
||||
@@ -307,8 +409,11 @@ impl Reader {
|
||||
OpenedDevice {
|
||||
device,
|
||||
path: path.to_path_buf(),
|
||||
held: HashSet::new(),
|
||||
dropped: false,
|
||||
},
|
||||
);
|
||||
self.retries.retain(|retry| retry.path != path);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -327,28 +432,65 @@ impl Reader {
|
||||
if let Some(opened) = self.devices.remove(&fd) {
|
||||
let borrowed = unsafe { BorrowedFd::borrow_raw(fd) };
|
||||
let _ = self.poller.delete(borrowed);
|
||||
drop(opened);
|
||||
let OpenedDevice { device, held, .. } = opened;
|
||||
drop(device);
|
||||
self.release_codes(held);
|
||||
}
|
||||
}
|
||||
|
||||
fn release_all(&mut self) {
|
||||
let mut held = HashSet::new();
|
||||
for opened in self.devices.values_mut() {
|
||||
held.extend(opened.held.drain());
|
||||
}
|
||||
self.release_codes(held);
|
||||
}
|
||||
|
||||
fn release_codes(&mut self, codes: HashSet<u16>) {
|
||||
let mut codes: Vec<u16> = codes
|
||||
.into_iter()
|
||||
.filter(|code| !self.is_held(*code))
|
||||
.collect();
|
||||
codes.sort_unstable();
|
||||
for code in codes {
|
||||
self.dispatch(self.native_event(code, false));
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_session_gate(&mut self) {
|
||||
let active = self.gate.is_active();
|
||||
if self.delivering && !active {
|
||||
self.release_all();
|
||||
}
|
||||
self.delivering = active;
|
||||
}
|
||||
|
||||
fn run(&mut self) {
|
||||
let monitor = self.try_attach_monitor();
|
||||
let inotify = self.try_attach_inotify();
|
||||
self.open_all_devices();
|
||||
let mut events = Events::new();
|
||||
loop {
|
||||
if self.stop.load(Ordering::Acquire) {
|
||||
break;
|
||||
}
|
||||
events.clear();
|
||||
if self.poller.wait(&mut events, None).is_err() {
|
||||
if self
|
||||
.poller
|
||||
.wait(&mut events, self.next_retry_timeout())
|
||||
.is_err()
|
||||
{
|
||||
break;
|
||||
}
|
||||
let mut device_fds_to_drain: Vec<RawFd> = Vec::new();
|
||||
let mut drain_monitor = false;
|
||||
let mut drain_inotify = false;
|
||||
let mut got_exit = false;
|
||||
for event in events.iter() {
|
||||
match event.key {
|
||||
EXIT_KEY => got_exit = true,
|
||||
MONITOR_KEY => drain_monitor = true,
|
||||
INOTIFY_KEY => drain_inotify = true,
|
||||
fd_key => {
|
||||
if let Some(fd) = device_fd_from_poll_key(fd_key) {
|
||||
device_fds_to_drain.push(fd);
|
||||
@@ -361,26 +503,43 @@ impl Reader {
|
||||
break;
|
||||
}
|
||||
|
||||
self.rearm(monitor.as_ref(), drain_monitor, &device_fds_to_drain);
|
||||
self.rearm(
|
||||
monitor.as_ref(),
|
||||
drain_monitor,
|
||||
inotify.as_ref().filter(|_| drain_inotify),
|
||||
&device_fds_to_drain,
|
||||
);
|
||||
|
||||
self.apply_session_gate();
|
||||
if drain_monitor && let Some(monitor) = monitor.as_ref() {
|
||||
self.drain_monitor(monitor);
|
||||
}
|
||||
if drain_inotify && let Some(inotify) = inotify.as_ref() {
|
||||
self.drain_inotify(inotify);
|
||||
}
|
||||
self.run_due_retries();
|
||||
for fd in device_fds_to_drain {
|
||||
self.drain_device(fd);
|
||||
}
|
||||
}
|
||||
|
||||
if !self.stop.load(Ordering::Acquire) {
|
||||
self.release_all();
|
||||
}
|
||||
if let Some(monitor) = monitor.as_ref() {
|
||||
let borrowed = unsafe { BorrowedFd::borrow_raw(monitor.as_raw_fd()) };
|
||||
let _ = self.poller.delete(borrowed);
|
||||
}
|
||||
if let Some(inotify) = inotify.as_ref() {
|
||||
let _ = self.poller.delete(inotify.as_fd());
|
||||
}
|
||||
}
|
||||
|
||||
fn rearm(
|
||||
&self,
|
||||
monitor: Option<&udev::MonitorSocket>,
|
||||
drain_monitor: bool,
|
||||
inotify: Option<&Inotify>,
|
||||
device_fds: &[RawFd],
|
||||
) {
|
||||
let _ = self
|
||||
@@ -392,6 +551,11 @@ impl Reader {
|
||||
.poller
|
||||
.modify(borrowed, PollEvent::readable(MONITOR_KEY));
|
||||
}
|
||||
if let Some(inotify) = inotify {
|
||||
let _ = self
|
||||
.poller
|
||||
.modify(inotify.as_fd(), PollEvent::readable(INOTIFY_KEY));
|
||||
}
|
||||
for fd in device_fds {
|
||||
if self.devices.contains_key(fd) {
|
||||
let Some(poll_key) = poll_key_for_device_fd(*fd) else {
|
||||
@@ -428,13 +592,13 @@ impl Reader {
|
||||
for (action, devnode, seat) in pending {
|
||||
match action.as_str() {
|
||||
"add" => {
|
||||
if !self.seat.is_empty()
|
||||
if !self.seats.seat.is_empty()
|
||||
&& let Some(seat) = seat.as_deref()
|
||||
&& seat != self.seat
|
||||
&& seat != self.seats.seat
|
||||
{
|
||||
continue;
|
||||
}
|
||||
let _ = self.open_device(&devnode);
|
||||
self.open_hotplugged_device(&devnode);
|
||||
}
|
||||
"remove" => self.close_device_by_path(&devnode),
|
||||
_ => {}
|
||||
@@ -442,22 +606,60 @@ impl Reader {
|
||||
}
|
||||
}
|
||||
|
||||
fn drain_inotify(&mut self, inotify: &Inotify) {
|
||||
let mut paths: Vec<PathBuf> = Vec::new();
|
||||
while let Ok(events) = inotify.read_events() {
|
||||
if events.is_empty() {
|
||||
break;
|
||||
}
|
||||
for event in events {
|
||||
let Some(name) = event.name.as_deref().and_then(OsStr::to_str) else {
|
||||
continue;
|
||||
};
|
||||
if !is_event_node_name(name) || !self.seats.includes(name) {
|
||||
continue;
|
||||
}
|
||||
let path = Path::new(DEV_INPUT_DIR).join(name);
|
||||
if !paths.contains(&path) {
|
||||
paths.push(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
for path in paths {
|
||||
self.open_hotplugged_device(&path);
|
||||
}
|
||||
}
|
||||
|
||||
fn drain_device(&mut self, fd: RawFd) {
|
||||
let mut decoded: Vec<(u16, i32)> = Vec::new();
|
||||
let mut decoded: Vec<DeviceInput> = Vec::new();
|
||||
let mut device_dead = false;
|
||||
{
|
||||
let Some(opened) = self.devices.get_mut(&fd) else {
|
||||
return;
|
||||
};
|
||||
loop {
|
||||
let fetch_result = opened.device.fetch_events();
|
||||
match fetch_result {
|
||||
match opened.device.fetch_events() {
|
||||
Ok(events) => {
|
||||
for ev in events {
|
||||
if ev.event_type() != EventType::KEY {
|
||||
continue;
|
||||
match (ev.event_type(), ev.code()) {
|
||||
(EventType::SYNCHRONIZATION, code)
|
||||
if code == SynchronizationCode::SYN_DROPPED.0 =>
|
||||
{
|
||||
opened.dropped = true;
|
||||
}
|
||||
(EventType::SYNCHRONIZATION, code)
|
||||
if opened.dropped
|
||||
&& code == SynchronizationCode::SYN_REPORT.0 =>
|
||||
{
|
||||
opened.dropped = false;
|
||||
decoded.push(DeviceInput::Resync);
|
||||
}
|
||||
_ if opened.dropped => {}
|
||||
(EventType::KEY, code) => {
|
||||
decoded.push(DeviceInput::Key(code, ev.value()));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
decoded.push((ev.code(), ev.value()));
|
||||
}
|
||||
}
|
||||
Err(err) if err.kind() == std::io::ErrorKind::WouldBlock => break,
|
||||
@@ -472,50 +674,86 @@ impl Reader {
|
||||
self.close_device_by_fd(fd);
|
||||
return;
|
||||
}
|
||||
for (code, value) in decoded {
|
||||
if value == 2 {
|
||||
continue;
|
||||
if !self.delivering {
|
||||
return;
|
||||
}
|
||||
for input in decoded {
|
||||
match input {
|
||||
DeviceInput::Key(_, 2) => {}
|
||||
DeviceInput::Key(code, value) => self.translate_event(fd, code, value == 1),
|
||||
DeviceInput::Resync => self.resync_device(fd),
|
||||
}
|
||||
self.translate_event(code, value == 1);
|
||||
}
|
||||
}
|
||||
|
||||
fn translate_event(&mut self, code: u16, is_press: bool) {
|
||||
fn resync_device(&mut self, fd: RawFd) {
|
||||
let Some(opened) = self.devices.get_mut(&fd) else {
|
||||
return;
|
||||
};
|
||||
let released = match opened.device.get_key_state() {
|
||||
Ok(state) => {
|
||||
let down: HashSet<u16> = state
|
||||
.iter()
|
||||
.map(|key| key.code())
|
||||
.filter(|code| is_routable_key_code(*code))
|
||||
.collect();
|
||||
let released: HashSet<u16> = opened.held.difference(&down).copied().collect();
|
||||
opened.held = down;
|
||||
released
|
||||
}
|
||||
Err(_) => std::mem::take(&mut opened.held),
|
||||
};
|
||||
self.release_codes(released);
|
||||
}
|
||||
|
||||
fn translate_event(&mut self, fd: RawFd, code: u16, is_press: bool) {
|
||||
if !is_routable_key_code(code) {
|
||||
return;
|
||||
}
|
||||
if let Some(opened) = self.devices.get_mut(&fd) {
|
||||
if is_press {
|
||||
opened.held.insert(code);
|
||||
} else {
|
||||
opened.held.remove(&code);
|
||||
}
|
||||
}
|
||||
self.dispatch(self.native_event(code, is_press));
|
||||
}
|
||||
|
||||
fn native_event(&self, code: u16, is_press: bool) -> NativeEvent {
|
||||
let ctrl = self.modifier_state_ctrl();
|
||||
let alt = self.modifier_state_alt();
|
||||
let shift = self.modifier_state_shift();
|
||||
let meta = self.modifier_state_meta();
|
||||
if let Some(button) = keymap::evdev_button_to_browser_button(code) {
|
||||
let event = NativeEvent::Mouse {
|
||||
return NativeEvent::Mouse {
|
||||
kind: if is_press {
|
||||
MouseKind::Down
|
||||
} else {
|
||||
MouseKind::Up
|
||||
},
|
||||
button,
|
||||
ctrl: self.modifier_state_ctrl(),
|
||||
alt: self.modifier_state_alt(),
|
||||
shift: self.modifier_state_shift(),
|
||||
meta: self.modifier_state_meta(),
|
||||
ctrl,
|
||||
alt,
|
||||
shift,
|
||||
meta,
|
||||
};
|
||||
self.dispatch(event);
|
||||
return;
|
||||
}
|
||||
let key_name = match keymap::keycode_to_name(code) {
|
||||
Some(name) => name,
|
||||
None => return,
|
||||
};
|
||||
if is_press {
|
||||
self.held_keys.insert(code);
|
||||
} else {
|
||||
self.held_keys.remove(&code);
|
||||
}
|
||||
let event = NativeEvent::Key {
|
||||
NativeEvent::Key {
|
||||
kind: if is_press { KeyKind::Down } else { KeyKind::Up },
|
||||
keycode: code,
|
||||
key_name,
|
||||
ctrl: self.modifier_state_ctrl(),
|
||||
alt: self.modifier_state_alt(),
|
||||
shift: self.modifier_state_shift(),
|
||||
meta: self.modifier_state_meta(),
|
||||
};
|
||||
self.dispatch(event);
|
||||
key_name: keymap::keycode_to_name(code).unwrap_or_default(),
|
||||
ctrl,
|
||||
alt,
|
||||
shift,
|
||||
meta,
|
||||
}
|
||||
}
|
||||
|
||||
fn is_held(&self, code: u16) -> bool {
|
||||
self.devices
|
||||
.values()
|
||||
.any(|opened| opened.held.contains(&code))
|
||||
}
|
||||
|
||||
fn dispatch(&self, event: NativeEvent) {
|
||||
@@ -528,32 +766,53 @@ impl Reader {
|
||||
}
|
||||
|
||||
fn modifier_state_ctrl(&self) -> bool {
|
||||
self.held_keys.contains(&keymap::LEFT_CTRL) || self.held_keys.contains(&keymap::RIGHT_CTRL)
|
||||
self.is_held(keymap::LEFT_CTRL) || self.is_held(keymap::RIGHT_CTRL)
|
||||
}
|
||||
fn modifier_state_alt(&self) -> bool {
|
||||
self.held_keys.contains(&keymap::LEFT_ALT) || self.held_keys.contains(&keymap::RIGHT_ALT)
|
||||
self.is_held(keymap::LEFT_ALT) || self.is_held(keymap::RIGHT_ALT)
|
||||
}
|
||||
fn modifier_state_shift(&self) -> bool {
|
||||
self.held_keys.contains(&keymap::LEFT_SHIFT)
|
||||
|| self.held_keys.contains(&keymap::RIGHT_SHIFT)
|
||||
self.is_held(keymap::LEFT_SHIFT) || self.is_held(keymap::RIGHT_SHIFT)
|
||||
}
|
||||
fn modifier_state_meta(&self) -> bool {
|
||||
self.held_keys.contains(&keymap::LEFT_META) || self.held_keys.contains(&keymap::RIGHT_META)
|
||||
self.is_held(keymap::LEFT_META) || self.is_held(keymap::RIGHT_META)
|
||||
}
|
||||
}
|
||||
|
||||
const KEYBOARD_KEY_CODES: Range<u16> = KeyCode::KEY_ESC.code()..KeyCode::BTN_0.code();
|
||||
const EXTENDED_KEY_CODES: Range<u16> = KeyCode::KEY_OK.code()..KeyCode::BTN_TRIGGER_HAPPY1.code();
|
||||
|
||||
fn is_routable_key_code(code: u16) -> bool {
|
||||
keymap::keycode_to_name(code).is_some()
|
||||
KEYBOARD_KEY_CODES.contains(&code)
|
||||
|| EXTENDED_KEY_CODES.contains(&code)
|
||||
|| keymap::evdev_button_to_browser_button(code).is_some()
|
||||
}
|
||||
|
||||
fn device_has_routable_input(device: &Device) -> bool {
|
||||
fn device_has_routable_input(device: &RawDevice) -> bool {
|
||||
device.supported_keys().is_some_and(|keys| {
|
||||
keys.iter()
|
||||
.any(|key: KeyCode| is_routable_key_code(key.code()))
|
||||
})
|
||||
}
|
||||
|
||||
fn is_full_keyboard_key_set(supports: impl Fn(u16) -> bool) -> bool {
|
||||
FULL_KEYBOARD_KEY_CODES.into_iter().all(supports)
|
||||
}
|
||||
|
||||
fn device_is_full_keyboard(device: &RawDevice) -> bool {
|
||||
device
|
||||
.supported_keys()
|
||||
.is_some_and(|keys| is_full_keyboard_key_set(|code| keys.contains(KeyCode::new(code))))
|
||||
}
|
||||
|
||||
fn open_read_only(path: &Path) -> std::io::Result<RawDevice> {
|
||||
let file = std::fs::OpenOptions::new()
|
||||
.read(true)
|
||||
.custom_flags(OFlag::O_NONBLOCK.bits())
|
||||
.open(path)?;
|
||||
RawDevice::from_fd(OwnedFd::from(file))
|
||||
}
|
||||
|
||||
impl Drop for Reader {
|
||||
fn drop(&mut self) {
|
||||
let fds: Vec<RawFd> = self.devices.keys().copied().collect();
|
||||
@@ -570,11 +829,12 @@ struct HookInner {
|
||||
stop: Option<Arc<AtomicBool>>,
|
||||
exit_fd: Option<Arc<ExitFd>>,
|
||||
thread: Option<JoinHandle<()>>,
|
||||
opened: bool,
|
||||
session_watch: Option<SessionWatch>,
|
||||
}
|
||||
|
||||
impl HookInner {
|
||||
fn stop_and_join(&mut self) {
|
||||
self.session_watch = None;
|
||||
if let Some(stop) = &self.stop {
|
||||
stop.store(true, Ordering::Release);
|
||||
}
|
||||
@@ -586,7 +846,6 @@ impl HookInner {
|
||||
}
|
||||
self.stop = None;
|
||||
self.exit_fd = None;
|
||||
self.opened = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -605,7 +864,6 @@ impl EvdevHook {
|
||||
.build_threadsafe_function::<NativeEvent>()
|
||||
.weak::<true>()
|
||||
.callee_handled::<false>()
|
||||
.max_queue_size::<EVENT_QUEUE_LIMIT>()
|
||||
.build()
|
||||
.map_err(|err| {
|
||||
generic_error(format!("failed to build callback: {}", err.reason))
|
||||
@@ -617,7 +875,7 @@ impl EvdevHook {
|
||||
stop: None,
|
||||
exit_fd: None,
|
||||
thread: None,
|
||||
opened: false,
|
||||
session_watch: None,
|
||||
}),
|
||||
})
|
||||
}
|
||||
@@ -628,8 +886,11 @@ impl EvdevHook {
|
||||
.inner
|
||||
.lock()
|
||||
.map_err(|_| generic_error("hook lock poisoned"))?;
|
||||
if inner.thread.is_some() {
|
||||
return Ok(inner.opened);
|
||||
if let Some(thread) = inner.thread.as_ref() {
|
||||
if !thread.is_finished() {
|
||||
return Ok(true);
|
||||
}
|
||||
inner.stop_and_join();
|
||||
}
|
||||
let stop = Arc::new(AtomicBool::new(false));
|
||||
let exit_fd = Arc::new(
|
||||
@@ -640,8 +901,12 @@ impl EvdevHook {
|
||||
.map_err(|err| generic_error(format!("evdev start failed: {err}")))?;
|
||||
|
||||
reader.open_all_devices();
|
||||
let opened = !reader.devices.is_empty();
|
||||
if !reader.has_full_keyboard() {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
let session_watch = SessionWatch::spawn(reader.gate.clone())
|
||||
.map_err(|err| generic_error(format!("failed to spawn logind thread: {err}")))?;
|
||||
let join = thread::Builder::new()
|
||||
.name("fluxer-linux-evdev-reader".to_string())
|
||||
.spawn(move || {
|
||||
@@ -653,8 +918,8 @@ impl EvdevHook {
|
||||
inner.stop = Some(stop);
|
||||
inner.exit_fd = Some(exit_fd);
|
||||
inner.thread = Some(join);
|
||||
inner.opened = opened;
|
||||
Ok(opened)
|
||||
inner.session_watch = Some(session_watch);
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
#[napi]
|
||||
@@ -676,9 +941,12 @@ impl Drop for EvdevHook {
|
||||
}
|
||||
}
|
||||
|
||||
#[napi(js_name = "nameToEvdevKeycode")]
|
||||
pub fn name_to_evdev_keycode(name: String) -> u32 {
|
||||
u32::from(keymap::name_to_keycode(&name))
|
||||
#[napi(js_name = "isKeyboardReadable")]
|
||||
pub fn is_keyboard_readable() -> bool {
|
||||
SeatFilter::detect()
|
||||
.event_node_paths()
|
||||
.iter()
|
||||
.any(|path| open_read_only(path).is_ok_and(|device| device_is_full_keyboard(&device)))
|
||||
}
|
||||
|
||||
fn generic_error(reason: impl Into<String>) -> napi::Error {
|
||||
@@ -693,11 +961,50 @@ mod tests {
|
||||
fn control_poll_keys_are_legal_and_outside_fd_range() {
|
||||
assert_eq!(EXIT_KEY, 0);
|
||||
assert_eq!(MONITOR_KEY, 1);
|
||||
assert_ne!(EXIT_KEY, MONITOR_KEY);
|
||||
assert_eq!(INOTIFY_KEY, 2);
|
||||
assert_eq!(poll_key_for_device_fd(0), Some(DEVICE_KEY_BASE));
|
||||
assert_eq!(device_fd_from_poll_key(DEVICE_KEY_BASE), Some(0));
|
||||
assert_eq!(device_fd_from_poll_key(EXIT_KEY), None);
|
||||
assert_eq!(device_fd_from_poll_key(MONITOR_KEY), None);
|
||||
assert_eq!(device_fd_from_poll_key(INOTIFY_KEY), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn full_keyboard_requires_every_key_from_escape_to_s() {
|
||||
assert!(is_full_keyboard_key_set(|code| code < 200));
|
||||
assert!(!is_full_keyboard_key_set(|code| code != 0 && code != 17));
|
||||
assert!(!is_full_keyboard_key_set(|code| code == 0 || code >= 32));
|
||||
assert!(is_full_keyboard_key_set(|code| (1..32).contains(&code)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn event_node_names_match_only_event_devices() {
|
||||
assert!(is_event_node_name("event0"));
|
||||
assert!(is_event_node_name("event17"));
|
||||
assert!(!is_event_node_name("mouse0"));
|
||||
assert!(!is_event_node_name("js0"));
|
||||
assert!(!is_event_node_name("by-id"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn session_gate_notifies_only_on_change() {
|
||||
let poller = Arc::new(Poller::new().expect("create poller"));
|
||||
let gate = SessionGate::new(poller.clone());
|
||||
assert!(!gate.is_active());
|
||||
gate.set_active(false);
|
||||
let mut events = Events::new();
|
||||
poller
|
||||
.wait(&mut events, Some(Duration::from_millis(0)))
|
||||
.expect("poll without notification");
|
||||
gate.set_active(true);
|
||||
assert!(gate.is_active());
|
||||
let started = Instant::now();
|
||||
poller
|
||||
.wait(&mut events, Some(Duration::from_secs(5)))
|
||||
.expect("poll with notification");
|
||||
assert!(started.elapsed() < Duration::from_secs(5));
|
||||
gate.set_active(false);
|
||||
assert!(!gate.is_active());
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -722,6 +1029,37 @@ mod tests {
|
||||
assert!(is_routable_key_code(KeyCode::BTN_FORWARD.code()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_key_main_maps_to_a_dom_code_is_routable() {
|
||||
let source = include_str!("../../../src/main/GlobalShortcutKeys.ts");
|
||||
let table = source
|
||||
.split("const EVDEV_KEY_CODES")
|
||||
.nth(1)
|
||||
.and_then(|rest| rest.split("]);").next())
|
||||
.expect("EVDEV_KEY_CODES table");
|
||||
let codes: Vec<u16> = table
|
||||
.split("[0x")
|
||||
.skip(1)
|
||||
.map(|entry| {
|
||||
let hex = entry.split(',').next().expect("code");
|
||||
u16::from_str_radix(hex, 16).expect("hex code")
|
||||
})
|
||||
.collect();
|
||||
assert!(codes.len() > 150);
|
||||
for code in codes {
|
||||
assert!(is_routable_key_code(code), "evdev code {code:#x}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn routable_key_code_filter_keeps_keys_without_a_native_name() {
|
||||
assert!(is_routable_key_code(KeyCode::KEY_CALC.code()));
|
||||
assert!(is_routable_key_code(KeyCode::KEY_PLAYCD.code()));
|
||||
assert!(is_routable_key_code(KeyCode::KEY_FN.code()));
|
||||
assert!(!is_routable_key_code(0));
|
||||
assert!(!is_routable_key_code(KeyCode::BTN_TRIGGER_HAPPY1.code()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn routable_key_code_filter_ignores_tablet_pad_and_tool_buttons() {
|
||||
assert!(!is_routable_key_code(KeyCode::BTN_0.code()));
|
||||
|
||||
@@ -0,0 +1,283 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
use std::{
|
||||
sync::{
|
||||
Arc,
|
||||
atomic::{AtomicBool, Ordering},
|
||||
},
|
||||
thread::{self, JoinHandle},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use event_listener::Event;
|
||||
use futures_lite::{StreamExt, future};
|
||||
use polling::Poller;
|
||||
use zbus::{Connection, Proxy, zvariant::OwnedObjectPath};
|
||||
|
||||
const LOGIND_DESTINATION: &str = "org.freedesktop.login1";
|
||||
const LOGIND_PATH: &str = "/org/freedesktop/login1";
|
||||
const MANAGER_INTERFACE: &str = "org.freedesktop.login1.Manager";
|
||||
const SESSION_INTERFACE: &str = "org.freedesktop.login1.Session";
|
||||
const USER_INTERFACE: &str = "org.freedesktop.login1.User";
|
||||
const RETRY_DELAYS: [Duration; 5] = [
|
||||
Duration::from_secs(1),
|
||||
Duration::from_secs(2),
|
||||
Duration::from_secs(5),
|
||||
Duration::from_secs(10),
|
||||
Duration::from_secs(30),
|
||||
];
|
||||
|
||||
pub struct SessionGate {
|
||||
active: AtomicBool,
|
||||
poller: Arc<Poller>,
|
||||
}
|
||||
|
||||
impl SessionGate {
|
||||
pub fn new(poller: Arc<Poller>) -> Self {
|
||||
Self {
|
||||
active: AtomicBool::new(false),
|
||||
poller,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_active(&self) -> bool {
|
||||
self.active.load(Ordering::Acquire)
|
||||
}
|
||||
|
||||
pub fn set_active(&self, active: bool) {
|
||||
if self.active.swap(active, Ordering::AcqRel) != active {
|
||||
let _ = self.poller.notify();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct StopSignal {
|
||||
stopped: AtomicBool,
|
||||
event: Event,
|
||||
}
|
||||
|
||||
impl StopSignal {
|
||||
async fn wait(&self) {
|
||||
loop {
|
||||
if self.stopped.load(Ordering::Acquire) {
|
||||
return;
|
||||
}
|
||||
let listener = self.event.listen();
|
||||
if self.stopped.load(Ordering::Acquire) {
|
||||
return;
|
||||
}
|
||||
listener.await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct SessionWatch {
|
||||
stop: Arc<StopSignal>,
|
||||
thread: Option<JoinHandle<()>>,
|
||||
}
|
||||
|
||||
impl SessionWatch {
|
||||
pub fn spawn(gate: Arc<SessionGate>) -> std::io::Result<Self> {
|
||||
let stop = Arc::new(StopSignal {
|
||||
stopped: AtomicBool::new(false),
|
||||
event: Event::new(),
|
||||
});
|
||||
let thread_stop = stop.clone();
|
||||
let thread = thread::Builder::new()
|
||||
.name("fluxer-linux-evdev-logind".to_string())
|
||||
.spawn(move || {
|
||||
async_io::block_on(future::or(supervise(&gate), thread_stop.wait()));
|
||||
})?;
|
||||
Ok(Self {
|
||||
stop,
|
||||
thread: Some(thread),
|
||||
})
|
||||
}
|
||||
|
||||
fn shutdown(&mut self) {
|
||||
self.stop.stopped.store(true, Ordering::Release);
|
||||
self.stop.event.notify(usize::MAX);
|
||||
if let Some(thread) = self.thread.take() {
|
||||
let _ = thread.join();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for SessionWatch {
|
||||
fn drop(&mut self) {
|
||||
self.shutdown();
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum WatchEnd {
|
||||
Unreachable(zbus::Error),
|
||||
NoSession(zbus::Error),
|
||||
Lost(Option<zbus::Error>),
|
||||
}
|
||||
|
||||
fn retry_delay(failures: usize) -> Duration {
|
||||
RETRY_DELAYS[failures.min(RETRY_DELAYS.len() - 1)]
|
||||
}
|
||||
|
||||
async fn supervise(gate: &SessionGate) {
|
||||
let mut ever_subscribed = false;
|
||||
let mut failures = 0usize;
|
||||
loop {
|
||||
let mut subscribed = false;
|
||||
let end = watch_session(gate, &mut subscribed).await;
|
||||
if subscribed {
|
||||
ever_subscribed = true;
|
||||
failures = 0;
|
||||
}
|
||||
match &end {
|
||||
WatchEnd::Unreachable(err) if !ever_subscribed => {
|
||||
eprintln!("[linux-evdev] logind unreachable, session gating disabled: {err}");
|
||||
gate.set_active(true);
|
||||
return;
|
||||
}
|
||||
WatchEnd::NoSession(err) if !ever_subscribed => {
|
||||
eprintln!("[linux-evdev] no logind session found, session gating disabled: {err}");
|
||||
gate.set_active(true);
|
||||
return;
|
||||
}
|
||||
WatchEnd::Unreachable(err) | WatchEnd::NoSession(err) | WatchEnd::Lost(Some(err))
|
||||
if failures == 0 =>
|
||||
{
|
||||
eprintln!("[linux-evdev] logind session watch lost, retrying: {err}");
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
async_io::Timer::after(retry_delay(failures)).await;
|
||||
failures = failures.saturating_add(1);
|
||||
}
|
||||
}
|
||||
|
||||
fn session_id() -> Option<String> {
|
||||
std::env::var("XDG_SESSION_ID")
|
||||
.ok()
|
||||
.filter(|value| !value.is_empty())
|
||||
}
|
||||
|
||||
fn is_unreachable_error(err: &zbus::Error) -> bool {
|
||||
match err {
|
||||
zbus::Error::MethodError(name, _, _) => matches!(
|
||||
name.as_str(),
|
||||
"org.freedesktop.DBus.Error.ServiceUnknown"
|
||||
| "org.freedesktop.DBus.Error.NameHasNoOwner"
|
||||
| "org.freedesktop.DBus.Error.AccessDenied"
|
||||
| "org.freedesktop.DBus.Error.Disconnected"
|
||||
),
|
||||
zbus::Error::InputOutput(_) | zbus::Error::Address(_) | zbus::Error::Handshake(_) => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn classify(err: zbus::Error) -> WatchEnd {
|
||||
if is_unreachable_error(&err) {
|
||||
WatchEnd::Unreachable(err)
|
||||
} else {
|
||||
WatchEnd::NoSession(err)
|
||||
}
|
||||
}
|
||||
|
||||
fn is_session_path(path: &OwnedObjectPath) -> bool {
|
||||
path.as_str() != "/"
|
||||
}
|
||||
|
||||
async fn display_session(
|
||||
connection: &Connection,
|
||||
manager: &Proxy<'_>,
|
||||
) -> zbus::Result<OwnedObjectPath> {
|
||||
let user_path: OwnedObjectPath = manager.call("GetUserByPID", &(std::process::id(),)).await?;
|
||||
let user = Proxy::new(connection, LOGIND_DESTINATION, user_path, USER_INTERFACE).await?;
|
||||
let (id, path): (String, OwnedObjectPath) = user.get_property("Display").await?;
|
||||
if id.is_empty() || !is_session_path(&path) {
|
||||
return Err(zbus::Error::Failure(
|
||||
"user has no display session".to_string(),
|
||||
));
|
||||
}
|
||||
Ok(path)
|
||||
}
|
||||
|
||||
async fn resolve_session(
|
||||
connection: &Connection,
|
||||
manager: &Proxy<'_>,
|
||||
) -> Result<OwnedObjectPath, WatchEnd> {
|
||||
if let Some(id) = session_id() {
|
||||
match manager
|
||||
.call::<_, _, OwnedObjectPath>("GetSession", &(id,))
|
||||
.await
|
||||
{
|
||||
Ok(path) => return Ok(path),
|
||||
Err(err) if is_unreachable_error(&err) => return Err(WatchEnd::Unreachable(err)),
|
||||
Err(_) => {}
|
||||
}
|
||||
}
|
||||
match manager
|
||||
.call::<_, _, OwnedObjectPath>("GetSessionByPID", &(std::process::id(),))
|
||||
.await
|
||||
{
|
||||
Ok(path) => return Ok(path),
|
||||
Err(err) if is_unreachable_error(&err) => return Err(WatchEnd::Unreachable(err)),
|
||||
Err(_) => {}
|
||||
}
|
||||
display_session(connection, manager).await.map_err(classify)
|
||||
}
|
||||
|
||||
async fn watch_session(gate: &SessionGate, subscribed: &mut bool) -> WatchEnd {
|
||||
let connection = match Connection::system().await {
|
||||
Ok(connection) => connection,
|
||||
Err(err) => return WatchEnd::Unreachable(err),
|
||||
};
|
||||
let manager = match Proxy::new(
|
||||
&connection,
|
||||
LOGIND_DESTINATION,
|
||||
LOGIND_PATH,
|
||||
MANAGER_INTERFACE,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(manager) => manager,
|
||||
Err(err) => return classify(err),
|
||||
};
|
||||
let session_path = match resolve_session(&connection, &manager).await {
|
||||
Ok(path) => path,
|
||||
Err(end) => return end,
|
||||
};
|
||||
let session = match Proxy::new(
|
||||
&connection,
|
||||
LOGIND_DESTINATION,
|
||||
session_path,
|
||||
SESSION_INTERFACE,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(session) => session,
|
||||
Err(err) => return classify(err),
|
||||
};
|
||||
let mut owner_changes = match session.receive_owner_changed().await {
|
||||
Ok(stream) => stream,
|
||||
Err(err) => return classify(err),
|
||||
};
|
||||
let mut changes = session.receive_property_changed::<bool>("Active").await;
|
||||
match session.get_property::<bool>("Active").await {
|
||||
Ok(active) => gate.set_active(active),
|
||||
Err(err) => return classify(err),
|
||||
}
|
||||
*subscribed = true;
|
||||
loop {
|
||||
let change = future::or(async { changes.next().await }, async {
|
||||
owner_changes.next().await;
|
||||
None
|
||||
})
|
||||
.await;
|
||||
let Some(change) = change else {
|
||||
return WatchEnd::Lost(None);
|
||||
};
|
||||
match change.get().await {
|
||||
Ok(active) => gate.set_active(active),
|
||||
Err(err) => return WatchEnd::Lost(Some(err)),
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user