//! XDG Portal 权限冒烟测试示例。 //! //! 本示例演示完整的 Portal ScreenCast 授权流程,分四步: //! 创建 Screencast proxy,再创建 session,然后选择源(显示器/窗口), //! 最后 start() 触发系统授权对话框(用户点击"共享"后返回流信息)。 //! //! 运行:`cargo run --example test_portal`(参见 AGENTS.md "Useful manual commands")。 //! //! Rust 异步模型(与 Go 对比): //! - Go 用 goroutine + channel;async 函数本身**惰性**,需 runtime 驱动。 //! - 本示例刻意**不用** `#[tokio::main]` 宏,而是手动 `Runtime::new()` + `block_on` //! (与 src/backend_detect.rs 同款"手动 runtime"模式);这是因为 ashpd 内部缓存 //! zbus::Connection 到全局 OnceLock,若宏自动建的 runtime 被 drop, //! 缓存的 connection 会"僵尸化"导致后续 hang(详见 AGENTS.md)。 //! //! 对照 Go:`go func() { ... }()` ≈ `tokio::spawn(async { ... })`; //! 而 `block_on` 类似 Go 的 `select {}` 阻塞 main goroutine 等待退出。 // ashpd = XDG Portal 的 Rust 高层绑定,封装了 D-Bus ScreenCast 接口 use ashpd::desktop::screencast::{CursorMode, Screencast, SelectSourcesOptions, SourceType}; // PersistMode 控制"恢复令牌"持久化级别(DoNot / Persistent / ExplicitlyRevoked) use ashpd::desktop::PersistMode; // BitFlags = 位域集合类型(一个值可同时包含多个 SourceType,类比 Go 的 iota | 操作) use ashpd::enumflags2::BitFlags; // 同步 main → 手动创建 tokio Runtime → block_on 阻塞驱动 async 块。 // 这种"同步外壳 + 异步内核"的写法等价于 `#[tokio::main] async fn main()`, // 但保留了显式控制 runtime 生命周期的灵活性(参见文件头说明)。 fn main() { // 手动创建 tokio runtime(含 reactor + executor + 时间驱动); // unwrap() 仅示例用;生产代码应返回 Result 并 `?` 传播(但 fn main 不返回 Result) let rt = tokio::runtime::Runtime::new().unwrap(); // block_on 阻塞当前线程直到传入的 future 完成;这是同步↔异步边界 rt.block_on(async { // async {} 块构造一个匿名 future,仅在 block_on poll 时才执行(惰性,与 goroutine 不同) eprintln!("1. Creating Screencast proxy..."); // Screencast::new() 内部通过 D-Bus 连接 org.freedesktop.portal.ScreenCast; // .await 让出执行权直到 future 就绪(Go 没有这个语法,需 channel/锁模拟) let proxy = match Screencast::new().await { Ok(p) => { eprintln!(" OK"); p } Err(e) => { eprintln!(" FAIL: {e}"); // early-return 仅退出 async 块(不是退出 main),block_on 返回 () return; } }; eprintln!("2. Creating session..."); // create_session 建立一个 ScreenCast 会话句柄; // Default::default() 用类型默认参数(ashpd 推断为 SessionOptions,所有字段取 Default) let session = match proxy.create_session(Default::default()).await { Ok(s) => { eprintln!(" OK"); s } Err(e) => { eprintln!(" FAIL: {e}"); return; } }; eprintln!("3. Selecting sources..."); // BitFlags 表达"可选多显示器/窗口/工作区"集合; // 这里 `into()` 将单个 Monitor 转为位域(Go 类似 flag = 1 << iota) let sources: BitFlags = SourceType::Monitor.into(); // Builder 链式:每次 set_X 返回 &mut Self(类似 Go functional-options 模式但更显式) // - cursor_mode Embedded:光标嵌入帧内 // - sources: 仅 Monitor(去掉窗口,简化授权 UX) // - multiple=false:单选(一次只授权一个显示器) // - persist_mode DoNot:不申请恢复令牌(避免持久权限残留) // 整个 builder 链构造一个 future,末尾的 .await 等待 D-Bus 返回 let result = proxy .select_sources( &session, SelectSourcesOptions::default() .set_cursor_mode(CursorMode::Embedded) .set_sources(sources) .set_multiple(false) .set_persist_mode(PersistMode::DoNot), ) .await; match result { Ok(_) => eprintln!(" OK"), Err(e) => { eprintln!(" FAIL: {e}"); return; } } eprintln!("4. Starting (should show dialog)..."); // start() 触发系统授权对话框(D-Bus 调用阻塞直到用户响应); // 第二参数 parent_window = None(无父窗口,常见于 CLI 程序) let response = match proxy.start(&session, None, Default::default()).await { Ok(r) => { eprintln!(" OK"); r } Err(e) => { eprintln!(" FAIL: {e}"); return; } }; // Portal D-Bus 响应是双层结构:外层是 Request::response(Ok/Err), // 内层才是 ScreenCast 流信息(streams() 返回 PipeWire 节点 + dmabuf 信息列表) match response.response() { Ok(r) => eprintln!(" Got {} stream(s)", r.streams().len()), Err(e) => eprintln!(" Response error: {e}"), } }); }