Format Rust sources at 100 columns

This commit is contained in:
Samuel Onoja committed 2026-10-22 01:28:03 +01:00
1 parent 4661f595f8
commit 610478f8c7
336 files changed
+23497 -7892

No files matched your search

+174 -61
View File
@@ -120,8 +120,12 @@ impl RecorderControl {
if !session.bar_controlled || session.phase != Phase::Running {
return Err(error("Recording has not started"));
}
serde_json::to_string(&session.recording.ok_or_else(|| error("No recording options"))?)
.map_err(|e| error(&e.to_string()))
serde_json::to_string(
&session
.recording
.ok_or_else(|| error("No recording options"))?,
)
.map_err(|e| error(&e.to_string()))
}
async fn use_bar_controls(
@@ -148,12 +152,18 @@ async fn authorize_recorder(
connection: &Connection,
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(), 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(),
@@ -183,14 +193,18 @@ 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()))?;
@@ -200,7 +214,9 @@ pub(crate) async fn authorize(connection: &Connection, header: &Header<'_>) -> z
.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())
@@ -249,7 +265,10 @@ 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;
});
}
}
@@ -295,12 +314,9 @@ impl Backend {
"Session limit reached or duplicate session".into(),
));
}
let work = Arc::new(
self.work
.clone()
.try_acquire_owned()
.map_err(|_| zbus::fdo::Error::LimitsExceeded("Capture work is still shutting down".into()))?,
);
let work = Arc::new(self.work.clone().try_acquire_owned().map_err(|_| {
zbus::fdo::Error::LimitsExceeded("Capture work is still shutting down".into())
})?);
let inserted = connection
.object_server()
.at(
@@ -346,14 +362,21 @@ 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"));
}
@@ -389,7 +412,10 @@ 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"));
}
@@ -457,10 +483,15 @@ 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, recording)) if !cancel.stopped.load(Ordering::SeqCst) => {
Ok((children, streams, mut reply, recording))
if !cancel.stopped.load(Ordering::SeqCst) =>
{
if let Some(session) = self.sessions.lock().await.get_mut(session_handle.as_str()) {
session.phase = Phase::Running;
session.recording = recording;
@@ -475,7 +506,8 @@ 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))
@@ -665,7 +697,8 @@ 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<_>>();
@@ -673,7 +706,10 @@ 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()?;
@@ -708,7 +744,8 @@ async fn start_streams(
return Err("Invalid persistence selection".into());
}
let selected = validate_selection(&prompt.sources, &selection.names, multiple)?;
if !bar_controlled && (selection.audio != crate::recording::Audio::Off || selection.region) {
if !bar_controlled && (selection.audio != crate::recording::Audio::Off || selection.region)
{
return Err("Recording options are only available to the native recorder".into());
}
crate::desktop::wait_for_surface_removal_using(|| {
@@ -739,7 +776,12 @@ async fn start_streams(
return Err(StartError::Cancelled);
}
crate::desktop::wait_for_surface_removal_using(|| {
capture_ipc(&cancel, &work, "has-client-surfaces", serde_json::json!({"pid": pid}))
capture_ipc(
&cancel,
&work,
"has-client-surfaces",
serde_json::json!({"pid": pid}),
)
})
.await?;
Some(crate::recording_region::Region::parse(
@@ -814,11 +856,15 @@ 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());
@@ -838,7 +884,10 @@ 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());
}
@@ -848,12 +897,23 @@ async fn start_streams(
.map(|region| {
let (name, ready) = streams.first().ok_or("Missing recording display")?;
let metadata = monitor_metadata(&final_outputs, name, ready)?;
let (x, y) = <(i32, i32)>::try_from(metadata["position"].try_clone().map_err(|e| e.to_string())?)
.map_err(|e| e.to_string())?;
let (width, height) = <(i32, i32)>::try_from(metadata["size"].try_clone().map_err(|e| e.to_string())?)
.map_err(|e| e.to_string())?;
let (x, y) = <(i32, i32)>::try_from(
metadata["position"]
.try_clone()
.map_err(|e| e.to_string())?,
)
.map_err(|e| e.to_string())?;
let (width, height) = <(i32, i32)>::try_from(
metadata["size"].try_clone().map_err(|e| e.to_string())?,
)
.map_err(|e| e.to_string())?;
region.crop(
crate::recording_region::Region { x, y, width, height },
crate::recording_region::Region {
x,
y,
width,
height,
},
(ready.width, ready.height),
)
})
@@ -874,10 +934,11 @@ 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)
@@ -895,7 +956,9 @@ 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")?,
@@ -907,15 +970,25 @@ 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]
@@ -961,13 +1034,23 @@ fn monitor_metadata(outputs: &serde_json::Value, name: &str, ready: &Ready) -> R
),
(
"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());
}
@@ -990,7 +1073,11 @@ fn validate_selection(sources: &[Source], names: &[String], multiple: bool) -> R
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;
@@ -1024,7 +1111,10 @@ 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?;
@@ -1048,7 +1138,9 @@ 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
@@ -1097,10 +1189,11 @@ mod tests {
permits.remove(0);
assert!(backend.work.clone().try_acquire_owned().is_err());
release.send(()).unwrap();
let recovered = tokio::time::timeout(Duration::from_secs(1), backend.work.clone().acquire_owned())
.await
.unwrap()
.unwrap();
let recovered =
tokio::time::timeout(Duration::from_secs(1), backend.work.clone().acquire_owned())
.await
.unwrap()
.unwrap();
assert_eq!(recovered.num_permits(), 1);
assert_eq!(backend.work.available_permits(), 0);
drop(recovered);
@@ -1203,7 +1296,10 @@ 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());
@@ -1260,7 +1356,10 @@ 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());
}
@@ -1277,8 +1376,22 @@ 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
);
}
}
+20 -7
View File
@@ -49,7 +49,10 @@ impl Bridge {
#[cfg(test)]
pub(crate) fn test_pair() -> (Self, UnixStream) {
let (stream, peer) = UnixStream::pair().unwrap();
(Self::from_stream(stream, Duration::from_secs(1)).unwrap(), peer)
(
Self::from_stream(stream, Duration::from_secs(1)).unwrap(),
peer,
)
}
pub(crate) async fn call(&self, command: &'static str, args: Json) -> Result<Json, String> {
@@ -86,7 +89,10 @@ impl Bridge {
return Err("Compositor connection is closed".to_owned());
}
connection.id = connection.id.checked_add(1).ok_or("Compositor request IDs exhausted")?;
connection.id = connection
.id
.checked_add(1)
.ok_or("Compositor request IDs exhausted")?;
let request = ferese_ipc::Request {
version: ferese_ipc::VERSION,
id: connection.id,
@@ -98,10 +104,13 @@ impl Bridge {
.stream
.set_read_timeout(if wait { None } else { Some(self.0.timeout) })
.map_err(|error| error.to_string())?;
ferese_ipc::write_frame(&mut connection.stream, &request).map_err(|error| error.to_string())?;
let value: Json = ferese_ipc::read_frame(&mut connection.stream).map_err(|error| error.to_string())?;
ferese_ipc::write_frame(&mut connection.stream, &request)
.map_err(|error| error.to_string())?;
let value: Json = ferese_ipc::read_frame(&mut connection.stream)
.map_err(|error| error.to_string())?;
let has_result = value.get("result").is_some();
let response: ferese_ipc::Response = serde_json::from_value(value).map_err(|error| error.to_string())?;
let response: ferese_ipc::Response =
serde_json::from_value(value).map_err(|error| error.to_string())?;
if response.id != request.id || response.version != ferese_ipc::VERSION {
return Err("Unexpected compositor response ID or protocol version".into());
}
@@ -128,8 +137,12 @@ 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) fn is_closed(&self) -> bool {
+34 -7
View File
@@ -9,7 +9,9 @@ fn pair() -> (Bridge, UnixStream) {
fn pair_with_timeout(timeout: Duration) -> (Bridge, UnixStream) {
let (client, server) = UnixStream::pair().unwrap();
server.set_read_timeout(Some(Duration::from_secs(2))).unwrap();
server
.set_read_timeout(Some(Duration::from_secs(2)))
.unwrap();
(Bridge::from_stream(client, timeout).unwrap(), server)
}
@@ -21,19 +23,29 @@ async fn valid_errors_preserve_connection_and_request_ids_are_unique() {
write_frame(&mut server, &Response::error(first.id, "denied", "Denied")).unwrap();
let second: Request = read_frame(&mut server).unwrap();
assert_eq!(second.id, first.id + 1);
write_frame(&mut server, &Response::success(second.id, json!({"enabled":false}))).unwrap();
write_frame(
&mut server,
&Response::success(second.id, json!({"enabled":false})),
)
.unwrap();
let third: Request = read_frame(&mut server).unwrap();
assert_eq!(third.id, second.id + 1);
write_frame(&mut server, &Response::success(third.id, Json::Null)).unwrap();
});
assert_eq!(bridge.call("enable", json!({})).await.unwrap_err(), "Denied");
assert_eq!(
bridge.call("enable", json!({})).await.unwrap_err(),
"Denied"
);
assert!(!bridge.is_closed());
assert_eq!(
bridge.call("disable", json!({})).await.unwrap(),
json!({"enabled":false})
);
assert!(!bridge.is_closed());
assert_eq!(bridge.call("empty-result", json!({})).await.unwrap(), Json::Null);
assert_eq!(
bridge.call("empty-result", json!({})).await.unwrap(),
Json::Null
);
peer.join().unwrap();
}
@@ -44,7 +56,13 @@ async fn timeout_closes_stream_before_a_late_reply_can_be_reused() {
let peer = std::thread::spawn(move || {
let request: Request = read_frame(&mut server).unwrap();
blocked.recv_timeout(Duration::from_secs(2)).unwrap();
assert!(write_frame(&mut server, &Response::success(request.id, json!({"enabled":true}))).is_err());
assert!(
write_frame(
&mut server,
&Response::success(request.id, json!({"enabled":true}))
)
.is_err()
);
assert_eq!(server.read(&mut [0u8; 1]).unwrap(), 0);
});
assert!(bridge.call("enable", json!({})).await.is_err());
@@ -52,7 +70,13 @@ async fn timeout_closes_stream_before_a_late_reply_can_be_reused() {
tokio::time::timeout(Duration::from_secs(1), bridge.terminated())
.await
.unwrap();
assert!(bridge.call("disable", json!({})).await.unwrap_err().contains("closed"));
assert!(
bridge
.call("disable", json!({}))
.await
.unwrap_err()
.contains("closed")
);
release.send(()).unwrap();
peer.join().unwrap();
}
@@ -99,7 +123,10 @@ async fn malformed_or_mismatched_replies_poison_the_bridge() {
}
write_frame(&mut server, &response).unwrap();
});
assert!(bridge.call("enable", json!({})).await.is_err(), "case {case}");
assert!(
bridge.call("enable", json!({})).await.is_err(),
"case {case}"
);
assert!(bridge.is_closed(), "case {case}");
assert!(bridge.call("disable", json!({})).await.is_err());
peer.join().unwrap();
+86 -23
View File
@@ -102,7 +102,9 @@ 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)
@@ -110,7 +112,9 @@ 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)?;
@@ -138,7 +142,11 @@ impl Capture {
Ok(())
}
pub fn validate_sources(&mut self, selected: &[(String, u32)], stop: &AtomicBool) -> Result<(), String> {
pub fn validate_sources(
&mut self,
selected: &[(String, u32)],
stop: &AtomicBool,
) -> Result<(), String> {
self.sync(stop)?;
for (name, generation) in selected {
if self.generation(name) != Some(*generation) {
@@ -162,19 +170,34 @@ 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 {
@@ -184,11 +207,18 @@ 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,
@@ -206,7 +236,11 @@ 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,
@@ -251,7 +285,9 @@ 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());
}
}
@@ -293,9 +329,13 @@ 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),
@@ -320,7 +360,9 @@ 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;
}
@@ -372,7 +414,9 @@ 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());
@@ -382,13 +426,20 @@ 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,
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,
(),
);
let proxy = pool.create_buffer(0, width as i32, height as i32, stride as i32, format, qh, ());
pool.destroy();
frame.copy(&proxy);
@@ -491,7 +542,9 @@ 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());
@@ -527,7 +580,9 @@ 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,
_ => (),
}
@@ -545,13 +600,17 @@ mod tests {
for discovery in [true, false] {
let (client, mut server) = UnixStream::pair().unwrap();
server.set_read_timeout(Some(Duration::from_secs(2))).unwrap();
server
.set_read_timeout(Some(Duration::from_secs(2)))
.unwrap();
let stop = Arc::new(AtomicBool::new(false));
let flag = stop.clone();
let worker = std::thread::spawn(move || {
let connection = Connection::from_socket(client).unwrap();
if discovery {
Capture::connect_with_connection(connection, &flag).err().unwrap()
Capture::connect_with_connection(connection, &flag)
.err()
.unwrap()
} else {
let queue = connection.new_event_queue();
let mut capture = Capture {
@@ -591,7 +650,11 @@ 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));
+12 -5
View File
@@ -49,7 +49,9 @@ 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());
@@ -110,11 +112,15 @@ impl cosmic::Application for Consent {
fn subscription(&self) -> cosmic::iced::Subscription<Message> {
cosmic::iced::Subscription::batch([
ferese_theme_client::service::subscription().map(|snapshot| Message::ThemeChanged(Box::new(snapshot))),
ferese_theme_client::service::subscription()
.map(|snapshot| Message::ThemeChanged(Box::new(snapshot))),
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),
key:
cosmic::iced::keyboard::Key::Named(cosmic::iced::keyboard::key::Named::Escape),
..
}) => Some(Message::Cancel),
_ => None,
@@ -126,7 +132,8 @@ impl cosmic::Application for Consent {
match message {
Message::ThemeChanged(snapshot) => {
self.palette = Palette::from_resolved(&snapshot.presented).flat();
self.font = ferese_theme::font(Some(&snapshot.presented.tokens.typography.font_family));
self.font =
ferese_theme::font(Some(&snapshot.presented.tokens.typography.font_family));
self.background = cosmic::iced::Color {
a: ferese_theme::material_opacity(&snapshot.presented),
..self.palette.sidebar
+188 -55
View File
@@ -59,7 +59,9 @@ 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());
@@ -74,7 +76,9 @@ 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()));
@@ -83,7 +87,10 @@ 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;
}
}
@@ -243,13 +250,19 @@ 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())?
}
pub(crate) fn ipc_sync(command: &'static str, args: serde_json::Value) -> Result<serde_json::Value, String> {
pub(crate) fn ipc_sync(
command: &'static str,
args: serde_json::Value,
) -> Result<serde_json::Value, String> {
let path = ferese_ipc::socket::resolve(None).map_err(|error| error.to_string())?;
let mut stream = UnixStream::connect(path).map_err(|error| error.to_string())?;
@@ -271,10 +284,14 @@ pub(crate) fn ipc_sync(command: &'static str, args: serde_json::Value) -> Result
exchange(&mut stream, &request)
}
fn exchange(stream: &mut UnixStream, request: &ferese_ipc::Request) -> Result<serde_json::Value, String> {
fn exchange(
stream: &mut UnixStream,
request: &ferese_ipc::Request,
) -> Result<serde_json::Value, String> {
ferese_ipc::write_frame(stream, request).map_err(|error| error.to_string())?;
let response: ferese_ipc::Response = ferese_ipc::read_frame(stream).map_err(|error| error.to_string())?;
let response: ferese_ipc::Response =
ferese_ipc::read_frame(stream).map_err(|error| error.to_string())?;
if response.id != request.id || response.version != ferese_ipc::VERSION {
return Err("Unexpected compositor response ID or protocol version".into());
}
@@ -322,7 +339,8 @@ async fn select_geometry(point: bool) -> Result<Option<String>, String> {
}
pub(crate) async fn wait_for_surface_removal(pid: u32) -> Result<(), String> {
wait_for_surface_removal_using(|| ipc("has-client-surfaces", serde_json::json!({"pid": pid}))).await
wait_for_surface_removal_using(|| ipc("has-client-surfaces", serde_json::json!({"pid": pid})))
.await
}
pub(crate) async fn wait_for_surface_removal_using<
@@ -344,7 +362,10 @@ 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")?;
@@ -501,14 +522,20 @@ 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)
@@ -564,7 +591,8 @@ 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());
}
@@ -595,7 +623,9 @@ 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())?,
)])))
}
@@ -608,7 +638,9 @@ 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());
}
@@ -628,7 +660,8 @@ 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)
}
@@ -696,7 +729,11 @@ 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")?;
@@ -719,7 +756,8 @@ 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());
@@ -728,7 +766,9 @@ 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
@@ -799,25 +839,35 @@ fn edit_wallpaper(
let source = file.source();
let mut document = ferese_config::Document::parse(source).map_err(|error| error.to_string())?;
let directory = path.parent().ok_or("Invalid config path")?;
let before = wallpaper_theme(&document, directory, appearance)?.tokens.background;
let before = wallpaper_theme(&document, directory, appearance)?
.tokens
.background;
let (variant, other) = match appearance {
Appearance::Light => ("light", Appearance::Dark),
Appearance::Dark => ("dark", Appearance::Light),
};
let other_before = wallpaper_theme(&document, directory, other)?.tokens.background;
let other_before = wallpaper_theme(&document, directory, other)?
.tokens
.background;
let base = format!("theme.{variant}.background");
if target == "background" && before.lock_path.is_none() {
let old = before
.path
.unwrap_or_else(|| ferese_config::default_wallpaper_for(appearance).into());
document
.set(&format!("{base}.lock_path"), old.to_string_lossy().into_owned().into())
.set(
&format!("{base}.lock_path"),
old.to_string_lossy().into_owned().into(),
)
.map_err(|error| error.to_string())?;
}
if target != "lockscreen" {
document
.set(&format!("{base}.path"), image.to_string_lossy().into_owned().into())
.set(
&format!("{base}.path"),
image.to_string_lossy().into_owned().into(),
)
.map_err(|error| error.to_string())?;
}
@@ -830,10 +880,15 @@ fn edit_wallpaper(
.map_err(|error| error.to_string())?;
}
let after = wallpaper_theme(&document, directory, appearance)?.tokens.background;
let after = wallpaper_theme(&document, directory, appearance)?
.tokens
.background;
if (target != "lockscreen" && after.path.as_deref() != Some(image))
|| (target != "background" && after.lock_path.as_deref() != Some(image))
|| wallpaper_theme(&document, directory, other)?.tokens.background != other_before
|| wallpaper_theme(&document, directory, other)?
.tokens
.background
!= other_before
{
return Err("Wallpaper change did not resolve to the requested image".into());
}
@@ -867,11 +922,18 @@ mod tests {
(ferese_ipc::VERSION, 8, false),
] {
let (mut client, mut server) = UnixStream::pair().unwrap();
client.set_read_timeout(Some(Duration::from_secs(2))).unwrap();
server.set_read_timeout(Some(Duration::from_secs(2))).unwrap();
client
.set_read_timeout(Some(Duration::from_secs(2)))
.unwrap();
server
.set_read_timeout(Some(Duration::from_secs(2)))
.unwrap();
let peer = std::thread::spawn(move || {
let _: ferese_ipc::Request = ferese_ipc::read_frame(&mut server).unwrap();
let mut response = ferese_ipc::Response::success(id, serde_json::json!({"path": "/tmp/capture.png"}));
let mut response = ferese_ipc::Response::success(
id,
serde_json::json!({"path": "/tmp/capture.png"}),
);
response.version = version;
ferese_ipc::write_frame(&mut server, &response).unwrap();
});
@@ -894,7 +956,11 @@ 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());
}
}
@@ -955,29 +1021,60 @@ mod tests {
)
.unwrap();
let resolve = |mode| {
let document = ferese_config::Document::parse(&std::fs::read_to_string(&path).unwrap()).unwrap();
wallpaper_theme(&document, dir.path(), mode).unwrap().tokens.background
let document =
ferese_config::Document::parse(&std::fs::read_to_string(&path).unwrap())
.unwrap();
wallpaper_theme(&document, dir.path(), mode)
.unwrap()
.tokens
.background
};
let other = if appearance == Light { Dark } else { Light };
let untouched = resolve(other);
edit_wallpaper(&path, Path::new("/desktop.png"), "background", appearance).unwrap();
assert_eq!(resolve(appearance).path.as_deref(), Some(Path::new("/desktop.png")));
assert_eq!(resolve(appearance).lock_path.as_deref(), Some(Path::new("/old.png")));
assert_eq!(
resolve(appearance).path.as_deref(),
Some(Path::new("/desktop.png"))
);
assert_eq!(
resolve(appearance).lock_path.as_deref(),
Some(Path::new("/old.png"))
);
assert_eq!(resolve(other), untouched);
assert!(std::fs::read_to_string(&path).unwrap().contains("Keep my settings"));
assert!(
std::fs::read_to_string(&path)
.unwrap()
.contains("Keep my settings")
);
edit_wallpaper(&path, Path::new("/lock.png"), "lockscreen", appearance).unwrap();
assert_eq!(resolve(appearance).path.as_deref(), Some(Path::new("/desktop.png")));
assert_eq!(resolve(appearance).lock_path.as_deref(), Some(Path::new("/lock.png")));
assert_eq!(
resolve(appearance).path.as_deref(),
Some(Path::new("/desktop.png"))
);
assert_eq!(
resolve(appearance).lock_path.as_deref(),
Some(Path::new("/lock.png"))
);
assert_eq!(resolve(other), untouched);
edit_wallpaper(&path, Path::new("/both.png"), "both", appearance).unwrap();
assert_eq!(resolve(appearance).path.as_deref(), Some(Path::new("/both.png")));
assert_eq!(resolve(appearance).lock_path.as_deref(), Some(Path::new("/both.png")));
assert_eq!(
resolve(appearance).path.as_deref(),
Some(Path::new("/both.png"))
);
assert_eq!(
resolve(appearance).lock_path.as_deref(),
Some(Path::new("/both.png"))
);
assert_eq!(resolve(other), untouched);
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(), "/old.png");
assert_eq!(document.get("commands.terminal").unwrap(), &serde_json::json!(["foot"]));
assert_eq!(
document.get("commands.terminal").unwrap(),
&serde_json::json!(["foot"])
);
}
}
@@ -1000,15 +1097,20 @@ mod tests {
"#,
)
.unwrap();
let before = ferese_config::Document::parse(&std::fs::read_to_string(&path).unwrap()).unwrap();
let before =
ferese_config::Document::parse(&std::fs::read_to_string(&path).unwrap()).unwrap();
let other = if appearance == Light { Dark } else { Light };
let old_active = wallpaper_theme(&before, dir.path(), appearance)
.unwrap()
.tokens
.background;
let old_other = wallpaper_theme(&before, dir.path(), other).unwrap().tokens.background;
let old_other = wallpaper_theme(&before, dir.path(), other)
.unwrap()
.tokens
.background;
edit_wallpaper(&path, Path::new("/new.png"), "background", appearance).unwrap();
let after = ferese_config::Document::parse(&std::fs::read_to_string(&path).unwrap()).unwrap();
let after =
ferese_config::Document::parse(&std::fs::read_to_string(&path).unwrap()).unwrap();
let current = wallpaper_theme(&after, dir.path(), appearance)
.unwrap()
.tokens
@@ -1016,22 +1118,35 @@ mod tests {
assert_eq!(current.path.as_deref(), Some(Path::new("/new.png")));
assert_eq!(current.lock_path, old_active.lock_path);
assert_eq!(
wallpaper_theme(&after, dir.path(), other).unwrap().tokens.background,
wallpaper_theme(&after, dir.path(), other)
.unwrap()
.tokens
.background,
old_other
);
assert_eq!(after.get("theme.mode").unwrap(), "auto");
assert_eq!(after.get("theme.background"), before.get("theme.background"));
assert_eq!(
after.get("theme.background"),
before.get("theme.background")
);
edit_wallpaper(&path, Path::new("/new-lock.png"), "lockscreen", appearance).unwrap();
let after = ferese_config::Document::parse(&std::fs::read_to_string(&path).unwrap()).unwrap();
let after =
ferese_config::Document::parse(&std::fs::read_to_string(&path).unwrap()).unwrap();
let current = wallpaper_theme(&after, dir.path(), appearance)
.unwrap()
.tokens
.background;
assert_eq!(current.path.as_deref(), Some(Path::new("/new.png")));
assert_eq!(current.lock_path.as_deref(), Some(Path::new("/new-lock.png")));
assert_eq!(
wallpaper_theme(&after, dir.path(), other).unwrap().tokens.background,
current.lock_path.as_deref(),
Some(Path::new("/new-lock.png"))
);
assert_eq!(
wallpaper_theme(&after, dir.path(), other)
.unwrap()
.tokens
.background,
old_other
);
}
@@ -1053,16 +1168,30 @@ mod tests {
"#,
)
.unwrap();
let before = ferese_config::Document::parse(&std::fs::read_to_string(&path).unwrap()).unwrap();
let previous = wallpaper_theme(&before, dir.path(), Dark).unwrap().tokens.background;
let before =
ferese_config::Document::parse(&std::fs::read_to_string(&path).unwrap()).unwrap();
let previous = wallpaper_theme(&before, dir.path(), Dark)
.unwrap()
.tokens
.background;
assert_eq!(previous.lock_path, None);
edit_wallpaper(&path, Path::new("/new.png"), "background", Dark).unwrap();
let after = ferese_config::Document::parse(&std::fs::read_to_string(&path).unwrap()).unwrap();
let current = wallpaper_theme(&after, dir.path(), Dark).unwrap().tokens.background;
let after =
ferese_config::Document::parse(&std::fs::read_to_string(&path).unwrap()).unwrap();
let current = wallpaper_theme(&after, dir.path(), Dark)
.unwrap()
.tokens
.background;
assert_eq!(current.lock_path, previous.path);
assert_eq!(
wallpaper_theme(&after, dir.path(), Light).unwrap().tokens.background,
wallpaper_theme(&before, dir.path(), Light).unwrap().tokens.background
wallpaper_theme(&after, dir.path(), Light)
.unwrap()
.tokens
.background,
wallpaper_theme(&before, dir.path(), Light)
.unwrap()
.tokens
.background
);
}
@@ -1109,6 +1238,10 @@ 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")
);
}
}
+78 -32
View File
@@ -21,7 +21,11 @@ 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());
}
@@ -47,7 +51,8 @@ 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
@@ -59,19 +64,22 @@ 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(_) => {
.insert_source(
EisRequestSource::new(context, 1),
|event, connection, state| {
let action = match event {
Ok(event) => state.request(connection, event),
Err(_) => {
state.signal.stop();
PostAction::Remove
}
};
if connection.flush().is_err() {
state.signal.stop();
PostAction::Remove
}
};
if connection.flush().is_err() {
state.signal.stop();
}
Ok(action)
})
Ok(action)
},
)
.map_err(|e| e.to_string())?;
let mut state = State {
signal: signal.clone(),
@@ -91,7 +99,8 @@ 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>(())
@@ -104,7 +113,14 @@ 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> {
@@ -156,14 +172,18 @@ 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;
@@ -179,7 +199,11 @@ 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(),
);
}
},
);
@@ -189,11 +213,15 @@ 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);
@@ -227,11 +255,14 @@ 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;
}
@@ -266,7 +297,12 @@ 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());
@@ -346,7 +382,10 @@ 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
@@ -370,8 +409,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))
@@ -386,7 +425,10 @@ 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![
@@ -401,7 +443,11 @@ 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 {
+64 -21
View File
@@ -64,7 +64,10 @@ 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();
}
}
@@ -107,7 +110,9 @@ 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;
@@ -119,7 +124,8 @@ 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())
@@ -131,7 +137,11 @@ 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()
}
};
@@ -140,13 +150,19 @@ 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)
&& Self::state_changed(
@@ -182,7 +198,9 @@ 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",
@@ -196,10 +214,16 @@ 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();
}
@@ -217,7 +241,9 @@ 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 {
@@ -237,7 +263,9 @@ 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",
@@ -280,8 +308,13 @@ 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() {
@@ -317,7 +350,9 @@ 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()),
))
}
}
@@ -333,11 +368,15 @@ 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;
@@ -430,7 +469,9 @@ 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
@@ -470,7 +511,9 @@ 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;
+111 -41
View File
@@ -87,7 +87,10 @@ 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;
});
}
}
@@ -98,7 +101,9 @@ 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;
@@ -163,7 +168,10 @@ 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;
}
@@ -186,9 +194,11 @@ impl InputCapture {
if let Some(position) = event["cursor_position"]
.as_array()
.filter(|position| position.len() == 2)
&& let Ok(position) =
Value::from((position[0].as_f64().unwrap_or(0.0), position[1].as_f64().unwrap_or(0.0)))
.try_to_owned()
&& 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);
}
@@ -205,8 +215,12 @@ 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,7 +252,8 @@ 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"));
}
@@ -266,7 +281,9 @@ 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
{
@@ -362,7 +379,9 @@ 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?;
@@ -401,11 +420,18 @@ 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());
}
@@ -461,7 +487,9 @@ 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! {
@@ -471,20 +499,25 @@ 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 = self
.finish_native_call(connection, &session_handle, &bridge, result.unwrap())
.await?;
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(),
@@ -506,7 +539,9 @@ 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"));
@@ -514,15 +549,20 @@ 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<_>>>()?;
@@ -534,7 +574,10 @@ 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()));
}
@@ -563,7 +606,9 @@ 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() {
@@ -572,10 +617,11 @@ 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())
}
@@ -588,7 +634,9 @@ 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
@@ -600,12 +648,16 @@ 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;
}
let result = bridge.call("input-capture-enable", json!({"session":id})).await;
let result = bridge
.call("input-capture-enable", json!({"session":id}))
.await;
self.finish_native_call(connection, &session_handle, &bridge, result)
.await?;
Ok((0, Options::new()))
@@ -619,9 +671,13 @@ 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 result = bridge.call("input-capture-disable", json!({"session":id})).await;
let result = bridge
.call("input-capture-disable", json!({"session":id}))
.await;
self.finish_native_call(connection, &session_handle, &bridge, result)
.await?;
Ok((0, Options::new()))
@@ -635,7 +691,9 @@ 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")
@@ -769,12 +827,19 @@ mod tests {
..State::default()
}),
});
capture.sessions.lock().await.insert(path.to_string(), session.clone());
capture
.sessions
.lock()
.await
.insert(path.to_string(), session.clone());
let response = std::thread::spawn(move || {
let request: ferese_ipc::Request = ferese_ipc::read_frame(&mut peer).unwrap();
if !transport_failure {
ferese_ipc::write_frame(&mut peer, &ferese_ipc::Response::error(request.id, "denied", "Denied"))
.unwrap();
ferese_ipc::write_frame(
&mut peer,
&ferese_ipc::Response::error(request.id, "denied", "Denied"),
)
.unwrap();
}
});
let result = control.call("input-capture-disable", json!({})).await;
@@ -785,7 +850,10 @@ mod tests {
.is_err()
);
response.join().unwrap();
assert_eq!(session.cancel.stopped.load(Ordering::SeqCst), transport_failure);
assert_eq!(
session.cancel.stopped.load(Ordering::SeqCst),
transport_failure
);
assert_eq!(watch.is_closed(), transport_failure);
assert_eq!(
capture.sessions.lock().await.contains_key(path.as_str()),
@@ -801,7 +869,9 @@ 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!(
+27 -8
View File
@@ -64,7 +64,8 @@ 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)?;
@@ -224,7 +225,8 @@ fn store(index: usize, value: bool) -> Result<(), String> {
fn store_path(path: &Path, index: usize, value: bool) -> Result<(), String> {
let file = ferese_config::file::ConfigFile::open(path)?;
let mut document = ferese_config::Document::parse(file.source()).map_err(|error| error.to_string())?;
let mut document =
ferese_config::Document::parse(file.source()).map_err(|error| error.to_string())?;
if values(&document).is_none() {
return Err("Invalid lockdown policy".into());
}
@@ -246,14 +248,23 @@ mod tests {
let directory = tempfile::tempdir().unwrap();
let path = directory.path().join("config.kdl");
let settings = ferese_config::file::ConfigFile::open(&path).unwrap();
assert!(store_path(&path, 4, true).unwrap_err().contains("another app"));
assert!(
store_path(&path, 4, true)
.unwrap_err()
.contains("another app")
);
assert!(!path.exists());
settings.replace("theme \"light\"\n", None, |_| Ok(())).unwrap();
settings
.replace("theme \"light\"\n", None, |_| Ok(()))
.unwrap();
store_path(&path, 4, true).unwrap();
let source = std::fs::read_to_string(&path).unwrap();
let document = ferese_config::Document::parse(&source).unwrap();
assert_eq!(document.get("theme").and_then(|value| value.as_str()), Some("light"));
assert_eq!(
document.get("theme").and_then(|value| value.as_str()),
Some("light")
);
assert!(load_path(&path).unwrap()[4]);
}
@@ -264,7 +275,10 @@ mod tests {
for size in [ferese_config::file::LIMIT, ferese_config::file::LIMIT + 1] {
let source = format!("//{}\n", "x".repeat(size - 3));
std::fs::write(&path, &source).unwrap();
assert_eq!(load_path(&path).is_some(), size == ferese_config::file::LIMIT);
assert_eq!(
load_path(&path).is_some(),
size == ferese_config::file::LIMIT
);
assert!(store_path(&path, 4, true).unwrap_err().contains("60 KiB"));
assert_eq!(std::fs::read_to_string(&path).unwrap(), source);
}
@@ -276,7 +290,10 @@ mod tests {
let path = directory.path().join("config.kdl");
let source = "portals { lockdown { disable-camera \"yes\"; }; }";
std::fs::write(&path, source).unwrap();
assert_eq!(store_path(&path, 4, true).unwrap_err(), "Invalid lockdown policy");
assert_eq!(
store_path(&path, 4, true).unwrap_err(),
"Invalid lockdown policy"
);
assert_eq!(std::fs::read_to_string(&path).unwrap(), source);
assert!(ferese_config::file::ConfigFile::open(&path).is_ok());
}
@@ -292,7 +309,9 @@ 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();
+8 -4
View File
@@ -21,14 +21,18 @@ 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())?);
+19 -8
View File
@@ -77,7 +77,10 @@ pub(crate) struct Parent {
}
impl Parent {
pub(crate) fn attach(window: &dyn cosmic::iced::window::Window, parent: &str) -> Result<ParentAttachment, String> {
pub(crate) fn attach(
window: &dyn cosmic::iced::window::Window,
parent: &str,
) -> Result<ParentAttachment, String> {
use cosmic::iced::window::raw_window_handle::{RawDisplayHandle, RawWindowHandle};
let handle = match classify_parent(parent) {
ParentKind::None => {
@@ -87,7 +90,8 @@ impl Parent {
return Ok(ParentAttachment {
parent: None,
diagnostic: Some(
"showing this dialog unparented: X11 parent windows are not supported yet".to_owned(),
"showing this dialog unparented: X11 parent windows are not supported yet"
.to_owned(),
),
});
}
@@ -104,8 +108,9 @@ 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(
@@ -114,15 +119,19 @@ 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();
@@ -197,7 +206,9 @@ mod tests {
#[test]
fn an_x11_parent_is_never_reinterpreted_as_a_wayland_handle() {
match classify_parent("x11:0x2400003") {
ParentKind::Wayland(handle) => panic!("an X11 parent must not become a Wayland handle: {handle}"),
ParentKind::Wayland(handle) => {
panic!("an X11 parent must not become a Wayland handle: {handle}")
}
ParentKind::X11 => {}
other => panic!("unexpected classification: {other:?}"),
}
+78 -20
View File
@@ -58,12 +58,19 @@ 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()
@@ -97,11 +104,27 @@ async fn usb_consent(
description.push_str(&format!(
"\n{} — {}",
label,
if writable { "read and write" } else { "read only" }
if writable {
"read and write"
} else {
"read only"
}
));
selected.push((
id,
HashMap::from([("writable".to_owned(), OwnedValue::from(writable))]),
));
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([(
@@ -131,7 +154,9 @@ impl Background {
loop {
let result = async {
let path = ferese_ipc::socket::resolve(None).map_err(|error| error.to_string())?;
let stream = UnixStream::connect(path).await.map_err(|error| error.to_string())?;
let stream = UnixStream::connect(path)
.await
.map_err(|error| error.to_string())?;
let mut events = BackgroundEvents::subscribe(stream).await?;
loop {
let next = match events.next().await {
@@ -187,7 +212,10 @@ impl BackgroundEvents {
let mut bytes = Vec::new();
ferese_ipc::write_frame(&mut bytes, &request).map_err(|error| error.to_string())?;
tokio::time::timeout(Duration::from_secs(2), async {
stream.write_all(&bytes).await.map_err(|error| error.to_string())?;
stream
.write_all(&bytes)
.await
.map_err(|error| error.to_string())?;
let response: ferese_ipc::Response = read_event_frame(&mut stream).await?;
if response.version != ferese_ipc::VERSION || response.id != request.id {
return Err("Unexpected event subscription response".into());
@@ -195,7 +223,10 @@ impl BackgroundEvents {
if let Some(error) = response.error {
return Err(error.message);
}
if response.result.as_ref().and_then(|value| value["version"].as_u64())
if response
.result
.as_ref()
.and_then(|value| value["version"].as_u64())
!= Some(u64::from(ferese_ipc::events::VERSION))
{
return Err("Unsupported event stream version".into());
@@ -226,8 +257,9 @@ impl BackgroundEvents {
}
Change::FocusChanged { focus } => {
for window in self.windows.as_array_mut().into_iter().flatten() {
window["focused"] =
serde_json::json!(focus["window"].is_number() && window["id"] == focus["window"]);
window["focused"] = serde_json::json!(
focus["window"].is_number() && window["id"] == focus["window"]
);
}
self.changed = true;
}
@@ -241,13 +273,18 @@ impl BackgroundEvents {
}
}
async fn read_event_frame<T: serde::de::DeserializeOwned>(stream: &mut UnixStream) -> Result<T, String> {
async fn read_event_frame<T: serde::de::DeserializeOwned>(
stream: &mut UnixStream,
) -> Result<T, String> {
let size = stream.read_u32().await.map_err(|error| error.to_string())? as usize;
if size > ferese_ipc::MAX_PAYLOAD_SIZE {
return Err("Oversized IPC event frame".into());
}
let mut bytes = vec![0; size];
stream.read_exact(&mut bytes).await.map_err(|error| error.to_string())?;
stream
.read_exact(&mut bytes)
.await
.map_err(|error| error.to_string())?;
serde_json::from_slice(&bytes).map_err(|error| error.to_string())
}
@@ -299,7 +336,8 @@ 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)]
@@ -340,7 +378,13 @@ 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
@@ -367,7 +411,9 @@ fn set_autostart(directory: &Path, app: &str, enable: bool, argv: &[String], fla
}
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
@@ -450,7 +496,11 @@ mod tests {
request.args,
serde_json::json!({"version": ferese_ipc::events::VERSION})
);
send_frame(&mut peer, &ferese_ipc::Response::success(request.id, request.args)).await;
send_frame(
&mut peer,
&ferese_ipc::Response::success(request.id, request.args),
)
.await;
peer
});
let mut events = BackgroundEvents::subscribe(client).await.unwrap();
@@ -547,7 +597,9 @@ mod tests {
windows: serde_json::json!([]),
changed: false,
};
peer.write_u32((ferese_ipc::MAX_PAYLOAD_SIZE + 1) as u32).await.unwrap();
peer.write_u32((ferese_ipc::MAX_PAYLOAD_SIZE + 1) as u32)
.await
.unwrap();
assert!(events.next().await.unwrap_err().contains("Oversized"));
drop(peer);
assert!(events.next().await.is_err());
@@ -575,9 +627,15 @@ 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");
}
+58 -23
View File
@@ -86,7 +86,9 @@ 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());
}
@@ -118,7 +120,12 @@ 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";
@@ -130,7 +137,10 @@ 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,7 +181,8 @@ impl cosmic::Application for Picker {
fn subscription(&self) -> cosmic::iced::Subscription<Message> {
cosmic::iced::Subscription::batch([
ferese_theme_client::service::subscription().map(|snapshot| Message::ThemeChanged(Box::new(snapshot))),
ferese_theme_client::service::subscription()
.map(|snapshot| Message::ThemeChanged(Box::new(snapshot))),
cosmic::iced::event::listen_with(|event, _, id| match event {
cosmic::iced::Event::Window(cosmic::iced::window::Event::Opened { .. }) => {
Some(Message::WindowOpened(id))
@@ -185,7 +196,8 @@ impl cosmic::Application for Picker {
match message {
Message::ThemeChanged(snapshot) => {
self.palette = ferese_theme::Palette::from_resolved(&snapshot.presented).flat();
self.font = ferese_theme::font(Some(&snapshot.presented.tokens.typography.font_family));
self.font =
ferese_theme::font(Some(&snapshot.presented.tokens.typography.font_family));
self.background = cosmic::iced::Color {
a: ferese_theme::material_opacity(&snapshot.presented),
..self.palette.sidebar
@@ -232,7 +244,11 @@ impl cosmic::Application for Picker {
Message::Audio(audio) if self.prompt.recording => self.audio = audio,
Message::Region if self.prompt.recording => self.region = !self.region,
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 {
@@ -306,20 +322,26 @@ 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),
@@ -357,12 +379,16 @@ impl cosmic::Application for Picker {
content = content.push(
row![
self.text("Select a region").width(Length::Fill),
ferese_theme::controls::switch(self.region, self.palette).on_press(Message::Region),
ferese_theme::controls::switch(self.region, self.palette)
.on_press(Message::Region),
]
.align_y(Alignment::Center),
);
let mut audio = column([]).spacing(4);
for choices in [[Audio::Off, Audio::System], [Audio::Microphone, Audio::Both]] {
for choices in [
[Audio::Off, Audio::System],
[Audio::Microphone, Audio::Both],
] {
let mut choices_row = row([]).spacing(4);
for choice in choices {
choices_row = choices_row.push(
@@ -404,7 +430,11 @@ impl cosmic::Application for Picker {
let share = ferese_theme::controls::text_button(
if self.prompt.recording {
if self.region { "Select region" } else { "Record" }
if self.region {
"Select region"
} else {
"Record"
}
} else if self.prompt.window_capture {
"Capture"
} else {
@@ -422,9 +452,13 @@ impl cosmic::Application for Picker {
self.background.a,
))
.on_press(Message::Cancel);
let footer = row![cosmic::iced::widget::Space::new().width(Length::Fill), cancel, share]
.spacing(10)
.align_y(Alignment::Center);
let footer = row![
cosmic::iced::widget::Space::new().width(Length::Fill),
cancel,
share
]
.spacing(10)
.align_y(Alignment::Center);
// Reserve space for the actions even when the compositor shrinks the
// dialog. The source list and options share the remaining scroll area.
container(
@@ -447,10 +481,11 @@ 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>(
+5 -3
View File
@@ -2,9 +2,11 @@ mod recorder;
mod recording;
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);
}
+155 -52
View File
@@ -38,15 +38,32 @@ 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").await.map_err(|e| e.to_string())?;
let portal = Proxy::new(connection, DESKTOP, DESKTOP_PATH, "org.freedesktop.portal.ScreenCast")
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 returned: OwnedObjectPath = portal
.call(method, body)
.await
@@ -78,7 +95,14 @@ 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."
@@ -112,10 +136,13 @@ pub async fn run() -> Result<(), String> {
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
@@ -129,16 +156,32 @@ pub async fn run() -> Result<(), String> {
"org.ferese.ScreenRecorder",
)
.await?;
control.call::<_, _, ()>("UseBarControls", &(handle.clone(),)).await
control
.call::<_, _, ()>("UseBarControls", &(handle.clone(),))
.await
})
.await,
Ok(Ok(()))
);
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, native).await;
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,
native,
)
.await;
let _: Result<(), _> = session.call("Close", &()).await;
match result {
@@ -158,7 +201,10 @@ async fn record_session(
stop: &mut watch::Receiver<bool>,
native: 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,
@@ -187,7 +233,11 @@ 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?
@@ -216,21 +266,30 @@ async fn record_session(
)
.await
.map_err(|e| e.to_string())?;
let options: String =
tokio::time::timeout(Duration::from_secs(3), control.call("RecordingOptions", &(handle,)))
.await
.map_err(|_| "Recording options timed out")?
.map_err(|e| {
format!("Cannot read recording options; update the Ferese portal and recorder together: {e}")
})?;
let options: String = tokio::time::timeout(
Duration::from_secs(3),
control.call("RecordingOptions", &(handle,)),
)
.await
.map_err(|_| "Recording options timed out")?
.map_err(|e| {
format!(
"Cannot read recording options; update the Ferese portal and recorder together: {e}"
)
})?;
serde_json::from_str::<Recording>(&options).map_err(|e| e.to_string())?
} else {
Recording::default()
};
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
@@ -281,7 +340,8 @@ 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-{}-{}",
@@ -392,7 +452,8 @@ fn encode(
gst::Element::link_many([&source, &queue, &filter, &convert, &encoder, &mux, &sink])
.map_err(|e| e.to_string())?;
} else {
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 mut audio_inputs = Vec::new();
@@ -463,13 +524,17 @@ fn encode(
.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;
@@ -545,7 +610,8 @@ fn encode(
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)
}
@@ -560,7 +626,10 @@ 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"]
@@ -629,16 +698,31 @@ mod tests {
fn encoder_captures_isolated_system_and_microphone_audio() {
// The isolated fixture plays 440 Hz on the default output and 660 Hz
// on the default microphone. Both tones must survive combined capture.
let server = std::env::var("FERESE_TEST_RECORD_PULSE_SERVER").expect("isolated audio server required");
let server = std::env::var("FERESE_TEST_RECORD_PULSE_SERVER")
.expect("isolated audio server required");
assert_eq!(std::env::var("PULSE_SERVER").unwrap(), server);
let socket = Path::new(server.strip_prefix("unix:").expect("explicit Unix socket required"));
let socket = Path::new(
server
.strip_prefix("unix:")
.expect("explicit Unix socket required"),
);
assert!(socket.starts_with(std::env::temp_dir()));
for audio in [Audio::System, Audio::Microphone, Audio::Both, Audio::Both, Audio::Both] {
for audio in [
Audio::System,
Audio::Microphone,
Audio::Both,
Audio::Both,
Audio::Both,
] {
assert_cropped_audio_recording(false, 0, Some(audio));
}
}
fn assert_cropped_audio_recording(video_eos: bool, audio_count: usize, selected_audio: Option<Audio>) {
fn assert_cropped_audio_recording(
video_eos: bool,
audio_count: usize,
selected_audio: Option<Audio>,
) {
use crate::recording::Crop;
use std::sync::Mutex;
gst::init().unwrap();
@@ -706,9 +790,12 @@ mod tests {
.set_property("location", path.to_str().unwrap());
let geometry = Arc::new(Mutex::new(None));
let observed = geometry.clone();
decoder.by_name("video").unwrap().static_pad("sink").unwrap().add_probe(
gst::PadProbeType::BUFFER,
move |pad, _| {
decoder
.by_name("video")
.unwrap()
.static_pad("sink")
.unwrap()
.add_probe(gst::PadProbeType::BUFFER, move |pad, _| {
let caps = pad.current_caps().unwrap();
let structure = caps.structure(0).unwrap();
*observed.lock().unwrap() = Some((
@@ -716,13 +803,15 @@ mod tests {
structure.get::<i32>("height").unwrap(),
));
gst::PadProbeReturn::Ok
},
);
});
let samples = Arc::new(Mutex::new(Vec::new()));
let observed = samples.clone();
decoder.by_name("audio").unwrap().static_pad("sink").unwrap().add_probe(
gst::PadProbeType::BUFFER,
move |_, info| {
decoder
.by_name("audio")
.unwrap()
.static_pad("sink")
.unwrap()
.add_probe(gst::PadProbeType::BUFFER, move |_, info| {
let buffer = info.buffer().unwrap();
let data = buffer.map_readable().unwrap();
observed.lock().unwrap().extend(
@@ -731,8 +820,7 @@ mod tests {
.map(|sample| i16::from_le_bytes([sample[0], sample[1]])),
);
gst::PadProbeReturn::Ok
},
);
});
decoder.set_state(gst::State::Playing).unwrap();
let result = decoder.bus().unwrap().timed_pop_filtered(
gst::ClockTime::from_seconds(5),
@@ -740,7 +828,10 @@ mod tests {
);
decoder.set_state(gst::State::Null).unwrap();
assert!(
matches!(result.as_ref().map(|m| m.view()), Some(gst::MessageView::Eos(_))),
matches!(
result.as_ref().map(|m| m.view()),
Some(gst::MessageView::Eos(_))
),
"Saved recording did not decode: {result:?}"
);
assert_eq!(*geometry.lock().unwrap(), Some((201, 129)));
@@ -751,7 +842,9 @@ mod tests {
Some(Audio::Microphone) => vec![660.],
Some(Audio::Both) => vec![440., 660.],
Some(Audio::Off) => unreachable!("These tests require audio"),
None => (0..audio_count).map(|index| 440. + index as f64 * 220.).collect(),
None => (0..audio_count)
.map(|index| 440. + index as f64 * 220.)
.collect(),
};
for frequency in frequencies {
let amplitude = tone_amplitude(&samples, frequency);
@@ -770,7 +863,8 @@ mod tests {
let mut imaginary = 0.;
let mut weight = 0.;
for (index, sample) in samples.iter().enumerate() {
let window = 0.5 - 0.5 * (std::f64::consts::TAU * index as f64 / (samples.len() - 1) as f64).cos();
let window = 0.5
- 0.5 * (std::f64::consts::TAU * index as f64 / (samples.len() - 1) as f64).cos();
let angle = std::f64::consts::TAU * frequency * index as f64 / 48_000.;
let sample = *sample as f64 / i16::MAX as f64 * window;
real += sample * angle.cos();
@@ -785,13 +879,19 @@ mod tests {
let tone = |frequency: f64| {
(0..24_000)
.map(|index| {
(0.2 * i16::MAX as f64 * (std::f64::consts::TAU * frequency * index as f64 / 48_000.).sin()) as i16
(0.2 * i16::MAX as f64
* (std::f64::consts::TAU * frequency * index as f64 / 48_000.).sin())
as i16
})
.collect::<Vec<_>>()
};
let system = tone(440.);
let microphone = tone(660.);
let both = system.iter().zip(&microphone).map(|(a, b)| a + b).collect::<Vec<_>>();
let both = system
.iter()
.zip(&microphone)
.map(|(a, b)| a + b)
.collect::<Vec<_>>();
assert!(tone_amplitude(&both, 440.) > 0.05);
assert!(tone_amplitude(&both, 660.) > 0.05);
assert!(tone_amplitude(&microphone, 440.) < 0.01);
@@ -819,7 +919,10 @@ 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:?}"
);
}
+18 -4
View File
@@ -10,10 +10,17 @@ pub(crate) struct Region {
impl Region {
pub(crate) fn parse(value: &str) -> Result<Self, String> {
let (position, size) = value.trim().split_once(' ').ok_or("Invalid region selection")?;
let (position, size) = value
.trim()
.split_once(' ')
.ok_or("Invalid region selection")?;
let (x, y) = position.split_once(',').ok_or("Invalid region position")?;
let (width, height) = size.split_once('x').ok_or("Invalid region size")?;
let number = |value: &str| value.parse::<i32>().map_err(|_| "Invalid region coordinate".to_owned());
let number = |value: &str| {
value
.parse::<i32>()
.map_err(|_| "Invalid region coordinate".to_owned())
};
let region = Self {
x: number(x)?,
y: number(y)?,
@@ -58,7 +65,8 @@ impl Region {
let near = |value: i64, pixels: u32, logical: i32| {
(value * i64::from(pixels) + i64::from(logical) - 1) / i64::from(logical)
};
let far = |value: i64, pixels: u32, logical: i32| value * i64::from(pixels) / i64::from(logical);
let far =
|value: i64, pixels: u32, logical: i32| value * i64::from(pixels) / i64::from(logical);
let left = near(x, physical.0, output.width);
let top = near(y, physical.1, output.height);
let end_x = far(end_x, physical.0, output.width);
@@ -119,7 +127,13 @@ mod tests {
#[test]
fn region_selection_rejects_empty_overflow_and_malformed_bounds() {
for value in ["", "1,2 0x20", "1,2 -1x20", "2147483647,0 2x20", "1,2 20x30 extra"] {
for value in [
"",
"1,2 0x20",
"1,2 -1x20",
"2147483647,0 2x20",
"1,2 20x30 extra",
] {
assert!(Region::parse(value).is_err(), "{value}");
}
assert!(
+57 -20
View File
@@ -33,7 +33,9 @@ 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,7 +45,9 @@ 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:")
})
})
}
@@ -52,15 +56,14 @@ 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()
@@ -69,7 +72,8 @@ pub(crate) fn selection_unchanged(sources: &[Source], before: &Json, after: &Jso
}
}
_ => false,
})
},
)
}
impl Restore {
@@ -123,13 +127,29 @@ 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
@@ -178,7 +198,8 @@ 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())]);
@@ -198,14 +219,20 @@ 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)
@@ -215,9 +242,14 @@ 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}]);
@@ -229,7 +261,8 @@ 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());
}
@@ -257,7 +290,11 @@ 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]
+35 -9
View File
@@ -25,7 +25,8 @@ struct Appearance {
impl Appearance {
fn from_resolved(theme: &ResolvedTheme) -> Self {
let accent = ferese_config::theme::rgba(&theme.tokens.colors.accent).expect("validated accent");
let accent =
ferese_config::theme::rgba(&theme.tokens.colors.accent).expect("validated accent");
Self {
scheme: if theme.appearance == ThemeAppearance::Light {
2
@@ -41,7 +42,10 @@ 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(), self.contrast.into()),
("reduced-motion".into(), self.reduced_motion.into()),
])
@@ -57,7 +61,10 @@ impl Settings {
.ok()
.and_then(|mut connection| connection.get(ferese_config::families::builtins).ok())
.map(|snapshot| snapshot.theme);
Self(Arc::new(RwLock::new(initial_appearance(theme.as_ref(), load_config))))
Self(Arc::new(RwLock::new(initial_appearance(
theme.as_ref(),
load_config,
))))
}
async fn update_appearance(&self, next: Appearance, emitter: &SignalEmitter<'_>) {
@@ -160,7 +167,10 @@ fn matches_namespace(filters: &[String], namespace: &str) -> bool {
})
}
fn initial_appearance(theme: Option<&ResolvedTheme>, load: impl FnOnce() -> Option<Appearance>) -> Appearance {
fn initial_appearance(
theme: Option<&ResolvedTheme>,
load: impl FnOnce() -> Option<Appearance>,
) -> Appearance {
theme
.map(Appearance::from_resolved)
.or_else(load)
@@ -188,7 +198,9 @@ fn load_path(path: &Path) -> Result<Appearance, String> {
let source = match read_source(path) {
Ok(source) => source,
Err(_) if !path.try_exists().map_err(|error| error.to_string())? => {
return Ok(Appearance::from_resolved(&ferese_config::theme::default_theme()));
return Ok(Appearance::from_resolved(
&ferese_config::theme::default_theme(),
));
}
Err(error) => return Err(error),
};
@@ -216,8 +228,13 @@ mod tests {
std::fs::write(&path, source).unwrap();
let appearance = initial_appearance(None, || load_path(&path).ok());
let document = Document::parse(source).unwrap();
let expected =
ferese_config::theme::resolve(&document, root.path(), jiff::Timestamp::now(), read_source).unwrap();
let expected = ferese_config::theme::resolve(
&document,
root.path(),
jiff::Timestamp::now(),
read_source,
)
.unwrap();
assert_eq!(appearance, Appearance::from_resolved(&expected.theme));
assert_eq!(appearance.scheme, 2);
assert_eq!(appearance.reduced_motion, 1);
@@ -227,7 +244,11 @@ mod tests {
Appearance::from_resolved(&ferese_config::theme::default_theme()).accent
);
std::fs::write(&path, r#"theme { mode "dark"; }; animations { enabled #false; }"#).unwrap();
std::fs::write(
&path,
r#"theme { mode "dark"; }; animations { enabled #false; }"#,
)
.unwrap();
let next = load_path(&path).unwrap();
assert_eq!(next.scheme, 1);
assert_eq!(next.reduced_motion, 1);
@@ -268,7 +289,12 @@ 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 [
+62 -16
View File
@@ -89,7 +89,10 @@ 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;
}
}
@@ -132,14 +135,25 @@ 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
@@ -148,7 +162,11 @@ 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();
}
}
@@ -185,7 +203,13 @@ 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! {
@@ -198,7 +222,11 @@ 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"]
@@ -207,13 +235,16 @@ 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() {
@@ -222,9 +253,12 @@ 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;
}
}
}
@@ -249,11 +283,15 @@ 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;
@@ -467,7 +505,9 @@ 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(())
@@ -512,7 +552,10 @@ 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
@@ -552,7 +595,10 @@ 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) => {
+39 -14
View File
@@ -68,7 +68,10 @@ 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 {
@@ -99,10 +102,13 @@ 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> {
@@ -137,7 +143,11 @@ 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 || {
@@ -201,10 +211,14 @@ pub fn run(name: String, cursor: bool, generation: Option<u32>) -> Result<(), Bo
.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;
@@ -241,7 +255,8 @@ pub fn run(name: String, cursor: bool, generation: Option<u32>) -> Result<(), Bo
|| 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);
@@ -254,7 +269,10 @@ pub fn run(name: String, cursor: bool, generation: Option<u32>) -> Result<(), Bo
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)
));
@@ -272,7 +290,10 @@ pub fn run(name: String, cursor: bool, generation: Option<u32>) -> Result<(), Bo
)
));
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);
@@ -335,7 +356,9 @@ pub fn run(name: String, cursor: bool, generation: Option<u32>) -> Result<(), Bo
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) => {
@@ -382,7 +405,9 @@ pub fn run(name: String, cursor: bool, generation: Option<u32>) -> Result<(), Bo
} 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;