Fix compositor output routing and pointer hints; format imports

This commit is contained in:
Samuel Onoja committed 2026-09-30 06:39:58 +01:00
1 parent 88e4f3c135
commit 46509f880a
108 files changed
+3682 -8371

No files matched your search

+58 -154
View File
@@ -1,26 +1,22 @@
//! ScreenCast portal backend. Only the portal frontend may create sessions;
//! saved monitor consent is restored only through the frontend permission store.
use crate::{
capture::{Capture, Source},
picker::Prompt,
stream::Ready,
};
use std::sync::{
Arc,
atomic::{AtomicBool, Ordering},
};
use std::{collections::HashMap, process::Stdio, time::Duration};
use tokio::{
io::{AsyncBufReadExt, AsyncWriteExt, BufReader},
process::{Child, Command},
sync::{Mutex, Notify},
};
use zbus::{
Connection,
message::Header,
object_server::SignalEmitter,
zvariant::{OwnedObjectPath, OwnedValue, Value},
};
use std::collections::HashMap;
use std::process::Stdio;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::time::Duration;
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
use tokio::process::{Child, Command};
use tokio::sync::{Mutex, Notify};
use zbus::Connection;
use zbus::message::Header;
use zbus::object_server::SignalEmitter;
use zbus::zvariant::{OwnedObjectPath, OwnedValue, Value};
use crate::capture::{Capture, Source};
use crate::picker::Prompt;
use crate::stream::Ready;
pub(crate) type Options = HashMap<String, OwnedValue>;
pub(crate) static CONFIG_TRANSACTION: std::sync::Mutex<()> = std::sync::Mutex::new(());
@@ -105,18 +101,12 @@ impl RecorderControl {
#[zbus(connection)] connection: &Connection,
#[zbus(header)] header: Header<'_>,
) -> zbus::fdo::Result<()> {
let sender = header
.sender()
.ok_or_else(|| error("Missing recorder caller"))?;
let sender = header.sender().ok_or_else(|| error("Missing recorder caller"))?;
if !owns_session(sender.as_str(), session_handle.as_str()) {
return Err(zbus::fdo::Error::AccessDenied(
"Not your recording session".into(),
));
return Err(zbus::fdo::Error::AccessDenied("Not your recording session".into()));
}
let dbus = zbus::fdo::DBusProxy::new(connection).await?;
let pid = dbus
.get_connection_unix_process_id(sender.clone().into())
.await?;
let pid = dbus.get_connection_unix_process_id(sender.clone().into()).await?;
if !shell_recorder(pid) {
return Err(zbus::fdo::Error::AccessDenied(
"The shell must own the recording controls".into(),
@@ -155,18 +145,14 @@ fn shell_recorder(pid: u32) -> bool {
let parent = std::fs::read_to_string(format!("/proc/{pid}/status"))
.ok()
.and_then(|status| {
status.lines().find_map(|line| {
line.strip_prefix("PPid:")
.and_then(|s| s.trim().parse::<u32>().ok())
})
status
.lines()
.find_map(|line| line.strip_prefix("PPid:").and_then(|s| s.trim().parse::<u32>().ok()))
});
parent.is_some_and(|pid| matches_binary(pid, "ferese-shell") == Some(true))
}
pub(crate) async fn authorize(
connection: &Connection,
header: &Header<'_>,
) -> zbus::fdo::Result<String> {
pub(crate) async fn authorize(connection: &Connection, header: &Header<'_>) -> zbus::fdo::Result<String> {
let sender = header
.sender()
.ok_or_else(|| zbus::fdo::Error::AccessDenied("Missing caller".into()))?;
@@ -176,9 +162,7 @@ pub(crate) async fn authorize(
.await
.map_err(|_| zbus::fdo::Error::AccessDenied("Desktop portal is unavailable".into()))?;
if sender.as_str() != owner.as_str() {
return Err(zbus::fdo::Error::AccessDenied(
"Use the desktop portal".into(),
));
return Err(zbus::fdo::Error::AccessDenied("Use the desktop portal".into()));
}
Ok(sender.to_string())
@@ -227,10 +211,7 @@ impl Backend {
let conn = connection.clone();
let path = path.to_owned();
tokio::spawn(async move {
let _ = conn
.object_server()
.remove::<SessionObject, _>(path.as_str())
.await;
let _ = conn.object_server().remove::<SessionObject, _>(path.as_str()).await;
});
}
}
@@ -319,21 +300,14 @@ impl Backend {
) -> zbus::fdo::Result<(u32, Options)> {
let owner = authorize(connection, &header).await?;
let settings = source_options(&options).and_then(|(multiple, cursor, types)| {
Ok((
multiple,
cursor,
types,
crate::restore::persist_mode(&options)?,
))
Ok((multiple, cursor, types, crate::restore::persist_mode(&options)?))
});
let mut sessions = self.sessions.lock().await;
let session = sessions
.get_mut(session_handle.as_str())
.ok_or_else(|| error("Unknown session"))?;
if session.owner != owner
|| session.app != app_id
|| !matches!(session.phase, Phase::Created | Phase::Selected)
if session.owner != owner || session.app != app_id || !matches!(session.phase, Phase::Created | Phase::Selected)
{
return Err(error("Invalid session state"));
}
@@ -369,10 +343,7 @@ impl Backend {
) -> zbus::fdo::Result<(u32, Options)> {
let owner = authorize(connection, &header).await?;
if !handle
.as_str()
.starts_with("/org/freedesktop/portal/desktop/request/")
{
if !handle.as_str().starts_with("/org/freedesktop/portal/desktop/request/") {
return Err(error("Invalid request path"));
}
@@ -435,10 +406,7 @@ impl Backend {
result.unwrap_or_else(|_| Err(StartError::Failed("Sharing request timed out".into())))
}
};
let _ = connection
.object_server()
.remove::<Request, _>(handle)
.await;
let _ = connection.object_server().remove::<Request, _>(handle).await;
match result {
Ok((children, streams, mut reply)) if !cancel.stopped.load(Ordering::SeqCst) => {
@@ -455,8 +423,7 @@ impl Backend {
});
reply.insert(
"streams".into(),
OwnedValue::try_from(Value::from(streams))
.map_err(|e| error(&e.to_string()))?,
OwnedValue::try_from(Value::from(streams)).map_err(|e| error(&e.to_string()))?,
);
Ok((0, reply))
@@ -593,8 +560,7 @@ async fn start_streams(
let rememberable = sources
.iter()
.filter(|source| {
crate::restore::Restore::create(app, cursor, std::slice::from_ref(*source), &outputs, 1)
.is_some()
crate::restore::Restore::create(app, cursor, std::slice::from_ref(*source), &outputs, 1).is_some()
})
.map(|source| source.name.clone())
.collect::<Vec<_>>();
@@ -602,10 +568,7 @@ async fn start_streams(
let selected = restore.resolve(app, cursor, multiple, &sources, &outputs)?;
let selected = validate_selection(
&sources,
&selected
.iter()
.map(|source| source.name.clone())
.collect::<Vec<_>>(),
&selected.iter().map(|source| source.name.clone()).collect::<Vec<_>>(),
multiple,
)
.ok()?;
@@ -707,15 +670,11 @@ async fn start_streams(
.map_err(|e| e.to_string())?;
let mut line = String::new();
let output = child.stdout.take().unwrap();
tokio::time::timeout(
Duration::from_secs(10),
BufReader::new(output).read_line(&mut line),
)
.await
.map_err(|_| "PipeWire stream startup timed out")?
.map_err(|e| e.to_string())?;
let ready: Ready =
serde_json::from_str(&line).map_err(|_| "PipeWire stream could not start")?;
tokio::time::timeout(Duration::from_secs(10), BufReader::new(output).read_line(&mut line))
.await
.map_err(|_| "PipeWire stream startup timed out")?
.map_err(|e| e.to_string())?;
let ready: Ready = serde_json::from_str(&line).map_err(|_| "PipeWire stream could not start")?;
if ready.node == u32::MAX {
return Err("Invalid PipeWire node".into());
@@ -734,10 +693,7 @@ async fn start_streams(
return Err("A selected display changed during stream startup".into());
}
if children
.iter_mut()
.any(|child| !matches!(child.try_wait(), Ok(None)))
{
if children.iter_mut().any(|child| !matches!(child.try_wait(), Ok(None))) {
return Err("A selected stream ended during startup".into());
}
@@ -751,11 +707,10 @@ async fn start_streams(
.find(|source| source.name == name)
.ok_or("Unknown window stream")?;
let id = source.window_id().ok_or("Invalid selected window")?;
if !final_windows.as_array().is_some_and(|windows| {
windows
.iter()
.any(|window| window["id"].as_u64() == Some(id))
}) {
if !final_windows
.as_array()
.is_some_and(|windows| windows.iter().any(|window| window["id"].as_u64() == Some(id)))
{
return Err("A selected window closed during startup".to_owned());
}
window_metadata(source, &ready)
@@ -773,9 +728,7 @@ fn window_metadata(source: &Source, ready: &Ready) -> Result<Options, String> {
if source.window_id().is_none() || ready.width == 0 || ready.height == 0 {
return Err("Invalid window stream".into());
}
let (width, height) = ready
.logical_size
.ok_or("Missing captured window geometry")?;
let (width, height) = ready.logical_size.ok_or("Missing captured window geometry")?;
let size = (
i32::try_from(width).map_err(|_| "Invalid window width")?,
i32::try_from(height).map_err(|_| "Invalid window height")?,
@@ -787,25 +740,15 @@ fn window_metadata(source: &Source, ready: &Ready) -> Result<Options, String> {
("source_type".into(), 2u32.into()),
(
"size".into(),
Value::from(size)
.try_to_owned()
.map_err(|e| e.to_string())?,
Value::from(size).try_to_owned().map_err(|e| e.to_string())?,
),
]))
}
fn monitor_metadata(
outputs: &serde_json::Value,
name: &str,
ready: &Ready,
) -> Result<Options, String> {
fn monitor_metadata(outputs: &serde_json::Value, name: &str, ready: &Ready) -> Result<Options, String> {
let output = outputs
.as_array()
.and_then(|outputs| {
outputs
.iter()
.find(|output| output["name"].as_str() == Some(name))
})
.and_then(|outputs| outputs.iter().find(|output| output["name"].as_str() == Some(name)))
.ok_or("Selected monitor is unavailable")?;
let integer = |key: &str| {
output[key]
@@ -851,23 +794,13 @@ fn monitor_metadata(
),
(
"size".into(),
Value::from(size)
.try_to_owned()
.map_err(|error| error.to_string())?,
Value::from(size).try_to_owned().map_err(|error| error.to_string())?,
),
]))
}
fn validate_selection(
sources: &[Source],
names: &[String],
multiple: bool,
) -> Result<Vec<Source>, String> {
if names.is_empty()
|| names.len() > 8
|| names.len() > sources.len()
|| (!multiple && names.len() != 1)
{
fn validate_selection(sources: &[Source], names: &[String], multiple: bool) -> Result<Vec<Source>, String> {
if names.is_empty() || names.len() > 8 || names.len() > sources.len() || (!multiple && names.len() != 1) {
return Err("Invalid display selection".into());
}
@@ -890,11 +823,7 @@ fn validate_selection(
async fn supervise(mut children: Vec<Child>, cancel: Arc<Cancel>) {
loop {
if cancel.stopped.load(Ordering::SeqCst)
|| children
.iter_mut()
.any(|c| !matches!(c.try_wait(), Ok(None)))
{
if cancel.stopped.load(Ordering::SeqCst) || children.iter_mut().any(|c| !matches!(c.try_wait(), Ok(None))) {
break;
}
tokio::time::sleep(Duration::from_millis(100)).await;
@@ -928,10 +857,7 @@ pub async fn run() -> Result<(), Box<dyn std::error::Error>> {
.serve_at(PATH, shortcuts.clone())?
.serve_at(PATH, inhibit.clone())?
.serve_at(PATH, input_capture.clone())?
.serve_at(
"/org/ferese/ScreenRecorder",
RecorderControl(backend.clone()),
)?
.serve_at("/org/ferese/ScreenRecorder", RecorderControl(backend.clone()))?
.build()
.await?;
@@ -955,9 +881,7 @@ pub async fn run() -> Result<(), Box<dyn std::error::Error>> {
requests.revoke_stale(owner.as_deref()).await;
shortcuts.revoke_stale(&connection, owner.as_deref()).await;
inhibit.revoke_stale(&connection, owner.as_deref()).await;
input_capture
.revoke_stale(&connection, owner.as_deref())
.await;
input_capture.revoke_stale(&connection, owner.as_deref()).await;
let stale: Vec<_> = backend
.sessions
@@ -1057,10 +981,7 @@ mod tests {
scale: 1,
})
.collect::<Vec<_>>();
let names = sources
.iter()
.map(|source| source.name.clone())
.collect::<Vec<_>>();
let names = sources.iter().map(|source| source.name.clone()).collect::<Vec<_>>();
assert!(validate_selection(&sources, &names[..8], true).is_ok());
assert!(validate_selection(&sources, &names, true).is_err());
assert!(validate_selection(&sources, &names[..2], false).is_err());
@@ -1117,10 +1038,7 @@ mod tests {
scale: 1,
})
.collect::<Vec<_>>();
let names = sources
.iter()
.map(|source| source.name.clone())
.collect::<Vec<_>>();
let names = sources.iter().map(|source| source.name.clone()).collect::<Vec<_>>();
assert_eq!(validate_selection(&sources, &names, true).unwrap().len(), 6);
assert!(validate_selection(&sources, &names, false).is_err());
}
@@ -1137,22 +1055,8 @@ mod tests {
scale: 1,
};
assert!(validate_selection(std::slice::from_ref(&source), &[], false).is_err());
assert!(
validate_selection(std::slice::from_ref(&source), &["other".into()], false).is_err()
);
assert!(
validate_selection(
std::slice::from_ref(&source),
&["test".into(), "test".into()],
true
)
.is_err()
);
assert_eq!(
validate_selection(&[source], &["test".into()], false)
.unwrap()
.len(),
1
);
assert!(validate_selection(std::slice::from_ref(&source), &["other".into()], false).is_err());
assert!(validate_selection(std::slice::from_ref(&source), &["test".into(), "test".into()], true).is_err());
assert_eq!(validate_selection(&[source], &["test".into()], false).unwrap().len(), 1);
}
}
+8 -17
View File
@@ -1,5 +1,8 @@
use std::os::unix::net::UnixStream;
use std::sync::Arc;
use std::time::Duration;
use serde_json::Value as Json;
use std::{os::unix::net::UnixStream, sync::Arc, time::Duration};
#[derive(Clone)]
pub(crate) struct Bridge(Arc<BridgeInner>);
@@ -41,17 +44,9 @@ impl Bridge {
async fn request(&self, command: &'static str, args: Json, wait: bool) -> Result<Json, String> {
let bridge = self.clone();
tokio::task::spawn_blocking(move || {
let mut stream = bridge
.0
.stream
.lock()
.map_err(|_| "Compositor connection failed")?;
let mut stream = bridge.0.stream.lock().map_err(|_| "Compositor connection failed")?;
stream
.set_read_timeout(if wait {
None
} else {
Some(Duration::from_secs(2))
})
.set_read_timeout(if wait { None } else { Some(Duration::from_secs(2)) })
.map_err(|error| error.to_string())?;
let request = ferese_ipc::Request {
version: ferese_ipc::VERSION,
@@ -73,12 +68,8 @@ impl Bridge {
}
pub(crate) async fn capture_watch(&self, session: u64) -> Result<Json, String> {
self.request(
"input-capture-watch",
serde_json::json!({"session":session}),
true,
)
.await
self.request("input-capture-watch", serde_json::json!({"session":session}), true)
.await
}
pub(crate) async fn closed(&self) {
+28 -88
View File
@@ -1,20 +1,15 @@
//! Bounded, cancellable Wayland capture. No screenshots or external capture tools.
use std::fs::File;
use std::os::fd::{AsFd, AsRawFd, FromRawFd};
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::time::{Duration, Instant};
use ferese_protocols::window_capture::v1::client::ferese_window_capture_manager_v1 as window_manager;
use memmap2::MmapMut;
use serde::{Deserialize, Serialize};
use std::{
fs::File,
os::fd::{AsFd, AsRawFd, FromRawFd},
sync::{
Arc,
atomic::{AtomicBool, Ordering},
},
time::{Duration, Instant},
};
use wayland_client::{
Connection, Dispatch, EventQueue, QueueHandle, WEnum,
protocol::{wl_buffer, wl_output, wl_registry, wl_shm, wl_shm_pool},
};
use wayland_client::protocol::{wl_buffer, wl_output, wl_registry, wl_shm, wl_shm_pool};
use wayland_client::{Connection, Dispatch, EventQueue, QueueHandle, WEnum};
use wayland_protocols_wlr::screencopy::v1::client::{
zwlr_screencopy_frame_v1 as frame, zwlr_screencopy_manager_v1 as manager,
};
@@ -104,9 +99,7 @@ impl Capture {
// uninterruptible roundtrip on a stalled compositor.
this.sync(stop)?;
this.sync(stop)?;
if (this.state.manager.is_none() && this.state.window_manager.is_none())
|| this.state.shm.is_none()
{
if (this.state.manager.is_none() && this.state.window_manager.is_none()) || this.state.shm.is_none() {
return Err("Ferese screen capture is unavailable in this session".into());
}
Ok(this)
@@ -114,9 +107,7 @@ impl Capture {
fn sync(&mut self, stop: &AtomicBool) -> Result<(), String> {
let done = Arc::new(AtomicBool::new(false));
self.connection
.display()
.sync(&self.queue.handle(), done.clone());
self.connection.display().sync(&self.queue.handle(), done.clone());
let deadline = Instant::now() + Duration::from_secs(5);
while !done.load(Ordering::Relaxed) {
self.dispatch(stop, deadline)?;
@@ -168,34 +159,19 @@ impl Capture {
.collect()
}
pub fn frame(
&mut self,
name: &str,
cursor: bool,
stop: &AtomicBool,
mut pixels: Vec<u8>,
) -> Result<Frame, String> {
pub fn frame(&mut self, name: &str, cursor: bool, stop: &AtomicBool, mut pixels: Vec<u8>) -> Result<Frame, String> {
self.state.result = None;
self.state.flipped = false;
self.state.logical_size = None;
self.state.busy = false;
let (frame, transform) = if let Some(id) = name
.strip_prefix("window:")
.and_then(|id| id.parse::<u64>().ok())
{
let (frame, transform) = if let Some(id) = name.strip_prefix("window:").and_then(|id| id.parse::<u64>().ok()) {
let manager = self
.state
.window_manager
.as_ref()
.ok_or("Window capture is unavailable")?;
(
manager.capture_window(
(id >> 32) as u32,
id as u32,
cursor as u32,
&self.queue.handle(),
(),
),
manager.capture_window((id >> 32) as u32, id as u32, cursor as u32, &self.queue.handle(), ()),
wl_output::Transform::Normal,
)
} else {
@@ -205,18 +181,11 @@ impl Capture {
.iter()
.find(|o| o.source.name == name)
.ok_or("Shared monitor disconnected")?;
if self
.selected_output
.is_some_and(|selected| selected != output.global)
{
if self.selected_output.is_some_and(|selected| selected != output.global) {
return Err("Shared monitor was replaced".into());
}
self.selected_output = Some(output.global);
let manager = self
.state
.manager
.as_ref()
.ok_or("Monitor capture is unavailable")?;
let manager = self.state.manager.as_ref().ok_or("Monitor capture is unavailable")?;
(
manager.capture_output(cursor as i32, &output.proxy, &self.queue.handle(), ()),
output.transform,
@@ -234,11 +203,7 @@ impl Capture {
frame.destroy();
self.state.result.take().unwrap()?;
let buffer = self
.state
.buffer
.as_ref()
.ok_or("Capture supplied no buffer")?;
let buffer = self.state.buffer.as_ref().ok_or("Capture supplied no buffer")?;
let (width, height) = copy_oriented_pixels(
&buffer.map,
buffer.width,
@@ -283,9 +248,7 @@ impl Capture {
if ready > 0 {
read.read().map_err(|e| e.to_string())?;
} else if ready < 0
&& std::io::Error::last_os_error().kind() != std::io::ErrorKind::Interrupted
{
} else if ready < 0 && std::io::Error::last_os_error().kind() != std::io::ErrorKind::Interrupted {
return Err(std::io::Error::last_os_error().to_string());
}
}
@@ -327,13 +290,9 @@ impl Dispatch<wl_registry::WlRegistry, ()> for State {
scale: 1,
},
}),
"ferese_window_capture_manager_v1" => {
state.window_manager = Some(registry.bind(name, 1, qh, ()))
}
"ferese_window_capture_manager_v1" => state.window_manager = Some(registry.bind(name, 1, qh, ())),
"wl_shm" => state.shm = Some(registry.bind(name, 1, qh, ())),
"zwlr_screencopy_manager_v1" => {
state.manager = Some(registry.bind(name, version.min(3), qh, ()))
}
"zwlr_screencopy_manager_v1" => state.manager = Some(registry.bind(name, version.min(3), qh, ())),
_ => (),
},
wl_registry::Event::GlobalRemove { name } => state.outputs.retain(|o| o.global != name),
@@ -358,9 +317,7 @@ impl Dispatch<wl_output::WlOutput, u32> for State {
match event {
wl_output::Event::Name { name } => output.source.name = name,
wl_output::Event::Description { description } => output.source.label = description,
wl_output::Event::Geometry {
x, y, transform, ..
} => {
wl_output::Event::Geometry { x, y, transform, .. } => {
if let WEnum::Value(transform) = transform {
output.transform = transform;
}
@@ -412,9 +369,7 @@ impl Dispatch<frame::ZwlrScreencopyFrameV1, ()> for State {
return Ok(());
}
// SAFETY: fixed NUL-terminated name and a newly owned memfd.
let fd = unsafe {
libc::memfd_create(c"ferese-screencast".as_ptr(), libc::MFD_CLOEXEC)
};
let fd = unsafe { libc::memfd_create(c"ferese-screencast".as_ptr(), libc::MFD_CLOEXEC) };
if fd < 0 {
return Err(std::io::Error::last_os_error().to_string());
@@ -424,20 +379,13 @@ impl Dispatch<frame::ZwlrScreencopyFrameV1, ()> for State {
file.set_len(bytes as u64).map_err(|e| e.to_string())?;
// SAFETY: the file has the checked mapping length and lives with the mapping.
let map = unsafe { MmapMut::map_mut(&file) }.map_err(|e| e.to_string())?;
let pool = state
.shm
.as_ref()
.ok_or("Missing shared memory global")?
.create_pool(file.as_fd(), bytes as i32, qh, ());
let proxy = pool.create_buffer(
0,
width as i32,
height as i32,
stride as i32,
format,
let pool = state.shm.as_ref().ok_or("Missing shared memory global")?.create_pool(
file.as_fd(),
bytes as i32,
qh,
(),
);
let proxy = pool.create_buffer(0, width as i32, height as i32, stride as i32, format, qh, ());
pool.destroy();
frame.copy(&proxy);
@@ -540,9 +488,7 @@ fn copy_oriented_pixels(
fn buffer_size(width: u32, height: u32, stride: u32) -> Result<usize, String> {
let row = width.checked_mul(4).ok_or("Capture dimensions overflow")?;
let bytes = stride
.checked_mul(height)
.ok_or("Capture dimensions overflow")?;
let bytes = stride.checked_mul(height).ok_or("Capture dimensions overflow")?;
if width == 0 || height == 0 || stride < row || bytes > 128 * 1024 * 1024 {
return Err("Unsupported capture dimensions".into());
@@ -578,9 +524,7 @@ impl Dispatch<window_manager::FereseWindowCaptureManagerV1, ()> for State {
_: &QueueHandle<Self>,
) {
match event {
window_manager::Event::Geometry { width, height, .. } => {
state.logical_size = Some((width, height))
}
window_manager::Event::Geometry { width, height, .. } => state.logical_size = Some((width, height)),
window_manager::Event::Busy { .. } => state.busy = true,
_ => (),
}
@@ -609,11 +553,7 @@ mod tests {
for (transform, size, expected) in cases {
for y_invert in [false, true] {
let mut source = Vec::new();
let rows = if y_invert {
[b"def", b"abc"]
} else {
[b"abc", b"def"]
};
let rows = if y_invert { [b"def", b"abc"] } else { [b"abc", b"def"] };
for row in rows {
for &value in row {
source.extend_from_slice(&pixel(value));
+11 -23
View File
@@ -1,14 +1,14 @@
use cosmic::{
Element,
app::{Core, Settings, Task},
iced::{Alignment, Length},
widget::{button, column, container, image, row, scrollable, text_input},
};
use ferese_theme::{Palette, accent_button, controls, material::ModalMaterial, text};
use serde::{Deserialize, Serialize};
use std::io::{Read, Write};
use std::sync::Arc;
use cosmic::Element;
use cosmic::app::{Core, Settings, Task};
use cosmic::iced::{Alignment, Length};
use cosmic::widget::{button, column, container, image, row, scrollable, text_input};
use ferese_theme::material::ModalMaterial;
use ferese_theme::{Palette, accent_button, controls, text};
use serde::{Deserialize, Serialize};
#[derive(Clone, Serialize, Deserialize)]
pub(crate) struct Prompt {
pub title: String,
@@ -30,15 +30,7 @@ pub(crate) struct ShortcutField {
#[derive(Clone, Debug)]
enum Message {
Opened(cosmic::iced::window::Id),
Attached(
Result<
(
Option<Arc<crate::parent::Parent>>,
Result<ModalMaterial, String>,
),
String,
>,
),
Attached(Result<(Option<Arc<crate::parent::Parent>>, Result<ModalMaterial, String>), String>),
Accept,
Cancel,
Shortcut(usize, String),
@@ -56,9 +48,7 @@ struct Consent {
pub(crate) fn run() -> Result<(), Box<dyn std::error::Error>> {
let mut input = String::new();
std::io::stdin()
.take(256 * 1024 + 1)
.read_to_string(&mut input)?;
std::io::stdin().take(256 * 1024 + 1).read_to_string(&mut input)?;
if input.len() > 256 * 1024 {
return Err("Consent request is too large".into());
@@ -124,9 +114,7 @@ impl cosmic::Application for Consent {
fn subscription(&self) -> cosmic::iced::Subscription<Message> {
cosmic::iced::event::listen_with(|event, _, id| match event {
cosmic::iced::Event::Window(cosmic::iced::window::Event::Opened { .. }) => {
Some(Message::Opened(id))
}
cosmic::iced::Event::Window(cosmic::iced::window::Event::Opened { .. }) => Some(Message::Opened(id)),
cosmic::iced::Event::Keyboard(cosmic::iced::keyboard::Event::KeyPressed {
key: cosmic::iced::keyboard::Key::Named(cosmic::iced::keyboard::key::Named::Escape),
..
+55 -125
View File
@@ -1,22 +1,21 @@
use crate::{
backend::{Cancel, Options, Request, authorize},
consent::Prompt,
};
use std::{
collections::HashMap,
io::Read,
os::unix::{fs::PermissionsExt, net::UnixStream},
path::{Path, PathBuf},
process::Stdio,
sync::{Arc, atomic::Ordering},
time::Duration,
};
use tokio::{io::AsyncWriteExt, sync::Mutex};
use zbus::{
Connection,
message::Header,
zvariant::{OwnedObjectPath, Value},
};
use std::collections::HashMap;
use std::io::Read;
use std::os::unix::fs::PermissionsExt;
use std::os::unix::net::UnixStream;
use std::path::{Path, PathBuf};
use std::process::Stdio;
use std::sync::Arc;
use std::sync::atomic::Ordering;
use std::time::Duration;
use tokio::io::AsyncWriteExt;
use tokio::sync::Mutex;
use zbus::Connection;
use zbus::message::Header;
use zbus::zvariant::{OwnedObjectPath, Value};
use crate::backend::{Cancel, Options, Request, authorize};
use crate::consent::Prompt;
const PATH: &str = "/org/freedesktop/portal/desktop";
const MAX_IMAGE_BYTES: u64 = 128 * 1024 * 1024;
@@ -56,9 +55,7 @@ impl Requests {
}
let mut pending = self.0.lock().await;
if pending.len() >= self.1 || pending.contains_key(handle.as_str()) {
return Err(zbus::fdo::Error::LimitsExceeded(
"Request limit reached".into(),
));
return Err(zbus::fdo::Error::LimitsExceeded("Request limit reached".into()));
}
let cancel = Arc::new(Cancel::default());
if !connection
@@ -72,9 +69,7 @@ impl Requests {
)
.await?
{
return Err(zbus::fdo::Error::InvalidArgs(
"Request already exists".into(),
));
return Err(zbus::fdo::Error::InvalidArgs("Request already exists".into()));
}
pending.insert(handle.to_string(), (owner, cancel.clone()));
Ok(cancel)
@@ -82,10 +77,7 @@ impl Requests {
pub(crate) async fn end(&self, connection: &Connection, handle: &OwnedObjectPath) {
self.0.lock().await.remove(handle.as_str());
let _ = connection
.object_server()
.remove::<Request, _>(handle)
.await;
let _ = connection.object_server().remove::<Request, _>(handle).await;
}
}
@@ -231,10 +223,7 @@ pub(crate) async fn consent(
}
}
pub(crate) async fn ipc(
command: &'static str,
args: serde_json::Value,
) -> Result<serde_json::Value, String> {
pub(crate) async fn ipc(command: &'static str, args: serde_json::Value) -> Result<serde_json::Value, String> {
tokio::task::spawn_blocking(move || ipc_sync(command, args))
.await
.map_err(|error| error.to_string())?
@@ -260,8 +249,7 @@ fn ipc_sync(command: &'static str, args: serde_json::Value) -> Result<serde_json
args,
};
ferese_ipc::write_frame(&mut stream, &request).map_err(|error| error.to_string())?;
let response: ferese_ipc::Response =
ferese_ipc::read_frame(&mut stream).map_err(|error| error.to_string())?;
let response: ferese_ipc::Response = ferese_ipc::read_frame(&mut stream).map_err(|error| error.to_string())?;
if let Some(error) = response.error {
return Err(error.message);
}
@@ -313,10 +301,7 @@ async fn capture(geometry: Option<String>) -> Result<Vec<u8>, String> {
capture_command("screenshot", serde_json::json!({"geometry": geometry})).await
}
async fn capture_command(
command: &'static str,
args: serde_json::Value,
) -> Result<Vec<u8>, String> {
async fn capture_command(command: &'static str, args: serde_json::Value) -> Result<Vec<u8>, String> {
tokio::task::spawn_blocking(move || {
let result = ipc_sync(command, args)?;
let path = result["path"].as_str().ok_or("Missing screenshot path")?;
@@ -462,20 +447,14 @@ fn window_sources(windows: &serde_json::Value) -> Result<Vec<crate::capture::Sou
Ok(sources)
}
pub(crate) fn sharing_window_sources(
windows: &serde_json::Value,
) -> Result<Vec<crate::capture::Source>, String> {
pub(crate) fn sharing_window_sources(windows: &serde_json::Value) -> Result<Vec<crate::capture::Source>, String> {
let windows = windows.as_array().ok_or("Invalid window list")?;
let eligible = windows
.iter()
.filter(|window| {
window["workspace"].as_u64().is_some()
&& window["capture_width"]
.as_i64()
.is_some_and(|width| width > 0)
&& window["capture_height"]
.as_i64()
.is_some_and(|height| height > 0)
&& window["capture_width"].as_i64().is_some_and(|width| width > 0)
&& window["capture_height"].as_i64().is_some_and(|height| height > 0)
&& window["app_id"].as_str() != Some("dev.ferese.ScreenShare")
})
.take(128)
@@ -530,8 +509,7 @@ async fn select_window(app: &str, parent: &str) -> Result<Option<u64>, String> {
if !output.status.success() {
return Ok(None);
}
let names: Vec<String> =
serde_json::from_slice(&output.stdout).map_err(|_| "Invalid window selection")?;
let names: Vec<String> = serde_json::from_slice(&output.stdout).map_err(|_| "Invalid window selection")?;
if names.len() != 1 || !prompt.sources.iter().any(|source| source.name == names[0]) {
return Err("Unknown selected window".into());
}
@@ -562,9 +540,7 @@ async fn pick_color(app: &str, parent: &str) -> Result<Option<Options>, String>
let color = png_color(&bytes)?;
Ok(Some(HashMap::from([(
"color".into(),
Value::from(color)
.try_to_owned()
.map_err(|error| error.to_string())?,
Value::from(color).try_to_owned().map_err(|error| error.to_string())?,
)])))
}
@@ -577,9 +553,7 @@ fn png_color(bytes: &[u8]) -> Result<(f64, f64, f64), String> {
.filter(|size| *size <= 16 * 1024)
.ok_or("Unexpected pixel dimensions")?;
let mut pixels = vec![0; size];
let info = reader
.next_frame(&mut pixels)
.map_err(|error| error.to_string())?;
let info = reader.next_frame(&mut pixels).map_err(|error| error.to_string())?;
if info.width == 0 || info.height == 0 {
return Err("Empty image".into());
}
@@ -599,8 +573,7 @@ fn private_directory(name: &str) -> Result<PathBuf, String> {
.join("ferese/portal")
.join(name);
std::fs::create_dir_all(&directory).map_err(|error| error.to_string())?;
std::fs::set_permissions(&directory, std::fs::Permissions::from_mode(0o700))
.map_err(|error| error.to_string())?;
std::fs::set_permissions(&directory, std::fs::Permissions::from_mode(0o700)).map_err(|error| error.to_string())?;
Ok(directory)
}
@@ -668,11 +641,7 @@ fn prepare_wallpaper(input: &Path, cancel: &Cancel) -> Result<tempfile::NamedTem
.custom_flags(libc::O_NONBLOCK)
.open(input)
.map_err(|error| error.to_string())?;
if !input
.metadata()
.map_err(|error| error.to_string())?
.is_file()
{
if !input.metadata().map_err(|error| error.to_string())?.is_file() {
return Err("Wallpaper must be a regular file".into());
}
let directory = private_directory("wallpapers")?;
@@ -695,8 +664,7 @@ fn prepare_wallpaper(input: &Path, cancel: &Cancel) -> Result<tempfile::NamedTem
if size > MAX_IMAGE_BYTES {
return Err("Wallpaper is too large".into());
}
std::io::Write::write_all(&mut file, &buffer[..bytes])
.map_err(|error| error.to_string())?;
std::io::Write::write_all(&mut file, &buffer[..bytes]).map_err(|error| error.to_string())?;
}
if size == 0 {
return Err("Wallpaper is empty".into());
@@ -705,9 +673,7 @@ fn prepare_wallpaper(input: &Path, cancel: &Cancel) -> Result<tempfile::NamedTem
.map_err(|error| error.to_string())?
.with_guessed_format()
.map_err(|error| error.to_string())?;
let (width, height) = reader
.into_dimensions()
.map_err(|error| error.to_string())?;
let (width, height) = reader.into_dimensions().map_err(|error| error.to_string())?;
if width == 0
|| height == 0
|| width > 16384
@@ -741,8 +707,7 @@ fn edit_wallpaper(path: &Path, image: &Path, target: &str) -> Result<(), String>
Err(error) if error.kind() == std::io::ErrorKind::NotFound => String::new(),
Err(error) => return Err(error.to_string()),
};
let mut document =
ferese_config::Document::parse(&source).map_err(|error| error.to_string())?;
let mut document = ferese_config::Document::parse(&source).map_err(|error| error.to_string())?;
if target == "background" && document.get("theme.background.lock_path").is_none() {
let old = document
.get("theme.background.path")
@@ -755,10 +720,7 @@ fn edit_wallpaper(path: &Path, image: &Path, target: &str) -> Result<(), String>
}
if target != "lockscreen" {
document
.set(
"theme.background.path",
image.to_string_lossy().into_owned().into(),
)
.set("theme.background.path", image.to_string_lossy().into_owned().into())
.map_err(|error| error.to_string())?;
}
if target != "background" {
@@ -772,11 +734,8 @@ fn edit_wallpaper(path: &Path, image: &Path, target: &str) -> Result<(), String>
let parent = path.parent().ok_or("Invalid config path")?;
std::fs::create_dir_all(parent).map_err(|error| error.to_string())?;
let mut temp = tempfile::NamedTempFile::new_in(parent).map_err(|error| error.to_string())?;
std::io::Write::write_all(&mut temp, document.to_string().as_bytes())
.map_err(|error| error.to_string())?;
temp.as_file()
.sync_all()
.map_err(|error| error.to_string())?;
std::io::Write::write_all(&mut temp, document.to_string().as_bytes()).map_err(|error| error.to_string())?;
temp.as_file().sync_all().map_err(|error| error.to_string())?;
if std::fs::read_to_string(&path).unwrap_or_default() != source {
return Err("Configuration changed; please try again".into());
}
@@ -806,11 +765,7 @@ mod tests {
fn local_uris_preserve_special_characters_and_reject_network_paths() {
let path = Path::new("/tmp/wallpaper #1_日本.png");
assert_eq!(local_path(&file_uri(path).unwrap()).unwrap(), path);
for uri in [
"https://example.com/a.png",
"file://example.com/a.png",
"not a URI",
] {
for uri in ["https://example.com/a.png", "file://example.com/a.png", "not a URI"] {
assert!(local_path(uri).is_err());
}
}
@@ -862,46 +817,25 @@ mod tests {
fn wallpaper_targets_preserve_independent_paths_and_other_settings() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("config.kdl");
std::fs::write(&path, "// Keep my settings\ntheme { background { path \"/old.png\"; }; }; commands { terminal \"foot\"; }\n").unwrap();
std::fs::write(
&path,
"// Keep my settings\ntheme { background { path \"/old.png\"; }; }; commands { terminal \"foot\"; }\n",
)
.unwrap();
edit_wallpaper(&path, Path::new("/desktop.png"), "background").unwrap();
let document =
ferese_config::Document::parse(&std::fs::read_to_string(&path).unwrap()).unwrap();
assert_eq!(
document.get("theme.background.path").unwrap(),
"/desktop.png"
);
assert_eq!(
document.get("theme.background.lock_path").unwrap(),
"/old.png"
);
assert!(
std::fs::read_to_string(&path)
.unwrap()
.contains("Keep my settings")
);
let document = ferese_config::Document::parse(&std::fs::read_to_string(&path).unwrap()).unwrap();
assert_eq!(document.get("theme.background.path").unwrap(), "/desktop.png");
assert_eq!(document.get("theme.background.lock_path").unwrap(), "/old.png");
assert!(std::fs::read_to_string(&path).unwrap().contains("Keep my settings"));
edit_wallpaper(&path, Path::new("/lock.png"), "lockscreen").unwrap();
let document =
ferese_config::Document::parse(&std::fs::read_to_string(&path).unwrap()).unwrap();
assert_eq!(
document.get("theme.background.path").unwrap(),
"/desktop.png"
);
assert_eq!(
document.get("theme.background.lock_path").unwrap(),
"/lock.png"
);
let document = ferese_config::Document::parse(&std::fs::read_to_string(&path).unwrap()).unwrap();
assert_eq!(document.get("theme.background.path").unwrap(), "/desktop.png");
assert_eq!(document.get("theme.background.lock_path").unwrap(), "/lock.png");
edit_wallpaper(&path, Path::new("/both.png"), "both").unwrap();
let document =
ferese_config::Document::parse(&std::fs::read_to_string(&path).unwrap()).unwrap();
let document = ferese_config::Document::parse(&std::fs::read_to_string(&path).unwrap()).unwrap();
assert_eq!(document.get("theme.background.path").unwrap(), "/both.png");
assert_eq!(
document.get("theme.background.lock_path").unwrap(),
"/both.png"
);
assert_eq!(
document.get("commands.terminal").unwrap(),
&serde_json::json!(["foot"])
);
assert_eq!(document.get("theme.background.lock_path").unwrap(), "/both.png");
assert_eq!(document.get("commands.terminal").unwrap(), &serde_json::json!(["foot"]));
}
#[test]
@@ -912,10 +846,6 @@ mod tests {
std::fs::write(&path, [0u8; 32]).unwrap();
let cancel = Cancel::default();
cancel.stop();
assert!(
prepare_wallpaper(&path, &cancel)
.unwrap_err()
.contains("cancelled")
);
assert!(prepare_wallpaper(&path, &cancel).unwrap_err().contains("cancelled"));
}
}
+47 -101
View File
@@ -1,23 +1,15 @@
use std::{
fs::File,
io::Write,
os::{
fd::{AsFd, OwnedFd},
unix::net::UnixStream,
},
sync::{
Arc,
atomic::{AtomicBool, Ordering},
},
};
use std::fs::File;
use std::io::Write;
use std::os::fd::{AsFd, OwnedFd};
use std::os::unix::net::UnixStream;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use calloop::{EventLoop, LoopSignal, PostAction, channel};
use reis::{
calloop::{EisRequestSource, EisRequestSourceEvent},
eis,
enumflags2::BitFlags,
request::{Connection, Device, DeviceCapability, EisRequest},
};
use reis::calloop::{EisRequestSource, EisRequestSourceEvent};
use reis::eis;
use reis::enumflags2::BitFlags;
use reis::request::{Connection, Device, DeviceCapability, EisRequest};
use serde_json::Value;
use crate::backend::Cancel;
@@ -29,11 +21,7 @@ pub(crate) struct Worker {
}
impl Worker {
pub(crate) fn start(
capabilities: u32,
keymap: &str,
cancel: Arc<Cancel>,
) -> Result<(Self, OwnedFd), String> {
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());
}
@@ -59,8 +47,7 @@ impl Worker {
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 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
@@ -72,22 +59,19 @@ impl Worker {
.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() {
.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
}
Ok(action)
},
)
};
if connection.flush().is_err() {
state.signal.stop();
}
Ok(action)
})
.map_err(|e| e.to_string())?;
let mut state = State {
signal: signal.clone(),
@@ -107,8 +91,7 @@ impl Worker {
.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);
connection.disconnected(eis::connection::DisconnectReason::Disconnected, None);
}
state.ready.store(false, Ordering::SeqCst);
Ok::<(), String>(())
@@ -121,14 +104,7 @@ impl Worker {
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(),
))
Ok((Self { sender, signal, ready }, client.into()))
}
pub(crate) fn send(&self, events: Vec<Value>) -> Result<(), String> {
@@ -180,18 +156,14 @@ impl State {
capabilities |= DeviceCapability::Keyboard;
}
if self.capabilities & 2 != 0 {
capabilities |= DeviceCapability::Pointer
| DeviceCapability::Button
| DeviceCapability::Scroll;
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;
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;
@@ -207,11 +179,7 @@ impl State {
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(),
);
keyboard.keymap(eis::keyboard::KeymapType::Xkb, self.keymap_size, self.keymap.as_fd());
}
},
);
@@ -221,15 +189,11 @@ impl State {
}
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,
|_| {},
);
& (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);
@@ -263,14 +227,11 @@ impl State {
return PostAction::Remove;
}
self.ready.store(
(self.keyboard.is_none() || self.keyboard_ready)
&& (self.pointer.is_none() || self.pointer_ready),
(self.keyboard.is_none() || self.keyboard_ready) && (self.pointer.is_none() || self.pointer_ready),
Ordering::SeqCst,
);
}
EisRequestSourceEvent::Request(
EisRequest::Disconnect | EisRequest::DeviceClosed(_),
) => {
EisRequestSourceEvent::Request(EisRequest::Disconnect | EisRequest::DeviceClosed(_)) => {
self.signal.stop();
return PostAction::Remove;
}
@@ -307,12 +268,7 @@ impl State {
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)
{
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());
@@ -392,10 +348,7 @@ impl State {
}
fn now() -> u64 {
let mut time = libc::timespec {
tv_sec: 0,
tv_nsec: 0,
};
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
@@ -406,12 +359,12 @@ fn now() -> u64 {
#[cfg(test)]
mod tests {
use super::*;
use std::process::{Command, Stdio};
use std::time::{Duration, Instant};
use serde_json::json;
use std::{
process::{Command, Stdio},
time::{Duration, Instant},
};
use super::*;
#[test]
#[ignore = "requires Python and the system libei receiver library"]
@@ -419,8 +372,8 @@ mod tests {
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 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))
@@ -435,10 +388,7 @@ mod tests {
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)
);
panic!("Receiver did not bind: {}", String::from_utf8_lossy(&output.stderr));
}
worker
.send(vec![
@@ -453,11 +403,7 @@ mod tests {
])
.unwrap();
let output = client.wait_with_output().unwrap();
assert!(
output.status.success(),
"{}",
String::from_utf8_lossy(&output.stderr)
);
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 {
+74 -92
View File
@@ -1,21 +1,17 @@
use std::{
collections::HashMap,
sync::{Arc, atomic::Ordering},
time::Duration,
};
use tokio::sync::{Mutex, Notify};
use zbus::{
Connection,
message::Header,
object_server::SignalEmitter,
zvariant::{OwnedFd, OwnedObjectPath, Value},
};
use std::collections::HashMap;
use std::sync::Arc;
use std::sync::atomic::Ordering;
use std::time::Duration;
use crate::{
backend::{Cancel, Options, authorize},
bridge::Bridge,
desktop::Requests,
};
use tokio::sync::{Mutex, Notify};
use zbus::Connection;
use zbus::message::Header;
use zbus::object_server::SignalEmitter;
use zbus::zvariant::{OwnedFd, OwnedObjectPath, Value};
use crate::backend::{Cancel, Options, authorize};
use crate::bridge::Bridge;
use crate::desktop::Requests;
const PATH: &str = "/org/freedesktop/portal/desktop";
@@ -68,10 +64,7 @@ impl Inhibit {
if let Ok(emitter) = SignalEmitter::new(connection, path) {
let _ = MonitorObject::closed(&emitter).await;
}
let _ = connection
.object_server()
.remove::<MonitorObject, _>(path)
.await;
let _ = connection.object_server().remove::<MonitorObject, _>(path).await;
self.changed.notify_one();
}
}
@@ -114,9 +107,7 @@ impl Inhibit {
since = state["revision"].as_u64();
for (path, monitor) in monitors {
if !state["monitor-owners"].as_array().is_some_and(|owners| {
owners
.iter()
.any(|owner| owner.as_u64() == Some(monitor.native_owner))
owners.iter().any(|owner| owner.as_u64() == Some(monitor.native_owner))
}) {
self.end(&connection, &path).await;
continue;
@@ -128,8 +119,7 @@ impl Inhibit {
let transitions = match *last {
None => vec![state.clone()],
Some((_, _, revision, _)) => {
let events =
state["transitions"].as_array().cloned().unwrap_or_default();
let events = state["transitions"].as_array().cloned().unwrap_or_default();
if events
.first()
.and_then(|event| event["revision"].as_u64())
@@ -141,11 +131,7 @@ impl Inhibit {
}
events
.into_iter()
.filter(|event| {
event["revision"]
.as_u64()
.is_some_and(|next| next > revision)
})
.filter(|event| event["revision"].as_u64().is_some_and(|next| next > revision))
.collect()
}
};
@@ -154,19 +140,13 @@ impl Inhibit {
let phase = event["session-state"].as_u64().unwrap_or(1) as u32;
let revision = event["revision"].as_u64().unwrap_or(0);
let token = event["query-token"].as_u64().unwrap_or(0) as u32;
let changed = last.as_ref().is_none_or(|old| {
old.0 != locked || old.1 != phase || (phase == 2 && old.3 != token)
});
let changed = last
.as_ref()
.is_none_or(|old| old.0 != locked || old.1 != phase || (phase == 2 && old.3 != token));
if changed {
let values = HashMap::from([
(
"screensaver-active".into(),
Value::from(locked).try_into().unwrap(),
),
(
"session-state".into(),
Value::from(phase).try_into().unwrap(),
),
("screensaver-active".into(), Value::from(locked).try_into().unwrap()),
("session-state".into(), Value::from(phase).try_into().unwrap()),
]);
if let Ok(emitter) = SignalEmitter::new(&connection, PATH) {
if Self::state_changed(
@@ -203,9 +183,7 @@ impl Inhibit {
use futures_util::StreamExt;
loop {
let result: Result<(), String> = async {
let connection = Connection::system()
.await
.map_err(|error| error.to_string())?;
let connection = Connection::system().await.map_err(|error| error.to_string())?;
let proxy = zbus::Proxy::new(
&connection,
"org.freedesktop.login1",
@@ -219,16 +197,10 @@ impl Inhibit {
.await
.map_err(|error| error.to_string())?;
while let Some(message) = signals.next().await {
let (ending,): (bool,) = message
.body()
.deserialize()
.map_err(|error| error.to_string())?;
let (ending,): (bool,) = message.body().deserialize().map_err(|error| error.to_string())?;
if let Ok(bridge) = Bridge::connect() {
let _ = bridge
.call(
"logind-session-ending",
serde_json::json!({"ending": ending}),
)
.call("logind-session-ending", serde_json::json!({"ending": ending}))
.await;
bridge.close();
}
@@ -246,9 +218,7 @@ impl Inhibit {
fn flags_what(flags: u32) -> zbus::fdo::Result<String> {
if flags == 0 || flags & !15 != 0 {
return Err(zbus::fdo::Error::InvalidArgs(
"Invalid inhibition flags".into(),
));
return Err(zbus::fdo::Error::InvalidArgs("Invalid inhibition flags".into()));
}
let mut what = Vec::new();
if flags & 1 != 0 {
@@ -268,9 +238,7 @@ async fn login_inhibitor(what: &str, app: &str, reason: &str) -> Result<Option<O
return Ok(None);
}
tokio::time::timeout(Duration::from_secs(5), async {
let connection = Connection::system()
.await
.map_err(|error| error.to_string())?;
let connection = Connection::system().await.map_err(|error| error.to_string())?;
let proxy = zbus::Proxy::new(
&connection,
"org.freedesktop.login1",
@@ -309,13 +277,8 @@ impl Inhibit {
.transpose()
.map_err(|_| zbus::fdo::Error::InvalidArgs("Invalid inhibition reason".into()))?
.unwrap_or_else(|| "Application requested session inhibition".into());
if app_id.len() > 512
|| reason.len() > 1024
|| app_id.chars().chain(reason.chars()).any(char::is_control)
{
return Err(zbus::fdo::Error::InvalidArgs(
"Invalid application or reason".into(),
));
if app_id.len() > 512 || reason.len() > 1024 || app_id.chars().chain(reason.chars()).any(char::is_control) {
return Err(zbus::fdo::Error::InvalidArgs("Invalid application or reason".into()));
}
let cancel = self.requests.begin(connection, &header, &handle).await?;
let bridge = match Bridge::connect() {
@@ -351,9 +314,7 @@ impl Inhibit {
bridge.close();
self.requests.end(connection, &handle).await;
Err(zbus::fdo::Error::Failed(
result
.err()
.unwrap_or_else(|| "Inhibition was cancelled".into()),
result.err().unwrap_or_else(|| "Inhibition was cancelled".into()),
))
}
}
@@ -369,15 +330,11 @@ impl Inhibit {
#[zbus(header)] header: Header<'_>,
) -> zbus::fdo::Result<u32> {
let owner = authorize(connection, &header).await?;
if !session_handle
.as_str()
.starts_with(&format!("{PATH}/session/"))
if !session_handle.as_str().starts_with(&format!("{PATH}/session/"))
|| session_handle.as_str().len() > 512
|| app_id.len() > 512
{
return Err(zbus::fdo::Error::InvalidArgs(
"Invalid session monitor".into(),
));
return Err(zbus::fdo::Error::InvalidArgs("Invalid session monitor".into()));
}
let cancel = self.requests.begin(connection, &header, &handle).await?;
let mut monitors = self.monitors.lock().await;
@@ -394,30 +351,59 @@ impl Inhibit {
_ = cancel.wait() => Err("Monitor creation was cancelled".into()),
result = bridge.call("portal-monitor-register", serde_json::json!({})) => result,
};
let native = match result { Ok(native) => native, Err(error) => { bridge.close(); return Err(zbus::fdo::Error::Failed(error)); } };
let native = match result {
Ok(native) => native,
Err(error) => {
bridge.close();
return Err(zbus::fdo::Error::Failed(error));
}
};
let Some(native_owner) = native["monitor-owner"].as_u64() else {
bridge.close();
return Err(zbus::fdo::Error::Failed("Missing monitor owner".into()));
};
let exported = connection.object_server().at(&session_handle, MonitorObject {
backend: self.clone(), path: session_handle.to_string(), owner: owner.clone(), cancel: cancel.clone(),
}).await;
let exported = connection
.object_server()
.at(
&session_handle,
MonitorObject {
backend: self.clone(),
path: session_handle.to_string(),
owner: owner.clone(),
cancel: cancel.clone(),
},
)
.await;
match exported {
Ok(true) if !cancel.stopped.load(Ordering::SeqCst) => {
monitors.insert(session_handle.to_string(), Arc::new(Monitor {
owner, cancel: cancel.clone(), last: Mutex::new(None), bridge,
native_owner,
}));
monitors.insert(
session_handle.to_string(),
Arc::new(Monitor {
owner,
cancel: cancel.clone(),
last: Mutex::new(None),
bridge,
native_owner,
}),
);
self.changed.notify_one();
Ok(0)
}
result => {
bridge.close();
if matches!(result, Ok(true)) { let _ = connection.object_server().remove::<MonitorObject, _>(&session_handle).await; }
result.map(|_| if cancel.stopped.load(Ordering::SeqCst) { 1 } else { 2 }).map_err(Into::into)
if matches!(result, Ok(true)) {
let _ = connection
.object_server()
.remove::<MonitorObject, _>(&session_handle)
.await;
}
result
.map(|_| if cancel.stopped.load(Ordering::SeqCst) { 1 } else { 2 })
.map_err(Into::into)
}
}
}.await;
}
.await;
attempt
};
drop(monitors);
@@ -442,9 +428,7 @@ impl Inhibit {
.ok_or_else(|| zbus::fdo::Error::AccessDenied("Not your session monitor".into()))?;
let last = monitor.last.lock().await;
if monitor.cancel.stopped.load(Ordering::SeqCst) {
return Err(zbus::fdo::Error::AccessDenied(
"Session monitor closed".into(),
));
return Err(zbus::fdo::Error::AccessDenied("Session monitor closed".into()));
}
if let Some((_, 2, _, token)) = *last {
monitor
@@ -484,9 +468,7 @@ impl MonitorObject {
#[zbus(header)] header: Header<'_>,
) -> zbus::fdo::Result<()> {
if authorize(connection, &header).await? != self.owner {
return Err(zbus::fdo::Error::AccessDenied(
"Not your session monitor".into(),
));
return Err(zbus::fdo::Error::AccessDenied("Not your session monitor".into()));
}
self.cancel.stop();
self.backend.end(connection, &self.path).await;
+47 -108
View File
@@ -1,24 +1,19 @@
use std::{
collections::HashMap,
sync::{Arc, atomic::Ordering},
time::Duration,
};
use std::collections::HashMap;
use std::sync::Arc;
use std::sync::atomic::Ordering;
use std::time::Duration;
use serde_json::json;
use tokio::sync::Mutex;
use zbus::{
Connection,
message::Header,
object_server::SignalEmitter,
zvariant::{OwnedFd, OwnedObjectPath, Value},
};
use zbus::Connection;
use zbus::message::Header;
use zbus::object_server::SignalEmitter;
use zbus::zvariant::{OwnedFd, OwnedObjectPath, Value};
use crate::{
backend::{Cancel, Options, authorize},
bridge::Bridge,
desktop::Requests,
eis::Worker,
};
use crate::backend::{Cancel, Options, authorize};
use crate::bridge::Bridge;
use crate::desktop::Requests;
use crate::eis::Worker;
const PATH: &str = "/org/freedesktop/portal/desktop";
@@ -92,10 +87,7 @@ impl InputCapture {
let connection = connection.clone();
let path = path.to_owned();
tokio::spawn(async move {
let _ = connection
.object_server()
.remove::<SessionObject, _>(path)
.await;
let _ = connection.object_server().remove::<SessionObject, _>(path).await;
});
}
}
@@ -106,9 +98,7 @@ impl InputCapture {
.lock()
.await
.iter()
.filter_map(|(path, session)| {
(Some(session.owner.as_str()) != owner).then_some(path.clone())
})
.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;
@@ -152,10 +142,7 @@ impl InputCapture {
if session.cancel.stopped.load(Ordering::SeqCst) {
break;
}
let transport = state
.worker
.as_ref()
.map(|worker| worker.send(events.clone()));
let transport = state.worker.as_ref().map(|worker| worker.send(events.clone()));
if transport.is_some_and(|result| result.is_err()) {
break;
}
@@ -179,11 +166,9 @@ impl InputCapture {
.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()
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);
}
@@ -201,12 +186,8 @@ impl InputCapture {
)
.await
}
Some("disabled") => {
Self::disabled(&emitter, path.clone(), Options::new()).await
}
Some("zones") => {
Self::zones_changed(&emitter, path.clone(), Options::new()).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(())
@@ -238,8 +219,7 @@ 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"))?;
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"));
}
@@ -267,9 +247,7 @@ impl InputCapture {
#[zbus(header)] header: Header<'_>,
) -> zbus::fdo::Result<Options> {
let owner = authorize(connection, &header).await?;
if !session_handle
.as_str()
.starts_with(&format!("{PATH}/session/"))
if !session_handle.as_str().starts_with(&format!("{PATH}/session/"))
|| session_handle.as_str().len() > 512
|| app_id.len() > 512
{
@@ -365,9 +343,7 @@ impl InputCapture {
#[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 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?;
@@ -406,18 +382,11 @@ impl InputCapture {
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 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)
{
if cancel.stopped.load(Ordering::SeqCst) || session.cancel.stopped.load(Ordering::SeqCst) {
bridge.close();
return Err("Input capture approval was cancelled".to_owned());
}
@@ -473,9 +442,7 @@ impl InputCapture {
#[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 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! {
@@ -485,23 +452,18 @@ impl InputCapture {
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()
{
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"))?;
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()))?,
Value::from(zones).try_to_owned().map_err(|e| failed(e.to_string()))?,
),
(
"zone_set".into(),
@@ -523,9 +485,7 @@ impl InputCapture {
#[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 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"));
@@ -533,20 +493,15 @@ impl InputCapture {
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 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"))?;
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<_>>>()?;
@@ -558,10 +513,7 @@ impl InputCapture {
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()
{
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()));
}
@@ -588,9 +540,7 @@ impl InputCapture {
#[zbus(connection)] connection: &Connection,
#[zbus(header)] header: Header<'_>,
) -> zbus::fdo::Result<OwnedFd> {
let session = self
.session(connection, &header, &session_handle, &app_id)
.await?;
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() {
@@ -599,11 +549,10 @@ impl InputCapture {
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)?;
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())
}
@@ -616,9 +565,7 @@ impl InputCapture {
#[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 session = self.session(connection, &header, &session_handle, &app_id).await?;
let (bridge, id) = Self::native(&session).await?;
let ready = session
.state
@@ -630,9 +577,7 @@ impl InputCapture {
.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
{
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;
@@ -652,9 +597,7 @@ impl InputCapture {
#[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 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}))
@@ -671,9 +614,7 @@ impl InputCapture {
#[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 session = self.session(connection, &header, &session_handle, &app_id).await?;
let (bridge, id) = Self::native(&session).await?;
let activation = options
.get("activation_id")
@@ -770,9 +711,7 @@ mod tests {
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()
);
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!(
+14 -14
View File
@@ -1,6 +1,13 @@
use std::{borrow::Cow, collections::HashMap, sync::Arc, time::Duration};
use std::borrow::Cow;
use std::collections::HashMap;
use std::sync::Arc;
use std::time::Duration;
use tokio::sync::RwLock;
use zbus::{Connection, message::Header, object_server::SignalEmitter, zvariant::Value};
use zbus::Connection;
use zbus::message::Header;
use zbus::object_server::SignalEmitter;
use zbus::zvariant::Value;
const PATH: &str = "/org/freedesktop/portal/desktop";
const KEYS: [&str; 7] = [
@@ -55,8 +62,7 @@ impl Lockdown {
connection: &Connection,
header: Option<Header<'_>>,
) -> zbus::fdo::Result<()> {
let header =
header.ok_or_else(|| zbus::fdo::Error::AccessDenied("Missing policy caller".into()))?;
let header = header.ok_or_else(|| zbus::fdo::Error::AccessDenied("Missing policy caller".into()))?;
crate::backend::authorize(connection, &header).await?;
let mut current = self.0.write().await;
store(index, value).map_err(zbus::fdo::Error::Failed)?;
@@ -209,8 +215,7 @@ fn store(index: usize, value: bool) -> Result<(), String> {
Err(error) => return Err(error.to_string()),
_ => return Err("Configuration is too large".into()),
};
let mut document =
ferese_config::Document::parse(&source).map_err(|error| error.to_string())?;
let mut document = ferese_config::Document::parse(&source).map_err(|error| error.to_string())?;
if values(&document).is_none() {
return Err("Invalid lockdown policy".into());
}
@@ -223,11 +228,8 @@ fn store(index: usize, value: bool) -> Result<(), String> {
let directory = path.parent().ok_or("Invalid config path")?;
std::fs::create_dir_all(directory).map_err(|error| error.to_string())?;
let mut file = tempfile::NamedTempFile::new_in(directory).map_err(|error| error.to_string())?;
std::io::Write::write_all(&mut file, document.to_string().as_bytes())
.map_err(|error| error.to_string())?;
file.as_file()
.sync_all()
.map_err(|error| error.to_string())?;
std::io::Write::write_all(&mut file, document.to_string().as_bytes()).map_err(|error| error.to_string())?;
file.as_file().sync_all().map_err(|error| error.to_string())?;
if std::fs::read_to_string(&path).unwrap_or_default() != source {
return Err("Configuration changed; please try again".into());
}
@@ -250,9 +252,7 @@ mod tests {
let values = values(&enabled).unwrap();
assert!(values[1] && values[4]);
assert!(!values[0]);
let invalid =
ferese_config::Document::parse("portals { lockdown { disable-camera \"yes\"; }; }")
.unwrap();
let invalid = ferese_config::Document::parse("portals { lockdown { disable-camera \"yes\"; }; }").unwrap();
assert!(super::values(&invalid).is_none());
for source in ["portals #false", "portals { lockdown \"bad\"; }"] {
let invalid = ferese_config::Document::parse(source).unwrap();
+4 -8
View File
@@ -19,18 +19,14 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
let args: Vec<_> = std::env::args().skip(1).collect();
match args.as_slice() {
[flag, name, cursor] if flag == "--stream" => {
stream::run(name.clone(), cursor == "embedded", None)
}
[flag, name, cursor] if flag == "--stream" => stream::run(name.clone(), cursor == "embedded", None),
[flag, id, cursor] if flag == "--stream-window" => {
let id: u64 = id.parse()?;
stream::run(format!("window:{id}"), cursor == "embedded", None)
}
[flag, name, cursor, generation] if flag == "--stream" => stream::run(
name.clone(),
cursor == "embedded",
Some(generation.parse()?),
),
[flag, name, cursor, generation] if flag == "--stream" => {
stream::run(name.clone(), cursor == "embedded", Some(generation.parse()?))
}
[flag] if flag == "--sources" => {
let capture = capture::Capture::connect(&std::sync::atomic::AtomicBool::new(false))?;
println!("{}", serde_json::to_string(&capture.sources())?);
+12 -23
View File
@@ -1,12 +1,9 @@
use wayland_client::{
Connection, Dispatch, Proxy, QueueHandle, delegate_noop,
globals::{GlobalListContents, registry_queue_init},
protocol::{wl_registry, wl_surface},
};
use wayland_protocols::xdg::foreign::zv2::client::{
zxdg_imported_v2::{self, ZxdgImportedV2},
zxdg_importer_v2::ZxdgImporterV2,
};
use wayland_client::globals::{GlobalListContents, registry_queue_init};
use wayland_client::protocol::{wl_registry, wl_surface};
use wayland_client::{Connection, Dispatch, Proxy, QueueHandle, delegate_noop};
use wayland_protocols::xdg::foreign::zv2::client::zxdg_imported_v2;
use wayland_protocols::xdg::foreign::zv2::client::zxdg_imported_v2::ZxdgImportedV2;
use wayland_protocols::xdg::foreign::zv2::client::zxdg_importer_v2::ZxdgImporterV2;
#[derive(Debug)]
pub(crate) struct Parent {
@@ -16,10 +13,7 @@ pub(crate) struct Parent {
}
impl Parent {
pub(crate) fn attach(
window: &dyn cosmic::iced::window::Window,
parent: &str,
) -> Result<Option<Self>, String> {
pub(crate) fn attach(window: &dyn cosmic::iced::window::Window, parent: &str) -> Result<Option<Self>, String> {
use cosmic::iced::window::raw_window_handle::{RawDisplayHandle, RawWindowHandle};
if parent.is_empty() {
return Ok(None);
@@ -36,9 +30,8 @@ impl Parent {
return Err("Portal parenting requires Wayland".into());
};
// The window lends live handles; the borrowed backend never owns the display.
let backend = unsafe {
wayland_client::backend::Backend::from_foreign_display(display.display.as_ptr().cast())
};
let backend =
unsafe { wayland_client::backend::Backend::from_foreign_display(display.display.as_ptr().cast()) };
let connection = Connection::from_backend(backend);
let id = unsafe {
wayland_client::backend::ObjectId::from_ptr(
@@ -47,19 +40,15 @@ impl Parent {
)
}
.map_err(|error| error.to_string())?;
let surface =
wl_surface::WlSurface::from_id(&connection, id).map_err(|error| error.to_string())?;
let (globals, mut queue) =
registry_queue_init::<State>(&connection).map_err(|error| error.to_string())?;
let surface = wl_surface::WlSurface::from_id(&connection, id).map_err(|error| error.to_string())?;
let (globals, mut queue) = registry_queue_init::<State>(&connection).map_err(|error| error.to_string())?;
let qh = queue.handle();
let importer = globals
.bind::<ZxdgImporterV2, _, _>(&qh, 1..=1, ())
.map_err(|error| error.to_string())?;
let imported = importer.import_toplevel(handle.into(), &qh, ());
let mut state = State::default();
queue
.roundtrip(&mut state)
.map_err(|error| error.to_string())?;
queue.roundtrip(&mut state).map_err(|error| error.to_string())?;
if state.invalid {
imported.destroy();
importer.destroy();
+23 -65
View File
@@ -1,20 +1,16 @@
use crate::{
backend::{Options, authorize},
desktop::{Requests, consent, ipc, reply},
};
use std::{
collections::{HashMap, HashSet},
path::Path,
sync::Arc,
time::Duration,
};
use std::collections::{HashMap, HashSet};
use std::path::Path;
use std::sync::Arc;
use std::time::Duration;
use tokio::sync::RwLock;
use zbus::{
Connection,
message::Header,
object_server::SignalEmitter,
zvariant::{OwnedObjectPath, OwnedValue, Value},
};
use zbus::Connection;
use zbus::message::Header;
use zbus::object_server::SignalEmitter;
use zbus::zvariant::{OwnedObjectPath, OwnedValue, Value};
use crate::backend::{Options, authorize};
use crate::desktop::{Requests, consent, ipc, reply};
const PATH: &str = "/org/freedesktop/portal/desktop";
@@ -56,19 +52,12 @@ fn usb_label(info: &Options, id: &str) -> String {
.and_then(|value| HashMap::<String, OwnedValue>::try_from(value).ok())
.unwrap_or_default();
let property = |keys: &[&str]| {
keys.iter().find_map(|key| {
properties
.get(*key)
.and_then(|value| <&str>::try_from(value).ok())
})
keys.iter()
.find_map(|key| properties.get(*key).and_then(|value| <&str>::try_from(value).ok()))
};
let vendor = property(&["ID_VENDOR_FROM_DATABASE", "ID_VENDOR", "ID_VENDOR_ID"]);
let model = property(&["ID_MODEL_FROM_DATABASE", "ID_MODEL", "ID_MODEL_ID"]);
let label = [vendor, model]
.into_iter()
.flatten()
.collect::<Vec<_>>()
.join(" ");
let label = [vendor, model].into_iter().flatten().collect::<Vec<_>>().join(" ");
let label = if label.is_empty() { id } else { &label };
label
.chars()
@@ -102,27 +91,11 @@ async fn usb_consent(
description.push_str(&format!(
"\n{} — {}",
label,
if writable {
"read and write"
} else {
"read only"
}
));
selected.push((
id,
HashMap::from([("writable".to_owned(), OwnedValue::from(writable))]),
if writable { "read and write" } else { "read only" }
));
selected.push((id, HashMap::from([("writable".to_owned(), OwnedValue::from(writable))])));
}
if !consent(
app,
parent,
"USB device access",
&description,
"Allow access",
None,
)
.await?
{
if !consent(app, parent, "USB device access", &description, "Allow access", None).await? {
return Ok(None);
}
Ok(Some(HashMap::from([(
@@ -212,8 +185,7 @@ impl Background {
let directory = dirs::config_dir()
.ok_or_else(|| zbus::fdo::Error::Failed("Missing config directory".into()))?
.join("autostart");
set_autostart(&directory, &app_id, enable, &commandline, flags)
.map_err(zbus::fdo::Error::InvalidArgs)
set_autostart(&directory, &app_id, enable, &commandline, flags).map_err(zbus::fdo::Error::InvalidArgs)
}
#[zbus(signal)]
@@ -254,13 +226,7 @@ fn app_states(windows: &serde_json::Value) -> HashMap<String, u32> {
apps
}
fn set_autostart(
directory: &Path,
app: &str,
enable: bool,
argv: &[String],
flags: u32,
) -> Result<bool, String> {
fn set_autostart(directory: &Path, app: &str, enable: bool, argv: &[String], flags: u32) -> Result<bool, String> {
if app.is_empty()
|| app.len() > 255
|| !app
@@ -287,9 +253,7 @@ fn set_autostart(
}
return Ok(true);
}
if argv.len() > 128
|| (argv.first().is_none_or(|argument| argument.is_empty()) && flags & 1 == 0)
{
if argv.len() > 128 || (argv.first().is_none_or(|argument| argument.is_empty()) && flags & 1 == 0) {
return Err("Invalid autostart command".into());
}
let exec = argv
@@ -377,15 +341,9 @@ mod tests {
"ID_VENDOR_FROM_DATABASE".to_owned(),
Value::from("Example Vendor").try_to_owned().unwrap(),
),
(
"ID_MODEL".to_owned(),
Value::from("Gamepad").try_to_owned().unwrap(),
),
("ID_MODEL".to_owned(), Value::from("Gamepad").try_to_owned().unwrap()),
]);
let info = HashMap::from([(
"properties".into(),
Value::from(properties).try_to_owned().unwrap(),
)]);
let info = HashMap::from([("properties".into(), Value::from(properties).try_to_owned().unwrap())]);
assert_eq!(usb_label(&info, "opaque-id"), "Example Vendor Gamepad");
assert_eq!(usb_label(&Options::new(), "fallback-id"), "fallback-id");
}
+30 -66
View File
@@ -1,13 +1,13 @@
use crate::capture::Source;
use cosmic::{
ApplicationExt, Element,
app::{Core, Settings, Task},
iced::{Alignment, Length},
widget::{button, column, container, row, scrollable},
};
use std::io::{Read, Write};
use cosmic::app::{Core, Settings, Task};
use cosmic::iced::{Alignment, Length};
use cosmic::widget::{button, column, container, row, scrollable};
use cosmic::{ApplicationExt, Element};
use ferese_theme::icons::symbolic as glyph;
use serde::{Deserialize, Serialize};
use std::io::{Read, Write};
use crate::capture::Source;
#[derive(Clone, Serialize, Deserialize)]
pub struct Prompt {
@@ -82,9 +82,7 @@ struct Picker {
pub fn run() -> Result<(), Box<dyn std::error::Error>> {
let mut input = String::new();
std::io::stdin()
.take(256 * 1024 + 1)
.read_to_string(&mut input)?;
std::io::stdin().take(256 * 1024 + 1).read_to_string(&mut input)?;
if input.len() > 256 * 1024 {
return Err("Picker request is too large".into());
}
@@ -95,8 +93,7 @@ pub fn run() -> Result<(), Box<dyn std::error::Error>> {
}
let (theme, font, background, palette) = appearance();
let height =
180. + prompt.source_list_height() + if prompt.persist_mode > 0 { 64. } else { 0. };
let height = 180. + prompt.source_list_height() + if prompt.persist_mode > 0 { 64. } else { 0. };
cosmic::app::run::<Picker>(
Settings::default()
.size(cosmic::iced::Size::new(400., height))
@@ -114,12 +111,7 @@ pub fn run() -> Result<(), Box<dyn std::error::Error>> {
impl cosmic::Application for Picker {
type Executor = cosmic::executor::Default;
type Flags = (
Prompt,
cosmic::iced::Color,
ferese_theme::Palette,
cosmic::font::Font,
);
type Flags = (Prompt, cosmic::iced::Color, ferese_theme::Palette, cosmic::font::Font);
type Message = Message;
const APP_ID: &'static str = "dev.ferese.ScreenShare";
@@ -131,10 +123,7 @@ impl cosmic::Application for Picker {
&mut self.core
}
fn init(
mut core: Core,
(prompt, background, palette, font): Self::Flags,
) -> (Self, Task<Message>) {
fn init(mut core: Core, (prompt, background, palette, font): Self::Flags) -> (Self, Task<Message>) {
core.window.show_headerbar = false;
core.window.border_padding = Some(0);
core.window.content_container = false;
@@ -171,9 +160,7 @@ impl cosmic::Application for Picker {
fn subscription(&self) -> cosmic::iced::Subscription<Message> {
cosmic::iced::event::listen_with(|event, _, id| match event {
cosmic::iced::Event::Window(cosmic::iced::window::Event::Opened { .. }) => {
Some(Message::WindowOpened(id))
}
cosmic::iced::Event::Window(cosmic::iced::window::Event::Opened { .. }) => Some(Message::WindowOpened(id)),
_ => None,
})
}
@@ -183,12 +170,8 @@ impl cosmic::Application for Picker {
Message::WindowOpened(id) => {
let parent = self.prompt.parent.clone();
return cosmic::iced::window::run(id, move |window| {
let parent =
crate::parent::Parent::attach(window, &parent)?.map(std::sync::Arc::new);
Ok((
parent,
ferese_theme::material::ModalMaterial::attach(window),
))
let parent = crate::parent::Parent::attach(window, &parent)?.map(std::sync::Arc::new);
Ok((parent, ferese_theme::material::ModalMaterial::attach(window)))
})
.map(|result| cosmic::Action::App(Message::Attached(result)));
}
@@ -216,11 +199,7 @@ impl cosmic::Application for Picker {
}
Message::Remember if self.can_remember() => self.remember = !self.remember,
Message::Share if !self.selected.is_empty() => {
let names: Vec<_> = self
.selected
.iter()
.map(|i| &self.prompt.sources[*i].name)
.collect();
let names: Vec<_> = self.selected.iter().map(|i| &self.prompt.sources[*i].name).collect();
if self.prompt.window_capture {
println!("{}", serde_json::to_string(&names).unwrap());
} else {
@@ -288,26 +267,20 @@ impl cosmic::Application for Picker {
for (index, source) in self.prompt.sources.iter().enumerate() {
let is_window = self.prompt.window_capture || source.window_id().is_some();
if !self.prompt.window_capture && previous_kind != Some(is_window) {
sources = sources.push(
self.text(if is_window { "Windows" } else { "Displays" })
.size(12),
);
sources = sources.push(self.text(if is_window { "Windows" } else { "Displays" }).size(12));
previous_kind = Some(is_window);
}
let selected = self.selected.contains(&index);
let mut entry = row![
glyph(ferese_theme::icons::DISPLAY, 24),
column![
self.text(
if self.prompt.window_capture || source.window_id().is_some() {
&source.label
} else {
&source.name
}
)
self.text(if self.prompt.window_capture || source.window_id().is_some() {
&source.label
} else {
&source.name
})
.size(14),
self.text(format!("{} × {}", source.width, source.height))
.size(12)
self.text(format!("{} × {}", source.width, source.height)).size(12)
]
.spacing(3)
.width(Length::Fill),
@@ -329,8 +302,7 @@ impl cosmic::Application for Picker {
.on_press(Message::Select(index)),
);
}
content = content
.push(scrollable(sources).height(Length::Fixed(self.prompt.source_list_height())));
content = content.push(scrollable(sources).height(Length::Fixed(self.prompt.source_list_height())));
content = content.push(
self.text(if self.prompt.window_capture {
"Only the selected window will be captured."
@@ -354,21 +326,14 @@ impl cosmic::Application for Picker {
ferese_theme::menus::switch(self.remember, self.palette, 1.),
self.font,
))
.class(ferese_theme::controls::navigation_style(
self.palette,
false,
))
.class(ferese_theme::controls::navigation_style(self.palette, false))
.width(Length::Fill)
.on_press_maybe(self.can_remember().then_some(Message::Remember)),
);
}
let mut share = button::custom(
row![
self.text(if self.prompt.window_capture {
"Capture"
} else {
"Share"
}),
self.text(if self.prompt.window_capture { "Capture" } else { "Share" }),
glyph(ferese_theme::icons::ARROW, 16)
]
.spacing(8)
@@ -396,11 +361,10 @@ impl Picker {
fn can_remember(&self) -> bool {
!self.selected.is_empty()
&& self.selected.len() <= 16
&& self.selected.iter().all(|index| {
self.prompt
.rememberable
.contains(&self.prompt.sources[*index].name)
})
&& self
.selected
.iter()
.all(|index| self.prompt.rememberable.contains(&self.prompt.sources[*index].name))
}
fn text<'a>(
+3 -5
View File
@@ -3,11 +3,9 @@
mod recorder;
fn main() {
if let Err(error) = tokio::runtime::Runtime::new().and_then(|runtime| {
runtime
.block_on(recorder::run())
.map_err(std::io::Error::other)
}) {
if let Err(error) = tokio::runtime::Runtime::new()
.and_then(|runtime| runtime.block_on(recorder::run()).map_err(std::io::Error::other))
{
recorder::event("error", Some(&error.to_string()), None);
std::process::exit(1);
}
+39 -101
View File
@@ -1,22 +1,19 @@
use std::collections::HashMap;
use std::fs::{File, OpenOptions};
use std::io::{Read, Write};
use std::os::fd::AsRawFd;
use std::os::unix::fs::OpenOptionsExt;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::time::{Duration, Instant};
use futures_util::StreamExt;
use gstreamer::{self as gst, prelude::*};
use std::{
collections::HashMap,
fs::{File, OpenOptions},
io::{Read, Write},
os::{fd::AsRawFd, unix::fs::OpenOptionsExt},
path::{Path, PathBuf},
sync::{
Arc,
atomic::{AtomicBool, Ordering},
},
time::{Duration, Instant},
};
use gstreamer as gst;
use gstreamer::prelude::*;
use tokio::sync::watch;
use zbus::{
Connection, Proxy,
zvariant::{OwnedFd, OwnedObjectPath, OwnedValue, Value},
};
use zbus::zvariant::{OwnedFd, OwnedObjectPath, OwnedValue, Value};
use zbus::{Connection, Proxy};
type Options = HashMap<String, OwnedValue>;
const DESKTOP: &str = "org.freedesktop.portal.Desktop";
@@ -38,32 +35,15 @@ async fn request<B: serde::Serialize + zbus::zvariant::DynamicType>(
body: &B,
stop: &mut watch::Receiver<bool>,
) -> Result<Option<Options>, String> {
let sender = connection
.unique_name()
.ok_or("No D-Bus identity")?
.as_str()[1..]
.replace('.', "_");
let sender = connection.unique_name().ok_or("No D-Bus identity")?.as_str()[1..].replace('.', "_");
let path = format!("{DESKTOP_PATH}/request/{sender}/{token}");
let request = Proxy::new(
connection,
DESKTOP,
path.as_str(),
"org.freedesktop.portal.Request",
)
.await
.map_err(|e| e.to_string())?;
let mut responses = request
.receive_signal("Response")
let request = Proxy::new(connection, DESKTOP, path.as_str(), "org.freedesktop.portal.Request")
.await
.map_err(|e| e.to_string())?;
let mut responses = request.receive_signal("Response").await.map_err(|e| e.to_string())?;
let portal = Proxy::new(connection, DESKTOP, DESKTOP_PATH, "org.freedesktop.portal.ScreenCast")
.await
.map_err(|e| e.to_string())?;
let portal = Proxy::new(
connection,
DESKTOP,
DESKTOP_PATH,
"org.freedesktop.portal.ScreenCast",
)
.await
.map_err(|e| e.to_string())?;
let returned: OwnedObjectPath = portal
.call(method, body)
.await
@@ -90,14 +70,7 @@ async fn request<B: serde::Serialize + zbus::zvariant::DynamicType>(
pub async fn run() -> Result<(), String> {
gst::init().map_err(|e| e.to_string())?;
for factory in [
"pipewiresrc",
"queue",
"videoconvert",
"vp8enc",
"webmmux",
"fdsink",
] {
for factory in ["pipewiresrc", "queue", "videoconvert", "vp8enc", "webmmux", "fdsink"] {
if gst::ElementFactory::find(factory).is_none() {
return Err(format!(
"Recording needs the GStreamer {factory} plugin. See the recording installation guide."
@@ -127,13 +100,10 @@ pub async fn run() -> Result<(), String> {
event("cancelled", None, None);
return Ok(());
};
let handle = created
.get("session_handle")
.ok_or("Portal returned no session")?;
let handle = created.get("session_handle").ok_or("Portal returned no session")?;
// The public portal historically returns this path as a D-Bus string.
let handle = OwnedObjectPath::try_from(
String::try_from(handle.try_clone().map_err(|e| e.to_string())?)
.map_err(|e| e.to_string())?,
String::try_from(handle.try_clone().map_err(|e| e.to_string())?).map_err(|e| e.to_string())?,
)
.map_err(|e| e.to_string())?;
// Older/foreign backends keep their normal sharing indicator. Only Ferese
@@ -146,19 +116,12 @@ pub async fn run() -> Result<(), String> {
"org.ferese.ScreenRecorder",
)
.await?;
control
.call::<_, _, ()>("UseBarControls", &(handle.clone(),))
.await
control.call::<_, _, ()>("UseBarControls", &(handle.clone(),)).await
})
.await;
let session = Proxy::new(
&connection,
DESKTOP,
handle.as_str(),
"org.freedesktop.portal.Session",
)
.await
.map_err(|e| e.to_string())?;
let session = Proxy::new(&connection, DESKTOP, handle.as_str(), "org.freedesktop.portal.Session")
.await
.map_err(|e| e.to_string())?;
let result = record_session(&connection, &session, &handle, &token, &output, &mut stop).await;
let _: Result<(), _> = session.call("Close", &()).await;
match result {
@@ -176,10 +139,7 @@ async fn record_session(
output: &Path,
stop: &mut watch::Receiver<bool>,
) -> Result<Option<PathBuf>, String> {
let mut closed = session
.receive_signal("Closed")
.await
.map_err(|e| e.to_string())?;
let mut closed = session.receive_signal("Closed").await.map_err(|e| e.to_string())?;
if request(
connection,
"SelectSources",
@@ -205,11 +165,7 @@ async fn record_session(
connection,
"Start",
&token(2),
&(
handle,
"",
HashMap::from([("handle_token", option_string(token(2)))]),
),
&(handle, "", HashMap::from([("handle_token", option_string(token(2)))])),
stop,
)
.await?
@@ -226,14 +182,9 @@ async fn record_session(
if streams.len() != 1 {
return Err("Recording requires exactly one display".into());
}
let portal = Proxy::new(
connection,
DESKTOP,
DESKTOP_PATH,
"org.freedesktop.portal.ScreenCast",
)
.await
.map_err(|e| e.to_string())?;
let portal = Proxy::new(connection, DESKTOP, DESKTOP_PATH, "org.freedesktop.portal.ScreenCast")
.await
.map_err(|e| e.to_string())?;
let remote: OwnedFd = portal
.call("OpenPipeWireRemote", &(handle, Options::new()))
.await
@@ -278,8 +229,7 @@ fn output_directory() -> Result<PathBuf, String> {
}
}
fn recording_file(directory: &Path) -> Result<(File, PathBuf, PathBuf), String> {
std::fs::create_dir_all(directory)
.map_err(|e| format!("Cannot create recording folder: {e}"))?;
std::fs::create_dir_all(directory).map_err(|e| format!("Cannot create recording folder: {e}"))?;
let name = format!(
"Recording-{}-{}",
jiff::Zoned::now().strftime("%Y-%m-%d_%H-%M-%S"),
@@ -338,24 +288,19 @@ fn encode(source: gst::Element, output: &Path, stop: Arc<AtomicBool>) -> Result<
pipeline
.add_many([&source, &queue, &convert, &encoder, &mux, &sink])
.map_err(|e| e.to_string())?;
gst::Element::link_many([&source, &queue, &convert, &encoder, &mux, &sink])
.map_err(|e| e.to_string())?;
gst::Element::link_many([&source, &queue, &convert, &encoder, &mux, &sink]).map_err(|e| e.to_string())?;
let announced = Arc::new(AtomicBool::new(false));
let notice = announced.clone();
source
.static_pad("src")
.ok_or("Capture has no video output")?
.add_probe(gst::PadProbeType::BUFFER, move |_, info| {
if info.buffer().is_some_and(|b| b.size() > 0)
&& !notice.swap(true, Ordering::Relaxed)
{
if info.buffer().is_some_and(|b| b.size() > 0) && !notice.swap(true, Ordering::Relaxed) {
event("recording", None, None);
}
gst::PadProbeReturn::Ok
});
pipeline
.set_state(gst::State::Playing)
.map_err(|e| e.to_string())?;
pipeline.set_state(gst::State::Playing).map_err(|e| e.to_string())?;
let bus = pipeline.bus().ok_or("Encoder has no message bus")?;
let mut finishing = None;
let started = Instant::now();
@@ -404,8 +349,7 @@ fn encode(source: gst::Element, output: &Path, stop: Arc<AtomicBool>) -> Result<
result.map_err(|e| format!("{e}. Incomplete file: {}", partial.display()))?;
file.sync_all()
.map_err(|e| format!("Cannot finish saving {}: {e}", partial.display()))?;
std::fs::rename(&partial, &destination)
.map_err(|e| format!("Cannot finish saving {}: {e}", partial.display()))?;
std::fs::rename(&partial, &destination).map_err(|e| format!("Cannot finish saving {}: {e}", partial.display()))?;
Ok(destination)
}
@@ -419,10 +363,7 @@ mod tests {
let (_, second, _) = recording_file(directory.path()).unwrap();
assert_ne!(first, second);
use std::os::unix::fs::PermissionsExt;
assert_eq!(
std::fs::metadata(first).unwrap().permissions().mode() & 0o777,
0o600
);
assert_eq!(std::fs::metadata(first).unwrap().permissions().mode() & 0o777, 0o600);
}
#[test]
#[ignore = "requires an explicitly provided temporary PipeWire test node"]
@@ -493,10 +434,7 @@ mod tests {
"Invalid saved duration: {duration:?}"
);
assert!(
matches!(
message.as_ref().map(|m| m.view()),
Some(gst::MessageView::Eos(_))
),
matches!(message.as_ref().map(|m| m.view()), Some(gst::MessageView::Eos(_))),
"Saved video did not decode: {message:?}"
);
}
+23 -58
View File
@@ -1,8 +1,10 @@
use crate::{backend::Options, capture::Source};
use serde::{Deserialize, Serialize};
use serde_json::Value as Json;
use zbus::zvariant::{OwnedValue, Value};
use crate::backend::Options;
use crate::capture::Source;
const VENDOR: &str = "Ferese";
const VERSION: u32 = 1;
const MAX_DATA: usize = 16 * 1024;
@@ -31,9 +33,7 @@ pub(crate) fn persist_mode(options: &Options) -> zbus::fdo::Result<u32> {
.map_err(|_| zbus::fdo::Error::InvalidArgs("Invalid persistence option".into()))?
.unwrap_or(0);
if mode > 2 {
return Err(zbus::fdo::Error::InvalidArgs(
"Unsupported persistence mode".into(),
));
return Err(zbus::fdo::Error::InvalidArgs("Unsupported persistence mode".into()));
}
Ok(mode)
}
@@ -43,9 +43,7 @@ fn identity<'a>(outputs: &'a Json, name: &str) -> Option<&'a str> {
(output["name"].as_str() == Some(name) && output["enabled"] == true)
.then(|| output["identity"].as_str())
.flatten()
.filter(|identity| {
!identity.is_empty() && identity.len() <= 512 && identity.starts_with("drm-edid:")
})
.filter(|identity| !identity.is_empty() && identity.len() <= 512 && identity.starts_with("drm-edid:"))
})
}
@@ -54,14 +52,15 @@ pub(crate) fn selection_unchanged(sources: &[Source], before: &Json, after: &Jso
outputs
.as_array()
.and_then(|outputs| {
outputs.iter().find(|output| {
output["name"].as_str() == Some(name) && output["enabled"] == true
})
outputs
.iter()
.find(|output| output["name"].as_str() == Some(name) && output["enabled"] == true)
})
.map(|output| (output["identity"].clone(), output["id"].clone()))
};
sources.iter().all(
|source| match (find(before, &source.name), find(after, &source.name)) {
sources
.iter()
.all(|source| match (find(before, &source.name), find(after, &source.name)) {
(Some((identity, id)), Some((current_identity, current_id))) => {
if identity.is_null() {
id.as_u64().is_some() && id == current_id && current_identity.is_null()
@@ -70,8 +69,7 @@ pub(crate) fn selection_unchanged(sources: &[Source], before: &Json, after: &Jso
}
}
_ => false,
},
)
})
}
impl Restore {
@@ -125,29 +123,13 @@ impl Restore {
{
return None;
}
selected.push(
sources
.iter()
.find(|source| source.name == monitor.name)?
.clone(),
);
selected.push(sources.iter().find(|source| source.name == monitor.name)?.clone());
}
Some(selected)
}
pub(crate) fn create(
app: &str,
cursor: bool,
sources: &[Source],
outputs: &Json,
mode: u32,
) -> Option<Self> {
if app.is_empty()
|| app.len() > 512
|| sources.is_empty()
|| sources.len() > 16
|| !(1..=2).contains(&mode)
{
pub(crate) fn create(app: &str, cursor: bool, sources: &[Source], outputs: &Json, mode: u32) -> Option<Self> {
if app.is_empty() || app.len() > 512 || sources.is_empty() || sources.len() > 16 || !(1..=2).contains(&mode) {
return None;
}
let monitors = sources
@@ -196,8 +178,7 @@ mod tests {
#[test]
fn restore_is_bound_to_app_capture_options_and_display_identity() {
let outputs =
serde_json::json!([{"name":"DP-1", "identity":"drm-edid:123", "enabled":true}]);
let outputs = serde_json::json!([{"name":"DP-1", "identity":"drm-edid:123", "enabled":true}]);
let sources = [source()];
let saved = Restore::create("org.test.App", true, &sources, &outputs, 2).unwrap();
let options = Options::from([("restore_data".into(), saved.value().unwrap())]);
@@ -217,20 +198,14 @@ mod tests {
.resolve("org.test.App", false, false, &sources, &outputs)
.is_none()
);
assert!(
restored
.resolve("org.test.App", true, false, &[], &outputs)
.is_none()
);
let changed =
serde_json::json!([{"name":"DP-1", "identity":"drm-edid:456", "enabled":true}]);
assert!(restored.resolve("org.test.App", true, false, &[], &outputs).is_none());
let changed = serde_json::json!([{"name":"DP-1", "identity":"drm-edid:456", "enabled":true}]);
assert!(
restored
.resolve("org.test.App", true, false, &sources, &changed)
.is_none()
);
let disabled =
serde_json::json!([{"name":"DP-1", "identity":"drm-edid:123", "enabled":false}]);
let disabled = serde_json::json!([{"name":"DP-1", "identity":"drm-edid:123", "enabled":false}]);
assert!(
restored
.resolve("org.test.App", true, false, &sources, &disabled)
@@ -240,14 +215,9 @@ mod tests {
#[test]
fn transient_permission_cannot_be_upgraded_without_new_consent() {
let outputs =
serde_json::json!([{"name":"DP-1", "identity":"drm-edid:123", "enabled":true}]);
let outputs = serde_json::json!([{"name":"DP-1", "identity":"drm-edid:123", "enabled":true}]);
let saved = Restore::create("org.test.App", false, &[source()], &outputs, 1).unwrap();
let restored = Restore::read(&Options::from([(
"restore_data".into(),
saved.value().unwrap(),
)]))
.unwrap();
let restored = Restore::read(&Options::from([("restore_data".into(), saved.value().unwrap())])).unwrap();
assert_eq!(restored.effective_mode(2), 1);
assert_eq!(restored.effective_mode(0), 0);
let weak = serde_json::json!([{"name":"DP-1", "identity":"drm:DP-1", "enabled":true}]);
@@ -259,8 +229,7 @@ mod tests {
fn anonymous_nested_selection_has_lifetime_checks_without_saved_grants() {
let outputs = serde_json::json!([{"name":"DP-1", "id":1, "identity":null, "enabled":true}]);
assert!(selection_unchanged(&[source()], &outputs, &outputs));
let replacement =
serde_json::json!([{"name":"DP-1", "id":2, "identity":null, "enabled":true}]);
let replacement = serde_json::json!([{"name":"DP-1", "id":2, "identity":null, "enabled":true}]);
assert!(!selection_unchanged(&[source()], &outputs, &replacement));
assert!(Restore::create("org.test.App", false, &[source()], &outputs, 2).is_none());
}
@@ -288,11 +257,7 @@ mod tests {
.is_none()
);
saved.monitors[1] = saved.monitors[0].clone();
assert!(
saved
.resolve("org.test.App", false, true, &sources, &outputs)
.is_none()
);
assert!(saved.resolve("org.test.App", false, true, &sources, &outputs).is_none());
}
#[test]
+13 -17
View File
@@ -1,12 +1,13 @@
use std::collections::HashMap;
use std::sync::Arc;
use std::time::Duration;
use ferese_config::Document;
use ferese_theme::Palette;
use std::{collections::HashMap, sync::Arc, time::Duration};
use tokio::sync::RwLock;
use zbus::{
Connection,
object_server::SignalEmitter,
zvariant::{OwnedValue, Value},
};
use zbus::Connection;
use zbus::object_server::SignalEmitter;
use zbus::zvariant::{OwnedValue, Value};
const APPEARANCE: &str = "org.freedesktop.appearance";
const PATH: &str = "/org/freedesktop/portal/desktop";
@@ -50,10 +51,7 @@ impl Appearance {
fn values(self) -> HashMap<String, OwnedValue> {
HashMap::from([
("color-scheme".into(), self.scheme.into()),
(
"accent-color".into(),
Value::from(self.accent).try_to_owned().unwrap(),
),
("accent-color".into(), Value::from(self.accent).try_to_owned().unwrap()),
("contrast".into(), 0u32.into()),
("reduced-motion".into(), self.reduced_motion.into()),
])
@@ -156,12 +154,7 @@ mod tests {
#[test]
fn namespaces_support_only_trailing_section_wildcards() {
for filter in [
"",
"org.freedesktop.appearance",
"org.freedesktop.*",
"org.*",
] {
for filter in ["", "org.freedesktop.appearance", "org.freedesktop.*", "org.*"] {
assert!(matches_namespace(&[filter.into()], APPEARANCE));
}
for filter in [
@@ -177,7 +170,10 @@ mod tests {
#[test]
fn reports_ferese_theme_and_reduced_motion() {
let doc = Document::parse("theme { colors { surface-base \"#ffffff\"; accent \"#ff8000\"; }; }; animations { enabled #false; }").unwrap();
let doc = Document::parse(
"theme { colors { surface-base \"#ffffff\"; accent \"#ff8000\"; }; }; animations { enabled #false; }",
)
.unwrap();
let appearance = Appearance::from_document(Some(&doc));
assert_eq!(appearance.scheme, 2);
assert_eq!(appearance.reduced_motion, 1);
+31 -76
View File
@@ -1,20 +1,21 @@
use std::{collections::HashMap, io::Read, process::Stdio, sync::Arc, time::Duration};
use std::collections::HashMap;
use std::io::Read;
use std::process::Stdio;
use std::sync::Arc;
use std::time::Duration;
use serde_json::json;
use tokio::{io::AsyncWriteExt, sync::Mutex};
use zbus::{
Connection,
message::Header,
object_server::SignalEmitter,
zvariant::{OwnedObjectPath, OwnedValue, Value},
};
use tokio::io::AsyncWriteExt;
use tokio::sync::Mutex;
use zbus::Connection;
use zbus::message::Header;
use zbus::object_server::SignalEmitter;
use zbus::zvariant::{OwnedObjectPath, OwnedValue, Value};
use crate::{
backend::{Cancel, Options, authorize},
bridge::Bridge,
consent::{Prompt, ShortcutField},
desktop::Requests,
};
use crate::backend::{Cancel, Options, authorize};
use crate::bridge::Bridge;
use crate::consent::{Prompt, ShortcutField};
use crate::desktop::Requests;
const PATH: &str = "/org/freedesktop/portal/desktop";
type Shortcuts = Vec<(String, Options)>;
@@ -88,10 +89,7 @@ impl GlobalShortcuts {
if let Ok(emitter) = SignalEmitter::new(connection, path) {
let _ = SessionObject::closed(&emitter).await;
}
let _ = connection
.object_server()
.remove::<SessionObject, _>(path)
.await;
let _ = connection.object_server().remove::<SessionObject, _>(path).await;
}
}
@@ -134,25 +132,14 @@ impl GlobalShortcuts {
.filter(|field| !field.trigger.trim().is_empty())
.map(|field| json!({"id":field.id,"trigger":field.trigger.trim()}))
.collect::<Vec<_>>();
match bridge
.call("portal-shortcuts-register", json!(registration))
.await
{
match bridge.call("portal-shortcuts-register", json!(registration)).await {
Ok(_) => {
let mut state = session.state.lock().await;
if session
.cancel
.stopped
.load(std::sync::atomic::Ordering::SeqCst)
if session.cancel.stopped.load(std::sync::atomic::Ordering::SeqCst)
|| cancel.stopped.load(std::sync::atomic::Ordering::SeqCst)
{
if first
|| session
.cancel
.stopped
.load(std::sync::atomic::Ordering::SeqCst)
{
if first || session.cancel.stopped.load(std::sync::atomic::Ordering::SeqCst) {
bridge.close();
} else {
let previous = previous
@@ -161,11 +148,7 @@ impl GlobalShortcuts {
.map(|field| json!({"id":field.id,"trigger":field.trigger}))
.collect::<Vec<_>>();
if bridge
.call("portal-shortcuts-register", json!(previous))
.await
.is_err()
{
if bridge.call("portal-shortcuts-register", json!(previous)).await.is_err() {
bridge.close();
}
}
@@ -202,13 +185,7 @@ impl GlobalShortcuts {
}
}
async fn watch(
&self,
connection: Connection,
path: OwnedObjectPath,
session: Arc<Session>,
bridge: Bridge,
) {
async fn watch(&self, connection: Connection, path: OwnedObjectPath, session: Arc<Session>, bridge: Bridge) {
let emitter = SignalEmitter::new(&connection, PATH).unwrap();
loop {
tokio::select! {
@@ -221,11 +198,7 @@ impl GlobalShortcuts {
_ => break,
};
let mut state = session.state.lock().await;
if session
.cancel
.stopped
.load(std::sync::atomic::Ordering::SeqCst)
{
if session.cancel.stopped.load(std::sync::atomic::Ordering::SeqCst) {
break;
}
if let Some(active) = value["shortcuts"]
@@ -234,16 +207,13 @@ impl GlobalShortcuts {
{
let mut changed = false;
for field in &mut state.fields {
if !field.trigger.is_empty()
&& !active.iter().any(|item| item["id"] == field.id)
{
if !field.trigger.is_empty() && !active.iter().any(|item| item["id"] == field.id) {
field.trigger.clear();
changed = true;
}
}
if changed {
let _ = Self::shortcuts_changed(&emitter, path.clone(), results(&state.fields))
.await;
let _ = Self::shortcuts_changed(&emitter, path.clone(), results(&state.fields)).await;
}
}
for event in value["events"].as_array().into_iter().flatten() {
@@ -252,12 +222,9 @@ impl GlobalShortcuts {
};
let timestamp = event["timestamp"].as_u64().unwrap_or(0);
if event["active"] == true {
let _ = Self::activated(&emitter, path.clone(), id, timestamp, Options::new())
.await;
let _ = Self::activated(&emitter, path.clone(), id, timestamp, Options::new()).await;
} else {
let _ =
Self::deactivated(&emitter, path.clone(), id, timestamp, Options::new())
.await;
let _ = Self::deactivated(&emitter, path.clone(), id, timestamp, Options::new()).await;
}
}
}
@@ -282,15 +249,11 @@ impl GlobalShortcuts {
#[zbus(header)] header: Header<'_>,
) -> zbus::fdo::Result<(u32, Options)> {
let owner = authorize(connection, &header).await?;
if !session_handle
.as_str()
.starts_with(&format!("{PATH}/session/"))
if !session_handle.as_str().starts_with(&format!("{PATH}/session/"))
|| session_handle.as_str().len() > 512
|| app_id.len() > 512
{
return Err(zbus::fdo::Error::InvalidArgs(
"Invalid shortcut session".into(),
));
return Err(zbus::fdo::Error::InvalidArgs("Invalid shortcut session".into()));
}
let cancel = self.requests.begin(connection, &header, &handle).await?;
let mut sessions = self.sessions.lock().await;
@@ -500,9 +463,7 @@ impl SessionObject {
#[zbus(header)] header: Header<'_>,
) -> zbus::fdo::Result<()> {
if header.sender().map(|sender| sender.as_str()) != Some(self.owner.as_str()) {
return Err(zbus::fdo::Error::AccessDenied(
"Not your shortcut session".into(),
));
return Err(zbus::fdo::Error::AccessDenied("Not your shortcut session".into()));
}
self.backend.end(connection, &self.path).await;
Ok(())
@@ -547,10 +508,7 @@ fn fields(shortcuts: Shortcuts) -> Result<Vec<ShortcutField>, String> {
Ok(fields)
}
fn validate_selection(
requested: &[ShortcutField],
selected: &[ShortcutField],
) -> Result<(), String> {
fn validate_selection(requested: &[ShortcutField], selected: &[ShortcutField]) -> Result<(), String> {
if selected.len() != requested.len()
|| selected.iter().zip(requested).any(|(chosen, original)| {
chosen.id != original.id
@@ -590,10 +548,7 @@ fn reply(response: Result<Option<Shortcuts>, String>) -> (u32, Options) {
match response {
Ok(Some(shortcuts)) => (
0,
HashMap::from([(
"shortcuts".into(),
Value::from(shortcuts).try_into().unwrap(),
)]),
HashMap::from([("shortcuts".into(), Value::from(shortcuts).try_into().unwrap())]),
),
Ok(None) => (1, Options::new()),
Err(error) => {
+24 -49
View File
@@ -1,17 +1,17 @@
//! PipeWire runs outside the compositor. Keep one frame, drop old frames under
//! backpressure, and destroy the node when the portal's control pipe closes.
use crate::capture::{Capture, Frame};
use pipewire::{self as pw, properties::properties, spa};
use std::io::{Read, Write};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};
use pipewire as pw;
use pipewire::properties::properties;
use pipewire::spa;
use serde::{Deserialize, Serialize};
use spa::pod::Pod;
use std::{
io::{Read, Write},
sync::{
Arc, Mutex,
atomic::{AtomicBool, Ordering},
},
time::{Duration, Instant},
};
use crate::capture::{Capture, Frame};
#[derive(Debug, Serialize, Deserialize)]
pub struct Ready {
@@ -70,10 +70,7 @@ impl<'a> OutputBuffer<'a> {
)
.cast::<spa::sys::spa_meta_header>();
if let Some(header) = header.as_mut() {
let mut now = libc::timespec {
tv_sec: 0,
tv_nsec: 0,
};
let mut now = libc::timespec { tv_sec: 0, tv_nsec: 0 };
header.pts = if libc::clock_gettime(libc::CLOCK_MONOTONIC, &mut now) == 0 {
now.tv_sec * 1_000_000_000 + now.tv_nsec
} else {
@@ -104,13 +101,10 @@ fn int_property(key: u32, value: i32) -> spa::pod::Property {
}
}
fn pod(object: spa::pod::Object) -> Vec<u8> {
spa::pod::serialize::PodSerializer::serialize(
std::io::Cursor::new(Vec::new()),
&spa::pod::Value::Object(object),
)
.unwrap()
.0
.into_inner()
spa::pod::serialize::PodSerializer::serialize(std::io::Cursor::new(Vec::new()), &spa::pod::Value::Object(object))
.unwrap()
.0
.into_inner()
}
fn video_format(width: u32, height: u32) -> Vec<u8> {
@@ -145,11 +139,7 @@ fn video_format(width: u32, height: u32) -> Vec<u8> {
))
}
pub fn run(
name: String,
cursor: bool,
generation: Option<u32>,
) -> Result<(), Box<dyn std::error::Error>> {
pub fn run(name: String, cursor: bool, generation: Option<u32>) -> Result<(), Box<dyn std::error::Error>> {
let stop = Arc::new(AtomicBool::new(false));
let pipe_stop = stop.clone();
std::thread::spawn(move || {
@@ -213,14 +203,10 @@ pub fn run(
.add_local_listener_with_user_data(data)
.state_changed(move |stream, data, _, state| {
data.fresh.store(false, Ordering::SeqCst);
data.streaming.store(
matches!(state, pw::stream::StreamState::Streaming),
Ordering::Relaxed,
);
data.streaming
.store(matches!(state, pw::stream::StreamState::Streaming), Ordering::Relaxed);
match state {
pw::stream::StreamState::Paused | pw::stream::StreamState::Streaming
if !data.announced =>
{
pw::stream::StreamState::Paused | pw::stream::StreamState::Streaming if !data.announced => {
let node = stream.node_id();
if node != u32::MAX {
data.announced = true;
@@ -257,8 +243,7 @@ pub fn run(
|| format.size().width == 0
|| format.size().height == 0
|| (!resizable && (format.size().width, format.size().height) != (width, height))
|| u64::from(format.size().width) * u64::from(format.size().height) * 4
> 128 * 1024 * 1024
|| u64::from(format.size().width) * u64::from(format.size().height) * 4 > 128 * 1024 * 1024
|| format.format() != spa::param::video::VideoFormat::BGRx
{
data.stop.store(true, Ordering::Relaxed);
@@ -271,10 +256,7 @@ pub fn run(
spa::param::ParamType::Buffers,
int_property(spa::sys::SPA_PARAM_BUFFERS_buffers, 4),
int_property(spa::sys::SPA_PARAM_BUFFERS_blocks, 1),
int_property(
spa::sys::SPA_PARAM_BUFFERS_size,
(width * height * 4) as i32
),
int_property(spa::sys::SPA_PARAM_BUFFERS_size, (width * height * 4) as i32),
int_property(spa::sys::SPA_PARAM_BUFFERS_stride, (width * 4) as i32),
int_property(spa::sys::SPA_PARAM_BUFFERS_align, 16)
));
@@ -292,10 +274,7 @@ pub fn run(
)
));
if stream
.update_params(&mut [
Pod::from_bytes(&buffers).unwrap(),
Pod::from_bytes(&header).unwrap(),
])
.update_params(&mut [Pod::from_bytes(&buffers).unwrap(), Pod::from_bytes(&header).unwrap()])
.is_err()
{
data.stop.store(true, Ordering::Relaxed);
@@ -358,9 +337,7 @@ pub fn run(
while !capture_stop.load(Ordering::Relaxed) {
let start = Instant::now();
if capture_streaming.load(Ordering::Relaxed)
|| checked.elapsed() >= Duration::from_secs(1)
{
if capture_streaming.load(Ordering::Relaxed) || checked.elapsed() >= Duration::from_secs(1) {
checked = Instant::now();
match capture.frame(&name, cursor, &capture_stop, std::mem::take(&mut spare)) {
Ok(frame) if resizable || (frame.width == width && frame.height == height) => {
@@ -407,9 +384,7 @@ pub fn run(
} else {
Duration::from_millis(10)
};
mainloop
.loop_()
.iterate(pw::loop_::Timeout::Finite(timeout));
mainloop.loop_().iterate(pw::loop_::Timeout::Finite(timeout));
if streaming.load(Ordering::Relaxed) && Instant::now() >= next_frame {
let _ = stream.trigger_process();
next_frame = Instant::now() + interval;