lamco-portal-0.4.0/.cargo_vcs_info.json0000644000000001611046102023000134470ustar { "git": { "sha1": "3780013af936d5d1481c4316b1499094978b1dd2" }, "path_in_vcs": "crates/lamco-portal" }lamco-portal-0.4.0/.gitignore000064400000000000000000000002601046102023000142050ustar 00000000000000# Rust build artifacts /target/ # Cargo.lock for libraries (comment out for binaries) # Cargo.lock # IDE files .vscode/ .idea/ *.swp *.swo *~ # OS files .DS_Store Thumbs.db lamco-portal-0.4.0/CHANGELOG.md000064400000000000000000000137411046102023000140360ustar 00000000000000# Changelog All notable changes to lamco-portal will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [0.3.4] - 2026-03-15 ### Changed - **BREAKING**: Upgrade ashpd from 0.12.3 to 0.13.7 - All proxy types drop lifetime parameter (e.g., `RemoteDesktop<'a>` -> `RemoteDesktop`) - Session types simplified: `Session<'static, RemoteDesktop<'static>>` -> `Session` - `select_devices()` and `select_sources()` now use builder-style options - All `notify_*()` methods take typed options structs - Clipboard methods generic over `IsClipboardSession` trait - `set_selection()` uses `SetSelectionOptions` builder - `request()` takes `RequestClipboardOptions` ### Added - Portal version detection: `RemoteDesktopManager::version()`, `ClipboardManager::version()` - Clipboard grant verification via `is_clipboard_enabled()` on start response - Session::Closed signal now properly delivered (ashpd PR #359 fix) ### Fixed - Session::Closed signal was silently discarded in ashpd 0.12 (upstream bug) ## [0.3.3] - 2026-03-15 ### Changed - Bump to Rust edition 2024, minimum supported Rust version 1.85 ## [0.3.2] - 2026-03-04 ### Changed - Upgrade to zbus 5 and ashpd 0.12.3 - Code cleanup and reduced boilerplate ## [0.3.1] - 2026-01-29 ### Fixed - Fixed clipboard request timing - call immediately after CreateSession instead of after SelectDevices/SelectSources - Portal spec requires clipboard.request() when session state is INIT - Resolves "Invalid state" errors from portal daemon on GNOME Flatpak - Added session cleanup in all error paths to prevent orphaned D-Bus session objects - Portal sessions now properly closed when device/source selection fails - Prevents stale session state that causes subsequent clipboard requests to fail ### Changed - Improved code quality and comment clarity - Reduced verbose logging in clipboard operations ## [0.3.0] - 2025-12-31 ### Added - **Restore token support** for session persistence (Portal v4+ required) - `create_session()` now returns `(PortalSessionHandle, Option)` with restore token - `start_session()` now returns restore token from portal response - Enables unattended operation - no permission dialogs on subsequent runs - Token should be stored securely and passed via `PortalConfig::restore_token` ### Changed - **BREAKING:** `PortalManager::create_session()` return type changed from `Result` to `Result<(PortalSessionHandle, Option)>` - **BREAKING:** `RemoteDesktopManager::start_session()` return type changed from `Result<(RawFd, Vec)>` to `Result<(RawFd, Vec, Option)>` - Default `persist_mode` changed from `DoNot` to `ExplicitlyRevoked` for better session persistence UX ### Notes - Restore tokens are only available on Portal v4+ (GNOME 45+, KDE Plasma 6+) - Tokens should be stored securely (e.g., system keyring, encrypted file) - If portal doesn't support tokens, returns `None` (behavior unchanged for Portal v3) - See examples and documentation for proper token storage and restoration patterns ## [0.2.2] - 2025-12-24 ### Fixed - Fixed non-blocking FD handling for clipboard read (EAGAIN error) - Portal's `selection_read()` returns a non-blocking pipe FD - Now sets FD to blocking mode using fcntl before reading - Uses `spawn_blocking` for proper blocking I/O on tokio threadpool - Fixes Linux→Windows image clipboard copy which was failing with "Resource temporarily unavailable (os error 11)" ## [0.2.1] - 2025-12-23 ### Fixed - **CRITICAL:** Fixed FD ownership issue causing PipeWire stream to close prematurely - Changed return type from `OwnedFd` to `RawFd` with `std::mem::forget()` to prevent auto-close - Fixes black screen bug where PipeWire stream stuck in Connecting state ### Added - Enhanced debug logging for Portal session startup ## [0.2.0] - 2025-12-21 ### Added - `dbus-clipboard` feature for D-Bus clipboard integration (GNOME fallback when Portal clipboard unavailable) ## [0.1.2] - 2025-12-17 ### Fixed - Added `#![cfg_attr(docsrs, feature(doc_cfg))]` for proper docs.rs conditional documentation - Converted to workspace package inheritance (edition, rust-version, license, homepage, repository, authors) - Converted to workspace lint inheritance ### Added - Added LICENSE-MIT and LICENSE-APACHE files to crate directory - Added CHANGELOG.md ## [0.1.1] - 2025-12-15 ### Added - Initial release on crates.io - **`PortalManager`** - Main entry point for portal interactions - Session creation with ScreenCast + RemoteDesktop combined sessions - Clipboard integration support - Graceful resource cleanup - **`PortalConfig`** - Configuration builder - Cursor mode selection (hidden, embedded, metadata) - Source type selection (monitors, windows) - Persist mode for remembering permissions - Multi-monitor support - **`PortalSessionHandle`** - Session state management - PipeWire file descriptor access - Stream information (node ID, position, size) - ashpd session reference for input injection - **`ScreenCastManager`** - Screen capture coordination - **`RemoteDesktopManager`** - Input injection (keyboard, mouse, scroll) - **`ClipboardManager`** - Portal-based clipboard access - **`PortalClipboardSink`** - Integration with lamco-clipboard-core (optional) - **`DbusClipboardBridge`** - D-Bus clipboard for GNOME fallback (optional) - Typed error handling with `PortalError` - Re-exports of ashpd types for convenience ### Platform Support - Linux only (Wayland required) - Tested on GNOME, KDE Plasma, Sway [Unreleased]: https://github.com/lamco-admin/lamco-wayland/compare/lamco-portal-v0.2.0...HEAD [0.2.0]: https://github.com/lamco-admin/lamco-wayland/compare/lamco-portal-v0.1.2...lamco-portal-v0.2.0 [0.1.2]: https://github.com/lamco-admin/lamco-wayland/compare/lamco-portal-v0.1.1...lamco-portal-v0.1.2 [0.1.1]: https://github.com/lamco-admin/lamco-wayland/releases/tag/lamco-portal-v0.1.1 lamco-portal-0.4.0/Cargo.lock0000644000001062751046102023000114370ustar # This file is automatically @generated by Cargo. # It is not intended for manual editing. version = 4 [[package]] name = "anyhow" version = "1.0.100" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" [[package]] name = "ashpd" version = "0.13.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "313dc617cf7b7e5d58021f999756898e60bdddd64eab2bc2f67909659e3ce5f9" dependencies = [ "enumflags2", "futures-util", "getrandom 0.4.2", "serde", "serde_repr", "tokio", "zbus", ] [[package]] name = "async-broadcast" version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532" dependencies = [ "event-listener", "event-listener-strategy", "futures-core", "pin-project-lite", ] [[package]] name = "async-channel" version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" dependencies = [ "concurrent-queue", "event-listener-strategy", "futures-core", "pin-project-lite", ] [[package]] name = "async-executor" version = "1.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "497c00e0fd83a72a79a39fcbd8e3e2f055d6f6c7e025f3b3d91f4f8e76527fb8" dependencies = [ "async-task", "concurrent-queue", "fastrand", "futures-lite", "pin-project-lite", "slab", ] [[package]] name = "async-io" version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc" dependencies = [ "autocfg", "cfg-if", "concurrent-queue", "futures-io", "futures-lite", "parking", "polling", "rustix", "slab", "windows-sys 0.61.2", ] [[package]] name = "async-lock" version = "3.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5fd03604047cee9b6ce9de9f70c6cd540a0520c813cbd49bae61f33ab80ed1dc" dependencies = [ "event-listener", "event-listener-strategy", "pin-project-lite", ] [[package]] name = "async-process" version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75" dependencies = [ "async-channel", "async-io", "async-lock", "async-signal", "async-task", "blocking", "cfg-if", "event-listener", "futures-lite", "rustix", ] [[package]] name = "async-recursion" version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "async-signal" version = "0.2.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "43c070bbf59cd3570b6b2dd54cd772527c7c3620fce8be898406dd3ed6adc64c" dependencies = [ "async-io", "async-lock", "atomic-waker", "cfg-if", "futures-core", "futures-io", "rustix", "signal-hook-registry", "slab", "windows-sys 0.61.2", ] [[package]] name = "async-stream" version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476" dependencies = [ "async-stream-impl", "futures-core", "pin-project-lite", ] [[package]] name = "async-stream-impl" version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "async-task" version = "4.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" [[package]] name = "async-trait" version = "0.1.89" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "atomic-waker" version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" [[package]] name = "autocfg" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "bitflags" version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" [[package]] name = "blocking" version = "1.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21" dependencies = [ "async-channel", "async-task", "futures-io", "futures-lite", "piper", ] [[package]] name = "bumpalo" version = "3.19.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" [[package]] name = "bytes" version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" [[package]] name = "cfg-if" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "concurrent-queue" version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" dependencies = [ "crossbeam-utils", ] [[package]] name = "crossbeam-utils" version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" [[package]] name = "endi" version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099" [[package]] name = "enumflags2" version = "0.7.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef" dependencies = [ "enumflags2_derive", "serde", ] [[package]] name = "enumflags2_derive" version = "0.7.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "equivalent" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "errno" version = "0.3.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", "windows-sys 0.61.2", ] [[package]] name = "event-listener" version = "5.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" dependencies = [ "concurrent-queue", "parking", "pin-project-lite", ] [[package]] name = "event-listener-strategy" version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" dependencies = [ "event-listener", "pin-project-lite", ] [[package]] name = "fastrand" version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" [[package]] name = "foldhash" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" [[package]] name = "futures-core" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" [[package]] name = "futures-io" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" [[package]] name = "futures-lite" version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" dependencies = [ "fastrand", "futures-core", "futures-io", "parking", "pin-project-lite", ] [[package]] name = "futures-macro" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "futures-task" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" [[package]] name = "futures-util" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" dependencies = [ "futures-core", "futures-macro", "futures-task", "pin-project-lite", "pin-utils", "slab", ] [[package]] name = "getrandom" version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", "libc", "r-efi 5.3.0", "wasip2", ] [[package]] name = "getrandom" version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" dependencies = [ "cfg-if", "libc", "r-efi 6.0.0", "wasip2", "wasip3", ] [[package]] name = "hashbrown" version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" dependencies = [ "foldhash", ] [[package]] name = "hashbrown" version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" [[package]] name = "heck" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "hermit-abi" version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" [[package]] name = "hex" version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" [[package]] name = "id-arena" version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" [[package]] name = "indexmap" version = "2.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2" dependencies = [ "equivalent", "hashbrown 0.16.1", "serde", "serde_core", ] [[package]] name = "itoa" version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" [[package]] name = "js-sys" version = "0.3.83" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "464a3709c7f55f1f721e5389aa6ea4e3bc6aba669353300af094b29ffbdde1d8" dependencies = [ "once_cell", "wasm-bindgen", ] [[package]] name = "lamco-portal" version = "0.4.0" dependencies = [ "ashpd", "enumflags2", "futures-util", "libc", "thiserror", "tokio", "tokio-test", "tracing", "tracing-subscriber", "uuid", "zbus", ] [[package]] name = "lazy_static" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "leb128fmt" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" [[package]] name = "libc" version = "0.2.178" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091" [[package]] name = "linux-raw-sys" version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" [[package]] name = "log" version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" [[package]] name = "memchr" version = "2.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" [[package]] name = "memoffset" version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" dependencies = [ "autocfg", ] [[package]] name = "mio" version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" dependencies = [ "libc", "wasi", "windows-sys 0.61.2", ] [[package]] name = "nu-ansi-term" version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ "windows-sys 0.61.2", ] [[package]] name = "once_cell" version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" [[package]] name = "ordered-stream" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50" dependencies = [ "futures-core", "pin-project-lite", ] [[package]] name = "parking" version = "2.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" [[package]] name = "pin-project-lite" version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" [[package]] name = "pin-utils" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "piper" version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96c8c490f422ef9a4efd2cb5b42b76c8613d7e7dfc1caf667b8a3350a5acc066" dependencies = [ "atomic-waker", "fastrand", "futures-io", ] [[package]] name = "polling" version = "3.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" dependencies = [ "cfg-if", "concurrent-queue", "hermit-abi", "pin-project-lite", "rustix", "windows-sys 0.61.2", ] [[package]] name = "prettyplease" version = "0.2.37" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" dependencies = [ "proc-macro2", "syn", ] [[package]] name = "proc-macro-crate" version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" dependencies = [ "toml_edit", ] [[package]] name = "proc-macro2" version = "1.0.103" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" dependencies = [ "unicode-ident", ] [[package]] name = "quote" version = "1.0.42" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" dependencies = [ "proc-macro2", ] [[package]] name = "r-efi" version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" [[package]] name = "r-efi" version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" [[package]] name = "rustix" version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" dependencies = [ "bitflags", "errno", "libc", "linux-raw-sys", "windows-sys 0.61.2", ] [[package]] name = "rustversion" version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "semver" version = "1.0.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" [[package]] name = "serde" version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" dependencies = [ "serde_core", "serde_derive", ] [[package]] name = "serde_core" version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "serde_json" version = "1.0.149" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" dependencies = [ "itoa", "memchr", "serde", "serde_core", "zmij", ] [[package]] name = "serde_repr" version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "sharded-slab" version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" dependencies = [ "lazy_static", ] [[package]] name = "signal-hook-registry" version = "1.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7664a098b8e616bdfcc2dc0e9ac44eb231eedf41db4e9fe95d8d32ec728dedad" dependencies = [ "libc", ] [[package]] name = "slab" version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" [[package]] name = "smallvec" version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" [[package]] name = "socket2" version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881" dependencies = [ "libc", "windows-sys 0.60.2", ] [[package]] name = "syn" version = "2.0.111" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] [[package]] name = "tempfile" version = "3.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" dependencies = [ "fastrand", "getrandom 0.3.4", "once_cell", "rustix", "windows-sys 0.61.2", ] [[package]] name = "thiserror" version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "thread_local" version = "1.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" dependencies = [ "cfg-if", ] [[package]] name = "tokio" version = "1.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408" dependencies = [ "bytes", "libc", "mio", "pin-project-lite", "signal-hook-registry", "socket2", "tokio-macros", "tracing", "windows-sys 0.61.2", ] [[package]] name = "tokio-macros" version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "tokio-stream" version = "0.1.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" dependencies = [ "futures-core", "pin-project-lite", "tokio", ] [[package]] name = "tokio-test" version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2468baabc3311435b55dd935f702f42cd1b8abb7e754fb7dfb16bd36aa88f9f7" dependencies = [ "async-stream", "bytes", "futures-core", "tokio", "tokio-stream", ] [[package]] name = "toml_datetime" version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2cdb639ebbc97961c51720f858597f7f24c4fc295327923af55b74c3c724533" dependencies = [ "serde_core", ] [[package]] name = "toml_edit" version = "0.23.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d7cbc3b4b49633d57a0509303158ca50de80ae32c265093b24c414705807832" dependencies = [ "indexmap", "toml_datetime", "toml_parser", "winnow", ] [[package]] name = "toml_parser" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0cbe268d35bdb4bb5a56a2de88d0ad0eb70af5384a99d648cd4b3d04039800e" dependencies = [ "winnow", ] [[package]] name = "tracing" version = "0.1.43" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2d15d90a0b5c19378952d479dc858407149d7bb45a14de0142f6c534b16fc647" dependencies = [ "pin-project-lite", "tracing-attributes", "tracing-core", ] [[package]] name = "tracing-attributes" version = "0.1.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", "syn", ] [[package]] name = "tracing-core" version = "0.1.35" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a04e24fab5c89c6a36eb8558c9656f30d81de51dfa4d3b45f26b21d61fa0a6c" dependencies = [ "once_cell", "valuable", ] [[package]] name = "tracing-log" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" dependencies = [ "log", "once_cell", "tracing-core", ] [[package]] name = "tracing-subscriber" version = "0.3.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" dependencies = [ "nu-ansi-term", "sharded-slab", "smallvec", "thread_local", "tracing-core", "tracing-log", ] [[package]] name = "uds_windows" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89daebc3e6fd160ac4aa9fc8b3bf71e1f74fbf92367ae71fb83a037e8bf164b9" dependencies = [ "memoffset", "tempfile", "winapi", ] [[package]] name = "unicode-ident" version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" [[package]] name = "unicode-xid" version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" [[package]] name = "uuid" version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2e054861b4bd027cd373e18e8d8d8e6548085000e41290d95ce0c373a654b4a" dependencies = [ "getrandom 0.3.4", "js-sys", "serde_core", "wasm-bindgen", ] [[package]] name = "valuable" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" [[package]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasip2" version = "1.0.1+wasi-0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" dependencies = [ "wit-bindgen 0.46.0", ] [[package]] name = "wasip3" version = "0.4.0+wasi-0.3.0-rc-2026-01-06" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" dependencies = [ "wit-bindgen 0.51.0", ] [[package]] name = "wasm-bindgen" version = "0.2.106" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0d759f433fa64a2d763d1340820e46e111a7a5ab75f993d1852d70b03dbb80fd" dependencies = [ "cfg-if", "once_cell", "rustversion", "wasm-bindgen-macro", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-macro" version = "0.2.106" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48cb0d2638f8baedbc542ed444afc0644a29166f1595371af4fecf8ce1e7eeb3" dependencies = [ "quote", "wasm-bindgen-macro-support", ] [[package]] name = "wasm-bindgen-macro-support" version = "0.2.106" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cefb59d5cd5f92d9dcf80e4683949f15ca4b511f4ac0a6e14d4e1ac60c6ecd40" dependencies = [ "bumpalo", "proc-macro2", "quote", "syn", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" version = "0.2.106" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cbc538057e648b67f72a982e708d485b2efa771e1ac05fec311f9f63e5800db4" dependencies = [ "unicode-ident", ] [[package]] name = "wasm-encoder" version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" dependencies = [ "leb128fmt", "wasmparser", ] [[package]] name = "wasm-metadata" version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" dependencies = [ "anyhow", "indexmap", "wasm-encoder", "wasmparser", ] [[package]] name = "wasmparser" version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" dependencies = [ "bitflags", "hashbrown 0.15.5", "indexmap", "semver", ] [[package]] name = "winapi" version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" dependencies = [ "winapi-i686-pc-windows-gnu", "winapi-x86_64-pc-windows-gnu", ] [[package]] name = "winapi-i686-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windows-link" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" [[package]] name = "windows-sys" version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" dependencies = [ "windows-targets", ] [[package]] name = "windows-sys" version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" dependencies = [ "windows-link", ] [[package]] name = "windows-targets" version = "0.53.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" dependencies = [ "windows-link", "windows_aarch64_gnullvm", "windows_aarch64_msvc", "windows_i686_gnu", "windows_i686_gnullvm", "windows_i686_msvc", "windows_x86_64_gnu", "windows_x86_64_gnullvm", "windows_x86_64_msvc", ] [[package]] name = "windows_aarch64_gnullvm" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" [[package]] name = "windows_aarch64_msvc" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" [[package]] name = "windows_i686_gnu" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" [[package]] name = "windows_i686_gnullvm" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" [[package]] name = "windows_i686_msvc" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" [[package]] name = "windows_x86_64_gnu" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" [[package]] name = "windows_x86_64_gnullvm" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" [[package]] name = "windows_x86_64_msvc" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" [[package]] name = "winnow" version = "0.7.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" dependencies = [ "memchr", ] [[package]] name = "wit-bindgen" version = "0.46.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" [[package]] name = "wit-bindgen" version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" dependencies = [ "wit-bindgen-rust-macro", ] [[package]] name = "wit-bindgen-core" version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" dependencies = [ "anyhow", "heck", "wit-parser", ] [[package]] name = "wit-bindgen-rust" version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" dependencies = [ "anyhow", "heck", "indexmap", "prettyplease", "syn", "wasm-metadata", "wit-bindgen-core", "wit-component", ] [[package]] name = "wit-bindgen-rust-macro" version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" dependencies = [ "anyhow", "prettyplease", "proc-macro2", "quote", "syn", "wit-bindgen-core", "wit-bindgen-rust", ] [[package]] name = "wit-component" version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" dependencies = [ "anyhow", "bitflags", "indexmap", "log", "serde", "serde_derive", "serde_json", "wasm-encoder", "wasm-metadata", "wasmparser", "wit-parser", ] [[package]] name = "wit-parser" version = "0.244.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" dependencies = [ "anyhow", "id-arena", "indexmap", "log", "semver", "serde", "serde_derive", "serde_json", "unicode-xid", "wasmparser", ] [[package]] name = "zbus" version = "5.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ca82f95dbd3943a40a53cfded6c2d0a2ca26192011846a1810c4256ef92c60bc" dependencies = [ "async-broadcast", "async-executor", "async-io", "async-lock", "async-process", "async-recursion", "async-task", "async-trait", "blocking", "enumflags2", "event-listener", "futures-core", "futures-lite", "hex", "libc", "ordered-stream", "rustix", "serde", "serde_repr", "tokio", "tracing", "uds_windows", "uuid", "windows-sys 0.61.2", "winnow", "zbus_macros", "zbus_names", "zvariant", ] [[package]] name = "zbus_macros" version = "5.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "897e79616e84aac4b2c46e9132a4f63b93105d54fe8c0e8f6bffc21fa8d49222" dependencies = [ "proc-macro-crate", "proc-macro2", "quote", "syn", "zbus_names", "zvariant", "zvariant_utils", ] [[package]] name = "zbus_names" version = "4.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ffd8af6d5b78619bab301ff3c560a5bd22426150253db278f164d6cf3b72c50f" dependencies = [ "serde", "winnow", "zvariant", ] [[package]] name = "zmij" version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" [[package]] name = "zvariant" version = "5.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5708299b21903bbe348e94729f22c49c55d04720a004aa350f1f9c122fd2540b" dependencies = [ "endi", "enumflags2", "serde", "winnow", "zvariant_derive", "zvariant_utils", ] [[package]] name = "zvariant_derive" version = "5.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b59b012ebe9c46656f9cc08d8da8b4c726510aef12559da3e5f1bf72780752c" dependencies = [ "proc-macro-crate", "proc-macro2", "quote", "syn", "zvariant_utils", ] [[package]] name = "zvariant_utils" version = "3.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f75c23a64ef8f40f13a6989991e643554d9bef1d682a281160cf0c1bc389c5e9" dependencies = [ "proc-macro2", "quote", "serde", "syn", "winnow", ] lamco-portal-0.4.0/Cargo.toml0000644000000060101046102023000114440ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies. # # If you are reading this file be aware that the original Cargo.toml # will likely look very different (and much more reasonable). # See Cargo.toml.orig for the original contents. [package] edition = "2024" rust-version = "1.85" name = "lamco-portal" version = "0.4.0" authors = ["Greg Lamberson "] build = false exclude = [ "/.github/", "/tests/", "*.orig", "*.original", "*.bak", ] autolib = false autobins = false autoexamples = false autotests = false autobenches = false description = "XDG Desktop Portal integration for Wayland screen capture and input control, by Lamco Development" homepage = "https://lamco.ai" documentation = "https://docs.rs/lamco-portal" readme = "README.md" keywords = [ "wayland", "portal", "screencast", "pipewire", "screen-capture", ] categories = [ "api-bindings", "os::unix-apis", "asynchronous", ] license = "MIT OR Apache-2.0" repository = "https://github.com/lamco-admin/lamco-wayland" resolver = "2" [package.metadata.docs.rs] all-features = true targets = ["x86_64-unknown-linux-gnu"] rustdoc-args = [ "--cfg", "docsrs", ] [badges.maintenance] status = "actively-developed" [features] dbus-clipboard = [] default = [] [lib] name = "lamco_portal" path = "src/lib.rs" [[example]] name = "basic" path = "examples/basic.rs" [[example]] name = "config" path = "examples/config.rs" [[example]] name = "input" path = "examples/input.rs" [dependencies.ashpd] version = "0.13.7" features = [ "remote_desktop", "screencast", "clipboard", ] [dependencies.enumflags2] version = "0.7" [dependencies.futures-util] version = "0.3" [dependencies.libc] version = "0.2" [dependencies.thiserror] version = "1.0" [dependencies.tokio] version = "1.35" features = [ "sync", "macros", "rt-multi-thread", "signal", ] [dependencies.tracing] version = "0.1" [dependencies.uuid] version = "1.0" features = ["v4"] [dependencies.zbus] version = "5" [dev-dependencies.tokio-test] version = "0.4" [dev-dependencies.tracing-subscriber] version = "0.3" [lints.clippy] as_conversions = "warn" cast_lossless = "warn" cast_possible_truncation = "warn" cast_possible_wrap = "warn" large_futures = "warn" missing_errors_doc = "allow" missing_panics_doc = "allow" missing_safety_doc = "warn" module_name_repetitions = "allow" multiple_unsafe_ops_per_block = "warn" must_use_candidate = "allow" or_fun_call = "warn" panic = "warn" rc_buffer = "warn" similar_names = "warn" undocumented_unsafe_blocks = "warn" unwrap_used = "warn" wildcard_imports = "warn" [lints.rust] elided_lifetimes_in_paths = "warn" invalid_reference_casting = "warn" single_use_lifetimes = "warn" unreachable_pub = "warn" unsafe_code = "warn" unsafe_op_in_unsafe_fn = "warn" unused_unsafe = "warn" lamco-portal-0.4.0/Cargo.toml.orig000064400000000000000000000073201046102023000151100ustar 00000000000000[package] # ============================================================================ # PACKAGE IDENTITY # ============================================================================ name = "lamco-portal" version = "0.4.0" # ============================================================================ # PACKAGE CONFIGURATION (inherited from workspace) # ============================================================================ edition.workspace = true rust-version.workspace = true license.workspace = true homepage.workspace = true repository.workspace = true authors.workspace = true # ============================================================================ # CRATE-SPECIFIC METADATA # ============================================================================ description = "XDG Desktop Portal integration for Wayland screen capture and input control, by Lamco Development" documentation = "https://docs.rs/lamco-portal" readme = "README.md" # Keywords for crates.io search (max 5, max 20 chars each) keywords = ["wayland", "portal", "screencast", "pipewire", "screen-capture"] # Categories for crates.io browsing (max 5) # See: https://crates.io/category_slugs categories = [ "api-bindings", # Wrapper around D-Bus portal interfaces "os::unix-apis", # Linux/Unix specific functionality "asynchronous", # Async/await API design ] # ============================================================================ # PUBLISHING CONFIGURATION # ============================================================================ # Files to exclude from the published crate (reduces download size) exclude = [ "/.github/", "/tests/", "*.orig", "*.original", "*.bak", ] # ============================================================================ # BADGES (maintenance status) # ============================================================================ [badges] maintenance = { status = "actively-developed" } # ============================================================================ # DOCS.RS CONFIGURATION # ============================================================================ [package.metadata.docs.rs] # Build docs with all features enabled for comprehensive documentation all-features = true # docs.rs is Linux-only, which matches our target targets = ["x86_64-unknown-linux-gnu"] # Enable docsrs cfg for conditional documentation rustdoc-args = ["--cfg", "docsrs"] # ============================================================================ # LINTS (inherited from workspace) # ============================================================================ [lints] workspace = true # ============================================================================ # FEATURES # ============================================================================ [features] default = [] # Enable D-Bus clipboard bridge for GNOME fallback (SelectionOwnerChanged workaround) dbus-clipboard = [] # ============================================================================ # DEPENDENCIES # ============================================================================ [dependencies] # Portal and D-Bus integration ashpd = { version = "0.13.7", features = ["remote_desktop", "screencast", "clipboard"] } zbus = "5" # Async runtime tokio = { version = "1.35", features = ["sync", "macros", "rt-multi-thread", "signal"] } futures-util = "0.3" # Logging tracing = "0.1" # Error handling thiserror = "1.0" # For bitflags (transitive from ashpd but used explicitly) enumflags2 = "0.7" # For setting FD flags (blocking mode for Portal clipboard pipes) libc = "0.2" # For session tracking IDs in logging uuid = { version = "1.0", features = ["v4"] } [dev-dependencies] tokio-test = "0.4" tracing-subscriber = "0.3" lamco-portal-0.4.0/LICENSE-APACHE000064400000000000000000000013361046102023000141460ustar 00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ Copyright 2025 Lamco Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. lamco-portal-0.4.0/LICENSE-MIT000064400000000000000000000020461046102023000136550ustar 00000000000000MIT License Copyright (c) 2025 Lamco Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. lamco-portal-0.4.0/README.md000064400000000000000000000163121046102023000135010ustar 00000000000000# lamco-portal [![Crates.io](https://img.shields.io/crates/v/lamco-portal.svg)](https://crates.io/crates/lamco-portal) [![Documentation](https://docs.rs/lamco-portal/badge.svg)](https://docs.rs/lamco-portal) [![License](https://img.shields.io/crates/l/lamco-portal.svg)](https://github.com/lamco-admin/lamco-wayland/tree/main/lamco-portal) High-level Rust interface to XDG Desktop Portal for Wayland screen capture and input control. ## Features - **Screen Capture**: Capture monitor or window content through PipeWire streams - **Input Injection**: Send keyboard and mouse events to the desktop - **Clipboard Integration**: Portal-based clipboard for remote desktop scenarios - **Multi-Monitor**: Handle multiple displays simultaneously - **Flexible Configuration**: Builder pattern and struct literals - **Typed Errors**: Match and handle specific error conditions ## Requirements - Wayland compositor (GNOME, KDE Plasma, Sway, etc.) - `xdg-desktop-portal` installed and running - Portal backend for your compositor: - GNOME: `xdg-desktop-portal-gnome` - KDE: `xdg-desktop-portal-kde` - wlroots: `xdg-desktop-portal-wlr` - PipeWire for video streaming ## Quick Start Add to your `Cargo.toml`: ```toml [dependencies] lamco-portal = "0.1" tokio = { version = "1", features = ["full"] } ``` Basic usage: ```rust use lamco_portal::{PortalManager, PortalConfig}; #[tokio::main] async fn main() -> Result<(), Box> { // Create portal manager with default config let manager = PortalManager::with_default().await?; // Create session (triggers permission dialog) let session = manager.create_session("my-session".to_string(), None).await?; // Access PipeWire FD for video capture let fd = session.pipewire_fd(); let streams = session.streams(); println!("Capturing {} streams on PipeWire FD {}", streams.len(), fd); // Inject mouse movement manager.remote_desktop() .notify_pointer_motion_absolute( session.ashpd_session(), 0, // stream index 100.0, // x position 200.0, // y position ) .await?; Ok(()) } ``` ## Feature Flags ```toml [dependencies] # Default - basic portal functionality lamco-portal = "0.1" # With D-Bus clipboard bridge for GNOME (SelectionOwnerChanged workaround) lamco-portal = { version = "0.1", features = ["dbus-clipboard"] } # With ClipboardSink trait for lamco-clipboard-core integration lamco-portal = { version = "0.1", features = ["clipboard-sink"] } ``` | Feature | Description | |---------|-------------| | `dbus-clipboard` | D-Bus clipboard bridge for GNOME - works around missing SelectionOwnerChanged signals | | `clipboard-sink` | ClipboardSink trait implementation for lamco-clipboard-core integration | ## Configuration Customize Portal behavior: ```rust use lamco_portal::{PortalManager, PortalConfig}; use ashpd::desktop::screencast::CursorMode; use ashpd::desktop::PersistMode; let config = PortalConfig::builder() .cursor_mode(CursorMode::Embedded) // Embed cursor in video .persist_mode(PersistMode::Application) // Remember permission .build(); let manager = PortalManager::new(config).await?; ``` ## Error Handling Handle specific error conditions: ```rust use lamco_portal::PortalError; match manager.create_session("session-1".to_string(), None).await { Ok(session) => { println!("Session created successfully"); } Err(PortalError::PermissionDenied) => { eprintln!("User denied permission"); } Err(PortalError::PortalNotAvailable) => { eprintln!("Portal not installed - install xdg-desktop-portal"); } Err(e) => { eprintln!("Other error: {}", e); } } ``` ## Examples See the `examples/` directory for complete examples: - `basic.rs` - Simple screen capture setup - `input.rs` - Input injection (keyboard/mouse) - `clipboard.rs` - Clipboard integration Run examples with: ```bash cargo run --example basic ``` ## Platform Support | Platform | Status | Backend Package | |----------|--------|----------------| | GNOME (Wayland) | ✅ Supported | xdg-desktop-portal-gnome | | KDE Plasma (Wayland) | ✅ Supported | xdg-desktop-portal-kde | | Sway / wlroots | ✅ Supported | xdg-desktop-portal-wlr | | X11 | ❌ Not Supported | Wayland only | ## Security This library triggers system permission dialogs. Users must explicitly grant: - **Screen capture access** (which monitors/windows to share) - **Input injection access** (keyboard/mouse control) - **Clipboard access** (if using clipboard features) Permissions can be remembered per-application using `PersistMode::Application` to skip the dialog on subsequent runs. ## Architecture ``` ┌─────────────────┐ │ Your Application│ └────────┬────────┘ │ v ┌─────────────────┐ │ lamco-portal │ │ (this crate) │ └────────┬────────┘ │ v ┌─────────────────┐ │ ashpd │ ← Low-level Portal bindings └────────┬────────┘ │ v ┌─────────────────┐ │ xdg-desktop- │ │ portal │ ← System Portal service └────────┬────────┘ │ ┌────┴────┬─────────────┐ v v v ┌────────┐┌────────┐ ┌──────────┐ │PipeWire││D-Bus │ │Compositor│ └────────┘└────────┘ └──────────┘ ``` ## Troubleshooting ### "Portal not available" error **Solution**: Install xdg-desktop-portal and the appropriate backend: ```bash # Arch Linux sudo pacman -S xdg-desktop-portal xdg-desktop-portal-gnome # Ubuntu/Debian sudo apt install xdg-desktop-portal xdg-desktop-portal-gnome # Fedora sudo dnf install xdg-desktop-portal xdg-desktop-portal-gnome ``` ### "User denied permission" error **Solution**: This is expected behavior when the user clicks "Cancel" in the permission dialog. Handle it gracefully in your application. ### No streams available **Causes**: - User denied screen access - No monitors/windows available to share - Portal backend not running **Solution**: Check that your portal backend is running and try again. ## About Developed by [Lamco Development](https://lamco.ai). Part of the lamco-wayland ecosystem for building Wayland-native applications in Rust. ## License Licensed under either of: - Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0) - MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) at your option. ## Contributing Contributions are welcome! Please feel free to submit a Pull Request. ## Related Projects - [ashpd](https://github.com/bilelmoussaoui/ashpd) - Low-level Portal bindings - [pipewire-rs](https://gitlab.freedesktop.org/pipewire/pipewire-rs) - PipeWire bindings - [xdg-desktop-portal](https://github.com/flatpak/xdg-desktop-portal) - Portal specification lamco-portal-0.4.0/examples/basic.rs000064400000000000000000000041571046102023000154730ustar 00000000000000//! Basic screen capture example //! //! This example demonstrates: //! - Creating a Portal manager with default configuration //! - Creating a session (triggers permission dialog) //! - Accessing PipeWire FD and stream information //! //! Run with: cargo run --example basic use lamco_portal::PortalManager; #[tokio::main] async fn main() -> Result<(), Box> { // Initialize tracing for logging tracing_subscriber::fmt::init(); println!("=== lamco-portal Basic Example ===\n"); // Create portal manager with default configuration println!("Creating Portal manager..."); let manager = PortalManager::with_default().await?; println!("✓ Portal manager created\n"); // Create a session (this will trigger the system permission dialog) println!("Creating session (permission dialog will appear)..."); let (session, restore_token) = manager.create_session("basic-example".to_string(), None).await?; println!("✓ Session created: {}\n", session.session_id()); if let Some(token) = restore_token { println!(" Restore token received ({} chars)", token.len()); println!(" (Store this token to avoid dialogs on next run)\n"); } // Display PipeWire information println!("PipeWire Details:"); println!(" File Descriptor: {}", session.pipewire_fd()); println!(" Available Streams: {}\n", session.streams().len()); // Display stream information println!("Stream Information:"); for (i, stream) in session.streams().iter().enumerate() { println!(" Stream {}: ", i); println!(" Node ID: {}", stream.node_id); println!(" Size: {}x{}", stream.size.0, stream.size.1); println!(" Position: ({}, {})", stream.position.0, stream.position.1); println!(" Type: {:?}", stream.source_type); } println!("\nSession active. Press Ctrl+C to exit."); println!("(In a real application, you would pass the PipeWire FD to your video capture code)"); // Keep session alive tokio::signal::ctrl_c().await?; println!("\nShutting down..."); manager.cleanup().await?; Ok(()) } lamco-portal-0.4.0/examples/config.rs000064400000000000000000000075041046102023000156560ustar 00000000000000//! Configuration example //! //! This example demonstrates: //! - Customizing Portal configuration with builder pattern //! - Setting cursor mode, persistence, and source types //! - Using struct literal configuration //! //! Run with: cargo run --example config use ashpd::desktop::PersistMode; use ashpd::desktop::remote_desktop::DeviceType; use ashpd::desktop::screencast::{CursorMode, SourceType}; use lamco_portal::PortalManager; #[tokio::main] async fn main() -> Result<(), Box> { use lamco_portal::PortalConfig; // Initialize tracing for logging tracing_subscriber::fmt::init(); println!("=== lamco-portal Configuration Example ===\n"); // Example 1: Using defaults println!("1. Creating manager with default configuration:"); let manager1 = PortalManager::with_default().await?; println!(" ✓ Default config: cursor=Metadata, persist=DoNot, multi-monitor=true\n"); manager1.cleanup().await?; // Example 2: Using builder pattern println!("2. Creating manager with builder pattern:"); let config2 = PortalConfig::builder() .cursor_mode(CursorMode::Embedded) // Embed cursor in video stream .persist_mode(PersistMode::Application) // Remember permission .source_type(SourceType::Monitor.into()) // Only monitors (no windows) .allow_multiple(false) // Single monitor only .build(); let manager2 = PortalManager::new(config2).await?; println!(" ✓ Custom config: cursor=Embedded, persist=Application, single-monitor\n"); manager2.cleanup().await?; // Example 3: Using struct literal println!("3. Creating manager with struct literal:"); let config3 = PortalConfig { cursor_mode: CursorMode::Hidden, // No cursor in stream persist_mode: PersistMode::ExplicitlyRevoked, // Remember until revoked source_type: SourceType::Window.into(), // Only windows (no monitors) devices: DeviceType::Keyboard.into(), // Keyboard only, no pointer allow_multiple: true, restore_token: None, }; let manager3 = PortalManager::new(config3).await?; println!(" ✓ Custom config: cursor=Hidden, windows-only, keyboard-only\n"); manager3.cleanup().await?; // Example 4: Monitor-only capture with embedded cursor println!("4. Creating session with embedded cursor (monitor-only):"); let config4 = PortalConfig::builder() .cursor_mode(CursorMode::Embedded) .source_type(SourceType::Monitor.into()) .build(); let manager4 = PortalManager::new(config4).await?; println!(" Creating session (permission dialog will appear)..."); let (session, _restore_token) = manager4.create_session("config-example".to_string(), None).await?; println!(" ✓ Session created with {} streams", session.streams().len()); println!("\n Configuration notes:"); println!(" - Cursor is embedded in the video stream"); println!(" - Only monitors are available for selection"); println!(" - Permission will be requested each time (DoNot persist)"); println!("\nPress Ctrl+C to exit."); tokio::signal::ctrl_c().await?; manager4.cleanup().await?; println!("\n=== Configuration Options ==="); println!("CursorMode:"); println!(" - Hidden: No cursor in stream"); println!(" - Embedded: Cursor baked into video"); println!(" - Metadata: Cursor position as metadata (recommended for RDP)"); println!("\nPersistMode:"); println!(" - DoNot: Request permission every time"); println!(" - Application: Remember for this app"); println!(" - ExplicitlyRevoked: Remember until user revokes"); println!("\nSourceType:"); println!(" - Monitor: Physical monitors"); println!(" - Window: Individual windows"); println!(" - Monitor | Window: Both (default)"); Ok(()) } lamco-portal-0.4.0/examples/input.rs000064400000000000000000000077301046102023000155510ustar 00000000000000//! Input injection example //! //! This example demonstrates: //! - Creating a Portal session with input capabilities //! - Injecting mouse movements and clicks //! - Injecting keyboard events //! //! Run with: cargo run --example input //! //! SAFETY: This example will move your mouse and simulate clicks! //! Make sure you're ready before running it. use std::time::Duration; use lamco_portal::PortalManager; use tokio::time::sleep; #[tokio::main] async fn main() -> Result<(), Box> { // Initialize tracing for logging tracing_subscriber::fmt::init(); println!("=== lamco-portal Input Injection Example ===\n"); println!("⚠️ WARNING: This example will move your mouse and simulate clicks!"); println!("⚠️ You have 3 seconds to cancel (Ctrl+C)...\n"); sleep(Duration::from_secs(3)).await; // Create portal manager println!("Creating Portal manager..."); let manager = PortalManager::with_default().await?; println!("✓ Portal manager created\n"); // Create session (triggers permission dialog) println!("Creating session (permission dialog will appear)..."); println!("Make sure to grant BOTH screen capture AND input control permissions!\n"); let (session, _restore_token) = manager.create_session("input-example".to_string(), None).await?; println!("✓ Session created\n"); // Get the first stream for pointer positioning let stream_index = 0; if session.streams().is_empty() { eprintln!("No streams available!"); return Ok(()); } println!("Demonstrating input injection...\n"); // Example 1: Move mouse to center of screen println!("1. Moving mouse to screen center..."); manager .remote_desktop() .notify_pointer_motion_absolute( session.ashpd_session(), stream_index, 0.5, // 50% x (center) 0.5, // 50% y (center) ) .await?; sleep(Duration::from_secs(1)).await; // Example 2: Move to top-left corner println!("2. Moving mouse to top-left corner..."); manager .remote_desktop() .notify_pointer_motion_absolute( session.ashpd_session(), stream_index, 0.1, // 10% x 0.1, // 10% y ) .await?; sleep(Duration::from_secs(1)).await; // Example 3: Move to bottom-right corner println!("3. Moving mouse to bottom-right corner..."); manager .remote_desktop() .notify_pointer_motion_absolute( session.ashpd_session(), stream_index, 0.9, // 90% x 0.9, // 90% y ) .await?; sleep(Duration::from_secs(1)).await; // Example 4: Simulate a left click (press and release) println!("4. Simulating left mouse click..."); // Button 1 = left mouse button manager .remote_desktop() .notify_pointer_button(session.ashpd_session(), 1, true) // Press .await?; sleep(Duration::from_millis(100)).await; manager .remote_desktop() .notify_pointer_button(session.ashpd_session(), 1, false) // Release .await?; sleep(Duration::from_secs(1)).await; // Example 5: Keyboard input (simulate pressing 'A' key) println!("5. Simulating 'A' key press..."); // Keycode 30 = 'A' key (Linux keycode) manager .remote_desktop() .notify_keyboard_keycode(session.ashpd_session(), 30, true) // Press .await?; sleep(Duration::from_millis(100)).await; manager .remote_desktop() .notify_keyboard_keycode(session.ashpd_session(), 30, false) // Release .await?; println!("\n✓ Input injection demonstration complete!"); println!("\nNOTE: In a real application, you would:"); println!(" - Get mouse coordinates from your remote desktop protocol"); println!(" - Convert protocol keycodes to Linux keycodes"); println!(" - Handle button states properly"); manager.cleanup().await?; Ok(()) } lamco-portal-0.4.0/src/clipboard.rs000064400000000000000000000273161046102023000153240ustar 00000000000000//! Portal Clipboard Integration //! //! Implements delayed rendering clipboard using Portal Clipboard D-Bus API. //! This replaces wl-clipboard-rs with proper Portal integration that supports //! format announcement without data transfer (delayed rendering model). //! //! Architecture: //! - SetSelection() announces available formats to Wayland //! - SelectionTransfer signal notifies when data is requested //! - SelectionWrite() provides data via file descriptor //! - SelectionOwnerChanged signal monitors local clipboard changes //! - SelectionRead() reads local clipboard data use std::sync::Arc; use ashpd::desktop::Session; use ashpd::desktop::clipboard::{Clipboard, RequestClipboardOptions, SetSelectionOptions}; use ashpd::desktop::remote_desktop::RemoteDesktop; use tokio::sync::mpsc; use tracing::{debug, error, info, trace, warn}; /// Selection transfer event from Portal #[derive(Debug, Clone)] pub struct SelectionTransferEvent { pub mime_type: String, pub serial: u32, } /// Portal Clipboard Manager /// /// Integrates RDP clipboard with Wayland via Portal Clipboard API. /// Supports delayed rendering where formats are announced without data, /// and data is only transferred when actually requested. pub struct ClipboardManager { /// Portal Clipboard interface (Arc-wrapped for sharing across tasks) clipboard: Arc, } impl ClipboardManager { /// Create new Portal Clipboard manager pub async fn new() -> crate::Result { info!("Initializing Portal Clipboard manager"); trace!("Creating ashpd Clipboard proxy (uses global D-Bus connection)"); let clipboard = Clipboard::new().await.map_err(|e| { warn!(error = %e, "Failed to create Portal Clipboard proxy"); crate::PortalError::clipboard(format!("Failed to create Portal Clipboard: {}", e)) })?; let version = clipboard.version(); info!("Portal Clipboard version: {}", version); trace!("ashpd Clipboard proxy created successfully"); info!("Portal Clipboard manager created (will be enabled when session is ready)"); let manager = Self { clipboard: Arc::new(clipboard), }; Ok(manager) } /// Get the clipboard portal version pub fn version(&self) -> u32 { self.clipboard.version() } /// Start listening for SelectionTransfer events (delayed rendering requests) pub async fn start_selection_transfer_listener( &self, event_tx: mpsc::UnboundedSender, ) -> crate::Result<()> { let clipboard = Arc::clone(&self.clipboard); tokio::spawn(async move { use futures_util::stream::StreamExt; let stream_result = clipboard.receive_selection_transfer::().await; match stream_result { Ok(stream) => { let mut stream = Box::pin(stream); while let Some((_, mime_type, serial)) = stream.next().await { debug!("SelectionTransfer signal: mime={}, serial={}", mime_type, serial); let event = SelectionTransferEvent { mime_type, serial }; if event_tx.send(event).is_err() { info!("SelectionTransfer listener stopping (receiver dropped)"); break; } } info!("SelectionTransfer listener task ended"); } Err(e) => { info!("Failed to receive SelectionTransfer stream: {:#}", e); } } }); info!("SelectionTransfer listener started - ready for delayed rendering"); Ok(()) } /// Start listening for SelectionOwnerChanged events (local clipboard changes) pub async fn start_owner_changed_listener( &self, event_tx: mpsc::UnboundedSender>, ) -> crate::Result<()> { use futures_util::stream::StreamExt; let clipboard = Arc::clone(&self.clipboard); tokio::spawn(async move { info!("SelectionOwnerChanged listener task starting - attempting to receive stream"); let stream_result = clipboard.receive_selection_owner_changed::().await; match stream_result { Ok(stream) => { info!("SelectionOwnerChanged stream created successfully - waiting for signals"); let mut stream = Box::pin(stream); let mut event_count = 0; while let Some((_, change)) = stream.next().await { event_count += 1; info!("SelectionOwnerChanged event #{}: received from Portal", event_count); let is_owner = change.session_is_owner().unwrap_or(false); let mime_types = change.mime_types(); info!(" session_is_owner: {}, mime_types: {:?}", is_owner, mime_types); if is_owner { debug!("Ignoring SelectionOwnerChanged - we are the owner"); continue; } info!( "Local clipboard changed - new owner has {} formats: {:?}", mime_types.len(), mime_types ); if event_tx.send(mime_types.to_vec()).is_err() { info!("SelectionOwnerChanged listener stopping (receiver dropped)"); break; } } warn!("SelectionOwnerChanged listener task ended after {} events", event_count); } Err(e) => { error!("Failed to receive SelectionOwnerChanged stream: {:#}", e); error!("This means Linux->Windows clipboard will NOT work"); error!("Portal backend may not support this signal, or permission denied"); } } }); info!("SelectionOwnerChanged listener started - monitoring local clipboard"); Ok(()) } /// Request clipboard access for session /// /// Must be called BEFORE the session is started (session state must be INIT). pub async fn enable_for_session(&self, session: &Session) -> crate::Result<()> { info!("Requesting clipboard access for session"); trace!("Calling clipboard.request() - requires session state INIT"); match self .clipboard .request(session, RequestClipboardOptions::default()) .await { Ok(()) => { info!("Portal Clipboard enabled for session"); Ok(()) } Err(e) => { warn!( error = %e, error_debug = ?e, "clipboard.request() failed" ); trace!("Possible causes: state != INIT, already requested, or RD version < 2"); Err(crate::PortalError::clipboard(format!( "Failed to request clipboard access for session: {}", e ))) } } } /// Announce RDP clipboard formats to Wayland (delayed rendering) pub async fn announce_rdp_formats( &self, session: &Session, mime_types: Vec, ) -> crate::Result<()> { if mime_types.is_empty() { debug!("No formats to announce"); return Ok(()); } let mime_refs: Vec<&str> = mime_types.iter().map(|s| s.as_str()).collect(); let options = SetSelectionOptions::default().set_mime_types(&mime_refs); self.clipboard .set_selection(session, options) .await .map_err(|e| crate::PortalError::clipboard(format!("Failed to set Portal selection: {}", e)))?; info!("Announced {} RDP formats to Portal: {:?}", mime_types.len(), mime_types); Ok(()) } /// Get reference to Portal Clipboard for direct API access pub fn portal_clipboard(&self) -> &Clipboard { &self.clipboard } /// Read from local Wayland clipboard #[expect(unsafe_code, reason = "fcntl to clear O_NONBLOCK on portal pipe FD")] pub async fn read_local_clipboard( &self, session: &Session, mime_type: &str, ) -> crate::Result> { use std::io::Read; use std::os::fd::AsRawFd; debug!("Reading local clipboard: {}", mime_type); let fd = self .clipboard .selection_read(session, mime_type) .await .map_err(|e| crate::PortalError::clipboard(format!("Failed to get SelectionRead fd: {}", e)))?; let std_fd: std::os::fd::OwnedFd = fd.into(); let mut std_file = std::fs::File::from(std_fd); // Portal returns non-blocking pipe FD - set to blocking mode let raw_fd = std_file.as_raw_fd(); // SAFETY: raw_fd is a valid file descriptor from std_file.as_raw_fd() let flags = unsafe { libc::fcntl(raw_fd, libc::F_GETFL) }; if flags != -1 { // SAFETY: raw_fd is valid, clearing O_NONBLOCK for blocking reads unsafe { libc::fcntl(raw_fd, libc::F_SETFL, flags & !libc::O_NONBLOCK) }; } let result = tokio::task::spawn_blocking(move || { let mut data = Vec::new(); std_file.read_to_end(&mut data)?; Ok::, std::io::Error>(data) }) .await .map_err(|e| crate::PortalError::clipboard(format!("Join error reading clipboard: {}", e)))? .map_err(|e| crate::PortalError::clipboard(format!("I/O error reading clipboard: {}", e)))?; info!("Read {} bytes from local clipboard ({})", result.len(), mime_type); Ok(result) } /// Write clipboard data to Portal via file descriptor pub async fn write_selection_data( &self, session: &Session, serial: u32, data: Vec, ) -> crate::Result<()> { use tokio::io::AsyncWriteExt; debug!("Writing {} bytes to Portal clipboard (serial {})", data.len(), serial); let fd = self .clipboard .selection_write(session, serial) .await .map_err(|e| crate::PortalError::clipboard(format!("Failed to get SelectionWrite fd: {}", e)))?; let std_fd: std::os::fd::OwnedFd = fd.into(); let std_file = std::fs::File::from(std_fd); let mut file = tokio::fs::File::from_std(std_file); match file.write_all(&data).await { Ok(()) => { file.flush().await?; drop(file); self.clipboard .selection_write_done(session, serial, true) .await .map_err(|e| crate::PortalError::clipboard(format!("Failed to notify write completion: {}", e)))?; info!("Wrote {} bytes to Portal clipboard (serial {})", data.len(), serial); Ok(()) } Err(e) => { drop(file); let _ = self.clipboard.selection_write_done(session, serial, false).await; Err(crate::PortalError::clipboard(format!( "Failed to write clipboard data: {}", e ))) } } } } impl std::fmt::Debug for ClipboardManager { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("PortalClipboardManager") .field("clipboard", &"") .finish() } } lamco-portal-0.4.0/src/config.rs000064400000000000000000000211001046102023000146130ustar 00000000000000//! Configuration types for Portal operations //! //! Provides flexible configuration for Portal sessions through both struct literals //! and builder patterns. use ashpd::desktop::PersistMode; use ashpd::desktop::remote_desktop::DeviceType; use ashpd::desktop::screencast::{CursorMode, SourceType}; use enumflags2::BitFlags; /// Configuration for Portal session behavior /// /// Controls how Portal requests are made and what capabilities are requested. /// All fields have sensible defaults suitable for screen capture and input control. /// /// # Examples /// /// Using defaults: /// ```no_run /// # use lamco_portal::PortalConfig; /// let config = PortalConfig::default(); /// ``` /// /// Using struct literal with defaults: /// ```no_run /// # use lamco_portal::{PortalConfig}; /// # use ashpd::desktop::screencast::CursorMode; /// let config = PortalConfig { /// cursor_mode: CursorMode::Embedded, /// ..Default::default() /// }; /// ``` /// /// Using builder: /// ```no_run /// # use lamco_portal::PortalConfig; /// # use ashpd::desktop::screencast::CursorMode; /// # use ashpd::desktop::PersistMode; /// let config = PortalConfig::builder() /// .cursor_mode(CursorMode::Embedded) /// .persist_mode(PersistMode::Application) /// .build(); /// ``` #[derive(Debug, Clone)] pub struct PortalConfig { /// How cursor should be handled in screen capture /// /// - `Hidden`: Cursor not visible in stream /// - `Embedded`: Cursor baked into video stream /// - `Metadata`: Cursor position provided as metadata (recommended for remote desktop) pub cursor_mode: CursorMode, /// Whether to persist session permissions /// /// - `DoNot`: Request permission every time (most secure) /// - `Application`: Remember permission for this app (skip dialog on reconnect) /// - `ExplicitlyRevoked`: Remember until user explicitly revokes pub persist_mode: PersistMode, /// What types of sources can be captured /// /// Can be combined: `SourceType::Monitor | SourceType::Window` /// - `Monitor`: Physical monitors /// - `Window`: Individual windows /// - `Virtual`: Virtual sources (uncommon) pub source_type: BitFlags, /// What input devices to enable for injection /// /// Can be combined: `DeviceType::Keyboard | DeviceType::Pointer` /// - `Keyboard`: Keyboard input injection /// - `Pointer`: Mouse/pointer input injection /// - `Touchscreen`: Touch input injection (less common) pub devices: BitFlags, /// Allow selecting multiple sources (monitors/windows) /// /// Most screen sharing scenarios want `true` to support multi-monitor setups pub allow_multiple: bool, /// Restore token from previous session /// /// If provided and session was persisted, can skip permission dialog. /// Obtain from previous session via Portal response (advanced usage). pub restore_token: Option, } impl Default for PortalConfig { /// Create configuration with sensible defaults /// /// - Cursor as metadata (best for remote desktop) /// - Persist until explicitly revoked (enables unattended operation) /// - Both monitors and windows available /// - Keyboard + pointer input enabled /// - Multiple sources allowed /// - No restore token (will be obtained after first grant) fn default() -> Self { Self { cursor_mode: CursorMode::Metadata, persist_mode: PersistMode::ExplicitlyRevoked, // Mode 2 - persist indefinitely source_type: SourceType::Monitor | SourceType::Window, devices: DeviceType::Keyboard | DeviceType::Pointer, allow_multiple: true, restore_token: None, } } } impl PortalConfig { /// Create a new builder for PortalConfig /// /// # Examples /// /// ```no_run /// # use lamco_portal::PortalConfig; /// # use ashpd::desktop::screencast::CursorMode; /// let config = PortalConfig::builder() /// .cursor_mode(CursorMode::Hidden) /// .build(); /// ``` pub fn builder() -> PortalConfigBuilder { PortalConfigBuilder::default() } } /// Builder for PortalConfig /// /// Provides a fluent API for configuring Portal behavior. /// All fields are optional and will use sensible defaults if not specified. /// /// # Examples /// /// ```no_run /// # use lamco_portal::PortalConfig; /// # use ashpd::desktop::screencast::{CursorMode, SourceType}; /// # use ashpd::desktop::PersistMode; /// # use ashpd::desktop::remote_desktop::DeviceType; /// let config = PortalConfig::builder() /// .cursor_mode(CursorMode::Embedded) /// .persist_mode(PersistMode::Application) /// .source_type(SourceType::Monitor.into()) /// .devices(DeviceType::Keyboard.into()) /// .allow_multiple(false) /// .build(); /// ``` #[derive(Default, Debug)] pub struct PortalConfigBuilder { cursor_mode: Option, persist_mode: Option, source_type: Option>, devices: Option>, allow_multiple: Option, restore_token: Option, } impl PortalConfigBuilder { /// Set cursor mode for screen capture /// /// Default: `CursorMode::Metadata` pub fn cursor_mode(mut self, mode: CursorMode) -> Self { self.cursor_mode = Some(mode); self } /// Set session persistence mode /// /// Default: `PersistMode::DoNot` pub fn persist_mode(mut self, mode: PersistMode) -> Self { self.persist_mode = Some(mode); self } /// Set source types that can be captured /// /// Default: `SourceType::Monitor | SourceType::Window` pub fn source_type(mut self, types: BitFlags) -> Self { self.source_type = Some(types); self } /// Set input device types to enable /// /// Default: `DeviceType::Keyboard | DeviceType::Pointer` pub fn devices(mut self, devices: BitFlags) -> Self { self.devices = Some(devices); self } /// Set whether multiple sources can be selected /// /// Default: `true` pub fn allow_multiple(mut self, allow: bool) -> Self { self.allow_multiple = Some(allow); self } /// Set restore token from previous session /// /// Default: `None` pub fn restore_token(mut self, token: String) -> Self { self.restore_token = Some(token); self } /// Build the PortalConfig /// /// Uses defaults for any unspecified fields. pub fn build(self) -> PortalConfig { let defaults = PortalConfig::default(); PortalConfig { cursor_mode: self.cursor_mode.unwrap_or(defaults.cursor_mode), persist_mode: self.persist_mode.unwrap_or(defaults.persist_mode), source_type: self.source_type.unwrap_or(defaults.source_type), devices: self.devices.unwrap_or(defaults.devices), allow_multiple: self.allow_multiple.unwrap_or(defaults.allow_multiple), restore_token: self.restore_token.or(defaults.restore_token), } } } #[cfg(test)] mod tests { use super::*; #[test] fn test_default_config() { let config = PortalConfig::default(); assert!(matches!(config.cursor_mode, CursorMode::Metadata)); assert!(matches!(config.persist_mode, PersistMode::ExplicitlyRevoked)); assert!(config.allow_multiple); assert!(config.restore_token.is_none()); } #[test] fn test_builder_with_defaults() { let config = PortalConfig::builder().build(); assert!(matches!(config.cursor_mode, CursorMode::Metadata)); assert!(matches!(config.persist_mode, PersistMode::ExplicitlyRevoked)); } #[test] fn test_builder_with_custom_values() { let config = PortalConfig::builder() .cursor_mode(CursorMode::Embedded) .persist_mode(PersistMode::Application) .allow_multiple(false) .restore_token("test-token".to_string()) .build(); assert!(matches!(config.cursor_mode, CursorMode::Embedded)); assert!(matches!(config.persist_mode, PersistMode::Application)); assert!(!config.allow_multiple); assert_eq!(config.restore_token, Some("test-token".to_string())); } #[test] fn test_struct_literal_with_defaults() { let config = PortalConfig { cursor_mode: CursorMode::Hidden, ..Default::default() }; assert!(matches!(config.cursor_mode, CursorMode::Hidden)); assert!(matches!(config.persist_mode, PersistMode::ExplicitlyRevoked)); // Still default } } lamco-portal-0.4.0/src/dbus_clipboard.rs000064400000000000000000000230301046102023000163260ustar 00000000000000//! D-Bus clipboard bridge for GNOME fallback. //! //! This module provides a workaround for GNOME where the XDG Desktop Portal's //! `SelectionOwnerChanged` signal is not reliably emitted. It connects to the //! `org.wayland_rdp.Clipboard` D-Bus interface provided by the `wayland-rdp-clipboard` //! GNOME Shell extension. //! //! # Feature Flag //! //! This module requires the `dbus-clipboard` feature: //! //! ```toml //! [dependencies] //! lamco-portal = { version = "0.1", features = ["dbus-clipboard"] } //! ``` //! //! # D-Bus Interface //! //! The bridge listens to the following D-Bus interface: //! //! - **Service**: `org.wayland_rdp.Clipboard` //! - **Path**: `/org/wayland_rdp/Clipboard` //! - **Interface**: `org.wayland_rdp.Clipboard` //! - **Signal**: `ClipboardChanged(mime_types: Vec, content_hash: String)` //! //! # Example //! //! ```ignore //! use lamco_portal::dbus_clipboard::DbusClipboardBridge; //! //! let bridge = DbusClipboardBridge::connect().await?; //! let mut receiver = bridge.subscribe(); //! //! while let Some(event) = receiver.recv().await { //! println!("Clipboard changed: {:?}", event.mime_types); //! } //! ``` use std::sync::Arc; use tokio::sync::broadcast; use tracing::{debug, error, info, warn}; use zbus::{Connection, proxy}; use crate::error::PortalError; /// Well-known D-Bus service name for the clipboard extension. pub const DBUS_SERVICE: &str = "org.wayland_rdp.Clipboard"; /// D-Bus object path for the clipboard interface. pub const DBUS_PATH: &str = "/org/wayland_rdp/Clipboard"; /// D-Bus interface name for clipboard operations. pub const DBUS_INTERFACE: &str = "org.wayland_rdp.Clipboard"; /// Event emitted when the clipboard content changes via D-Bus. #[derive(Debug, Clone)] pub struct DbusClipboardEvent { /// MIME types available in the clipboard. pub mime_types: Vec, /// Hash of the clipboard content (for deduplication). pub content_hash: String, } /// D-Bus proxy for the wayland-rdp-clipboard GNOME Shell extension. #[proxy( interface = "org.wayland_rdp.Clipboard", default_service = "org.wayland_rdp.Clipboard", default_path = "/org/wayland_rdp/Clipboard" )] trait WaylandRdpClipboard { /// Signal emitted when clipboard content changes. #[zbus(signal)] fn clipboard_changed(&self, mime_types: Vec, content_hash: String); /// Get the current clipboard MIME types. fn get_mime_types(&self) -> zbus::Result>; } /// D-Bus clipboard bridge for GNOME fallback. /// /// This bridge connects to the `org.wayland_rdp.Clipboard` D-Bus service /// provided by the GNOME Shell extension and forwards clipboard change /// events to subscribers. pub struct DbusClipboardBridge { _connection: Arc, sender: broadcast::Sender, } impl DbusClipboardBridge { /// Connect to the D-Bus clipboard service. /// /// Returns an error if the D-Bus connection fails or if the /// clipboard service is not available. /// /// # Example /// /// ```ignore /// let bridge = DbusClipboardBridge::connect().await?; /// ``` pub async fn connect() -> Result { let connection = Connection::session() .await .map_err(|e| PortalError::session_creation(format!("D-Bus connection failed: {}", e)))?; let connection = Arc::new(connection); let (sender, _) = broadcast::channel(64); let bridge = Self { _connection: connection.clone(), sender, }; // Spawn the signal listener task let sender_clone = bridge.sender.clone(); let conn_clone = connection.clone(); tokio::spawn(async move { if let Err(e) = Self::listen_for_signals(conn_clone, sender_clone).await { error!("D-Bus clipboard listener error: {}", e); } }); info!("D-Bus clipboard bridge connected"); Ok(bridge) } /// Subscribe to clipboard change events. /// /// Returns a broadcast receiver that will receive events whenever /// the clipboard content changes. pub fn subscribe(&self) -> broadcast::Receiver { self.sender.subscribe() } /// Check if the D-Bus clipboard service is available. /// /// This performs a name lookup on the session bus to verify that /// the `org.wayland_rdp.Clipboard` service is registered. pub async fn is_available() -> bool { let Ok(conn) = Connection::session().await else { return false; }; let Ok(dbus) = zbus::fdo::DBusProxy::new(&conn).await else { return false; }; // Use the service name directly - the proxy handles conversion dbus.name_has_owner(DBUS_SERVICE.try_into().expect("valid bus name")) .await .unwrap_or(false) } /// Get the current clipboard MIME types from the D-Bus service. /// /// Returns `None` if the service is not available or an error occurs. pub async fn get_current_mime_types(connection: &Connection) -> Option> { let proxy = WaylandRdpClipboardProxy::new(connection).await.ok()?; proxy.get_mime_types().await.ok() } /// Internal: Listen for clipboard change signals. async fn listen_for_signals( connection: Arc, sender: broadcast::Sender, ) -> Result<(), PortalError> { let proxy = WaylandRdpClipboardProxy::new(&connection) .await .map_err(|e| PortalError::session_creation(format!("Failed to create proxy: {}", e)))?; let mut stream = proxy .receive_clipboard_changed() .await .map_err(|e| PortalError::session_creation(format!("Failed to subscribe to signal: {}", e)))?; debug!("Listening for D-Bus clipboard signals"); use futures_util::StreamExt; while let Some(signal) = stream.next().await { match signal.args() { Ok(args) => { let event = DbusClipboardEvent { mime_types: args.mime_types.clone(), content_hash: args.content_hash.clone(), }; let hash_preview = if event.content_hash.len() > 16 { &event.content_hash[..16] } else { &event.content_hash }; debug!( "D-Bus clipboard change: {} MIME types, hash={}", event.mime_types.len(), hash_preview ); // Send to subscribers (ignore errors if no receivers) let _ = sender.send(event); } Err(e) => { warn!("Failed to parse clipboard signal args: {}", e); } } } warn!("D-Bus clipboard signal stream ended"); Ok(()) } } /// Builder for configuring the D-Bus clipboard bridge. pub struct DbusClipboardBridgeBuilder { channel_capacity: usize, } impl Default for DbusClipboardBridgeBuilder { fn default() -> Self { Self::new() } } impl DbusClipboardBridgeBuilder { /// Create a new builder with default settings. pub fn new() -> Self { Self { channel_capacity: 64 } } /// Set the broadcast channel capacity. /// /// Higher values allow more buffered events but use more memory. /// Default is 64. pub fn channel_capacity(mut self, capacity: usize) -> Self { self.channel_capacity = capacity; self } /// Build and connect the D-Bus clipboard bridge. pub async fn build(self) -> Result { let connection = Connection::session() .await .map_err(|e| PortalError::session_creation(format!("D-Bus connection failed: {}", e)))?; let connection = Arc::new(connection); let (sender, _) = broadcast::channel(self.channel_capacity); let bridge = DbusClipboardBridge { _connection: connection.clone(), sender, }; // Spawn the signal listener task let sender_clone = bridge.sender.clone(); let conn_clone = connection.clone(); tokio::spawn(async move { if let Err(e) = DbusClipboardBridge::listen_for_signals(conn_clone, sender_clone).await { error!("D-Bus clipboard listener error: {}", e); } }); info!("D-Bus clipboard bridge connected (capacity={})", self.channel_capacity); Ok(bridge) } } #[cfg(test)] mod tests { use super::*; #[test] fn test_constants() { assert_eq!(DBUS_SERVICE, "org.wayland_rdp.Clipboard"); assert_eq!(DBUS_PATH, "/org/wayland_rdp/Clipboard"); assert_eq!(DBUS_INTERFACE, "org.wayland_rdp.Clipboard"); } #[test] fn test_event_clone() { let event = DbusClipboardEvent { mime_types: vec!["text/plain".to_string()], content_hash: "abc123".to_string(), }; let cloned = event.clone(); assert_eq!(cloned.mime_types, event.mime_types); assert_eq!(cloned.content_hash, event.content_hash); } #[test] fn test_builder_default() { let builder = DbusClipboardBridgeBuilder::default(); assert_eq!(builder.channel_capacity, 64); } #[test] fn test_builder_capacity() { let builder = DbusClipboardBridgeBuilder::new().channel_capacity(128); assert_eq!(builder.channel_capacity, 128); } } lamco-portal-0.4.0/src/error.rs000064400000000000000000000131521046102023000145070ustar 00000000000000//! Error types for Portal operations //! //! Provides typed errors that library users can match and handle specifically. use thiserror::Error; /// Errors that can occur during Portal operations /// /// All Portal operations return `Result`, allowing users to /// handle different error cases appropriately. /// /// # Examples /// /// ```no_run /// # use lamco_portal::{PortalManager, PortalConfig, PortalError}; /// # async fn example() -> Result<(), PortalError> { /// let manager = PortalManager::new(PortalConfig::default()).await?; /// /// match manager.create_session("session-1".to_string(), None).await { /// Ok(session) => { /// println!("Session created successfully"); /// } /// Err(PortalError::PermissionDenied) => { /// eprintln!("User denied permission"); /// } /// Err(PortalError::PortalNotAvailable) => { /// eprintln!("Portal not installed or not running"); /// } /// Err(e) => { /// eprintln!("Other error: {}", e); /// } /// } /// # Ok(()) /// # } /// ``` #[derive(Error, Debug)] pub enum PortalError { /// Failed to connect to D-Bus session bus /// /// This usually indicates D-Bus is not running or not accessible. /// Common on systems without a desktop session. #[error("Failed to connect to D-Bus session bus")] DbusConnection(#[from] zbus::Error), /// Portal request failed /// /// This covers all Portal-specific errors from ashpd, including: /// - User denied permission /// - Portal not available /// - Invalid request parameters #[error("Portal request failed: {0}")] PortalRequest(#[from] ashpd::Error), /// User denied the Portal permission request /// /// The user explicitly denied permission in the system dialog. /// The application should handle this gracefully. #[error("User denied permission")] PermissionDenied, /// Portal is not available on this system /// /// This can occur when: /// - xdg-desktop-portal is not installed /// - No portal backend is running (e.g., xdg-desktop-portal-gnome) /// - Not running in a Wayland session #[error("Portal not available - check xdg-desktop-portal installation")] PortalNotAvailable, /// Session creation failed /// /// Failed to create a Portal session. This may be due to: /// - Portal not responding /// - Invalid configuration /// - System limitations #[error("Session creation failed: {0}")] SessionCreation(String), /// No streams available after session start /// /// This occurs when the session starts successfully but no PipeWire /// streams are provided. Usually indicates user denied screen access /// or no screens/windows are available to share. #[error("No streams available - user may have denied screen access")] NoStreamsAvailable, /// Failed to open PipeWire connection /// /// The Portal session started but we couldn't get the PipeWire file /// descriptor for accessing the stream. #[error("Failed to open PipeWire connection: {0}")] PipeWireFailed(String), /// Input injection failed /// /// Failed to inject keyboard or pointer input through the Portal. /// This may occur if input permission wasn't granted or the session /// is no longer valid. #[error("Input injection failed: {0}")] InputInjectionFailed(String), /// Clipboard operation failed /// /// Failed to perform a clipboard operation through the Portal. #[error("Clipboard operation failed: {0}")] ClipboardFailed(String), /// I/O operation failed /// /// File descriptor or pipe operation failed during clipboard data transfer. #[error("I/O operation failed: {0}")] IoError(#[from] std::io::Error), /// Invalid configuration /// /// The provided configuration is invalid or incompatible. #[error("Invalid configuration: {0}")] InvalidConfig(String), } /// Result type for Portal operations /// /// This is a convenience alias for `Result`. pub type Result = std::result::Result; // Helper implementations for common error patterns impl PortalError { /// Create a session creation error pub(crate) fn session_creation(msg: impl Into) -> Self { Self::SessionCreation(msg.into()) } /// Create a PipeWire error #[allow(dead_code)] pub(crate) fn pipewire_failed(msg: impl Into) -> Self { Self::PipeWireFailed(msg.into()) } /// Create an input injection error pub(crate) fn input_injection(msg: impl Into) -> Self { Self::InputInjectionFailed(msg.into()) } /// Create a clipboard error #[allow(dead_code)] pub(crate) fn clipboard(msg: impl Into) -> Self { Self::ClipboardFailed(msg.into()) } /// Create an invalid config error #[allow(dead_code)] pub(crate) fn invalid_config(msg: impl Into) -> Self { Self::InvalidConfig(msg.into()) } } #[cfg(test)] mod tests { use super::*; #[test] fn test_error_display() { let err = PortalError::PermissionDenied; assert_eq!(err.to_string(), "User denied permission"); let err = PortalError::session_creation("test reason"); assert_eq!(err.to_string(), "Session creation failed: test reason"); } #[test] fn test_error_helpers() { let err = PortalError::pipewire_failed("connection lost"); assert!(matches!(err, PortalError::PipeWireFailed(_))); let err = PortalError::input_injection("invalid keycode"); assert!(matches!(err, PortalError::InputInjectionFailed(_))); } } lamco-portal-0.4.0/src/lib.rs000064400000000000000000000465231046102023000141340ustar 00000000000000#![cfg_attr(docsrs, feature(doc_cfg))] //! XDG Desktop Portal integration for Wayland screen capture and input control //! //! This library provides a high-level Rust interface to the XDG Desktop Portal, //! specifically the ScreenCast, RemoteDesktop, and Clipboard interfaces. It enables //! applications to capture screen content via PipeWire and inject input events //! on Wayland compositors. //! //! # Features //! //! - **Screen capture**: Capture monitor or window content through PipeWire streams //! - **Input injection**: Send keyboard and mouse events to the desktop //! - **Clipboard integration**: Portal-based clipboard for remote desktop scenarios //! - **Multi-monitor support**: Handle multiple displays simultaneously //! - **Flexible configuration**: Builder pattern and struct literals for Portal options //! - **Typed errors**: Handle different failure modes appropriately //! //! # Requirements //! //! This library requires: //! - A Wayland compositor (e.g., GNOME, KDE Plasma, Sway) //! - `xdg-desktop-portal` installed and running //! - A portal backend for your compositor (e.g., `xdg-desktop-portal-gnome`) //! - PipeWire for video streaming //! //! # Quick Start //! //! ```no_run //! use lamco_portal::{PortalManager, PortalConfig}; //! //! # async fn example() -> Result<(), Box> { //! // Create portal manager with default config //! let manager = PortalManager::with_default().await?; //! //! // Create a session (triggers permission dialog) //! let (session, restore_token) = manager.create_session("my-session".to_string(), None).await?; //! //! // Access PipeWire file descriptor for video capture //! let fd = session.pipewire_fd(); //! let streams = session.streams(); //! //! println!("Capturing {} streams on PipeWire FD {}", streams.len(), fd); //! # Ok(()) //! # } //! ``` //! //! # Configuration //! //! Customize Portal behavior using [`PortalConfig`]: //! //! ```no_run //! use lamco_portal::{PortalManager, PortalConfig}; //! use ashpd::desktop::screencast::CursorMode; //! use ashpd::desktop::PersistMode; //! //! # async fn example() -> Result<(), Box> { //! let config = PortalConfig::builder() //! .cursor_mode(CursorMode::Embedded) // Embed cursor in video //! .persist_mode(PersistMode::Application) // Remember permission //! .build(); //! //! let manager = PortalManager::new(config).await?; //! # Ok(()) //! # } //! ``` //! //! # Input Injection //! //! Send keyboard and mouse events through the RemoteDesktop portal: //! //! ```no_run //! # use lamco_portal::{PortalManager, PortalConfig}; //! # async fn example() -> Result<(), Box> { //! # let manager = PortalManager::with_default().await?; //! # let (session, _token) = manager.create_session("my-session".to_string(), None).await?; //! // Move mouse to absolute position //! manager.remote_desktop() //! .notify_pointer_motion_absolute( //! session.ashpd_session(), //! 0, // stream index //! 100.0, // x position //! 200.0, // y position //! ) //! .await?; //! //! // Click mouse button //! manager.remote_desktop() //! .notify_pointer_button( //! session.ashpd_session(), //! 1, // button 1 (left) //! true, // pressed //! ) //! .await?; //! # Ok(()) //! # } //! ``` //! //! # Error Handling //! //! The library uses typed errors via [`PortalError`]: //! //! ```no_run //! # use lamco_portal::{PortalManager, PortalConfig, PortalError}; //! # async fn example() -> Result<(), Box> { //! # let manager = PortalManager::with_default().await?; //! match manager.create_session("my-session".to_string(), None).await { //! Ok((session, _token)) => { //! println!("Session created successfully"); //! } //! Err(PortalError::PermissionDenied) => { //! eprintln!("User denied permission in dialog"); //! } //! Err(PortalError::PortalNotAvailable) => { //! eprintln!("Portal not installed - install xdg-desktop-portal"); //! } //! Err(e) => { //! eprintln!("Other error: {}", e); //! } //! } //! # Ok(()) //! # } //! ``` //! //! # Platform Notes //! //! - **GNOME**: Works out of the box with `xdg-desktop-portal-gnome` //! - **KDE Plasma**: Use `xdg-desktop-portal-kde` //! - **wlroots** (Sway, etc.): Use `xdg-desktop-portal-wlr` //! - **X11**: Not supported - Wayland only //! //! # Security //! //! This library triggers system permission dialogs. Users must explicitly grant: //! - Screen capture access (which monitors/windows to share) //! - Input injection access (keyboard/mouse control) //! - Clipboard access (if using clipboard features) //! //! Permissions can be remembered per-application using [`ashpd::desktop::PersistMode::Application`]. use std::sync::Arc; use tracing::{debug, info, trace, warn}; pub mod clipboard; pub mod config; pub mod error; pub mod remote_desktop; pub mod screencast; pub mod session; // Optional D-Bus clipboard bridge for GNOME fallback #[cfg(feature = "dbus-clipboard")] pub mod dbus_clipboard; pub use clipboard::ClipboardManager; pub use config::{PortalConfig, PortalConfigBuilder}; // Re-export D-Bus clipboard bridge types when feature is enabled #[cfg(feature = "dbus-clipboard")] pub use dbus_clipboard::{DbusClipboardBridge, DbusClipboardEvent}; pub use error::{PortalError, Result}; pub use remote_desktop::RemoteDesktopManager; pub use screencast::ScreenCastManager; pub use session::{PortalSessionHandle, SourceType, StreamInfo}; /// Portal manager coordinates all portal interactions /// /// This is the main entry point for interacting with XDG Desktop Portals. /// It manages the lifecycle of Portal sessions and provides access to /// specialized managers for screen capture, input injection, and clipboard. /// /// # Lifecycle /// /// 1. Create a `PortalManager` with [`PortalManager::new`] or [`PortalManager::with_default`] /// 2. Create a session with [`PortalManager::create_session`] (triggers permission dialog) /// 3. Use the session for screen capture via PipeWire and input injection /// 4. Clean up with [`PortalManager::cleanup`] when done /// /// # Examples /// /// ```no_run /// use lamco_portal::{PortalManager, PortalConfig}; /// /// # async fn example() -> Result<(), Box> { /// // Simple usage with defaults /// let manager = PortalManager::with_default().await?; /// let session = manager.create_session("session-1".to_string(), None).await?; /// /// // Access specialized managers /// let screencast = manager.screencast(); /// let remote_desktop = manager.remote_desktop(); /// # Ok(()) /// # } /// ``` pub struct PortalManager { config: PortalConfig, #[expect(dead_code, reason = "connection kept alive for session lifetime")] connection: zbus::Connection, screencast: Arc, remote_desktop: Arc, clipboard: Option>, } impl PortalManager { /// Create new portal manager with specified configuration /// /// # Examples /// /// With defaults: /// ```no_run /// # use lamco_portal::{PortalManager, PortalConfig}; /// # async fn example() -> Result<(), Box> { /// let manager = PortalManager::new(PortalConfig::default()).await?; /// # Ok(()) /// # } /// ``` /// /// With custom config: /// ```no_run /// # use lamco_portal::{PortalManager, PortalConfig}; /// # use ashpd::desktop::screencast::CursorMode; /// # async fn example() -> Result<(), Box> { /// let config = PortalConfig { /// cursor_mode: CursorMode::Embedded, /// ..Default::default() /// }; /// let manager = PortalManager::new(config).await?; /// # Ok(()) /// # } /// ``` pub async fn new(config: PortalConfig) -> Result { info!("Initializing Portal Manager"); // Connect to session D-Bus let connection = zbus::Connection::session().await?; // Log connection details for debugging session issues if let Some(unique_name) = connection.unique_name() { debug!( dbus_unique_name = %unique_name, "Connected to D-Bus session bus" ); } else { debug!("Connected to D-Bus session bus (no unique name yet)"); } let screencast = Arc::new(ScreenCastManager::new(connection.clone(), &config).await?); let remote_desktop = Arc::new(RemoteDesktopManager::new(connection.clone(), &config).await?); // Clipboard manager requires a RemoteDesktop session // It will be created after session is established in create_session_with_clipboard() info!("Portal Manager initialized successfully"); Ok(Self { config, connection, screencast, remote_desktop, clipboard: None, // Created later with session }) } /// Create new portal manager with default configuration /// /// # Examples /// /// ```no_run /// # use lamco_portal::PortalManager; /// # async fn example() -> Result<(), Box> { /// let manager = PortalManager::with_default().await?; /// # Ok(()) /// # } /// ``` pub async fn with_default() -> Result { Self::new(PortalConfig::default()).await } /// Create a complete portal session (ScreenCast for video, RemoteDesktop for input, optionally Clipboard) /// /// This triggers the user permission dialog and returns a session handle /// with PipeWire access for video and input injection capabilities. /// /// # Arguments /// /// * `session_id` - Unique identifier for this session (user-provided) /// * `clipboard` - Optional Clipboard manager to enable for this session /// /// # Flow /// /// 1. Create combined RemoteDesktop session (includes ScreenCast capability) /// 2. Select devices (keyboard + pointer for input injection) /// 3. Select sources (monitors to capture for screen sharing) /// 4. Request clipboard access (if clipboard provided) ← BEFORE START /// 5. Start session (triggers permission dialog, unless restore token valid) /// 6. Get PipeWire FD, stream information, and restore token /// /// # Returns /// /// Tuple of (PortalSessionHandle, Optional restore token) /// /// The restore token should be stored securely and passed in the next /// session's PortalConfig to avoid permission dialogs. /// /// # Examples /// /// ```no_run /// # use lamco_portal::{PortalManager, PortalConfig}; /// # async fn example() -> Result<(), Box> { /// let manager = PortalManager::new(PortalConfig::default()).await?; /// let session = manager.create_session("my-session-1".to_string(), None).await?; /// # Ok(()) /// # } /// ``` pub async fn create_session( &self, session_id: String, clipboard: Option<&crate::clipboard::ClipboardManager>, ) -> Result<(PortalSessionHandle, Option)> { info!("Creating combined portal session (ScreenCast + RemoteDesktop)"); // RemoteDesktop session type supports both input injection and screen sharing let remote_desktop_session = self .remote_desktop .create_session() .await .map_err(|e| PortalError::session_creation(format!("RemoteDesktop session: {}", e)))?; // Log session creation for clipboard debugging // Note: ashpd Session.path() is private, so we generate our own tracking ID let session_tracking_id = uuid::Uuid::new_v4().to_string()[..8].to_string(); info!( session_id = %session_tracking_id, "RemoteDesktop session created" ); trace!( session_id = %session_tracking_id, "Session state is INIT (required for clipboard.request)" ); // Portal spec requires clipboard.request() when session state is INIT, // which means we must call it before SelectDevices or SelectSources. if let Some(clipboard_mgr) = clipboard { debug!(session_id = %session_tracking_id, "Requesting clipboard access"); match clipboard_mgr.enable_for_session(&remote_desktop_session).await { Ok(()) => { info!(session_id = %session_tracking_id, "Clipboard enabled for session"); } Err(e) => { warn!( session_id = %session_tracking_id, error = %e, "Clipboard request failed" ); if format!("{}", e).contains("Invalid state") { warn!("Portal daemon may have stale session state - clipboard unavailable"); } } } } // Select devices for input injection (from config) // Must close session on any error to prevent orphaned D-Bus state. // ashpd's Session does NOT implement Drop with Close() - we must do it explicitly. if let Err(e) = self .remote_desktop .select_devices(&remote_desktop_session, self.config.devices) .await { warn!("Device selection failed, closing session: {}", e); let _ = remote_desktop_session.close().await; return Err(PortalError::session_creation(format!("Device selection: {}", e))); } info!("Input devices selected from config"); // ScreenCast is required to make screen sources available for sharing let screencast_proxy = ashpd::desktop::screencast::Screencast::new().await?; let source_options = ashpd::desktop::screencast::SelectSourcesOptions::default() .set_cursor_mode(self.config.cursor_mode) .set_sources(self.config.source_type) .set_multiple(self.config.allow_multiple) .set_persist_mode(self.config.persist_mode) .set_restore_token(self.config.restore_token.as_deref()); if let Err(e) = screencast_proxy .select_sources(&remote_desktop_session, source_options) .await { // Close session before returning to prevent GNOME Shell from tracking stale state. // Without cleanup, retry attempts fail with "Invalid state" from portal daemon. warn!("Source selection failed, closing session: {}", e); let _ = remote_desktop_session.close().await; return Err(PortalError::session_creation(format!("Source selection: {}", e))); } info!("Screen sources selected - permission dialog will appear"); // Start the combined session (triggers permission dialog, unless restore token valid) // Note: clipboard.request() was already called earlier, immediately after CreateSession let (pipewire_fd, streams, restore_token) = match self.remote_desktop.start_session(&remote_desktop_session).await { Ok(result) => result, Err(e) => { warn!("Session start failed, closing session: {}", e); let _ = remote_desktop_session.close().await; return Err(PortalError::session_creation(format!("Session start: {}", e))); } }; info!("Portal session started successfully"); info!(" PipeWire FD: {:?}", pipewire_fd); info!(" Streams: {}", streams.len()); if let Some(ref token) = restore_token { info!(" Restore Token: Received ({} chars)", token.len()); } else { debug!(" Restore Token: None (portal may not support persistence)"); } if streams.is_empty() { warn!("No streams available, closing session"); let _ = remote_desktop_session.close().await; return Err(PortalError::NoStreamsAvailable); } // Keep session alive for input injection to remain functional let stream_count = streams.len(); let handle = PortalSessionHandle::new( session_id.clone(), pipewire_fd, streams, Some(session_id.clone()), // Store session ID for input operations remote_desktop_session, // Pass the actual ashpd session for input injection ); info!("Portal session handle created with {} streams", stream_count); Ok((handle, restore_token)) } /// Access the ScreenCast manager /// /// Use this to access ScreenCast-specific functionality if needed. /// Most users will use [`PortalManager::create_session`] instead. pub fn screencast(&self) -> &Arc { &self.screencast } /// Access the RemoteDesktop manager /// /// Use this to inject input events (keyboard, mouse, scroll) into /// the desktop session. Requires an active session from /// [`PortalManager::create_session`]. /// /// # Examples /// /// ```no_run /// # use lamco_portal::PortalManager; /// # async fn example() -> Result<(), Box> { /// # let manager = PortalManager::with_default().await?; /// # let (session, _token) = manager.create_session("s1".to_string(), None).await?; /// // Inject mouse movement /// manager.remote_desktop() /// .notify_pointer_motion_absolute( /// session.ashpd_session(), /// 0, 100.0, 200.0 /// ) /// .await?; /// # Ok(()) /// # } /// ``` pub fn remote_desktop(&self) -> &Arc { &self.remote_desktop } /// Access the Clipboard manager if available /// /// Returns `None` if no clipboard manager has been set. /// Clipboard integration is optional and must be explicitly enabled. pub fn clipboard(&self) -> Option<&Arc> { self.clipboard.as_ref() } /// Set clipboard manager (called after session creation) /// /// This is typically used internally during session setup. /// Most users should not need to call this directly. pub fn set_clipboard(&mut self, clipboard: Arc) { self.clipboard = Some(clipboard); } /// Cleanup all portal resources /// /// Portal sessions are automatically cleaned up when dropped, /// so calling this explicitly is optional. It can be useful for /// logging cleanup or performing graceful shutdown. pub async fn cleanup(&self) -> Result<()> { info!("Cleaning up portal resources"); // Portal sessions are automatically cleaned up when dropped Ok(()) } } #[cfg(test)] mod tests { use super::*; #[tokio::test] #[ignore] // Requires Wayland session async fn test_portal_manager_creation() { let config = PortalConfig::default(); let manager = PortalManager::new(config).await; // May fail if not in Wayland session or portal not available if manager.is_err() { eprintln!("Portal manager creation failed (expected if not in Wayland session)"); } } #[tokio::test] #[ignore] // Requires Wayland session async fn test_portal_manager_with_default() { let manager = PortalManager::with_default().await; // May fail if not in Wayland session or portal not available if manager.is_err() { eprintln!("Portal manager creation failed (expected if not in Wayland session)"); } } } lamco-portal-0.4.0/src/remote_desktop.rs000064400000000000000000000224601046102023000164040ustar 00000000000000//! RemoteDesktop portal integration //! //! Provides input injection and screen capture via RemoteDesktop portal. use std::os::fd::IntoRawFd; use ashpd::desktop::remote_desktop::{ DeviceType, KeyState, NotifyKeyboardKeycodeOptions, NotifyPointerAxisOptions, NotifyPointerButtonOptions, NotifyPointerMotionAbsoluteOptions, NotifyPointerMotionOptions, RemoteDesktop, SelectDevicesOptions, StartOptions, }; use enumflags2::BitFlags; use tracing::{debug, info}; use super::session::StreamInfo; use crate::config::PortalConfig; use crate::error::{PortalError, Result}; /// RemoteDesktop portal manager /// /// Caches the ashpd RemoteDesktop proxy to avoid creating a new D-Bus proxy /// on every input injection call. pub struct RemoteDesktopManager { config: PortalConfig, proxy: RemoteDesktop, } impl RemoteDesktopManager { /// Create new RemoteDesktop manager pub async fn new(_connection: zbus::Connection, config: &PortalConfig) -> Result { info!("Initializing RemoteDesktop portal manager"); let proxy = RemoteDesktop::new().await?; let version = proxy.version(); info!("RemoteDesktop portal version: {}", version); Ok(Self { config: config.clone(), proxy, }) } /// Get the portal interface version pub fn version(&self) -> u32 { self.proxy.version() } /// Create a remote desktop session pub async fn create_session(&self) -> Result> { info!("Creating RemoteDesktop session"); let session = self.proxy.create_session(Default::default()).await?; debug!("RemoteDesktop session created"); Ok(session) } /// Select devices for remote control pub async fn select_devices( &self, session: &ashpd::desktop::Session, devices: BitFlags, ) -> Result<()> { info!("Selecting devices: {:?}", devices); let options = SelectDevicesOptions::default() .set_devices(devices) .set_persist_mode(self.config.persist_mode) .set_restore_token(self.config.restore_token.as_deref()); self.proxy.select_devices(session, options).await?; info!("Devices selected successfully"); Ok(()) } /// Start the remote desktop session /// /// Returns: (PipeWire FD, Stream info, Optional restore token) /// /// The restore token, if present, should be stored and passed in future /// sessions via PortalConfig to avoid permission dialogs. pub async fn start_session( &self, session: &ashpd::desktop::Session, ) -> Result<(std::os::fd::RawFd, Vec, Option)> { info!("Starting RemoteDesktop session"); let response = self.proxy.start(session, None, StartOptions::default()).await?; let selected = response.response()?; let restore_token = selected.restore_token().map(|s| s.to_string()); if let Some(ref token) = restore_token { info!("Restore token received from portal (length: {} chars)", token.len()); debug!("Restore token: {}", token); } else { debug!("No restore token in response (portal may not support persistence)"); } // ashpd 0.13: streams() returns &[Stream] directly, no Option wrapper let streams = selected.streams(); info!( "RemoteDesktop started with {} devices and {} streams", selected.devices().bits(), streams.len() ); // Check if clipboard was actually granted (new in 0.13) if selected.is_clipboard_enabled() { info!("Clipboard access was granted by the portal"); } // Get PipeWire FD use ashpd::desktop::screencast::Screencast; let screencast_proxy = Screencast::new().await?; let fd = screencast_proxy .open_pipe_wire_remote(session, Default::default()) .await?; info!("PipeWire FD obtained: {:?}", fd); let stream_info: Vec = streams .iter() .map(|stream| { let node_id = stream.pipe_wire_node_id(); let size = stream.size().unwrap_or((0, 0)); let position = stream.position().unwrap_or((0, 0)); info!( "Portal provided stream: node_id={}, size=({}, {}), position=({}, {})", node_id, size.0, size.1, position.0, position.1 ); StreamInfo { node_id, position, size: ( size.0.max(0).try_into().unwrap_or(0), size.1.max(0).try_into().unwrap_or(0), ), source_type: super::session::SourceType::Monitor, } }) .collect(); info!("Total streams from Portal: {}", stream_info.len()); // Transfer FD ownership — PipeWire thread takes responsibility for closing it let raw_fd = fd.into_raw_fd(); info!("FD {} ownership transferred to caller", raw_fd); Ok((raw_fd, stream_info, restore_token)) } /// Inject pointer motion (relative) pub async fn notify_pointer_motion( &self, session: &ashpd::desktop::Session, dx: f64, dy: f64, ) -> Result<()> { self.proxy .notify_pointer_motion(session, dx, dy, NotifyPointerMotionOptions::default()) .await?; Ok(()) } /// Inject pointer motion (absolute in stream coordinates) pub async fn notify_pointer_motion_absolute( &self, session: &ashpd::desktop::Session, stream: u32, x: f64, y: f64, ) -> Result<()> { debug!("Injecting pointer motion: stream={}, x={:.2}, y={:.2}", stream, x, y); self.proxy .notify_pointer_motion_absolute(session, stream, x, y, NotifyPointerMotionAbsoluteOptions::default()) .await .map_err(|e| PortalError::input_injection(format!("Pointer motion: {}", e)))?; debug!("Pointer motion injected successfully"); Ok(()) } /// Inject pointer button pub async fn notify_pointer_button( &self, session: &ashpd::desktop::Session, button: i32, pressed: bool, ) -> Result<()> { debug!("Injecting pointer button: button={}, pressed={}", button, pressed); let state = if pressed { KeyState::Pressed } else { KeyState::Released }; self.proxy .notify_pointer_button(session, button, state, NotifyPointerButtonOptions::default()) .await .map_err(|e| PortalError::input_injection(format!("Pointer button: {}", e)))?; debug!("Pointer button injected successfully"); Ok(()) } /// Inject pointer axis (scroll) pub async fn notify_pointer_axis( &self, session: &ashpd::desktop::Session, dx: f64, dy: f64, ) -> Result<()> { self.proxy .notify_pointer_axis(session, dx, dy, NotifyPointerAxisOptions::default().set_finish(true)) .await?; Ok(()) } /// Inject keyboard keysym (for Unicode input) /// /// Sends a keyboard event by XKB keysym rather than evdev keycode. /// Used for characters that don't map to a single keycode, such as /// CJK characters or accented letters. XKB Unicode keysyms use /// the range 0x01000000 + Unicode code point. pub async fn notify_keyboard_keysym( &self, session: &ashpd::desktop::Session, keysym: i32, pressed: bool, ) -> Result<()> { use ashpd::desktop::remote_desktop::NotifyKeyboardKeysymOptions; debug!( "Injecting keyboard keysym: keysym=0x{:08X}, pressed={}", keysym, pressed ); let state = if pressed { KeyState::Pressed } else { KeyState::Released }; self.proxy .notify_keyboard_keysym(session, keysym, state, NotifyKeyboardKeysymOptions::default()) .await .map_err(|e| PortalError::input_injection(format!("Keyboard keysym: {}", e)))?; debug!("Keyboard keysym event injected successfully"); Ok(()) } /// Inject keyboard key pub async fn notify_keyboard_keycode( &self, session: &ashpd::desktop::Session, keycode: i32, pressed: bool, ) -> Result<()> { debug!("Injecting keyboard: keycode={}, pressed={}", keycode, pressed); let state = if pressed { KeyState::Pressed } else { KeyState::Released }; self.proxy .notify_keyboard_keycode(session, keycode, state, NotifyKeyboardKeycodeOptions::default()) .await .map_err(|e| PortalError::input_injection(format!("Keyboard keycode: {}", e)))?; debug!("Keyboard event injected successfully"); Ok(()) } } #[cfg(test)] mod tests { use super::*; #[tokio::test] #[ignore] async fn test_remote_desktop_session_creation() { let connection = zbus::Connection::session().await.unwrap(); let config = PortalConfig::default(); let _manager = RemoteDesktopManager::new(connection, &config).await.unwrap(); } } lamco-portal-0.4.0/src/screencast.rs000064400000000000000000000060021046102023000155040ustar 00000000000000//! ScreenCast portal integration //! //! Provides access to screen content via xdg-desktop-portal ScreenCast interface. use std::os::fd::{IntoRawFd, RawFd}; use ashpd::desktop::screencast::Screencast; use tracing::{debug, info}; use super::session::StreamInfo; use crate::config::PortalConfig; use crate::error::Result; /// ScreenCast portal manager /// /// Caches the ashpd Screencast proxy to avoid creating a new D-Bus proxy /// on every operation. pub struct ScreenCastManager { #[expect(dead_code, reason = "config reserved for future use")] config: PortalConfig, proxy: Screencast, } impl ScreenCastManager { /// Create new ScreenCast manager pub async fn new(_connection: zbus::Connection, config: &PortalConfig) -> Result { info!("Initializing ScreenCast portal manager"); let proxy = Screencast::new().await?; Ok(Self { config: config.clone(), proxy, }) } /// Create a screencast session pub async fn create_session(&self) -> Result> { info!("Creating ScreenCast session"); let session = self.proxy.create_session(Default::default()).await?; debug!("ScreenCast session created"); Ok(session) } /// Start the screencast and get PipeWire details pub async fn start(&self, session: &ashpd::desktop::Session) -> Result<(RawFd, Vec)> { info!("Starting screencast session"); let streams_request = self.proxy.start(session, None, Default::default()).await?; let streams = streams_request.response()?; info!("Screencast started with {} streams", streams.streams().len()); // Get PipeWire FD let fd = self.proxy.open_pipe_wire_remote(session, Default::default()).await?; // Transfer FD ownership — caller takes responsibility for closing it let raw_fd = fd.into_raw_fd(); info!("PipeWire FD obtained: {}", raw_fd); let stream_info: Vec = streams .streams() .iter() .map(|stream| { let size = stream.size().unwrap_or((0, 0)); StreamInfo { node_id: stream.pipe_wire_node_id(), position: stream.position().unwrap_or((0, 0)), size: ( size.0.max(0).try_into().unwrap_or(0), size.1.max(0).try_into().unwrap_or(0), ), source_type: super::session::SourceType::Monitor, } }) .collect(); Ok((raw_fd, stream_info)) } } #[cfg(test)] mod tests { use super::*; #[tokio::test] #[ignore] // Ignore in CI, run manually async fn test_screencast_manager_creation() { let connection = zbus::Connection::session().await.unwrap(); let config = PortalConfig::default(); let manager = ScreenCastManager::new(connection, &config).await; assert!(manager.is_ok()); } } lamco-portal-0.4.0/src/session.rs000064400000000000000000000145571046102023000150530ustar 00000000000000//! Portal session management //! //! Manages the lifecycle of portal sessions and associated resources. use std::os::fd::RawFd; use tracing::info; /// Information about a PipeWire stream from the portal #[derive(Debug, Clone)] pub struct StreamInfo { /// PipeWire node ID pub node_id: u32, /// Stream position (for multi-monitor) pub position: (i32, i32), /// Stream size pub size: (u32, u32), /// Source type (monitor, window, etc.) pub source_type: SourceType, } /// Source type for streams #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum SourceType { Monitor, Window, Virtual, } /// Handle to an active portal session /// /// This represents a running Portal session with screen capture and input /// injection capabilities. It provides access to: /// - PipeWire file descriptor for video stream capture /// - Stream information (one per monitor/window) /// - The underlying ashpd session for input injection /// /// # Lifecycle /// /// Created by [`crate::PortalManager::create_session`]. The session remains active /// until this handle is dropped. Dropping the handle will automatically close /// the Portal session and stop all streams. /// /// # Examples /// /// ```no_run /// # use lamco_portal::PortalManager; /// # async fn example() -> Result<(), Box> { /// let manager = PortalManager::with_default().await?; /// let (session, _token) = manager.create_session("my-session".to_string(), None).await?; /// /// // Access PipeWire FD for video capture /// let fd = session.pipewire_fd(); /// println!("PipeWire FD: {}", fd); /// /// // Get stream information /// for stream in session.streams() { /// println!("Stream {}: {}x{} at ({}, {})", /// stream.node_id, /// stream.size.0, stream.size.1, /// stream.position.0, stream.position.1 /// ); /// } /// /// // Use for input injection /// manager.remote_desktop() /// .notify_pointer_button(session.ashpd_session(), 1, true) /// .await?; /// # Ok(()) /// # } /// ``` pub struct PortalSessionHandle { /// Session identifier from portal pub session_id: String, /// PipeWire file descriptor (raw - ownership transferred to PipeWire thread) pipewire_fd: RawFd, /// Available streams (one per monitor typically) pub streams: Vec, /// RemoteDesktop session for input injection pub remote_desktop_session: Option, /// Active ashpd session (needed for input injection) pub session: ashpd::desktop::Session, } impl PortalSessionHandle { /// Create new session handle pub fn new( session_id: String, pipewire_fd: RawFd, streams: Vec, remote_desktop_session: Option, session: ashpd::desktop::Session, ) -> Self { info!( "Created portal session handle: {}, {} streams, fd: {:?}", session_id, streams.len(), pipewire_fd ); Self { session_id, pipewire_fd, streams, remote_desktop_session, session, } } /// Get PipeWire file descriptor as raw fd /// /// Returns the raw file descriptor for use with PipeWire. /// /// Note: Ownership was transferred when this handle was created (via std::mem::forget). /// The FD will NOT be closed when this handle drops - PipeWire thread owns it. pub fn pipewire_fd(&self) -> RawFd { self.pipewire_fd } /// Get stream information pub fn streams(&self) -> &[StreamInfo] { &self.streams } /// Get session ID pub fn session_id(&self) -> &str { &self.session_id } /// Get remote desktop session (for input injection) pub fn remote_desktop_session(&self) -> Option<&str> { self.remote_desktop_session.as_deref() } /// Get reference to the underlying ashpd session /// /// Required for input injection operations via [`crate::remote_desktop::RemoteDesktopManager`]. /// Most operations that need this will accept `session.ashpd_session()`. /// /// # Examples /// /// ```no_run /// # use lamco_portal::PortalManager; /// # async fn example() -> Result<(), Box> { /// # let manager = PortalManager::with_default().await?; /// # let (session, _token) = manager.create_session("s1".to_string(), None).await?; /// // Inject input using the ashpd session /// manager.remote_desktop() /// .notify_pointer_button(session.ashpd_session(), 1, true) /// .await?; /// # Ok(()) /// # } /// ``` pub fn ashpd_session(&self) -> &ashpd::desktop::Session { &self.session } /// Explicitly close the portal session /// /// This consumes the handle and closes all resources. /// /// NOTE: OwnedFd auto-closes on drop, but ashpd's Session does NOT call /// Close() on the D-Bus interface when dropped. For error paths during /// session creation, we must call session.close() explicitly - see lib.rs. /// For normally-started sessions, dropping the handle is typically fine /// since the session will be closed by the compositor when we disconnect. pub fn close(self) { info!("Closing portal session: {}", self.session_id); drop(self); } } #[cfg(test)] mod tests { use super::*; #[test] fn test_stream_info_creation() { let stream = StreamInfo { node_id: 42, position: (0, 0), size: (1920, 1080), source_type: SourceType::Monitor, }; assert_eq!(stream.node_id, 42); assert_eq!(stream.position, (0, 0)); assert_eq!(stream.size, (1920, 1080)); assert!(matches!(stream.source_type, SourceType::Monitor)); } #[test] fn test_source_type_variants() { assert!(matches!(SourceType::Monitor, SourceType::Monitor)); assert!(matches!(SourceType::Window, SourceType::Window)); assert!(matches!(SourceType::Virtual, SourceType::Virtual)); } // Note: PortalSessionHandle::new() requires an actual ashpd::Session which // can only be created with a D-Bus connection. Integration tests for session // creation are marked with #[ignore] and require a running Wayland session. }