├── .envrc ├── .github ├── dependabot.yml └── workflows │ ├── ci.yaml │ └── typos.yml ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── _typos.toml ├── contrib └── PKGBUILD │ └── PKGBUILD ├── flake.lock ├── flake.nix ├── libs ├── accessdialog │ ├── Cargo.toml │ ├── build.rs │ ├── src │ │ └── lib.rs │ └── ui │ │ └── dialog.slint └── screenshotdialog │ ├── Cargo.toml │ ├── build.rs │ ├── src │ └── lib.rs │ └── ui │ └── selectwindow.slint ├── meson.build ├── misc ├── luminous.portal ├── org.freedesktop.impl.portal.desktop.luminous.service.in └── xdg-desktop-portal-luminous.service.in ├── shell.nix └── src ├── access.rs ├── main.rs ├── pipewirethread.rs ├── remotedesktop.rs ├── remotedesktop ├── dispatch.rs ├── remote_thread.rs └── state.rs ├── request.rs ├── screencast.rs ├── screenshot.rs ├── session.rs ├── settings.rs ├── settings └── config.rs └── utils.rs /.envrc: -------------------------------------------------------------------------------- 1 | use flake 2 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | # docs 7 | # https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file 8 | version: 2 9 | updates: 10 | - package-ecosystem: "cargo" 11 | directory: "/" 12 | schedule: 13 | interval: "weekly" 14 | # We release on Tuesdays and open dependabot PRs will rebase after the 15 | # version bump and thus consume unnecessary workers during release, thus 16 | # let's open new ones on Wednesday 17 | day: "wednesday" 18 | ignore: 19 | - dependency-name: "*" 20 | update-types: ["version-update:semver-patch"] 21 | groups: 22 | # Only update polars as a whole as there are many subcrates that need to 23 | # be updated at once. We explicitly depend on some of them, so batch their 24 | # updates to not take up dependabot PR slots with dysfunctional PRs 25 | polars: 26 | patterns: 27 | - "polars" 28 | - "polars-*" 29 | - package-ecosystem: "github-actions" 30 | directory: "/" 31 | schedule: 32 | interval: "weekly" 33 | day: "wednesday" 34 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: Build xdg-desktop-portal-luminous (archlinux) 2 | 3 | on: [push, pull_request] 4 | jobs: 5 | build: 6 | name: "Build xdg-desktop-portal-luminous" 7 | runs-on: ubuntu-latest 8 | container: 9 | image: archlinux:latest 10 | steps: 11 | - uses: actions/checkout@v4 12 | - name: "install deps" 13 | run: | 14 | pacman -Syu --noconfirm base-devel clang meson ninja pipewire libxkbcommon wayland cairo pango 15 | - uses: dtolnay/rust-toolchain@stable 16 | with: 17 | components: clippy rustfmt 18 | - name: Run fmt check 19 | run: cargo fmt --all -- --check 20 | - name: Run clippy 21 | run: cargo clippy -- -D warnings 22 | - name: build target 23 | run: | 24 | meson setup build 25 | meson compile -C build 26 | -------------------------------------------------------------------------------- /.github/workflows/typos.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # yamllint disable rule:line-length 3 | name: check_typos 4 | 5 | on: # yamllint disable-line rule:truthy 6 | push: 7 | pull_request: 8 | branches: 9 | - '**' 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - uses: actions/checkout@v4 17 | 18 | - name: typos-action 19 | uses: crate-ci/typos@v1.32.0 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | .direnv 3 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace.package] 2 | version = "0.1.10" 3 | edition = "2024" 4 | license = "MIT" 5 | repository = "https://github.com/waycrate/xdg-desktop-portal-luminous" 6 | keywords = ["wayland"] 7 | 8 | [package] 9 | name = "xdg-desktop-portal-luminous" 10 | version.workspace = true 11 | edition.workspace = true 12 | license.workspace = true 13 | repository.workspace = true 14 | keywords = ["wayland"] 15 | 16 | [workspace] 17 | members = ["libs/screenshotdialog", "libs/accessdialog", "."] 18 | 19 | [workspace.dependencies] 20 | screenshotdialog = { path = "libs/screenshotdialog" } 21 | accessdialog = { path = "libs/accessdialog" } 22 | slint = "1.11.0" 23 | slint-build = "1.11.0" 24 | 25 | [dependencies] 26 | screenshotdialog.workspace = true 27 | accessdialog.workspace = true 28 | 29 | zbus = { version = "5.7", default-features = false, features = [ 30 | "tokio", 31 | "url", 32 | ] } 33 | tokio = { version = "1.45.0", features = ["full"] } 34 | serde = { version = "1.0.219", features = ["derive"] } 35 | tracing = "0.1.41" 36 | tracing-subscriber = "0.3.19" 37 | url = { version = "2.5", features = ["serde"] } 38 | serde_repr = "0.1" 39 | image = { version = "=0.24", default-features = false, features = [ 40 | "jpeg", 41 | "png", 42 | "pnm", 43 | "qoi", 44 | ] } 45 | 46 | bitflags = "2.9.0" 47 | enumflags2 = "0.7.11" 48 | anyhow = "1.0.98" 49 | 50 | # pipewire 51 | pipewire = "0.8.0" 52 | libspa-sys = "0.8.0" 53 | 54 | libwayshot = { version = "0.3.0" } 55 | rustix = { version = "1.0.5", features = ["fs", "use-libc"] } 56 | 57 | # REMOTE 58 | wayland-protocols = { version = "0.32.6", default-features = false, features = [ 59 | "unstable", 60 | "client", 61 | ] } 62 | 63 | wayland-protocols-wlr = { version = "0.3.6", default-features = false, features = [ 64 | "client", 65 | ] } 66 | wayland-client = { version = "0.31.8" } 67 | 68 | wayland-protocols-misc = { version = "0.3.6", features = ["client"] } 69 | xkbcommon = "0.8.0" 70 | tempfile = "3.20.0" 71 | thiserror = "2.0.12" 72 | toml = "0.8.20" 73 | csscolorparser = "0.7.0" 74 | notify = "8.0.0" 75 | futures = "0.3.31" 76 | libwaysip = "0.4.0" 77 | 78 | calloop = "0.14.1" 79 | calloop-wayland-source = "0.4.0" 80 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU General Public License is a free, copyleft license for 11 | software and other kinds of works. 12 | 13 | The licenses for most software and other practical works are designed 14 | to take away your freedom to share and change the works. By contrast, 15 | the GNU General Public License is intended to guarantee your freedom to 16 | share and change all versions of a program--to make sure it remains free 17 | software for all its users. We, the Free Software Foundation, use the 18 | GNU General Public License for most of our software; it applies also to 19 | any other work released this way by its authors. You can apply it to 20 | your programs, too. 21 | 22 | When we speak of free software, we are referring to freedom, not 23 | price. Our General Public Licenses are designed to make sure that you 24 | have the freedom to distribute copies of free software (and charge for 25 | them if you wish), that you receive source code or can get it if you 26 | want it, that you can change the software or use pieces of it in new 27 | free programs, and that you know you can do these things. 28 | 29 | To protect your rights, we need to prevent others from denying you 30 | these rights or asking you to surrender the rights. Therefore, you have 31 | certain responsibilities if you distribute copies of the software, or if 32 | you modify it: responsibilities to respect the freedom of others. 33 | 34 | For example, if you distribute copies of such a program, whether 35 | gratis or for a fee, you must pass on to the recipients the same 36 | freedoms that you received. You must make sure that they, too, receive 37 | or can get the source code. And you must show them these terms so they 38 | know their rights. 39 | 40 | Developers that use the GNU GPL protect your rights with two steps: 41 | (1) assert copyright on the software, and (2) offer you this License 42 | giving you legal permission to copy, distribute and/or modify it. 43 | 44 | For the developers' and authors' protection, the GPL clearly explains 45 | that there is no warranty for this free software. For both users' and 46 | authors' sake, the GPL requires that modified versions be marked as 47 | changed, so that their problems will not be attributed erroneously to 48 | authors of previous versions. 49 | 50 | Some devices are designed to deny users access to install or run 51 | modified versions of the software inside them, although the manufacturer 52 | can do so. This is fundamentally incompatible with the aim of 53 | protecting users' freedom to change the software. The systematic 54 | pattern of such abuse occurs in the area of products for individuals to 55 | use, which is precisely where it is most unacceptable. Therefore, we 56 | have designed this version of the GPL to prohibit the practice for those 57 | products. If such problems arise substantially in other domains, we 58 | stand ready to extend this provision to those domains in future versions 59 | of the GPL, as needed to protect the freedom of users. 60 | 61 | Finally, every program is threatened constantly by software patents. 62 | States should not allow patents to restrict development and use of 63 | software on general-purpose computers, but in those that do, we wish to 64 | avoid the special danger that patents applied to a free program could 65 | make it effectively proprietary. To prevent this, the GPL assures that 66 | patents cannot be used to render the program non-free. 67 | 68 | The precise terms and conditions for copying, distribution and 69 | modification follow. 70 | 71 | TERMS AND CONDITIONS 72 | 73 | 0. Definitions. 74 | 75 | "This License" refers to version 3 of the GNU General Public License. 76 | 77 | "Copyright" also means copyright-like laws that apply to other kinds of 78 | works, such as semiconductor masks. 79 | 80 | "The Program" refers to any copyrightable work licensed under this 81 | License. Each licensee is addressed as "you". "Licensees" and 82 | "recipients" may be individuals or organizations. 83 | 84 | To "modify" a work means to copy from or adapt all or part of the work 85 | in a fashion requiring copyright permission, other than the making of an 86 | exact copy. The resulting work is called a "modified version" of the 87 | earlier work or a work "based on" the earlier work. 88 | 89 | A "covered work" means either the unmodified Program or a work based 90 | on the Program. 91 | 92 | To "propagate" a work means to do anything with it that, without 93 | permission, would make you directly or secondarily liable for 94 | infringement under applicable copyright law, except executing it on a 95 | computer or modifying a private copy. Propagation includes copying, 96 | distribution (with or without modification), making available to the 97 | public, and in some countries other activities as well. 98 | 99 | To "convey" a work means any kind of propagation that enables other 100 | parties to make or receive copies. Mere interaction with a user through 101 | a computer network, with no transfer of a copy, is not conveying. 102 | 103 | An interactive user interface displays "Appropriate Legal Notices" 104 | to the extent that it includes a convenient and prominently visible 105 | feature that (1) displays an appropriate copyright notice, and (2) 106 | tells the user that there is no warranty for the work (except to the 107 | extent that warranties are provided), that licensees may convey the 108 | work under this License, and how to view a copy of this License. If 109 | the interface presents a list of user commands or options, such as a 110 | menu, a prominent item in the list meets this criterion. 111 | 112 | 1. Source Code. 113 | 114 | The "source code" for a work means the preferred form of the work 115 | for making modifications to it. "Object code" means any non-source 116 | form of a work. 117 | 118 | A "Standard Interface" means an interface that either is an official 119 | standard defined by a recognized standards body, or, in the case of 120 | interfaces specified for a particular programming language, one that 121 | is widely used among developers working in that language. 122 | 123 | The "System Libraries" of an executable work include anything, other 124 | than the work as a whole, that (a) is included in the normal form of 125 | packaging a Major Component, but which is not part of that Major 126 | Component, and (b) serves only to enable use of the work with that 127 | Major Component, or to implement a Standard Interface for which an 128 | implementation is available to the public in source code form. A 129 | "Major Component", in this context, means a major essential component 130 | (kernel, window system, and so on) of the specific operating system 131 | (if any) on which the executable work runs, or a compiler used to 132 | produce the work, or an object code interpreter used to run it. 133 | 134 | The "Corresponding Source" for a work in object code form means all 135 | the source code needed to generate, install, and (for an executable 136 | work) run the object code and to modify the work, including scripts to 137 | control those activities. However, it does not include the work's 138 | System Libraries, or general-purpose tools or generally available free 139 | programs which are used unmodified in performing those activities but 140 | which are not part of the work. For example, Corresponding Source 141 | includes interface definition files associated with source files for 142 | the work, and the source code for shared libraries and dynamically 143 | linked subprograms that the work is specifically designed to require, 144 | such as by intimate data communication or control flow between those 145 | subprograms and other parts of the work. 146 | 147 | The Corresponding Source need not include anything that users 148 | can regenerate automatically from other parts of the Corresponding 149 | Source. 150 | 151 | The Corresponding Source for a work in source code form is that 152 | same work. 153 | 154 | 2. Basic Permissions. 155 | 156 | All rights granted under this License are granted for the term of 157 | copyright on the Program, and are irrevocable provided the stated 158 | conditions are met. This License explicitly affirms your unlimited 159 | permission to run the unmodified Program. The output from running a 160 | covered work is covered by this License only if the output, given its 161 | content, constitutes a covered work. This License acknowledges your 162 | rights of fair use or other equivalent, as provided by copyright law. 163 | 164 | You may make, run and propagate covered works that you do not 165 | convey, without conditions so long as your license otherwise remains 166 | in force. You may convey covered works to others for the sole purpose 167 | of having them make modifications exclusively for you, or provide you 168 | with facilities for running those works, provided that you comply with 169 | the terms of this License in conveying all material for which you do 170 | not control copyright. Those thus making or running the covered works 171 | for you must do so exclusively on your behalf, under your direction 172 | and control, on terms that prohibit them from making any copies of 173 | your copyrighted material outside their relationship with you. 174 | 175 | Conveying under any other circumstances is permitted solely under 176 | the conditions stated below. Sublicensing is not allowed; section 10 177 | makes it unnecessary. 178 | 179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 180 | 181 | No covered work shall be deemed part of an effective technological 182 | measure under any applicable law fulfilling obligations under article 183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 184 | similar laws prohibiting or restricting circumvention of such 185 | measures. 186 | 187 | When you convey a covered work, you waive any legal power to forbid 188 | circumvention of technological measures to the extent such circumvention 189 | is effected by exercising rights under this License with respect to 190 | the covered work, and you disclaim any intention to limit operation or 191 | modification of the work as a means of enforcing, against the work's 192 | users, your or third parties' legal rights to forbid circumvention of 193 | technological measures. 194 | 195 | 4. Conveying Verbatim Copies. 196 | 197 | You may convey verbatim copies of the Program's source code as you 198 | receive it, in any medium, provided that you conspicuously and 199 | appropriately publish on each copy an appropriate copyright notice; 200 | keep intact all notices stating that this License and any 201 | non-permissive terms added in accord with section 7 apply to the code; 202 | keep intact all notices of the absence of any warranty; and give all 203 | recipients a copy of this License along with the Program. 204 | 205 | You may charge any price or no price for each copy that you convey, 206 | and you may offer support or warranty protection for a fee. 207 | 208 | 5. Conveying Modified Source Versions. 209 | 210 | You may convey a work based on the Program, or the modifications to 211 | produce it from the Program, in the form of source code under the 212 | terms of section 4, provided that you also meet all of these conditions: 213 | 214 | a) The work must carry prominent notices stating that you modified 215 | it, and giving a relevant date. 216 | 217 | b) The work must carry prominent notices stating that it is 218 | released under this License and any conditions added under section 219 | 7. This requirement modifies the requirement in section 4 to 220 | "keep intact all notices". 221 | 222 | c) You must license the entire work, as a whole, under this 223 | License to anyone who comes into possession of a copy. This 224 | License will therefore apply, along with any applicable section 7 225 | additional terms, to the whole of the work, and all its parts, 226 | regardless of how they are packaged. This License gives no 227 | permission to license the work in any other way, but it does not 228 | invalidate such permission if you have separately received it. 229 | 230 | d) If the work has interactive user interfaces, each must display 231 | Appropriate Legal Notices; however, if the Program has interactive 232 | interfaces that do not display Appropriate Legal Notices, your 233 | work need not make them do so. 234 | 235 | A compilation of a covered work with other separate and independent 236 | works, which are not by their nature extensions of the covered work, 237 | and which are not combined with it such as to form a larger program, 238 | in or on a volume of a storage or distribution medium, is called an 239 | "aggregate" if the compilation and its resulting copyright are not 240 | used to limit the access or legal rights of the compilation's users 241 | beyond what the individual works permit. Inclusion of a covered work 242 | in an aggregate does not cause this License to apply to the other 243 | parts of the aggregate. 244 | 245 | 6. Conveying Non-Source Forms. 246 | 247 | You may convey a covered work in object code form under the terms 248 | of sections 4 and 5, provided that you also convey the 249 | machine-readable Corresponding Source under the terms of this License, 250 | in one of these ways: 251 | 252 | a) Convey the object code in, or embodied in, a physical product 253 | (including a physical distribution medium), accompanied by the 254 | Corresponding Source fixed on a durable physical medium 255 | customarily used for software interchange. 256 | 257 | b) Convey the object code in, or embodied in, a physical product 258 | (including a physical distribution medium), accompanied by a 259 | written offer, valid for at least three years and valid for as 260 | long as you offer spare parts or customer support for that product 261 | model, to give anyone who possesses the object code either (1) a 262 | copy of the Corresponding Source for all the software in the 263 | product that is covered by this License, on a durable physical 264 | medium customarily used for software interchange, for a price no 265 | more than your reasonable cost of physically performing this 266 | conveying of source, or (2) access to copy the 267 | Corresponding Source from a network server at no charge. 268 | 269 | c) Convey individual copies of the object code with a copy of the 270 | written offer to provide the Corresponding Source. This 271 | alternative is allowed only occasionally and noncommercially, and 272 | only if you received the object code with such an offer, in accord 273 | with subsection 6b. 274 | 275 | d) Convey the object code by offering access from a designated 276 | place (gratis or for a charge), and offer equivalent access to the 277 | Corresponding Source in the same way through the same place at no 278 | further charge. You need not require recipients to copy the 279 | Corresponding Source along with the object code. If the place to 280 | copy the object code is a network server, the Corresponding Source 281 | may be on a different server (operated by you or a third party) 282 | that supports equivalent copying facilities, provided you maintain 283 | clear directions next to the object code saying where to find the 284 | Corresponding Source. Regardless of what server hosts the 285 | Corresponding Source, you remain obligated to ensure that it is 286 | available for as long as needed to satisfy these requirements. 287 | 288 | e) Convey the object code using peer-to-peer transmission, provided 289 | you inform other peers where the object code and Corresponding 290 | Source of the work are being offered to the general public at no 291 | charge under subsection 6d. 292 | 293 | A separable portion of the object code, whose source code is excluded 294 | from the Corresponding Source as a System Library, need not be 295 | included in conveying the object code work. 296 | 297 | A "User Product" is either (1) a "consumer product", which means any 298 | tangible personal property which is normally used for personal, family, 299 | or household purposes, or (2) anything designed or sold for incorporation 300 | into a dwelling. In determining whether a product is a consumer product, 301 | doubtful cases shall be resolved in favor of coverage. For a particular 302 | product received by a particular user, "normally used" refers to a 303 | typical or common use of that class of product, regardless of the status 304 | of the particular user or of the way in which the particular user 305 | actually uses, or expects or is expected to use, the product. A product 306 | is a consumer product regardless of whether the product has substantial 307 | commercial, industrial or non-consumer uses, unless such uses represent 308 | the only significant mode of use of the product. 309 | 310 | "Installation Information" for a User Product means any methods, 311 | procedures, authorization keys, or other information required to install 312 | and execute modified versions of a covered work in that User Product from 313 | a modified version of its Corresponding Source. The information must 314 | suffice to ensure that the continued functioning of the modified object 315 | code is in no case prevented or interfered with solely because 316 | modification has been made. 317 | 318 | If you convey an object code work under this section in, or with, or 319 | specifically for use in, a User Product, and the conveying occurs as 320 | part of a transaction in which the right of possession and use of the 321 | User Product is transferred to the recipient in perpetuity or for a 322 | fixed term (regardless of how the transaction is characterized), the 323 | Corresponding Source conveyed under this section must be accompanied 324 | by the Installation Information. But this requirement does not apply 325 | if neither you nor any third party retains the ability to install 326 | modified object code on the User Product (for example, the work has 327 | been installed in ROM). 328 | 329 | The requirement to provide Installation Information does not include a 330 | requirement to continue to provide support service, warranty, or updates 331 | for a work that has been modified or installed by the recipient, or for 332 | the User Product in which it has been modified or installed. Access to a 333 | network may be denied when the modification itself materially and 334 | adversely affects the operation of the network or violates the rules and 335 | protocols for communication across the network. 336 | 337 | Corresponding Source conveyed, and Installation Information provided, 338 | in accord with this section must be in a format that is publicly 339 | documented (and with an implementation available to the public in 340 | source code form), and must require no special password or key for 341 | unpacking, reading or copying. 342 | 343 | 7. Additional Terms. 344 | 345 | "Additional permissions" are terms that supplement the terms of this 346 | License by making exceptions from one or more of its conditions. 347 | Additional permissions that are applicable to the entire Program shall 348 | be treated as though they were included in this License, to the extent 349 | that they are valid under applicable law. If additional permissions 350 | apply only to part of the Program, that part may be used separately 351 | under those permissions, but the entire Program remains governed by 352 | this License without regard to the additional permissions. 353 | 354 | When you convey a copy of a covered work, you may at your option 355 | remove any additional permissions from that copy, or from any part of 356 | it. (Additional permissions may be written to require their own 357 | removal in certain cases when you modify the work.) You may place 358 | additional permissions on material, added by you to a covered work, 359 | for which you have or can give appropriate copyright permission. 360 | 361 | Notwithstanding any other provision of this License, for material you 362 | add to a covered work, you may (if authorized by the copyright holders of 363 | that material) supplement the terms of this License with terms: 364 | 365 | a) Disclaiming warranty or limiting liability differently from the 366 | terms of sections 15 and 16 of this License; or 367 | 368 | b) Requiring preservation of specified reasonable legal notices or 369 | author attributions in that material or in the Appropriate Legal 370 | Notices displayed by works containing it; or 371 | 372 | c) Prohibiting misrepresentation of the origin of that material, or 373 | requiring that modified versions of such material be marked in 374 | reasonable ways as different from the original version; or 375 | 376 | d) Limiting the use for publicity purposes of names of licensors or 377 | authors of the material; or 378 | 379 | e) Declining to grant rights under trademark law for use of some 380 | trade names, trademarks, or service marks; or 381 | 382 | f) Requiring indemnification of licensors and authors of that 383 | material by anyone who conveys the material (or modified versions of 384 | it) with contractual assumptions of liability to the recipient, for 385 | any liability that these contractual assumptions directly impose on 386 | those licensors and authors. 387 | 388 | All other non-permissive additional terms are considered "further 389 | restrictions" within the meaning of section 10. If the Program as you 390 | received it, or any part of it, contains a notice stating that it is 391 | governed by this License along with a term that is a further 392 | restriction, you may remove that term. If a license document contains 393 | a further restriction but permits relicensing or conveying under this 394 | License, you may add to a covered work material governed by the terms 395 | of that license document, provided that the further restriction does 396 | not survive such relicensing or conveying. 397 | 398 | If you add terms to a covered work in accord with this section, you 399 | must place, in the relevant source files, a statement of the 400 | additional terms that apply to those files, or a notice indicating 401 | where to find the applicable terms. 402 | 403 | Additional terms, permissive or non-permissive, may be stated in the 404 | form of a separately written license, or stated as exceptions; 405 | the above requirements apply either way. 406 | 407 | 8. Termination. 408 | 409 | You may not propagate or modify a covered work except as expressly 410 | provided under this License. Any attempt otherwise to propagate or 411 | modify it is void, and will automatically terminate your rights under 412 | this License (including any patent licenses granted under the third 413 | paragraph of section 11). 414 | 415 | However, if you cease all violation of this License, then your 416 | license from a particular copyright holder is reinstated (a) 417 | provisionally, unless and until the copyright holder explicitly and 418 | finally terminates your license, and (b) permanently, if the copyright 419 | holder fails to notify you of the violation by some reasonable means 420 | prior to 60 days after the cessation. 421 | 422 | Moreover, your license from a particular copyright holder is 423 | reinstated permanently if the copyright holder notifies you of the 424 | violation by some reasonable means, this is the first time you have 425 | received notice of violation of this License (for any work) from that 426 | copyright holder, and you cure the violation prior to 30 days after 427 | your receipt of the notice. 428 | 429 | Termination of your rights under this section does not terminate the 430 | licenses of parties who have received copies or rights from you under 431 | this License. If your rights have been terminated and not permanently 432 | reinstated, you do not qualify to receive new licenses for the same 433 | material under section 10. 434 | 435 | 9. Acceptance Not Required for Having Copies. 436 | 437 | You are not required to accept this License in order to receive or 438 | run a copy of the Program. Ancillary propagation of a covered work 439 | occurring solely as a consequence of using peer-to-peer transmission 440 | to receive a copy likewise does not require acceptance. However, 441 | nothing other than this License grants you permission to propagate or 442 | modify any covered work. These actions infringe copyright if you do 443 | not accept this License. Therefore, by modifying or propagating a 444 | covered work, you indicate your acceptance of this License to do so. 445 | 446 | 10. Automatic Licensing of Downstream Recipients. 447 | 448 | Each time you convey a covered work, the recipient automatically 449 | receives a license from the original licensors, to run, modify and 450 | propagate that work, subject to this License. You are not responsible 451 | for enforcing compliance by third parties with this License. 452 | 453 | An "entity transaction" is a transaction transferring control of an 454 | organization, or substantially all assets of one, or subdividing an 455 | organization, or merging organizations. If propagation of a covered 456 | work results from an entity transaction, each party to that 457 | transaction who receives a copy of the work also receives whatever 458 | licenses to the work the party's predecessor in interest had or could 459 | give under the previous paragraph, plus a right to possession of the 460 | Corresponding Source of the work from the predecessor in interest, if 461 | the predecessor has it or can get it with reasonable efforts. 462 | 463 | You may not impose any further restrictions on the exercise of the 464 | rights granted or affirmed under this License. For example, you may 465 | not impose a license fee, royalty, or other charge for exercise of 466 | rights granted under this License, and you may not initiate litigation 467 | (including a cross-claim or counterclaim in a lawsuit) alleging that 468 | any patent claim is infringed by making, using, selling, offering for 469 | sale, or importing the Program or any portion of it. 470 | 471 | 11. Patents. 472 | 473 | A "contributor" is a copyright holder who authorizes use under this 474 | License of the Program or a work on which the Program is based. The 475 | work thus licensed is called the contributor's "contributor version". 476 | 477 | A contributor's "essential patent claims" are all patent claims 478 | owned or controlled by the contributor, whether already acquired or 479 | hereafter acquired, that would be infringed by some manner, permitted 480 | by this License, of making, using, or selling its contributor version, 481 | but do not include claims that would be infringed only as a 482 | consequence of further modification of the contributor version. For 483 | purposes of this definition, "control" includes the right to grant 484 | patent sublicenses in a manner consistent with the requirements of 485 | this License. 486 | 487 | Each contributor grants you a non-exclusive, worldwide, royalty-free 488 | patent license under the contributor's essential patent claims, to 489 | make, use, sell, offer for sale, import and otherwise run, modify and 490 | propagate the contents of its contributor version. 491 | 492 | In the following three paragraphs, a "patent license" is any express 493 | agreement or commitment, however denominated, not to enforce a patent 494 | (such as an express permission to practice a patent or covenant not to 495 | sue for patent infringement). To "grant" such a patent license to a 496 | party means to make such an agreement or commitment not to enforce a 497 | patent against the party. 498 | 499 | If you convey a covered work, knowingly relying on a patent license, 500 | and the Corresponding Source of the work is not available for anyone 501 | to copy, free of charge and under the terms of this License, through a 502 | publicly available network server or other readily accessible means, 503 | then you must either (1) cause the Corresponding Source to be so 504 | available, or (2) arrange to deprive yourself of the benefit of the 505 | patent license for this particular work, or (3) arrange, in a manner 506 | consistent with the requirements of this License, to extend the patent 507 | license to downstream recipients. "Knowingly relying" means you have 508 | actual knowledge that, but for the patent license, your conveying the 509 | covered work in a country, or your recipient's use of the covered work 510 | in a country, would infringe one or more identifiable patents in that 511 | country that you have reason to believe are valid. 512 | 513 | If, pursuant to or in connection with a single transaction or 514 | arrangement, you convey, or propagate by procuring conveyance of, a 515 | covered work, and grant a patent license to some of the parties 516 | receiving the covered work authorizing them to use, propagate, modify 517 | or convey a specific copy of the covered work, then the patent license 518 | you grant is automatically extended to all recipients of the covered 519 | work and works based on it. 520 | 521 | A patent license is "discriminatory" if it does not include within 522 | the scope of its coverage, prohibits the exercise of, or is 523 | conditioned on the non-exercise of one or more of the rights that are 524 | specifically granted under this License. You may not convey a covered 525 | work if you are a party to an arrangement with a third party that is 526 | in the business of distributing software, under which you make payment 527 | to the third party based on the extent of your activity of conveying 528 | the work, and under which the third party grants, to any of the 529 | parties who would receive the covered work from you, a discriminatory 530 | patent license (a) in connection with copies of the covered work 531 | conveyed by you (or copies made from those copies), or (b) primarily 532 | for and in connection with specific products or compilations that 533 | contain the covered work, unless you entered into that arrangement, 534 | or that patent license was granted, prior to 28 March 2007. 535 | 536 | Nothing in this License shall be construed as excluding or limiting 537 | any implied license or other defenses to infringement that may 538 | otherwise be available to you under applicable patent law. 539 | 540 | 12. No Surrender of Others' Freedom. 541 | 542 | If conditions are imposed on you (whether by court order, agreement or 543 | otherwise) that contradict the conditions of this License, they do not 544 | excuse you from the conditions of this License. If you cannot convey a 545 | covered work so as to satisfy simultaneously your obligations under this 546 | License and any other pertinent obligations, then as a consequence you may 547 | not convey it at all. For example, if you agree to terms that obligate you 548 | to collect a royalty for further conveying from those to whom you convey 549 | the Program, the only way you could satisfy both those terms and this 550 | License would be to refrain entirely from conveying the Program. 551 | 552 | 13. Use with the GNU Affero General Public License. 553 | 554 | Notwithstanding any other provision of this License, you have 555 | permission to link or combine any covered work with a work licensed 556 | under version 3 of the GNU Affero General Public License into a single 557 | combined work, and to convey the resulting work. The terms of this 558 | License will continue to apply to the part which is the covered work, 559 | but the special requirements of the GNU Affero General Public License, 560 | section 13, concerning interaction through a network will apply to the 561 | combination as such. 562 | 563 | 14. Revised Versions of this License. 564 | 565 | The Free Software Foundation may publish revised and/or new versions of 566 | the GNU General Public License from time to time. Such new versions will 567 | be similar in spirit to the present version, but may differ in detail to 568 | address new problems or concerns. 569 | 570 | Each version is given a distinguishing version number. If the 571 | Program specifies that a certain numbered version of the GNU General 572 | Public License "or any later version" applies to it, you have the 573 | option of following the terms and conditions either of that numbered 574 | version or of any later version published by the Free Software 575 | Foundation. If the Program does not specify a version number of the 576 | GNU General Public License, you may choose any version ever published 577 | by the Free Software Foundation. 578 | 579 | If the Program specifies that a proxy can decide which future 580 | versions of the GNU General Public License can be used, that proxy's 581 | public statement of acceptance of a version permanently authorizes you 582 | to choose that version for the Program. 583 | 584 | Later license versions may give you additional or different 585 | permissions. However, no additional obligations are imposed on any 586 | author or copyright holder as a result of your choosing to follow a 587 | later version. 588 | 589 | 15. Disclaimer of Warranty. 590 | 591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 599 | 600 | 16. Limitation of Liability. 601 | 602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 610 | SUCH DAMAGES. 611 | 612 | 17. Interpretation of Sections 15 and 16. 613 | 614 | If the disclaimer of warranty and limitation of liability provided 615 | above cannot be given local legal effect according to their terms, 616 | reviewing courts shall apply local law that most closely approximates 617 | an absolute waiver of all civil liability in connection with the 618 | Program, unless a warranty or assumption of liability accompanies a 619 | copy of the Program in return for a fee. 620 | 621 | END OF TERMS AND CONDITIONS 622 | 623 | How to Apply These Terms to Your New Programs 624 | 625 | If you develop a new program, and you want it to be of the greatest 626 | possible use to the public, the best way to achieve this is to make it 627 | free software which everyone can redistribute and change under these terms. 628 | 629 | To do so, attach the following notices to the program. It is safest 630 | to attach them to the start of each source file to most effectively 631 | state the exclusion of warranty; and each file should have at least 632 | the "copyright" line and a pointer to where the full notice is found. 633 | 634 | 635 | Copyright (C) 636 | 637 | This program is free software: you can redistribute it and/or modify 638 | it under the terms of the GNU General Public License as published by 639 | the Free Software Foundation, either version 3 of the License, or 640 | (at your option) any later version. 641 | 642 | This program is distributed in the hope that it will be useful, 643 | but WITHOUT ANY WARRANTY; without even the implied warranty of 644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 645 | GNU General Public License for more details. 646 | 647 | You should have received a copy of the GNU General Public License 648 | along with this program. If not, see . 649 | 650 | Also add information on how to contact you by electronic and paper mail. 651 | 652 | If the program does terminal interaction, make it output a short 653 | notice like this when it starts in an interactive mode: 654 | 655 | Copyright (C) 656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 657 | This is free software, and you are welcome to redistribute it 658 | under certain conditions; type `show c' for details. 659 | 660 | The hypothetical commands `show w' and `show c' should show the appropriate 661 | parts of the General Public License. Of course, your program's commands 662 | might be different; for a GUI interface, you would use an "about box". 663 | 664 | You should also get your employer (if you work as a programmer) or school, 665 | if any, to sign a "copyright disclaimer" for the program, if necessary. 666 | For more information on this, and how to apply and follow the GNU GPL, see 667 | . 668 | 669 | The GNU General Public License does not permit incorporating your program 670 | into proprietary programs. If your program is a subroutine library, you 671 | may consider it more useful to permit linking proprietary applications with 672 | the library. If this is what you want to do, use the GNU Lesser General 673 | Public License instead of this License. But first, please read 674 | . 675 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # The Luminous portal: 2 | 3 | An alternative to xdg-desktop-portal-wlr for wlroots compositors. This project is a stand alone binary and does not depend on grim. 4 | `libwayshot` is used as the screencopy backend to enable screenshots. 5 | 6 | ![https://github.com/waycrate/xdg-desktop-portal-luminous/actions](https://github.com/waycrate/xdg-desktop-portal-luminous/actions/workflows/ci.yaml/badge.svg) 7 | 8 | # Exposed interfaces: 9 | 10 | 1. org.freedesktop.impl.portal.RemoteDesktop 11 | 1. org.freedesktop.impl.portal.ScreenCast 12 | 1. org.freedesktop.impl.portal.ScreenShot 13 | 1. org.freedesktop.impl.portal.Settings 14 | 15 | # Settings: 16 | 17 | Luminous is configured through the following auto hot-reloaded file: `~/.config/xdg-desktop-portal-luminous/config.toml`. 18 | 19 | ```toml 20 | color_scheme = "dark" # can also be "light" 21 | accent_color = "#880022" 22 | ``` 23 | 24 | # How to set priority of portal backend: 25 | 26 | The following file needs to be created `~/.config/xdg-desktop-portal/CURRENT_DESKTOP_NAME-portals.conf`. 27 | (eg: For the `sway` desktop, `sway-portals.conf` must exist.) 28 | 29 | Eg: 30 | ``` 31 | [preferred] 32 | default=luminous 33 | org.freedesktop.impl.portal.Settings=luminous;gtk 34 | ``` 35 | 36 | # Future goals: 37 | 38 | * Do not rely on slurp binary. We feel calling binaries is a hack to achieve some end goal, it is almost always better to programmatically invoke the given API. 39 | 40 | # Building: 41 | 42 | ```sh 43 | meson build 44 | ninja -C build install 45 | ``` 46 | 47 | # Requirements: 48 | 49 | Build time requirements are marked with `*`. 50 | 51 | 1. cargo * 52 | 1. libclang * 53 | 1. meson * 54 | 1. ninja * 55 | 1. pipewire * 56 | 1. pkg-config * 57 | 1. rustc * 58 | 1. xkbcommon * 59 | 1. slurp 60 | 1. wayland 61 | 1. wayland-protocols * 62 | -------------------------------------------------------------------------------- /_typos.toml: -------------------------------------------------------------------------------- 1 | [default.extend-words] 2 | datas = "datas" 3 | -------------------------------------------------------------------------------- /contrib/PKGBUILD/PKGBUILD: -------------------------------------------------------------------------------- 1 | _pkgname=xdg-desktop-portal-luminous 2 | pkgname="${_pkgname}-git" 3 | pkgver=r27.b3a3e8a 4 | pkgrel=1 5 | url='https://github.com/waycrate/xdg-desktop-portal-luminous' 6 | pkgdesc='xdg-desktop-portal backend for wlroots based compositors, providing screenshot and screencast' 7 | arch=('x86_64' 'aarch64') 8 | license=('BSD-2-Clause') 9 | depends=('xdg-desktop-portal' 'slurp') 10 | provides=("xdg-desktop-portal-impl") 11 | makedepends=('git' 'ninja' 'meson' 'rust') 12 | source=("${_pkgname}::git+${url}.git") 13 | sha256sums=('SKIP') 14 | options+=(!lto) 15 | 16 | build() { 17 | cd "${_pkgname}" 18 | meson setup build \ 19 | -Dprefix=/usr \ 20 | -Dlibexecdir=lib \ 21 | -Dbuildtype=release 22 | ninja -C build 23 | } 24 | 25 | package() { 26 | cd "${_pkgname}" 27 | DESTDIR="${pkgdir}" ninja -C build install 28 | } 29 | 30 | pkgver() { 31 | cd "${_pkgname}" 32 | printf "r%s.%s" "$(git rev-list --count HEAD)" "$(git rev-parse --short=7 HEAD)" 33 | } 34 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "flake-compat": { 4 | "locked": { 5 | "lastModified": 1733328505, 6 | "narHash": "sha256-NeCCThCEP3eCl2l/+27kNNK7QrwZB1IJCrXfrbv5oqU=", 7 | "rev": "ff81ac966bb2cae68946d5ed5fc4994f96d0ffec", 8 | "revCount": 69, 9 | "type": "tarball", 10 | "url": "https://api.flakehub.com/f/pinned/edolstra/flake-compat/1.1.0/01948eb7-9cba-704f-bbf3-3fa956735b52/source.tar.gz" 11 | }, 12 | "original": { 13 | "type": "tarball", 14 | "url": "https://flakehub.com/f/edolstra/flake-compat/1.tar.gz" 15 | } 16 | }, 17 | "nixpkgs": { 18 | "locked": { 19 | "lastModified": 1743583204, 20 | "narHash": "sha256-F7n4+KOIfWrwoQjXrL2wD9RhFYLs2/GGe/MQY1sSdlE=", 21 | "owner": "NixOS", 22 | "repo": "nixpkgs", 23 | "rev": "2c8d3f48d33929642c1c12cd243df4cc7d2ce434", 24 | "type": "github" 25 | }, 26 | "original": { 27 | "owner": "NixOS", 28 | "ref": "nixos-unstable", 29 | "repo": "nixpkgs", 30 | "type": "github" 31 | } 32 | }, 33 | "root": { 34 | "inputs": { 35 | "flake-compat": "flake-compat", 36 | "nixpkgs": "nixpkgs" 37 | } 38 | } 39 | }, 40 | "root": "root", 41 | "version": 7 42 | } 43 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "xdg-desktop-portal-luminous devel and build"; 3 | 4 | # Unstable required until Rust 1.85 (2024 edition) is on stable 5 | inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; 6 | 7 | # shell.nix compatibility 8 | inputs.flake-compat.url = "https://flakehub.com/f/edolstra/flake-compat/1.tar.gz"; 9 | 10 | outputs = { self, nixpkgs, ... }: 11 | let 12 | # System types to support. 13 | targetSystems = [ "x86_64-linux" "aarch64-linux" ]; 14 | 15 | # Helper function to generate an attrset '{ x86_64-linux = f "x86_64-linux"; ... }'. 16 | forAllSystems = nixpkgs.lib.genAttrs targetSystems; 17 | in { 18 | packages = forAllSystems (system: 19 | let 20 | pkgs = nixpkgs.legacyPackages.${system}; 21 | in 22 | { 23 | # rustPlatform.buildRustPackage is not used because we build with Meson+Ninja 24 | default = pkgs.stdenv.mkDerivation rec { 25 | pname = "xdg-desktop-portal-luminous"; 26 | version = (builtins.fromTOML (builtins.readFile ./Cargo.toml)).workspace.package.version; 27 | 28 | src = ./.; 29 | 30 | nativeBuildInputs = with pkgs; [ 31 | rustPlatform.cargoSetupHook # Make Cargo find cargoDeps 32 | rustPlatform.bindgenHook 33 | cargo rustc 34 | 35 | meson 36 | ninja 37 | pkg-config 38 | ]; 39 | 40 | cargoDeps = pkgs.rustPlatform.importCargoLock { 41 | lockFile = ./Cargo.lock; 42 | }; 43 | 44 | buildInputs = with pkgs; [ 45 | pipewire 46 | libxkbcommon 47 | pango 48 | cairo 49 | ]; 50 | 51 | meta = with nixpkgs.lib; { 52 | description = "An alternative to xdg-desktop-portal-wlr for wlroots compositors"; 53 | homepage = "https://github.com/waycrate/xdg-desktop-portal-luminous"; 54 | }; 55 | }; 56 | } 57 | ); 58 | devShells = forAllSystems (system: 59 | let 60 | pkgs = nixpkgs.legacyPackages.${system}; 61 | in 62 | { 63 | default = pkgs.mkShell { 64 | strictDeps = true; 65 | RUST_SRC_PATH = "${pkgs.rustPlatform.rustLibSrc}"; 66 | nativeBuildInputs = with pkgs; [ 67 | cargo 68 | rustc 69 | rustPlatform.bindgenHook 70 | pkg-config 71 | meson 72 | ninja 73 | 74 | rustfmt 75 | clippy 76 | rust-analyzer 77 | ]; 78 | 79 | inherit (self.packages.${system}.default) buildInputs; 80 | }; 81 | } 82 | ); 83 | }; 84 | } 85 | -------------------------------------------------------------------------------- /libs/accessdialog/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "accessdialog" 3 | version.workspace = true 4 | edition.workspace = true 5 | repository.workspace = true 6 | license.workspace = true 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | slint.workspace = true 11 | 12 | [build-dependencies] 13 | slint-build.workspace = true 14 | -------------------------------------------------------------------------------- /libs/accessdialog/build.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | slint_build::compile("ui/dialog.slint").unwrap(); 3 | } 4 | -------------------------------------------------------------------------------- /libs/accessdialog/src/lib.rs: -------------------------------------------------------------------------------- 1 | slint::include_modules!(); 2 | 3 | use std::sync::mpsc; 4 | 5 | thread_local! { 6 | static GLOBAL_SELECT_UI : AppWindow = AppWindow::new().expect("Should can be inited"); 7 | } 8 | 9 | fn init_slots(ui: &AppWindow, sender: mpsc::Sender) { 10 | let global = ConfirmSlots::get(ui); 11 | let send_confirm = sender.clone(); 12 | global.on_Reject(move || { 13 | let _ = sender.send(false); 14 | let _ = slint::quit_event_loop(); 15 | }); 16 | global.on_Confirm(move || { 17 | let _ = send_confirm.send(true); 18 | let _ = slint::quit_event_loop(); 19 | }); 20 | } 21 | 22 | pub fn confirmgui(title: String, information: String) -> bool { 23 | GLOBAL_SELECT_UI.with(|ui| { 24 | ui.set_init_title(title.into()); 25 | ui.set_information(information.into()); 26 | let (sender, receiver) = mpsc::channel(); 27 | init_slots(ui, sender); 28 | ui.run().expect("Cannot run the ui"); 29 | receiver 30 | .recv_timeout(std::time::Duration::from_nanos(300)) 31 | .unwrap_or_default() 32 | }) 33 | } 34 | -------------------------------------------------------------------------------- /libs/accessdialog/ui/dialog.slint: -------------------------------------------------------------------------------- 1 | import { Button, VerticalBox, HorizontalBox} from "std-widgets.slint"; 2 | 3 | export global ConfirmSlots { 4 | callback Confirm(); 5 | callback Reject(); 6 | } 7 | 8 | export component AppWindow inherits Window { 9 | in-out property information : "No information"; 10 | in-out property init_title : "AccessWindow"; 11 | title: init_title; 12 | width: 400px; 13 | height: 100px; 14 | VerticalBox { 15 | Text { 16 | horizontal-alignment: center; 17 | text: information; 18 | font-size: 25px; 19 | } 20 | HorizontalBox { 21 | alignment: center; 22 | Button { 23 | height: 30px; 24 | text: "Reject"; 25 | clicked => { ConfirmSlots.Reject() } 26 | } 27 | Button { 28 | height: 30px; 29 | text: "Accept"; 30 | clicked => { ConfirmSlots.Confirm() } 31 | } 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /libs/screenshotdialog/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "screenshotdialog" 3 | version.workspace = true 4 | edition.workspace = true 5 | repository.workspace = true 6 | license.workspace = true 7 | 8 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 9 | 10 | [dependencies] 11 | slint.workspace = true 12 | 13 | [build-dependencies] 14 | slint-build.workspace = true 15 | -------------------------------------------------------------------------------- /libs/screenshotdialog/build.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | slint_build::compile("ui/selectwindow.slint").unwrap(); 3 | } 4 | -------------------------------------------------------------------------------- /libs/screenshotdialog/src/lib.rs: -------------------------------------------------------------------------------- 1 | use std::rc::Rc; 2 | 3 | use slint::VecModel; 4 | slint::include_modules!(); 5 | 6 | use std::sync::mpsc; 7 | 8 | thread_local! { 9 | static GLOBAL_SELECT_UI : AppWindow = AppWindow::new().expect("Should can be init"); 10 | } 11 | 12 | #[derive(Debug)] 13 | pub enum SlintSelection { 14 | GlobalScreen { showcursor: bool }, 15 | Slurp, 16 | Canceled, 17 | Selection { showcursor: bool, index: i32 }, 18 | } 19 | 20 | fn init_slots(ui: &AppWindow, sender: mpsc::Sender) { 21 | let global = SelectSlots::get(ui); 22 | let sender_slurp = sender.clone(); 23 | global.on_useSlurp(move || { 24 | let _ = sender_slurp.send(SlintSelection::Slurp); 25 | let _ = slint::quit_event_loop(); 26 | }); 27 | let sender_global = sender.clone(); 28 | global.on_useGlobal(move |showcursor| { 29 | let _ = sender_global.send(SlintSelection::GlobalScreen { showcursor }); 30 | let _ = slint::quit_event_loop(); 31 | }); 32 | 33 | global.on_selectScreen(move |index, showcursor| { 34 | let _ = sender.send(SlintSelection::Selection { index, showcursor }); 35 | let _ = slint::quit_event_loop(); 36 | }); 37 | } 38 | 39 | pub fn selectgui(screen: Vec) -> SlintSelection { 40 | GLOBAL_SELECT_UI.with(|ui| { 41 | ui.set_infos(Rc::new(VecModel::from(screen)).into()); 42 | let (sender, receiver) = mpsc::channel(); 43 | init_slots(ui, sender); 44 | ui.run().expect("Cannot run the ui"); 45 | if let Ok(message) = receiver.recv_timeout(std::time::Duration::from_nanos(300)) { 46 | message 47 | } else { 48 | SlintSelection::Canceled 49 | } 50 | }) 51 | } 52 | -------------------------------------------------------------------------------- /libs/screenshotdialog/ui/selectwindow.slint: -------------------------------------------------------------------------------- 1 | import { Button, VerticalBox , HorizontalBox, GridBox, GroupBox, ListView, CheckBox} from "std-widgets.slint"; 2 | 3 | export struct ScreenInfo { 4 | name: string, 5 | description: string 6 | } 7 | 8 | export global SelectSlots { 9 | callback useSlurp(); 10 | callback selectScreen(int, bool); 11 | callback useGlobal(bool); 12 | } 13 | 14 | export component AppWindow inherits Window { 15 | in property <[ScreenInfo]> infos: []; 16 | in-out property showcursor : false ; 17 | no-frame: true; 18 | width: 1000px; 19 | height: 700px; 20 | VerticalBox { 21 | HorizontalBox { 22 | Button { 23 | text: "Global Screen"; 24 | height: 200px; 25 | clicked => { SelectSlots.useGlobal(showcursor) } 26 | } 27 | Button { 28 | text: "Slurp"; 29 | height: 200px; 30 | clicked => { SelectSlots.useSlurp() } 31 | } 32 | } 33 | GroupBox { 34 | title: "select a screen"; 35 | ListView { 36 | min-height : 200px; 37 | for data[index] in infos: Button{ 38 | text: "\{data.name} : \{data.description}"; 39 | height: 80px; 40 | clicked => { SelectSlots.selectScreen(index,showcursor) } 41 | } 42 | } 43 | } 44 | HorizontalBox { 45 | alignment: end; 46 | CheckBox { 47 | text: "show cursor?"; 48 | checked <=> showcursor; 49 | } 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /meson.build: -------------------------------------------------------------------------------- 1 | project( 2 | 'xdg-desktop-portal-luminous', 3 | 'rust', 4 | version: '0.1.9', 5 | meson_version: '>= 1.1.0', 6 | ) 7 | 8 | cargo = find_program('cargo', version: '>= 1.85') 9 | 10 | find_program('rustc', version: '>= 1.85') 11 | 12 | if get_option('debug') 13 | command = [ 14 | cargo, 15 | 'build', 16 | '&&', 17 | 'cp', 18 | meson.global_source_root() / 'target' / 'debug' / meson.project_name(), 19 | '@OUTPUT@', 20 | ] 21 | else 22 | command = [ 23 | cargo, 24 | 'build', 25 | '--release', '&&', 26 | 'cp', 27 | meson.global_source_root() / 'target' / 'release' / meson.project_name(), 28 | '@OUTPUT@', 29 | ] 30 | endif 31 | 32 | prefix = get_option('prefix') 33 | 34 | xdg_install_dir = prefix / get_option('libexecdir') 35 | 36 | portal_dir = prefix / get_option('datadir') / 'xdg-desktop-portal' / 'portals' 37 | 38 | dbus1_dir = prefix / get_option('datadir') / 'dbus-1' / 'services' 39 | 40 | system_dir = prefix / get_option('libdir') / 'systemd' / 'user' 41 | global_conf = configuration_data() 42 | 43 | global_conf.set('xdg_install_dir', xdg_install_dir) 44 | 45 | configure_file( 46 | input: 'misc/xdg-desktop-portal-luminous.service.in', 47 | output: 'xdg-desktop-portal-luminous.service', 48 | configuration: global_conf, 49 | ) 50 | 51 | configure_file( 52 | input: 'misc/org.freedesktop.impl.portal.desktop.luminous.service.in', 53 | output: 'org.freedesktop.impl.portal.desktop.luminous.service', 54 | configuration: global_conf, 55 | ) 56 | 57 | custom_target( 58 | 'xdg-desktop-portal-luminous', 59 | output: 'xdg-desktop-portal-luminous', 60 | build_by_default: true, 61 | install: true, 62 | install_dir: xdg_install_dir, 63 | console: true, 64 | command: command, 65 | ) 66 | 67 | install_data('misc/luminous.portal', install_dir: portal_dir) 68 | 69 | install_data( 70 | meson.project_build_root() / 'org.freedesktop.impl.portal.desktop.luminous.service', 71 | install_dir: dbus1_dir, 72 | ) 73 | 74 | install_data( 75 | meson.project_build_root() / 'xdg-desktop-portal-luminous.service', 76 | install_dir: system_dir, 77 | ) 78 | -------------------------------------------------------------------------------- /misc/luminous.portal: -------------------------------------------------------------------------------- 1 | [portal] 2 | DBusName=org.freedesktop.impl.portal.desktop.luminous 3 | Interfaces=org.freedesktop.impl.portal.Screenshot;org.freedesktop.impl.portal.ScreenCast;org.freedesktop.impl.portal.RemoteDesktop;org.freedesktop.impl.portal.Settings;org.freedesktop.impl.portal.Access; 4 | -------------------------------------------------------------------------------- /misc/org.freedesktop.impl.portal.desktop.luminous.service.in: -------------------------------------------------------------------------------- 1 | [D-BUS Service] 2 | Name=org.freedesktop.impl.portal.desktop.luminous 3 | Exec=@xdg_install_dir@/xdg-desktop-portal-luminous 4 | SystemdService=xdg-desktop-portal-luminous.service 5 | -------------------------------------------------------------------------------- /misc/xdg-desktop-portal-luminous.service.in: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Xdg Desktop Portal backend for wlroots-based compositors written with rust 3 | PartOf=graphical-session.target 4 | After=graphical-session.target 5 | ConditionEnvironment=WAYLAND_DISPLAY 6 | 7 | [Service] 8 | Type=dbus 9 | BusName=org.freedesktop.impl.portal.desktop.luminous 10 | ExecStart=@xdg_install_dir@/xdg-desktop-portal-luminous 11 | Restart=on-failure 12 | -------------------------------------------------------------------------------- /shell.nix: -------------------------------------------------------------------------------- 1 | (import 2 | ( 3 | let 4 | lock = builtins.fromJSON (builtins.readFile ./flake.lock); 5 | nodeName = lock.nodes.root.inputs.flake-compat; 6 | in 7 | fetchTarball { 8 | url = lock.nodes.${nodeName}.locked.url or "https://github.com/edolstra/flake-compat/archive/${lock.nodes.${nodeName}.locked.rev}.tar.gz"; 9 | sha256 = lock.nodes.${nodeName}.locked.narHash; 10 | } 11 | ) 12 | { src = ./.; } 13 | ).shellNix 14 | -------------------------------------------------------------------------------- /src/access.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use zbus::{ 4 | fdo, interface, 5 | zvariant::{ObjectPath, OwnedValue, Type, as_value::optional}, 6 | }; 7 | 8 | use serde::{Deserialize, Serialize}; 9 | 10 | use crate::PortalResponse; 11 | #[derive(Type, Debug, Default, Deserialize, Serialize)] 12 | /// Specified options for a [`Screencast::select_sources`] request. 13 | #[zvariant(signature = "dict")] 14 | pub struct AccessOption { 15 | /// A string that will be used as the last element of the handle. 16 | /// What types of content to record. 17 | #[serde(with = "optional", skip_serializing_if = "Option::is_none", default)] 18 | pub modal: Option, 19 | /// Whether to allow selecting multiple sources. 20 | #[serde(with = "optional", skip_serializing_if = "Option::is_none", default)] 21 | pub deny_label: Option, 22 | /// Determines how the cursor will be drawn in the screen cast stream. 23 | #[serde(with = "optional", skip_serializing_if = "Option::is_none", default)] 24 | pub grant_label: Option, 25 | #[serde(with = "optional", skip_serializing_if = "Option::is_none", default)] 26 | pub icon: Option, 27 | #[serde(with = "optional", skip_serializing_if = "Option::is_none", default)] 28 | pub choices: Option>, 29 | } 30 | 31 | #[derive(Clone, Serialize, Deserialize, Type, Debug)] 32 | /// Presents the user with a choice to select from or as a checkbox. 33 | pub struct Choice(String, String, Vec<(String, String)>, String); 34 | 35 | #[derive(Debug)] 36 | pub struct AccessBackend; 37 | 38 | #[interface(name = "org.freedesktop.impl.portal.Access")] 39 | impl AccessBackend { 40 | #[allow(clippy::too_many_arguments)] 41 | async fn access_dialog( 42 | &self, 43 | _request_handle: ObjectPath<'_>, 44 | _app_id: String, 45 | _parent_window: String, 46 | title: String, 47 | sub_title: String, 48 | _body: String, 49 | _options: AccessOption, 50 | ) -> fdo::Result>> { 51 | if accessdialog::confirmgui(title, sub_title) { 52 | Ok(PortalResponse::Success(HashMap::new())) 53 | } else { 54 | Ok(PortalResponse::Cancelled) 55 | } 56 | } 57 | // add code here 58 | } 59 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | mod access; 2 | mod remotedesktop; 3 | mod request; 4 | mod screencast; 5 | mod screenshot; 6 | mod session; 7 | mod settings; 8 | mod utils; 9 | 10 | use access::AccessBackend; 11 | use remotedesktop::RemoteDesktopBackend; 12 | use screencast::ScreenCastBackend; 13 | use screenshot::ScreenShotBackend; 14 | use settings::{AccentColor, SETTING_CONFIG, SettingsBackend, SettingsConfig}; 15 | 16 | use std::collections::HashMap; 17 | use std::future::pending; 18 | use zbus::{Connection, connection, object_server::SignalEmitter, zvariant}; 19 | 20 | use futures::{ 21 | SinkExt, StreamExt, 22 | channel::mpsc::{Receiver, channel}, 23 | }; 24 | use notify::{Config, Event, EventKind, RecommendedWatcher, RecursiveMode, Watcher}; 25 | use std::path::Path; 26 | 27 | mod pipewirethread; 28 | use std::sync::OnceLock; 29 | 30 | const PORTAL_RESPONSE_SUCCESS: u32 = 0; 31 | const PORTAL_RESPONSE_CANCELLED: u32 = 1; 32 | const PORTAL_RESPONSE_OTHER: u32 = 2; 33 | 34 | static SESSION: OnceLock = OnceLock::new(); 35 | 36 | async fn get_connection() -> zbus::Connection { 37 | if let Some(cnx) = SESSION.get() { 38 | cnx.clone() 39 | } else { 40 | panic!("Cannot get cnx"); 41 | } 42 | } 43 | 44 | async fn set_connection(connection: Connection) { 45 | SESSION.set(connection).expect("Cannot set a OnceLock"); 46 | } 47 | 48 | #[derive(zvariant::Type)] 49 | #[zvariant(signature = "(ua{sv})")] 50 | enum PortalResponse { 51 | Success(T), 52 | Cancelled, 53 | Other, 54 | } 55 | 56 | impl serde::Serialize for PortalResponse { 57 | fn serialize(&self, serializer: S) -> Result { 58 | match self { 59 | Self::Success(res) => (PORTAL_RESPONSE_SUCCESS, res).serialize(serializer), 60 | Self::Cancelled => ( 61 | PORTAL_RESPONSE_CANCELLED, 62 | HashMap::::new(), 63 | ) 64 | .serialize(serializer), 65 | Self::Other => ( 66 | PORTAL_RESPONSE_OTHER, 67 | HashMap::::new(), 68 | ) 69 | .serialize(serializer), 70 | } 71 | } 72 | } 73 | 74 | fn async_watcher() -> notify::Result<(RecommendedWatcher, Receiver>)> { 75 | let (mut tx, rx) = channel(1); 76 | 77 | // Automatically select the best implementation for your platform. 78 | // You can also access each implementation directly e.g. INotifyWatcher. 79 | let watcher = RecommendedWatcher::new( 80 | move |res| { 81 | futures::executor::block_on(async { 82 | tx.send(res).await.unwrap(); 83 | }) 84 | }, 85 | Config::default(), 86 | )?; 87 | 88 | Ok((watcher, rx)) 89 | } 90 | 91 | async fn async_watch>(path: P) -> notify::Result<()> { 92 | let connection = get_connection().await; 93 | let (mut watcher, mut rx) = async_watcher()?; 94 | 95 | let signal_context = 96 | SignalEmitter::new(&connection, "/org/freedesktop/portal/desktop").unwrap(); 97 | // Add a path to be watched. All files and directories at that path and 98 | // below will be monitored for changes. 99 | watcher.watch(path.as_ref(), RecursiveMode::Recursive)?; 100 | 101 | while let Some(res) = rx.next().await { 102 | match res { 103 | Ok(Event { 104 | kind: EventKind::Modify(_), 105 | .. 106 | }) 107 | | Ok(Event { 108 | kind: EventKind::Create(_), 109 | .. 110 | }) => { 111 | let mut config = SETTING_CONFIG.lock().await; 112 | *config = SettingsConfig::config_from_file(); 113 | let _ = SettingsBackend::setting_changed( 114 | &signal_context, 115 | "org.freedesktop.appearance".to_string(), 116 | "color-scheme".to_string(), 117 | config.get_color_scheme().into(), 118 | ) 119 | .await; 120 | let _ = SettingsBackend::setting_changed( 121 | &signal_context, 122 | "org.freedesktop.appearance".to_string(), 123 | "accent-color".to_string(), 124 | AccentColor::new(config.get_accent_color()) 125 | .try_into() 126 | .unwrap(), 127 | ) 128 | .await; 129 | } 130 | Err(e) => println!("watch error: {:?}", e), 131 | _ => {} 132 | } 133 | } 134 | 135 | Ok(()) 136 | } 137 | 138 | #[tokio::main] 139 | async fn main() -> anyhow::Result<()> { 140 | unsafe { std::env::set_var("RUST_LOG", "xdg-desktop-protal-luminous=info") } 141 | tracing_subscriber::fmt().init(); 142 | tracing::info!("luminous Start"); 143 | 144 | let conn = connection::Builder::session()? 145 | .name("org.freedesktop.impl.portal.desktop.luminous")? 146 | .serve_at("/org/freedesktop/portal/desktop", AccessBackend)? 147 | .serve_at("/org/freedesktop/portal/desktop", ScreenShotBackend)? 148 | .serve_at("/org/freedesktop/portal/desktop", ScreenCastBackend)? 149 | .serve_at("/org/freedesktop/portal/desktop", RemoteDesktopBackend)? 150 | .serve_at("/org/freedesktop/portal/desktop", SettingsBackend)? 151 | .build() 152 | .await?; 153 | 154 | set_connection(conn).await; 155 | tokio::spawn(async { 156 | let Ok(home) = std::env::var("HOME") else { 157 | return; 158 | }; 159 | let config_path = std::path::Path::new(home.as_str()) 160 | .join(".config") 161 | .join("xdg-desktop-portal-luminous"); 162 | if let Err(e) = async_watch(config_path).await { 163 | tracing::info!("Maybe file is not exist, error: {e}"); 164 | } 165 | }); 166 | 167 | pending::<()>().await; 168 | 169 | Ok(()) 170 | } 171 | -------------------------------------------------------------------------------- /src/pipewirethread.rs: -------------------------------------------------------------------------------- 1 | use libwayshot::CaptureRegion; 2 | use libwayshot::{WayshotConnection, reexport::WlOutput}; 3 | use pipewire::{ 4 | spa::{ 5 | self, 6 | pod::{self, deserialize::PodDeserializer, serialize::PodSerializer}, 7 | }, 8 | stream::StreamState, 9 | }; 10 | use rustix::fd::BorrowedFd; 11 | 12 | use std::{cell::RefCell, io, os::fd::IntoRawFd, rc::Rc, slice}; 13 | 14 | use tokio::sync::oneshot; 15 | 16 | pub struct ScreencastThread { 17 | node_id: u32, 18 | thread_stop_tx: pipewire::channel::Sender<()>, 19 | } 20 | 21 | impl ScreencastThread { 22 | pub async fn start_cast( 23 | overlay_cursor: bool, 24 | width: u32, 25 | height: u32, 26 | capture_region: Option, 27 | output: WlOutput, 28 | connection: WayshotConnection, 29 | ) -> anyhow::Result { 30 | let (tx, rx) = oneshot::channel(); 31 | let (thread_stop_tx, thread_stop_rx) = pipewire::channel::channel::<()>(); 32 | std::thread::spawn(move || { 33 | match start_stream( 34 | connection, 35 | overlay_cursor, 36 | width, 37 | height, 38 | capture_region, 39 | output, 40 | ) { 41 | Ok((loop_, listener, context, node_id_rx)) => { 42 | tx.send(Ok(node_id_rx)).unwrap(); 43 | let weak_loop = loop_.downgrade(); 44 | let _receiver = thread_stop_rx.attach(loop_.loop_(), move |()| { 45 | weak_loop.upgrade().unwrap().quit(); 46 | }); 47 | loop_.run(); 48 | // XXX fix segfault with opposite drop order 49 | drop(listener); 50 | drop(context); 51 | } 52 | Err(err) => tx.send(Err(err)).unwrap(), 53 | }; 54 | }); 55 | Ok(Self { 56 | node_id: rx.await??.await??, 57 | thread_stop_tx, 58 | }) 59 | } 60 | 61 | pub fn node_id(&self) -> u32 { 62 | self.node_id 63 | } 64 | 65 | pub fn stop(&self) { 66 | let _ = self.thread_stop_tx.send(()); 67 | } 68 | } 69 | 70 | type PipewireStreamResult = ( 71 | pipewire::main_loop::MainLoop, 72 | pipewire::stream::StreamListener<()>, 73 | pipewire::context::Context, 74 | oneshot::Receiver>, 75 | ); 76 | 77 | fn start_stream( 78 | connection: WayshotConnection, 79 | overlay_cursor: bool, 80 | width: u32, 81 | height: u32, 82 | capture_region: Option, 83 | output: WlOutput, 84 | ) -> Result { 85 | let loop_ = pipewire::main_loop::MainLoop::new(None).unwrap(); 86 | let context = pipewire::context::Context::new(&loop_).unwrap(); 87 | let core = context.connect(None).unwrap(); 88 | 89 | let name = "wayshot-screenshot"; // XXX randomize? 90 | 91 | let stream = pipewire::stream::Stream::new( 92 | &core, 93 | name, 94 | pipewire::properties::properties! { 95 | "media.class" => "Video/Source", 96 | "node.name" => "wayshot-screenshot", // XXX 97 | }, 98 | )?; 99 | 100 | let (node_id_tx, node_id_rx) = oneshot::channel(); 101 | let mut node_id_tx = Some(node_id_tx); 102 | let stream_cell: Rc>> = Rc::new(RefCell::new(None)); 103 | let stream_cell_clone = stream_cell.clone(); 104 | 105 | let listener = stream 106 | .add_local_listener_with_user_data(()) 107 | .state_changed(move |_, _, old, new| { 108 | tracing::info!("state-changed '{:?}' -> '{:?}'", old, new); 109 | match new { 110 | StreamState::Paused => { 111 | let stream = stream_cell_clone.borrow_mut(); 112 | let stream = stream.as_ref().unwrap(); 113 | if let Some(node_id_tx) = node_id_tx.take() { 114 | node_id_tx.send(Ok(stream.node_id())).unwrap(); 115 | } 116 | } 117 | StreamState::Error(e) => { 118 | tracing::error!("Error! : {e}"); 119 | } 120 | _ => {} 121 | } 122 | }) 123 | .param_changed(|_, _, id, pod| { 124 | if id != libspa_sys::SPA_PARAM_Format { 125 | return; 126 | } 127 | if let Some(pod) = pod { 128 | let value = PodDeserializer::deserialize_from::(pod.as_bytes()); 129 | tracing::info!("param-changed: {} {:?}", id, value); 130 | } 131 | }) 132 | .add_buffer(move |_, _, buffer| { 133 | let buf = unsafe { &mut *(*buffer).buffer }; 134 | let datas = unsafe { slice::from_raw_parts_mut(buf.datas, buf.n_datas as usize) }; 135 | for data in datas { 136 | let name = c"pipewire-screencopy"; 137 | let fd = rustix::fs::memfd_create(name, rustix::fs::MemfdFlags::CLOEXEC).unwrap(); 138 | rustix::fs::ftruncate(&fd, (width * height * 4) as _).unwrap(); 139 | 140 | data.type_ = libspa_sys::SPA_DATA_MemFd; 141 | data.flags = 0; 142 | data.fd = fd.into_raw_fd().into(); 143 | 144 | data.data = std::ptr::null_mut(); 145 | data.maxsize = width * height * 4; 146 | data.mapoffset = 0; 147 | let chunk = unsafe { &mut *data.chunk }; 148 | chunk.size = width * height * 4; 149 | chunk.offset = 0; 150 | chunk.stride = 4 * width as i32; 151 | } 152 | }) 153 | .remove_buffer(|_, _, buffer| { 154 | let buf = unsafe { &mut *(*buffer).buffer }; 155 | let datas = unsafe { slice::from_raw_parts_mut(buf.datas, buf.n_datas as usize) }; 156 | 157 | for data in datas { 158 | unsafe { rustix::io::close(data.fd as _) }; 159 | data.fd = -1; 160 | } 161 | }) 162 | .process(move |stream, ()| { 163 | if let Some(mut buffer) = stream.dequeue_buffer() { 164 | let datas = buffer.datas_mut(); 165 | let fd = unsafe { BorrowedFd::borrow_raw(datas[0].as_raw().fd as _) }; 166 | // TODO error 167 | connection 168 | .capture_output_frame_shm_fd(overlay_cursor as i32, &output, fd, capture_region) 169 | .unwrap(); 170 | } 171 | }) 172 | .register()?; 173 | 174 | let format = format(width, height); 175 | let buffers = buffers(width, height); 176 | 177 | let params = &mut [ 178 | pod::Pod::from_bytes(&format).unwrap(), 179 | pod::Pod::from_bytes(&buffers).unwrap(), 180 | ]; 181 | 182 | let flags = pipewire::stream::StreamFlags::ALLOC_BUFFERS; 183 | stream.connect(pipewire::spa::utils::Direction::Output, None, flags, params)?; 184 | 185 | *stream_cell.borrow_mut() = Some(stream); 186 | 187 | Ok((loop_, listener, context, node_id_rx)) 188 | } 189 | 190 | fn value_to_bytes(value: pod::Value) -> Vec { 191 | let mut bytes = Vec::new(); 192 | let mut cursor = io::Cursor::new(&mut bytes); 193 | PodSerializer::serialize(&mut cursor, &value).unwrap(); 194 | bytes 195 | } 196 | 197 | fn buffers(width: u32, height: u32) -> Vec { 198 | value_to_bytes(pod::Value::Object(pod::Object { 199 | type_: libspa_sys::SPA_TYPE_OBJECT_ParamBuffers, 200 | id: libspa_sys::SPA_PARAM_Buffers, 201 | properties: vec![ 202 | /* 203 | pod::Property { 204 | key: spa_sys::SPA_PARAM_BUFFERS_dataType, 205 | flags: pod::PropertyFlags::empty(), 206 | value: pod::Value::Choice(pod::ChoiceValue::Int(spa::utils::Choice( 207 | spa::utils::ChoiceFlags::empty(), 208 | spa::utils::ChoiceEnum::Flags { 209 | default: 1 << spa_sys::SPA_DATA_MemFd, 210 | flags: vec![], 211 | }, 212 | ))), 213 | }, 214 | */ 215 | pod::Property { 216 | key: libspa_sys::SPA_PARAM_BUFFERS_size, 217 | flags: pod::PropertyFlags::empty(), 218 | value: pod::Value::Int(width as i32 * height as i32 * 4), 219 | }, 220 | pod::Property { 221 | key: libspa_sys::SPA_PARAM_BUFFERS_stride, 222 | flags: pod::PropertyFlags::empty(), 223 | value: pod::Value::Int(width as i32 * 4), 224 | }, 225 | pod::Property { 226 | key: libspa_sys::SPA_PARAM_BUFFERS_align, 227 | flags: pod::PropertyFlags::empty(), 228 | value: pod::Value::Int(16), 229 | }, 230 | pod::Property { 231 | key: libspa_sys::SPA_PARAM_BUFFERS_blocks, 232 | flags: pod::PropertyFlags::empty(), 233 | value: pod::Value::Int(1), 234 | }, 235 | pod::Property { 236 | key: libspa_sys::SPA_PARAM_BUFFERS_buffers, 237 | flags: pod::PropertyFlags::empty(), 238 | value: pod::Value::Choice(pod::ChoiceValue::Int(spa::utils::Choice( 239 | spa::utils::ChoiceFlags::empty(), 240 | spa::utils::ChoiceEnum::Range { 241 | default: 4, 242 | min: 1, 243 | max: 32, 244 | }, 245 | ))), 246 | }, 247 | ], 248 | })) 249 | } 250 | 251 | #[allow(unused)] 252 | fn buffers2(width: u32, height: u32) -> Vec { 253 | value_to_bytes(pod::Value::Object(spa::pod::object!( 254 | spa::utils::SpaTypes::ObjectParamBuffers, 255 | spa::param::ParamType::Buffers, 256 | ))) 257 | } 258 | 259 | fn format(width: u32, height: u32) -> Vec { 260 | value_to_bytes(pod::Value::Object(spa::pod::object!( 261 | spa::utils::SpaTypes::ObjectParamFormat, 262 | spa::param::ParamType::EnumFormat, 263 | spa::pod::property!( 264 | spa::param::format::FormatProperties::MediaType, 265 | Id, 266 | spa::param::format::MediaType::Video 267 | ), 268 | spa::pod::property!( 269 | spa::param::format::FormatProperties::MediaSubtype, 270 | Id, 271 | spa::param::format::MediaSubtype::Raw 272 | ), 273 | spa::pod::property!( 274 | spa::param::format::FormatProperties::VideoFormat, 275 | Choice, 276 | Enum, 277 | Id, 278 | spa::param::video::VideoFormat::RGBA, 279 | spa::param::video::VideoFormat::RGBx, 280 | spa::param::video::VideoFormat::RGB8P, 281 | spa::param::video::VideoFormat::BGR, 282 | spa::param::video::VideoFormat::YUY2, 283 | ), 284 | // XXX modifiers 285 | spa::pod::property!( 286 | spa::param::format::FormatProperties::VideoSize, 287 | Choice, 288 | Range, 289 | Rectangle, 290 | spa::utils::Rectangle { width, height }, 291 | spa::utils::Rectangle { width, height }, 292 | spa::utils::Rectangle { width, height } 293 | ), 294 | spa::pod::property!( 295 | spa::param::format::FormatProperties::VideoFramerate, 296 | Choice, 297 | Range, 298 | Fraction, 299 | spa::utils::Fraction { num: 60, denom: 1 }, 300 | spa::utils::Fraction { num: 60, denom: 1 }, 301 | spa::utils::Fraction { num: 60, denom: 1 } 302 | ), 303 | // TODO max framerate 304 | ))) 305 | } 306 | -------------------------------------------------------------------------------- /src/remotedesktop.rs: -------------------------------------------------------------------------------- 1 | mod dispatch; 2 | mod remote_thread; 3 | mod state; 4 | 5 | use libwaysip::SelectionType; 6 | use remote_thread::RemoteControl; 7 | 8 | use std::collections::HashMap; 9 | 10 | use enumflags2::BitFlags; 11 | use zbus::interface; 12 | 13 | use zbus::zvariant::{ 14 | ObjectPath, OwnedValue, Type, Value, 15 | as_value::{self, optional}, 16 | }; 17 | 18 | use serde::{Deserialize, Serialize}; 19 | 20 | use std::sync::{Arc, LazyLock}; 21 | use tokio::sync::Mutex; 22 | 23 | use crate::pipewirethread::ScreencastThread; 24 | use crate::request::RequestInterface; 25 | use crate::session::{ 26 | DeviceType, PersistMode, SESSIONS, Session, SessionType, SourceType, append_session, 27 | }; 28 | 29 | use crate::PortalResponse; 30 | 31 | use self::remote_thread::KeyOrPointerRequest; 32 | 33 | #[derive(Type, Debug, Default, Serialize, Deserialize)] 34 | /// Specified options for a [`Screencast::create_session`] request. 35 | #[zvariant(signature = "dict")] 36 | struct SessionCreateResult { 37 | #[serde(with = "as_value")] 38 | handle_token: String, 39 | } 40 | 41 | #[derive(Clone, Serialize, Deserialize, Type, Default, Debug)] 42 | /// A PipeWire stream. 43 | pub struct Stream(u32, StreamProperties); 44 | 45 | #[derive(Clone, Default, Type, Debug, Serialize, Deserialize)] 46 | /// The stream properties. 47 | #[zvariant(signature = "dict")] 48 | struct StreamProperties { 49 | #[serde(with = "optional", skip_serializing_if = "Option::is_none", default)] 50 | id: Option, 51 | #[serde(with = "optional", skip_serializing_if = "Option::is_none", default)] 52 | position: Option<(i32, i32)>, 53 | #[serde(with = "as_value")] 54 | size: (i32, i32), 55 | #[serde(with = "as_value")] 56 | source_type: SourceType, 57 | } 58 | 59 | // TODO: this is copy from ashpd, but the dict is a little different from xdg_desktop_portal 60 | #[derive(Clone, Default, Debug, Type, Serialize, Deserialize)] 61 | #[zvariant(signature = "dict")] 62 | struct RemoteStartReturnValue { 63 | #[serde(with = "as_value")] 64 | streams: Vec, 65 | #[serde(with = "as_value")] 66 | devices: BitFlags, 67 | #[serde(with = "as_value")] 68 | clipboard_enabled: bool, 69 | #[serde(with = "as_value")] 70 | screen_share_enabled: bool, 71 | } 72 | 73 | #[derive(Type, Debug, Default, Deserialize, Serialize)] 74 | /// Specified options for a [`RemoteDesktop::select_devices`] request. 75 | #[zvariant(signature = "dict")] 76 | pub struct SelectDevicesOptions { 77 | /// A string that will be used as the last element of the handle. 78 | /// The device types to request remote controlling of. Default is all. 79 | #[serde(with = "optional", skip_serializing_if = "Option::is_none", default)] 80 | pub types: Option>, 81 | #[serde(with = "optional", skip_serializing_if = "Option::is_none", default)] 82 | pub restore_token: Option, 83 | #[serde(with = "optional", skip_serializing_if = "Option::is_none", default)] 84 | pub persist_mode: Option, 85 | } 86 | 87 | pub struct RemoteSessionData { 88 | session_handle: String, 89 | cast_thread: ScreencastThread, 90 | remote_control: RemoteControl, 91 | } 92 | 93 | pub static REMOTE_SESSIONS: LazyLock>>> = 94 | LazyLock::new(|| Arc::new(Mutex::new(Vec::new()))); 95 | 96 | pub async fn append_remote_session(session: RemoteSessionData) { 97 | let mut sessions = REMOTE_SESSIONS.lock().await; 98 | sessions.push(session) 99 | } 100 | 101 | pub async fn remove_remote_session(path: &str) { 102 | let mut sessions = REMOTE_SESSIONS.lock().await; 103 | let Some(index) = sessions 104 | .iter() 105 | .position(|the_session| the_session.session_handle == path) 106 | else { 107 | return; 108 | }; 109 | sessions[index].cast_thread.stop(); 110 | sessions[index].remote_control.stop(); 111 | tracing::info!("session {} is stopped", sessions[index].session_handle); 112 | sessions.remove(index); 113 | } 114 | 115 | pub struct RemoteDesktopBackend; 116 | 117 | #[interface(name = "org.freedesktop.impl.portal.RemoteDesktop")] 118 | impl RemoteDesktopBackend { 119 | #[zbus(property, name = "version")] 120 | fn version(&self) -> u32 { 121 | 2 122 | } 123 | 124 | #[zbus(property)] 125 | fn available_device_types(&self) -> u32 { 126 | (DeviceType::Keyboard | DeviceType::Pointer).bits() 127 | } 128 | 129 | async fn create_session( 130 | &self, 131 | request_handle: ObjectPath<'_>, 132 | session_handle: ObjectPath<'_>, 133 | app_id: String, 134 | _options: HashMap>, 135 | #[zbus(object_server)] server: &zbus::ObjectServer, 136 | ) -> zbus::fdo::Result> { 137 | tracing::info!( 138 | "Start shot: path :{}, appid: {}", 139 | request_handle.as_str(), 140 | app_id 141 | ); 142 | server 143 | .at( 144 | request_handle.clone(), 145 | RequestInterface { 146 | handle_path: request_handle.clone().into(), 147 | }, 148 | ) 149 | .await?; 150 | let current_session = Session::new(session_handle.clone(), SessionType::Remote); 151 | append_session(current_session.clone()).await; 152 | server.at(session_handle.clone(), current_session).await?; 153 | Ok(PortalResponse::Success(SessionCreateResult { 154 | handle_token: session_handle.to_string(), 155 | })) 156 | } 157 | 158 | async fn select_devices( 159 | &self, 160 | _request_handle: ObjectPath<'_>, 161 | session_handle: ObjectPath<'_>, 162 | _app_id: String, 163 | options: SelectDevicesOptions, 164 | ) -> zbus::fdo::Result>> { 165 | let mut locked_sessions = SESSIONS.lock().await; 166 | let Some(index) = locked_sessions 167 | .iter() 168 | .position(|this_session| this_session.handle_path == session_handle.clone().into()) 169 | else { 170 | tracing::warn!("No session is created or it is removed"); 171 | return Ok(PortalResponse::Other); 172 | }; 173 | if locked_sessions[index].session_type != SessionType::Remote { 174 | return Ok(PortalResponse::Other); 175 | } 176 | locked_sessions[index].set_remote_options(options); 177 | Ok(PortalResponse::Success(HashMap::new())) 178 | } 179 | 180 | async fn start( 181 | &self, 182 | _request_handle: ObjectPath<'_>, 183 | session_handle: ObjectPath<'_>, 184 | _app_id: String, 185 | _parent_window: String, 186 | _options: HashMap>, 187 | ) -> zbus::fdo::Result> { 188 | let locked_sessions = SESSIONS.lock().await; 189 | let Some(index) = locked_sessions 190 | .iter() 191 | .position(|this_session| this_session.handle_path == session_handle.clone().into()) 192 | else { 193 | tracing::warn!("No session is created or it is removed"); 194 | return Ok(PortalResponse::Other); 195 | }; 196 | 197 | let current_session = locked_sessions[index].clone(); 198 | if current_session.session_type != SessionType::Remote { 199 | return Ok(PortalResponse::Other); 200 | } 201 | let device_type = current_session.device_type; 202 | drop(locked_sessions); 203 | 204 | let remote_sessions = REMOTE_SESSIONS.lock().await; 205 | if let Some(session) = remote_sessions 206 | .iter() 207 | .find(|session| session.session_handle == session_handle.to_string()) 208 | { 209 | return Ok(PortalResponse::Success(RemoteStartReturnValue { 210 | streams: vec![Stream( 211 | session.cast_thread.node_id(), 212 | StreamProperties::default(), 213 | )], 214 | devices: device_type, 215 | ..Default::default() 216 | })); 217 | } 218 | drop(remote_sessions); 219 | 220 | let show_cursor = current_session.cursor_mode.show_cursor(); 221 | let connection = libwayshot::WayshotConnection::new().unwrap(); 222 | let info = match libwaysip::get_area( 223 | Some(libwaysip::WaysipConnection { 224 | connection: &connection.conn, 225 | globals: &connection.globals, 226 | }), 227 | SelectionType::Screen, 228 | ) { 229 | Ok(Some(info)) => info, 230 | Ok(None) => return Err(zbus::Error::Failure("You cancel it".to_string()).into()), 231 | Err(e) => return Err(zbus::Error::Failure(format!("wayland error, {e}")).into()), 232 | }; 233 | 234 | use libwaysip::Size; 235 | let screen_info = info.screen_info; 236 | 237 | let Size { width, height } = screen_info.get_wloutput_size(); 238 | 239 | tracing::info!("{width}, {height}"); 240 | let output = screen_info.wl_output; 241 | 242 | let cast_thread = ScreencastThread::start_cast( 243 | show_cursor, 244 | width as u32, 245 | height as u32, 246 | None, 247 | output, 248 | connection, 249 | ) 250 | .await 251 | .map_err(|e| zbus::Error::Failure(format!("cannot start pipewire stream, error: {e}")))?; 252 | 253 | let remote_control = RemoteControl::init(); 254 | let node_id = cast_thread.node_id(); 255 | 256 | append_remote_session(RemoteSessionData { 257 | session_handle: session_handle.to_string(), 258 | cast_thread, 259 | remote_control, 260 | }) 261 | .await; 262 | 263 | Ok(PortalResponse::Success(RemoteStartReturnValue { 264 | streams: vec![Stream( 265 | node_id, 266 | StreamProperties { 267 | size: (width, height), 268 | source_type: SourceType::Monitor, 269 | ..Default::default() 270 | }, 271 | )], 272 | devices: device_type, 273 | screen_share_enabled: true, 274 | ..Default::default() 275 | })) 276 | } 277 | 278 | // keyboard and else 279 | async fn notify_pointer_motion( 280 | &self, 281 | session_handle: ObjectPath<'_>, 282 | _options: HashMap>, 283 | dx: f64, 284 | dy: f64, 285 | ) -> zbus::fdo::Result<()> { 286 | let remote_sessions = REMOTE_SESSIONS.lock().await; 287 | let Some(session) = remote_sessions 288 | .iter() 289 | .find(|session| session.session_handle == session_handle.to_string()) 290 | else { 291 | return Ok(()); 292 | }; 293 | let remote_control = &session.remote_control; 294 | remote_control 295 | .sender 296 | .send(KeyOrPointerRequest::PointerMotion { dx, dy }) 297 | .map_err(|_| zbus::Error::Failure("Send failed".to_string()))?; 298 | Ok(()) 299 | } 300 | 301 | async fn notify_pointer_motion_absolute( 302 | &self, 303 | session_handle: ObjectPath<'_>, 304 | _options: HashMap>, 305 | _steam: u32, 306 | x: f64, 307 | y: f64, 308 | ) -> zbus::fdo::Result<()> { 309 | let remote_sessions = REMOTE_SESSIONS.lock().await; 310 | let Some(session) = remote_sessions 311 | .iter() 312 | .find(|session| session.session_handle == session_handle.to_string()) 313 | else { 314 | return Ok(()); 315 | }; 316 | let remote_control = &session.remote_control; 317 | remote_control 318 | .sender 319 | .send(KeyOrPointerRequest::PointerMotionAbsolute { 320 | x, 321 | y, 322 | x_extent: 2000, 323 | y_extent: 2000, 324 | }) 325 | .map_err(|_| zbus::Error::Failure("Send failed".to_string()))?; 326 | Ok(()) 327 | } 328 | 329 | async fn notify_pointer_button( 330 | &self, 331 | session_handle: ObjectPath<'_>, 332 | _options: HashMap>, 333 | button: i32, 334 | state: u32, 335 | ) -> zbus::fdo::Result<()> { 336 | let remote_sessions = REMOTE_SESSIONS.lock().await; 337 | let Some(session) = remote_sessions 338 | .iter() 339 | .find(|session| session.session_handle == session_handle.to_string()) 340 | else { 341 | return Ok(()); 342 | }; 343 | let remote_control = &session.remote_control; 344 | remote_control 345 | .sender 346 | .send(KeyOrPointerRequest::PointerButton { button, state }) 347 | .map_err(|_| zbus::Error::Failure("Send failed".to_string()))?; 348 | Ok(()) 349 | } 350 | 351 | async fn notify_pointer_axis( 352 | &self, 353 | session_handle: ObjectPath<'_>, 354 | _options: HashMap>, 355 | dx: f64, 356 | dy: f64, 357 | ) -> zbus::fdo::Result<()> { 358 | let remote_sessions = REMOTE_SESSIONS.lock().await; 359 | let Some(session) = remote_sessions 360 | .iter() 361 | .find(|session| session.session_handle == session_handle.to_string()) 362 | else { 363 | return Ok(()); 364 | }; 365 | let remote_control = &session.remote_control; 366 | remote_control 367 | .sender 368 | .send(KeyOrPointerRequest::PointerAxis { dx, dy }) 369 | .map_err(|_| zbus::Error::Failure("Send failed".to_string()))?; 370 | Ok(()) 371 | } 372 | 373 | async fn notify_pointer_axis_discrate( 374 | &self, 375 | session_handle: ObjectPath<'_>, 376 | _options: HashMap>, 377 | axis: u32, 378 | steps: i32, 379 | ) -> zbus::fdo::Result<()> { 380 | let remote_sessions = REMOTE_SESSIONS.lock().await; 381 | let Some(session) = remote_sessions 382 | .iter() 383 | .find(|session| session.session_handle == session_handle.to_string()) 384 | else { 385 | return Ok(()); 386 | }; 387 | let remote_control = &session.remote_control; 388 | remote_control 389 | .sender 390 | .send(KeyOrPointerRequest::PointerAxisDiscrate { axis, steps }) 391 | .map_err(|_| zbus::Error::Failure("Send failed".to_string()))?; 392 | Ok(()) 393 | } 394 | 395 | async fn notify_keyboard_keycode( 396 | &self, 397 | session_handle: ObjectPath<'_>, 398 | _options: HashMap>, 399 | keycode: i32, 400 | state: u32, 401 | ) -> zbus::fdo::Result<()> { 402 | let remote_sessions = REMOTE_SESSIONS.lock().await; 403 | let Some(session) = remote_sessions 404 | .iter() 405 | .find(|session| session.session_handle == session_handle.to_string()) 406 | else { 407 | return Ok(()); 408 | }; 409 | let remote_control = &session.remote_control; 410 | remote_control 411 | .sender 412 | .send(KeyOrPointerRequest::KeyboardKeycode { keycode, state }) 413 | .map_err(|_| zbus::Error::Failure("Send failed".to_string()))?; 414 | Ok(()) 415 | } 416 | 417 | async fn notify_keyboard_keysym( 418 | &self, 419 | session_handle: ObjectPath<'_>, 420 | _options: HashMap>, 421 | keysym: i32, 422 | state: u32, 423 | ) -> zbus::fdo::Result<()> { 424 | let remote_sessions = REMOTE_SESSIONS.lock().await; 425 | let Some(session) = remote_sessions 426 | .iter() 427 | .find(|session| session.session_handle == session_handle.to_string()) 428 | else { 429 | return Ok(()); 430 | }; 431 | let remote_control = &session.remote_control; 432 | remote_control 433 | .sender 434 | .send(KeyOrPointerRequest::KeyboardKeysym { keysym, state }) 435 | .map_err(|_| zbus::Error::Failure("Send failed".to_string()))?; 436 | Ok(()) 437 | } 438 | } 439 | -------------------------------------------------------------------------------- /src/remotedesktop/dispatch.rs: -------------------------------------------------------------------------------- 1 | use super::state::AppData; 2 | use wayland_client::{ 3 | Connection, Dispatch, Proxy, QueueHandle, delegate_noop, 4 | globals::GlobalListContents, 5 | protocol::{wl_registry, wl_seat::WlSeat, wl_shm::WlShm}, 6 | }; 7 | use wayland_protocols_misc::zwp_virtual_keyboard_v1::client::{ 8 | zwp_virtual_keyboard_manager_v1::ZwpVirtualKeyboardManagerV1, 9 | zwp_virtual_keyboard_v1::ZwpVirtualKeyboardV1, 10 | }; 11 | 12 | use std::{ffi::CString, fs::File, io::Write, path::PathBuf}; 13 | use wayland_protocols_wlr::virtual_pointer::v1::client::{ 14 | zwlr_virtual_pointer_manager_v1::ZwlrVirtualPointerManagerV1, 15 | zwlr_virtual_pointer_v1::ZwlrVirtualPointerV1, 16 | }; 17 | use xkbcommon::xkb; 18 | 19 | pub fn get_keymap_as_file() -> (File, u32) { 20 | let context = xkb::Context::new(xkb::CONTEXT_NO_FLAGS); 21 | 22 | let keymap = xkb::Keymap::new_from_names( 23 | &context, 24 | "", 25 | "", 26 | "us", 27 | "", 28 | None, 29 | xkb::KEYMAP_COMPILE_NO_FLAGS, 30 | ) 31 | .expect("xkbcommon keymap panicked!"); 32 | let xkb_state = xkb::State::new(&keymap); 33 | let keymap = xkb_state 34 | .get_keymap() 35 | .get_as_string(xkb::KEYMAP_FORMAT_TEXT_V1); 36 | let keymap = CString::new(keymap).expect("Keymap should not contain interior nul bytes"); 37 | let keymap = keymap.as_bytes_with_nul(); 38 | let dir = std::env::var_os("XDG_RUNTIME_DIR") 39 | .map(PathBuf::from) 40 | .unwrap_or_else(std::env::temp_dir); 41 | let mut file = tempfile::tempfile_in(dir).expect("File could not be created!"); 42 | file.write_all(keymap).unwrap(); 43 | file.flush().unwrap(); 44 | (file, keymap.len() as u32) 45 | } 46 | 47 | impl Dispatch for AppData { 48 | fn event( 49 | _state: &mut Self, 50 | _proxy: &wl_registry::WlRegistry, 51 | _event: ::Event, 52 | _data: &GlobalListContents, 53 | _conn: &Connection, 54 | _qhandle: &QueueHandle, 55 | ) { 56 | } 57 | } 58 | 59 | delegate_noop!(AppData: ignore ZwpVirtualKeyboardManagerV1); 60 | delegate_noop!(AppData: ignore ZwpVirtualKeyboardV1); 61 | delegate_noop!(AppData: ignore ZwlrVirtualPointerManagerV1); 62 | delegate_noop!(AppData: ignore ZwlrVirtualPointerV1); 63 | delegate_noop!(AppData: ignore WlSeat); 64 | delegate_noop!(AppData: ignore WlShm); 65 | delegate_noop!(AppData: ignore wl_registry::WlRegistry); 66 | -------------------------------------------------------------------------------- /src/remotedesktop/remote_thread.rs: -------------------------------------------------------------------------------- 1 | use wayland_client::Connection; 2 | use wayland_client::globals::registry_queue_init; 3 | use wayland_client::protocol::wl_keyboard; 4 | use wayland_client::protocol::wl_seat::WlSeat; 5 | use wayland_protocols_misc::zwp_virtual_keyboard_v1::client::zwp_virtual_keyboard_manager_v1::ZwpVirtualKeyboardManagerV1; 6 | use wayland_protocols_wlr::virtual_pointer::v1::client::zwlr_virtual_pointer_manager_v1::ZwlrVirtualPointerManagerV1; 7 | 8 | use super::dispatch::get_keymap_as_file; 9 | use super::state::AppData; 10 | use super::state::KeyPointerError; 11 | use std::sync::Mutex; 12 | use std::sync::atomic::Ordering; 13 | 14 | use std::os::fd::AsFd; 15 | use std::sync::Arc; 16 | use std::sync::atomic::AtomicBool; 17 | use std::sync::mpsc::{self, Receiver, Sender}; 18 | 19 | use calloop::EventLoop; 20 | use calloop_wayland_source::WaylandSource; 21 | 22 | #[derive(Debug)] 23 | pub enum KeyOrPointerRequest { 24 | PointerMotion { 25 | dx: f64, 26 | dy: f64, 27 | }, 28 | PointerMotionAbsolute { 29 | x: f64, 30 | y: f64, 31 | x_extent: u32, 32 | y_extent: u32, 33 | }, 34 | PointerButton { 35 | button: i32, 36 | state: u32, 37 | }, 38 | PointerAxis { 39 | dx: f64, 40 | dy: f64, 41 | }, 42 | PointerAxisDiscrate { 43 | axis: u32, 44 | steps: i32, 45 | }, 46 | KeyboardKeycode { 47 | keycode: i32, 48 | state: u32, 49 | }, 50 | KeyboardKeysym { 51 | keysym: i32, 52 | state: u32, 53 | }, 54 | Exit, 55 | } 56 | 57 | #[derive(Debug)] 58 | pub struct RemoteControl { 59 | pub sender: Sender, 60 | } 61 | 62 | impl RemoteControl { 63 | pub fn init() -> Self { 64 | let (sender, receiver) = mpsc::channel(); 65 | std::thread::spawn(move || { 66 | let _ = remote_loop(receiver); 67 | }); 68 | Self { sender } 69 | } 70 | 71 | pub fn stop(&self) { 72 | let _ = self.sender.send(KeyOrPointerRequest::Exit); 73 | } 74 | } 75 | 76 | pub fn remote_loop(receiver: Receiver) -> Result<(), KeyPointerError> { 77 | // Create a Wayland connection by connecting to the server through the 78 | // environment-provided configuration. 79 | let conn = Connection::connect_to_env().map_err(|_| { 80 | KeyPointerError::InitFailedConnection("Cannot create connection".to_string()) 81 | })?; 82 | 83 | // Retrieve the WlDisplay Wayland object from the connection. This object is 84 | // the starting point of any Wayland program, from which all other objects will 85 | // be created. 86 | let display = conn.display(); 87 | 88 | let (globals, event_queue) = registry_queue_init::(&conn)?; // We just need the 89 | 90 | let qh = event_queue.handle(); 91 | let seat = globals.bind::(&qh, 7..=9, ())?; 92 | let virtual_keyboard_manager = 93 | globals.bind::(&qh, 1..=1, ())?; 94 | 95 | let virtual_keyboard = virtual_keyboard_manager.create_virtual_keyboard(&seat, &qh, ()); 96 | let (file, size) = get_keymap_as_file(); 97 | virtual_keyboard.keymap(wl_keyboard::KeymapFormat::XkbV1.into(), file.as_fd(), size); 98 | 99 | let virtual_pointer_manager = 100 | globals.bind::(&qh, 1..=2, ())?; 101 | let pointer = virtual_pointer_manager.create_virtual_pointer(Some(&seat), &qh, ()); 102 | // Create an event queue for our event processing 103 | // An get its handle to associated new objects to it 104 | 105 | // Create a wl_registry object by sending the wl_display.get_registry request 106 | // This method takes two arguments: a handle to the queue the newly created 107 | // wl_registry will be assigned to, and the user-data that should be associated 108 | // with this registry (here it is () as we don't need user-data). 109 | let _registry = display.get_registry(&qh, ()); 110 | 111 | let mut event_loop: EventLoop = 112 | EventLoop::try_new().expect("Failed to initialize the event loop"); 113 | 114 | WaylandSource::new(conn, event_queue) 115 | .insert(event_loop.handle()) 116 | .expect("Failed to init wayland source"); 117 | 118 | let to_exit = Arc::new(AtomicBool::new(false)); 119 | 120 | let events: Arc>> = Arc::new(Mutex::new(Vec::new())); 121 | 122 | let to_exit2 = to_exit.clone(); 123 | let to_exit3 = to_exit.clone(); 124 | let events_2 = events.clone(); 125 | let thread = std::thread::spawn(move || { 126 | let to_exit = to_exit2; 127 | let events = events_2; 128 | 129 | for message in receiver.iter() { 130 | if to_exit.load(Ordering::Relaxed) { 131 | break; 132 | } 133 | let mut events_local = events.lock().unwrap(); 134 | events_local.push(message); 135 | } 136 | to_exit.store(true, Ordering::Relaxed); 137 | }); 138 | 139 | // At this point everything is ready, and we just need to wait to receive the events 140 | // from the wl_registry, our callback will print the advertized globals. 141 | let mut data = AppData::new(virtual_keyboard, pointer); 142 | 143 | let signal = event_loop.get_signal(); 144 | event_loop 145 | .run( 146 | std::time::Duration::from_millis(20), 147 | &mut data, 148 | move |data| { 149 | if to_exit3.load(Ordering::Relaxed) { 150 | signal.stop(); 151 | return; 152 | } 153 | let mut local_events = events.lock().expect( 154 | "This events only used in this callback, so it should always can be unlocked", 155 | ); 156 | let mut swapped_events = vec![]; 157 | std::mem::swap(&mut *local_events, &mut swapped_events); 158 | drop(local_events); 159 | for message in swapped_events { 160 | match message { 161 | KeyOrPointerRequest::PointerMotion { dx, dy } => { 162 | data.notify_pointer_motion(dx, dy) 163 | } 164 | KeyOrPointerRequest::PointerMotionAbsolute { 165 | x, 166 | y, 167 | x_extent, 168 | y_extent, 169 | } => data.notify_pointer_motion_absolute(x, y, x_extent, y_extent), 170 | KeyOrPointerRequest::PointerButton { button, state } => { 171 | data.notify_pointer_button(button, state) 172 | } 173 | KeyOrPointerRequest::PointerAxis { dx, dy } => { 174 | data.notify_pointer_axis(dx, dy) 175 | } 176 | KeyOrPointerRequest::PointerAxisDiscrate { axis, steps } => { 177 | data.notify_pointer_axis_discrete(axis, steps) 178 | } 179 | KeyOrPointerRequest::KeyboardKeycode { keycode, state } => { 180 | data.notify_keyboard_keycode(keycode, state) 181 | } 182 | KeyOrPointerRequest::KeyboardKeysym { keysym, state } => { 183 | data.notify_keyboard_keysym(keysym, state) 184 | } 185 | KeyOrPointerRequest::Exit => { 186 | signal.stop(); 187 | break; 188 | } 189 | } 190 | } 191 | }, 192 | ) 193 | .expect("Error during event loop"); 194 | 195 | to_exit.store(true, Ordering::Relaxed); 196 | let _ = thread.join(); 197 | 198 | Ok(()) 199 | } 200 | -------------------------------------------------------------------------------- /src/remotedesktop/state.rs: -------------------------------------------------------------------------------- 1 | use wayland_protocols_misc::zwp_virtual_keyboard_v1::client::zwp_virtual_keyboard_v1::ZwpVirtualKeyboardV1; 2 | 3 | use wayland_client::{ 4 | DispatchError, 5 | globals::{BindError, GlobalError}, 6 | protocol::wl_pointer, 7 | }; 8 | use wayland_protocols_wlr::virtual_pointer::v1::client::zwlr_virtual_pointer_v1::ZwlrVirtualPointerV1; 9 | 10 | use thiserror::Error; 11 | // This struct represents the state of our app. This simple app does not 12 | // need any state, by this type still supports the `Dispatch` implementations. 13 | #[derive(Debug)] 14 | pub struct AppData { 15 | pub(crate) virtual_keyboard: ZwpVirtualKeyboardV1, 16 | 17 | pub(crate) virtual_pointer: ZwlrVirtualPointerV1, 18 | } 19 | 20 | impl AppData { 21 | pub fn new( 22 | virtual_keyboard: ZwpVirtualKeyboardV1, 23 | virtual_pointer: ZwlrVirtualPointerV1, 24 | ) -> Self { 25 | Self { 26 | virtual_keyboard, 27 | virtual_pointer, 28 | } 29 | } 30 | } 31 | 32 | impl Drop for AppData { 33 | fn drop(&mut self) { 34 | self.virtual_pointer.destroy(); 35 | self.virtual_keyboard.destroy(); 36 | } 37 | } 38 | 39 | #[derive(Error, Debug)] 40 | pub enum KeyPointerError { 41 | #[error("Connection create Error")] 42 | InitFailedConnection(String), 43 | #[error("Error during queue")] 44 | FailedDuringQueue(#[from] DispatchError), 45 | #[error("GlobalError")] 46 | GlobalError(#[from] GlobalError), 47 | #[error("BindError")] 48 | BindFailed(#[from] BindError), 49 | } 50 | 51 | impl AppData { 52 | pub fn notify_pointer_motion(&self, dx: f64, dy: f64) { 53 | self.virtual_pointer.motion(10, dx, dy); 54 | } 55 | 56 | pub fn notify_pointer_motion_absolute(&self, x: f64, y: f64, x_extent: u32, y_extent: u32) { 57 | self.virtual_pointer 58 | .motion_absolute(10, x as u32, y as u32, x_extent, y_extent); 59 | } 60 | 61 | pub fn notify_pointer_button(&self, button: i32, state: u32) { 62 | self.virtual_pointer.button( 63 | 100, 64 | button as u32, 65 | if state == 0 { 66 | wl_pointer::ButtonState::Released 67 | } else { 68 | wl_pointer::ButtonState::Pressed 69 | }, 70 | ); 71 | } 72 | 73 | pub fn notify_pointer_axis(&self, dx: f64, dy: f64) { 74 | self.virtual_pointer 75 | .axis(100, wl_pointer::Axis::HorizontalScroll, dx); 76 | self.virtual_pointer 77 | .axis(100, wl_pointer::Axis::VerticalScroll, dy); 78 | } 79 | 80 | pub fn notify_pointer_axis_discrete(&self, axis: u32, steps: i32) { 81 | self.virtual_pointer.axis_discrete( 82 | 100, 83 | if axis == 0 { 84 | wl_pointer::Axis::VerticalScroll 85 | } else { 86 | wl_pointer::Axis::HorizontalScroll 87 | }, 88 | 10.0, 89 | steps, 90 | ); 91 | } 92 | 93 | pub fn notify_keyboard_keycode(&self, keycode: i32, state: u32) { 94 | self.virtual_keyboard.key(100, keycode as u32, state); 95 | } 96 | 97 | pub fn notify_keyboard_keysym(&self, keysym: i32, state: u32) { 98 | self.virtual_keyboard.key(100, keysym as u32, state); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/request.rs: -------------------------------------------------------------------------------- 1 | use zbus::{interface, zvariant::OwnedObjectPath}; 2 | 3 | pub struct RequestInterface { 4 | pub handle_path: OwnedObjectPath, 5 | } 6 | 7 | #[interface(name = "org.freedesktop.impl.portal.Request")] 8 | impl RequestInterface { 9 | async fn close( 10 | &self, 11 | #[zbus(object_server)] server: &zbus::ObjectServer, 12 | ) -> zbus::fdo::Result<()> { 13 | server 14 | .remove::(&self.handle_path) 15 | .await?; 16 | Ok(()) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/screencast.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use zbus::interface; 4 | 5 | use zbus::zvariant::{ 6 | ObjectPath, OwnedValue, Type, Value, 7 | as_value::{self, optional}, 8 | }; 9 | 10 | use enumflags2::BitFlags; 11 | 12 | use serde::{Deserialize, Serialize}; 13 | 14 | use std::sync::Arc; 15 | use std::sync::LazyLock; 16 | use tokio::sync::Mutex; 17 | 18 | use crate::PortalResponse; 19 | use crate::pipewirethread::ScreencastThread; 20 | use crate::request::RequestInterface; 21 | use crate::session::{ 22 | CursorMode, PersistMode, SESSIONS, Session, SessionType, SourceType, append_session, 23 | }; 24 | 25 | use libwaysip::SelectionType; 26 | 27 | #[derive(Type, Debug, Default, Serialize, Deserialize)] 28 | /// Specified options for a [`Screencast::create_session`] request. 29 | #[zvariant(signature = "dict")] 30 | struct SessionCreateResult { 31 | #[serde(with = "as_value")] 32 | handle_token: String, 33 | } 34 | 35 | #[derive(Type, Debug, Default, Serialize, Deserialize)] 36 | /// Specified options for a [`Screencast::select_sources`] request. 37 | #[zvariant(signature = "dict")] 38 | pub struct SelectSourcesOptions { 39 | /// A string that will be used as the last element of the handle. 40 | /// What types of content to record. 41 | #[serde(with = "optional", skip_serializing_if = "Option::is_none", default)] 42 | pub types: Option>, 43 | /// Whether to allow selecting multiple sources. 44 | #[serde(with = "optional", skip_serializing_if = "Option::is_none", default)] 45 | pub multiple: Option, 46 | /// Determines how the cursor will be drawn in the screen cast stream. 47 | #[serde(with = "optional", skip_serializing_if = "Option::is_none", default)] 48 | pub cursor_mode: Option, 49 | #[serde(with = "optional", skip_serializing_if = "Option::is_none", default)] 50 | pub restore_token: Option, 51 | #[serde(with = "optional", skip_serializing_if = "Option::is_none", default)] 52 | pub persist_mode: Option, 53 | } 54 | 55 | #[derive(Clone, Serialize, Deserialize, Type, Default, Debug)] 56 | /// A PipeWire stream. 57 | pub struct Stream(u32, StreamProperties); 58 | 59 | #[derive(Clone, Default, Type, Debug, Serialize, Deserialize)] 60 | /// The stream properties. 61 | #[zvariant(signature = "dict")] 62 | struct StreamProperties { 63 | #[serde(with = "optional", skip_serializing_if = "Option::is_none", default)] 64 | id: Option, 65 | #[serde(with = "optional", skip_serializing_if = "Option::is_none", default)] 66 | position: Option<(i32, i32)>, 67 | #[serde(with = "as_value")] 68 | size: (i32, i32), 69 | #[serde(with = "as_value")] 70 | source_type: SourceType, 71 | } 72 | 73 | // TODO: this is copy from ashpd, but the dict is a little different from xdg_desktop_portal 74 | #[derive(Clone, Default, Debug, Type, Serialize, Deserialize)] 75 | #[zvariant(signature = "dict")] 76 | struct StartReturnValue { 77 | #[serde(with = "as_value")] 78 | streams: Vec, 79 | #[serde(with = "as_value")] 80 | persist_mode: u32, 81 | #[serde(with = "optional", skip_serializing_if = "Option::is_none", default)] 82 | restore_token: Option, 83 | } 84 | 85 | pub struct CastSessionData { 86 | session_handle: String, 87 | cast_thread: ScreencastThread, 88 | } 89 | pub static CAST_SESSIONS: LazyLock>>> = 90 | LazyLock::new(|| Arc::new(Mutex::new(Vec::new()))); 91 | 92 | pub async fn append_cast_session(session: CastSessionData) { 93 | let mut sessions = CAST_SESSIONS.lock().await; 94 | sessions.push(session) 95 | } 96 | 97 | pub async fn remove_cast_session(path: &str) { 98 | let mut sessions = CAST_SESSIONS.lock().await; 99 | let Some(index) = sessions 100 | .iter() 101 | .position(|the_session| the_session.session_handle == path) 102 | else { 103 | return; 104 | }; 105 | sessions[index].cast_thread.stop(); 106 | tracing::info!("session {} is stopped", sessions[index].session_handle); 107 | sessions.remove(index); 108 | } 109 | 110 | pub struct ScreenCastBackend; 111 | 112 | #[interface(name = "org.freedesktop.impl.portal.ScreenCast")] 113 | impl ScreenCastBackend { 114 | #[zbus(property, name = "version")] 115 | fn version(&self) -> u32 { 116 | 4 117 | } 118 | 119 | #[zbus(property)] 120 | fn available_cursor_modes(&self) -> u32 { 121 | (CursorMode::Hidden | CursorMode::Embedded).bits() 122 | } 123 | 124 | #[zbus(property)] 125 | fn available_source_types(&self) -> u32 { 126 | BitFlags::from_flag(SourceType::Monitor).bits() 127 | } 128 | 129 | async fn create_session( 130 | &self, 131 | request_handle: ObjectPath<'_>, 132 | session_handle: ObjectPath<'_>, 133 | app_id: String, 134 | _options: HashMap>, 135 | #[zbus(object_server)] server: &zbus::ObjectServer, 136 | ) -> zbus::fdo::Result> { 137 | tracing::info!( 138 | "Start shot: path :{}, appid: {}", 139 | request_handle.as_str(), 140 | app_id 141 | ); 142 | server 143 | .at( 144 | request_handle.clone(), 145 | RequestInterface { 146 | handle_path: request_handle.clone().into(), 147 | }, 148 | ) 149 | .await?; 150 | let current_session = Session::new(session_handle.clone(), SessionType::ScreenCast); 151 | append_session(current_session.clone()).await; 152 | server.at(session_handle.clone(), current_session).await?; 153 | Ok(PortalResponse::Success(SessionCreateResult { 154 | handle_token: session_handle.to_string(), 155 | })) 156 | } 157 | 158 | async fn select_sources( 159 | &self, 160 | _request_handle: ObjectPath<'_>, 161 | session_handle: ObjectPath<'_>, 162 | _app_id: String, 163 | options: SelectSourcesOptions, 164 | ) -> zbus::fdo::Result>> { 165 | let mut locked_sessions = SESSIONS.lock().await; 166 | let Some(index) = locked_sessions 167 | .iter() 168 | .position(|this_session| this_session.handle_path == session_handle.clone().into()) 169 | else { 170 | tracing::warn!("No session is created or it is removed"); 171 | return Ok(PortalResponse::Other); 172 | }; 173 | locked_sessions[index].set_screencast_options(options); 174 | Ok(PortalResponse::Success(HashMap::new())) 175 | } 176 | 177 | async fn start( 178 | &self, 179 | _request_handle: ObjectPath<'_>, 180 | session_handle: ObjectPath<'_>, 181 | _app_id: String, 182 | _parent_window: String, 183 | _options: HashMap>, 184 | ) -> zbus::fdo::Result> { 185 | let cast_sessions = CAST_SESSIONS.lock().await; 186 | if let Some(session) = cast_sessions 187 | .iter() 188 | .find(|session| session.session_handle == session_handle.to_string()) 189 | { 190 | return Ok(PortalResponse::Success(StartReturnValue { 191 | streams: vec![Stream( 192 | session.cast_thread.node_id(), 193 | StreamProperties::default(), 194 | )], 195 | ..Default::default() 196 | })); 197 | } 198 | drop(cast_sessions); 199 | 200 | let locked_sessions = SESSIONS.lock().await; 201 | let Some(index) = locked_sessions 202 | .iter() 203 | .position(|this_session| this_session.handle_path == session_handle.clone().into()) 204 | else { 205 | tracing::warn!("No session is created or it is removed"); 206 | return Ok(PortalResponse::Other); 207 | }; 208 | 209 | let current_session = locked_sessions[index].clone(); 210 | if current_session.session_type != SessionType::ScreenCast { 211 | return Ok(PortalResponse::Other); 212 | } 213 | drop(locked_sessions); 214 | 215 | let show_cursor = current_session.cursor_mode.show_cursor(); 216 | let connection = libwayshot::WayshotConnection::new().unwrap(); 217 | 218 | let info = match libwaysip::get_area( 219 | Some(libwaysip::WaysipConnection { 220 | connection: &connection.conn, 221 | globals: &connection.globals, 222 | }), 223 | SelectionType::Screen, 224 | ) { 225 | Ok(Some(info)) => info, 226 | Ok(None) => return Err(zbus::Error::Failure("You cancel it".to_string()).into()), 227 | Err(e) => return Err(zbus::Error::Failure(format!("wayland error, {e}")).into()), 228 | }; 229 | 230 | use libwaysip::Size; 231 | let screen_info = info.screen_info; 232 | 233 | let Size { width, height } = screen_info.get_wloutput_size(); 234 | let output = screen_info.wl_output; 235 | let cast_thread = ScreencastThread::start_cast( 236 | show_cursor, 237 | width as u32, 238 | height as u32, 239 | None, 240 | output, 241 | connection, 242 | ) 243 | .await 244 | .map_err(|e| zbus::Error::Failure(format!("cannot start pipewire stream, error: {e}")))?; 245 | 246 | let node_id = cast_thread.node_id(); 247 | 248 | append_cast_session(CastSessionData { 249 | session_handle: session_handle.to_string(), 250 | cast_thread, 251 | }) 252 | .await; 253 | 254 | Ok(PortalResponse::Success(StartReturnValue { 255 | streams: vec![Stream( 256 | node_id, 257 | StreamProperties { 258 | size: (width, height), 259 | source_type: SourceType::Monitor, 260 | ..Default::default() 261 | }, 262 | )], 263 | ..Default::default() 264 | })) 265 | } 266 | } 267 | -------------------------------------------------------------------------------- /src/screenshot.rs: -------------------------------------------------------------------------------- 1 | use libwayshot::WayshotConnection; 2 | use libwaysip::Position; 3 | use screenshotdialog::ScreenInfo; 4 | use screenshotdialog::SlintSelection; 5 | use serde::Deserialize; 6 | use serde::Serialize; 7 | use std::collections::HashMap; 8 | use zbus::zvariant::{Type, Value}; 9 | use zbus::{ 10 | fdo, interface, 11 | zvariant::{ 12 | ObjectPath, 13 | as_value::{self, optional}, 14 | }, 15 | }; 16 | 17 | use crate::PortalResponse; 18 | use crate::utils::USER_RUNNING_DIR; 19 | 20 | use libwaysip::SelectionType; 21 | 22 | #[derive(Type, Serialize, Deserialize)] 23 | #[zvariant(signature = "dict")] 24 | struct Screenshot { 25 | #[serde(with = "as_value")] 26 | uri: url::Url, 27 | } 28 | 29 | #[derive(Clone, Copy, PartialEq, Type, Serialize, Deserialize)] 30 | #[zvariant(signature = "dict")] 31 | struct Color { 32 | #[serde(with = "as_value")] 33 | color: [f64; 3], 34 | } 35 | 36 | #[derive(Type, Debug, Serialize, Deserialize)] 37 | #[zvariant(signature = "dict")] 38 | pub struct ScreenshotOption { 39 | #[serde(with = "as_value")] 40 | interactive: bool, 41 | #[serde(with = "optional", skip_serializing_if = "Option::is_none", default)] 42 | modal: Option, 43 | #[serde(with = "optional", skip_serializing_if = "Option::is_none", default)] 44 | permission_store_checked: Option, 45 | } 46 | 47 | #[derive(Debug)] 48 | pub struct ScreenShotBackend; 49 | 50 | #[interface(name = "org.freedesktop.impl.portal.Screenshot")] 51 | impl ScreenShotBackend { 52 | #[zbus(property, name = "version")] 53 | fn version(&self) -> u32 { 54 | 1 55 | } 56 | fn screenshot( 57 | &mut self, 58 | handle: ObjectPath<'_>, 59 | app_id: String, 60 | _parent_window: String, 61 | options: ScreenshotOption, 62 | ) -> fdo::Result> { 63 | tracing::info!("Start shot: path :{}, appid: {}", handle.as_str(), app_id); 64 | let wayshot_connection = WayshotConnection::new() 65 | .map_err(|_| zbus::Error::Failure("Cannot update outputInfos".to_string()))?; 66 | 67 | let image_buffer = if options.interactive { 68 | let wayinfos = wayshot_connection.get_all_outputs(); 69 | let screen_infos = wayinfos 70 | .iter() 71 | .map(|screen| ScreenInfo { 72 | name: screen.name.clone().into(), 73 | description: screen.description.clone().into(), 74 | }) 75 | .collect(); 76 | match screenshotdialog::selectgui(screen_infos) { 77 | SlintSelection::Canceled => return Ok(PortalResponse::Cancelled), 78 | SlintSelection::Slurp => { 79 | let info = match libwaysip::get_area(None, SelectionType::Area) { 80 | Ok(Some(info)) => info, 81 | Ok(None) => { 82 | return Err(zbus::Error::Failure("You cancel it".to_string()).into()); 83 | } 84 | Err(e) => { 85 | return Err(zbus::Error::Failure(format!("wayland error, {e}")).into()); 86 | } 87 | }; 88 | 89 | let Position { 90 | x: x_coordinate, 91 | y: y_coordinate, 92 | } = info.left_top_point(); 93 | let width = info.width(); 94 | let height = info.height(); 95 | 96 | wayshot_connection 97 | .screenshot( 98 | libwayshot::CaptureRegion { 99 | x_coordinate, 100 | y_coordinate, 101 | width, 102 | height, 103 | }, 104 | false, 105 | ) 106 | .map_err(|e| { 107 | zbus::Error::Failure(format!("Wayland screencopy failed, {e}")) 108 | })? 109 | } 110 | SlintSelection::GlobalScreen { showcursor } => wayshot_connection 111 | .screenshot_all(showcursor) 112 | .map_err(|e| zbus::Error::Failure(format!("Wayland screencopy failed, {e}")))?, 113 | SlintSelection::Selection { index, showcursor } => wayshot_connection 114 | .screenshot_single_output(&wayinfos[index as usize], showcursor) 115 | .map_err(|e| zbus::Error::Failure(format!("Wayland screencopy failed, {e}")))?, 116 | } 117 | } else { 118 | wayshot_connection 119 | .screenshot_all(false) 120 | .map_err(|e| zbus::Error::Failure(format!("Wayland screencopy failed, {e}")))? 121 | }; 122 | let savepath = USER_RUNNING_DIR.join("wayshot.png"); 123 | image_buffer.save(&savepath).map_err(|e| { 124 | zbus::Error::Failure(format!("Cannot save to {}, e: {e}", savepath.display())) 125 | })?; 126 | tracing::info!("Shot Finished"); 127 | Ok(PortalResponse::Success(Screenshot { 128 | uri: url::Url::from_file_path(savepath).unwrap(), 129 | })) 130 | } 131 | 132 | fn pick_color( 133 | &mut self, 134 | _handle: ObjectPath<'_>, 135 | _app_id: String, 136 | _parent_window: String, 137 | _options: HashMap>, 138 | ) -> fdo::Result> { 139 | let wayshot_connection = WayshotConnection::new() 140 | .map_err(|_| zbus::Error::Failure("Cannot update outputInfos".to_string()))?; 141 | let info = match libwaysip::get_area(None, SelectionType::Point) { 142 | Ok(Some(info)) => info, 143 | Ok(None) => return Err(zbus::Error::Failure("You cancel it".to_string()).into()), 144 | Err(e) => return Err(zbus::Error::Failure(format!("wayland error, {e}")).into()), 145 | }; 146 | let Position { 147 | x: x_coordinate, 148 | y: y_coordinate, 149 | } = info.left_top_point(); 150 | 151 | let image = wayshot_connection 152 | .screenshot( 153 | libwayshot::CaptureRegion { 154 | x_coordinate, 155 | y_coordinate, 156 | width: 1, 157 | height: 1, 158 | }, 159 | false, 160 | ) 161 | .map_err(|e| zbus::Error::Failure(format!("Wayland screencopy failed, {e}")))?; 162 | 163 | let pixel = image.get_pixel(0, 0); 164 | Ok(PortalResponse::Success(Color { 165 | color: [ 166 | pixel.0[0] as f64 / 256.0, 167 | pixel.0[1] as f64 / 256.0, 168 | pixel.0[2] as f64 / 256.0, 169 | ], 170 | })) 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /src/session.rs: -------------------------------------------------------------------------------- 1 | use enumflags2::{BitFlags, bitflags}; 2 | use zbus::{interface, object_server::SignalEmitter, zvariant::OwnedObjectPath}; 3 | 4 | use serde::{Deserialize, Serialize}; 5 | use serde_repr::{Deserialize_repr, Serialize_repr}; 6 | use zbus::zvariant::Type; 7 | 8 | use std::sync::{Arc, LazyLock}; 9 | use tokio::sync::Mutex; 10 | 11 | use crate::{ 12 | remotedesktop::{SelectDevicesOptions, remove_remote_session}, 13 | screencast::{SelectSourcesOptions, remove_cast_session}, 14 | }; 15 | 16 | pub static SESSIONS: LazyLock>>> = 17 | LazyLock::new(|| Arc::new(Mutex::new(Vec::new()))); 18 | 19 | pub async fn append_session(session: Session) { 20 | let mut sessions = SESSIONS.lock().await; 21 | sessions.push(session) 22 | } 23 | 24 | pub async fn remove_session(session: &Session) { 25 | let mut sessions = SESSIONS.lock().await; 26 | let Some(index) = sessions 27 | .iter() 28 | .position(|the_session| the_session.handle_path == session.handle_path) 29 | else { 30 | return; 31 | }; 32 | remove_cast_session(&session.handle_path.to_string()).await; 33 | remove_remote_session(&session.handle_path.to_string()).await; 34 | sessions.remove(index); 35 | } 36 | 37 | #[bitflags] 38 | #[derive(Serialize, Default, Deserialize, PartialEq, Eq, Copy, Clone, Debug, Type)] 39 | #[repr(u32)] 40 | /// A bit flag for the available sources to record. 41 | pub enum SourceType { 42 | #[default] 43 | /// A monitor. 44 | Monitor, 45 | /// A specific window 46 | Window, 47 | /// Virtual 48 | Virtual, 49 | } 50 | 51 | #[bitflags] 52 | #[derive(Serialize_repr, Deserialize_repr, PartialEq, Eq, Debug, Copy, Clone, Type, Default)] 53 | #[repr(u32)] 54 | /// A bit flag for the possible cursor modes. 55 | pub enum CursorMode { 56 | #[default] 57 | /// The cursor is not part of the screen cast stream. 58 | Hidden = 1, 59 | /// The cursor is embedded as part of the stream buffers. 60 | Embedded = 2, 61 | /// The cursor is not part of the screen cast stream, but sent as PipeWire 62 | /// stream metadata. 63 | Metadata = 4, 64 | } 65 | 66 | // Remote 67 | #[bitflags] 68 | #[derive(Serialize_repr, Deserialize_repr, PartialEq, Eq, Debug, Copy, Clone, Type, Default)] 69 | #[repr(u32)] 70 | /// A bit flag for the possible cursor modes. 71 | pub enum DeviceType { 72 | #[default] 73 | /// The cursor is not part of the screen cast stream. 74 | Keyboard = 1, 75 | /// The cursor is embedded as part of the stream buffers. 76 | Pointer = 2, 77 | /// The cursor is not part of the screen cast stream, but sent as PipeWire 78 | /// stream metadata. 79 | TouchScreen = 4, 80 | } 81 | 82 | impl CursorMode { 83 | pub fn show_cursor(&self) -> bool { 84 | !matches!(self, CursorMode::Hidden) 85 | } 86 | } 87 | 88 | #[derive(Debug, Clone, PartialEq, Eq)] 89 | pub enum SessionType { 90 | ScreenCast, 91 | Remote, 92 | } 93 | 94 | #[derive(Default, Serialize, Deserialize, PartialEq, Eq, Debug, Copy, Clone, Type)] 95 | #[repr(u32)] 96 | /// Persistence mode for a screencast session. 97 | pub enum PersistMode { 98 | #[default] 99 | /// Do not persist. 100 | DoNot = 0, 101 | /// Persist while the application is running. 102 | Application = 1, 103 | /// Persist until explicitly revoked. 104 | ExplicitlyRevoked = 2, 105 | } 106 | 107 | #[derive(Debug, Clone)] 108 | // TODO: when is remote? 109 | pub struct Session { 110 | pub session_type: SessionType, 111 | pub handle_path: OwnedObjectPath, 112 | pub source_type: BitFlags, 113 | pub multiple: bool, 114 | pub cursor_mode: CursorMode, 115 | pub persist_mode: PersistMode, 116 | 117 | pub device_type: BitFlags, 118 | } 119 | 120 | impl Session { 121 | pub fn new>(path: P, session_type: SessionType) -> Self { 122 | Self { 123 | session_type, 124 | handle_path: path.into(), 125 | source_type: SourceType::Monitor.into(), 126 | multiple: false, 127 | cursor_mode: CursorMode::Hidden, 128 | persist_mode: PersistMode::DoNot, 129 | device_type: DeviceType::Keyboard.into(), 130 | } 131 | } 132 | pub fn set_screencast_options(&mut self, options: SelectSourcesOptions) { 133 | if let Some(types) = options.types { 134 | self.source_type = types; 135 | } 136 | self.multiple = options.multiple.is_some_and(|content| content); 137 | if let Some(cursormode) = options.cursor_mode { 138 | self.cursor_mode = cursormode; 139 | } 140 | if let Some(persist_mode) = options.persist_mode { 141 | self.persist_mode = persist_mode; 142 | } 143 | } 144 | 145 | pub fn set_remote_options(&mut self, options: SelectDevicesOptions) { 146 | if let Some(types) = options.types { 147 | self.device_type = types; 148 | } 149 | if let Some(persist_mode) = options.persist_mode { 150 | self.persist_mode = persist_mode; 151 | } 152 | } 153 | } 154 | 155 | #[interface(name = "org.freedesktop.impl.portal.Session")] 156 | impl Session { 157 | async fn close( 158 | &self, 159 | #[zbus(signal_emitter)] cxts: SignalEmitter<'_>, 160 | #[zbus(object_server)] server: &zbus::ObjectServer, 161 | ) -> zbus::fdo::Result<()> { 162 | server 163 | .remove::(&self.handle_path) 164 | .await?; 165 | remove_session(self).await; 166 | Self::closed(&cxts, "Closed").await?; 167 | Ok(()) 168 | } 169 | 170 | #[zbus(property, name = "version")] 171 | fn version(&self) -> u32 { 172 | 2 173 | } 174 | 175 | #[zbus(signal)] 176 | async fn closed(signal_ctxt: &SignalEmitter<'_>, message: &str) -> zbus::Result<()>; 177 | } 178 | -------------------------------------------------------------------------------- /src/settings.rs: -------------------------------------------------------------------------------- 1 | mod config; 2 | use tokio::sync::Mutex; 3 | use zbus::{fdo, interface, object_server::SignalEmitter}; 4 | 5 | use zbus::zvariant::{OwnedValue, Type, Value}; 6 | 7 | const DEFAULT_COLOR: u32 = 0; 8 | const DARK_COLOR: u32 = 1; 9 | const LIGHT_COLOR: u32 = 2; 10 | 11 | const APPEARANCE: &str = "org.freedesktop.appearance"; 12 | const COLOR_SCHEME: &str = "color-scheme"; 13 | const ACCENT_COLOR: &str = "accent-color"; 14 | 15 | use std::collections::HashMap; 16 | use std::sync::Arc; 17 | use std::sync::LazyLock; 18 | 19 | pub use self::config::SettingsConfig; 20 | 21 | pub static SETTING_CONFIG: LazyLock>> = 22 | LazyLock::new(|| Arc::new(Mutex::new(SettingsConfig::config_from_file()))); 23 | 24 | #[derive(Clone, Copy, PartialEq, Type, OwnedValue, Value)] 25 | pub struct AccentColor { 26 | red: f64, 27 | green: f64, 28 | blue: f64, 29 | } 30 | 31 | impl AccentColor { 32 | pub fn new(rgb: [f64; 3]) -> Self { 33 | Self { 34 | red: rgb[0], 35 | green: rgb[1], 36 | blue: rgb[2], 37 | } 38 | } 39 | } 40 | 41 | #[derive(Debug)] 42 | pub struct SettingsBackend; 43 | 44 | #[interface(name = "org.freedesktop.impl.portal.Settings")] 45 | impl SettingsBackend { 46 | #[zbus(property, name = "version")] 47 | fn version(&self) -> u32 { 48 | 1 49 | } 50 | 51 | async fn read(&self, namespace: String, key: String) -> fdo::Result { 52 | if namespace != APPEARANCE { 53 | return Err(zbus::fdo::Error::Failed("No such namespace".to_string())); 54 | } 55 | let config = SETTING_CONFIG.lock().await; 56 | if key == COLOR_SCHEME { 57 | return Ok(OwnedValue::from(config.get_color_scheme())); 58 | } 59 | if key == ACCENT_COLOR { 60 | return Ok(AccentColor::new(config.get_accent_color()) 61 | .try_into() 62 | .unwrap()); 63 | } 64 | Err(zbus::fdo::Error::Failed("No such key".to_string())) 65 | } 66 | 67 | async fn read_all( 68 | &self, 69 | namespaces: Vec<&str>, 70 | ) -> fdo::Result>> { 71 | if !namespaces.contains(&APPEARANCE) { 72 | return Err(zbus::fdo::Error::Failed("No such namespace".to_string())); 73 | } 74 | let mut output_setting = HashMap::::new(); 75 | let config = SETTING_CONFIG.lock().await; 76 | output_setting.insert(COLOR_SCHEME.to_string(), config.get_color_scheme().into()); 77 | output_setting.insert( 78 | ACCENT_COLOR.to_string(), 79 | OwnedValue::try_from(AccentColor::new(config.get_accent_color())).unwrap(), 80 | ); 81 | let output = HashMap::>::from_iter([( 82 | APPEARANCE.to_string(), 83 | output_setting, 84 | )]); 85 | Ok(output) 86 | } 87 | 88 | #[zbus(signal)] 89 | pub async fn setting_changed( 90 | ctxt: &SignalEmitter<'_>, 91 | namespace: String, 92 | key: String, 93 | value: OwnedValue, 94 | ) -> zbus::Result<()>; 95 | // add code here 96 | } 97 | -------------------------------------------------------------------------------- /src/settings/config.rs: -------------------------------------------------------------------------------- 1 | use serde::Deserialize; 2 | use std::io::Read; 3 | const DEFAULT_COLOR_NAME: &str = "default"; 4 | const DARK_COLOR_NAME: &str = "dark"; 5 | const LIGHT_COLOR_NAME: &str = "light"; 6 | 7 | const DEFAULT_ACCENT_COLLOR: &str = "#ffffff"; 8 | 9 | #[derive(Deserialize, PartialEq, Eq, Debug)] 10 | pub struct SettingsConfig { 11 | pub color_scheme: String, 12 | pub accent_color: String, 13 | } 14 | 15 | impl SettingsConfig { 16 | pub fn get_color_scheme(&self) -> u32 { 17 | match self.color_scheme.as_str() { 18 | DEFAULT_COLOR_NAME => super::DEFAULT_COLOR, 19 | DARK_COLOR_NAME => super::DARK_COLOR, 20 | LIGHT_COLOR_NAME => super::LIGHT_COLOR, 21 | _ => unreachable!(), 22 | } 23 | } 24 | pub fn get_accent_color(&self) -> [f64; 3] { 25 | let color = csscolorparser::parse(&self.accent_color) 26 | .map(|color| color.to_rgba8()) 27 | .unwrap_or( 28 | csscolorparser::parse(DEFAULT_ACCENT_COLLOR) 29 | .unwrap() 30 | .to_rgba8(), 31 | ); 32 | [ 33 | color[0] as f64 / 256.0, 34 | color[1] as f64 / 256.0, 35 | color[2] as f64 / 256.0, 36 | ] 37 | } 38 | } 39 | 40 | impl Default for SettingsConfig { 41 | fn default() -> Self { 42 | SettingsConfig { 43 | color_scheme: DEFAULT_COLOR_NAME.to_string(), 44 | accent_color: DEFAULT_ACCENT_COLLOR.to_string(), 45 | } 46 | } 47 | } 48 | 49 | impl SettingsConfig { 50 | pub fn config_from_file() -> Self { 51 | let Ok(home) = std::env::var("HOME") else { 52 | return Self::default(); 53 | }; 54 | let config_path = std::path::Path::new(home.as_str()) 55 | .join(".config") 56 | .join("xdg-desktop-portal-luminous") 57 | .join("config.toml"); 58 | let Ok(mut file) = std::fs::OpenOptions::new().read(true).open(config_path) else { 59 | return Self::default(); 60 | }; 61 | let mut buf = String::new(); 62 | if file.read_to_string(&mut buf).is_err() { 63 | return Self::default(); 64 | }; 65 | toml::from_str(&buf).unwrap_or(Self::default()) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/utils.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | 3 | use std::sync::LazyLock; 4 | 5 | pub static USER_RUNNING_DIR: LazyLock = LazyLock::new(|| { 6 | let cache_dir = std::env::var("XDG_RUNTIME_DIR").unwrap_or("/tmp".to_string()); 7 | PathBuf::from(cache_dir) 8 | }); 9 | --------------------------------------------------------------------------------