Fix compositor output routing and pointer hints; format imports
This commit is contained in:
1 parent
88e4f3c135
commit
46509f880a
108 files changed
+3682
-8371
No files matched your search
+58
-154
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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,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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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;
|
||||
|
||||
Reference in new issue
Block a user