Format Rust sources at 100 columns
This commit is contained in:
1 parent
4661f595f8
commit
610478f8c7
336 files changed
+23497
-7892
No files matched your search
+174
-61
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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>(
|
||||
|
||||
@@ -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
@@ -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(µphone).map(|(a, b)| a + b).collect::<Vec<_>>();
|
||||
let both = system
|
||||
.iter()
|
||||
.zip(µphone)
|
||||
.map(|(a, b)| a + b)
|
||||
.collect::<Vec<_>>();
|
||||
assert!(tone_amplitude(&both, 440.) > 0.05);
|
||||
assert!(tone_amplitude(&both, 660.) > 0.05);
|
||||
assert!(tone_amplitude(µphone, 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:?}"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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;
|
||||
|
||||
Reference in new issue
Block a user