Implement native InputCapture with private EIS receiver transport
Add consent, zones and outside-edge barriers, connection-scoped leases, bounded input queues and keyboard/pointer forwarding. Restore local input on release, locking, topology or keymap changes, receiver loss and emergency escape. Preserve held-key state and distinguish finger stops from absent scroll axes. Validation: independent review cleared; 5 native InputCapture unit tests; 32 portal tests; official D-Bus contract checks on a private bus; real installed libei receiver interoperability test. Live physical barrier and consent interaction remain to be checked in a desktop session.
This commit is contained in:
1 parent
baca852ee9
commit
d26e1511d1
22 files changed
+2349
-11
No files matched your search
Generated
+14
@@ -4886,6 +4886,18 @@ version = "0.8.11"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4"
|
checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "reis"
|
||||||
|
version = "0.7.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4b967ec6489a42067a20724f11987bb96178ea796873cbe1afd25fb93ee6f85f"
|
||||||
|
dependencies = [
|
||||||
|
"calloop 0.14.4",
|
||||||
|
"enumflags2",
|
||||||
|
"log",
|
||||||
|
"rustix 1.1.5",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "renderdoc-sys"
|
name = "renderdoc-sys"
|
||||||
version = "1.1.0"
|
version = "1.1.0"
|
||||||
@@ -7415,6 +7427,7 @@ checksum = "2fb433233f2df9344722454bc7e96465c9d03bff9d77c248f9e7523fe79585b5"
|
|||||||
name = "xdg-desktop-portal-ferese"
|
name = "xdg-desktop-portal-ferese"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"calloop 0.14.4",
|
||||||
"dirs",
|
"dirs",
|
||||||
"ferese-config",
|
"ferese-config",
|
||||||
"ferese-ipc",
|
"ferese-ipc",
|
||||||
@@ -7429,6 +7442,7 @@ dependencies = [
|
|||||||
"memmap2 0.9.11",
|
"memmap2 0.9.11",
|
||||||
"pipewire",
|
"pipewire",
|
||||||
"png 0.18.1",
|
"png 0.18.1",
|
||||||
|
"reis",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ impl XdgActivationHandler for Ferese {
|
|||||||
data: XdgActivationTokenData,
|
data: XdgActivationTokenData,
|
||||||
surface: WlSurface,
|
surface: WlSurface,
|
||||||
) {
|
) {
|
||||||
if self.session_lock.active {
|
if self.session_lock.active || self.input_capture.captures(1) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let valid = data.serial.is_some() && data.timestamp.elapsed() <= TOKEN_MAX_AGE;
|
let valid = data.serial.is_some() && data.timestamp.elapsed() <= TOKEN_MAX_AGE;
|
||||||
|
|||||||
@@ -108,6 +108,7 @@ pub fn handle_commit(state: &mut Ferese, surface: &WlSurface) {
|
|||||||
|
|
||||||
let policy = layer.cached_state();
|
let policy = layer.cached_state();
|
||||||
if !state.session_lock.active
|
if !state.session_lock.active
|
||||||
|
&& !state.input_capture.captures(1)
|
||||||
&& policy.keyboard_interactivity == KeyboardInteractivity::Exclusive
|
&& policy.keyboard_interactivity == KeyboardInteractivity::Exclusive
|
||||||
&& matches!(policy.layer, Layer::Top | Layer::Overlay)
|
&& matches!(policy.layer, Layer::Top | Layer::Overlay)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -254,7 +254,7 @@ impl XdgShellHandler for Ferese {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn grab(&mut self, surface: PopupSurface, seat: wl_seat::WlSeat, serial: Serial) {
|
fn grab(&mut self, surface: PopupSurface, seat: wl_seat::WlSeat, serial: Serial) {
|
||||||
if self.session_lock.active {
|
if self.session_lock.active || self.input_capture.active() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let Some(seat) = Seat::from_resource(&seat) else {
|
let Some(seat) = Seat::from_resource(&seat) else {
|
||||||
|
|||||||
@@ -59,6 +59,27 @@ impl Ferese {
|
|||||||
self.focus_lock_surface();
|
self.focus_lock_surface();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if self.input_capture.active()
|
||||||
|
&& matches!(
|
||||||
|
&event,
|
||||||
|
InputEvent::GestureSwipeBegin { .. }
|
||||||
|
| InputEvent::GestureSwipeUpdate { .. }
|
||||||
|
| InputEvent::GestureSwipeEnd { .. }
|
||||||
|
| InputEvent::GesturePinchBegin { .. }
|
||||||
|
| InputEvent::GesturePinchUpdate { .. }
|
||||||
|
| InputEvent::GesturePinchEnd { .. }
|
||||||
|
| InputEvent::GestureHoldBegin { .. }
|
||||||
|
| InputEvent::GestureHoldEnd { .. }
|
||||||
|
| InputEvent::TouchDown { .. }
|
||||||
|
| InputEvent::TouchMotion { .. }
|
||||||
|
| InputEvent::TouchUp { .. }
|
||||||
|
| InputEvent::TouchCancel { .. }
|
||||||
|
| InputEvent::TouchFrame { .. }
|
||||||
|
)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
match event {
|
match event {
|
||||||
InputEvent::GestureSwipeBegin { event } => {
|
InputEvent::GestureSwipeBegin { event } => {
|
||||||
let pointer = seat.get_pointer().expect("seat has a pointer");
|
let pointer = seat.get_pointer().expect("seat has a pointer");
|
||||||
@@ -143,6 +164,13 @@ impl Ferese {
|
|||||||
let keycode = event.key_code();
|
let keycode = event.key_code();
|
||||||
let state = event.state();
|
let state = event.state();
|
||||||
|
|
||||||
|
let was_captured = self.input_capture.active();
|
||||||
|
if self.input_capture.captures(1) {
|
||||||
|
keyboard.unset_grab(self);
|
||||||
|
if keyboard.current_focus().is_some() {
|
||||||
|
keyboard.set_focus(self, None, SERIAL_COUNTER.next_serial());
|
||||||
|
}
|
||||||
|
}
|
||||||
keyboard.input::<(), _>(
|
keyboard.input::<(), _>(
|
||||||
self,
|
self,
|
||||||
keycode,
|
keycode,
|
||||||
@@ -160,6 +188,17 @@ impl Ferese {
|
|||||||
return FilterResult::Forward;
|
return FilterResult::Forward;
|
||||||
}
|
}
|
||||||
let symbol = keysym.modified_sym().raw();
|
let symbol = keysym.modified_sym().raw();
|
||||||
|
if data.input_capture.active() && emergency_shortcut_escape(symbol, modifiers.ctrl, modifiers.alt) {
|
||||||
|
if state == KeyState::Pressed {
|
||||||
|
data.input_capture.disable_all();
|
||||||
|
data.intercepted_keys.insert(keycode);
|
||||||
|
}
|
||||||
|
return FilterResult::Intercept(());
|
||||||
|
}
|
||||||
|
if data.input_capture.captures(1) {
|
||||||
|
data.input_capture.push(serde_json::json!({"type":"key", "key":keycode.raw().saturating_sub(8), "pressed":state == KeyState::Pressed}));
|
||||||
|
return FilterResult::Forward;
|
||||||
|
}
|
||||||
if data.overview.is_active()
|
if data.overview.is_active()
|
||||||
&& matches!(symbol, keysyms::KEY_Return | keysyms::KEY_KP_Enter)
|
&& matches!(symbol, keysyms::KEY_Return | keysyms::KEY_KP_Enter)
|
||||||
{
|
{
|
||||||
@@ -249,12 +288,18 @@ impl Ferese {
|
|||||||
FilterResult::Intercept(())
|
FilterResult::Intercept(())
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
if was_captured && !self.input_capture.active() {
|
||||||
|
self.restore_input_capture_focus();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
InputEvent::PointerMotionAbsolute { event, .. } => {
|
InputEvent::PointerMotionAbsolute { event, .. } => {
|
||||||
let Some(position) = self.absolute_event_position(&event) else {
|
let Some(position) = self.absolute_event_position(&event) else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
let pointer = self.seat.get_pointer().expect("seat has a pointer");
|
let pointer = self.seat.get_pointer().expect("seat has a pointer");
|
||||||
|
if self.capture_pointer_motion(pointer.current_location(), position, true) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
let position = self.constrain_pointer_position(&pointer, position);
|
let position = self.constrain_pointer_position(&pointer, position);
|
||||||
|
|
||||||
pointer.motion(
|
pointer.motion(
|
||||||
@@ -284,6 +329,9 @@ impl Ferese {
|
|||||||
};
|
};
|
||||||
let delta = event.delta();
|
let delta = event.delta();
|
||||||
let delta_unaccel = event.delta_unaccel();
|
let delta_unaccel = event.delta_unaccel();
|
||||||
|
if self.capture_pointer_motion(position, position + delta, false) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
pointer.relative_motion(
|
pointer.relative_motion(
|
||||||
self,
|
self,
|
||||||
@@ -313,6 +361,12 @@ impl Ferese {
|
|||||||
crate::backends::direct::render_all(self);
|
crate::backends::direct::render_all(self);
|
||||||
}
|
}
|
||||||
InputEvent::PointerButton { event, .. } => {
|
InputEvent::PointerButton { event, .. } => {
|
||||||
|
if self.input_capture.active() {
|
||||||
|
if self.input_capture.captures(2) {
|
||||||
|
self.input_capture.push(serde_json::json!({"type":"button", "button":event.button_code(), "pressed":event.state() == ButtonState::Pressed}));
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
let pointer = self.seat.get_pointer().expect("seat has a pointer");
|
let pointer = self.seat.get_pointer().expect("seat has a pointer");
|
||||||
let serial = SERIAL_COUNTER.next_serial();
|
let serial = SERIAL_COUNTER.next_serial();
|
||||||
|
|
||||||
@@ -411,6 +465,12 @@ impl Ferese {
|
|||||||
let vertical = event.amount(Axis::Vertical).unwrap_or_else(|| {
|
let vertical = event.amount(Axis::Vertical).unwrap_or_else(|| {
|
||||||
event.amount_v120(Axis::Vertical).unwrap_or(0.0) * 15.0 / 120.0
|
event.amount_v120(Axis::Vertical).unwrap_or(0.0) * 15.0 / 120.0
|
||||||
});
|
});
|
||||||
|
if self.input_capture.active() {
|
||||||
|
if self.input_capture.captures(2) {
|
||||||
|
self.input_capture.push(serde_json::json!({"type":"scroll", "x":horizontal, "y":vertical, "v120_x":event.amount_v120(Axis::Horizontal), "v120_y":event.amount_v120(Axis::Vertical), "stop_x":source == AxisSource::Finger && event.amount(Axis::Horizontal) == Some(0.0), "stop_y":source == AxisSource::Finger && event.amount(Axis::Vertical) == Some(0.0)}));
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
let pointer = self.seat.get_pointer().expect("seat has a pointer");
|
let pointer = self.seat.get_pointer().expect("seat has a pointer");
|
||||||
if self.scroll_overview_strip(
|
if self.scroll_overview_strip(
|
||||||
pointer.current_location(),
|
pointer.current_location(),
|
||||||
@@ -664,6 +724,9 @@ impl Ferese {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn focus_window_at(&mut self, position: Point<f64, Logical>, serial: Serial, raise: bool) {
|
fn focus_window_at(&mut self, position: Point<f64, Logical>, serial: Serial, raise: bool) {
|
||||||
|
if self.input_capture.captures(1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if self.session_lock.active {
|
if self.session_lock.active {
|
||||||
self.focus_output_at(position);
|
self.focus_output_at(position);
|
||||||
self.focus_lock_surface();
|
self.focus_lock_surface();
|
||||||
|
|||||||
@@ -0,0 +1,719 @@
|
|||||||
|
use std::{
|
||||||
|
collections::{HashMap, VecDeque},
|
||||||
|
sync::mpsc::SyncSender,
|
||||||
|
};
|
||||||
|
|
||||||
|
use ferese_ipc::Response;
|
||||||
|
use serde::Deserialize;
|
||||||
|
use serde_json::{Value, json};
|
||||||
|
use smithay::wayland::keyboard_shortcuts_inhibit::KeyboardShortcutsInhibitorSeat;
|
||||||
|
|
||||||
|
const MAX_SESSIONS: usize = 8;
|
||||||
|
const MAX_BARRIERS: usize = 64;
|
||||||
|
const MAX_EVENTS: usize = 512;
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
|
pub(crate) struct Zone {
|
||||||
|
pub width: u32,
|
||||||
|
pub height: u32,
|
||||||
|
pub x: i32,
|
||||||
|
pub y: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Zone {
|
||||||
|
fn right(self) -> f64 {
|
||||||
|
f64::from(self.x) + f64::from(self.width)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn bottom(self) -> f64 {
|
||||||
|
f64::from(self.y) + f64::from(self.height)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn contains(self, point: (f64, f64)) -> bool {
|
||||||
|
point.0 >= f64::from(self.x)
|
||||||
|
&& point.0 < self.right()
|
||||||
|
&& point.1 >= f64::from(self.y)
|
||||||
|
&& point.1 < self.bottom()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
|
#[serde(deny_unknown_fields)]
|
||||||
|
pub(crate) struct Barrier {
|
||||||
|
pub id: u32,
|
||||||
|
pub position: [i32; 4],
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug)]
|
||||||
|
enum Edge {
|
||||||
|
Left,
|
||||||
|
Right,
|
||||||
|
Top,
|
||||||
|
Bottom,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn edge(barrier: &Barrier, zones: &[Zone]) -> Option<Edge> {
|
||||||
|
if barrier.id == 0 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let [x1, y1, x2, y2] = barrier.position.map(f64::from);
|
||||||
|
let horizontal = y1 == y2 && x1 <= x2;
|
||||||
|
let vertical = x1 == x2 && y1 <= y2;
|
||||||
|
if !horizontal && !vertical {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
for zone in zones {
|
||||||
|
let found = if horizontal && x1 >= f64::from(zone.x) && x2 < zone.right() {
|
||||||
|
if y1 == f64::from(zone.y) {
|
||||||
|
Some(Edge::Top)
|
||||||
|
} else if y1 == zone.bottom() {
|
||||||
|
Some(Edge::Bottom)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
} else if vertical && y1 >= f64::from(zone.y) && y2 < zone.bottom() {
|
||||||
|
if x1 == f64::from(zone.x) {
|
||||||
|
Some(Edge::Left)
|
||||||
|
} else if x1 == zone.right() {
|
||||||
|
Some(Edge::Right)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
let Some(found) = found else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let intersects_neighbor = zones.iter().any(|other| {
|
||||||
|
if other == zone {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
match found {
|
||||||
|
Edge::Top | Edge::Bottom => {
|
||||||
|
let outside = y1
|
||||||
|
+ if matches!(found, Edge::Top) {
|
||||||
|
-0.5
|
||||||
|
} else {
|
||||||
|
0.5
|
||||||
|
};
|
||||||
|
outside >= f64::from(other.y)
|
||||||
|
&& outside < other.bottom()
|
||||||
|
&& x1 < other.right()
|
||||||
|
&& x2 + 1.0 > f64::from(other.x)
|
||||||
|
}
|
||||||
|
Edge::Left | Edge::Right => {
|
||||||
|
let outside = x1
|
||||||
|
+ if matches!(found, Edge::Left) {
|
||||||
|
-0.5
|
||||||
|
} else {
|
||||||
|
0.5
|
||||||
|
};
|
||||||
|
outside >= f64::from(other.x)
|
||||||
|
&& outside < other.right()
|
||||||
|
&& y1 < other.bottom()
|
||||||
|
&& y2 + 1.0 > f64::from(other.y)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if !intersects_neighbor {
|
||||||
|
return Some(found);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn crossed(barrier: &Barrier, edge: Edge, from: (f64, f64), to: (f64, f64)) -> Option<f64> {
|
||||||
|
let [x1, y1, x2, y2] = barrier.position.map(f64::from);
|
||||||
|
let (distance, movement, start, end) = match edge {
|
||||||
|
Edge::Left if from.0 >= x1 && to.0 < x1 => (x1 - from.0, to.0 - from.0, y1, y2 + 1.0),
|
||||||
|
Edge::Right if from.0 < x1 && to.0 >= x1 => (x1 - from.0, to.0 - from.0, y1, y2 + 1.0),
|
||||||
|
Edge::Top if from.1 >= y1 && to.1 < y1 => (y1 - from.1, to.1 - from.1, x1, x2 + 1.0),
|
||||||
|
Edge::Bottom if from.1 < y1 && to.1 >= y1 => (y1 - from.1, to.1 - from.1, x1, x2 + 1.0),
|
||||||
|
_ => return None,
|
||||||
|
};
|
||||||
|
let fraction = distance / movement;
|
||||||
|
let along = if matches!(edge, Edge::Left | Edge::Right) {
|
||||||
|
from.1 + (to.1 - from.1) * fraction
|
||||||
|
} else {
|
||||||
|
from.0 + (to.0 - from.0) * fraction
|
||||||
|
};
|
||||||
|
(fraction >= 0.0 && fraction <= 1.0 && along >= start && along < end).then_some(fraction)
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Session {
|
||||||
|
owner: u64,
|
||||||
|
capabilities: u32,
|
||||||
|
barriers: Vec<(Barrier, Edge)>,
|
||||||
|
enabled: bool,
|
||||||
|
closed: bool,
|
||||||
|
activation: Option<u32>,
|
||||||
|
events: VecDeque<Value>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub(crate) struct InputCapture {
|
||||||
|
sessions: HashMap<u64, Session>,
|
||||||
|
zones: Vec<Zone>,
|
||||||
|
zone_set: u32,
|
||||||
|
next_session: u64,
|
||||||
|
next_activation: u32,
|
||||||
|
active: Option<u64>,
|
||||||
|
last_absolute: Option<(f64, f64)>,
|
||||||
|
pub(crate) restore_focus: bool,
|
||||||
|
waiters: HashMap<u64, (u64, u64, SyncSender<Response>)>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl InputCapture {
|
||||||
|
pub(crate) fn update_zones(&mut self, mut zones: Vec<Zone>) {
|
||||||
|
zones.sort_by_key(|zone| (zone.x, zone.y, zone.width, zone.height));
|
||||||
|
zones.dedup();
|
||||||
|
if self.zones == zones {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self.zones = zones;
|
||||||
|
self.zone_set = self.zone_set.wrapping_add(1);
|
||||||
|
self.disable_all();
|
||||||
|
let ids = self.sessions.keys().copied().collect::<Vec<_>>();
|
||||||
|
for id in ids {
|
||||||
|
self.sessions.get_mut(&id).unwrap().barriers.clear();
|
||||||
|
self.enqueue(id, json!({"type":"zones", "zone_set":self.zone_set}));
|
||||||
|
}
|
||||||
|
self.flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn register(&mut self, owner: u64, capabilities: u32) -> Result<u64, String> {
|
||||||
|
if self.sessions.len() >= MAX_SESSIONS || capabilities == 0 || capabilities & !3 != 0 {
|
||||||
|
return Err("Unsupported capabilities or input capture session limit reached".into());
|
||||||
|
}
|
||||||
|
self.next_session = self
|
||||||
|
.next_session
|
||||||
|
.checked_add(1)
|
||||||
|
.ok_or("Input capture IDs exhausted")?;
|
||||||
|
let id = self.next_session;
|
||||||
|
self.sessions.insert(
|
||||||
|
id,
|
||||||
|
Session {
|
||||||
|
owner,
|
||||||
|
capabilities,
|
||||||
|
barriers: Vec::new(),
|
||||||
|
enabled: false,
|
||||||
|
closed: false,
|
||||||
|
activation: None,
|
||||||
|
events: VecDeque::new(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
Ok(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn zones(&self, id: u64) -> Result<Value, String> {
|
||||||
|
self.session(id)?;
|
||||||
|
Ok(
|
||||||
|
json!({"zone_set":self.zone_set, "zones":self.zones.iter().map(|zone|
|
||||||
|
(zone.width, zone.height, zone.x, zone.y)).collect::<Vec<_>>()}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn session(&self, id: u64) -> Result<&Session, String> {
|
||||||
|
self.sessions
|
||||||
|
.get(&id)
|
||||||
|
.ok_or("Unknown input capture session".into())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn set_barriers(
|
||||||
|
&mut self,
|
||||||
|
id: u64,
|
||||||
|
zone_set: u32,
|
||||||
|
barriers: Vec<Barrier>,
|
||||||
|
) -> Result<Vec<u32>, String> {
|
||||||
|
self.session(id)?;
|
||||||
|
if barriers.len() > MAX_BARRIERS {
|
||||||
|
return Err("Too many pointer barriers".into());
|
||||||
|
}
|
||||||
|
self.disable(id)?;
|
||||||
|
let mut accepted = Vec::new();
|
||||||
|
let mut failed = Vec::new();
|
||||||
|
let mut counts = HashMap::new();
|
||||||
|
for barrier in &barriers {
|
||||||
|
*counts.entry(barrier.id).or_insert(0) += 1;
|
||||||
|
}
|
||||||
|
for barrier in barriers {
|
||||||
|
let kind = edge(&barrier, &self.zones);
|
||||||
|
if zone_set != self.zone_set || counts[&barrier.id] != 1 || kind.is_none() {
|
||||||
|
failed.push(barrier.id);
|
||||||
|
} else {
|
||||||
|
accepted.push((barrier, kind.unwrap()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.sessions.get_mut(&id).unwrap().barriers = accepted;
|
||||||
|
Ok(failed)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn enable(&mut self, id: u64) -> Result<(), String> {
|
||||||
|
let session = self
|
||||||
|
.sessions
|
||||||
|
.get_mut(&id)
|
||||||
|
.ok_or("Unknown input capture session")?;
|
||||||
|
if session.closed {
|
||||||
|
return Err("Input capture session is closed".into());
|
||||||
|
}
|
||||||
|
session.enabled = true;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn has_sessions(&self) -> bool {
|
||||||
|
!self.sessions.is_empty()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn active(&self) -> bool {
|
||||||
|
self.active.is_some()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn captures(&self, capability: u32) -> bool {
|
||||||
|
self.active
|
||||||
|
.and_then(|id| self.sessions.get(&id))
|
||||||
|
.is_some_and(|session| session.capabilities & capability != 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn motion(&mut self, from: (f64, f64), to: (f64, f64), allowed: bool) -> bool {
|
||||||
|
if self.active() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if !allowed
|
||||||
|
|| ![from.0, from.1, to.0, to.1]
|
||||||
|
.iter()
|
||||||
|
.all(|value| value.is_finite())
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
let hit = self
|
||||||
|
.sessions
|
||||||
|
.iter()
|
||||||
|
.filter(|(_, session)| session.enabled && !session.closed)
|
||||||
|
.flat_map(|(id, session)| {
|
||||||
|
session.barriers.iter().filter_map(move |(barrier, kind)| {
|
||||||
|
crossed(barrier, *kind, from, to).map(|fraction| (*id, barrier.id, fraction))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.min_by(|left, right| left.2.total_cmp(&right.2).then(left.0.cmp(&right.0)));
|
||||||
|
let Some((id, barrier, _)) = hit else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
self.next_activation = self.next_activation.wrapping_add(1);
|
||||||
|
let activation = self.next_activation;
|
||||||
|
self.active = Some(id);
|
||||||
|
let session = self.sessions.get_mut(&id).unwrap();
|
||||||
|
session.activation = Some(activation);
|
||||||
|
self.enqueue(
|
||||||
|
id,
|
||||||
|
json!({"type":"activated", "activation_id":activation,
|
||||||
|
"barrier_id":barrier, "cursor_position":to}),
|
||||||
|
);
|
||||||
|
self.flush();
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
fn enqueue(&mut self, id: u64, event: Value) {
|
||||||
|
let Some(session) = self.sessions.get_mut(&id) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
if session.closed {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if session.events.len() >= MAX_EVENTS {
|
||||||
|
session.events.clear();
|
||||||
|
session.events.push_back(json!({"type":"closed"}));
|
||||||
|
session.closed = true;
|
||||||
|
session.enabled = false;
|
||||||
|
session.activation = None;
|
||||||
|
if self.active == Some(id) {
|
||||||
|
self.active = None;
|
||||||
|
self.last_absolute = None;
|
||||||
|
self.restore_focus = true;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
session.events.push_back(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn push(&mut self, event: Value) {
|
||||||
|
if let Some(id) = self.active {
|
||||||
|
self.enqueue(id, event);
|
||||||
|
self.flush();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn absolute_delta(&mut self, point: (f64, f64)) -> (f64, f64) {
|
||||||
|
let previous = self.last_absolute.replace(point).unwrap_or(point);
|
||||||
|
(point.0 - previous.0, point.1 - previous.1)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn close_all(&mut self) {
|
||||||
|
if self.active.take().is_some() {
|
||||||
|
self.restore_focus = true;
|
||||||
|
}
|
||||||
|
self.last_absolute = None;
|
||||||
|
self.sessions.clear();
|
||||||
|
self.flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn release(&mut self, id: u64, activation: Option<u32>) -> Result<(), String> {
|
||||||
|
let session = self.session(id)?;
|
||||||
|
if activation.is_some() && session.activation != activation {
|
||||||
|
return Err("Stale input capture activation".into());
|
||||||
|
}
|
||||||
|
if self.active == Some(id) {
|
||||||
|
self.active = None;
|
||||||
|
self.last_absolute = None;
|
||||||
|
self.restore_focus = true;
|
||||||
|
let session = self.sessions.get_mut(&id).unwrap();
|
||||||
|
let activation = session.activation.take();
|
||||||
|
self.enqueue(
|
||||||
|
id,
|
||||||
|
json!({"type":"deactivated", "activation_id":activation}),
|
||||||
|
);
|
||||||
|
self.flush();
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn disable(&mut self, id: u64) -> Result<(), String> {
|
||||||
|
self.release(id, None)?;
|
||||||
|
let session = self.sessions.get_mut(&id).unwrap();
|
||||||
|
if session.enabled {
|
||||||
|
session.enabled = false;
|
||||||
|
self.enqueue(id, json!({"type":"disabled"}));
|
||||||
|
}
|
||||||
|
self.flush();
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn disable_all(&mut self) {
|
||||||
|
let ids = self.sessions.keys().copied().collect::<Vec<_>>();
|
||||||
|
for id in ids {
|
||||||
|
let _ = self.disable(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn valid_position(&self, point: (f64, f64)) -> bool {
|
||||||
|
point.0.is_finite()
|
||||||
|
&& point.1.is_finite()
|
||||||
|
&& self.zones.iter().any(|zone| zone.contains(point))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn watch(
|
||||||
|
&mut self,
|
||||||
|
owner: u64,
|
||||||
|
request: u64,
|
||||||
|
id: u64,
|
||||||
|
response: SyncSender<Response>,
|
||||||
|
) {
|
||||||
|
if !self.sessions.contains_key(&id)
|
||||||
|
|| self.waiters.len() >= MAX_SESSIONS
|
||||||
|
|| self.waiters.values().any(|(_, token, _)| *token == id)
|
||||||
|
{
|
||||||
|
let _ = response.try_send(Response::error(
|
||||||
|
request,
|
||||||
|
"invalid_capture",
|
||||||
|
"Unknown or already watched input capture session",
|
||||||
|
));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
self.waiters.insert(owner, (request, id, response));
|
||||||
|
self.flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn flush(&mut self) {
|
||||||
|
let ready = self
|
||||||
|
.waiters
|
||||||
|
.iter()
|
||||||
|
.filter_map(|(owner, (_, id, _))| {
|
||||||
|
self.sessions
|
||||||
|
.get(id)
|
||||||
|
.is_none_or(|session| !session.events.is_empty())
|
||||||
|
.then_some(*owner)
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
for owner in ready {
|
||||||
|
let (request, id, response) = self.waiters.remove(&owner).unwrap();
|
||||||
|
let result = if let Some(session) = self.sessions.get_mut(&id) {
|
||||||
|
json!({"events":session.events.drain(..).collect::<Vec<_>>()})
|
||||||
|
} else {
|
||||||
|
json!({"events":[{"type":"closed"}]})
|
||||||
|
};
|
||||||
|
let _ = response.try_send(Response::success(request, result));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn remove_owner(&mut self, owner: u64) {
|
||||||
|
self.waiters.remove(&owner);
|
||||||
|
let ids = self
|
||||||
|
.sessions
|
||||||
|
.iter()
|
||||||
|
.filter_map(|(id, session)| (session.owner == owner).then_some(*id))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
for id in ids {
|
||||||
|
let _ = self.release(id, None);
|
||||||
|
self.sessions.remove(&id);
|
||||||
|
}
|
||||||
|
self.flush();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
fn zones() -> Vec<Zone> {
|
||||||
|
vec![
|
||||||
|
Zone {
|
||||||
|
width: 1920,
|
||||||
|
height: 1080,
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
},
|
||||||
|
Zone {
|
||||||
|
width: 1920,
|
||||||
|
height: 1080,
|
||||||
|
x: 1920,
|
||||||
|
y: 0,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn barriers_use_inclusive_pixel_span_and_only_outer_union_edges() {
|
||||||
|
let zones = zones();
|
||||||
|
for position in [
|
||||||
|
[0, 0, 1919, 0],
|
||||||
|
[0, 1080, 1919, 1080],
|
||||||
|
[1920, 0, 3839, 0],
|
||||||
|
[3840, 0, 3840, 1079],
|
||||||
|
[0, 0, 0, 1079],
|
||||||
|
] {
|
||||||
|
assert!(
|
||||||
|
edge(&Barrier { id: 1, position }, &zones).is_some(),
|
||||||
|
"{position:?}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
for position in [
|
||||||
|
[1920, 0, 1920, 1079],
|
||||||
|
[0, 0, 1920, 0],
|
||||||
|
[5, 10, 8, 13],
|
||||||
|
[50, 50, 50, 200],
|
||||||
|
[0, 5, 0, 4],
|
||||||
|
] {
|
||||||
|
assert!(
|
||||||
|
edge(&Barrier { id: 1, position }, &zones).is_none(),
|
||||||
|
"{position:?}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let barrier = Barrier {
|
||||||
|
id: 1,
|
||||||
|
position: [3840, 0, 3840, 1079],
|
||||||
|
};
|
||||||
|
assert!(crossed(&barrier, Edge::Right, (3830.0, 1079.5), (3900.0, 1079.5)).is_some());
|
||||||
|
assert!(crossed(&barrier, Edge::Right, (3830.0, 1080.0), (3900.0, 1080.0)).is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn absolute_samples_report_incremental_motion_and_reset_on_release() {
|
||||||
|
let mut capture = InputCapture::default();
|
||||||
|
assert_eq!(capture.absolute_delta((100.0, 50.0)), (0.0, 0.0));
|
||||||
|
assert_eq!(capture.absolute_delta((110.0, 55.0)), (10.0, 5.0));
|
||||||
|
assert_eq!(capture.absolute_delta((120.0, 60.0)), (10.0, 5.0));
|
||||||
|
capture.close_all();
|
||||||
|
assert_eq!(capture.absolute_delta((600.0, 700.0)), (0.0, 0.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn duplicate_ids_are_rejected_without_installing_a_barrier() {
|
||||||
|
let mut capture = InputCapture::default();
|
||||||
|
capture.update_zones(zones());
|
||||||
|
let id = capture.register(1, 3).unwrap();
|
||||||
|
let barrier = Barrier {
|
||||||
|
id: 9,
|
||||||
|
position: [3840, 0, 3840, 1079],
|
||||||
|
};
|
||||||
|
let failed = capture
|
||||||
|
.set_barriers(id, capture.zone_set, vec![barrier.clone(), barrier])
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(failed, vec![9, 9]);
|
||||||
|
assert!(capture.sessions[&id].barriers.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn stalled_control_and_input_queues_close_and_restore_local_focus() {
|
||||||
|
let mut capture = InputCapture::default();
|
||||||
|
capture.update_zones(zones());
|
||||||
|
let id = capture.register(1, 3).unwrap();
|
||||||
|
capture
|
||||||
|
.set_barriers(
|
||||||
|
id,
|
||||||
|
capture.zone_set,
|
||||||
|
vec![Barrier {
|
||||||
|
id: 1,
|
||||||
|
position: [3840, 0, 3840, 1079],
|
||||||
|
}],
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
capture.enable(id).unwrap();
|
||||||
|
assert!(capture.motion((3839.0, 50.0), (3900.0, 50.0), true));
|
||||||
|
for _ in 0..MAX_EVENTS + 10 {
|
||||||
|
capture.push(json!({"type":"motion", "x":1.0, "y":0.0}));
|
||||||
|
}
|
||||||
|
assert!(!capture.active());
|
||||||
|
assert!(capture.restore_focus);
|
||||||
|
assert!(capture.enable(id).is_err());
|
||||||
|
assert_eq!(capture.sessions[&id].events.len(), 1);
|
||||||
|
assert_eq!(capture.sessions[&id].events[0]["type"], "closed");
|
||||||
|
|
||||||
|
let id = capture.register(2, 2).unwrap();
|
||||||
|
for _ in 0..MAX_EVENTS + 10 {
|
||||||
|
capture.enable(id).ok();
|
||||||
|
capture.disable(id).unwrap();
|
||||||
|
}
|
||||||
|
assert_eq!(capture.sessions[&id].events.len(), 1);
|
||||||
|
assert!(capture.sessions[&id].closed);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn release_preserves_enable_but_topology_changes_disable_and_invalidate() {
|
||||||
|
let mut capture = InputCapture::default();
|
||||||
|
capture.update_zones(zones());
|
||||||
|
let id = capture.register(9, 3).unwrap();
|
||||||
|
let barrier = Barrier {
|
||||||
|
id: 1,
|
||||||
|
position: [3840, 0, 3840, 1079],
|
||||||
|
};
|
||||||
|
assert!(
|
||||||
|
capture
|
||||||
|
.set_barriers(id, capture.zone_set, vec![barrier])
|
||||||
|
.unwrap()
|
||||||
|
.is_empty()
|
||||||
|
);
|
||||||
|
capture.enable(id).unwrap();
|
||||||
|
assert!(capture.motion((3839.0, 50.0), (3900.0, 50.0), true));
|
||||||
|
let activation = capture.sessions[&id].activation.unwrap();
|
||||||
|
assert!(
|
||||||
|
capture
|
||||||
|
.release(id, Some(activation.wrapping_add(1)))
|
||||||
|
.is_err()
|
||||||
|
);
|
||||||
|
capture.release(id, Some(activation)).unwrap();
|
||||||
|
assert!(capture.sessions[&id].enabled);
|
||||||
|
capture.update_zones(vec![zones()[0]]);
|
||||||
|
assert!(!capture.active());
|
||||||
|
assert!(!capture.sessions[&id].enabled);
|
||||||
|
assert!(capture.sessions[&id].barriers.is_empty());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl crate::Ferese {
|
||||||
|
pub(crate) fn refresh_input_capture_zones(&mut self) {
|
||||||
|
if !self.input_capture.has_sessions() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let active = self.input_capture.active();
|
||||||
|
let zones = self
|
||||||
|
.space
|
||||||
|
.outputs()
|
||||||
|
.filter_map(|output| {
|
||||||
|
let rect = self.space.output_geometry(output)?;
|
||||||
|
Some(Zone {
|
||||||
|
width: u32::try_from(rect.size.w).ok()?,
|
||||||
|
height: u32::try_from(rect.size.h).ok()?,
|
||||||
|
x: rect.loc.x,
|
||||||
|
y: rect.loc.y,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
self.input_capture.update_zones(zones);
|
||||||
|
if active && !self.input_capture.active() {
|
||||||
|
self.restore_input_capture_focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn restore_input_capture_focus(&mut self) {
|
||||||
|
self.input_capture.restore_focus = false;
|
||||||
|
self.cursor_redraw_pending = true;
|
||||||
|
if !self.session_lock.active {
|
||||||
|
self.restore_keyboard_focus();
|
||||||
|
if let Some(pointer) = self.seat.get_pointer() {
|
||||||
|
let location = pointer.current_location();
|
||||||
|
pointer.motion(
|
||||||
|
self,
|
||||||
|
self.surface_under(location),
|
||||||
|
&smithay::input::pointer::MotionEvent {
|
||||||
|
location,
|
||||||
|
serial: smithay::utils::SERIAL_COUNTER.next_serial(),
|
||||||
|
time: 0,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
pointer.frame(self);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn capture_pointer_motion(
|
||||||
|
&mut self,
|
||||||
|
from: smithay::utils::Point<f64, smithay::utils::Logical>,
|
||||||
|
to: smithay::utils::Point<f64, smithay::utils::Logical>,
|
||||||
|
absolute: bool,
|
||||||
|
) -> bool {
|
||||||
|
let pointer = self.seat.get_pointer().expect("seat has pointer");
|
||||||
|
let active = self.input_capture.active();
|
||||||
|
let allowed = !self.session_lock.active
|
||||||
|
&& !self.overview.is_presenting()
|
||||||
|
&& !pointer.is_grabbed()
|
||||||
|
&& !self.swipe.active()
|
||||||
|
&& self
|
||||||
|
.seat
|
||||||
|
.get_keyboard()
|
||||||
|
.is_none_or(|keyboard| !keyboard.is_grabbed())
|
||||||
|
&& self
|
||||||
|
.seat
|
||||||
|
.get_touch()
|
||||||
|
.is_none_or(|touch| !touch.is_grabbed())
|
||||||
|
&& !self.seat.keyboard_shortcuts_inhibited();
|
||||||
|
if !self
|
||||||
|
.input_capture
|
||||||
|
.motion((from.x, from.y), (to.x, to.y), allowed)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
let delta = if absolute {
|
||||||
|
self.input_capture.absolute_delta((to.x, to.y))
|
||||||
|
} else {
|
||||||
|
(to.x - from.x, to.y - from.y)
|
||||||
|
};
|
||||||
|
if !active {
|
||||||
|
self.cursor_redraw_pending = true;
|
||||||
|
pointer.motion(
|
||||||
|
self,
|
||||||
|
None,
|
||||||
|
&smithay::input::pointer::MotionEvent {
|
||||||
|
location: from,
|
||||||
|
serial: smithay::utils::SERIAL_COUNTER.next_serial(),
|
||||||
|
time: 0,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
pointer.frame(self);
|
||||||
|
if self.input_capture.captures(1) {
|
||||||
|
let keyboard = self.seat.get_keyboard().expect("seat has keyboard");
|
||||||
|
let keys = keyboard
|
||||||
|
.pressed_keys()
|
||||||
|
.iter()
|
||||||
|
.filter(|key| !self.intercepted_keys.contains(key))
|
||||||
|
.map(|key| key.raw().saturating_sub(8))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
keyboard.set_focus(self, None, smithay::utils::SERIAL_COUNTER.next_serial());
|
||||||
|
self.input_capture.push(json!({"type":"keys", "keys":keys}));
|
||||||
|
}
|
||||||
|
} else if self.input_capture.captures(2) {
|
||||||
|
self.input_capture
|
||||||
|
.push(json!({"type":"motion", "x":delta.0, "y":delta.1}));
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
+135
-1
@@ -47,6 +47,7 @@ const DEADLINE_IDLE: Duration = Duration::from_secs(1);
|
|||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
struct IpcCall {
|
struct IpcCall {
|
||||||
|
native_portal: bool,
|
||||||
owner: u64,
|
owner: u64,
|
||||||
request: Request,
|
request: Request,
|
||||||
response: SyncSender<Response>,
|
response: SyncSender<Response>,
|
||||||
@@ -121,13 +122,42 @@ pub(crate) fn init(
|
|||||||
let call = match event {
|
let call = match event {
|
||||||
IpcEvent::Call(call) => call,
|
IpcEvent::Call(call) => call,
|
||||||
IpcEvent::Closed(owner) => {
|
IpcEvent::Closed(owner) => {
|
||||||
|
let captured = state.input_capture.active();
|
||||||
|
state.input_capture.remove_owner(owner);
|
||||||
|
if captured && !state.input_capture.active() {
|
||||||
|
state.restore_input_capture_focus();
|
||||||
|
}
|
||||||
state.portal_shortcuts.remove(owner);
|
state.portal_shortcuts.remove(owner);
|
||||||
state.portal_session.remove(owner);
|
state.portal_session.remove(owner);
|
||||||
state.refresh_idle_inhibition();
|
state.refresh_idle_inhibition();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
if call.request.command == "session-watch" {
|
if call.request.command.starts_with("input-capture-") && !call.native_portal {
|
||||||
|
let _ = call.response.try_send(Response::error(
|
||||||
|
call.request.id,
|
||||||
|
"access_denied",
|
||||||
|
"Input capture is restricted to the native portal",
|
||||||
|
));
|
||||||
|
} else if call.request.command == "input-capture-watch" {
|
||||||
|
if let Err(error) = validate_request(&call.request) {
|
||||||
|
let _ = call.response.try_send(Response::error(
|
||||||
|
call.request.id,
|
||||||
|
error.code,
|
||||||
|
error.message,
|
||||||
|
));
|
||||||
|
} else if let Some(id) = call.request.args["session"].as_u64() {
|
||||||
|
state
|
||||||
|
.input_capture
|
||||||
|
.watch(call.owner, call.request.id, id, call.response);
|
||||||
|
} else {
|
||||||
|
let _ = call.response.try_send(Response::error(
|
||||||
|
call.request.id,
|
||||||
|
"invalid_argument",
|
||||||
|
"Missing capture session",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
} else if call.request.command == "session-watch" {
|
||||||
if let Err(error) = validate_request(&call.request) {
|
if let Err(error) = validate_request(&call.request) {
|
||||||
let _ = call.response.send(Response::error(
|
let _ = call.response.send(Response::error(
|
||||||
call.request.id,
|
call.request.id,
|
||||||
@@ -310,6 +340,7 @@ fn serve_connection(
|
|||||||
signal: LoopSignal,
|
signal: LoopSignal,
|
||||||
owner: u64,
|
owner: u64,
|
||||||
) {
|
) {
|
||||||
|
let native_portal = crate::handlers::window_capture::is_portal(&stream);
|
||||||
let _connection = IpcConnection {
|
let _connection = IpcConnection {
|
||||||
owner,
|
owner,
|
||||||
sender: sender.clone(),
|
sender: sender.clone(),
|
||||||
@@ -337,6 +368,7 @@ fn serve_connection(
|
|||||||
let screenshot_requested =
|
let screenshot_requested =
|
||||||
matches!(request.command.as_str(), "screenshot" | "screenshot-window");
|
matches!(request.command.as_str(), "screenshot" | "screenshot-window");
|
||||||
let call = IpcEvent::Call(IpcCall {
|
let call = IpcEvent::Call(IpcCall {
|
||||||
|
native_portal,
|
||||||
owner,
|
owner,
|
||||||
request,
|
request,
|
||||||
response,
|
response,
|
||||||
@@ -417,6 +449,8 @@ impl Ferese {
|
|||||||
&& !matches!(
|
&& !matches!(
|
||||||
request.command.as_str(),
|
request.command.as_str(),
|
||||||
"portal-shortcuts-poll"
|
"portal-shortcuts-poll"
|
||||||
|
| "input-capture-disable"
|
||||||
|
| "input-capture-release"
|
||||||
| "get-session-state"
|
| "get-session-state"
|
||||||
| "portal-inhibit"
|
| "portal-inhibit"
|
||||||
| "portal-monitor-register"
|
| "portal-monitor-register"
|
||||||
@@ -525,6 +559,106 @@ impl Ferese {
|
|||||||
"portal-shortcuts-poll" => {
|
"portal-shortcuts-poll" => {
|
||||||
return Ok(self.portal_shortcuts.poll(owner, self.session_lock.active));
|
return Ok(self.portal_shortcuts.poll(owner, self.session_lock.active));
|
||||||
}
|
}
|
||||||
|
"input-capture-register" => {
|
||||||
|
if self.session_lock.active {
|
||||||
|
return Err(CommandError::new("session_locked", "Session is locked"));
|
||||||
|
}
|
||||||
|
let id = self
|
||||||
|
.input_capture
|
||||||
|
.register(owner, u32_arg(args, "capabilities")?)
|
||||||
|
.map_err(|e| CommandError::new("invalid_capture", e))?;
|
||||||
|
self.refresh_input_capture_zones();
|
||||||
|
let keyboard = self.seat.get_keyboard().expect("seat has keyboard");
|
||||||
|
let keymap = keyboard.with_xkb_state(self, |state| {
|
||||||
|
let xkb = state.xkb().lock().unwrap();
|
||||||
|
// SAFETY: the ref-counted keymap does not escape the locked Xkb;
|
||||||
|
// only its owned string is retained for the EIS keyboard.
|
||||||
|
unsafe { xkb.keymap() }
|
||||||
|
.get_as_string(smithay::input::keyboard::xkb::KEYMAP_FORMAT_TEXT_V1)
|
||||||
|
});
|
||||||
|
return Ok(json!({"session":id, "keymap":keymap}));
|
||||||
|
}
|
||||||
|
"input-capture-zones" => {
|
||||||
|
self.refresh_input_capture_zones();
|
||||||
|
return self
|
||||||
|
.input_capture
|
||||||
|
.zones(
|
||||||
|
args["session"].as_u64().ok_or_else(|| {
|
||||||
|
CommandError::new("invalid_capture", "Missing session")
|
||||||
|
})?,
|
||||||
|
)
|
||||||
|
.map_err(|e| CommandError::new("invalid_capture", e));
|
||||||
|
}
|
||||||
|
"input-capture-barriers" => {
|
||||||
|
self.refresh_input_capture_zones();
|
||||||
|
let id = args["session"]
|
||||||
|
.as_u64()
|
||||||
|
.ok_or_else(|| CommandError::new("invalid_capture", "Missing session"))?;
|
||||||
|
let barriers = serde_json::from_value(args["barriers"].clone())
|
||||||
|
.map_err(|_| CommandError::new("invalid_capture", "Invalid barriers"))?;
|
||||||
|
let active = self.input_capture.active();
|
||||||
|
let failed = self
|
||||||
|
.input_capture
|
||||||
|
.set_barriers(id, u32_arg(args, "zone_set")?, barriers)
|
||||||
|
.map_err(|e| CommandError::new("invalid_capture", e))?;
|
||||||
|
if active && !self.input_capture.active() {
|
||||||
|
self.restore_input_capture_focus();
|
||||||
|
}
|
||||||
|
return Ok(json!({"failed_barriers":failed}));
|
||||||
|
}
|
||||||
|
"input-capture-enable" => {
|
||||||
|
self.refresh_input_capture_zones();
|
||||||
|
let id = args["session"]
|
||||||
|
.as_u64()
|
||||||
|
.ok_or_else(|| CommandError::new("invalid_capture", "Missing session"))?;
|
||||||
|
self.input_capture
|
||||||
|
.enable(id)
|
||||||
|
.map_err(|e| CommandError::new("invalid_capture", e))?;
|
||||||
|
}
|
||||||
|
"input-capture-disable" | "input-capture-release" => {
|
||||||
|
let id = args["session"]
|
||||||
|
.as_u64()
|
||||||
|
.ok_or_else(|| CommandError::new("invalid_capture", "Missing session"))?;
|
||||||
|
let position = args
|
||||||
|
.get("cursor_position")
|
||||||
|
.filter(|position| !position.is_null())
|
||||||
|
.map(|position| {
|
||||||
|
let position = position
|
||||||
|
.as_array()
|
||||||
|
.filter(|values| values.len() == 2)
|
||||||
|
.ok_or("Invalid cursor position")?;
|
||||||
|
let point = (
|
||||||
|
position[0].as_f64().ok_or("Invalid cursor position")?,
|
||||||
|
position[1].as_f64().ok_or("Invalid cursor position")?,
|
||||||
|
);
|
||||||
|
self.input_capture
|
||||||
|
.valid_position(point)
|
||||||
|
.then_some(point)
|
||||||
|
.ok_or("Cursor position is outside the capture zones")
|
||||||
|
})
|
||||||
|
.transpose()
|
||||||
|
.map_err(|e| CommandError::new("invalid_capture", e))?;
|
||||||
|
let active = self.input_capture.active();
|
||||||
|
let result = if command == "input-capture-disable" {
|
||||||
|
self.input_capture.disable(id)
|
||||||
|
} else {
|
||||||
|
self.input_capture.release(
|
||||||
|
id,
|
||||||
|
args["activation_id"]
|
||||||
|
.as_u64()
|
||||||
|
.and_then(|id| u32::try_from(id).ok()),
|
||||||
|
)
|
||||||
|
};
|
||||||
|
result.map_err(|e| CommandError::new("invalid_capture", e))?;
|
||||||
|
if active && !self.input_capture.active() {
|
||||||
|
if let Some(position) = position
|
||||||
|
&& let Some(pointer) = self.seat.get_pointer()
|
||||||
|
{
|
||||||
|
pointer.set_location(position.into());
|
||||||
|
}
|
||||||
|
self.restore_input_capture_focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
"exit" => {} // The IPC worker stops the loop after writing the response.
|
"exit" => {} // The IPC worker stops the loop after writing the response.
|
||||||
"request-logout" => self.request_logout_confirmation(),
|
"request-logout" => self.request_logout_confirmation(),
|
||||||
"reload-config" => self
|
"reload-config" => self
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ mod gestures;
|
|||||||
mod grabs;
|
mod grabs;
|
||||||
mod handlers;
|
mod handlers;
|
||||||
mod input;
|
mod input;
|
||||||
|
mod input_capture;
|
||||||
mod ipc;
|
mod ipc;
|
||||||
mod metrics;
|
mod metrics;
|
||||||
mod overview;
|
mod overview;
|
||||||
@@ -137,6 +138,9 @@ fn main() -> Result<(), Box<dyn Error>> {
|
|||||||
}
|
}
|
||||||
// All input/Wayland callbacks have returned, releasing seat locks.
|
// All input/Wayland callbacks have returned, releasing seat locks.
|
||||||
// Coalesce cursor changes and redraw here, never inside cursor_image.
|
// Coalesce cursor changes and redraw here, never inside cursor_image.
|
||||||
|
if state.input_capture.restore_focus {
|
||||||
|
state.restore_input_capture_focus();
|
||||||
|
}
|
||||||
let wallpaper_retry = state.wallpaper.take_retry_wakeup();
|
let wallpaper_retry = state.wallpaper.take_retry_wakeup();
|
||||||
if std::mem::take(&mut state.cursor_redraw_pending) || wallpaper_changed || wallpaper_retry
|
if std::mem::take(&mut state.cursor_redraw_pending) || wallpaper_changed || wallpaper_retry
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -123,6 +123,7 @@ impl SessionLockHandler for Ferese {
|
|||||||
}
|
}
|
||||||
self.cancel_logout_confirmation();
|
self.cancel_logout_confirmation();
|
||||||
self.session_lock.active = true;
|
self.session_lock.active = true;
|
||||||
|
self.input_capture.disable_all();
|
||||||
self.portal_session.set_locked(true);
|
self.portal_session.set_locked(true);
|
||||||
self.session_lock.owner = Some(confirmation.ext_session_lock().clone());
|
self.session_lock.owner = Some(confirmation.ext_session_lock().clone());
|
||||||
self.session_lock.confirmation = Some(confirmation);
|
self.session_lock.confirmation = Some(confirmation);
|
||||||
|
|||||||
@@ -232,6 +232,7 @@ pub struct Ferese {
|
|||||||
pub display_handle: DisplayHandle,
|
pub display_handle: DisplayHandle,
|
||||||
pub loop_signal: LoopSignal,
|
pub loop_signal: LoopSignal,
|
||||||
pub(crate) loop_handle: smithay::reexports::calloop::LoopHandle<'static, Self>,
|
pub(crate) loop_handle: smithay::reexports::calloop::LoopHandle<'static, Self>,
|
||||||
|
pub(crate) input_capture: crate::input_capture::InputCapture,
|
||||||
pub(crate) portal_session: crate::portal_session::PortalSession,
|
pub(crate) portal_session: crate::portal_session::PortalSession,
|
||||||
pub space: Space<Window>,
|
pub space: Space<Window>,
|
||||||
pub workspaces: WorkspaceSet,
|
pub workspaces: WorkspaceSet,
|
||||||
@@ -453,6 +454,7 @@ impl Ferese {
|
|||||||
loop_signal: event_loop.get_signal(),
|
loop_signal: event_loop.get_signal(),
|
||||||
loop_handle: event_loop.handle(),
|
loop_handle: event_loop.handle(),
|
||||||
portal_session: Default::default(),
|
portal_session: Default::default(),
|
||||||
|
input_capture: Default::default(),
|
||||||
space: Space::default(),
|
space: Space::default(),
|
||||||
workspaces: WorkspaceSet::new(
|
workspaces: WorkspaceSet::new(
|
||||||
config.layout_mode,
|
config.layout_mode,
|
||||||
@@ -582,6 +584,10 @@ impl Ferese {
|
|||||||
|
|
||||||
if let Some(keyboard) = self.seat.get_keyboard() {
|
if let Some(keyboard) = self.seat.get_keyboard() {
|
||||||
if keyboard_changed {
|
if keyboard_changed {
|
||||||
|
self.input_capture.close_all();
|
||||||
|
if self.input_capture.restore_focus {
|
||||||
|
self.restore_input_capture_focus();
|
||||||
|
}
|
||||||
let options = (!config.input_settings.xkb_options.is_empty())
|
let options = (!config.input_settings.xkb_options.is_empty())
|
||||||
.then(|| config.input_settings.xkb_options.join(","));
|
.then(|| config.input_settings.xkb_options.join(","));
|
||||||
|
|
||||||
@@ -896,6 +902,9 @@ impl Ferese {
|
|||||||
if self.session_lock.active {
|
if self.session_lock.active {
|
||||||
return self.lock_surface_under(position);
|
return self.lock_surface_under(position);
|
||||||
}
|
}
|
||||||
|
if self.input_capture.active() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
self.layer_surface_under(position, &[Layer::Overlay, Layer::Top])
|
self.layer_surface_under(position, &[Layer::Overlay, Layer::Top])
|
||||||
.map(|(_, surface, origin)| (surface, origin))
|
.map(|(_, surface, origin)| (surface, origin))
|
||||||
.or_else(|| self.window_surface_under(position))
|
.or_else(|| self.window_surface_under(position))
|
||||||
@@ -1594,6 +1603,7 @@ impl Ferese {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn relayout(&mut self) {
|
pub fn relayout(&mut self) {
|
||||||
|
self.refresh_input_capture_zones();
|
||||||
let visible_workspaces = self.visible_workspace_ids();
|
let visible_workspaces = self.visible_workspace_ids();
|
||||||
self.prune_empty_workspaces(&visible_workspaces);
|
self.prune_empty_workspaces(&visible_workspaces);
|
||||||
if self.session_lock.active {
|
if self.session_lock.active {
|
||||||
@@ -2428,6 +2438,9 @@ impl Ferese {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn focus_direction_with_slide(&mut self, direction: Direction, slide: bool) {
|
fn focus_direction_with_slide(&mut self, direction: Direction, slide: bool) {
|
||||||
|
if self.input_capture.captures(1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if self.focus_overview_direction(direction) {
|
if self.focus_overview_direction(direction) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -3007,6 +3020,9 @@ impl Ferese {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn restore_keyboard_focus(&mut self) {
|
pub(crate) fn restore_keyboard_focus(&mut self) {
|
||||||
|
if self.input_capture.captures(1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if self.session_lock.active {
|
if self.session_lock.active {
|
||||||
self.focus_lock_surface();
|
self.focus_lock_surface();
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -2863,6 +2863,9 @@ fn cursor_elements(
|
|||||||
output_geometry: Rectangle<i32, Logical>,
|
output_geometry: Rectangle<i32, Logical>,
|
||||||
scale: f64,
|
scale: f64,
|
||||||
) -> Vec<AnimatedWindowRenderElement> {
|
) -> Vec<AnimatedWindowRenderElement> {
|
||||||
|
if state.input_capture.active() {
|
||||||
|
return Vec::new();
|
||||||
|
}
|
||||||
let Some(pointer) = state.seat.get_pointer() else {
|
let Some(pointer) = state.seat.get_pointer() else {
|
||||||
return Vec::new();
|
return Vec::new();
|
||||||
};
|
};
|
||||||
|
|||||||
+21
-4
@@ -47,13 +47,20 @@ standard file, print and application dialogs. Install both backends.
|
|||||||
for acknowledgements for up to one second. Power confirmations list blockers
|
for acknowledgements for up to one second. Power confirmations list blockers
|
||||||
and recheck them before acting; authentication failure cancels the query.
|
and recheck them before acting; authentication failure cancels the query.
|
||||||
UserSwitch is recorded, but Ferese currently has no user-switch operation.
|
UserSwitch is recorded, but Ferese currently has no user-switch operation.
|
||||||
|
- **InputCapture:** approved keyboard and pointer capture over a private EIS
|
||||||
|
connection, with standard screen zones and pointer barriers on outside display
|
||||||
|
edges. Capture begins at a configured barrier; releasing it restores local
|
||||||
|
input. Ctrl+Alt+Escape returns control immediately. Locking, topology/keymap
|
||||||
|
changes, receiver disconnects and stalled consumers stop capture. Permission
|
||||||
|
is requested for every session; touch capture and remembered grants are not
|
||||||
|
offered. This is an input receiver, not remote input injection.
|
||||||
- **ScreenCast:** monitor and isolated window sharing with a native picker, PipeWire streams and explicit opt-in
|
- **ScreenCast:** monitor and isolated window sharing with a native picker, PipeWire streams and explicit opt-in
|
||||||
saved permissions. The frontend stores and revokes saved choices. Invalid
|
saved permissions. The frontend stores and revokes saved choices. Invalid
|
||||||
restore data or changed display identity requires fresh consent.
|
restore data or changed display identity requires fresh consent.
|
||||||
See [screen sharing](screen-sharing.md) for lifetime and recording details.
|
See [screen sharing](screen-sharing.md) for lifetime and recording details.
|
||||||
|
|
||||||
Native request dialogs use Ferese's theme and are floating windows. Screenshot,
|
Native request dialogs use Ferese's theme and are floating windows. Screenshot,
|
||||||
PickColor, Wallpaper, USB and ScreenCast consent dialogs use Wayland parent identifiers to establish
|
PickColor, Wallpaper, USB, InputCapture and ScreenCast consent dialogs use Wayland parent identifiers to establish
|
||||||
a transient relationship with the requesting window.
|
a transient relationship with the requesting window.
|
||||||
Cancelling a request or losing the portal frontend terminates its pending helper.
|
Cancelling a request or losing the portal frontend terminates its pending helper.
|
||||||
|
|
||||||
@@ -80,7 +87,7 @@ Matching D-Bus signatures does not establish complete feature parity.
|
|||||||
| GlobalShortcuts | Native sessions, configurable keyboard triggers, conflict checks and activation/release signals |
|
| GlobalShortcuts | Native sessions, configurable keyboard triggers, conflict checks and activation/release signals |
|
||||||
| RemoteDesktop | Deferred: authorized input injection and EIS transport |
|
| RemoteDesktop | Deferred: authorized input injection and EIS transport |
|
||||||
| Clipboard | Deferred with RemoteDesktop |
|
| Clipboard | Deferred with RemoteDesktop |
|
||||||
| InputCapture | Missing zones, pointer barriers and EIS transport |
|
| InputCapture | Native v2 keyboard/pointer consent, zones, outside-edge barriers and private EIS receiver transport |
|
||||||
| Lockdown | Native seven-property policy provider with persisted configuration and change signals |
|
| Lockdown | Native seven-property policy provider with persisted configuration and change signals |
|
||||||
| Inhibit | Native inhibitors and session monitors; UserSwitch recorded until a switch operation exists |
|
| Inhibit | Native inhibitors and session monitors; UserSwitch recorded until a switch operation exists |
|
||||||
|
|
||||||
@@ -95,8 +102,7 @@ only to the native portal executable and checks session opt-in and locking.
|
|||||||
Window resizing renegotiates PipeWire video dimensions. Closing the selected
|
Window resizing renegotiates PipeWire video dimensions. Closing the selected
|
||||||
window ends sharing; its lifetime ID cannot select a replacement app. Window
|
window ends sharing; its lifetime ID cannot select a replacement app. Window
|
||||||
selections require fresh consent and are not saved as display permissions.
|
selections require fresh consent and are not saved as display permissions.
|
||||||
InputCapture still needs native zones, barriers and EIS transport. RemoteDesktop
|
RemoteDesktop and its Clipboard integration remain deferred.
|
||||||
and its Clipboard integration remain deferred.
|
|
||||||
|
|
||||||
## Validation
|
## Validation
|
||||||
|
|
||||||
@@ -155,3 +161,14 @@ closure using a disposable nested compositor and a real PipeWire consumer:
|
|||||||
```sh
|
```sh
|
||||||
FERESE_TEST_WINDOW_STREAM=1 python3 scripts/tests/test_window_stream_isolated.py
|
FERESE_TEST_WINDOW_STREAM=1 python3 scripts/tests/test_window_stream_isolated.py
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Check InputCapture state and its transport against an installed libei receiver:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cargo test --locked -p ferese input_capture::tests
|
||||||
|
cargo test --locked -p xdg-desktop-portal-ferese eis::tests -- --ignored
|
||||||
|
```
|
||||||
|
|
||||||
|
The receiver test uses a private socket pair and synthetic input; it never
|
||||||
|
captures the host keyboard or changes host focus. Real pointer-barrier crossings
|
||||||
|
and interactive consent still need a live session check.
|
||||||
@@ -9,3 +9,4 @@ org.freedesktop.impl.portal.Usb=ferese;
|
|||||||
org.freedesktop.impl.portal.Lockdown=ferese;
|
org.freedesktop.impl.portal.Lockdown=ferese;
|
||||||
org.freedesktop.impl.portal.GlobalShortcuts=ferese;
|
org.freedesktop.impl.portal.GlobalShortcuts=ferese;
|
||||||
org.freedesktop.impl.portal.Inhibit=ferese;
|
org.freedesktop.impl.portal.Inhibit=ferese;
|
||||||
|
org.freedesktop.impl.portal.InputCapture=ferese;
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
[portal]
|
[portal]
|
||||||
DBusName=org.freedesktop.impl.portal.desktop.ferese
|
DBusName=org.freedesktop.impl.portal.desktop.ferese
|
||||||
Interfaces=org.freedesktop.impl.portal.ScreenCast;org.freedesktop.impl.portal.Settings;org.freedesktop.impl.portal.Screenshot;org.freedesktop.impl.portal.Wallpaper;org.freedesktop.impl.portal.Background;org.freedesktop.impl.portal.Usb;org.freedesktop.impl.portal.Lockdown;org.freedesktop.impl.portal.GlobalShortcuts;org.freedesktop.impl.portal.Inhibit;
|
Interfaces=org.freedesktop.impl.portal.ScreenCast;org.freedesktop.impl.portal.Settings;org.freedesktop.impl.portal.Screenshot;org.freedesktop.impl.portal.Wallpaper;org.freedesktop.impl.portal.Background;org.freedesktop.impl.portal.Usb;org.freedesktop.impl.portal.Lockdown;org.freedesktop.impl.portal.GlobalShortcuts;org.freedesktop.impl.portal.Inhibit;org.freedesktop.impl.portal.InputCapture;
|
||||||
UseIn=Ferese;
|
UseIn=Ferese;
|
||||||
@@ -14,6 +14,8 @@ name = "ferese-record"
|
|||||||
path = "src/record_main.rs"
|
path = "src/record_main.rs"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
reis = { version = "0.7.1", features = ["calloop"] }
|
||||||
|
calloop.workspace = true
|
||||||
ferese-protocols = { path = "../crates/ferese-protocols", default-features = false, features = ["client"] }
|
ferese-protocols = { path = "../crates/ferese-protocols", default-features = false, features = ["client"] }
|
||||||
ferese-ipc = { path = "../crates/ferese-ipc" }
|
ferese-ipc = { path = "../crates/ferese-ipc" }
|
||||||
url = "2.5"
|
url = "2.5"
|
||||||
|
|||||||
+10
-2
@@ -54,12 +54,15 @@ pub(crate) struct Cancel {
|
|||||||
impl Cancel {
|
impl Cancel {
|
||||||
pub(crate) fn stop(&self) {
|
pub(crate) fn stop(&self) {
|
||||||
self.stopped.store(true, Ordering::SeqCst);
|
self.stopped.store(true, Ordering::SeqCst);
|
||||||
self.changed.notify_one();
|
self.changed.notify_waiters();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) async fn wait(&self) {
|
pub(crate) async fn wait(&self) {
|
||||||
|
let changed = self.changed.notified();
|
||||||
|
tokio::pin!(changed);
|
||||||
|
changed.as_mut().enable();
|
||||||
if !self.stopped.load(Ordering::SeqCst) {
|
if !self.stopped.load(Ordering::SeqCst) {
|
||||||
self.changed.notified().await;
|
changed.await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -912,6 +915,7 @@ pub async fn run() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
let lockdown = crate::lockdown::Lockdown::new();
|
let lockdown = crate::lockdown::Lockdown::new();
|
||||||
let shortcuts = crate::shortcuts::GlobalShortcuts::new(requests.clone());
|
let shortcuts = crate::shortcuts::GlobalShortcuts::new(requests.clone());
|
||||||
let inhibit = crate::inhibit::Inhibit::default();
|
let inhibit = crate::inhibit::Inhibit::default();
|
||||||
|
let input_capture = crate::input_capture::InputCapture::new(requests.clone());
|
||||||
let connection = zbus::connection::Builder::session()?
|
let connection = zbus::connection::Builder::session()?
|
||||||
.name("org.freedesktop.impl.portal.desktop.ferese")?
|
.name("org.freedesktop.impl.portal.desktop.ferese")?
|
||||||
.serve_at(PATH, backend.clone())?
|
.serve_at(PATH, backend.clone())?
|
||||||
@@ -923,6 +927,7 @@ pub async fn run() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
.serve_at(PATH, lockdown.clone())?
|
.serve_at(PATH, lockdown.clone())?
|
||||||
.serve_at(PATH, shortcuts.clone())?
|
.serve_at(PATH, shortcuts.clone())?
|
||||||
.serve_at(PATH, inhibit.clone())?
|
.serve_at(PATH, inhibit.clone())?
|
||||||
|
.serve_at(PATH, input_capture.clone())?
|
||||||
.serve_at(
|
.serve_at(
|
||||||
"/org/ferese/ScreenRecorder",
|
"/org/ferese/ScreenRecorder",
|
||||||
RecorderControl(backend.clone()),
|
RecorderControl(backend.clone()),
|
||||||
@@ -950,6 +955,9 @@ pub async fn run() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
requests.revoke_stale(owner.as_deref()).await;
|
requests.revoke_stale(owner.as_deref()).await;
|
||||||
shortcuts.revoke_stale(&connection, owner.as_deref()).await;
|
shortcuts.revoke_stale(&connection, owner.as_deref()).await;
|
||||||
inhibit.revoke_stale(&connection, owner.as_deref()).await;
|
inhibit.revoke_stale(&connection, owner.as_deref()).await;
|
||||||
|
input_capture
|
||||||
|
.revoke_stale(&connection, owner.as_deref())
|
||||||
|
.await;
|
||||||
|
|
||||||
let stale: Vec<_> = backend
|
let stale: Vec<_> = backend
|
||||||
.sessions
|
.sessions
|
||||||
|
|||||||
@@ -72,6 +72,15 @@ impl Bridge {
|
|||||||
.map_err(|error| error.to_string())?
|
.map_err(|error| error.to_string())?
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn capture_watch(&self, session: u64) -> Result<Json, String> {
|
||||||
|
self.request(
|
||||||
|
"input-capture-watch",
|
||||||
|
serde_json::json!({"session":session}),
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) async fn closed(&self) {
|
pub(crate) async fn closed(&self) {
|
||||||
use std::io::Read;
|
use std::io::Read;
|
||||||
let Ok(reader) = self.0.shutdown.try_clone() else {
|
let Ok(reader) = self.0.shutdown.try_clone() else {
|
||||||
|
|||||||
@@ -0,0 +1,471 @@
|
|||||||
|
use std::{
|
||||||
|
fs::File,
|
||||||
|
io::Write,
|
||||||
|
os::{
|
||||||
|
fd::{AsFd, OwnedFd},
|
||||||
|
unix::net::UnixStream,
|
||||||
|
},
|
||||||
|
sync::{
|
||||||
|
Arc,
|
||||||
|
atomic::{AtomicBool, Ordering},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
use calloop::{EventLoop, LoopSignal, PostAction, channel};
|
||||||
|
use reis::{
|
||||||
|
calloop::{EisRequestSource, EisRequestSourceEvent},
|
||||||
|
eis,
|
||||||
|
enumflags2::BitFlags,
|
||||||
|
request::{Connection, Device, DeviceCapability, EisRequest},
|
||||||
|
};
|
||||||
|
use serde_json::Value;
|
||||||
|
|
||||||
|
use crate::backend::Cancel;
|
||||||
|
|
||||||
|
pub(crate) struct Worker {
|
||||||
|
sender: channel::SyncSender<Vec<Value>>,
|
||||||
|
signal: LoopSignal,
|
||||||
|
pub(crate) ready: Arc<AtomicBool>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Worker {
|
||||||
|
pub(crate) fn start(
|
||||||
|
capabilities: u32,
|
||||||
|
keymap: &str,
|
||||||
|
cancel: Arc<Cancel>,
|
||||||
|
) -> Result<(Self, OwnedFd), String> {
|
||||||
|
if keymap.is_empty() || keymap.len() > 256 * 1024 {
|
||||||
|
return Err("Invalid captured keyboard map".into());
|
||||||
|
}
|
||||||
|
let mut keymap_file = tempfile::tempfile().map_err(|e| e.to_string())?;
|
||||||
|
keymap_file
|
||||||
|
.write_all(keymap.as_bytes())
|
||||||
|
.and_then(|_| keymap_file.write_all(&[0]))
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
let keymap_size = keymap.len() as u32 + 1;
|
||||||
|
let (server, client) = UnixStream::pair().map_err(|e| e.to_string())?;
|
||||||
|
let ready = Arc::new(AtomicBool::new(false));
|
||||||
|
let thread_ready = ready.clone();
|
||||||
|
let (startup, started) = std::sync::mpsc::sync_channel(1);
|
||||||
|
std::thread::Builder::new()
|
||||||
|
.name("ferese-input-eis".into())
|
||||||
|
.spawn(move || {
|
||||||
|
struct Finish(Arc<Cancel>);
|
||||||
|
impl Drop for Finish {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
self.0.stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let _finish = Finish(cancel);
|
||||||
|
let run = || {
|
||||||
|
let context = eis::Context::new(server).map_err(|e| e.to_string())?;
|
||||||
|
let mut event_loop =
|
||||||
|
EventLoop::<State>::try_new().map_err(|e| e.to_string())?;
|
||||||
|
let signal = event_loop.get_signal();
|
||||||
|
let (sender, receiver) = channel::sync_channel(4);
|
||||||
|
event_loop
|
||||||
|
.handle()
|
||||||
|
.insert_source(receiver, |event, _, state| match event {
|
||||||
|
channel::Event::Msg(events) => state.events(events),
|
||||||
|
channel::Event::Closed => state.signal.stop(),
|
||||||
|
})
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
event_loop
|
||||||
|
.handle()
|
||||||
|
.insert_source(
|
||||||
|
EisRequestSource::new(context, 1),
|
||||||
|
|event, connection, state| {
|
||||||
|
let action = match event {
|
||||||
|
Ok(event) => state.request(connection, event),
|
||||||
|
Err(_) => {
|
||||||
|
state.signal.stop();
|
||||||
|
PostAction::Remove
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if connection.flush().is_err() {
|
||||||
|
state.signal.stop();
|
||||||
|
}
|
||||||
|
Ok(action)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
let mut state = State {
|
||||||
|
signal: signal.clone(),
|
||||||
|
ready: thread_ready.clone(),
|
||||||
|
capabilities,
|
||||||
|
keymap: keymap_file,
|
||||||
|
keymap_size,
|
||||||
|
connection: None,
|
||||||
|
keyboard: None,
|
||||||
|
pointer: None,
|
||||||
|
activation: None,
|
||||||
|
keyboard_ready: false,
|
||||||
|
pointer_ready: false,
|
||||||
|
};
|
||||||
|
startup
|
||||||
|
.send(Ok((sender, signal)))
|
||||||
|
.map_err(|_| "Input transport startup cancelled".to_owned())?;
|
||||||
|
let _ = event_loop.run(None, &mut state, |_| {});
|
||||||
|
if let Some(connection) = state.connection.take() {
|
||||||
|
connection
|
||||||
|
.disconnected(eis::connection::DisconnectReason::Disconnected, None);
|
||||||
|
}
|
||||||
|
state.ready.store(false, Ordering::SeqCst);
|
||||||
|
Ok::<(), String>(())
|
||||||
|
};
|
||||||
|
if let Err(error) = run() {
|
||||||
|
let _ = startup.send(Err(error));
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
let (sender, signal) = started
|
||||||
|
.recv_timeout(std::time::Duration::from_secs(2))
|
||||||
|
.map_err(|_| "Input transport startup timed out".to_owned())??;
|
||||||
|
Ok((
|
||||||
|
Self {
|
||||||
|
sender,
|
||||||
|
signal,
|
||||||
|
ready,
|
||||||
|
},
|
||||||
|
client.into(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn send(&self, events: Vec<Value>) -> Result<(), String> {
|
||||||
|
self.sender
|
||||||
|
.try_send(events)
|
||||||
|
.map_err(|_| "Input transport is closed or stalled".into())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn stop(&self) {
|
||||||
|
self.signal.stop();
|
||||||
|
self.signal.wakeup();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for Worker {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
self.stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct State {
|
||||||
|
signal: LoopSignal,
|
||||||
|
ready: Arc<AtomicBool>,
|
||||||
|
capabilities: u32,
|
||||||
|
keymap: File,
|
||||||
|
keymap_size: u32,
|
||||||
|
connection: Option<Connection>,
|
||||||
|
keyboard: Option<Device>,
|
||||||
|
pointer: Option<Device>,
|
||||||
|
activation: Option<u32>,
|
||||||
|
keyboard_ready: bool,
|
||||||
|
pointer_ready: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl State {
|
||||||
|
fn request(&mut self, connection: &Connection, event: EisRequestSourceEvent) -> PostAction {
|
||||||
|
match event {
|
||||||
|
EisRequestSourceEvent::Connected => {
|
||||||
|
if connection.context_type() != eis::handshake::ContextType::Receiver {
|
||||||
|
connection.disconnected(
|
||||||
|
eis::connection::DisconnectReason::Mode,
|
||||||
|
Some("Input capture requires a receiver context"),
|
||||||
|
);
|
||||||
|
self.signal.stop();
|
||||||
|
return PostAction::Remove;
|
||||||
|
}
|
||||||
|
let mut capabilities = BitFlags::<DeviceCapability>::empty();
|
||||||
|
if self.capabilities & 1 != 0 {
|
||||||
|
capabilities |= DeviceCapability::Keyboard;
|
||||||
|
}
|
||||||
|
if self.capabilities & 2 != 0 {
|
||||||
|
capabilities |= DeviceCapability::Pointer
|
||||||
|
| DeviceCapability::Button
|
||||||
|
| DeviceCapability::Scroll;
|
||||||
|
}
|
||||||
|
let _ = connection.add_seat(Some("Ferese input capture"), capabilities);
|
||||||
|
self.connection = Some(connection.clone());
|
||||||
|
}
|
||||||
|
EisRequestSourceEvent::Request(EisRequest::Bind(request)) => {
|
||||||
|
let keyboard = request.capabilities.contains(DeviceCapability::Keyboard)
|
||||||
|
&& self.capabilities & 1 != 0;
|
||||||
|
let pointer = request.capabilities.contains(DeviceCapability::Pointer)
|
||||||
|
&& self.capabilities & 2 != 0;
|
||||||
|
if !keyboard && let Some(device) = self.keyboard.take() {
|
||||||
|
device.remove();
|
||||||
|
self.keyboard_ready = false;
|
||||||
|
}
|
||||||
|
if !pointer && let Some(device) = self.pointer.take() {
|
||||||
|
device.remove();
|
||||||
|
self.pointer_ready = false;
|
||||||
|
}
|
||||||
|
if keyboard && self.keyboard.is_none() {
|
||||||
|
let device = request.seat.add_device(
|
||||||
|
Some("Ferese keyboard"),
|
||||||
|
eis::device::DeviceType::Virtual,
|
||||||
|
DeviceCapability::Keyboard.into(),
|
||||||
|
|device| {
|
||||||
|
if let Some(keyboard) = device.interface::<eis::Keyboard>() {
|
||||||
|
keyboard.keymap(
|
||||||
|
eis::keyboard::KeymapType::Xkb,
|
||||||
|
self.keymap_size,
|
||||||
|
self.keymap.as_fd(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
self.keyboard_ready = device.device().version() < 3;
|
||||||
|
device.resumed();
|
||||||
|
self.keyboard = Some(device);
|
||||||
|
}
|
||||||
|
if pointer && self.pointer.is_none() {
|
||||||
|
let caps = request.capabilities
|
||||||
|
& (DeviceCapability::Pointer
|
||||||
|
| DeviceCapability::Button
|
||||||
|
| DeviceCapability::Scroll);
|
||||||
|
let device = request.seat.add_device(
|
||||||
|
Some("Ferese pointer"),
|
||||||
|
eis::device::DeviceType::Virtual,
|
||||||
|
caps,
|
||||||
|
|_| {},
|
||||||
|
);
|
||||||
|
self.pointer_ready = device.device().version() < 3;
|
||||||
|
device.resumed();
|
||||||
|
self.pointer = Some(device);
|
||||||
|
}
|
||||||
|
let ready = self.keyboard.is_some() || self.pointer.is_some();
|
||||||
|
self.ready.store(
|
||||||
|
ready
|
||||||
|
&& (self.keyboard.is_none() || self.keyboard_ready)
|
||||||
|
&& (self.pointer.is_none() || self.pointer_ready),
|
||||||
|
Ordering::SeqCst,
|
||||||
|
);
|
||||||
|
if self.activation.is_some() || !ready {
|
||||||
|
self.signal.stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EisRequestSourceEvent::Request(EisRequest::Ready(request)) => {
|
||||||
|
if self
|
||||||
|
.keyboard
|
||||||
|
.as_ref()
|
||||||
|
.is_some_and(|device| device.device() == request.device.device())
|
||||||
|
{
|
||||||
|
self.keyboard_ready = true;
|
||||||
|
} else if self
|
||||||
|
.pointer
|
||||||
|
.as_ref()
|
||||||
|
.is_some_and(|device| device.device() == request.device.device())
|
||||||
|
{
|
||||||
|
self.pointer_ready = true;
|
||||||
|
} else {
|
||||||
|
self.signal.stop();
|
||||||
|
return PostAction::Remove;
|
||||||
|
}
|
||||||
|
self.ready.store(
|
||||||
|
(self.keyboard.is_none() || self.keyboard_ready)
|
||||||
|
&& (self.pointer.is_none() || self.pointer_ready),
|
||||||
|
Ordering::SeqCst,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
EisRequestSourceEvent::Request(
|
||||||
|
EisRequest::Disconnect | EisRequest::DeviceClosed(_),
|
||||||
|
) => {
|
||||||
|
self.signal.stop();
|
||||||
|
return PostAction::Remove;
|
||||||
|
}
|
||||||
|
EisRequestSourceEvent::Request(_) => {
|
||||||
|
connection.disconnected(
|
||||||
|
eis::connection::DisconnectReason::Mode,
|
||||||
|
Some("Input capture does not accept injected input"),
|
||||||
|
);
|
||||||
|
self.signal.stop();
|
||||||
|
return PostAction::Remove;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
PostAction::Continue
|
||||||
|
}
|
||||||
|
|
||||||
|
fn events(&mut self, events: Vec<Value>) {
|
||||||
|
for event in events {
|
||||||
|
match event["type"].as_str() {
|
||||||
|
Some("activated") => {
|
||||||
|
let activation = event["activation_id"].as_u64().unwrap_or(0) as u32;
|
||||||
|
self.activation = Some(activation);
|
||||||
|
for device in [&self.keyboard, &self.pointer].into_iter().flatten() {
|
||||||
|
device.start_emulating(activation);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some("deactivated" | "disabled" | "closed") => {
|
||||||
|
if self.activation.take().is_some() {
|
||||||
|
for device in [&self.keyboard, &self.pointer].into_iter().flatten() {
|
||||||
|
device.stop_emulating();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some("keys") if self.activation.is_some() => {
|
||||||
|
if let Some(device) = &self.keyboard
|
||||||
|
&& let Some(keyboard) = device.interface::<eis::Keyboard>()
|
||||||
|
{
|
||||||
|
for key in event["keys"]
|
||||||
|
.as_array()
|
||||||
|
.into_iter()
|
||||||
|
.flatten()
|
||||||
|
.filter_map(Value::as_u64)
|
||||||
|
{
|
||||||
|
keyboard.key(key as u32, eis::keyboard::KeyState::Press);
|
||||||
|
}
|
||||||
|
device.frame(now());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some("key") if self.activation.is_some() => {
|
||||||
|
if let Some(device) = &self.keyboard
|
||||||
|
&& let Some(keyboard) = device.interface::<eis::Keyboard>()
|
||||||
|
{
|
||||||
|
keyboard.key(
|
||||||
|
event["key"].as_u64().unwrap_or(0) as u32,
|
||||||
|
if event["pressed"] == true {
|
||||||
|
eis::keyboard::KeyState::Press
|
||||||
|
} else {
|
||||||
|
eis::keyboard::KeyState::Released
|
||||||
|
},
|
||||||
|
);
|
||||||
|
device.frame(now());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some("motion" | "button" | "scroll") if self.activation.is_some() => {
|
||||||
|
if let Some(device) = &self.pointer {
|
||||||
|
match event["type"].as_str() {
|
||||||
|
Some("motion") => {
|
||||||
|
if let Some(pointer) = device.interface::<eis::Pointer>() {
|
||||||
|
pointer.motion_relative(
|
||||||
|
event["x"].as_f64().unwrap_or(0.0) as f32,
|
||||||
|
event["y"].as_f64().unwrap_or(0.0) as f32,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some("button") => {
|
||||||
|
if let Some(button) = device.interface::<eis::Button>() {
|
||||||
|
button.button(
|
||||||
|
event["button"].as_u64().unwrap_or(0) as u32,
|
||||||
|
if event["pressed"] == true {
|
||||||
|
eis::button::ButtonState::Press
|
||||||
|
} else {
|
||||||
|
eis::button::ButtonState::Released
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some("scroll") => {
|
||||||
|
if let Some(scroll) = device.interface::<eis::Scroll>() {
|
||||||
|
let x = event["x"].as_f64().unwrap_or(0.0) as f32;
|
||||||
|
let y = event["y"].as_f64().unwrap_or(0.0) as f32;
|
||||||
|
if !event["v120_x"].is_null() || !event["v120_y"].is_null() {
|
||||||
|
scroll.scroll_discrete(
|
||||||
|
event["v120_x"].as_f64().unwrap_or(0.0) as i32,
|
||||||
|
event["v120_y"].as_f64().unwrap_or(0.0) as i32,
|
||||||
|
);
|
||||||
|
} else if x != 0.0 || y != 0.0 {
|
||||||
|
scroll.scroll(x, y);
|
||||||
|
}
|
||||||
|
let stop_x = event["stop_x"] == true;
|
||||||
|
let stop_y = event["stop_y"] == true;
|
||||||
|
if stop_x || stop_y {
|
||||||
|
scroll.scroll_stop(stop_x as u32, stop_y as u32, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
device.frame(now());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(connection) = &self.connection
|
||||||
|
&& connection.flush().is_err()
|
||||||
|
{
|
||||||
|
self.signal.stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn now() -> u64 {
|
||||||
|
let mut time = libc::timespec {
|
||||||
|
tv_sec: 0,
|
||||||
|
tv_nsec: 0,
|
||||||
|
};
|
||||||
|
// SAFETY: the initialized output pointer covers a timespec allocation.
|
||||||
|
if unsafe { libc::clock_gettime(libc::CLOCK_MONOTONIC, &mut time) } == 0 {
|
||||||
|
time.tv_sec as u64 * 1_000_000 + time.tv_nsec as u64 / 1_000
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use serde_json::json;
|
||||||
|
use std::{
|
||||||
|
process::{Command, Stdio},
|
||||||
|
time::{Duration, Instant},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[ignore = "requires Python and the system libei receiver library"]
|
||||||
|
fn libei_receives_keymap_motion_keyboard_and_explicit_axis_stop() {
|
||||||
|
let cancel = Arc::new(Cancel::default());
|
||||||
|
let map = "xkb_keymap { xkb_keycodes { include \"evdev+aliases(qwerty)\" }; xkb_types { include \"complete\" }; xkb_compatibility { include \"complete\" }; xkb_symbols { include \"pc+us+inet(evdev)\" }; };";
|
||||||
|
let (worker, fd) = Worker::start(3, map, cancel.clone()).unwrap();
|
||||||
|
let fixture = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||||
|
.join("../scripts/tests/fixtures/eis-receiver.py");
|
||||||
|
let mut client = Command::new("python3")
|
||||||
|
.arg(fixture)
|
||||||
|
.stdin(Stdio::from(fd))
|
||||||
|
.stdout(Stdio::piped())
|
||||||
|
.stderr(Stdio::piped())
|
||||||
|
.spawn()
|
||||||
|
.unwrap();
|
||||||
|
let deadline = Instant::now() + Duration::from_secs(5);
|
||||||
|
while !worker.ready.load(Ordering::SeqCst) && Instant::now() < deadline {
|
||||||
|
std::thread::sleep(Duration::from_millis(10));
|
||||||
|
}
|
||||||
|
if !worker.ready.load(Ordering::SeqCst) {
|
||||||
|
let _ = client.kill();
|
||||||
|
let output = client.wait_with_output().unwrap();
|
||||||
|
panic!(
|
||||||
|
"Receiver did not bind: {}",
|
||||||
|
String::from_utf8_lossy(&output.stderr)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
worker
|
||||||
|
.send(vec![
|
||||||
|
json!({"type":"activated", "activation_id":1}),
|
||||||
|
json!({"type":"motion", "x":10.0, "y":-5.0}),
|
||||||
|
json!({"type":"motion", "x":10.0, "y":-5.0}),
|
||||||
|
json!({"type":"key", "key":30, "pressed":true}),
|
||||||
|
json!({"type":"key", "key":30, "pressed":false}),
|
||||||
|
json!({"type":"scroll", "x":0.0, "y":4.0}),
|
||||||
|
json!({"type":"scroll", "x":0.0, "y":0.0, "stop_x":false, "stop_y":true}),
|
||||||
|
json!({"type":"deactivated"}),
|
||||||
|
])
|
||||||
|
.unwrap();
|
||||||
|
let output = client.wait_with_output().unwrap();
|
||||||
|
assert!(
|
||||||
|
output.status.success(),
|
||||||
|
"{}",
|
||||||
|
String::from_utf8_lossy(&output.stderr)
|
||||||
|
);
|
||||||
|
assert!(String::from_utf8_lossy(&output.stdout).contains("\"keymap\": true"));
|
||||||
|
let deadline = Instant::now() + Duration::from_secs(2);
|
||||||
|
while !cancel.stopped.load(Ordering::SeqCst) && Instant::now() < deadline {
|
||||||
|
std::thread::sleep(Duration::from_millis(10));
|
||||||
|
}
|
||||||
|
assert!(
|
||||||
|
cancel.stopped.load(Ordering::SeqCst),
|
||||||
|
"Receiver disconnect must revoke capture"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,784 @@
|
|||||||
|
use std::{
|
||||||
|
collections::HashMap,
|
||||||
|
sync::{Arc, atomic::Ordering},
|
||||||
|
time::Duration,
|
||||||
|
};
|
||||||
|
|
||||||
|
use serde_json::json;
|
||||||
|
use tokio::sync::Mutex;
|
||||||
|
use zbus::{
|
||||||
|
Connection,
|
||||||
|
message::Header,
|
||||||
|
object_server::SignalEmitter,
|
||||||
|
zvariant::{OwnedFd, OwnedObjectPath, Value},
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
backend::{Cancel, Options, authorize},
|
||||||
|
bridge::Bridge,
|
||||||
|
desktop::Requests,
|
||||||
|
eis::Worker,
|
||||||
|
};
|
||||||
|
|
||||||
|
const PATH: &str = "/org/freedesktop/portal/desktop";
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
struct State {
|
||||||
|
starting: bool,
|
||||||
|
started: bool,
|
||||||
|
id: u64,
|
||||||
|
capabilities: u32,
|
||||||
|
keymap: String,
|
||||||
|
bridge: Option<Bridge>,
|
||||||
|
watch: Option<Bridge>,
|
||||||
|
worker: Option<Worker>,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Session {
|
||||||
|
owner: String,
|
||||||
|
app: String,
|
||||||
|
cancel: Arc<Cancel>,
|
||||||
|
state: Mutex<State>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Default)]
|
||||||
|
pub(crate) struct InputCapture {
|
||||||
|
sessions: Arc<Mutex<HashMap<String, Arc<Session>>>>,
|
||||||
|
requests: Requests,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl InputCapture {
|
||||||
|
pub(crate) fn new(requests: Requests) -> Self {
|
||||||
|
Self {
|
||||||
|
requests,
|
||||||
|
..Self::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn session(
|
||||||
|
&self,
|
||||||
|
connection: &Connection,
|
||||||
|
header: &Header<'_>,
|
||||||
|
path: &OwnedObjectPath,
|
||||||
|
app: &str,
|
||||||
|
) -> zbus::fdo::Result<Arc<Session>> {
|
||||||
|
let owner = authorize(connection, header).await?;
|
||||||
|
self.sessions
|
||||||
|
.lock()
|
||||||
|
.await
|
||||||
|
.get(path.as_str())
|
||||||
|
.filter(|session| session.owner == owner && session.app == app)
|
||||||
|
.cloned()
|
||||||
|
.ok_or_else(|| invalid("Unknown input capture session"))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn end(&self, connection: &Connection, path: &str) {
|
||||||
|
let session = self.sessions.lock().await.remove(path);
|
||||||
|
if let Some(session) = session {
|
||||||
|
session.cancel.stop();
|
||||||
|
let state = session.state.lock().await;
|
||||||
|
if let Some(worker) = &state.worker {
|
||||||
|
worker.stop();
|
||||||
|
}
|
||||||
|
if let Some(bridge) = &state.bridge {
|
||||||
|
bridge.close();
|
||||||
|
}
|
||||||
|
if let Some(watch) = &state.watch {
|
||||||
|
watch.close();
|
||||||
|
}
|
||||||
|
if let Ok(emitter) = SignalEmitter::new(connection, path) {
|
||||||
|
let _ = SessionObject::closed(&emitter).await;
|
||||||
|
}
|
||||||
|
let connection = connection.clone();
|
||||||
|
let path = path.to_owned();
|
||||||
|
tokio::spawn(async move {
|
||||||
|
let _ = connection
|
||||||
|
.object_server()
|
||||||
|
.remove::<SessionObject, _>(path)
|
||||||
|
.await;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn revoke_stale(&self, connection: &Connection, owner: Option<&str>) {
|
||||||
|
let stale = self
|
||||||
|
.sessions
|
||||||
|
.lock()
|
||||||
|
.await
|
||||||
|
.iter()
|
||||||
|
.filter_map(|(path, session)| {
|
||||||
|
(Some(session.owner.as_str()) != owner).then_some(path.clone())
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
for path in stale {
|
||||||
|
self.end(connection, &path).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn native(session: &Session) -> zbus::fdo::Result<(Bridge, u64)> {
|
||||||
|
let state = session.state.lock().await;
|
||||||
|
if !state.started || session.cancel.stopped.load(Ordering::SeqCst) {
|
||||||
|
return Err(invalid("Start the session first"));
|
||||||
|
}
|
||||||
|
Ok((
|
||||||
|
state
|
||||||
|
.bridge
|
||||||
|
.clone()
|
||||||
|
.ok_or_else(|| invalid("Missing capture connection"))?,
|
||||||
|
state.id,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn watch(
|
||||||
|
self,
|
||||||
|
connection: Connection,
|
||||||
|
path: OwnedObjectPath,
|
||||||
|
session: Arc<Session>,
|
||||||
|
bridge: Bridge,
|
||||||
|
id: u64,
|
||||||
|
) {
|
||||||
|
loop {
|
||||||
|
let response = tokio::select! {
|
||||||
|
_ = session.cancel.wait() => break,
|
||||||
|
result = bridge.capture_watch(id) => result,
|
||||||
|
};
|
||||||
|
let Ok(response) = response else {
|
||||||
|
break;
|
||||||
|
};
|
||||||
|
let Some(events) = response["events"].as_array() else {
|
||||||
|
break;
|
||||||
|
};
|
||||||
|
let state = session.state.lock().await;
|
||||||
|
if session.cancel.stopped.load(Ordering::SeqCst) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
let transport = state
|
||||||
|
.worker
|
||||||
|
.as_ref()
|
||||||
|
.map(|worker| worker.send(events.clone()));
|
||||||
|
if transport.is_some_and(|result| result.is_err()) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
let mut closed = false;
|
||||||
|
let Ok(emitter) = SignalEmitter::new(&connection, PATH) else {
|
||||||
|
break;
|
||||||
|
};
|
||||||
|
for event in events {
|
||||||
|
let result = match event["type"].as_str() {
|
||||||
|
Some("activated") => {
|
||||||
|
let mut options = Options::new();
|
||||||
|
options.insert(
|
||||||
|
"activation_id".into(),
|
||||||
|
(event["activation_id"].as_u64().unwrap_or(0) as u32).into(),
|
||||||
|
);
|
||||||
|
options.insert(
|
||||||
|
"barrier_id".into(),
|
||||||
|
(event["barrier_id"].as_u64().unwrap_or(0) as u32).into(),
|
||||||
|
);
|
||||||
|
if let Some(position) = event["cursor_position"]
|
||||||
|
.as_array()
|
||||||
|
.filter(|position| position.len() == 2)
|
||||||
|
{
|
||||||
|
if let Ok(position) = Value::from((
|
||||||
|
position[0].as_f64().unwrap_or(0.0),
|
||||||
|
position[1].as_f64().unwrap_or(0.0),
|
||||||
|
))
|
||||||
|
.try_to_owned()
|
||||||
|
{
|
||||||
|
options.insert("cursor_position".into(), position);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Self::activated(&emitter, path.clone(), options).await
|
||||||
|
}
|
||||||
|
Some("deactivated") => {
|
||||||
|
Self::deactivated(
|
||||||
|
&emitter,
|
||||||
|
path.clone(),
|
||||||
|
Options::from([(
|
||||||
|
"activation_id".into(),
|
||||||
|
(event["activation_id"].as_u64().unwrap_or(0) as u32).into(),
|
||||||
|
)]),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
Some("disabled") => {
|
||||||
|
Self::disabled(&emitter, path.clone(), Options::new()).await
|
||||||
|
}
|
||||||
|
Some("zones") => {
|
||||||
|
Self::zones_changed(&emitter, path.clone(), Options::new()).await
|
||||||
|
}
|
||||||
|
Some("closed") => {
|
||||||
|
closed = true;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
_ => Ok(()),
|
||||||
|
};
|
||||||
|
if result.is_err() {
|
||||||
|
closed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
drop(state);
|
||||||
|
if closed {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.end(&connection, path.as_str()).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn invalid(message: &str) -> zbus::fdo::Error {
|
||||||
|
zbus::fdo::Error::InvalidArgs(message.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn failed(error: String) -> zbus::fdo::Error {
|
||||||
|
zbus::fdo::Error::Failed(error)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn capabilities(options: &Options) -> zbus::fdo::Result<u32> {
|
||||||
|
let requested = options
|
||||||
|
.get("capabilities")
|
||||||
|
.ok_or_else(|| invalid("Missing requested capabilities"))?;
|
||||||
|
let requested =
|
||||||
|
u32::try_from(requested).map_err(|_| invalid("Invalid requested capabilities"))?;
|
||||||
|
if requested == 0 || requested & 3 == 0 {
|
||||||
|
return Err(invalid("No supported capabilities requested"));
|
||||||
|
}
|
||||||
|
Ok(requested & 3)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[zbus::interface(name = "org.freedesktop.impl.portal.InputCapture")]
|
||||||
|
impl InputCapture {
|
||||||
|
#[zbus(property, name = "version")]
|
||||||
|
fn version(&self) -> u32 {
|
||||||
|
2
|
||||||
|
}
|
||||||
|
|
||||||
|
#[zbus(property)]
|
||||||
|
fn supported_capabilities(&self) -> u32 {
|
||||||
|
3
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn create_session2(
|
||||||
|
&self,
|
||||||
|
session_handle: OwnedObjectPath,
|
||||||
|
app_id: String,
|
||||||
|
_options: Options,
|
||||||
|
#[zbus(connection)] connection: &Connection,
|
||||||
|
#[zbus(header)] header: Header<'_>,
|
||||||
|
) -> zbus::fdo::Result<Options> {
|
||||||
|
let owner = authorize(connection, &header).await?;
|
||||||
|
if !session_handle
|
||||||
|
.as_str()
|
||||||
|
.starts_with(&format!("{PATH}/session/"))
|
||||||
|
|| session_handle.as_str().len() > 512
|
||||||
|
|| app_id.len() > 512
|
||||||
|
{
|
||||||
|
return Err(invalid("Invalid session"));
|
||||||
|
}
|
||||||
|
let mut sessions = self.sessions.lock().await;
|
||||||
|
if sessions.len() >= 8 || sessions.contains_key(session_handle.as_str()) {
|
||||||
|
return Err(invalid("Session limit reached or duplicate session"));
|
||||||
|
}
|
||||||
|
if !connection
|
||||||
|
.object_server()
|
||||||
|
.at(
|
||||||
|
&session_handle,
|
||||||
|
SessionObject {
|
||||||
|
backend: self.clone(),
|
||||||
|
path: session_handle.to_string(),
|
||||||
|
owner: owner.clone(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await?
|
||||||
|
{
|
||||||
|
return Err(invalid("Session object already exists"));
|
||||||
|
}
|
||||||
|
sessions.insert(
|
||||||
|
session_handle.to_string(),
|
||||||
|
Arc::new(Session {
|
||||||
|
owner,
|
||||||
|
app: app_id,
|
||||||
|
cancel: Arc::default(),
|
||||||
|
state: Mutex::default(),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
Ok(Options::new())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
async fn create_session(
|
||||||
|
&self,
|
||||||
|
handle: OwnedObjectPath,
|
||||||
|
session_handle: OwnedObjectPath,
|
||||||
|
app_id: String,
|
||||||
|
parent_window: String,
|
||||||
|
options: Options,
|
||||||
|
#[zbus(connection)] connection: &Connection,
|
||||||
|
#[zbus(header)] header: Header<'_>,
|
||||||
|
) -> zbus::fdo::Result<(u32, Options)> {
|
||||||
|
capabilities(&options)?;
|
||||||
|
self.create_session2(
|
||||||
|
session_handle.clone(),
|
||||||
|
app_id.clone(),
|
||||||
|
Options::new(),
|
||||||
|
connection,
|
||||||
|
header.clone(),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
let result = self
|
||||||
|
.start(
|
||||||
|
handle,
|
||||||
|
session_handle.clone(),
|
||||||
|
app_id,
|
||||||
|
parent_window,
|
||||||
|
options,
|
||||||
|
connection,
|
||||||
|
header,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
let (response, mut results) = match result {
|
||||||
|
Ok(result) => result,
|
||||||
|
Err(error) => {
|
||||||
|
self.end(connection, session_handle.as_str()).await;
|
||||||
|
return Err(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if response == 0 {
|
||||||
|
results.insert(
|
||||||
|
"session_id".into(),
|
||||||
|
Value::from(session_handle.to_string())
|
||||||
|
.try_to_owned()
|
||||||
|
.map_err(|e| failed(e.to_string()))?,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Ok((response, results))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
async fn start(
|
||||||
|
&self,
|
||||||
|
handle: OwnedObjectPath,
|
||||||
|
session_handle: OwnedObjectPath,
|
||||||
|
app_id: String,
|
||||||
|
parent_window: String,
|
||||||
|
options: Options,
|
||||||
|
#[zbus(connection)] connection: &Connection,
|
||||||
|
#[zbus(header)] header: Header<'_>,
|
||||||
|
) -> zbus::fdo::Result<(u32, Options)> {
|
||||||
|
let session = self
|
||||||
|
.session(connection, &header, &session_handle, &app_id)
|
||||||
|
.await?;
|
||||||
|
let caps = capabilities(&options)?;
|
||||||
|
crate::restore::persist_mode(&options)?;
|
||||||
|
let cancel = self.requests.begin(connection, &header, &handle).await?;
|
||||||
|
{
|
||||||
|
let mut state = session.state.lock().await;
|
||||||
|
if state.starting || state.started {
|
||||||
|
self.requests.end(connection, &handle).await;
|
||||||
|
return Err(invalid("Input capture can only be started once"));
|
||||||
|
}
|
||||||
|
state.starting = true;
|
||||||
|
}
|
||||||
|
let title = match caps {
|
||||||
|
1 => "Share keyboard input?",
|
||||||
|
2 => "Share pointer input?",
|
||||||
|
_ => "Share keyboard and pointer?",
|
||||||
|
};
|
||||||
|
let consent = crate::desktop::consent(
|
||||||
|
&app_id,
|
||||||
|
&parent_window,
|
||||||
|
title,
|
||||||
|
"Allow input capture at the screen edges this app configures. Captured input is sent to this app until it releases control. Press Ctrl+Alt+Escape to return control to Ferese.",
|
||||||
|
"Allow input capture",
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
let approved = tokio::select! {
|
||||||
|
_ = cancel.wait() => Ok(false),
|
||||||
|
_ = session.cancel.wait() => Ok(false),
|
||||||
|
result = consent => result,
|
||||||
|
};
|
||||||
|
let result = if approved == Ok(true)
|
||||||
|
&& !cancel.stopped.load(Ordering::SeqCst)
|
||||||
|
&& !session.cancel.stopped.load(Ordering::SeqCst)
|
||||||
|
{
|
||||||
|
async {
|
||||||
|
let bridge = Bridge::connect()?;
|
||||||
|
let reply = bridge
|
||||||
|
.call("input-capture-register", json!({"capabilities":caps}))
|
||||||
|
.await?;
|
||||||
|
let id = reply["session"]
|
||||||
|
.as_u64()
|
||||||
|
.ok_or("Missing capture session ID")?;
|
||||||
|
let keymap = reply["keymap"]
|
||||||
|
.as_str()
|
||||||
|
.ok_or("Missing keyboard map")?
|
||||||
|
.to_owned();
|
||||||
|
let watch = Bridge::connect()?;
|
||||||
|
let mut state = session.state.lock().await;
|
||||||
|
if cancel.stopped.load(Ordering::SeqCst)
|
||||||
|
|| session.cancel.stopped.load(Ordering::SeqCst)
|
||||||
|
{
|
||||||
|
bridge.close();
|
||||||
|
return Err("Input capture approval was cancelled".to_owned());
|
||||||
|
}
|
||||||
|
state.started = true;
|
||||||
|
state.starting = false;
|
||||||
|
state.id = id;
|
||||||
|
state.capabilities = caps;
|
||||||
|
state.keymap = keymap;
|
||||||
|
state.bridge = Some(bridge);
|
||||||
|
state.watch = Some(watch.clone());
|
||||||
|
tokio::spawn(self.clone().watch(
|
||||||
|
connection.clone(),
|
||||||
|
session_handle.clone(),
|
||||||
|
session.clone(),
|
||||||
|
watch,
|
||||||
|
id,
|
||||||
|
));
|
||||||
|
Ok(Options::from([
|
||||||
|
("capabilities".into(), caps.into()),
|
||||||
|
("clipboard_enabled".into(), false.into()),
|
||||||
|
("persist_mode".into(), 0u32.into()),
|
||||||
|
]))
|
||||||
|
}
|
||||||
|
.await
|
||||||
|
} else {
|
||||||
|
Err(approved.err().unwrap_or_else(|| "Cancelled".into()))
|
||||||
|
};
|
||||||
|
self.requests.end(connection, &handle).await;
|
||||||
|
match result {
|
||||||
|
Ok(results) => Ok((0, results)),
|
||||||
|
Err(error) => {
|
||||||
|
let response = if error == "Cancelled"
|
||||||
|
|| cancel.stopped.load(Ordering::SeqCst)
|
||||||
|
|| session.cancel.stopped.load(Ordering::SeqCst)
|
||||||
|
{
|
||||||
|
1
|
||||||
|
} else {
|
||||||
|
eprintln!("ferese input capture: {error}");
|
||||||
|
2
|
||||||
|
};
|
||||||
|
self.end(connection, session_handle.as_str()).await;
|
||||||
|
Ok((response, Options::new()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_zones(
|
||||||
|
&self,
|
||||||
|
handle: OwnedObjectPath,
|
||||||
|
session_handle: OwnedObjectPath,
|
||||||
|
app_id: String,
|
||||||
|
_options: Options,
|
||||||
|
#[zbus(connection)] connection: &Connection,
|
||||||
|
#[zbus(header)] header: Header<'_>,
|
||||||
|
) -> zbus::fdo::Result<(u32, Options)> {
|
||||||
|
let session = self
|
||||||
|
.session(connection, &header, &session_handle, &app_id)
|
||||||
|
.await?;
|
||||||
|
let (bridge, id) = Self::native(&session).await?;
|
||||||
|
let cancel = self.requests.begin(connection, &header, &handle).await?;
|
||||||
|
let result = tokio::select! {
|
||||||
|
biased;
|
||||||
|
_ = cancel.wait() => None,
|
||||||
|
_ = session.cancel.wait() => None,
|
||||||
|
result = bridge.call("input-capture-zones", json!({"session":id})) => Some(result),
|
||||||
|
};
|
||||||
|
self.requests.end(connection, &handle).await;
|
||||||
|
if cancel.stopped.load(Ordering::SeqCst)
|
||||||
|
|| session.cancel.stopped.load(Ordering::SeqCst)
|
||||||
|
|| result.is_none()
|
||||||
|
{
|
||||||
|
return Ok((1, Options::new()));
|
||||||
|
}
|
||||||
|
let result = result.unwrap().map_err(failed)?;
|
||||||
|
let zones: Vec<(u32, u32, i32, i32)> = serde_json::from_value(result["zones"].clone())
|
||||||
|
.map_err(|_| invalid("Invalid native capture zones"))?;
|
||||||
|
Ok((
|
||||||
|
0,
|
||||||
|
Options::from([
|
||||||
|
(
|
||||||
|
"zones".into(),
|
||||||
|
Value::from(zones)
|
||||||
|
.try_to_owned()
|
||||||
|
.map_err(|e| failed(e.to_string()))?,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"zone_set".into(),
|
||||||
|
(result["zone_set"].as_u64().unwrap_or(0) as u32).into(),
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
async fn set_pointer_barriers(
|
||||||
|
&self,
|
||||||
|
handle: OwnedObjectPath,
|
||||||
|
session_handle: OwnedObjectPath,
|
||||||
|
app_id: String,
|
||||||
|
_options: Options,
|
||||||
|
barriers: Vec<Options>,
|
||||||
|
zone_set: u32,
|
||||||
|
#[zbus(connection)] connection: &Connection,
|
||||||
|
#[zbus(header)] header: Header<'_>,
|
||||||
|
) -> zbus::fdo::Result<(u32, Options)> {
|
||||||
|
let session = self
|
||||||
|
.session(connection, &header, &session_handle, &app_id)
|
||||||
|
.await?;
|
||||||
|
let (bridge, id) = Self::native(&session).await?;
|
||||||
|
if barriers.len() > 64 {
|
||||||
|
return Err(invalid("Too many pointer barriers"));
|
||||||
|
}
|
||||||
|
let barriers = barriers
|
||||||
|
.into_iter()
|
||||||
|
.map(|barrier| {
|
||||||
|
let id = u32::try_from(
|
||||||
|
barrier
|
||||||
|
.get("barrier_id")
|
||||||
|
.ok_or_else(|| invalid("Missing barrier ID"))?,
|
||||||
|
)
|
||||||
|
.map_err(|_| invalid("Invalid barrier ID"))?;
|
||||||
|
let position = barrier
|
||||||
|
.get("position")
|
||||||
|
.ok_or_else(|| invalid("Missing barrier position"))?
|
||||||
|
.try_clone()
|
||||||
|
.map_err(|_| invalid("Invalid position"))?;
|
||||||
|
let position: (i32, i32, i32, i32) = position
|
||||||
|
.try_into()
|
||||||
|
.map_err(|_| invalid("Invalid barrier position"))?;
|
||||||
|
Ok(json!({"id":id,"position":[position.0,position.1,position.2,position.3]}))
|
||||||
|
})
|
||||||
|
.collect::<zbus::fdo::Result<Vec<_>>>()?;
|
||||||
|
let cancel = self.requests.begin(connection, &header, &handle).await?;
|
||||||
|
let result = tokio::select! {
|
||||||
|
biased;
|
||||||
|
_ = cancel.wait() => None,
|
||||||
|
_ = session.cancel.wait() => None,
|
||||||
|
result = bridge.call("input-capture-barriers", json!({"session":id,"zone_set":zone_set,"barriers":barriers})) => Some(result),
|
||||||
|
};
|
||||||
|
self.requests.end(connection, &handle).await;
|
||||||
|
if cancel.stopped.load(Ordering::SeqCst)
|
||||||
|
|| session.cancel.stopped.load(Ordering::SeqCst)
|
||||||
|
|| result.is_none()
|
||||||
|
{
|
||||||
|
self.end(connection, session_handle.as_str()).await;
|
||||||
|
return Ok((1, Options::new()));
|
||||||
|
}
|
||||||
|
let result = result.unwrap().map_err(failed)?;
|
||||||
|
let failed_barriers: Vec<u32> = serde_json::from_value(result["failed_barriers"].clone())
|
||||||
|
.map_err(|_| invalid("Invalid native barrier response"))?;
|
||||||
|
Ok((
|
||||||
|
0,
|
||||||
|
Options::from([(
|
||||||
|
"failed_barriers".into(),
|
||||||
|
Value::from(failed_barriers)
|
||||||
|
.try_to_owned()
|
||||||
|
.map_err(|e| failed(e.to_string()))?,
|
||||||
|
)]),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[zbus(name = "ConnectToEIS")]
|
||||||
|
async fn connect_to_eis(
|
||||||
|
&self,
|
||||||
|
session_handle: OwnedObjectPath,
|
||||||
|
app_id: String,
|
||||||
|
_options: Options,
|
||||||
|
#[zbus(connection)] connection: &Connection,
|
||||||
|
#[zbus(header)] header: Header<'_>,
|
||||||
|
) -> zbus::fdo::Result<OwnedFd> {
|
||||||
|
let session = self
|
||||||
|
.session(connection, &header, &session_handle, &app_id)
|
||||||
|
.await?;
|
||||||
|
Self::native(&session).await?;
|
||||||
|
let mut state = session.state.lock().await;
|
||||||
|
if state.worker.is_some() {
|
||||||
|
return Err(invalid("EIS has already been connected"));
|
||||||
|
}
|
||||||
|
let capabilities = state.capabilities;
|
||||||
|
let keymap = state.keymap.clone();
|
||||||
|
let cancel = session.cancel.clone();
|
||||||
|
let (worker, fd) =
|
||||||
|
tokio::task::spawn_blocking(move || Worker::start(capabilities, &keymap, cancel))
|
||||||
|
.await
|
||||||
|
.map_err(|e| failed(e.to_string()))?
|
||||||
|
.map_err(failed)?;
|
||||||
|
state.worker = Some(worker);
|
||||||
|
Ok(fd.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn enable(
|
||||||
|
&self,
|
||||||
|
session_handle: OwnedObjectPath,
|
||||||
|
app_id: String,
|
||||||
|
_options: Options,
|
||||||
|
#[zbus(connection)] connection: &Connection,
|
||||||
|
#[zbus(header)] header: Header<'_>,
|
||||||
|
) -> zbus::fdo::Result<(u32, Options)> {
|
||||||
|
let session = self
|
||||||
|
.session(connection, &header, &session_handle, &app_id)
|
||||||
|
.await?;
|
||||||
|
let (bridge, id) = Self::native(&session).await?;
|
||||||
|
let ready = session
|
||||||
|
.state
|
||||||
|
.lock()
|
||||||
|
.await
|
||||||
|
.worker
|
||||||
|
.as_ref()
|
||||||
|
.map(|worker| worker.ready.clone())
|
||||||
|
.ok_or_else(|| invalid("Connect to EIS before enabling input capture"))?;
|
||||||
|
let deadline = tokio::time::Instant::now() + Duration::from_secs(5);
|
||||||
|
while !ready.load(Ordering::SeqCst) {
|
||||||
|
if session.cancel.stopped.load(Ordering::SeqCst)
|
||||||
|
|| tokio::time::Instant::now() >= deadline
|
||||||
|
{
|
||||||
|
return Err(invalid("EIS receiver has not bound an input device"));
|
||||||
|
}
|
||||||
|
tokio::time::sleep(Duration::from_millis(10)).await;
|
||||||
|
}
|
||||||
|
bridge
|
||||||
|
.call("input-capture-enable", json!({"session":id}))
|
||||||
|
.await
|
||||||
|
.map_err(failed)?;
|
||||||
|
Ok((0, Options::new()))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn disable(
|
||||||
|
&self,
|
||||||
|
session_handle: OwnedObjectPath,
|
||||||
|
app_id: String,
|
||||||
|
_options: Options,
|
||||||
|
#[zbus(connection)] connection: &Connection,
|
||||||
|
#[zbus(header)] header: Header<'_>,
|
||||||
|
) -> zbus::fdo::Result<(u32, Options)> {
|
||||||
|
let session = self
|
||||||
|
.session(connection, &header, &session_handle, &app_id)
|
||||||
|
.await?;
|
||||||
|
let (bridge, id) = Self::native(&session).await?;
|
||||||
|
bridge
|
||||||
|
.call("input-capture-disable", json!({"session":id}))
|
||||||
|
.await
|
||||||
|
.map_err(failed)?;
|
||||||
|
Ok((0, Options::new()))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn release(
|
||||||
|
&self,
|
||||||
|
session_handle: OwnedObjectPath,
|
||||||
|
app_id: String,
|
||||||
|
options: Options,
|
||||||
|
#[zbus(connection)] connection: &Connection,
|
||||||
|
#[zbus(header)] header: Header<'_>,
|
||||||
|
) -> zbus::fdo::Result<(u32, Options)> {
|
||||||
|
let session = self
|
||||||
|
.session(connection, &header, &session_handle, &app_id)
|
||||||
|
.await?;
|
||||||
|
let (bridge, id) = Self::native(&session).await?;
|
||||||
|
let activation = options
|
||||||
|
.get("activation_id")
|
||||||
|
.map(u32::try_from)
|
||||||
|
.transpose()
|
||||||
|
.map_err(|_| invalid("Invalid activation ID"))?;
|
||||||
|
let position = options
|
||||||
|
.get("cursor_position")
|
||||||
|
.map(|value| {
|
||||||
|
let position: (f64, f64) = value
|
||||||
|
.try_clone()
|
||||||
|
.map_err(|_| invalid("Invalid cursor position"))?
|
||||||
|
.try_into()
|
||||||
|
.map_err(|_| invalid("Invalid cursor position"))?;
|
||||||
|
if !position.0.is_finite() || !position.1.is_finite() {
|
||||||
|
return Err(invalid("Invalid cursor position"));
|
||||||
|
}
|
||||||
|
Ok(position)
|
||||||
|
})
|
||||||
|
.transpose()?;
|
||||||
|
bridge
|
||||||
|
.call(
|
||||||
|
"input-capture-release",
|
||||||
|
json!({"session":id,"activation_id":activation,"cursor_position":position}),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(failed)?;
|
||||||
|
Ok((0, Options::new()))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[zbus(signal)]
|
||||||
|
async fn activated(
|
||||||
|
emitter: &SignalEmitter<'_>,
|
||||||
|
session_handle: OwnedObjectPath,
|
||||||
|
options: Options,
|
||||||
|
) -> zbus::Result<()>;
|
||||||
|
|
||||||
|
#[zbus(signal)]
|
||||||
|
async fn deactivated(
|
||||||
|
emitter: &SignalEmitter<'_>,
|
||||||
|
session_handle: OwnedObjectPath,
|
||||||
|
options: Options,
|
||||||
|
) -> zbus::Result<()>;
|
||||||
|
|
||||||
|
#[zbus(signal)]
|
||||||
|
async fn disabled(
|
||||||
|
emitter: &SignalEmitter<'_>,
|
||||||
|
session_handle: OwnedObjectPath,
|
||||||
|
options: Options,
|
||||||
|
) -> zbus::Result<()>;
|
||||||
|
|
||||||
|
#[zbus(signal)]
|
||||||
|
async fn zones_changed(
|
||||||
|
emitter: &SignalEmitter<'_>,
|
||||||
|
session_handle: OwnedObjectPath,
|
||||||
|
options: Options,
|
||||||
|
) -> zbus::Result<()>;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SessionObject {
|
||||||
|
backend: InputCapture,
|
||||||
|
path: String,
|
||||||
|
owner: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[zbus::interface(name = "org.freedesktop.impl.portal.Session")]
|
||||||
|
impl SessionObject {
|
||||||
|
#[zbus(property, name = "version")]
|
||||||
|
fn version(&self) -> u32 {
|
||||||
|
1
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn close(
|
||||||
|
&self,
|
||||||
|
#[zbus(connection)] connection: &Connection,
|
||||||
|
#[zbus(header)] header: Header<'_>,
|
||||||
|
) -> zbus::fdo::Result<()> {
|
||||||
|
if header.sender().map(|owner| owner.as_str()) != Some(self.owner.as_str()) {
|
||||||
|
return Err(zbus::fdo::Error::AccessDenied("Wrong session owner".into()));
|
||||||
|
}
|
||||||
|
self.backend.end(connection, &self.path).await;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[zbus(signal)]
|
||||||
|
async fn closed(emitter: &SignalEmitter<'_>) -> zbus::Result<()>;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn requested_capabilities_are_required_and_supported_subset_is_returned() {
|
||||||
|
assert!(capabilities(&Options::new()).is_err());
|
||||||
|
for requested in [0u32, 4, 8] {
|
||||||
|
assert!(
|
||||||
|
capabilities(&Options::from([("capabilities".into(), requested.into())])).is_err()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
for (requested, supported) in [(1u32, 1), (2, 2), (3, 3), (7, 3), (9, 1)] {
|
||||||
|
assert_eq!(
|
||||||
|
capabilities(&Options::from([("capabilities".into(), requested.into())])).unwrap(),
|
||||||
|
supported
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,7 +3,9 @@ mod bridge;
|
|||||||
mod capture;
|
mod capture;
|
||||||
mod consent;
|
mod consent;
|
||||||
mod desktop;
|
mod desktop;
|
||||||
|
mod eis;
|
||||||
mod inhibit;
|
mod inhibit;
|
||||||
|
mod input_capture;
|
||||||
mod lockdown;
|
mod lockdown;
|
||||||
mod parent;
|
mod parent;
|
||||||
mod permissions;
|
mod permissions;
|
||||||
|
|||||||
+77
@@ -0,0 +1,77 @@
|
|||||||
|
"""Receive Ferese's private test EIS connection through stdin using real libei."""
|
||||||
|
import ctypes as C
|
||||||
|
import ctypes.util
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import select
|
||||||
|
import time
|
||||||
|
|
||||||
|
lib = C.CDLL(ctypes.util.find_library("ei"))
|
||||||
|
|
||||||
|
|
||||||
|
def function(name, result, *arguments):
|
||||||
|
value = getattr(lib, name)
|
||||||
|
value.restype = result
|
||||||
|
value.argtypes = list(arguments)
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
ptr = C.c_void_p
|
||||||
|
new = function("ei_new_receiver", ptr, ptr)
|
||||||
|
setup = function("ei_setup_backend_fd", C.c_int, ptr, C.c_int)
|
||||||
|
dispatch = function("ei_dispatch", None, ptr)
|
||||||
|
next_event = function("ei_get_event", ptr, ptr)
|
||||||
|
kind = function("ei_event_get_type", C.c_int, ptr)
|
||||||
|
seat = function("ei_event_get_seat", ptr, ptr)
|
||||||
|
device = function("ei_event_get_device", ptr, ptr)
|
||||||
|
unref_event = function("ei_event_unref", ptr, ptr)
|
||||||
|
unref = function("ei_unref", ptr, ptr)
|
||||||
|
bind = function("ei_seat_bind_capabilities", None, ptr)
|
||||||
|
keymap = function("ei_device_keyboard_get_keymap", ptr, ptr)
|
||||||
|
map_fd = function("ei_keymap_get_fd", C.c_int, ptr)
|
||||||
|
map_size = function("ei_keymap_get_size", C.c_size_t, ptr)
|
||||||
|
dx = function("ei_event_pointer_get_dx", C.c_double, ptr)
|
||||||
|
dy = function("ei_event_pointer_get_dy", C.c_double, ptr)
|
||||||
|
key = function("ei_event_keyboard_get_key", C.c_uint32, ptr)
|
||||||
|
pressed = function("ei_event_keyboard_get_key_is_press", C.c_bool, ptr)
|
||||||
|
stop_x = function("ei_event_scroll_get_stop_x", C.c_bool, ptr)
|
||||||
|
stop_y = function("ei_event_scroll_get_stop_y", C.c_bool, ptr)
|
||||||
|
fd = function("ei_get_fd", C.c_int, ptr)
|
||||||
|
context = new(None)
|
||||||
|
assert context and setup(context, os.dup(0)) == 0
|
||||||
|
seen = {"motion": [], "keys": [], "stops": [], "starts": 0, "ends": 0, "keymap": False}
|
||||||
|
deadline = time.monotonic() + 10
|
||||||
|
try:
|
||||||
|
while time.monotonic() < deadline:
|
||||||
|
select.select([fd(context)], [], [], 0.05)
|
||||||
|
dispatch(context)
|
||||||
|
while event := next_event(context):
|
||||||
|
event_type = kind(event)
|
||||||
|
if event_type == 2:
|
||||||
|
raise AssertionError("EIS disconnected before completing capture")
|
||||||
|
if event_type == 3:
|
||||||
|
bind(seat(event), C.c_int(1), C.c_int(4), C.c_int(16), C.c_int(32), C.c_void_p())
|
||||||
|
elif event_type == 5:
|
||||||
|
if mapping := keymap(device(event)):
|
||||||
|
content = os.pread(map_fd(mapping), map_size(mapping), 0)
|
||||||
|
assert content.endswith(b"\0") and b"xkb_keymap" in content
|
||||||
|
seen["keymap"] = True
|
||||||
|
elif event_type == 200:
|
||||||
|
seen["starts"] += 1
|
||||||
|
elif event_type == 201:
|
||||||
|
seen["ends"] += 1
|
||||||
|
elif event_type == 300:
|
||||||
|
seen["motion"].append([dx(event), dy(event)])
|
||||||
|
elif event_type == 700:
|
||||||
|
seen["keys"].append([key(event), pressed(event)])
|
||||||
|
elif event_type == 601:
|
||||||
|
seen["stops"].append([stop_x(event), stop_y(event)])
|
||||||
|
unref_event(event)
|
||||||
|
if seen["ends"] == 2:
|
||||||
|
assert seen == {"motion": [[10.0, -5.0], [10.0, -5.0]], "keys": [[30, True], [30, False]], "stops": [[False, True]], "starts": 2, "ends": 2, "keymap": True}, seen
|
||||||
|
print(json.dumps(seen), flush=True)
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
raise AssertionError(f"Timed out: {seen}")
|
||||||
|
finally:
|
||||||
|
unref(context)
|
||||||
@@ -54,7 +54,7 @@ def private_bus_checks():
|
|||||||
raise AssertionError((root / "backend.log").read_text())
|
raise AssertionError((root / "backend.log").read_text())
|
||||||
time.sleep(0.05)
|
time.sleep(0.05)
|
||||||
node = ET.fromstring(xml)
|
node = ET.fromstring(xml)
|
||||||
for name in ("ScreenCast", "Settings", "Screenshot", "Wallpaper", "Background", "Usb", "Lockdown", "GlobalShortcuts", "Inhibit"):
|
for name in ("ScreenCast", "Settings", "Screenshot", "Wallpaper", "Background", "Usb", "Lockdown", "GlobalShortcuts", "Inhibit", "InputCapture"):
|
||||||
interface = node.find(f"interface[@name='org.freedesktop.impl.portal.{name}']")
|
interface = node.find(f"interface[@name='org.freedesktop.impl.portal.{name}']")
|
||||||
assert interface is not None, name
|
assert interface is not None, name
|
||||||
expected = ET.parse(f"/usr/share/dbus-1/interfaces/org.freedesktop.impl.portal.{name}.xml").getroot().find("interface")
|
expected = ET.parse(f"/usr/share/dbus-1/interfaces/org.freedesktop.impl.portal.{name}.xml").getroot().find("interface")
|
||||||
@@ -110,6 +110,18 @@ def private_bus_checks():
|
|||||||
GLib.Variant("(su)", ("org.freedesktop.portal.Desktop", 0)), GLib.VariantType.new("(u)"),
|
GLib.Variant("(su)", ("org.freedesktop.portal.Desktop", 0)), GLib.VariantType.new("(u)"),
|
||||||
Gio.DBusCallFlags.NONE, 5000, None).unpack()[0]
|
Gio.DBusCallFlags.NONE, 5000, None).unpack()[0]
|
||||||
assert request_name == 1
|
assert request_name == 1
|
||||||
|
capture = "org.freedesktop.impl.portal.InputCapture"
|
||||||
|
caps, = call("org.freedesktop.DBus.Properties", "Get", GLib.Variant("(ss)", (capture, "SupportedCapabilities")), "(v)")
|
||||||
|
assert caps == 3
|
||||||
|
session = PATH + "/session/test/inputcapture"
|
||||||
|
call(capture, "CreateSession2", GLib.Variant("(osa{sv})", (session, "org.test.Capture", {})), "(a{sv})")
|
||||||
|
try:
|
||||||
|
call(capture, "GetZones", GLib.Variant("(oosa{sv})", (PATH + "/request/test/zones", session, "org.test.Capture", {})))
|
||||||
|
raise AssertionError("Zones available without approval")
|
||||||
|
except GLib.Error as error:
|
||||||
|
assert "InvalidArgs" in str(error), error
|
||||||
|
bus.call_sync(NAME, session, "org.freedesktop.impl.portal.Session", "Close", None, None, Gio.DBusCallFlags.NONE, 5000, None)
|
||||||
|
|
||||||
version, = call("org.freedesktop.DBus.Properties", "Get", GLib.Variant("(ss)", ("org.freedesktop.impl.portal.ScreenCast", "version")), "(v)")
|
version, = call("org.freedesktop.DBus.Properties", "Get", GLib.Variant("(ss)", ("org.freedesktop.impl.portal.ScreenCast", "version")), "(v)")
|
||||||
assert version == 4
|
assert version == 4
|
||||||
source_types, = call("org.freedesktop.DBus.Properties", "Get", GLib.Variant("(ss)", ("org.freedesktop.impl.portal.ScreenCast", "AvailableSourceTypes")), "(v)")
|
source_types, = call("org.freedesktop.DBus.Properties", "Get", GLib.Variant("(ss)", ("org.freedesktop.impl.portal.ScreenCast", "AvailableSourceTypes")), "(v)")
|
||||||
|
|||||||
Reference in new issue
Block a user