@forsion/tangu-computer-use 0.5.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +223 -0
- package/LICENSE +26 -0
- package/LICENSE.upstream +21 -0
- package/README.md +169 -0
- package/UPSTREAM.md +277 -0
- package/check.mjs +369 -0
- package/icon.png +0 -0
- package/install.sh +112 -0
- package/main.js +541 -0
- package/manifest.json +47 -0
- package/native/linux/bridge-rs/Cargo.lock +1204 -0
- package/native/linux/bridge-rs/Cargo.toml +18 -0
- package/native/linux/bridge-rs/src/atspi.rs +640 -0
- package/native/linux/bridge-rs/src/error.rs +65 -0
- package/native/linux/bridge-rs/src/lib.rs +11 -0
- package/native/linux/bridge-rs/src/main.rs +1074 -0
- package/native/linux/bridge-rs/src/protocol.rs +49 -0
- package/native/linux/bridge-rs/src/state.rs +293 -0
- package/native/linux/bridge-rs/src/wayland.rs +89 -0
- package/native/linux/bridge-rs/src/x11.rs +909 -0
- package/native/linux/bridge-rs/tests/protocol_tests.rs +73 -0
- package/native/macos/agent_cursor.swift +184 -0
- package/native/macos/agent_cursor_motion.swift +252 -0
- package/native/macos/agent_cursor_tests.swift +128 -0
- package/native/macos/agent_highlight.swift +229 -0
- package/native/macos/agent_highlight_tests.swift +99 -0
- package/native/macos/bridge.swift +3852 -0
- package/native/macos/foreground_activity.swift +57 -0
- package/native/macos/foreground_activity_tests.swift +31 -0
- package/native/macos/live_stream.swift +289 -0
- package/native/windows/bridge-rs/Cargo.lock +396 -0
- package/native/windows/bridge-rs/Cargo.toml +30 -0
- package/native/windows/bridge-rs/src/capture.rs +518 -0
- package/native/windows/bridge-rs/src/error.rs +81 -0
- package/native/windows/bridge-rs/src/input.rs +501 -0
- package/native/windows/bridge-rs/src/lib.rs +16 -0
- package/native/windows/bridge-rs/src/main.rs +1683 -0
- package/native/windows/bridge-rs/src/protocol.rs +53 -0
- package/native/windows/bridge-rs/src/refs.rs +237 -0
- package/native/windows/bridge-rs/src/state.rs +55 -0
- package/native/windows/bridge-rs/src/uia.rs +1252 -0
- package/native/windows/bridge-rs/src/window.rs +718 -0
- package/native/windows/bridge-rs/tests/protocol_tests.rs +252 -0
- package/native/windows/bridge-rs/tests/refs_tests.rs +86 -0
- package/native/windows/bridge-rs/tests/state_tests.rs +33 -0
- package/package.json +74 -0
- package/prebuilt/linux/arm64/linux-bridge +0 -0
- package/prebuilt/linux/x64/linux-bridge +0 -0
- package/prebuilt/macos/arm64/bridge +0 -0
- package/prebuilt/macos/arm64/tangu-computer-use.app.json +10 -0
- package/prebuilt/macos/arm64/tangu-computer-use.app.zip +0 -0
- package/prebuilt/macos/x64/bridge +0 -0
- package/prebuilt/macos/x64/tangu-computer-use.app.json +10 -0
- package/prebuilt/macos/x64/tangu-computer-use.app.zip +0 -0
- package/prebuilt/windows/windows-bridge.exe +0 -0
- package/scripts/blind-click.check.mjs +128 -0
- package/scripts/build-native.mjs +302 -0
- package/scripts/build.mjs +84 -0
- package/scripts/calc-fixture.mjs +78 -0
- package/scripts/cli-exit.check.mjs +49 -0
- package/scripts/helper-path.check.mjs +82 -0
- package/scripts/helper-refresh.check.mjs +62 -0
- package/scripts/helper-signal.check.mjs +59 -0
- package/scripts/highlight.check.mjs +32 -0
- package/scripts/keychain-free-install.check.mjs +140 -0
- package/scripts/live-view.check.mjs +124 -0
- package/scripts/macos-bundle.d.mts +1 -0
- package/scripts/macos-bundle.mjs +119 -0
- package/scripts/make-signing-cert.sh +57 -0
- package/scripts/mini-foreground.check.mjs +11 -0
- package/scripts/no-foreground.check.mjs +81 -0
- package/scripts/overlay-visible.check.mjs +221 -0
- package/scripts/package-macos-app.mjs +39 -0
- package/scripts/permissions.check.mjs +199 -0
- package/scripts/platform-contract.check.mjs +30 -0
- package/scripts/setup-helper.mjs +154 -0
- package/scripts/tangu-computer-use.entitlements +10 -0
- package/scripts/verify-macos-bundles.mjs +25 -0
- package/scripts/verify-package.mjs +74 -0
- package/skills/computer-use/SKILL.md +110 -0
- package/src/foregroundNote.ts +43 -0
- package/src/helperState.ts +31 -0
- package/src/index.ts +63 -0
- package/src/onboarding.ts +198 -0
- package/src/pi-compat.ts +49 -0
- package/src/settings.ts +11 -0
- package/src/setup.ts +97 -0
- package/src/tools.ts +289 -0
- package/src/vendor/actions.ts +130 -0
- package/src/vendor/bridge.ts +2405 -0
- package/src/vendor/cdp.ts +658 -0
- package/src/vendor/config.ts +113 -0
- package/src/vendor/contract.ts +104 -0
- package/src/vendor/note.ts +195 -0
- package/src/vendor/outline.ts +651 -0
- package/src/vendor/output.ts +134 -0
- package/src/vendor/permissions.ts +111 -0
- package/src/vendor/platform/architecture.ts +23 -0
- package/src/vendor/platform/coerce.ts +16 -0
- package/src/vendor/platform/index.ts +59 -0
- package/src/vendor/platform/linux/backend.ts +186 -0
- package/src/vendor/platform/linux/helper.ts +238 -0
- package/src/vendor/platform/macos/backend.ts +131 -0
- package/src/vendor/platform/macos/browser.ts +110 -0
- package/src/vendor/platform/macos/helper-path.d.mts +9 -0
- package/src/vendor/platform/macos/helper-path.mjs +34 -0
- package/src/vendor/platform/macos/helper.ts +291 -0
- package/src/vendor/platform/macos/permissions.ts +146 -0
- package/src/vendor/platform/types.ts +222 -0
- package/src/vendor/platform/windows/backend.ts +142 -0
- package/src/vendor/platform/windows/helper.ts +140 -0
- package/src/vendor/root-selection.ts +25 -0
- package/src/vendor/runtime.ts +129 -0
- package/src/vendor/state.ts +146 -0
- package/src/vendor/view.ts +147 -0
- package/tangu-plugins/computer-use/dist/foregroundNote.js +19 -0
- package/tangu-plugins/computer-use/dist/index.js +5672 -0
- package/tangu-plugins/computer-use/tangu-plugin.json +9 -0
- package/tsconfig.json +22 -0
- package/types/tangu-agent.d.ts +142 -0
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
use serde::{Deserialize, Serialize};
|
|
2
|
+
use serde_json::Value;
|
|
3
|
+
|
|
4
|
+
use crate::ProtocolError;
|
|
5
|
+
|
|
6
|
+
pub const PROTOCOL_VERSION: u32 = 4;
|
|
7
|
+
|
|
8
|
+
#[derive(Debug, Clone, Deserialize)]
|
|
9
|
+
pub struct Request {
|
|
10
|
+
#[serde(rename = "protocolVersion")]
|
|
11
|
+
pub protocol_version: u32,
|
|
12
|
+
pub id: String,
|
|
13
|
+
pub cmd: String,
|
|
14
|
+
pub args: Value,
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
#[derive(Debug, Clone, Serialize)]
|
|
18
|
+
pub struct Response {
|
|
19
|
+
#[serde(rename = "protocolVersion")]
|
|
20
|
+
pub protocol_version: u32,
|
|
21
|
+
pub id: String,
|
|
22
|
+
pub ok: bool,
|
|
23
|
+
#[serde(skip_serializing_if = "Option::is_none")]
|
|
24
|
+
pub result: Option<Value>,
|
|
25
|
+
#[serde(skip_serializing_if = "Option::is_none")]
|
|
26
|
+
pub error: Option<ProtocolError>,
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
impl Response {
|
|
30
|
+
pub fn ok(id: &str, result: Value) -> Self {
|
|
31
|
+
Self {
|
|
32
|
+
protocol_version: PROTOCOL_VERSION,
|
|
33
|
+
id: id.to_owned(),
|
|
34
|
+
ok: true,
|
|
35
|
+
result: Some(result),
|
|
36
|
+
error: None,
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
pub fn err(id: &str, error: ProtocolError) -> Self {
|
|
41
|
+
Self {
|
|
42
|
+
protocol_version: PROTOCOL_VERSION,
|
|
43
|
+
id: id.to_owned(),
|
|
44
|
+
ok: false,
|
|
45
|
+
result: None,
|
|
46
|
+
error: Some(error),
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
use std::collections::{HashMap, VecDeque};
|
|
2
|
+
use std::sync::atomic::{AtomicU64, Ordering};
|
|
3
|
+
|
|
4
|
+
use crate::atspi::{AccessibleRef, NodeSnapshot, RootSnapshot};
|
|
5
|
+
use crate::{ErrorCode, ProtocolError};
|
|
6
|
+
|
|
7
|
+
pub const MAX_LOOK_RECORDS: usize = 8;
|
|
8
|
+
|
|
9
|
+
#[derive(Debug, Clone)]
|
|
10
|
+
pub struct LookRecord {
|
|
11
|
+
pub root: RootSnapshot,
|
|
12
|
+
pub nodes: HashMap<String, NodeSnapshot>,
|
|
13
|
+
pub image_width: u16,
|
|
14
|
+
pub image_height: u16,
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
#[derive(Debug, Default)]
|
|
18
|
+
pub struct HelperState {
|
|
19
|
+
roots: HashMap<String, RootSnapshot>,
|
|
20
|
+
root_refs: HashMap<AccessibleRef, String>,
|
|
21
|
+
looks: HashMap<String, LookRecord>,
|
|
22
|
+
look_order: VecDeque<String>,
|
|
23
|
+
next_root: u64,
|
|
24
|
+
next_look: u64,
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
impl HelperState {
|
|
28
|
+
pub fn replace_roots(&mut self, roots: Vec<RootSnapshot>) -> Vec<(String, RootSnapshot)> {
|
|
29
|
+
let mut next = HashMap::new();
|
|
30
|
+
let mut result = Vec::with_capacity(roots.len());
|
|
31
|
+
for root in roots {
|
|
32
|
+
let reference = self
|
|
33
|
+
.root_refs
|
|
34
|
+
.get(&root.accessible)
|
|
35
|
+
.cloned()
|
|
36
|
+
.unwrap_or_else(|| {
|
|
37
|
+
self.next_root += 1;
|
|
38
|
+
format!("@w{}", self.next_root)
|
|
39
|
+
});
|
|
40
|
+
self.root_refs
|
|
41
|
+
.insert(root.accessible.clone(), reference.clone());
|
|
42
|
+
next.insert(reference.clone(), root.clone());
|
|
43
|
+
result.push((reference, root));
|
|
44
|
+
}
|
|
45
|
+
self.roots = next;
|
|
46
|
+
self.root_refs
|
|
47
|
+
.retain(|_, reference| self.roots.contains_key(reference));
|
|
48
|
+
result
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
pub fn root(&self, reference: &str) -> Option<RootSnapshot> {
|
|
52
|
+
self.roots.get(reference).cloned()
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
pub fn roots(&self) -> Vec<(String, RootSnapshot)> {
|
|
56
|
+
let mut roots = self
|
|
57
|
+
.roots
|
|
58
|
+
.iter()
|
|
59
|
+
.map(|(reference, root)| (reference.clone(), root.clone()))
|
|
60
|
+
.collect::<Vec<_>>();
|
|
61
|
+
roots.sort_by(|a, b| a.0.cmp(&b.0));
|
|
62
|
+
roots
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
pub fn insert_look(
|
|
66
|
+
&mut self,
|
|
67
|
+
root: RootSnapshot,
|
|
68
|
+
raw_nodes: Vec<NodeSnapshot>,
|
|
69
|
+
base: Option<&LookRecord>,
|
|
70
|
+
) -> (String, Vec<NodeSnapshot>) {
|
|
71
|
+
self.next_look += 1;
|
|
72
|
+
let look_id = format!("look_{}", self.next_look);
|
|
73
|
+
let mut nodes = base.map(|record| record.nodes.clone()).unwrap_or_default();
|
|
74
|
+
let existing_refs = base
|
|
75
|
+
.map(|record| {
|
|
76
|
+
record
|
|
77
|
+
.nodes
|
|
78
|
+
.values()
|
|
79
|
+
.map(|node| (node.accessible.clone(), node.reference.clone()))
|
|
80
|
+
.collect::<HashMap<_, _>>()
|
|
81
|
+
})
|
|
82
|
+
.unwrap_or_default();
|
|
83
|
+
let mut output = Vec::with_capacity(raw_nodes.len());
|
|
84
|
+
for (index, mut node) in raw_nodes.into_iter().enumerate() {
|
|
85
|
+
node.reference = existing_refs
|
|
86
|
+
.get(&node.accessible)
|
|
87
|
+
.cloned()
|
|
88
|
+
.unwrap_or_else(|| format!("linux:{look_id}:@e{}", index + 1));
|
|
89
|
+
nodes.insert(node.reference.clone(), node.clone());
|
|
90
|
+
output.push(node);
|
|
91
|
+
}
|
|
92
|
+
let (image_width, image_height) = base
|
|
93
|
+
.map(|record| (record.image_width, record.image_height))
|
|
94
|
+
.or_else(|| {
|
|
95
|
+
root.frame.as_ref().map(|frame| {
|
|
96
|
+
(
|
|
97
|
+
frame.width.max(1).min(i32::from(u16::MAX)) as u16,
|
|
98
|
+
frame.height.max(1).min(i32::from(u16::MAX)) as u16,
|
|
99
|
+
)
|
|
100
|
+
})
|
|
101
|
+
})
|
|
102
|
+
.unwrap_or((1, 1));
|
|
103
|
+
self.looks.insert(
|
|
104
|
+
look_id.clone(),
|
|
105
|
+
LookRecord {
|
|
106
|
+
root,
|
|
107
|
+
nodes,
|
|
108
|
+
image_width,
|
|
109
|
+
image_height,
|
|
110
|
+
},
|
|
111
|
+
);
|
|
112
|
+
self.look_order.push_back(look_id.clone());
|
|
113
|
+
while self.look_order.len() > MAX_LOOK_RECORDS {
|
|
114
|
+
if let Some(expired) = self.look_order.pop_front() {
|
|
115
|
+
self.looks.remove(&expired);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
(look_id, output)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
pub fn set_look_image_size(
|
|
122
|
+
&mut self,
|
|
123
|
+
look_id: &str,
|
|
124
|
+
width: u16,
|
|
125
|
+
height: u16,
|
|
126
|
+
) -> Result<(), ProtocolError> {
|
|
127
|
+
let record = self.looks.get_mut(look_id).ok_or_else(|| {
|
|
128
|
+
ProtocolError::new("Owning look is no longer available", ErrorCode::StaleLook)
|
|
129
|
+
})?;
|
|
130
|
+
record.image_width = width.max(1);
|
|
131
|
+
record.image_height = height.max(1);
|
|
132
|
+
Ok(())
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
pub fn scope(
|
|
136
|
+
&self,
|
|
137
|
+
reference: &str,
|
|
138
|
+
base_look_id: Option<&str>,
|
|
139
|
+
) -> Result<(LookRecord, NodeSnapshot), ProtocolError> {
|
|
140
|
+
if let Some(look_id) = base_look_id {
|
|
141
|
+
let record = self.look(look_id)?;
|
|
142
|
+
let node = record.nodes.get(reference).cloned().ok_or_else(|| {
|
|
143
|
+
ProtocolError::new("Scope ref is not owned by baseLookId", ErrorCode::StaleRef)
|
|
144
|
+
})?;
|
|
145
|
+
return Ok((record, node));
|
|
146
|
+
}
|
|
147
|
+
self.look_order
|
|
148
|
+
.iter()
|
|
149
|
+
.rev()
|
|
150
|
+
.filter_map(|look_id| self.looks.get(look_id))
|
|
151
|
+
.find_map(|record| {
|
|
152
|
+
record
|
|
153
|
+
.nodes
|
|
154
|
+
.get(reference)
|
|
155
|
+
.cloned()
|
|
156
|
+
.map(|node| (record.clone(), node))
|
|
157
|
+
})
|
|
158
|
+
.ok_or_else(|| ProtocolError::new("Scope ref is stale", ErrorCode::StaleRef))
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
pub fn look(&self, look_id: &str) -> Result<LookRecord, ProtocolError> {
|
|
162
|
+
self.looks.get(look_id).cloned().ok_or_else(|| {
|
|
163
|
+
ProtocolError::new("Owning look is no longer available", ErrorCode::StaleLook)
|
|
164
|
+
})
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
pub fn element(&self, look_id: &str, reference: &str) -> Result<NodeSnapshot, ProtocolError> {
|
|
168
|
+
self.look(look_id)?
|
|
169
|
+
.nodes
|
|
170
|
+
.get(reference)
|
|
171
|
+
.cloned()
|
|
172
|
+
.ok_or_else(|| ProtocolError::new("Element reference is stale", ErrorCode::StaleRef))
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
pub fn fresh_state_id() -> String {
|
|
177
|
+
static NEXT: AtomicU64 = AtomicU64::new(1);
|
|
178
|
+
format!("w-{}", NEXT.fetch_add(1, Ordering::Relaxed))
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
#[cfg(test)]
|
|
182
|
+
mod tests {
|
|
183
|
+
use super::*;
|
|
184
|
+
|
|
185
|
+
fn accessible(n: usize) -> AccessibleRef {
|
|
186
|
+
AccessibleRef {
|
|
187
|
+
destination: format!(":1.{n}"),
|
|
188
|
+
path: format!("/node/{n}"),
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
fn root(n: usize) -> RootSnapshot {
|
|
193
|
+
RootSnapshot {
|
|
194
|
+
accessible: accessible(n),
|
|
195
|
+
pid: n as u64,
|
|
196
|
+
name: format!("root {n}"),
|
|
197
|
+
app_name: format!("app {n}"),
|
|
198
|
+
role: "frame".into(),
|
|
199
|
+
frame: None,
|
|
200
|
+
x11_window: None,
|
|
201
|
+
is_focused: false,
|
|
202
|
+
is_minimized: false,
|
|
203
|
+
z_order: None,
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
#[test]
|
|
208
|
+
fn root_refs_are_stable_and_dead_roots_are_removed() {
|
|
209
|
+
let mut state = HelperState::default();
|
|
210
|
+
let first = state.replace_roots(vec![root(1), root(2)]);
|
|
211
|
+
let second = state.replace_roots(vec![root(2)]);
|
|
212
|
+
assert_eq!(first[1].0, second[0].0);
|
|
213
|
+
assert!(state.root(&first[0].0).is_none());
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
#[test]
|
|
217
|
+
fn look_history_is_bounded_and_refs_are_look_scoped() {
|
|
218
|
+
let mut state = HelperState::default();
|
|
219
|
+
let mut first = String::new();
|
|
220
|
+
for n in 0..=MAX_LOOK_RECORDS {
|
|
221
|
+
let node = NodeSnapshot::minimal(accessible(n));
|
|
222
|
+
let (look, nodes) = state.insert_look(root(n), vec![node], None);
|
|
223
|
+
if n == 0 {
|
|
224
|
+
first = look.clone();
|
|
225
|
+
}
|
|
226
|
+
assert!(nodes[0].reference.starts_with(&format!("linux:{look}:")));
|
|
227
|
+
}
|
|
228
|
+
assert_eq!(state.look(&first).unwrap_err().code, ErrorCode::StaleLook);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
#[test]
|
|
232
|
+
fn scoped_look_requires_scope_ref_owned_by_base_look() {
|
|
233
|
+
let mut state = HelperState::default();
|
|
234
|
+
let (first, first_nodes) =
|
|
235
|
+
state.insert_look(root(1), vec![NodeSnapshot::minimal(accessible(10))], None);
|
|
236
|
+
let (second, _) =
|
|
237
|
+
state.insert_look(root(1), vec![NodeSnapshot::minimal(accessible(20))], None);
|
|
238
|
+
assert_eq!(
|
|
239
|
+
state
|
|
240
|
+
.scope(&first_nodes[0].reference, Some(&second))
|
|
241
|
+
.unwrap_err()
|
|
242
|
+
.code,
|
|
243
|
+
ErrorCode::StaleRef
|
|
244
|
+
);
|
|
245
|
+
assert_eq!(
|
|
246
|
+
state
|
|
247
|
+
.scope(&first_nodes[0].reference, Some(&first))
|
|
248
|
+
.unwrap()
|
|
249
|
+
.1
|
|
250
|
+
.accessible,
|
|
251
|
+
accessible(10)
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
#[test]
|
|
256
|
+
fn scoped_look_carries_base_records_and_reuses_known_refs() {
|
|
257
|
+
let mut state = HelperState::default();
|
|
258
|
+
let mut parent = NodeSnapshot::minimal(accessible(10));
|
|
259
|
+
let mut child = NodeSnapshot::minimal(accessible(11));
|
|
260
|
+
child.parent = Some(parent.accessible.clone());
|
|
261
|
+
let (base_id, base_nodes) = state.insert_look(root(1), vec![parent.clone(), child], None);
|
|
262
|
+
let base = state.look(&base_id).unwrap();
|
|
263
|
+
parent.name = "updated".into();
|
|
264
|
+
let (scoped_id, scoped_nodes) = state.insert_look(root(1), vec![parent], Some(&base));
|
|
265
|
+
assert_eq!(scoped_nodes[0].reference, base_nodes[0].reference);
|
|
266
|
+
assert_eq!(
|
|
267
|
+
state
|
|
268
|
+
.element(&scoped_id, &base_nodes[1].reference)
|
|
269
|
+
.unwrap()
|
|
270
|
+
.accessible,
|
|
271
|
+
accessible(11)
|
|
272
|
+
);
|
|
273
|
+
assert_eq!(
|
|
274
|
+
state
|
|
275
|
+
.element(&scoped_id, &base_nodes[0].reference)
|
|
276
|
+
.unwrap()
|
|
277
|
+
.name,
|
|
278
|
+
"updated"
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
#[test]
|
|
283
|
+
fn scoped_look_preserves_base_image_size() {
|
|
284
|
+
let mut state = HelperState::default();
|
|
285
|
+
let node = NodeSnapshot::minimal(accessible(10));
|
|
286
|
+
let (base_id, _) = state.insert_look(root(1), vec![node.clone()], None);
|
|
287
|
+
state.set_look_image_size(&base_id, 1000, 500).unwrap();
|
|
288
|
+
let base = state.look(&base_id).unwrap();
|
|
289
|
+
let (scoped_id, _) = state.insert_look(root(1), vec![node], Some(&base));
|
|
290
|
+
let scoped = state.look(&scoped_id).unwrap();
|
|
291
|
+
assert_eq!((scoped.image_width, scoped.image_height), (1000, 500));
|
|
292
|
+
}
|
|
293
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
//! Read-only Wayland portal capability diagnostics.
|
|
2
|
+
//!
|
|
3
|
+
//! Runtime capture and input are intentionally unsupported. The probe only
|
|
4
|
+
//! reads D-Bus properties and never creates a portal request or session.
|
|
5
|
+
|
|
6
|
+
use std::fmt;
|
|
7
|
+
|
|
8
|
+
use zbus::{Connection, Proxy};
|
|
9
|
+
|
|
10
|
+
const DESTINATION: &str = "org.freedesktop.portal.Desktop";
|
|
11
|
+
const DESKTOP_PATH: &str = "/org/freedesktop/portal/desktop";
|
|
12
|
+
const REMOTE_DESKTOP: &str = "org.freedesktop.portal.RemoteDesktop";
|
|
13
|
+
const SCREEN_CAST: &str = "org.freedesktop.portal.ScreenCast";
|
|
14
|
+
const CURSOR_HIDDEN: u32 = 1;
|
|
15
|
+
|
|
16
|
+
#[derive(Debug)]
|
|
17
|
+
pub enum PortalError {
|
|
18
|
+
Unavailable(String),
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
impl fmt::Display for PortalError {
|
|
22
|
+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
23
|
+
match self {
|
|
24
|
+
Self::Unavailable(message) => write!(f, "desktop portal unavailable: {message}"),
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
impl std::error::Error for PortalError {}
|
|
30
|
+
|
|
31
|
+
fn unavailable(error: impl fmt::Display) -> PortalError {
|
|
32
|
+
PortalError::Unavailable(error.to_string())
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
36
|
+
pub struct PortalCapabilities {
|
|
37
|
+
pub remote_desktop_version: u32,
|
|
38
|
+
pub screen_cast_version: u32,
|
|
39
|
+
pub available_device_types: u32,
|
|
40
|
+
pub available_source_types: u32,
|
|
41
|
+
pub available_cursor_modes: u32,
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
#[derive(Clone)]
|
|
45
|
+
pub struct PortalClient {
|
|
46
|
+
connection: Connection,
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
impl PortalClient {
|
|
50
|
+
pub async fn connect() -> Result<Self, PortalError> {
|
|
51
|
+
Ok(Self {
|
|
52
|
+
connection: Connection::session().await.map_err(unavailable)?,
|
|
53
|
+
})
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
pub async fn probe(&self) -> Result<PortalCapabilities, PortalError> {
|
|
57
|
+
let remote = Proxy::new(&self.connection, DESTINATION, DESKTOP_PATH, REMOTE_DESKTOP)
|
|
58
|
+
.await
|
|
59
|
+
.map_err(unavailable)?;
|
|
60
|
+
let screen = Proxy::new(&self.connection, DESTINATION, DESKTOP_PATH, SCREEN_CAST)
|
|
61
|
+
.await
|
|
62
|
+
.map_err(unavailable)?;
|
|
63
|
+
let remote_desktop_version = remote.get_property("version").await.map_err(unavailable)?;
|
|
64
|
+
let available_device_types = remote
|
|
65
|
+
.get_property("AvailableDeviceTypes")
|
|
66
|
+
.await
|
|
67
|
+
.map_err(unavailable)?;
|
|
68
|
+
let screen_cast_version = screen.get_property("version").await.map_err(unavailable)?;
|
|
69
|
+
let available_source_types = screen
|
|
70
|
+
.get_property("AvailableSourceTypes")
|
|
71
|
+
.await
|
|
72
|
+
.map_err(unavailable)?;
|
|
73
|
+
let available_cursor_modes = if screen_cast_version >= 2 {
|
|
74
|
+
screen
|
|
75
|
+
.get_property("AvailableCursorModes")
|
|
76
|
+
.await
|
|
77
|
+
.map_err(unavailable)?
|
|
78
|
+
} else {
|
|
79
|
+
CURSOR_HIDDEN
|
|
80
|
+
};
|
|
81
|
+
Ok(PortalCapabilities {
|
|
82
|
+
remote_desktop_version,
|
|
83
|
+
screen_cast_version,
|
|
84
|
+
available_device_types,
|
|
85
|
+
available_source_types,
|
|
86
|
+
available_cursor_modes,
|
|
87
|
+
})
|
|
88
|
+
}
|
|
89
|
+
}
|