Files
feresaria/portal/src/workspace.rs
T
Samuel Onoja ab11269246 Add workspace screen sharing (#64)
Add workspace selection to the screen-sharing picker with pinned capture, privacy filtering and bounded capture resources. Refresh captured tab labels when title or app-ID metadata changes.
2026-10-25 13:59:34 +01:00

178 lines
6.2 KiB
Rust

//! Portal compatibility: a workspace is an explicitly chosen, restricted view
//! of an existing monitor. Offer it only when MONITOR was requested; never
//! advertise an invented WORKSPACE bit or VIRTUAL (which creates a new monitor).
use crate::backend::Options;
use crate::capture::Source;
use crate::stream::Ready;
use serde_json::Value as Json;
use zbus::zvariant::Value;
pub(crate) fn sources(workspaces: &Json, outputs: &Json) -> Result<Vec<Source>, String> {
let workspaces = workspaces.as_array().ok_or("Invalid workspace list")?;
let outputs = outputs.as_array().ok_or("Invalid output list")?;
Ok(workspaces
.iter()
.take(128)
.filter_map(|workspace| {
let id = workspace["id"].as_u64()?;
let owner = workspace["output"].as_u64()?;
let output = outputs
.iter()
.find(|output| output["id"].as_u64() == Some(owner) && output["enabled"] == true)?;
let integer = |key: &str| {
output[key]
.as_i64()
.and_then(|value| i32::try_from(value).ok())
};
let (width, height) = (integer("width")?, integer("height")?);
if width <= 0 || height <= 0 {
return None;
}
Some(Source {
name: format!("workspace:{id}"),
label: format!(
"Workspace {} · {}",
workspace["index"].as_u64()?,
output["name"].as_str()?
),
width,
height,
x: integer("x")?,
y: integer("y")?,
scale: 1,
})
})
.collect())
}
pub(crate) fn selection_unchanged(
selected: &[Source],
before: &Json,
old_outputs: &Json,
after: &Json,
new_outputs: &Json,
) -> bool {
selected.iter().filter_map(Source::workspace_id).all(|id| {
let owner = |workspaces: &Json| {
workspaces
.as_array()?
.iter()
.find(|workspace| workspace["id"].as_u64() == Some(id))?["output"]
.as_u64()
};
let Some(old_owner) = owner(before) else {
return false;
};
if owner(after) != Some(old_owner) {
return false;
}
let output = |outputs: &Json| {
outputs
.as_array()?
.iter()
.find(|output| output["id"].as_u64() == Some(old_owner))
.cloned()
};
let (Some(old), Some(new)) = (output(old_outputs), output(new_outputs)) else {
return false;
};
new["enabled"] == true
&& [
"identity",
"name",
"width",
"height",
"x",
"y",
"scale",
"transform",
"current_mode",
]
.into_iter()
.all(|key| old[key] == new[key])
})
}
pub(crate) fn metadata(source: &Source, ready: &Ready) -> Result<Options, String> {
if source.workspace_id().is_none()
|| ready.width == 0
|| ready.height == 0
|| ready.logical_size != Some((source.width as u32, source.height as u32))
{
return Err("Invalid workspace stream geometry".into());
}
Ok(Options::from([
("source_type".into(), 1u32.into()),
(
"size".into(),
Value::from((source.width, source.height))
.try_to_owned()
.map_err(|error| error.to_string())?,
),
]))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::capture::SourceKind;
use serde_json::json;
fn outputs() -> Json {
json!([{"id": 5, "name": "DP-1", "enabled": true, "width": 1920, "height": 1080,
"x": 0, "y": 0, "scale": 2.0, "identity": "drm-edid:test", "transform": "normal",
"current_mode": {"width": 3840, "height": 2160}}])
}
#[test]
fn workspace_selection_uses_identity_not_index_and_never_remembers_consent() {
let outputs = outputs();
let before = json!([{"id": 42, "output": 5, "index": 1}]);
let selected = sources(&before, &outputs).unwrap();
assert_eq!(selected.len(), 1);
assert_eq!(selected[0].kind(), SourceKind::Workspace(42));
assert_eq!(selected[0].label, "Workspace 1 · DP-1");
let renamed = json!([{"id": 42, "output": 5, "index": 3}]);
assert!(selection_unchanged(
&selected, &before, &outputs, &renamed, &outputs
));
let replaced = json!([{"id": 43, "output": 5, "index": 1}]);
assert!(!selection_unchanged(
&selected, &before, &outputs, &replaced, &outputs
));
let moved = json!([{"id": 42, "output": 6, "index": 1}]);
assert!(!selection_unchanged(
&selected, &before, &outputs, &moved, &outputs
));
let mut resized = outputs.clone();
resized[0]["current_mode"]["width"] = 2560.into();
assert!(!selection_unchanged(
&selected, &before, &outputs, &before, &resized
));
assert!(
crate::restore::Restore::create("org.test.App", false, &selected, &outputs, 2)
.is_none()
);
}
#[test]
fn workspace_metadata_keeps_logical_geometry_for_a_smaller_pixel_canvas() {
let selected = sources(&json!([{"id": 42, "output": 5, "index": 1}]), &outputs()).unwrap();
let mut ready = Ready {
node: 7,
width: 1280,
height: 720,
logical_size: Some((1920, 1080)),
};
let metadata = metadata(&selected[0], &ready).unwrap();
assert_eq!(u32::try_from(&metadata["source_type"]).unwrap(), 1);
assert!(!metadata.contains_key("position"));
ready.logical_size = None;
assert!(super::metadata(&selected[0], &ready).is_err());
for name in ["window:", "workspace:no", "workspace:18446744073709551616"] {
assert_eq!(SourceKind::parse(name), SourceKind::Invalid);
}
assert_eq!(SourceKind::parse("DP-1"), SourceKind::Monitor);
}
}