├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── flake.lock ├── flake.nix ├── nix ├── repo │ ├── configs.nix │ └── devshells.nix └── watershot │ └── packages.nix ├── res ├── color_shapes.wgsl └── texture.wgsl └── src ├── macros.rs ├── main.rs ├── rendering.rs ├── runtime_data.rs ├── sctk_impls ├── compositor_handler.rs ├── keyboard_handler.rs ├── layer_shell_handler.rs ├── output_handler.rs ├── pointer_handler.rs ├── provides_registry_state.rs ├── seat_handler.rs └── shm_handler.rs ├── traits.rs ├── types.rs └── window ├── hyprland.rs ├── mod.rs └── search.rs /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | result 3 | 4 | # nixago: ignore-linked-files 5 | /treefmt.toml 6 | 7 | # VS Code user settings 8 | /.vscode/settings.json 9 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "watershot" 3 | authors = ["Kirottu"] 4 | version = "0.2.2" 5 | edition = "2021" 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | smithay-client-toolkit = "0.17.0" 11 | wayland-backend = { version = "0.1.2", features = ["client_system"] } 12 | wayland-client = { version = "0.30.2", features = ["calloop"] } 13 | image = { version = "0.24.5", default-features = false, features = ["pnm", "farbfeld", "jpeg", "jpeg_rayon", "png", "webp-encoder"] } 14 | ron = "0.8.0" 15 | serde = { version = "1.0.152", features = ["derive"] } 16 | fontconfig = "0.6.0" 17 | wl-clipboard-rs = "0.7.0" 18 | nix = { version = "0.26.1", default-features = false, features = ["process"] } 19 | clap = { version = "4.0.32", features = ["derive"] } 20 | chrono = "0.4.23" 21 | env_logger = { version = "0.10.0", default-features = false, features = ["auto-color"] } 22 | log = "0.4.17" 23 | wgpu = "0.17.0" 24 | raw-window-handle = "0.5.2" 25 | pollster = "0.3.0" 26 | bytemuck = { version = "1.13.1", features = ["derive"] } 27 | wgpu_text = "0.8.3" 28 | regex = "1.9.1" 29 | hyprland = "0.4.0-alpha.2" 30 | strum = { version = "0.25.0", features = ["derive", "strum_macros"] } 31 | -------------------------------------------------------------------------------- /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 | # Watershot 2 | 3 | A simple wayland native screenshot tool inspired by [Flameshot](https://flameshot.org/). 4 | 5 | ## Packages 6 | 7 | On an Arch based distro, you can simply install the AUR package [watershot](https://aur.archlinux.org/packages/watershot), 8 | or [watershot-git](https://aur.archlinux.org/packages/watershot-git) for the latest git version. 9 | 10 | This project is a flake! If you have Nix, you know what to do. 11 | 12 | ## Manual installation 13 | 14 | ### Dependencies 15 | 16 | Some of the cargo packages rely on system dependencies. For Nix based systems 17 | this is handled with flake.nix, but other systems may need to manually install: 18 | 19 | - fontconfig 20 | - pkgconfig 21 | - libxkbcommon 22 | 23 | ### Installation 24 | 25 | Simply clone the repository and install the program locally with cargo. You will 26 | need to have [grim](https://sr.ht/~emersion/grim/), if it is in a non-standard 27 | location you can use `--grim` or `-g` argument to set a custom path. A 28 | compositor that implements layer-shell is also a requirement. 29 | 30 | ``` 31 | git clone https://github.com/Kirottu/watershot 32 | cd watershot 33 | cargo install --path . 34 | ``` 35 | 36 | ## Usage 37 | 38 | Just run the executable. Do note that without any arguments, the screenshots are 39 | not saved/copied anywhere. 40 | 41 | ``` 42 | Commands: 43 | path The path to save the image to 44 | directory The directory to save the image to with a generated name 45 | help Print this message or the help of the given subcommand(s) 46 | 47 | Options: 48 | -c, --copy Copy the screenshot after exit 49 | -s, --stdout Output the screenshot into stdout in PNG format 50 | -g, --grim Path to the `grim` executable 51 | -h, --help Print help 52 | -V, --version Print version 53 | ``` 54 | 55 | ## Configuration 56 | 57 | Watershot supports configuration of colors, fonts, sizes, etc. via it's config 58 | file. The config file is saved in `~/.config/watershot.ron` and uses the ron 59 | config format. 60 | 61 | Here is an example config for it: 62 | 63 | ``` 64 | Config( 65 | handle_radius: 10, 66 | line_width: 2, 67 | display_highlight_width: 5, 68 | selection_color: Color( 69 | r: 0.38, 70 | g: 0.68, 71 | b: 0.94, 72 | a: 1.0, 73 | ), 74 | shade_color: Color( 75 | r: 0.11, 76 | g: 0.0, 77 | b: 0.11, 78 | a: 0.6, 79 | ), 80 | text_color: Color( 81 | r: 1.0, 82 | g: 1.0, 83 | b: 1.0, 84 | a: 1.0, 85 | ), 86 | mode_text_size: 50, 87 | font_family: "monospace", 88 | ) 89 | ``` 90 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "blank": { 4 | "locked": { 5 | "lastModified": 1625557891, 6 | "narHash": "sha256-O8/MWsPBGhhyPoPLHZAuoZiiHo9q6FLlEeIDEXuj6T4=", 7 | "owner": "divnix", 8 | "repo": "blank", 9 | "rev": "5a5d2684073d9f563072ed07c871d577a6c614a8", 10 | "type": "github" 11 | }, 12 | "original": { 13 | "owner": "divnix", 14 | "repo": "blank", 15 | "type": "github" 16 | } 17 | }, 18 | "crane": { 19 | "inputs": { 20 | "flake-compat": "flake-compat", 21 | "flake-utils": "flake-utils_2", 22 | "nixpkgs": [ 23 | "std", 24 | "paisano-mdbook-preprocessor", 25 | "nixpkgs" 26 | ], 27 | "rust-overlay": "rust-overlay" 28 | }, 29 | "locked": { 30 | "lastModified": 1676162383, 31 | "narHash": "sha256-krUCKdz7ebHlFYm/A7IbKDnj2ZmMMm3yIEQcooqm7+E=", 32 | "owner": "ipetkov", 33 | "repo": "crane", 34 | "rev": "6fb400ec631b22ccdbc7090b38207f7fb5cfb5f2", 35 | "type": "github" 36 | }, 37 | "original": { 38 | "owner": "ipetkov", 39 | "repo": "crane", 40 | "type": "github" 41 | } 42 | }, 43 | "devshell": { 44 | "inputs": { 45 | "flake-utils": [ 46 | "std", 47 | "flake-utils" 48 | ], 49 | "nixpkgs": [ 50 | "std", 51 | "nixpkgs" 52 | ] 53 | }, 54 | "locked": { 55 | "lastModified": 1682700442, 56 | "narHash": "sha256-qjaAAcCYgp1pBBG7mY9z95ODUBZMtUpf0Qp3Gt/Wha0=", 57 | "owner": "numtide", 58 | "repo": "devshell", 59 | "rev": "fb6673fe9fe4409e3f43ca86968261e970918a83", 60 | "type": "github" 61 | }, 62 | "original": { 63 | "owner": "numtide", 64 | "repo": "devshell", 65 | "type": "github" 66 | } 67 | }, 68 | "dmerge": { 69 | "inputs": { 70 | "nixlib": [ 71 | "std", 72 | "nixpkgs" 73 | ], 74 | "yants": [ 75 | "std", 76 | "yants" 77 | ] 78 | }, 79 | "locked": { 80 | "lastModified": 1659548052, 81 | "narHash": "sha256-fzI2gp1skGA8mQo/FBFrUAtY0GQkAIAaV/V127TJPyY=", 82 | "owner": "divnix", 83 | "repo": "data-merge", 84 | "rev": "d160d18ce7b1a45b88344aa3f13ed1163954b497", 85 | "type": "github" 86 | }, 87 | "original": { 88 | "owner": "divnix", 89 | "repo": "data-merge", 90 | "type": "github" 91 | } 92 | }, 93 | "fenix": { 94 | "inputs": { 95 | "nixpkgs": "nixpkgs_2", 96 | "rust-analyzer-src": "rust-analyzer-src" 97 | }, 98 | "locked": { 99 | "lastModified": 1677306201, 100 | "narHash": "sha256-VZ9x7qdTosFvVsrpgFHrtYfT6PU3yMIs7NRYn9ELapI=", 101 | "owner": "nix-community", 102 | "repo": "fenix", 103 | "rev": "0923f0c162f65ae40261ec940406049726cfeab4", 104 | "type": "github" 105 | }, 106 | "original": { 107 | "owner": "nix-community", 108 | "repo": "fenix", 109 | "type": "github" 110 | } 111 | }, 112 | "flake-compat": { 113 | "flake": false, 114 | "locked": { 115 | "lastModified": 1673956053, 116 | "narHash": "sha256-4gtG9iQuiKITOjNQQeQIpoIB6b16fm+504Ch3sNKLd8=", 117 | "owner": "edolstra", 118 | "repo": "flake-compat", 119 | "rev": "35bb57c0c8d8b62bbfd284272c928ceb64ddbde9", 120 | "type": "github" 121 | }, 122 | "original": { 123 | "owner": "edolstra", 124 | "repo": "flake-compat", 125 | "type": "github" 126 | } 127 | }, 128 | "flake-utils": { 129 | "locked": { 130 | "lastModified": 1659877975, 131 | "narHash": "sha256-zllb8aq3YO3h8B/U0/J1WBgAL8EX5yWf5pMj3G0NAmc=", 132 | "owner": "numtide", 133 | "repo": "flake-utils", 134 | "rev": "c0e246b9b83f637f4681389ecabcb2681b4f3af0", 135 | "type": "github" 136 | }, 137 | "original": { 138 | "owner": "numtide", 139 | "repo": "flake-utils", 140 | "type": "github" 141 | } 142 | }, 143 | "flake-utils_2": { 144 | "locked": { 145 | "lastModified": 1667395993, 146 | "narHash": "sha256-nuEHfE/LcWyuSWnS8t12N1wc105Qtau+/OdUAjtQ0rA=", 147 | "owner": "numtide", 148 | "repo": "flake-utils", 149 | "rev": "5aed5285a952e0b949eb3ba02c12fa4fcfef535f", 150 | "type": "github" 151 | }, 152 | "original": { 153 | "owner": "numtide", 154 | "repo": "flake-utils", 155 | "type": "github" 156 | } 157 | }, 158 | "incl": { 159 | "inputs": { 160 | "nixlib": [ 161 | "std", 162 | "nixpkgs" 163 | ] 164 | }, 165 | "locked": { 166 | "lastModified": 1669263024, 167 | "narHash": "sha256-E/+23NKtxAqYG/0ydYgxlgarKnxmDbg6rCMWnOBqn9Q=", 168 | "owner": "divnix", 169 | "repo": "incl", 170 | "rev": "ce7bebaee048e4cd7ebdb4cee7885e00c4e2abca", 171 | "type": "github" 172 | }, 173 | "original": { 174 | "owner": "divnix", 175 | "repo": "incl", 176 | "type": "github" 177 | } 178 | }, 179 | "n2c": { 180 | "inputs": { 181 | "flake-utils": [ 182 | "std", 183 | "flake-utils" 184 | ], 185 | "nixpkgs": [ 186 | "std", 187 | "nixpkgs" 188 | ] 189 | }, 190 | "locked": { 191 | "lastModified": 1677330646, 192 | "narHash": "sha256-hUYCwJneMjnxTvj30Fjow6UMJUITqHlpUGpXMPXUJsU=", 193 | "owner": "nlewo", 194 | "repo": "nix2container", 195 | "rev": "ebca8f58d450cae1a19c07701a5a8ae40afc9efc", 196 | "type": "github" 197 | }, 198 | "original": { 199 | "owner": "nlewo", 200 | "repo": "nix2container", 201 | "type": "github" 202 | } 203 | }, 204 | "nixago": { 205 | "inputs": { 206 | "flake-utils": [ 207 | "std", 208 | "flake-utils" 209 | ], 210 | "nixago-exts": [ 211 | "std", 212 | "blank" 213 | ], 214 | "nixpkgs": [ 215 | "std", 216 | "nixpkgs" 217 | ] 218 | }, 219 | "locked": { 220 | "lastModified": 1683210100, 221 | "narHash": "sha256-bhGDOlkWtlhVECpoOog4fWiFJmLCpVEg09a40aTjCbw=", 222 | "owner": "nix-community", 223 | "repo": "nixago", 224 | "rev": "1da60ad9412135f9ed7a004669fdcf3d378ec630", 225 | "type": "github" 226 | }, 227 | "original": { 228 | "owner": "nix-community", 229 | "repo": "nixago", 230 | "type": "github" 231 | } 232 | }, 233 | "nixpkgs": { 234 | "locked": { 235 | "lastModified": 1683442750, 236 | "narHash": "sha256-IiJ0WWW6OcCrVFl1ijE+gTaP0ChFfV6dNkJR05yStmw=", 237 | "owner": "NixOS", 238 | "repo": "nixpkgs", 239 | "rev": "eb751d65225ec53de9cf3d88acbf08d275882389", 240 | "type": "github" 241 | }, 242 | "original": { 243 | "owner": "NixOS", 244 | "ref": "nixpkgs-unstable", 245 | "repo": "nixpkgs", 246 | "type": "github" 247 | } 248 | }, 249 | "nixpkgs_2": { 250 | "locked": { 251 | "lastModified": 1677063315, 252 | "narHash": "sha256-qiB4ajTeAOVnVSAwCNEEkoybrAlA+cpeiBxLobHndE8=", 253 | "owner": "nixos", 254 | "repo": "nixpkgs", 255 | "rev": "988cc958c57ce4350ec248d2d53087777f9e1949", 256 | "type": "github" 257 | }, 258 | "original": { 259 | "owner": "nixos", 260 | "ref": "nixos-unstable", 261 | "repo": "nixpkgs", 262 | "type": "github" 263 | } 264 | }, 265 | "nosys": { 266 | "locked": { 267 | "lastModified": 1668010795, 268 | "narHash": "sha256-JBDVBnos8g0toU7EhIIqQ1If5m/nyBqtHhL3sicdPwI=", 269 | "owner": "divnix", 270 | "repo": "nosys", 271 | "rev": "feade0141487801c71ff55623b421ed535dbdefa", 272 | "type": "github" 273 | }, 274 | "original": { 275 | "owner": "divnix", 276 | "repo": "nosys", 277 | "type": "github" 278 | } 279 | }, 280 | "paisano": { 281 | "inputs": { 282 | "nixpkgs": [ 283 | "std", 284 | "nixpkgs" 285 | ], 286 | "nosys": "nosys", 287 | "yants": [ 288 | "std", 289 | "yants" 290 | ] 291 | }, 292 | "locked": { 293 | "lastModified": 1678949904, 294 | "narHash": "sha256-oAoF66hYYz1RPh3lEwb9/4e4iyBAfTbQKZRRQ8gP0Ds=", 295 | "owner": "paisano-nix", 296 | "repo": "core", 297 | "rev": "88f2aff10a5064551d1d4cb86800d17084489ce3", 298 | "type": "github" 299 | }, 300 | "original": { 301 | "owner": "paisano-nix", 302 | "repo": "core", 303 | "type": "github" 304 | } 305 | }, 306 | "paisano-actions": { 307 | "inputs": { 308 | "nixpkgs": [ 309 | "std", 310 | "paisano-mdbook-preprocessor", 311 | "nixpkgs" 312 | ] 313 | }, 314 | "locked": { 315 | "lastModified": 1677306424, 316 | "narHash": "sha256-H9/dI2rGEbKo4KEisqbRPHFG2ajF8Tm111NPdKGIf28=", 317 | "owner": "paisano-nix", 318 | "repo": "actions", 319 | "rev": "65ec4e080b3480167fc1a748c89a05901eea9a9b", 320 | "type": "github" 321 | }, 322 | "original": { 323 | "owner": "paisano-nix", 324 | "repo": "actions", 325 | "type": "github" 326 | } 327 | }, 328 | "paisano-mdbook-preprocessor": { 329 | "inputs": { 330 | "crane": "crane", 331 | "fenix": "fenix", 332 | "nixpkgs": [ 333 | "std", 334 | "nixpkgs" 335 | ], 336 | "paisano-actions": "paisano-actions", 337 | "std": [ 338 | "std" 339 | ] 340 | }, 341 | "locked": { 342 | "lastModified": 1680654400, 343 | "narHash": "sha256-Qdpio+ldhUK3zfl22Mhf8HUULdUOJXDWDdO7MIK69OU=", 344 | "owner": "paisano-nix", 345 | "repo": "mdbook-paisano-preprocessor", 346 | "rev": "11a8fc47f574f194a7ae7b8b98001f6143ba4cf1", 347 | "type": "github" 348 | }, 349 | "original": { 350 | "owner": "paisano-nix", 351 | "repo": "mdbook-paisano-preprocessor", 352 | "type": "github" 353 | } 354 | }, 355 | "paisano-tui": { 356 | "inputs": { 357 | "nixpkgs": [ 358 | "std", 359 | "blank" 360 | ], 361 | "std": [ 362 | "std" 363 | ] 364 | }, 365 | "locked": { 366 | "lastModified": 1681847764, 367 | "narHash": "sha256-mdd7PJW1BZvxy0cIKsPfAO+ohVl/V7heE5ZTAHzTdv8=", 368 | "owner": "paisano-nix", 369 | "repo": "tui", 370 | "rev": "3096bad91cae73ab8ab3367d31f8a143d248a244", 371 | "type": "github" 372 | }, 373 | "original": { 374 | "owner": "paisano-nix", 375 | "ref": "0.1.1", 376 | "repo": "tui", 377 | "type": "github" 378 | } 379 | }, 380 | "root": { 381 | "inputs": { 382 | "nixpkgs": "nixpkgs", 383 | "std": "std" 384 | } 385 | }, 386 | "rust-analyzer-src": { 387 | "flake": false, 388 | "locked": { 389 | "lastModified": 1677221702, 390 | "narHash": "sha256-1M+58rC4eTCWNmmX0hQVZP20t3tfYNunl9D/PrGUyGE=", 391 | "owner": "rust-lang", 392 | "repo": "rust-analyzer", 393 | "rev": "f5401f620699b26ed9d47a1d2e838143a18dbe3b", 394 | "type": "github" 395 | }, 396 | "original": { 397 | "owner": "rust-lang", 398 | "ref": "nightly", 399 | "repo": "rust-analyzer", 400 | "type": "github" 401 | } 402 | }, 403 | "rust-overlay": { 404 | "inputs": { 405 | "flake-utils": [ 406 | "std", 407 | "paisano-mdbook-preprocessor", 408 | "crane", 409 | "flake-utils" 410 | ], 411 | "nixpkgs": [ 412 | "std", 413 | "paisano-mdbook-preprocessor", 414 | "crane", 415 | "nixpkgs" 416 | ] 417 | }, 418 | "locked": { 419 | "lastModified": 1675391458, 420 | "narHash": "sha256-ukDKZw922BnK5ohL9LhwtaDAdCsJL7L6ScNEyF1lO9w=", 421 | "owner": "oxalica", 422 | "repo": "rust-overlay", 423 | "rev": "383a4acfd11d778d5c2efcf28376cbd845eeaedf", 424 | "type": "github" 425 | }, 426 | "original": { 427 | "owner": "oxalica", 428 | "repo": "rust-overlay", 429 | "type": "github" 430 | } 431 | }, 432 | "std": { 433 | "inputs": { 434 | "arion": [ 435 | "std", 436 | "blank" 437 | ], 438 | "blank": "blank", 439 | "devshell": "devshell", 440 | "dmerge": "dmerge", 441 | "flake-utils": "flake-utils", 442 | "incl": "incl", 443 | "makes": [ 444 | "std", 445 | "blank" 446 | ], 447 | "microvm": [ 448 | "std", 449 | "blank" 450 | ], 451 | "n2c": "n2c", 452 | "nixago": "nixago", 453 | "nixpkgs": [ 454 | "nixpkgs" 455 | ], 456 | "paisano": "paisano", 457 | "paisano-mdbook-preprocessor": "paisano-mdbook-preprocessor", 458 | "paisano-tui": "paisano-tui", 459 | "yants": "yants" 460 | }, 461 | "locked": { 462 | "lastModified": 1683210511, 463 | "narHash": "sha256-Ag85i6rHubOLB6ChsqGUyZlB2SQCjF7Seo5q12g7jJk=", 464 | "owner": "divnix", 465 | "repo": "std", 466 | "rev": "562310786b998bf52bd02bf7ac6bfcc743e8d45d", 467 | "type": "github" 468 | }, 469 | "original": { 470 | "owner": "divnix", 471 | "repo": "std", 472 | "type": "github" 473 | } 474 | }, 475 | "yants": { 476 | "inputs": { 477 | "nixpkgs": [ 478 | "std", 479 | "nixpkgs" 480 | ] 481 | }, 482 | "locked": { 483 | "lastModified": 1667096281, 484 | "narHash": "sha256-wRRec6ze0gJHmGn6m57/zhz/Kdvp9HS4Nl5fkQ+uIuA=", 485 | "owner": "divnix", 486 | "repo": "yants", 487 | "rev": "d18f356ec25cb94dc9c275870c3a7927a10f8c3c", 488 | "type": "github" 489 | }, 490 | "original": { 491 | "owner": "divnix", 492 | "repo": "yants", 493 | "type": "github" 494 | } 495 | } 496 | }, 497 | "root": "root", 498 | "version": 7 499 | } 500 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "A simple wayland native screenshot tool"; 3 | 4 | inputs = { 5 | nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; 6 | std = { 7 | url = "github:divnix/std"; 8 | inputs.nixpkgs.follows = "nixpkgs"; 9 | }; 10 | }; 11 | 12 | outputs = { 13 | self, 14 | std, 15 | ... 16 | } @ inputs: 17 | std.growOn { 18 | inherit inputs; 19 | systems = ["x86_64-linux" "aarch64-linux"]; 20 | cellsFrom = self + "/nix"; 21 | cellBlocks = with std.blockTypes; [ 22 | (installables "packages") 23 | (devshells "devshells") 24 | (nixago "configs") 25 | ]; 26 | } 27 | { 28 | packages = std.harvest self ["watershot" "packages"]; 29 | devShells = std.harvest self ["repo" "devshells"]; 30 | }; 31 | } 32 | -------------------------------------------------------------------------------- /nix/repo/configs.nix: -------------------------------------------------------------------------------- 1 | { 2 | inputs, 3 | cell, 4 | }: let 5 | inherit (inputs) std nixpkgs; 6 | in { 7 | treefmt = std.lib.cfg.treefmt { 8 | data.formatter = { 9 | nix = { 10 | command = "alejandra"; 11 | includes = ["*.nix"]; 12 | }; 13 | prettier = { 14 | command = "prettier"; 15 | options = ["--plugin" "prettier-plugin-toml" "--write"]; 16 | includes = [ 17 | "*.md" 18 | "*.mdx" 19 | "*.toml" 20 | ]; 21 | }; 22 | rustfmt = { 23 | command = "rustfmt"; 24 | includes = [ 25 | "*.rs" 26 | ]; 27 | }; 28 | }; 29 | packages = with nixpkgs; [ 30 | alejandra 31 | nodePackages.prettier 32 | nodePackages.prettier-plugin-toml 33 | rustfmt 34 | ]; 35 | devshell.startup.prettier-plugin-toml = nixpkgs.lib.stringsWithDeps.noDepEntry '' 36 | export NODE_PATH=${nixpkgs.nodePackages.prettier-plugin-toml}/lib/node_modules:$NODE_PATH 37 | ''; 38 | }; 39 | } 40 | -------------------------------------------------------------------------------- /nix/repo/devshells.nix: -------------------------------------------------------------------------------- 1 | { 2 | inputs, 3 | cell, 4 | }: let 5 | inherit (inputs) nixpkgs std; 6 | in 7 | nixpkgs.lib.mapAttrs (_: std.lib.dev.mkShell) { 8 | default = { 9 | name = "Watershot"; 10 | packages = with nixpkgs; [ 11 | rustc 12 | cargo 13 | rustfmt 14 | clippy 15 | 16 | fontconfig 17 | pkgconfig 18 | libxkbcommon 19 | grim 20 | ]; 21 | nixago = with cell.configs; [ 22 | treefmt 23 | ]; 24 | }; 25 | } 26 | -------------------------------------------------------------------------------- /nix/watershot/packages.nix: -------------------------------------------------------------------------------- 1 | { 2 | inputs, 3 | cell, 4 | }: let 5 | inherit (inputs) self std nixpkgs; 6 | inherit (nixpkgs) rustPlatform; 7 | cargo = builtins.fromTOML (builtins.readFile (self + "/Cargo.toml")); 8 | in { 9 | default = rustPlatform.buildRustPackage { 10 | pname = cargo.package.name; 11 | version = cargo.package.version; 12 | 13 | src = std.incl self [ 14 | "Cargo.toml" 15 | "Cargo.lock" 16 | "src" 17 | "res" 18 | ]; 19 | 20 | cargoLock.lockFile = self + "/Cargo.lock"; 21 | 22 | nativeBuildInputs = with nixpkgs; [ 23 | pkg-config 24 | makeWrapper 25 | ]; 26 | 27 | buildInputs = with nixpkgs; [ 28 | fontconfig 29 | libxkbcommon 30 | wayland 31 | vulkan-loader 32 | libGL 33 | ]; 34 | 35 | postFixup = '' 36 | patchelf --add-rpath ${nixpkgs.vulkan-loader}/lib $out/bin/watershot 37 | patchelf --add-rpath ${nixpkgs.libGL}/lib $out/bin/watershot 38 | 39 | wrapProgram $out/bin/watershot \ 40 | --add-flags "-g \"${nixpkgs.grim}/bin/grim\"" 41 | ''; 42 | }; 43 | } 44 | -------------------------------------------------------------------------------- /res/color_shapes.wgsl: -------------------------------------------------------------------------------- 1 | @group(0) @binding(0) 2 | var color: vec4; 3 | 4 | @vertex 5 | fn vs_main(@location(0) pos: vec2) -> @builtin(position) vec4 { 6 | return vec4(pos, 0.0, 1.0); 7 | } 8 | 9 | @fragment 10 | fn fs_main(@builtin(position) pos: vec4) -> @location(0) vec4 { 11 | return color; 12 | } -------------------------------------------------------------------------------- /res/texture.wgsl: -------------------------------------------------------------------------------- 1 | @group(0) @binding(0) 2 | var tex: texture_2d; 3 | @group(0) @binding(1) 4 | var tex_sampler: sampler; 5 | 6 | struct VertexInput { 7 | @location(0) pos: vec2, 8 | @location(1) tex_pos: vec2, 9 | } 10 | 11 | struct VertexOutput { 12 | @builtin(position) clip_position: vec4, 13 | @location(0) tex_pos: vec2, 14 | }; 15 | 16 | @vertex 17 | fn vs_main(in: VertexInput) -> VertexOutput { 18 | var out: VertexOutput; 19 | out.tex_pos = in.tex_pos; 20 | out.clip_position = vec4(in.pos, 0.0, 1.0); 21 | return out; 22 | } 23 | 24 | @fragment 25 | fn fs_main(in: VertexOutput) -> @location(0) vec4 { 26 | return textureSample(tex, tex_sampler, in.tex_pos); 27 | } -------------------------------------------------------------------------------- /src/macros.rs: -------------------------------------------------------------------------------- 1 | /// Get the handle positions for altering the selection 2 | #[macro_export] 3 | macro_rules! handles { 4 | ($extents:expr) => { 5 | &[ 6 | // Corners 7 | ( 8 | $extents.start_x, 9 | $extents.start_y, 10 | $crate::types::SelectionModifier::TopLeft, 11 | ), 12 | ( 13 | $extents.end_x, 14 | $extents.start_y, 15 | $crate::types::SelectionModifier::TopRight, 16 | ), 17 | ( 18 | $extents.end_x, 19 | $extents.end_y, 20 | $crate::types::SelectionModifier::BottomRight, 21 | ), 22 | ( 23 | $extents.start_x, 24 | $extents.end_y, 25 | $crate::types::SelectionModifier::BottomLeft, 26 | ), 27 | // Edges 28 | ( 29 | $extents.start_x + ($extents.end_x - $extents.start_x) / 2, 30 | $extents.start_y, 31 | $crate::types::SelectionModifier::Top, 32 | ), 33 | ( 34 | $extents.end_x, 35 | $extents.start_y + ($extents.end_y - $extents.start_y) / 2, 36 | $crate::types::SelectionModifier::Right, 37 | ), 38 | ( 39 | $extents.start_x + ($extents.end_x - $extents.start_x) / 2, 40 | $extents.end_y, 41 | $crate::types::SelectionModifier::Bottom, 42 | ), 43 | ( 44 | $extents.start_x, 45 | $extents.start_y + ($extents.end_y - $extents.start_y) / 2, 46 | $crate::types::SelectionModifier::Left, 47 | ), 48 | ] 49 | }; 50 | } 51 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use std::io::{self, Cursor, Write}; 2 | 3 | use chrono::Local; 4 | use clap::Parser; 5 | use image::{DynamicImage, ImageFormat}; 6 | use log::{error, info}; 7 | use runtime_data::RuntimeData; 8 | use smithay_client_toolkit::reexports::client::{globals::registry_queue_init, Connection}; 9 | use traits::{Contains, ToLocal}; 10 | use types::{Args, Config, ExitState, Monitor, Rect, SaveLocation, Selection}; 11 | use wl_clipboard_rs::copy; 12 | 13 | mod macros; 14 | mod runtime_data; 15 | mod traits; 16 | mod types; 17 | 18 | pub mod window; 19 | 20 | mod sctk_impls { 21 | mod compositor_handler; 22 | mod keyboard_handler; 23 | mod layer_shell_handler; 24 | mod output_handler; 25 | mod pointer_handler; 26 | mod provides_registry_state; 27 | mod seat_handler; 28 | mod shm_handler; 29 | } 30 | mod rendering; 31 | 32 | fn main() { 33 | let args = Args::parse(); 34 | env_logger::init(); 35 | 36 | if let Some(image) = gui(&args) { 37 | // Save the file if an argument for that is present 38 | if let Some(save_location) = &args.save { 39 | match save_location { 40 | SaveLocation::Path { path } => { 41 | if let Err(why) = image.save(path) { 42 | error!("Error saving image: {}", why); 43 | } 44 | } 45 | SaveLocation::Directory { path } => { 46 | let local = Local::now(); 47 | if let Err(why) = image.save( 48 | local 49 | .format(&format!("{}/Watershot_%d-%m-%Y_%H:%M.png", path)) 50 | .to_string(), 51 | ) { 52 | error!("Error saving image: {}", why); 53 | } 54 | } 55 | } 56 | } 57 | 58 | // Save the selected image into the buffer 59 | let mut buf = Cursor::new(Vec::new()); 60 | image 61 | .write_to(&mut buf, ImageFormat::Png) 62 | .expect("Failed to write image to buffer as PNG"); 63 | 64 | let buf = buf.into_inner(); 65 | 66 | if args.stdout { 67 | if let Err(why) = io::stdout().lock().write_all(&buf) { 68 | error!("Failed to write image content to stdout: {}", why); 69 | } 70 | } 71 | 72 | // Fork to serve copy requests 73 | if args.copy { 74 | match unsafe { nix::unistd::fork() } { 75 | Ok(nix::unistd::ForkResult::Parent { .. }) => { 76 | info!("Forked to serve copy requests") 77 | } 78 | Ok(nix::unistd::ForkResult::Child) => { 79 | // Serve copy requests 80 | let mut opts = copy::Options::new(); 81 | opts.foreground(true); 82 | opts.copy( 83 | copy::Source::Bytes(buf.into_boxed_slice()), 84 | copy::MimeType::Autodetect, 85 | ) 86 | .expect("Failed to serve copied image"); 87 | } 88 | Err(why) => println!("Failed to fork: {}", why), 89 | } 90 | } 91 | } 92 | } 93 | 94 | fn gui(args: &Args) -> Option { 95 | let conn = Connection::connect_to_env(); 96 | if conn.is_err() { 97 | log::error!("Could not connect to the Wayland server, make sure you run watershot within a Wayland session!"); 98 | std::process::exit(1); 99 | } 100 | 101 | let conn = conn.unwrap(); 102 | 103 | let (globals, mut event_queue) = registry_queue_init(&conn).unwrap(); 104 | let qh = event_queue.handle(); 105 | let mut runtime_data = RuntimeData::new(&qh, &globals, args.clone()); 106 | 107 | // Fetch the outputs from the compositor 108 | event_queue.roundtrip(&mut runtime_data).unwrap(); 109 | // Has to be iterated first to get the full area size 110 | let sizes = runtime_data 111 | .output_state 112 | .outputs() 113 | .map(|output| { 114 | let info = runtime_data.output_state.info(&output).unwrap(); 115 | let size = info 116 | .logical_size 117 | .map(|(w, h)| (w as u32, h as u32)) 118 | .expect("Can't determine monitor size!"); 119 | let pos = info 120 | .logical_position 121 | .expect("Can't determine monitor position!"); 122 | 123 | let rect = Rect { 124 | x: pos.0, 125 | y: pos.1, 126 | width: size.0 as i32, 127 | height: size.1 as i32, 128 | }; 129 | 130 | // Extend the area spanning all monitors with the current monitor 131 | runtime_data.area.extend(&rect); 132 | (rect, output, info) 133 | }) 134 | .collect::>(); 135 | 136 | runtime_data.scale_factor = runtime_data.image.width() as f32 / runtime_data.area.width as f32; 137 | 138 | for (rect, output, info) in sizes { 139 | runtime_data 140 | .monitors 141 | .push(Monitor::new(rect, &qh, &conn, output, info, &runtime_data)); 142 | } 143 | 144 | event_queue.roundtrip(&mut runtime_data).unwrap(); 145 | 146 | loop { 147 | event_queue.blocking_dispatch(&mut runtime_data).unwrap(); 148 | match runtime_data.exit { 149 | ExitState::ExitOnly => return None, 150 | ExitState::ExitWithSelection(rect) => { 151 | let image = match runtime_data.monitors.into_iter().find_map(|mon| { 152 | if mon.rect.contains(&rect) { 153 | Some(mon) 154 | } else { 155 | None 156 | } 157 | }) { 158 | Some(mon) => { 159 | let rect = rect.to_local(&mon.rect); 160 | mon.image.crop_imm( 161 | rect.x as u32, 162 | rect.y as u32, 163 | rect.width as u32, 164 | rect.height as u32, 165 | ) 166 | } 167 | None => runtime_data.image.crop_imm( 168 | (rect.x as f32 * runtime_data.scale_factor) as u32, 169 | (rect.y as f32 * runtime_data.scale_factor) as u32, 170 | (rect.width as f32 * runtime_data.scale_factor) as u32, 171 | (rect.height as f32 * runtime_data.scale_factor) as u32, 172 | ), 173 | }; 174 | 175 | return Some(image); 176 | } 177 | ExitState::None => (), 178 | } 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /src/rendering.rs: -------------------------------------------------------------------------------- 1 | use image::RgbaImage; 2 | use smithay_client_toolkit::output::OutputInfo; 3 | use wgpu::util::DeviceExt; 4 | use wgpu_text::glyph_brush::{HorizontalAlign, Layout, OwnedSection, OwnedText, VerticalAlign}; 5 | 6 | use crate::{ 7 | handles, 8 | runtime_data::RuntimeData, 9 | traits::{Padded, ToLocal, ToRender}, 10 | types::{Config, Monitor, Rect, Selection}, 11 | }; 12 | 13 | use wayland_client::protocol::wl_surface; 14 | 15 | const TOP_LEFT: [f32; 2] = [-1.0, 1.0]; 16 | const BOTTOM_LEFT: [f32; 2] = [-1.0, -1.0]; 17 | const TOP_RIGHT: [f32; 2] = [1.0, 1.0]; 18 | const BOTTOM_RIGHT: [f32; 2] = [1.0, -1.0]; 19 | 20 | const RECT_VERTICES: &[[f32; 2]] = &[TOP_RIGHT, TOP_LEFT, BOTTOM_LEFT, BOTTOM_RIGHT]; 21 | 22 | const RECT_INDICES: &[u32] = &[0, 1, 2, 0, 2, 3]; 23 | 24 | pub const CIRCLE_EDGES: u32 = 64; 25 | // 3 indices per edge/triangle 26 | // 8 circles per selection highlight 27 | // 24 indices from the selection highlight rectangle 28 | const MAX_SEL_INDICES: u64 = CIRCLE_EDGES as u64 * 3 * 8 + 24; 29 | 30 | const OVERLAY_MSAA: u32 = 4; 31 | 32 | pub struct Renderer { 33 | // Pipelines 34 | tex_pipeline: wgpu::RenderPipeline, 35 | tex_layout: wgpu::BindGroupLayout, 36 | tex_sampler: wgpu::Sampler, 37 | tex_vertex_buffer: wgpu::Buffer, 38 | 39 | overlay_pipeline: wgpu::RenderPipeline, 40 | shade_bind_group: wgpu::BindGroup, 41 | sel_bind_group: wgpu::BindGroup, 42 | } 43 | 44 | /// Monitor specific rendering related items 45 | pub struct MonSpecificRendering { 46 | /// Bind group for the background texture 47 | bg_bind_group: wgpu::BindGroup, 48 | 49 | shade_index_count: u32, 50 | shade_vertex_buffer: wgpu::Buffer, 51 | shade_index_buffer: wgpu::Buffer, 52 | 53 | sel_index_count: u32, 54 | sel_vertex_buffer: wgpu::Buffer, 55 | sel_index_buffer: wgpu::Buffer, 56 | 57 | /// Texture to render the overlay with anti-aliasing 58 | ms_tex: wgpu::TextureView, 59 | /// The target to resolve to when rendering the multisampled overlay 60 | ms_resolve_target_tex: wgpu::TextureView, 61 | /// Bind group for the resolve target texture 62 | ms_bind_group: wgpu::BindGroup, 63 | 64 | pub brush: wgpu_text::TextBrush, 65 | rect_mode_section: OwnedSection, 66 | display_mode_section: OwnedSection, 67 | window_mode_section: OwnedSection, 68 | } 69 | 70 | impl Renderer { 71 | pub fn new(device: &wgpu::Device, config: &Config, format: wgpu::TextureFormat) -> Self { 72 | let tex_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor { 73 | label: Some("Background shader"), 74 | source: wgpu::ShaderSource::Wgsl(include_str!("../res/texture.wgsl").into()), 75 | }); 76 | 77 | let tex_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { 78 | entries: &[ 79 | wgpu::BindGroupLayoutEntry { 80 | binding: 0, 81 | visibility: wgpu::ShaderStages::FRAGMENT, 82 | ty: wgpu::BindingType::Texture { 83 | sample_type: wgpu::TextureSampleType::Float { filterable: true }, 84 | view_dimension: wgpu::TextureViewDimension::D2, 85 | multisampled: false, 86 | }, 87 | count: None, 88 | }, 89 | wgpu::BindGroupLayoutEntry { 90 | binding: 1, 91 | visibility: wgpu::ShaderStages::FRAGMENT, 92 | ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), 93 | count: None, 94 | }, 95 | ], 96 | label: Some("Background bind group layout"), 97 | }); 98 | 99 | let tex_pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { 100 | label: Some("Background pipeline layout"), 101 | bind_group_layouts: &[&tex_layout], 102 | push_constant_ranges: &[], 103 | }); 104 | 105 | let tex_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { 106 | label: Some("Background pipeline"), 107 | layout: Some(&tex_pipeline_layout), 108 | vertex: wgpu::VertexState { 109 | module: &tex_shader, 110 | entry_point: "vs_main", 111 | buffers: &[TexVertex::desc()], 112 | }, 113 | fragment: Some(wgpu::FragmentState { 114 | module: &tex_shader, 115 | entry_point: "fs_main", 116 | targets: &[Some(wgpu::ColorTargetState { 117 | format, 118 | blend: Some(wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING), 119 | write_mask: wgpu::ColorWrites::ALL, 120 | })], 121 | }), 122 | primitive: wgpu::PrimitiveState { 123 | topology: wgpu::PrimitiveTopology::TriangleList, 124 | strip_index_format: None, 125 | front_face: wgpu::FrontFace::Ccw, 126 | cull_mode: Some(wgpu::Face::Back), 127 | polygon_mode: wgpu::PolygonMode::Fill, 128 | unclipped_depth: false, 129 | conservative: false, 130 | }, 131 | depth_stencil: None, 132 | multisample: wgpu::MultisampleState { 133 | count: 1, 134 | mask: !0, 135 | alpha_to_coverage_enabled: false, 136 | }, 137 | multiview: None, 138 | }); 139 | 140 | let tex_sampler = device.create_sampler(&wgpu::SamplerDescriptor { 141 | address_mode_u: wgpu::AddressMode::Repeat, 142 | address_mode_v: wgpu::AddressMode::Repeat, 143 | address_mode_w: wgpu::AddressMode::Repeat, 144 | mag_filter: wgpu::FilterMode::Linear, 145 | min_filter: wgpu::FilterMode::Nearest, 146 | mipmap_filter: wgpu::FilterMode::Nearest, 147 | ..Default::default() 148 | }); 149 | 150 | let tex_vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { 151 | label: Some("Background vertex buffer"), 152 | contents: bytemuck::cast_slice(TexVertex::RECT_VERTICES), 153 | usage: wgpu::BufferUsages::VERTEX, 154 | }); 155 | 156 | let color_shapes_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor { 157 | label: Some("Color shape shader"), 158 | source: wgpu::ShaderSource::Wgsl(include_str!("../res/color_shapes.wgsl").into()), 159 | }); 160 | 161 | let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { 162 | label: Some("Overlay bind group layout"), 163 | entries: &[wgpu::BindGroupLayoutEntry { 164 | binding: 0, 165 | visibility: wgpu::ShaderStages::FRAGMENT, 166 | ty: wgpu::BindingType::Buffer { 167 | ty: wgpu::BufferBindingType::Uniform, 168 | has_dynamic_offset: false, 169 | min_binding_size: None, 170 | }, 171 | count: None, 172 | }], 173 | }); 174 | 175 | let overlay_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { 176 | label: Some("Overlay render pipeline layout"), 177 | bind_group_layouts: &[&bind_group_layout], 178 | push_constant_ranges: &[], 179 | }); 180 | 181 | let overlay_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { 182 | label: Some("Overlay render pipeline"), 183 | layout: Some(&overlay_layout), 184 | vertex: wgpu::VertexState { 185 | module: &color_shapes_shader, 186 | entry_point: "vs_main", 187 | buffers: &[OverlayVertex::desc()], 188 | }, 189 | fragment: Some(wgpu::FragmentState { 190 | module: &color_shapes_shader, 191 | entry_point: "fs_main", 192 | targets: &[Some(wgpu::ColorTargetState { 193 | format, 194 | blend: Some(wgpu::BlendState::ALPHA_BLENDING), 195 | write_mask: wgpu::ColorWrites::ALL, 196 | })], 197 | }), 198 | primitive: wgpu::PrimitiveState { 199 | topology: wgpu::PrimitiveTopology::TriangleList, 200 | strip_index_format: None, 201 | front_face: wgpu::FrontFace::Ccw, 202 | cull_mode: None, 203 | polygon_mode: wgpu::PolygonMode::Fill, 204 | unclipped_depth: false, 205 | conservative: false, 206 | }, 207 | depth_stencil: None, 208 | multisample: wgpu::MultisampleState { 209 | count: OVERLAY_MSAA, 210 | mask: !0, 211 | alpha_to_coverage_enabled: false, 212 | }, 213 | multiview: None, 214 | }); 215 | 216 | let shade_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { 217 | label: Some("Shade color uniform buffer"), 218 | contents: bytemuck::cast_slice(&[config.shade_color]), 219 | usage: wgpu::BufferUsages::UNIFORM, 220 | }); 221 | 222 | let shade_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { 223 | label: Some("Shade bind group"), 224 | layout: &bind_group_layout, 225 | entries: &[wgpu::BindGroupEntry { 226 | binding: 0, 227 | resource: shade_buffer.as_entire_binding(), 228 | }], 229 | }); 230 | 231 | let sel_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { 232 | label: Some("Selection color uniform buffer"), 233 | contents: bytemuck::cast_slice(&[config.selection_color]), 234 | usage: wgpu::BufferUsages::UNIFORM, 235 | }); 236 | 237 | let sel_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { 238 | label: Some("Selection bind group"), 239 | layout: &bind_group_layout, 240 | entries: &[wgpu::BindGroupEntry { 241 | binding: 0, 242 | resource: sel_buffer.as_entire_binding(), 243 | }], 244 | }); 245 | 246 | Self { 247 | tex_pipeline, 248 | tex_layout, 249 | tex_sampler, 250 | tex_vertex_buffer, 251 | overlay_pipeline, 252 | shade_bind_group, 253 | sel_bind_group, 254 | } 255 | } 256 | 257 | pub fn render( 258 | &self, 259 | encoder: &mut wgpu::CommandEncoder, 260 | surface_view: &wgpu::TextureView, 261 | monitor: &mut Monitor, 262 | selection: &Selection, 263 | device: &wgpu::Device, 264 | queue: &wgpu::Queue, 265 | ) { 266 | let Some(rendering) = &mut monitor.rendering else { 267 | return 268 | }; 269 | // Render the screenshot as the background 270 | { 271 | let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { 272 | label: None, 273 | color_attachments: &[Some(wgpu::RenderPassColorAttachment { 274 | view: surface_view, 275 | resolve_target: None, 276 | ops: wgpu::Operations { 277 | load: wgpu::LoadOp::Clear(wgpu::Color::BLACK), 278 | store: true, 279 | }, 280 | })], 281 | depth_stencil_attachment: None, 282 | }); 283 | render_pass.set_pipeline(&self.tex_pipeline); 284 | render_pass.set_vertex_buffer(0, self.tex_vertex_buffer.slice(..)); 285 | render_pass.set_bind_group(0, &rendering.bg_bind_group, &[]); 286 | render_pass.draw(0..6, 0..1); 287 | } 288 | // Draw the shade to the multisampling texture 289 | { 290 | let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { 291 | label: None, 292 | color_attachments: &[Some(wgpu::RenderPassColorAttachment { 293 | view: &rendering.ms_tex, 294 | resolve_target: None, 295 | ops: wgpu::Operations { 296 | load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT), 297 | store: true, 298 | }, 299 | })], 300 | depth_stencil_attachment: None, 301 | }); 302 | render_pass.set_pipeline(&self.overlay_pipeline); 303 | render_pass.set_vertex_buffer(0, rendering.shade_vertex_buffer.slice(..)); 304 | render_pass.set_index_buffer( 305 | rendering.shade_index_buffer.slice(..), 306 | wgpu::IndexFormat::Uint32, 307 | ); 308 | render_pass.set_bind_group(0, &self.shade_bind_group, &[]); 309 | render_pass.draw_indexed(0..rendering.shade_index_count, 0, 0..1); 310 | } 311 | // Draw the selection outline to the multisampling texture, and resolve it to the resolve texture 312 | { 313 | let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { 314 | label: None, 315 | color_attachments: &[Some(wgpu::RenderPassColorAttachment { 316 | view: &rendering.ms_tex, 317 | resolve_target: Some(&rendering.ms_resolve_target_tex), 318 | ops: wgpu::Operations { 319 | load: wgpu::LoadOp::Load, 320 | store: false, 321 | }, 322 | })], 323 | depth_stencil_attachment: None, 324 | }); 325 | render_pass.set_pipeline(&self.overlay_pipeline); 326 | render_pass.set_vertex_buffer(0, rendering.sel_vertex_buffer.slice(..)); 327 | render_pass.set_index_buffer( 328 | rendering.sel_index_buffer.slice(..), 329 | wgpu::IndexFormat::Uint32, 330 | ); 331 | render_pass.set_bind_group(0, &self.sel_bind_group, &[]); 332 | render_pass.draw_indexed(0..rendering.sel_index_count, 0, 0..1); 333 | } 334 | // Draw the resolve target texture on top 335 | { 336 | let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { 337 | label: None, 338 | color_attachments: &[Some(wgpu::RenderPassColorAttachment { 339 | view: surface_view, 340 | resolve_target: None, 341 | ops: wgpu::Operations { 342 | load: wgpu::LoadOp::Load, 343 | store: true, 344 | }, 345 | })], 346 | depth_stencil_attachment: None, 347 | }); 348 | render_pass.set_pipeline(&self.tex_pipeline); 349 | render_pass.set_vertex_buffer(0, self.tex_vertex_buffer.slice(..)); 350 | render_pass.set_bind_group(0, &rendering.ms_bind_group, &[]); 351 | render_pass.draw(0..6, 0..1); 352 | } 353 | 354 | if let Some(section) = match selection { 355 | Selection::Rectangle(None) => Some(&rendering.rect_mode_section), 356 | Selection::Display(None) => Some(&rendering.display_mode_section), 357 | Selection::Window(None) => Some(&rendering.window_mode_section), 358 | _ => None, 359 | } { 360 | rendering.brush.queue(device, queue, vec![section]).unwrap(); 361 | 362 | let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { 363 | label: None, 364 | color_attachments: &[Some(wgpu::RenderPassColorAttachment { 365 | view: surface_view, 366 | resolve_target: None, 367 | ops: wgpu::Operations { 368 | load: wgpu::LoadOp::Load, 369 | store: true, 370 | }, 371 | })], 372 | depth_stencil_attachment: None, 373 | }); 374 | 375 | rendering.brush.draw(&mut render_pass); 376 | } 377 | } 378 | } 379 | 380 | impl MonSpecificRendering { 381 | pub fn new( 382 | rect: &Rect, 383 | info: &OutputInfo, 384 | format: wgpu::TextureFormat, 385 | background: RgbaImage, 386 | runtime_data: &RuntimeData, 387 | ) -> Self { 388 | let bg_tex_size = wgpu::Extent3d { 389 | width: background.width(), 390 | height: background.height(), 391 | depth_or_array_layers: 1, 392 | }; 393 | 394 | let bg_tex = runtime_data 395 | .device 396 | .create_texture(&wgpu::TextureDescriptor { 397 | label: None, 398 | size: bg_tex_size, 399 | mip_level_count: 1, 400 | sample_count: 1, 401 | dimension: wgpu::TextureDimension::D2, 402 | format: wgpu::TextureFormat::Rgba8UnormSrgb, 403 | usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST, 404 | view_formats: &[], 405 | }); 406 | 407 | runtime_data.queue.write_texture( 408 | wgpu::ImageCopyTexture { 409 | texture: &bg_tex, 410 | mip_level: 0, 411 | origin: wgpu::Origin3d::ZERO, 412 | aspect: wgpu::TextureAspect::All, 413 | }, 414 | &background, 415 | wgpu::ImageDataLayout { 416 | offset: 0, 417 | bytes_per_row: Some(4 * background.width()), 418 | rows_per_image: Some(background.height()), 419 | }, 420 | bg_tex_size, 421 | ); 422 | 423 | let bg_tex_view = bg_tex.create_view(&wgpu::TextureViewDescriptor::default()); 424 | 425 | let bg_bind_group = runtime_data 426 | .device 427 | .create_bind_group(&wgpu::BindGroupDescriptor { 428 | label: None, 429 | layout: &runtime_data.renderer.as_ref().unwrap().tex_layout, 430 | entries: &[ 431 | wgpu::BindGroupEntry { 432 | binding: 0, 433 | resource: wgpu::BindingResource::TextureView(&bg_tex_view), 434 | }, 435 | wgpu::BindGroupEntry { 436 | binding: 1, 437 | resource: wgpu::BindingResource::Sampler( 438 | &runtime_data.renderer.as_ref().unwrap().tex_sampler, 439 | ), 440 | }, 441 | ], 442 | }); 443 | 444 | let shade_vertex_buffer = runtime_data.device.create_buffer(&wgpu::BufferDescriptor { 445 | label: None, 446 | size: 8 * std::mem::size_of::() as u64, 447 | usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST, 448 | mapped_at_creation: false, 449 | }); 450 | 451 | let sel_vertex_buffer = runtime_data.device.create_buffer(&wgpu::BufferDescriptor { 452 | label: None, 453 | size: MAX_SEL_INDICES * std::mem::size_of::() as u64, 454 | usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST, 455 | mapped_at_creation: false, 456 | }); 457 | 458 | let sel_index_buffer = runtime_data.device.create_buffer(&wgpu::BufferDescriptor { 459 | label: None, 460 | size: MAX_SEL_INDICES * std::mem::size_of::() as u64, 461 | usage: wgpu::BufferUsages::INDEX | wgpu::BufferUsages::COPY_DST, 462 | mapped_at_creation: false, 463 | }); 464 | 465 | let shade_index_buffer = runtime_data.device.create_buffer(&wgpu::BufferDescriptor { 466 | label: None, 467 | size: 24 * std::mem::size_of::() as u64, 468 | usage: wgpu::BufferUsages::INDEX | wgpu::BufferUsages::COPY_DST, 469 | mapped_at_creation: false, 470 | }); 471 | 472 | let ms_size = wgpu::Extent3d { 473 | width: (rect.width * info.scale_factor) as u32, 474 | height: (rect.height * info.scale_factor) as u32, 475 | depth_or_array_layers: 1, 476 | }; 477 | 478 | let ms_tex = runtime_data 479 | .device 480 | .create_texture(&wgpu::TextureDescriptor { 481 | label: None, 482 | size: ms_size, 483 | mip_level_count: 1, 484 | sample_count: OVERLAY_MSAA, 485 | dimension: wgpu::TextureDimension::D2, 486 | format, 487 | usage: wgpu::TextureUsages::RENDER_ATTACHMENT, 488 | view_formats: &[], 489 | }) 490 | .create_view(&wgpu::TextureViewDescriptor::default()); 491 | 492 | let ms_resolve_target_tex = runtime_data 493 | .device 494 | .create_texture(&wgpu::TextureDescriptor { 495 | label: None, 496 | size: ms_size, 497 | mip_level_count: 1, 498 | sample_count: 1, 499 | dimension: wgpu::TextureDimension::D2, 500 | format, 501 | usage: wgpu::TextureUsages::RENDER_ATTACHMENT 502 | | wgpu::TextureUsages::TEXTURE_BINDING, 503 | view_formats: &[], 504 | }) 505 | .create_view(&wgpu::TextureViewDescriptor::default()); 506 | 507 | let ms_bind_group = runtime_data 508 | .device 509 | .create_bind_group(&wgpu::BindGroupDescriptor { 510 | label: None, 511 | layout: &runtime_data.renderer.as_ref().unwrap().tex_layout, 512 | entries: &[ 513 | wgpu::BindGroupEntry { 514 | binding: 0, 515 | resource: wgpu::BindingResource::TextureView(&ms_resolve_target_tex), 516 | }, 517 | wgpu::BindGroupEntry { 518 | binding: 1, 519 | resource: wgpu::BindingResource::Sampler( 520 | &runtime_data.renderer.as_ref().unwrap().tex_sampler, 521 | ), 522 | }, 523 | ], 524 | }); 525 | 526 | let brush = wgpu_text::BrushBuilder::using_font(runtime_data.font.clone()).build( 527 | &runtime_data.device, 528 | (rect.width * info.scale_factor) as u32, 529 | (rect.height * info.scale_factor) as u32, 530 | format, 531 | ); 532 | let pos = ( 533 | (rect.width * info.scale_factor) as f32 / 2.0, 534 | (rect.height * info.scale_factor) as f32 / 2.0, 535 | ); 536 | let layout = Layout::default() 537 | .h_align(HorizontalAlign::Center) 538 | .v_align(VerticalAlign::Center); 539 | 540 | let rect_mode_section = OwnedSection::default() 541 | .add_text( 542 | OwnedText::new("RECTANGLE MODE") 543 | .with_scale((runtime_data.config.mode_text_size * info.scale_factor) as f32) 544 | .with_color(runtime_data.config.text_color), 545 | ) 546 | .with_layout(layout) 547 | .with_screen_position(pos); 548 | let display_mode_section = OwnedSection::default() 549 | .add_text( 550 | OwnedText::new("DISPLAY MODE") 551 | .with_scale((runtime_data.config.mode_text_size * info.scale_factor) as f32) 552 | .with_color(runtime_data.config.text_color), 553 | ) 554 | .with_layout(layout) 555 | .with_screen_position(pos); 556 | 557 | let window_mode_section = OwnedSection::default() 558 | .add_text( 559 | OwnedText::new("WINDOW MODE") 560 | .with_scale((runtime_data.config.mode_text_size * info.scale_factor) as f32) 561 | .with_color(runtime_data.config.text_color), 562 | ) 563 | .with_layout(layout) 564 | .with_screen_position(pos); 565 | 566 | Self { 567 | bg_bind_group, 568 | shade_vertex_buffer, 569 | shade_index_buffer, 570 | sel_vertex_buffer, 571 | sel_index_buffer, 572 | ms_tex, 573 | ms_resolve_target_tex, 574 | ms_bind_group, 575 | brush, 576 | rect_mode_section, 577 | display_mode_section, 578 | window_mode_section, 579 | shade_index_count: 0, 580 | sel_index_count: 0, 581 | } 582 | } 583 | 584 | pub fn update_overlay_vertices( 585 | &mut self, 586 | mon_rect: &Rect, 587 | wl_surface: &wl_surface::WlSurface, 588 | selection: &Selection, 589 | config: &Config, 590 | queue: &wgpu::Queue, 591 | ) { 592 | let flatten_selection = selection.flattened(); 593 | 594 | let (shade_vertices, shade_indices, sel_vertices, sel_indices): ( 595 | Vec<[f32; 2]>, 596 | Vec, 597 | Vec<[f32; 2]>, 598 | Vec, 599 | ) = match flatten_selection { 600 | Selection::Rectangle(Some(selection)) => { 601 | match selection.extents.to_rect().constrain(mon_rect) { 602 | None => { 603 | self.shade_index_count = 6; 604 | self.sel_index_count = 0; 605 | 606 | ( 607 | RECT_VERTICES.to_vec(), 608 | RECT_INDICES.to_vec(), 609 | vec![], 610 | vec![], 611 | ) 612 | } 613 | Some(rect) => { 614 | let rect = rect.to_local(mon_rect); 615 | 616 | let outer = rect 617 | .padded(config.line_width as f32 / 2.0) 618 | .to_render(mon_rect.width, mon_rect.height); 619 | let inner = rect 620 | .padded(-config.line_width as f32 / 2.0) 621 | .to_render(mon_rect.width, mon_rect.height); 622 | 623 | let rect = rect.to_render(mon_rect.width, mon_rect.height); 624 | 625 | let (mut sel_vertices, mut sel_indices) = 626 | OverlayVertex::hollow_rect_vertices(&outer, &inner); 627 | let (shade_vertices, shade_indices) = OverlayVertex::hollow_rect_vertices( 628 | &Rect::new(-1.0, 1.0, 2.0, 2.0), 629 | &rect, 630 | ); 631 | 632 | let handles = handles!(selection.extents.to_local(mon_rect)); 633 | 634 | for (x, y, _) in handles { 635 | let (mut vertices, mut indices) = 636 | Circle::new(*x, *y, config.handle_radius) 637 | .to_vertices(mon_rect.width, mon_rect.height); 638 | 639 | for index in &mut indices { 640 | *index += sel_vertices.len() as u32; 641 | } 642 | 643 | sel_vertices.append(&mut vertices); 644 | sel_indices.append(&mut indices); 645 | } 646 | 647 | self.shade_index_count = shade_indices.len() as u32; 648 | self.sel_index_count = sel_indices.len() as u32; 649 | 650 | (shade_vertices, shade_indices, sel_vertices, sel_indices) 651 | } 652 | } 653 | } 654 | Selection::Display(Some(selection)) => { 655 | if selection.wl_surface == *wl_surface { 656 | self.shade_index_count = 0; 657 | self.sel_index_count = 24; 658 | 659 | let rect = mon_rect.to_local(mon_rect); 660 | 661 | let inner = rect 662 | .padded(-config.display_highlight_width) 663 | .to_render(rect.width, rect.height); 664 | 665 | let (vertices, indices) = OverlayVertex::hollow_rect_vertices( 666 | &rect.to_render(rect.width, rect.height), 667 | &inner, 668 | ); 669 | 670 | (vec![], vec![], vertices, indices) 671 | } else { 672 | self.shade_index_count = 6; 673 | self.sel_index_count = 0; 674 | ( 675 | RECT_VERTICES.to_vec(), 676 | RECT_INDICES.to_vec(), 677 | vec![], 678 | vec![], 679 | ) 680 | } 681 | } 682 | _ => { 683 | self.sel_index_count = 0; 684 | self.shade_index_count = 6; 685 | ( 686 | RECT_VERTICES.to_vec(), 687 | RECT_INDICES.to_vec(), 688 | vec![], 689 | vec![], 690 | ) 691 | } 692 | }; 693 | queue.write_buffer( 694 | &self.shade_vertex_buffer, 695 | 0, 696 | bytemuck::cast_slice(&shade_vertices), 697 | ); 698 | queue.write_buffer( 699 | &self.sel_vertex_buffer, 700 | 0, 701 | bytemuck::cast_slice(&sel_vertices), 702 | ); 703 | queue.write_buffer( 704 | &self.sel_index_buffer, 705 | 0, 706 | bytemuck::cast_slice(&sel_indices), 707 | ); 708 | queue.write_buffer( 709 | &self.shade_index_buffer, 710 | 0, 711 | bytemuck::cast_slice(&shade_indices), 712 | ); 713 | } 714 | } 715 | 716 | #[derive(Clone, Copy)] 717 | pub struct Circle { 718 | pub x: i32, 719 | pub y: i32, 720 | pub radius: i32, 721 | } 722 | 723 | impl Circle { 724 | fn new(x: i32, y: i32, radius: i32) -> Self { 725 | Self { x, y, radius } 726 | } 727 | 728 | fn to_vertices(self, width: i32, height: i32) -> (Vec<[f32; 2]>, Vec) { 729 | let mut vertices = vec![ 730 | [self.x as f32, self.y as f32], 731 | [(self.x - self.radius) as f32, self.y as f32], 732 | ]; 733 | 734 | let step = self.radius as f32 * 4.0 / CIRCLE_EDGES as f32; 735 | for i in 1..CIRCLE_EDGES / 2 { 736 | let offset = i as f32 * step; 737 | 738 | let mut x = (self.x - self.radius) as f32 + offset; 739 | let distance_to_center = x - self.x as f32; 740 | let fract = distance_to_center.abs() / self.radius as f32; 741 | 742 | let adjustment = (self.radius as f32 - distance_to_center.abs()) * fract; 743 | 744 | if distance_to_center < 0.0 { 745 | x -= adjustment; 746 | } else { 747 | x += adjustment; 748 | } 749 | 750 | let y = (self.radius.pow(2) as f32 - (x - self.x as f32).abs().powi(2)).sqrt(); 751 | vertices.push([x, self.y as f32 + y]); 752 | vertices.push([x, self.y as f32 - y]); 753 | } 754 | 755 | vertices.push([(self.x + self.radius) as f32, self.y as f32]); 756 | 757 | #[rustfmt::skip] 758 | let mut indices = vec![ 759 | // Leftmost triangles 760 | 0, 1, 2, 761 | 0, 1, 3, 762 | 763 | // Rightmost triangles 764 | 0, CIRCLE_EDGES, CIRCLE_EDGES - 1, 765 | 0, CIRCLE_EDGES, CIRCLE_EDGES - 2, 766 | ]; 767 | 768 | for i in 0..CIRCLE_EDGES - 4 { 769 | indices.extend(&[0, i + 2, i + 4]); 770 | } 771 | 772 | ( 773 | vertices 774 | .into_iter() 775 | .map(|vertex| vertex.to_render(width, height)) 776 | .collect(), 777 | indices, 778 | ) 779 | } 780 | } 781 | 782 | #[repr(C)] 783 | #[derive(Clone, Copy, Debug, bytemuck::Pod, bytemuck::Zeroable)] 784 | struct TexVertex { 785 | position: [f32; 2], 786 | tex_pos: [f32; 2], 787 | } 788 | 789 | impl TexVertex { 790 | const RECT_VERTICES: &'static [Self] = &[ 791 | // Upper left triangle 792 | Self { 793 | position: TOP_LEFT, 794 | tex_pos: [0.0, 0.0], 795 | }, 796 | Self { 797 | position: BOTTOM_LEFT, 798 | tex_pos: [0.0, 1.0], 799 | }, 800 | Self { 801 | position: TOP_RIGHT, 802 | tex_pos: [1.0, 0.0], 803 | }, 804 | // Lower right triangle 805 | Self { 806 | position: BOTTOM_RIGHT, 807 | tex_pos: [1.0, 1.0], 808 | }, 809 | Self { 810 | position: TOP_RIGHT, 811 | tex_pos: [1.0, 0.0], 812 | }, 813 | Self { 814 | position: BOTTOM_LEFT, 815 | tex_pos: [0.0, 1.0], 816 | }, 817 | ]; 818 | 819 | const ATTRS: [wgpu::VertexAttribute; 2] = 820 | wgpu::vertex_attr_array![0 => Float32x2, 1 => Float32x2]; 821 | 822 | fn desc() -> wgpu::VertexBufferLayout<'static> { 823 | wgpu::VertexBufferLayout { 824 | array_stride: std::mem::size_of::() as wgpu::BufferAddress, 825 | step_mode: wgpu::VertexStepMode::Vertex, 826 | attributes: &Self::ATTRS, 827 | } 828 | } 829 | } 830 | 831 | #[repr(C)] 832 | #[derive(Clone, Copy, Debug, bytemuck::Pod, bytemuck::Zeroable)] 833 | struct OverlayVertex { 834 | pos: [f32; 2], 835 | } 836 | 837 | impl OverlayVertex { 838 | const ATTRS: [wgpu::VertexAttribute; 1] = wgpu::vertex_attr_array![0 => Float32x2]; 839 | 840 | fn desc() -> wgpu::VertexBufferLayout<'static> { 841 | wgpu::VertexBufferLayout { 842 | array_stride: std::mem::size_of::() as wgpu::BufferAddress, 843 | step_mode: wgpu::VertexStepMode::Vertex, 844 | attributes: &Self::ATTRS, 845 | } 846 | } 847 | 848 | fn hollow_rect_vertices(outer: &Rect, inner: &Rect) -> (Vec<[f32; 2]>, Vec) { 849 | let top_left = [inner.x, inner.y]; 850 | let bottom_left = [inner.x, inner.y - inner.height]; 851 | let top_right = [inner.x + inner.width, inner.y]; 852 | let bottom_right = [inner.x + inner.width, inner.y - inner.height]; 853 | 854 | let outer_top_left = [outer.x, outer.y]; 855 | let outer_bottom_left = [outer.x, outer.y - outer.height]; 856 | let outer_top_right = [outer.x + outer.width, outer.y]; 857 | let outer_bottom_right = [outer.x + outer.width, outer.y - outer.height]; 858 | ( 859 | vec![ 860 | top_left, 861 | bottom_left, 862 | top_right, 863 | bottom_right, 864 | outer_top_left, 865 | outer_bottom_left, 866 | outer_top_right, 867 | outer_bottom_right, 868 | ], 869 | vec![ 870 | 5, 1, 0, 1, 5, 7, 7, 6, 3, 3, 1, 7, 3, 6, 2, 2, 6, 4, 2, 4, 0, 0, 4, 5, 871 | ], 872 | ) 873 | } 874 | } 875 | -------------------------------------------------------------------------------- /src/runtime_data.rs: -------------------------------------------------------------------------------- 1 | use std::{fs, io::Cursor, process::Command}; 2 | 3 | use fontconfig::Fontconfig; 4 | use image::DynamicImage; 5 | 6 | use smithay_client_toolkit::{ 7 | compositor::CompositorState, 8 | output::OutputState, 9 | reexports::client::{ 10 | globals::GlobalList, 11 | protocol::{wl_keyboard, wl_pointer, wl_surface}, 12 | QueueHandle, 13 | }, 14 | registry::RegistryState, 15 | seat::{pointer::ThemedPointer, SeatState}, 16 | shell::wlr_layer::LayerShell, 17 | shm::Shm, 18 | }; 19 | 20 | use crate::{ 21 | handles, 22 | rendering::Renderer, 23 | traits::{Contains, DistanceTo}, 24 | types::{ 25 | Args, ExitState, MonitorIdentification, RectangleSelection, SelectionModifier, 26 | SelectionState, 27 | }, 28 | window::{ 29 | hyprland::HyprlandBackend, CompositorBackend, FindWindowExt, InitializeBackend, 30 | WindowDescriptor, 31 | }, 32 | Config, Monitor, Rect, Selection, 33 | }; 34 | 35 | /// The main data worked on at runtime 36 | pub struct RuntimeData { 37 | // Different wayland things 38 | pub registry_state: RegistryState, 39 | pub seat_state: SeatState, 40 | pub output_state: OutputState, 41 | pub compositor_state: CompositorState, 42 | pub layer_state: LayerShell, 43 | pub shm_state: Shm, 44 | 45 | // Devices 46 | pub keyboard: Option, 47 | pub pointer: Option, 48 | 49 | pub pointer_surface: wl_surface::WlSurface, 50 | pub themed_pointer: Option, 51 | 52 | /// Combined area of all monitors 53 | pub area: Rect, 54 | /// The scale factor of the screenshot image 55 | pub scale_factor: f32, 56 | pub selection: Selection, 57 | pub monitors: Vec, 58 | pub config: Config, 59 | pub font: wgpu_text::glyph_brush::ab_glyph::FontArc, 60 | pub image: DynamicImage, 61 | pub exit: ExitState, 62 | pub args: Args, 63 | 64 | pub instance: wgpu::Instance, 65 | pub device: wgpu::Device, 66 | pub adapter: wgpu::Adapter, 67 | pub queue: wgpu::Queue, 68 | 69 | pub renderer: Option, 70 | 71 | pub compositor_backend: Option>, 72 | pub windows: Vec, 73 | } 74 | 75 | impl RuntimeData { 76 | pub fn get_preferred_backend() -> Option> { 77 | HyprlandBackend::try_new().ok() 78 | } 79 | 80 | pub fn new(qh: &QueueHandle, globals: &GlobalList, mut args: Args) -> Self { 81 | let output = Command::new(args.grim.as_ref().unwrap_or(&"grim".to_string())) 82 | .arg("-t") 83 | .arg("ppm") 84 | .arg("-") 85 | .output() 86 | .expect("Failed to run grim command!") 87 | .stdout; 88 | 89 | let image = image::io::Reader::with_format(Cursor::new(output), image::ImageFormat::Pnm) 90 | .decode() 91 | .expect("Failed to parse grim image!"); 92 | 93 | let config = Config::load().unwrap_or_default(); 94 | 95 | let fc = Fontconfig::new().expect("Failed to init FontConfig"); 96 | 97 | let fc_font = fc 98 | .find(&config.font_family, None) 99 | .expect("Failed to find font"); 100 | 101 | let compositor_state = 102 | CompositorState::bind(globals, qh).expect("wl_compositor is not available"); 103 | 104 | let instance = wgpu::Instance::new(wgpu::InstanceDescriptor { 105 | backends: wgpu::Backends::all(), 106 | ..Default::default() 107 | }); 108 | 109 | let pointer_surface = compositor_state.create_surface(qh); 110 | 111 | let adapter = 112 | pollster::block_on(instance.request_adapter(&wgpu::RequestAdapterOptionsBase { 113 | compatible_surface: None, 114 | ..Default::default() 115 | })) 116 | .unwrap(); 117 | 118 | let (device, queue) = pollster::block_on(adapter.request_device( 119 | &wgpu::DeviceDescriptor { 120 | label: None, 121 | features: wgpu::Features::TEXTURE_ADAPTER_SPECIFIC_FORMAT_FEATURES, 122 | ..Default::default() 123 | }, 124 | None, 125 | )) 126 | .unwrap(); 127 | 128 | let compositor_backend = Self::get_preferred_backend(); 129 | 130 | let mut selection = Selection::default(); 131 | let mut windows = Vec::default(); 132 | let mut exit = ExitState::None; 133 | 134 | if let Some(ref compositor_backend) = compositor_backend { 135 | (selection, windows, exit) = { 136 | let windows = compositor_backend.get_all_windows(); 137 | 138 | let selection = { 139 | if let Some(search_param) = args.window_search.take() { 140 | Selection::from_window(windows.find_by_search_param(search_param).cloned()) 141 | } else if args.window_under_cursor { 142 | let mouse_pos = compositor_backend.get_mouse_position(); 143 | Selection::from_window(windows.find_by_position(&mouse_pos).cloned()) 144 | } else if args.active_window { 145 | Selection::from_window(compositor_backend.get_focused()) 146 | } else { 147 | Selection::default() 148 | } 149 | }; 150 | 151 | if !args.auto_capture { 152 | (selection, windows, ExitState::None) 153 | } else if let Selection::Rectangle(Some(rect_sel)) = selection.flattened() { 154 | ( 155 | selection, 156 | windows, 157 | ExitState::ExitWithSelection(rect_sel.extents.to_rect()), 158 | ) 159 | } else { 160 | // TODO: Auto-capture for monitors 161 | (selection, windows, ExitState::None) 162 | } 163 | }; 164 | } 165 | 166 | RuntimeData { 167 | registry_state: RegistryState::new(globals), 168 | seat_state: SeatState::new(globals, qh), 169 | output_state: OutputState::new(globals, qh), 170 | compositor_state, 171 | layer_state: LayerShell::bind(globals, qh).expect("layer shell is not available"), 172 | shm_state: Shm::bind(globals, qh).expect("wl_shm is not available"), 173 | selection, 174 | config, 175 | area: Rect::default(), 176 | monitors: Vec::new(), 177 | // Set later 178 | scale_factor: 0.0, 179 | image, 180 | keyboard: None, 181 | pointer: None, 182 | themed_pointer: None, 183 | exit, 184 | args, 185 | pointer_surface, 186 | instance, 187 | adapter, 188 | device, 189 | queue, 190 | renderer: None, 191 | font: wgpu_text::glyph_brush::ab_glyph::FontArc::try_from_vec( 192 | fs::read(fc_font.path).expect("Failed to load font"), 193 | ) 194 | .expect("Invalid font data"), 195 | compositor_backend, 196 | windows, 197 | } 198 | } 199 | 200 | pub fn draw(&mut self, identification: MonitorIdentification, qh: &QueueHandle) { 201 | let Some(renderer) = &mut self.renderer else { 202 | return 203 | }; 204 | 205 | let monitor = match identification { 206 | MonitorIdentification::Layer(layer) => self 207 | .monitors 208 | .iter_mut() 209 | .find(|window| window.layer == layer) 210 | .unwrap(), 211 | MonitorIdentification::Surface(surface) => self 212 | .monitors 213 | .iter_mut() 214 | .find(|window| window.wl_surface == surface) 215 | .unwrap(), 216 | }; 217 | 218 | if let Some(rendering) = &mut monitor.rendering { 219 | rendering.update_overlay_vertices( 220 | &monitor.rect, 221 | &monitor.wl_surface, 222 | &self.selection, 223 | &self.config, 224 | &self.queue, 225 | ); 226 | } 227 | 228 | let surface_texture = monitor.surface.get_current_texture().unwrap(); 229 | let texture_view = surface_texture 230 | .texture 231 | .create_view(&wgpu::TextureViewDescriptor::default()); 232 | 233 | let mut encoder = self 234 | .device 235 | .create_command_encoder(&wgpu::CommandEncoderDescriptor::default()); 236 | 237 | renderer.render( 238 | &mut encoder, 239 | &texture_view, 240 | monitor, 241 | &self.selection, 242 | &self.device, 243 | &self.queue, 244 | ); 245 | 246 | self.queue.submit(Some(encoder.finish())); 247 | 248 | monitor 249 | .wl_surface 250 | .damage(0, 0, monitor.rect.width, monitor.rect.height); 251 | monitor.wl_surface.frame(qh, monitor.wl_surface.clone()); 252 | surface_texture.present(); 253 | monitor.wl_surface.commit(); 254 | } 255 | 256 | pub fn process_selection_handles( 257 | rect_sel: &mut Option, 258 | global_pos: (i32, i32), 259 | handle_radius: i32, 260 | ) -> SelectionState { 261 | if let Some(selection) = rect_sel { 262 | for (x, y, modifier) in handles!(selection.extents) { 263 | if global_pos.distance_to(&(*x, *y)) <= handle_radius { 264 | selection.modifier = Some(*modifier); 265 | selection.active = true; 266 | return SelectionState::HandlesChanged; 267 | } 268 | } 269 | if selection.extents.to_rect().contains(&global_pos) { 270 | selection.modifier = Some(SelectionModifier::Center( 271 | global_pos.0, 272 | global_pos.1, 273 | selection.extents, 274 | )); 275 | selection.active = true; 276 | return SelectionState::CenterChanged; 277 | } 278 | } 279 | 280 | SelectionState::Unchanged 281 | } 282 | } 283 | -------------------------------------------------------------------------------- /src/sctk_impls/compositor_handler.rs: -------------------------------------------------------------------------------- 1 | use smithay_client_toolkit::{ 2 | compositor::CompositorHandler, 3 | delegate_compositor, 4 | reexports::client::{protocol::wl_surface, Connection, QueueHandle}, 5 | }; 6 | 7 | use crate::{runtime_data::RuntimeData, types::MonitorIdentification}; 8 | 9 | delegate_compositor!(RuntimeData); 10 | 11 | impl CompositorHandler for RuntimeData { 12 | fn scale_factor_changed( 13 | &mut self, 14 | _conn: &Connection, 15 | _qh: &QueueHandle, 16 | _surface: &wl_surface::WlSurface, 17 | _new_factor: i32, 18 | ) { 19 | } 20 | 21 | fn frame( 22 | &mut self, 23 | _conn: &Connection, 24 | qh: &QueueHandle, 25 | surface: &wl_surface::WlSurface, 26 | _time: u32, 27 | ) { 28 | self.draw(MonitorIdentification::Surface(surface.clone()), qh); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/sctk_impls/keyboard_handler.rs: -------------------------------------------------------------------------------- 1 | use log::info; 2 | use smithay_client_toolkit::{ 3 | delegate_keyboard, 4 | reexports::client::{ 5 | protocol::{wl_keyboard, wl_surface}, 6 | Connection, QueueHandle, 7 | }, 8 | seat::keyboard::{keysyms, KeyEvent, KeyboardHandler, Modifiers}, 9 | }; 10 | 11 | use crate::{ 12 | runtime_data::RuntimeData, 13 | types::{ExitState, Selection}, 14 | }; 15 | 16 | delegate_keyboard!(RuntimeData); 17 | 18 | impl KeyboardHandler for RuntimeData { 19 | fn enter( 20 | &mut self, 21 | _: &Connection, 22 | _: &QueueHandle, 23 | _: &wl_keyboard::WlKeyboard, 24 | _: &wl_surface::WlSurface, 25 | _: u32, 26 | _: &[u32], 27 | _: &[u32], 28 | ) { 29 | } 30 | 31 | fn leave( 32 | &mut self, 33 | _: &Connection, 34 | _: &QueueHandle, 35 | _: &wl_keyboard::WlKeyboard, 36 | _: &wl_surface::WlSurface, 37 | _: u32, 38 | ) { 39 | } 40 | 41 | fn press_key( 42 | &mut self, 43 | _conn: &Connection, 44 | _qh: &QueueHandle, 45 | _: &wl_keyboard::WlKeyboard, 46 | _: u32, 47 | event: KeyEvent, 48 | ) { 49 | match event.keysym { 50 | // Exit without copying/saving 51 | keysyms::XKB_KEY_Escape => self.exit = ExitState::ExitOnly, 52 | // Switch selection mode 53 | keysyms::XKB_KEY_Tab => match &self.selection { 54 | Selection::Rectangle(_) => self.selection = Selection::Display(None), 55 | Selection::Display(_) => { 56 | if self.compositor_backend.is_some() { 57 | self.selection = Selection::Window(None) 58 | } else { 59 | self.selection = Selection::Rectangle(None) 60 | } 61 | } 62 | Selection::Window(_) => self.selection = Selection::Rectangle(None), 63 | }, 64 | // Exit with save if a valid selection exists 65 | keysyms::XKB_KEY_Return => { 66 | let flattened_selection = self.selection.flattened(); 67 | match flattened_selection { 68 | Selection::Rectangle(Some(selection)) => { 69 | let mut rect = selection.extents.to_rect(); 70 | // Alter coordinate space so the rect can be used to crop from the original image 71 | rect.x -= self.area.x; 72 | rect.y -= self.area.y; 73 | 74 | self.exit = ExitState::ExitWithSelection(rect) 75 | } 76 | Selection::Display(Some(selection)) => { 77 | let monitor = self 78 | .monitors 79 | .iter() 80 | .find(|monitor| monitor.wl_surface == selection.wl_surface) 81 | .unwrap(); 82 | 83 | let mut rect = monitor.rect; 84 | 85 | rect.x -= self.area.x; 86 | rect.y -= self.area.y; 87 | 88 | self.exit = ExitState::ExitWithSelection(rect) 89 | } 90 | Selection::Window(_) => unreachable!( 91 | "Window selection should have been flattened into Rectangle selection" 92 | ), 93 | _ => (), 94 | } 95 | } 96 | _ => (), 97 | } 98 | } 99 | 100 | fn release_key( 101 | &mut self, 102 | _: &Connection, 103 | _: &QueueHandle, 104 | _: &wl_keyboard::WlKeyboard, 105 | _: u32, 106 | event: KeyEvent, 107 | ) { 108 | info!("Key release: {:?}", event); 109 | } 110 | 111 | fn update_modifiers( 112 | &mut self, 113 | _: &Connection, 114 | _: &QueueHandle, 115 | _: &wl_keyboard::WlKeyboard, 116 | _serial: u32, 117 | modifiers: Modifiers, 118 | ) { 119 | info!("Update modifiers: {:?}", modifiers); 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/sctk_impls/layer_shell_handler.rs: -------------------------------------------------------------------------------- 1 | use smithay_client_toolkit::{ 2 | delegate_layer, 3 | reexports::client::{Connection, QueueHandle}, 4 | shell::wlr_layer::{LayerShellHandler, LayerSurface, LayerSurfaceConfigure}, 5 | }; 6 | 7 | use crate::{ 8 | rendering::{MonSpecificRendering, Renderer}, 9 | runtime_data::RuntimeData, 10 | types::MonitorIdentification, 11 | }; 12 | 13 | delegate_layer!(RuntimeData); 14 | 15 | impl LayerShellHandler for RuntimeData { 16 | fn closed(&mut self, _conn: &Connection, _qh: &QueueHandle, _layer: &LayerSurface) {} 17 | 18 | fn configure( 19 | &mut self, 20 | conn: &Connection, 21 | qh: &QueueHandle, 22 | layer: &LayerSurface, 23 | _configure: LayerSurfaceConfigure, 24 | _serial: u32, 25 | ) { 26 | let _ = self.themed_pointer.as_ref().unwrap().set_cursor( 27 | conn, 28 | "crosshair", 29 | self.shm_state.wl_shm(), 30 | &self.pointer_surface, 31 | 1, 32 | ); 33 | 34 | log::info!("{:?}", _configure); 35 | 36 | let monitor = self 37 | .monitors 38 | .iter() 39 | .find(|window| window.layer == *layer) 40 | .unwrap(); 41 | 42 | let cap = monitor.surface.get_capabilities(&self.adapter); 43 | 44 | if self.renderer.is_none() { 45 | self.renderer = Some(Renderer::new(&self.device, &self.config, cap.formats[0])); 46 | } 47 | 48 | monitor.surface.configure( 49 | &self.device, 50 | &wgpu::SurfaceConfiguration { 51 | usage: wgpu::TextureUsages::RENDER_ATTACHMENT, 52 | format: cap.formats[0], 53 | width: (monitor.rect.width * monitor.output_info.scale_factor) as u32, 54 | height: (monitor.rect.height * monitor.output_info.scale_factor) as u32, 55 | present_mode: wgpu::PresentMode::Mailbox, 56 | alpha_mode: wgpu::CompositeAlphaMode::Opaque, 57 | view_formats: vec![cap.formats[0]], 58 | }, 59 | ); 60 | 61 | let mon_rendering = MonSpecificRendering::new( 62 | &monitor.rect, 63 | &monitor.output_info, 64 | cap.formats[0], 65 | monitor.image.to_rgba8(), 66 | self, 67 | ); 68 | 69 | // Reborrow mutably to set the renderer 70 | let monitor = self 71 | .monitors 72 | .iter_mut() 73 | .find(|window| window.layer == *layer) 74 | .unwrap(); 75 | 76 | monitor.rendering = Some(mon_rendering); 77 | 78 | log::info!("{:?}", cap.formats); 79 | 80 | self.draw(MonitorIdentification::Layer(layer.clone()), qh); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/sctk_impls/output_handler.rs: -------------------------------------------------------------------------------- 1 | use smithay_client_toolkit::{ 2 | delegate_output, 3 | output::{OutputHandler, OutputState}, 4 | reexports::client::{protocol::wl_output, Connection, QueueHandle}, 5 | }; 6 | 7 | use crate::runtime_data::RuntimeData; 8 | 9 | delegate_output!(RuntimeData); 10 | 11 | impl OutputHandler for RuntimeData { 12 | fn output_state(&mut self) -> &mut OutputState { 13 | &mut self.output_state 14 | } 15 | 16 | fn new_output( 17 | &mut self, 18 | _conn: &Connection, 19 | _qh: &QueueHandle, 20 | _output: wl_output::WlOutput, 21 | ) { 22 | } 23 | 24 | fn update_output( 25 | &mut self, 26 | _conn: &Connection, 27 | _qh: &QueueHandle, 28 | _output: wl_output::WlOutput, 29 | ) { 30 | } 31 | 32 | fn output_destroyed( 33 | &mut self, 34 | _conn: &Connection, 35 | _qh: &QueueHandle, 36 | _output: wl_output::WlOutput, 37 | ) { 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/sctk_impls/pointer_handler.rs: -------------------------------------------------------------------------------- 1 | use log::info; 2 | use smithay_client_toolkit::{ 3 | delegate_pointer, 4 | reexports::client::{protocol::wl_pointer, Connection, QueueHandle}, 5 | seat::pointer::{PointerEvent, PointerEventKind, PointerHandler}, 6 | }; 7 | 8 | use crate::{ 9 | runtime_data::RuntimeData, 10 | traits::ToGlobal, 11 | types::{DisplaySelection, RectangleSelection, Selection, SelectionModifier, SelectionState}, 12 | window::FindWindowExt, 13 | }; 14 | 15 | delegate_pointer!(RuntimeData); 16 | 17 | impl PointerHandler for RuntimeData { 18 | fn pointer_frame( 19 | &mut self, 20 | _conn: &Connection, 21 | _qh: &QueueHandle, 22 | _pointer: &wl_pointer::WlPointer, 23 | events: &[PointerEvent], 24 | ) { 25 | use PointerEventKind::*; 26 | for event in events { 27 | let layer = self 28 | .monitors 29 | .iter() 30 | .find(|layer| layer.wl_surface == event.surface) 31 | .unwrap(); 32 | let global_pos = event.position.to_global(&layer.rect); 33 | 34 | match event.kind { 35 | Enter { .. } => { 36 | info!("Pointer entered @{:?}", event.position); 37 | } 38 | Leave { .. } => { 39 | info!("Pointer left"); 40 | } 41 | Motion { .. } => { 42 | if let Selection::Rectangle(Some(selection)) = &mut self.selection { 43 | if selection.active { 44 | match selection.modifier { 45 | // Handle selection modifiers, AKA the drag handles and moving it from the center 46 | Some(modifier) => match modifier { 47 | SelectionModifier::Left => { 48 | selection.extents.start_x = global_pos.0 49 | } 50 | SelectionModifier::Right => { 51 | selection.extents.end_x = global_pos.0 52 | } 53 | SelectionModifier::Top => { 54 | selection.extents.start_y = global_pos.1 55 | } 56 | SelectionModifier::Bottom => { 57 | selection.extents.end_y = global_pos.1 58 | } 59 | SelectionModifier::TopRight => { 60 | selection.extents.end_x = global_pos.0; 61 | selection.extents.start_y = global_pos.1; 62 | } 63 | SelectionModifier::BottomRight => { 64 | selection.extents.end_x = global_pos.0; 65 | selection.extents.end_y = global_pos.1; 66 | } 67 | SelectionModifier::BottomLeft => { 68 | selection.extents.start_x = global_pos.0; 69 | selection.extents.end_y = global_pos.1; 70 | } 71 | SelectionModifier::TopLeft => { 72 | selection.extents.start_x = global_pos.0; 73 | selection.extents.start_y = global_pos.1; 74 | } 75 | SelectionModifier::Center(x, y, mut extents) => { 76 | extents.start_x -= x - global_pos.0; 77 | extents.start_y -= y - global_pos.1; 78 | extents.end_x -= x - global_pos.0; 79 | extents.end_y -= y - global_pos.1; 80 | 81 | selection.extents = 82 | extents.to_rect_clamped(&self.area).to_extents(); 83 | } 84 | }, 85 | None => { 86 | selection.extents.end_x = global_pos.0; 87 | selection.extents.end_y = global_pos.1; 88 | } 89 | } 90 | } 91 | } 92 | } 93 | Press { button, .. } => { 94 | info!("Press {:x} @ {:?}", button, event.position); 95 | 96 | match &mut self.selection { 97 | Selection::Rectangle(ref mut selection) => { 98 | let handles_state = RuntimeData::process_selection_handles( 99 | selection, 100 | global_pos, 101 | self.config.handle_radius, 102 | ); 103 | if let SelectionState::Unchanged = handles_state { 104 | self.selection = Selection::Rectangle(Some( 105 | RectangleSelection::new(global_pos.0, global_pos.1), 106 | )); 107 | } 108 | } 109 | Selection::Display(_) => { 110 | self.selection = Selection::Display(Some(DisplaySelection::new( 111 | event.surface.clone(), 112 | ))); 113 | } 114 | Selection::Window(_) => { 115 | let mut flattened_selection = self.selection.flattened(); 116 | if let Selection::Rectangle(ref mut rect_sel) = flattened_selection { 117 | let handles_state = RuntimeData::process_selection_handles( 118 | rect_sel, 119 | global_pos, 120 | self.config.handle_radius, 121 | ); 122 | if let SelectionState::HandlesChanged = handles_state { 123 | self.selection = flattened_selection; 124 | } else { 125 | let win_sel = 126 | self.windows.find_by_position(&global_pos).cloned(); 127 | 128 | if win_sel.is_some() { 129 | self.selection = Selection::Window(win_sel); 130 | } 131 | } 132 | } 133 | } 134 | } 135 | } 136 | Release { button, .. } => { 137 | info!("Release {:x} @ {:?}", button, event.position); 138 | 139 | if let Selection::Rectangle(Some(selection)) = &mut self.selection { 140 | selection.active = false; 141 | } 142 | } 143 | Axis { 144 | horizontal, 145 | vertical, 146 | .. 147 | } => { 148 | info!("Scroll H:{:?}, V:{:?}", horizontal, vertical); 149 | } 150 | } 151 | } 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /src/sctk_impls/provides_registry_state.rs: -------------------------------------------------------------------------------- 1 | use smithay_client_toolkit::{ 2 | delegate_registry, 3 | output::OutputState, 4 | registry::{ProvidesRegistryState, RegistryState}, 5 | registry_handlers, 6 | seat::SeatState, 7 | }; 8 | 9 | use crate::runtime_data::RuntimeData; 10 | 11 | delegate_registry!(RuntimeData); 12 | 13 | impl ProvidesRegistryState for RuntimeData { 14 | fn registry(&mut self) -> &mut RegistryState { 15 | &mut self.registry_state 16 | } 17 | registry_handlers![OutputState, SeatState]; 18 | } 19 | -------------------------------------------------------------------------------- /src/sctk_impls/seat_handler.rs: -------------------------------------------------------------------------------- 1 | use log::info; 2 | use smithay_client_toolkit::{ 3 | delegate_seat, 4 | reexports::client::{protocol::wl_seat, Connection, QueueHandle}, 5 | seat::{pointer::ThemeSpec, Capability, SeatHandler, SeatState}, 6 | }; 7 | 8 | use crate::runtime_data::RuntimeData; 9 | 10 | delegate_seat!(RuntimeData); 11 | 12 | impl SeatHandler for RuntimeData { 13 | fn seat_state(&mut self) -> &mut SeatState { 14 | &mut self.seat_state 15 | } 16 | 17 | fn new_seat(&mut self, _: &Connection, _: &QueueHandle, _: wl_seat::WlSeat) {} 18 | 19 | fn new_capability( 20 | &mut self, 21 | _conn: &Connection, 22 | qh: &QueueHandle, 23 | seat: wl_seat::WlSeat, 24 | capability: Capability, 25 | ) { 26 | if capability == Capability::Keyboard && self.keyboard.is_none() { 27 | info!("Set keyboard capability"); 28 | let keyboard = self 29 | .seat_state 30 | .get_keyboard(qh, &seat, None) 31 | .expect("Failed to create keyboard"); 32 | self.keyboard = Some(keyboard); 33 | } 34 | 35 | if capability == Capability::Pointer 36 | && self.pointer.is_none() 37 | && self.themed_pointer.is_none() 38 | { 39 | info!("Set pointer capability"); 40 | 41 | let themed_pointer = self 42 | .seat_state 43 | .get_pointer_with_theme(qh, &seat, ThemeSpec::default()) 44 | .expect("Failed to create themed pointer"); 45 | self.pointer = Some(themed_pointer.pointer().clone()); 46 | self.themed_pointer = Some(themed_pointer); 47 | } 48 | } 49 | 50 | fn remove_capability( 51 | &mut self, 52 | _conn: &Connection, 53 | _: &QueueHandle, 54 | _: wl_seat::WlSeat, 55 | capability: Capability, 56 | ) { 57 | if capability == Capability::Keyboard && self.keyboard.is_some() { 58 | info!("Unset keyboard capability"); 59 | self.keyboard.take().unwrap().release(); 60 | } 61 | 62 | if capability == Capability::Pointer && self.pointer.is_some() { 63 | info!("Unset pointer capability"); 64 | self.pointer.take().unwrap().release(); 65 | } 66 | } 67 | 68 | fn remove_seat(&mut self, _: &Connection, _: &QueueHandle, _: wl_seat::WlSeat) {} 69 | } 70 | -------------------------------------------------------------------------------- /src/sctk_impls/shm_handler.rs: -------------------------------------------------------------------------------- 1 | use smithay_client_toolkit::{ 2 | delegate_shm, 3 | shm::{Shm, ShmHandler}, 4 | }; 5 | 6 | use crate::runtime_data::RuntimeData; 7 | 8 | delegate_shm!(RuntimeData); 9 | 10 | impl ShmHandler for RuntimeData { 11 | fn shm_state(&mut self) -> &mut Shm { 12 | &mut self.shm_state 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/traits.rs: -------------------------------------------------------------------------------- 1 | use crate::types::{Extents, Rect}; 2 | 3 | pub trait ToLocal { 4 | fn to_local(&self, rect: &Rect) -> T; 5 | } 6 | 7 | pub trait ToGlobal { 8 | fn to_global(&self, rect: &Rect) -> T; 9 | } 10 | 11 | pub trait ToRender { 12 | fn to_render(&self, width: U, height: U) -> T; 13 | } 14 | 15 | pub trait DistanceTo { 16 | fn distance_to(&self, other: &(T, T)) -> T; 17 | } 18 | 19 | pub trait Contains { 20 | fn contains(&self, item: &T) -> bool; 21 | } 22 | 23 | pub trait Padded { 24 | fn padded(&self, amount: T) -> Rect; 25 | } 26 | 27 | impl ToRender, i32> for Rect { 28 | fn to_render(&self, width: i32, height: i32) -> Rect { 29 | let width = width as f32; 30 | let height = height as f32; 31 | 32 | Rect::new( 33 | (self.x as f32 / width - 0.5) * 2.0, 34 | -(self.y as f32 / height - 0.5) * 2.0, 35 | (self.width as f32 / width) * 2.0, 36 | (self.height as f32 / height) * 2.0, 37 | ) 38 | } 39 | } 40 | 41 | impl ToRender, i32> for Rect { 42 | fn to_render(&self, width: i32, height: i32) -> Rect { 43 | let width = width as f32; 44 | let height = height as f32; 45 | 46 | Rect::new( 47 | (self.x / width - 0.5) * 2.0, 48 | -(self.y / height - 0.5) * 2.0, 49 | (self.width / width) * 2.0, 50 | (self.height / height) * 2.0, 51 | ) 52 | } 53 | } 54 | 55 | impl ToRender<[f32; 2], i32> for [f32; 2] { 56 | fn to_render(&self, width: i32, height: i32) -> [f32; 2] { 57 | [ 58 | (self[0] / width as f32 - 0.5) * 2.0, 59 | -(self[1] / height as f32 - 0.5) * 2.0, 60 | ] 61 | } 62 | } 63 | 64 | impl ToLocal for Extents { 65 | fn to_local(&self, rect: &Rect) -> Extents { 66 | Self { 67 | start_x: self.start_x - rect.x, 68 | start_y: self.start_y - rect.y, 69 | end_x: self.end_x - rect.x, 70 | end_y: self.end_y - rect.y, 71 | } 72 | } 73 | } 74 | 75 | impl ToLocal> for Rect { 76 | fn to_local(&self, rect: &Rect) -> Rect { 77 | Rect::::new(self.x - rect.x, self.y - rect.y, self.width, self.height) 78 | } 79 | } 80 | 81 | impl ToLocal<(i32, i32)> for (i32, i32) { 82 | fn to_local(&self, rect: &Rect) -> (i32, i32) { 83 | (self.0 - rect.x, self.1 - rect.y) 84 | } 85 | } 86 | 87 | impl DistanceTo for (i32, i32) { 88 | fn distance_to(&self, other: &(i32, i32)) -> i32 { 89 | let x = (other.0 - self.0) as f64; 90 | let y = (other.1 - self.1) as f64; 91 | f64::sqrt(x * x + y * y) as i32 92 | } 93 | } 94 | 95 | impl ToGlobal<(i32, i32)> for (f64, f64) { 96 | fn to_global(&self, rect: &Rect) -> (i32, i32) { 97 | (self.0 as i32 + rect.x, self.1 as i32 + rect.y) 98 | } 99 | } 100 | 101 | impl Contains<(i32, i32)> for Rect { 102 | fn contains(&self, pos: &(i32, i32)) -> bool { 103 | pos.0 >= self.x 104 | && pos.0 <= self.x + self.width 105 | && pos.1 >= self.y 106 | && pos.1 <= self.y + self.height 107 | } 108 | } 109 | 110 | impl Contains> for Rect { 111 | fn contains(&self, other: &Rect) -> bool { 112 | self.x <= other.x 113 | && self.y <= other.y 114 | && self.x + self.width >= other.x + other.width 115 | && self.y + self.height >= other.y + other.height 116 | } 117 | } 118 | 119 | impl Padded for Rect { 120 | fn padded(&self, amount: f32) -> Rect { 121 | let mut width = self.width as f32 + 2.0 * amount; 122 | let mut height = self.height as f32 + 2.0 * amount; 123 | 124 | // Make sure we have no negative size 125 | if width < 0.0 { 126 | width = 0.0; 127 | } 128 | 129 | if height < 0.0 { 130 | height = 0.0; 131 | } 132 | 133 | Rect::new( 134 | self.x as f32 - amount, 135 | self.y as f32 - amount, 136 | width, 137 | height, 138 | ) 139 | } 140 | } 141 | impl Padded for Rect { 142 | fn padded(&self, amount: i32) -> Rect { 143 | let mut width = self.width + 2 * amount; 144 | let mut height = self.height + 2 * amount; 145 | 146 | // Make sure we have no negative size 147 | if width < 0 { 148 | width = 0; 149 | } 150 | 151 | if height < 0 { 152 | height = 0; 153 | } 154 | 155 | Rect::new(self.x - amount, self.y - amount, width, height) 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /src/types.rs: -------------------------------------------------------------------------------- 1 | use std::{env, fs, io::Cursor, process::Command}; 2 | 3 | use clap::{Parser, Subcommand}; 4 | use image::DynamicImage; 5 | use raw_window_handle::{ 6 | HasRawDisplayHandle, HasRawWindowHandle, RawDisplayHandle, RawWindowHandle, 7 | WaylandDisplayHandle, WaylandWindowHandle, 8 | }; 9 | use serde::Deserialize; 10 | use smithay_client_toolkit::{ 11 | output::OutputInfo, 12 | shell::{ 13 | wlr_layer::{Anchor, KeyboardInteractivity, Layer, LayerSurface}, 14 | WaylandSurface, 15 | }, 16 | }; 17 | use wayland_client::{ 18 | protocol::{wl_output, wl_surface}, 19 | Connection, Proxy, QueueHandle, 20 | }; 21 | 22 | use crate::{rendering::MonSpecificRendering, runtime_data::RuntimeData, window::WindowDescriptor}; 23 | 24 | use crate::window::search::WindowSearchParam; 25 | 26 | #[derive(Parser, Clone, Debug)] 27 | #[command(author, version, about)] 28 | pub struct Args { 29 | /// Copy the screenshot after exit 30 | #[arg(short, long)] 31 | pub copy: bool, 32 | 33 | /// Output the screenshot into stdout in PNG format 34 | #[arg(short, long)] 35 | pub stdout: bool, 36 | 37 | /// Path to the `grim` executable 38 | #[arg(short, long)] 39 | pub grim: Option, 40 | 41 | /// Save the image into a file 42 | #[command(subcommand)] 43 | pub save: Option, 44 | 45 | /// Pre-selects a window by its class, title or initial versions of the two. 46 | /// The value passed can be a regex. 47 | /// Examples: "class=Alacritty" , "title=.*Visual Studio Code.*" 48 | #[arg(long, group = "capture-window")] 49 | pub window_search: Option, 50 | 51 | /// Pre-selects the window under the mouse cursor. 52 | #[arg(long, group = "capture-window")] 53 | pub window_under_cursor: bool, 54 | 55 | /// Pre-selects the currently-focused window. 56 | #[arg(long, group = "capture-window")] 57 | pub active_window: bool, 58 | 59 | /// Automatically captures the pre-selected window, skipping interactive mode. 60 | #[arg(long)] 61 | pub auto_capture: bool, 62 | } 63 | 64 | #[derive(Subcommand, Clone, Debug)] 65 | pub enum SaveLocation { 66 | /// The path to save the image to 67 | Path { path: String }, 68 | /// The directory to save the image to with a generated name 69 | Directory { path: String }, 70 | } 71 | 72 | /// The configuration for colors and other things like that 73 | #[derive(Debug, Deserialize)] 74 | pub struct Config { 75 | pub handle_radius: i32, 76 | pub line_width: i32, 77 | pub display_highlight_width: i32, 78 | pub selection_color: Color, 79 | pub shade_color: Color, 80 | pub text_color: Color, 81 | pub mode_text_size: i32, 82 | pub font_family: String, 83 | } 84 | 85 | impl Config { 86 | pub fn load() -> Result> { 87 | let string = fs::read_to_string(format!("{}/.config/watershot.ron", env::var("HOME")?))?; 88 | Ok(ron::from_str(&string)?) 89 | } 90 | } 91 | 92 | impl Default for Config { 93 | fn default() -> Self { 94 | Self { 95 | handle_radius: 10, 96 | line_width: 1, 97 | display_highlight_width: 5, 98 | selection_color: Color { 99 | r: 1.0, 100 | g: 1.0, 101 | b: 1.0, 102 | a: 1.0, 103 | }, 104 | shade_color: Color { 105 | r: 0.0, 106 | g: 0.0, 107 | b: 0.0, 108 | a: 0.5, 109 | }, 110 | text_color: Color { 111 | r: 0.8, 112 | g: 0.8, 113 | b: 0.8, 114 | a: 1.0, 115 | }, 116 | mode_text_size: 30, 117 | font_family: "monospace".to_string(), 118 | } 119 | } 120 | } 121 | 122 | #[repr(C)] 123 | #[derive(Debug, Deserialize, Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] 124 | pub struct Color { 125 | pub r: f32, 126 | pub g: f32, 127 | pub b: f32, 128 | pub a: f32, 129 | } 130 | 131 | impl From for wgpu_text::glyph_brush::Color { 132 | fn from(val: Color) -> Self { 133 | [val.r, val.g, val.b, val.a] 134 | } 135 | } 136 | 137 | /// Represents the layer and the monitor it resides on 138 | pub struct Monitor { 139 | pub layer: LayerSurface, 140 | pub wl_surface: wl_surface::WlSurface, 141 | pub surface: wgpu::Surface, 142 | pub output_info: OutputInfo, 143 | pub rect: Rect, 144 | pub image: DynamicImage, 145 | /// The wayland scale factor for this monitor 146 | pub rendering: Option, 147 | } 148 | 149 | impl Monitor { 150 | pub fn new( 151 | rect: Rect, 152 | qh: &QueueHandle, 153 | conn: &Connection, 154 | output: wl_output::WlOutput, 155 | info: OutputInfo, 156 | runtime_data: &RuntimeData, 157 | ) -> Self { 158 | let wl_surface = runtime_data.compositor_state.create_surface(qh); 159 | 160 | let layer = runtime_data.layer_state.create_layer_surface( 161 | qh, 162 | wl_surface.clone(), 163 | Layer::Overlay, 164 | Some("watershot"), 165 | Some(&output), 166 | ); 167 | 168 | // Set the right scale for the buffer 169 | wl_surface.set_buffer_scale(info.scale_factor); 170 | 171 | layer.set_anchor(Anchor::TOP | Anchor::BOTTOM | Anchor::LEFT | Anchor::RIGHT); 172 | layer.set_exclusive_zone(-1); 173 | layer.set_keyboard_interactivity(KeyboardInteractivity::Exclusive); 174 | 175 | layer.commit(); 176 | 177 | // Each monitor also gets their own screenshot to preserve clarity as much as possible 178 | let grim_output = Command::new( 179 | runtime_data 180 | .args 181 | .grim 182 | .as_ref() 183 | .unwrap_or(&"grim".to_string()), 184 | ) 185 | .arg("-t") 186 | .arg("ppm") 187 | .arg("-o") 188 | .arg(info.name.as_ref().unwrap()) 189 | .arg("-") 190 | .output() 191 | .expect("Failed to run grim command!") 192 | .stdout; 193 | 194 | let image = 195 | image::io::Reader::with_format(Cursor::new(grim_output), image::ImageFormat::Pnm) 196 | .decode() 197 | .expect("Failed to parse grim image!"); 198 | let handle = RawWgpuHandles::new(conn, &wl_surface); 199 | 200 | let surface = unsafe { runtime_data.instance.create_surface(&handle).unwrap() }; 201 | 202 | Self { 203 | layer, 204 | wl_surface, 205 | rect, 206 | output_info: info, 207 | image, 208 | surface, 209 | rendering: None, 210 | } 211 | } 212 | } 213 | 214 | #[derive(Debug, Copy, Clone)] 215 | pub struct Extents { 216 | pub start_x: i32, 217 | pub start_y: i32, 218 | pub end_x: i32, 219 | pub end_y: i32, 220 | } 221 | 222 | impl Extents { 223 | pub fn to_rect(self) -> Rect { 224 | let (x, width) = if self.start_x < self.end_x { 225 | (self.start_x, self.end_x - self.start_x) 226 | } else { 227 | (self.end_x, self.start_x - self.end_x) 228 | }; 229 | 230 | let (y, height) = if self.start_y < self.end_y { 231 | (self.start_y, self.end_y - self.start_y) 232 | } else { 233 | (self.end_y, self.start_y - self.end_y) 234 | }; 235 | Rect { 236 | x, 237 | y, 238 | width, 239 | height, 240 | } 241 | } 242 | 243 | pub fn to_rect_clamped(self, area: &Rect) -> Rect { 244 | let mut rect = self.to_rect(); 245 | 246 | rect.x = rect.x.clamp(area.x, area.x + area.width - rect.width); 247 | rect.y = rect.y.clamp(area.y, area.y + area.height - rect.height); 248 | 249 | rect 250 | } 251 | } 252 | 253 | #[derive(Debug, Default, Clone, Copy, Eq, PartialEq)] 254 | pub struct Rect { 255 | pub x: T, 256 | pub y: T, 257 | pub width: T, 258 | pub height: T, 259 | } 260 | 261 | impl Rect { 262 | pub fn new(x: T, y: T, width: T, height: T) -> Self { 263 | Self { 264 | x, 265 | y, 266 | width, 267 | height, 268 | } 269 | } 270 | } 271 | 272 | impl Rect { 273 | pub fn intersects(&self, other: &Self) -> bool { 274 | ((self.x + self.width).min(other.x + other.width) - self.x.max(other.x)) > 0 275 | && ((self.y + self.height).min(other.y + other.height) - self.y.max(other.y)) > 0 276 | } 277 | 278 | pub fn to_extents(self) -> Extents { 279 | Extents { 280 | start_x: self.x, 281 | start_y: self.y, 282 | end_x: self.x + self.width, 283 | end_y: self.y + self.height, 284 | } 285 | } 286 | 287 | pub fn extend(&mut self, other: &Self) { 288 | if *self == Self::default() { 289 | *self = *other; 290 | return; 291 | } 292 | 293 | let x = self.x.min(other.x); 294 | let y = self.y.min(other.y); 295 | let width = (self.x - x + self.width).max(other.x - x + other.width); 296 | let height = (self.y - y + self.height).max(other.y - y + other.height); 297 | 298 | *self = Self::new(x, y, width, height); 299 | } 300 | 301 | /// Constrain the rectangle to fit inside the provided rectangle 302 | pub fn constrain(&self, area: &Self) -> Option { 303 | if !self.intersects(area) { 304 | None 305 | } else { 306 | let mut res = *self; 307 | 308 | res.x = res.x.max(area.x); 309 | res.y = res.y.max(area.y); 310 | 311 | res.width = (self.x + self.width - res.x).clamp(0, area.width); 312 | res.height = (self.y + self.height - res.y).clamp(0, area.height); 313 | 314 | Some(res) 315 | } 316 | } 317 | } 318 | 319 | #[derive(Debug, Copy, Clone)] 320 | pub enum SelectionModifier { 321 | Left, 322 | Right, 323 | Top, 324 | Bottom, 325 | TopRight, 326 | BottomRight, 327 | BottomLeft, 328 | TopLeft, 329 | // Offset from top left corner and original extents 330 | Center(i32, i32, Extents), 331 | } 332 | 333 | #[derive(Clone)] 334 | pub enum Selection { 335 | Rectangle(Option), 336 | Display(Option), 337 | Window(Option), 338 | } 339 | 340 | impl Default for Selection { 341 | fn default() -> Self { 342 | Self::Rectangle(None) 343 | } 344 | } 345 | 346 | impl Selection { 347 | pub fn flattened(&self) -> Selection { 348 | match self { 349 | Self::Window(Some(window)) => Self::Rectangle(Some(RectangleSelection { 350 | extents: window.rect.to_extents(), 351 | modifier: None, 352 | active: false, 353 | })), 354 | Self::Window(None) => Self::Rectangle(None), 355 | _ => self.clone(), 356 | } 357 | } 358 | 359 | pub fn from_window(window: Option) -> Self { 360 | match window { 361 | Some(window) => Self::Window(Some(window)), 362 | None => Self::Rectangle(None), 363 | } 364 | } 365 | } 366 | 367 | #[derive(Debug, Clone, Copy)] 368 | pub struct RectangleSelection { 369 | pub extents: Extents, 370 | pub modifier: Option, 371 | pub active: bool, 372 | } 373 | 374 | #[derive(Debug, Clone)] 375 | pub struct DisplaySelection { 376 | pub wl_surface: wl_surface::WlSurface, 377 | } 378 | 379 | impl DisplaySelection { 380 | pub fn new(surface: wl_surface::WlSurface) -> Self { 381 | Self { 382 | wl_surface: surface, 383 | } 384 | } 385 | } 386 | 387 | impl RectangleSelection { 388 | pub fn new(x: i32, y: i32) -> Self { 389 | Self { 390 | extents: Extents { 391 | start_x: x, 392 | start_y: y, 393 | end_x: x, 394 | end_y: y, 395 | }, 396 | modifier: None, 397 | active: true, 398 | } 399 | } 400 | } 401 | 402 | pub enum MonitorIdentification { 403 | Layer(LayerSurface), 404 | Surface(wl_surface::WlSurface), 405 | } 406 | 407 | pub enum ExitState { 408 | /// Not going to exit 409 | None, 410 | /// Only exit 411 | ExitOnly, 412 | /// Exit and perform actions on the selection 413 | ExitWithSelection(Rect), 414 | } 415 | 416 | pub struct RawWgpuHandles { 417 | window: RawWindowHandle, 418 | display: RawDisplayHandle, 419 | } 420 | 421 | pub enum SelectionState { 422 | CenterChanged, 423 | HandlesChanged, 424 | Unchanged, 425 | } 426 | 427 | impl RawWgpuHandles { 428 | pub fn new(conn: &Connection, surface: &wl_surface::WlSurface) -> Self { 429 | let mut display_handle = WaylandDisplayHandle::empty(); 430 | display_handle.display = conn.backend().display_ptr() as *mut _; 431 | 432 | let mut window_handle = WaylandWindowHandle::empty(); 433 | window_handle.surface = surface.id().as_ptr() as *mut _; 434 | 435 | Self { 436 | window: RawWindowHandle::Wayland(window_handle), 437 | display: RawDisplayHandle::Wayland(display_handle), 438 | } 439 | } 440 | } 441 | 442 | unsafe impl HasRawWindowHandle for RawWgpuHandles { 443 | fn raw_window_handle(&self) -> RawWindowHandle { 444 | self.window 445 | } 446 | } 447 | 448 | unsafe impl HasRawDisplayHandle for RawWgpuHandles { 449 | fn raw_display_handle(&self) -> RawDisplayHandle { 450 | self.display 451 | } 452 | } 453 | -------------------------------------------------------------------------------- /src/window/hyprland.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashSet; 2 | 3 | use hyprland::{ 4 | data::{Client, Clients, CursorPosition, Monitors}, 5 | shared::{HyprData, HyprDataActiveOptional, WorkspaceId}, 6 | }; 7 | 8 | use crate::types::Rect; 9 | 10 | use super::{CompositorBackend, CompositorNotAvailable, InitializeBackend, WindowDescriptor}; 11 | 12 | pub struct HyprlandBackend; 13 | 14 | impl CompositorBackend for HyprlandBackend { 15 | fn get_all_windows(&self) -> Vec { 16 | // TODO: Special Workspaces don't appear under monitors, therefore 17 | // windows from specials can't be focused yet. 18 | let active_workspace_ids: HashSet = Monitors::get() 19 | .unwrap() 20 | .iter() 21 | .map(|monitor| monitor.active_workspace.id) 22 | .collect(); 23 | 24 | let mut windows: Vec<_> = Clients::get() 25 | .unwrap() 26 | .into_iter().filter(|client| active_workspace_ids.contains(&client.workspace.id)) 27 | .map(WindowDescriptor::from) 28 | .collect(); 29 | 30 | windows.reverse(); 31 | 32 | windows 33 | } 34 | 35 | fn get_focused(&self) -> Option { 36 | Client::get_active() 37 | .ok() 38 | .flatten() 39 | .map(WindowDescriptor::from) 40 | } 41 | 42 | fn get_mouse_position(&self) -> (i32, i32) { 43 | let CursorPosition { x, y } = CursorPosition::get().unwrap(); 44 | (x as i32, y as i32) 45 | } 46 | } 47 | 48 | impl InitializeBackend for HyprlandBackend { 49 | fn try_new() -> Result, super::CompositorNotAvailable> { 50 | let mut env_vars = std::env::vars(); 51 | match env_vars.find(|(key, _value)| key == "HYPRLAND_INSTANCE_SIGNATURE") { 52 | Some(_) => Ok(Box::new(HyprlandBackend) as Box), 53 | None => Err(CompositorNotAvailable::NotRunning), 54 | } 55 | } 56 | } 57 | 58 | impl From for WindowDescriptor { 59 | fn from(value: Client) -> Self { 60 | Self { 61 | initial_title: value.initial_title, 62 | title: value.title, 63 | initial_class: value.initial_class, 64 | class: value.class, 65 | rect: Rect { 66 | x: value.at.0 as i32, 67 | y: value.at.1 as i32, 68 | width: value.size.0 as i32, 69 | height: value.size.1 as i32, 70 | }, 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/window/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::{traits::Contains, types::Rect}; 2 | 3 | pub mod hyprland; 4 | pub mod search; 5 | 6 | #[derive(Debug, Clone)] 7 | pub struct WindowDescriptor { 8 | pub initial_title: String, 9 | pub title: String, 10 | pub initial_class: String, 11 | pub class: String, 12 | pub rect: Rect, 13 | } 14 | 15 | pub trait CompositorBackend { 16 | fn get_all_windows(&self) -> Vec; 17 | fn get_focused(&self) -> Option; 18 | fn get_mouse_position(&self) -> (i32, i32); 19 | } 20 | 21 | pub trait InitializeBackend { 22 | fn try_new() -> Result, CompositorNotAvailable>; 23 | } 24 | 25 | pub trait FindWindowExt { 26 | fn find_by_position(&self, position: &(i32, i32)) -> Option<&WindowDescriptor>; 27 | fn find_by_search_param(&self, param: search::WindowSearchParam) -> Option<&WindowDescriptor>; 28 | } 29 | 30 | impl FindWindowExt for Vec { 31 | fn find_by_position(&self, position: &(i32, i32)) -> Option<&WindowDescriptor> { 32 | self.iter().find(|window| window.rect.contains(position)) 33 | } 34 | 35 | fn find_by_search_param(&self, param: search::WindowSearchParam) -> Option<&WindowDescriptor> { 36 | use search::WindowSearchAttribute::*; 37 | 38 | self.iter().find(|window| { 39 | let attr_value = match param.attribute { 40 | InitialTitle => &window.initial_title, 41 | Title => &window.title, 42 | InitialClass => &window.initial_class, 43 | Class => &window.class, 44 | }; 45 | 46 | param.value.is_match(attr_value) 47 | }) 48 | } 49 | } 50 | 51 | pub enum CompositorNotAvailable { 52 | NotInstalled, 53 | NotRunning, 54 | } 55 | -------------------------------------------------------------------------------- /src/window/search.rs: -------------------------------------------------------------------------------- 1 | use std::str::FromStr; 2 | 3 | use regex::Regex; 4 | use strum::EnumString; 5 | 6 | #[derive(Debug, Clone, Copy, EnumString, PartialEq)] 7 | #[strum(serialize_all = "snake_case")] 8 | pub enum WindowSearchAttribute { 9 | InitialTitle, 10 | Title, 11 | InitialClass, 12 | Class, 13 | } 14 | 15 | #[derive(Debug, Clone)] 16 | pub struct WindowSearchParam { 17 | pub attribute: WindowSearchAttribute, 18 | pub value: Regex, 19 | } 20 | 21 | impl FromStr for WindowSearchParam { 22 | type Err = String; 23 | 24 | fn from_str(s: &str) -> Result { 25 | let (attribute, value) = s.split_once('=').ok_or( 26 | " 27 | Invalid search parameter. Search parameters should be in the form of 28 | \"attribute=value\". Valid attributes are \"initial_title\", \"title\", 29 | \"initial_class\", and \"class\". 30 | ", 31 | )?; 32 | 33 | let attribute: WindowSearchAttribute = attribute 34 | .parse() 35 | .map_err(|_| format!("Invalid search attribute \"{}\"", attribute))?; 36 | 37 | let value = Regex::new(value).map_err(|_| format!("Invalid search value \"{}\"", value))?; 38 | 39 | Ok(WindowSearchParam { attribute, value }) 40 | } 41 | } 42 | 43 | #[cfg(test)] 44 | mod tests { 45 | 46 | use super::*; 47 | 48 | #[test] 49 | fn test_window_search_param_from_str() { 50 | use WindowSearchAttribute::*; 51 | 52 | let params_and_expected = [ 53 | ("initial_title=^hello$", InitialTitle), 54 | ("title=^hello$", Title), 55 | ("initial_class=^hello$", InitialClass), 56 | ("class=^hello$", Class), 57 | ]; 58 | 59 | for (param_str, expected) in params_and_expected { 60 | let param: Result = param_str.parse(); 61 | 62 | assert!(param.is_ok()); 63 | 64 | let param = param.unwrap(); 65 | 66 | assert_eq!(param.attribute, expected, "Parsing {param_str}"); 67 | assert_eq!(param.value.as_str(), "^hello$"); 68 | } 69 | } 70 | } 71 | --------------------------------------------------------------------------------