diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 49f07bab..a9505f7f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -12,6 +12,13 @@ jobs: steps: - name: Checkout uses: actions/checkout@v2 + + - name: Install wayland dependencies + run: | + sudo apt-get update + sudo apt-get install -y --no-install-recommends \ + libwayland-dev \ + libegl-dev \ - name: Build run: | diff --git a/.gitignore b/.gitignore index 5a4f6354..2622eacf 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,8 @@ target *.gz *.out .direnv +*.jpg +*.jpeg +*.png +*.ppm +*.qoi diff --git a/Cargo.lock b/Cargo.lock index 2d5bff5d..96182bc9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8,11 +8,26 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "anstream" -version = "0.6.5" +version = "0.6.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d664a92ecae85fd0a7392615844904654d1d5f5514837f471ddef4a057aba1b6" +checksum = "d96bd03f33fe50a863e394ee9718a706f988b9079b20c3784fb726e7678b62fb" dependencies = [ "anstyle", "anstyle-parse", @@ -24,9 +39,9 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.4" +version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7079075b41f533b8c61d2a4d073c4676e1f8b249ff94a393b0595db304e0dd87" +checksum = "8901269c6307e8d93993578286ac0edf7f195079ffff5ebdeea6a59ffb7e36bc" [[package]] name = "anstyle-parse" @@ -43,7 +58,7 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e28923312444cdd728e4738b3f9c9cac739500909bb3d3c94b43551b16517648" dependencies = [ - "windows-sys 0.52.0", + "windows-sys", ] [[package]] @@ -53,7 +68,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7" dependencies = [ "anstyle", - "windows-sys 0.52.0", + "windows-sys", ] [[package]] @@ -70,15 +85,35 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.4.1" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" + +[[package]] +name = "bumpalo" +version = "3.15.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07" +checksum = "7ff69b9dd49fd426c69a0db9fc04dd934cdb6645ff000864d98f7e2af8830eaa" [[package]] name = "bytemuck" -version = "1.14.0" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "374d28ec25809ee0e23827c2ab573d729e293f281dfe393500e7ad618baa61c6" +checksum = "5d6d68c57235a3a081186990eca2867354726650f42f7516ca50c28d6281fd15" +dependencies = [ + "bytemuck_derive", +] + +[[package]] +name = "bytemuck_derive" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4da9a32f3fed317401fa3c862968128267c3106685286e15d5aaa3d7389c2f60" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] [[package]] name = "byteorder" @@ -88,10 +123,11 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "cc" -version = "1.0.83" +version = "1.0.90" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" +checksum = "8cd6604a82acf3039f1144f54b8eb34e91ffba622051189e71b781822d5ee1f5" dependencies = [ + "jobserver", "libc", ] @@ -101,20 +137,41 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "cfg_aliases" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e" + +[[package]] +name = "chrono" +version = "0.4.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eaf5903dcbc0a39312feb77df2ff4c76387d591b9fc7b04a238dcf8bb62639a" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-targets", +] + [[package]] name = "clap" -version = "4.4.11" +version = "4.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfaff671f6b22ca62406885ece523383b9b64022e341e53e009a62ebc47a45f2" +checksum = "949626d00e063efc93b6dca932419ceb5432f99769911c0b995f7e884c778813" dependencies = [ "clap_builder", + "clap_derive", ] [[package]] name = "clap_builder" -version = "4.4.11" +version = "4.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a216b506622bb1d316cd51328dce24e07bdff4a6128a47c7e7fad11878d5adbb" +checksum = "ae129e2e766ae0ec03484e609954119f123cc1fe650337e155d03b022f24f7b4" dependencies = [ "anstream", "anstyle", @@ -122,11 +179,23 @@ dependencies = [ "strsim", ] +[[package]] +name = "clap_derive" +version = "4.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90239a040c80f5e14809ca132ddc4176ab33d5e17e49691793296e3fcb34d72f" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "clap_lex" -version = "0.6.0" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "702fc72eb24e5a1e48ce58027a675bc24edd52096d5397d4aea7c6dd9eca0bd1" +checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce" [[package]] name = "color_quant" @@ -142,26 +211,43 @@ checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" [[package]] name = "console" -version = "0.15.7" +version = "0.15.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c926e00cc70edefdc64d3a5ff31cc65bb97a3460097762bd23afb4d8145fccf8" +checksum = "0e1f83fc076bd6dd27517eacdf25fef6c4dfe5f1d7448bafaaf3a26f13b5e4eb" dependencies = [ "encode_unicode", "lazy_static", "libc", "unicode-width", - "windows-sys 0.45.0", + "windows-sys", ] +[[package]] +name = "core-foundation-sys" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" + [[package]] name = "crc32fast" -version = "1.3.2" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" +checksum = "b3855a8a784b474f333699ef2bbca9db2c4a1f6d9088a90a2d25b1eb53111eaa" dependencies = [ "cfg-if", ] +[[package]] +name = "derive-new" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d150dea618e920167e5973d70ae6ece4385b7164e0d799fe7c122dd0a5d912ad" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "dialoguer" version = "0.11.0" @@ -191,12 +277,57 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ea835d29036a4087793836fa931b08837ad5e957da9e23886b29586fb9b6650" +[[package]] +name = "drm" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98888c4bbd601524c11a7ed63f814b8825f420514f78e96f752c437ae9cbb5d1" +dependencies = [ + "bitflags 2.5.0", + "bytemuck", + "drm-ffi", + "drm-fourcc", + "rustix", +] + +[[package]] +name = "drm-ffi" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97c98727e48b7ccb4f4aea8cfe881e5b07f702d17b7875991881b41af7278d53" +dependencies = [ + "drm-sys", + "rustix", +] + +[[package]] +name = "drm-fourcc" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0aafbcdb8afc29c1a7ee5fbe53b5d62f4565b35a042a662ca9fecd0b54dae6f4" + +[[package]] +name = "drm-sys" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd39dde40b6e196c2e8763f23d119ddb1a8714534bf7d77fa97a65b0feda3986" +dependencies = [ + "libc", + "linux-raw-sys 0.6.4", +] + [[package]] name = "encode_unicode" version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + [[package]] name = "errno" version = "0.3.8" @@ -204,7 +335,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys", +] + +[[package]] +name = "eyre" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd915d99f24784cdc19fd37ef22b97e3ff0ae756c7e492e9fbfe897d61e2aec" +dependencies = [ + "indenter", + "once_cell", ] [[package]] @@ -215,13 +356,19 @@ checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" [[package]] name = "fdeflate" -version = "0.3.1" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64d6dafc854908ff5da46ff3f8f473c6984119a2876a383a860246dd7841a868" +checksum = "4f9bfee30e4dedf0ab8b422f03af778d9612b63f502710fc500a334ebe2de645" dependencies = [ "simd-adler32", ] +[[package]] +name = "fixedbitset" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" + [[package]] name = "flate2" version = "1.0.28" @@ -232,6 +379,12 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + [[package]] name = "fuzzy-matcher" version = "0.3.7" @@ -241,27 +394,187 @@ dependencies = [ "thread_local", ] +[[package]] +name = "gbm" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45bf55ba6dd53ad0ac115046ff999c5324c283444ee6e0be82454c4e8eb2f36a" +dependencies = [ + "bitflags 2.5.0", + "drm", + "drm-fourcc", + "gbm-sys", + "libc", + "wayland-backend", + "wayland-server", +] + +[[package]] +name = "gbm-sys" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fd2d6bf7c0143b38beece05f9a5c4c851a49a8434f62bf58ff28da92b0ddc58" +dependencies = [ + "libc", +] + +[[package]] +name = "gl" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a94edab108827d67608095e269cf862e60d920f144a5026d3dbcfd8b877fb404" +dependencies = [ + "gl_generator", +] + +[[package]] +name = "gl_generator" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a95dfc23a2b4a9a2f5ab41d194f8bfda3cabec42af4e39f08c339eb2a0c124d" +dependencies = [ + "khronos_api", + "log", + "xml-rs", +] + +[[package]] +name = "gl_loader" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e32d96dd5f881490e537041d5532320812ba096097f07fccb4626578da0b99d3" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "glob" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" + +[[package]] +name = "hashbrown" +version = "0.14.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "home" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + [[package]] name = "image" -version = "0.24.7" +version = "0.24.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f3dfdbdd72063086ff443e297b61695500514b1e41095b6fb9a5ab48a70a711" +checksum = "5690139d2f55868e080017335e4b94cb7414274c74f1669c84fb5feba2c9f69d" dependencies = [ "bytemuck", "byteorder", "color_quant", "jpeg-decoder", - "num-rational", "num-traits", "png", "qoi", + "webp", +] + +[[package]] +name = "indenter" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683" + +[[package]] +name = "indexmap" +version = "2.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "io-lifetimes" +version = "2.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a611371471e98973dbcab4e0ec66c31a10bc356eeb4d54a0e05eac8158fe38c" + +[[package]] +name = "jobserver" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab46a6e9526ddef3ae7f787c06f0f2600639ba80ea3eade3d8e670a2230f51d6" +dependencies = [ + "libc", ] [[package]] name = "jpeg-decoder" -version = "0.3.0" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5d4a7da358eff58addd2877a45865158f0d78c911d43a5784ceb7bbf52833b0" + +[[package]] +name = "js-sys" +version = "0.3.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "khronos-egl" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6aae1df220ece3c0ada96b8153459b67eebe9ae9212258bb0134ae60416fdf76" +dependencies = [ + "libc", + "pkg-config", +] + +[[package]] +name = "khronos_api" +version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc0000e42512c92e31c2252315bda326620a4e034105e900c98ec492fa077b3e" +checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc" [[package]] name = "lazy_static" @@ -271,75 +584,102 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.151" +version = "0.2.153" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "302d7ab3130588088d277783b1e2d2e10c9e9e4a16dd9050e6ec93fb3e7048f4" +checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" [[package]] name = "libloading" -version = "0.8.1" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c571b676ddfc9a8c12f1f3d3085a7b163966a8fd8098a90640953ce5f6170161" +checksum = "e310b3a6b5907f99202fcdb4960ff45b93735d7c7d96b760fcff8db2dc0e103d" dependencies = [ "cfg-if", - "windows-sys 0.48.0", + "windows-targets", ] [[package]] name = "libwayshot" version = "0.3.2-dev" dependencies = [ + "drm", + "gbm", + "gl", "image", + "khronos-egl", "memmap2", - "nix 0.27.1", + "rustix", "thiserror", "tracing", + "wayland-backend", "wayland-client", "wayland-protocols", "wayland-protocols-wlr", ] +[[package]] +name = "libwebp-sys" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "829b6b604f31ed6d2bccbac841fe0788de93dbd87e4eb1ba2c4adfe8c012a838" +dependencies = [ + "cc", + "glob", +] + [[package]] name = "linux-raw-sys" -version = "0.4.12" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4cd1a83af159aa67994778be9070f0ae1bd732942279cabb14f86f986a21456" +checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" + +[[package]] +name = "linux-raw-sys" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0b5399f6804fbab912acbd8878ed3532d506b7c951b8f9f164ef90fef39e3f4" [[package]] name = "log" -version = "0.4.20" +version = "0.4.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" +checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" [[package]] name = "memchr" -version = "2.6.4" +version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" +checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" [[package]] name = "memmap2" -version = "0.9.2" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39a69c7c189ae418f83003da62820aca28d15a07725ce51fb924999335d622ff" +checksum = "fe751422e4a8caa417e13c3ea66452215d7d63e19e604f4980461212f3ae1322" dependencies = [ "libc", ] [[package]] name = "memoffset" -version = "0.7.1" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5de893c32cde5f383baa4c04c5d6dbdd735cfd4a794b0debdb2bb1b421da5ff4" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" dependencies = [ "autocfg", ] +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + [[package]] name = "miniz_oxide" -version = "0.7.1" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" +checksum = "9d811f3e15f28568be3407c8e7fdb6514c1cda3cb30683f15b6a1a1dc4ea14a7" dependencies = [ "adler", "simd-adler32", @@ -347,25 +687,24 @@ dependencies = [ [[package]] name = "nix" -version = "0.26.4" +version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "598beaf3cc6fdd9a5dfb1630c2800c7acd31df7aaf0f565796fba2b53ca1af1b" +checksum = "ab2156c4fce2f8df6c499cc1c763e4394b7482525bf2a9701c9d79d215f519e4" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.5.0", "cfg-if", + "cfg_aliases", "libc", - "memoffset", ] [[package]] -name = "nix" -version = "0.27.1" +name = "nom" +version = "7.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2eb04e9c688eff1c89d72b407f168cf79bb9e867a9d3323ed6c01519eb9cc053" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" dependencies = [ - "bitflags 2.4.1", - "cfg-if", - "libc", + "memchr", + "minimal-lexical", ] [[package]] @@ -379,47 +718,46 @@ dependencies = [ ] [[package]] -name = "num-integer" -version = "0.1.45" +name = "num-traits" +version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" +checksum = "da0df0e5185db44f69b44f26786fe401b6c293d1907744beaa7fa62b2e5a517a" dependencies = [ "autocfg", - "num-traits", ] [[package]] -name = "num-rational" -version = "0.4.1" +name = "once_cell" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0638a1c9d0a3c0914158145bc76cff373a75a627e6ecbfb71cbe6f453a5a19b0" -dependencies = [ - "autocfg", - "num-integer", - "num-traits", -] +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" [[package]] -name = "num-traits" -version = "0.2.17" +name = "os_pipe" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c" +checksum = "57119c3b893986491ec9aa85056780d3a0f3cf4da7cc09dd3650dbd6c6738fb9" dependencies = [ - "autocfg", + "libc", + "windows-sys", ] -[[package]] -name = "once_cell" -version = "1.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" - [[package]] name = "overload" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" +[[package]] +name = "petgraph" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1d3afd2628e69da2be385eb6f2fd57c8ac7977ceeff6dc166ff1657b0e386a9" +dependencies = [ + "fixedbitset", + "indexmap", +] + [[package]] name = "pin-project-lite" version = "0.2.13" @@ -428,15 +766,15 @@ checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" [[package]] name = "pkg-config" -version = "0.3.27" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" +checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" [[package]] name = "png" -version = "0.17.10" +version = "0.17.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd75bf2d8dd3702b9707cdbc56a5b9ef42cec752eb8b3bafc01234558442aa64" +checksum = "06e4b0d3d1312775e782c86c91a111aa1f910cbb65e1337f9975b5f9a554b5e1" dependencies = [ "bitflags 1.3.2", "crc32fast", @@ -447,9 +785,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.70" +version = "1.0.79" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39278fbbf5fb4f646ce651690877f89d1c5811a3d4acb27700c1cb3cdb78fd3b" +checksum = "e835ff2298f5721608eb1a980ecaee1aef2c132bf95ecc026a11b7bf3c01c02e" dependencies = [ "unicode-ident", ] @@ -465,42 +803,33 @@ dependencies = [ [[package]] name = "quick-xml" -version = "0.30.0" +version = "0.31.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eff6510e86862b57b210fd8cbe8ed3f0d7d600b9c2863cd4549a2e033c66e956" +checksum = "1004a344b30a54e2ee58d66a71b32d2db2feb0a31f9a2d302bf0536f15de2a33" dependencies = [ "memchr", ] [[package]] name = "quote" -version = "1.0.33" +version = "1.0.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" +checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" dependencies = [ "proc-macro2", ] -[[package]] -name = "redox_syscall" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" -dependencies = [ - "bitflags 1.3.2", -] - [[package]] name = "rustix" -version = "0.38.28" +version = "0.38.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72e572a5e8ca657d7366229cdde4bd14c4eb5499a9573d4d366fe1b599daa316" +checksum = "65e04861e65f21776e67888bfbea442b3642beaa0138fdb1dd7a84a52dffdb89" dependencies = [ - "bitflags 2.4.1", + "bitflags 2.5.0", "errno", "libc", - "linux-raw-sys", - "windows-sys 0.52.0", + "linux-raw-sys 0.4.13", + "windows-sys", ] [[package]] @@ -532,21 +861,21 @@ checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" [[package]] name = "smallvec" -version = "1.11.2" +version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4dccd0940a2dcdf68d092b8cbab7dc0ad8fa938bf95787e1b916b0e3d0e8e970" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" [[package]] name = "strsim" -version = "0.10.0" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" +checksum = "5ee073c9e4cd00e28217186dbe12796d692868f432bf2e97ee73bed0c56dfa01" [[package]] name = "syn" -version = "2.0.41" +version = "2.0.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44c8b28c477cc3bf0e7966561e3460130e1255f7a1cf71931075f1c5e7a7e269" +checksum = "7383cd0e49fff4b6b90ca5670bfd3e9d6a733b3f90c686605aa7eec8c4996032" dependencies = [ "proc-macro2", "quote", @@ -555,31 +884,30 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.8.1" +version = "3.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ef1adac450ad7f4b3c28589471ade84f25f731a7a0fe30d71dfa9f60fd808e5" +checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1" dependencies = [ "cfg-if", "fastrand", - "redox_syscall", "rustix", - "windows-sys 0.48.0", + "windows-sys", ] [[package]] name = "thiserror" -version = "1.0.51" +version = "1.0.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f11c217e1416d6f036b870f14e0413d480dbf28edbee1f877abaf0206af43bb7" +checksum = "03468839009160513471e86a034bb2c5c0e4baae3b43f79ffc55c4a5427b3297" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.51" +version = "1.0.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01742297787513b79cf8e29d1056ede1313e2420b7b3b15d0a768b4921f549df" +checksum = "c61f3ba182994efc43764a46c018c347bc492c79f024e705f46567b418f6d4f7" dependencies = [ "proc-macro2", "quote", @@ -588,9 +916,9 @@ dependencies = [ [[package]] name = "thread_local" -version = "1.1.7" +version = "1.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152" +checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" dependencies = [ "cfg-if", "once_cell", @@ -653,6 +981,20 @@ dependencies = [ "tracing-log", ] +[[package]] +name = "tree_magic_mini" +version = "3.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ee137597cdb361b55a4746983e4ac1b35ab6024396a419944ad473bb915265" +dependencies = [ + "fnv", + "home", + "memchr", + "nom", + "once_cell", + "petgraph", +] + [[package]] name = "unicode-ident" version = "1.0.12" @@ -677,15 +1019,69 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" +[[package]] +name = "wasm-bindgen" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.92" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" + [[package]] name = "wayland-backend" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19152ddd73f45f024ed4534d9ca2594e0ef252c1847695255dae47f34df9fbe4" +checksum = "9d50fa61ce90d76474c87f5fc002828d81b32677340112b4ef08079a9d459a40" dependencies = [ "cc", "downcast-rs", - "nix 0.26.4", + "rustix", "scoped-tls", "smallvec", "wayland-sys", @@ -693,23 +1089,33 @@ dependencies = [ [[package]] name = "wayland-client" -version = "0.31.1" +version = "0.31.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ca7d52347346f5473bf2f56705f360e8440873052e575e55890c4fa57843ed3" +checksum = "82fb96ee935c2cea6668ccb470fb7771f6215d1691746c2d896b447a00ad3f1f" dependencies = [ - "bitflags 2.4.1", - "nix 0.26.4", + "bitflags 2.5.0", + "rustix", "wayland-backend", "wayland-scanner", ] +[[package]] +name = "wayland-egl" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "355f652e5a24ae02d2ad536c8fc2d3dcc6c2bd635027cd6103a193e7d75eeda2" +dependencies = [ + "wayland-backend", + "wayland-sys", +] + [[package]] name = "wayland-protocols" -version = "0.31.0" +version = "0.31.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e253d7107ba913923dc253967f35e8561a3c65f914543e46843c88ddd729e21c" +checksum = "8f81f365b8b4a97f422ac0e8737c438024b5951734506b0e1d775c73030561f4" dependencies = [ - "bitflags 2.4.1", + "bitflags 2.5.0", "wayland-backend", "wayland-client", "wayland-scanner", @@ -721,7 +1127,7 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ad1f61b76b6c2d8742e10f9ba5c3737f6530b4c243132c2a2ccc8aa96fe25cd6" dependencies = [ - "bitflags 2.4.1", + "bitflags 2.5.0", "wayland-backend", "wayland-client", "wayland-protocols", @@ -730,15 +1136,29 @@ dependencies = [ [[package]] name = "wayland-scanner" -version = "0.31.0" +version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb8e28403665c9f9513202b7e1ed71ec56fde5c107816843fb14057910b2c09c" +checksum = "63b3a62929287001986fb58c789dce9b67604a397c15c611ad9f747300b6c283" dependencies = [ "proc-macro2", "quick-xml", "quote", ] +[[package]] +name = "wayland-server" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00e6e4d5c285bc24ba4ed2d5a4bd4febd5fd904451f465973225c8e99772fdb7" +dependencies = [ + "bitflags 2.5.0", + "downcast-rs", + "io-lifetimes", + "rustix", + "wayland-backend", + "wayland-scanner", +] + [[package]] name = "wayland-sys" version = "0.31.1" @@ -746,21 +1166,54 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "15a0c8eaff5216d07f226cb7a549159267f3467b289d9a2e52fd3ef5aae2b7af" dependencies = [ "dlib", + "libc", "log", + "memoffset", "pkg-config", ] +[[package]] +name = "waymirror-egl" +version = "0.1.0" +dependencies = [ + "gl", + "gl_loader", + "khronos-egl", + "libloading", + "libwayshot", + "thiserror", + "tracing", + "tracing-subscriber", + "wayland-backend", + "wayland-client", + "wayland-egl", + "wayland-protocols", +] + [[package]] name = "wayshot" version = "1.3.2-dev" dependencies = [ + "chrono", "clap", "dialoguer", + "eyre", "flate2", "image", "libwayshot", + "rustix", "tracing", "tracing-subscriber", + "wl-clipboard-rs", +] + +[[package]] +name = "webp" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bb5d8e7814e92297b0e1c773ce43d290bef6c17452dafd9fc49e5edb5beba71" +dependencies = [ + "libwebp-sys", ] [[package]] @@ -786,21 +1239,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] -name = "windows-sys" -version = "0.45.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" -dependencies = [ - "windows-targets 0.42.2", -] - -[[package]] -name = "windows-sys" -version = "0.48.0" +name = "windows-core" +version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" dependencies = [ - "windows-targets 0.48.5", + "windows-targets", ] [[package]] @@ -809,179 +1253,91 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets 0.52.0", -] - -[[package]] -name = "windows-targets" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" -dependencies = [ - "windows_aarch64_gnullvm 0.42.2", - "windows_aarch64_msvc 0.42.2", - "windows_i686_gnu 0.42.2", - "windows_i686_msvc 0.42.2", - "windows_x86_64_gnu 0.42.2", - "windows_x86_64_gnullvm 0.42.2", - "windows_x86_64_msvc 0.42.2", + "windows-targets", ] [[package]] name = "windows-targets" -version = "0.48.5" +version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +checksum = "7dd37b7e5ab9018759f893a1952c9420d060016fc19a472b4bb20d1bdd694d1b" dependencies = [ - "windows_aarch64_gnullvm 0.48.5", - "windows_aarch64_msvc 0.48.5", - "windows_i686_gnu 0.48.5", - "windows_i686_msvc 0.48.5", - "windows_x86_64_gnu 0.48.5", - "windows_x86_64_gnullvm 0.48.5", - "windows_x86_64_msvc 0.48.5", + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", ] -[[package]] -name = "windows-targets" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd" -dependencies = [ - "windows_aarch64_gnullvm 0.52.0", - "windows_aarch64_msvc 0.52.0", - "windows_i686_gnu 0.52.0", - "windows_i686_msvc 0.52.0", - "windows_x86_64_gnu 0.52.0", - "windows_x86_64_gnullvm 0.52.0", - "windows_x86_64_msvc 0.52.0", -] - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" - [[package]] name = "windows_aarch64_gnullvm" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.48.5" +version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" +checksum = "bcf46cf4c365c6f2d1cc93ce535f2c8b244591df96ceee75d8e83deb70a9cac9" [[package]] name = "windows_aarch64_msvc" -version = "0.52.0" +version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef" +checksum = "da9f259dd3bcf6990b55bffd094c4f7235817ba4ceebde8e6d11cd0c5633b675" [[package]] name = "windows_i686_gnu" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" - -[[package]] -name = "windows_i686_gnu" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" - -[[package]] -name = "windows_i686_gnu" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313" - -[[package]] -name = "windows_i686_msvc" -version = "0.42.2" +version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" +checksum = "b474d8268f99e0995f25b9f095bc7434632601028cf86590aea5c8a5cb7801d3" [[package]] name = "windows_i686_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" - -[[package]] -name = "windows_i686_msvc" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.48.5" +version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" +checksum = "1515e9a29e5bed743cb4415a9ecf5dfca648ce85ee42e15873c3cd8610ff8e02" [[package]] name = "windows_x86_64_gnu" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.42.2" +version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" +checksum = "5eee091590e89cc02ad514ffe3ead9eb6b660aedca2183455434b93546371a03" [[package]] name = "windows_x86_64_gnullvm" -version = "0.48.5" +version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e" +checksum = "77ca79f2451b49fa9e2af39f0747fe999fcda4f5e241b2898624dca97a1f2177" [[package]] name = "windows_x86_64_msvc" -version = "0.42.2" +version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" +checksum = "32b752e52a2da0ddfbdbcc6fceadfeede4c939ed16d13e648833a61dfb611ed8" [[package]] -name = "windows_x86_64_msvc" -version = "0.48.5" +name = "wl-clipboard-rs" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" +checksum = "12b41773911497b18ca8553c3daaf8ec9fe9819caf93d451d3055f69de028adb" +dependencies = [ + "derive-new", + "libc", + "log", + "nix", + "os_pipe", + "tempfile", + "thiserror", + "tree_magic_mini", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-protocols-wlr", +] [[package]] -name = "windows_x86_64_msvc" -version = "0.52.0" +name = "xml-rs" +version = "0.8.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" +checksum = "791978798f0597cfc70478424c2b4fdc2b7a8024aaff78497ef00f24ef674193" [[package]] name = "zeroize" diff --git a/Cargo.toml b/Cargo.toml index 4979f4eb..b13e9351 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [workspace] resolver = "2" -members = ["wayshot", "libwayshot"] +members = ["wayshot", "libwayshot","libwayshot/examples/waymirror-egl"] [workspace.package] authors = ["Shinyzenith "] diff --git a/README.md b/README.md index b0c60dbf..ced3676d 100644 --- a/README.md +++ b/README.md @@ -23,10 +23,10 @@ NOTE: Read `man 7 wayshot` for more examples. NOTE: Read `man wayshot` for flag information. -Region Selection: +Screenshot and Crop Region: ```bash -wayshot -s "$(slurp)" +wayshot -s ``` Fullscreen: @@ -38,13 +38,13 @@ wayshot Screenshot and copy to clipboard: ```bash -wayshot --stdout | wl-copy +wayshot --clipboard ``` Pick a hex color code, using ImageMagick: ```bash -wayshot -s "$(slurp -p)" --stdout | convert - -format '%[pixel:p{0,0}]' txt:-|grep -E "#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})" -o +wayshot -s - | convert - -format '%[pixel:p{0,0}]' txt:-|grep -E "#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})" -o ``` # Installation diff --git a/docs/wayshot.1.scd b/docs/wayshot.1.scd index a71f50aa..65dea812 100644 --- a/docs/wayshot.1.scd +++ b/docs/wayshot.1.scd @@ -6,53 +6,70 @@ Wayshot - Screenshot tool for compositors implementing zwlr_screencopy_v1 such a # SYNOPSIS -*wayshot* [_options_] +*wayshot* [_options_] [_output_] + +# ARGUMENTS + +*output*, + Location to send captured screenshot to, it can be of the following types: + 1. A directory, image outputs will be saved in the default format "wayshot-yyyy-mm-dd-hh-mm-ss.png" + + 2. A path (Encoding is automatically inferred from the extension). + + 3. '-' which sends the screenshot to stdout. + + The *--clipboard* option can also be used simultaneously with any of the above to copy the image to the clipboard too. # OPTIONS -*-h*, *--help* - Print help message and quit. +*-h*, + Print concise help messages and quit. + +*--help*, + Print full help message and quit. *-V*, *--version* Print version information. -*-d*, *--debug* - Enable debug mode. +*--log-level * + Log level to be used for printing to stderr + Possible values: trace, debug, info, warn, error + + Default value: info *-c*, *--cursor* Enable cursor visibility in screenshots. +*--clipboard* + Copy image contents to clipboard also. + Using this flag will cause the wayshot process to fork and persist in the background offering the image + on the wayland clipboard until some other program overwrites the clipboard. + *-e*, *--extension* - Set the image encoder. + Set the image encoder. Without this option, encoding is either inferred from the *output* filename or defaults to png. Valid arguments: - - jpeg - jpg - png (Default encoder) - ppm - qoi + - webp -*-f*, *--file* - Set a custom file path. The default path is `./{current_unix_timestamp}-wayshot.{encoder}` - eg: 1659034753-wayshot.png - -*-l*, *--listoutputs* +*-l*, *--list-outputs* List all valid output names. This flag is generally used in combination with *-o* flag. -*--chooseoutput* +*--choose-output* Present a fuzzy selector for display (wl_output) selection. *-o*, *--output* Choose a particular display (wl_output) to screenshot. -*-s*, *--slurp* - Choose a portion of your display to screenshot using the slurp program. - https://github.com/emersion/slurp . Valid arguments have the form - "%x %y %w %h" or "%x,%y %wx%h", where for example "%w" is an integer giving - the width of the region. - -*--stdout* - Emit image data to stdout. The following flag is helpful to pipe image data - to other programs. +*-s*, *--slurp*= + If this option is passed, wayshot takes a screenshot first and then uses the *slurp* program to select a portion of that screenshot + https://github.com/emersion/slurp . SLURP_ARGS are any arguments that need to be passed to *slurp*, see *slurp(1)* for more information. + + Example: + *wayshot -s="-b 11223377"* + - This takes a screenshot then calls slurp with "-b 11223377" as an argument for cropping the screenshot. # SEE ALSO - wayshot(7) diff --git a/docs/wayshot.7.scd b/docs/wayshot.7.scd index b4a59da2..459edcc0 100644 --- a/docs/wayshot.7.scd +++ b/docs/wayshot.7.scd @@ -6,22 +6,22 @@ Wayshot - Screenshot tool for compositors implementing zwlr_screencopy_v1 such a # SYNOPSIS -*wayshot* [_options_] +*wayshot* [_options_] [_output_] # REGION SELECTION -wayshot -s "$(slurp)" +wayshot -s -# FULLSCREEN +# CAPTURE FULLSCREEN wayshot # CUSTOM FILE PATH AND EXTENSION -wayshot -f ../screenshot.png --extension ppm +wayshot ../screenshot.png --extension ppm # SCREENSHOT AND COPY TO CLIPBOARD -wayshot --stdout -e jpeg | wl-copy +wayshot --clipboard # SCREENSHOT A PARTICULAR DISPLAY @@ -30,15 +30,15 @@ wayshot -o eDP-1 # PICK A HEX COLOR CODE, USING IMAGEMAGICk -wayshot -s "$(slurp)" --stdout | convert - -format '%[pixel:p{0,0}]' txt:-|grep -E "#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})" -o +wayshot -s - | convert - -format '%[pixel:p{0,0}]' txt:-|grep -E "#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})" -o # PICK A HEX COLOR CODE WITHOUT USING IMAGEMAGICK -wayshot -s "$(slurp -p)" --stdout -e ppm | tail -c 3 | od -An -tuC | xargs printf '#%02X%02X%02X\n' +wayshot -s - --encoding ppm | tail -c 3 | od -An -tuC | xargs printf '#%02X%02X%02X\n' # PICK A COLOR, USING IMAGEMAGICK -wayshot -s "$(slurp -p)" --stdout | convert - -format '%[pixel:p{0,0}]' txt:- +wayshot -s - | convert - -format '%[pixel:p{0,0}]' txt:- # AUTHORS diff --git a/libwayshot/Cargo.toml b/libwayshot/Cargo.toml index 8a9719b0..d811f78e 100644 --- a/libwayshot/Cargo.toml +++ b/libwayshot/Cargo.toml @@ -12,9 +12,16 @@ edition.workspace = true tracing.workspace = true image = { version = "0.24", default-features = false } memmap2 = "0.9.0" -nix = { version = "0.27.1", features = ["fs", "mman"] } +rustix = { version = "0.38", features = ["fs", "shm"] } thiserror = "1" wayland-client = "0.31.1" wayland-protocols = { version = "0.31.0", features = ["client", "unstable"] } wayland-protocols-wlr = { version = "0.2.0", features = ["client"] } +wayland-backend = { version = "0.3.3", features = ["client_system"] } + +gbm = "0.15.0" +drm = "0.12.0" + +gl = "0.14.0" +khronos-egl = { version = "6.0.0",features = ["static"] } \ No newline at end of file diff --git a/libwayshot/README.md b/libwayshot/README.md index a18983c7..a3bf2ab2 100644 --- a/libwayshot/README.md +++ b/libwayshot/README.md @@ -17,6 +17,6 @@ ```rust use libwayshot::WayshotConnection; -let wayshot_connection = WayshotConnection::new().unwrap(); -let image_buffer = wayshot_connection.screenshot_all().unwrap(); +let wayshot_connection = WayshotConnection::new()?; +let image_buffer = wayshot_connection.screenshot_all()?; ``` diff --git a/libwayshot/examples/waymirror-egl/Cargo.lock b/libwayshot/examples/waymirror-egl/Cargo.lock new file mode 100644 index 00000000..310b1996 --- /dev/null +++ b/libwayshot/examples/waymirror-egl/Cargo.lock @@ -0,0 +1,517 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "bitflags" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" + +[[package]] +name = "cc" +version = "1.0.90" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cd6604a82acf3039f1144f54b8eb34e91ffba622051189e71b781822d5ee1f5" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "dlib" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330c60081dcc4c72131f8eb70510f1ac07223e5d4163db481a04a0befcffa412" +dependencies = [ + "libloading", +] + +[[package]] +name = "downcast-rs" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ea835d29036a4087793836fa931b08837ad5e957da9e23886b29586fb9b6650" + +[[package]] +name = "errno" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "gl" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a94edab108827d67608095e269cf862e60d920f144a5026d3dbcfd8b877fb404" +dependencies = [ + "gl_generator", +] + +[[package]] +name = "gl_generator" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a95dfc23a2b4a9a2f5ab41d194f8bfda3cabec42af4e39f08c339eb2a0c124d" +dependencies = [ + "khronos_api", + "log", + "xml-rs", +] + +[[package]] +name = "gl_loader" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e32d96dd5f881490e537041d5532320812ba096097f07fccb4626578da0b99d3" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "khronos-egl" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6aae1df220ece3c0ada96b8153459b67eebe9ae9212258bb0134ae60416fdf76" +dependencies = [ + "libc", + "pkg-config", +] + +[[package]] +name = "khronos_api" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc" + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "libc" +version = "0.2.153" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" + +[[package]] +name = "libloading" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c2a198fb6b0eada2a8df47933734e6d35d350665a33a3593d7164fa52c75c19" +dependencies = [ + "cfg-if", + "windows-targets", +] + +[[package]] +name = "linux-raw-sys" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" + +[[package]] +name = "log" +version = "0.4.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" + +[[package]] +name = "memchr" +version = "2.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d" + +[[package]] +name = "nu-ansi-term" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +dependencies = [ + "overload", + "winapi", +] + +[[package]] +name = "once_cell" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" + +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + +[[package]] +name = "pin-project-lite" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" + +[[package]] +name = "pkg-config" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" + +[[package]] +name = "proc-macro2" +version = "1.0.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835ff2298f5721608eb1a980ecaee1aef2c132bf95ecc026a11b7bf3c01c02e" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quick-xml" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1004a344b30a54e2ee58d66a71b32d2db2feb0a31f9a2d302bf0536f15de2a33" +dependencies = [ + "memchr", +] + +[[package]] +name = "quote" +version = "1.0.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rustix" +version = "0.38.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65e04861e65f21776e67888bfbea442b3642beaa0138fdb1dd7a84a52dffdb89" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys", +] + +[[package]] +name = "scoped-tls" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "smallvec" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" + +[[package]] +name = "syn" +version = "2.0.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "002a1b3dbf967edfafc32655d0f377ab0bb7b994aa1d32c8cc7e9b8bf3ebb8f0" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "1.0.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03468839009160513471e86a034bb2c5c0e4baae3b43f79ffc55c4a5427b3297" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61f3ba182994efc43764a46c018c347bc492c79f024e705f46567b418f6d4f7" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thread_local" +version = "1.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" +dependencies = [ + "cfg-if", + "once_cell", +] + +[[package]] +name = "tracing" +version = "0.1.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b" +dependencies = [ + "nu-ansi-term", + "sharded-slab", + "smallvec", + "thread_local", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "valuable" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" + +[[package]] +name = "wayland-backend" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d50fa61ce90d76474c87f5fc002828d81b32677340112b4ef08079a9d459a40" +dependencies = [ + "cc", + "downcast-rs", + "rustix", + "scoped-tls", + "smallvec", + "wayland-sys", +] + +[[package]] +name = "wayland-client" +version = "0.31.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82fb96ee935c2cea6668ccb470fb7771f6215d1691746c2d896b447a00ad3f1f" +dependencies = [ + "bitflags", + "rustix", + "wayland-backend", + "wayland-scanner", +] + +[[package]] +name = "wayland-egl" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "355f652e5a24ae02d2ad536c8fc2d3dcc6c2bd635027cd6103a193e7d75eeda2" +dependencies = [ + "wayland-backend", + "wayland-sys", +] + +[[package]] +name = "wayland-egl-ctx" +version = "0.1.0" +dependencies = [ + "gl", + "gl_loader", + "khronos-egl", + "thiserror", + "tracing", + "tracing-subscriber", + "wayland-backend", + "wayland-client", + "wayland-egl", + "wayland-protocols", +] + +[[package]] +name = "wayland-protocols" +version = "0.31.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f81f365b8b4a97f422ac0e8737c438024b5951734506b0e1d775c73030561f4" +dependencies = [ + "bitflags", + "wayland-backend", + "wayland-client", + "wayland-scanner", +] + +[[package]] +name = "wayland-scanner" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b3a62929287001986fb58c789dce9b67604a397c15c611ad9f747300b6c283" +dependencies = [ + "proc-macro2", + "quick-xml", + "quote", +] + +[[package]] +name = "wayland-sys" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15a0c8eaff5216d07f226cb7a549159267f3467b289d9a2e52fd3ef5aae2b7af" +dependencies = [ + "dlib", + "log", + "pkg-config", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd37b7e5ab9018759f893a1952c9420d060016fc19a472b4bb20d1bdd694d1b" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bcf46cf4c365c6f2d1cc93ce535f2c8b244591df96ceee75d8e83deb70a9cac9" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da9f259dd3bcf6990b55bffd094c4f7235817ba4ceebde8e6d11cd0c5633b675" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b474d8268f99e0995f25b9f095bc7434632601028cf86590aea5c8a5cb7801d3" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1515e9a29e5bed743cb4415a9ecf5dfca648ce85ee42e15873c3cd8610ff8e02" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5eee091590e89cc02ad514ffe3ead9eb6b660aedca2183455434b93546371a03" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ca79f2451b49fa9e2af39f0747fe999fcda4f5e241b2898624dca97a1f2177" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32b752e52a2da0ddfbdbcc6fceadfeede4c939ed16d13e648833a61dfb611ed8" + +[[package]] +name = "xml-rs" +version = "0.8.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fcb9cbac069e033553e8bb871be2fbdffcab578eb25bd0f7c508cedc6dcd75a" diff --git a/libwayshot/examples/waymirror-egl/Cargo.toml b/libwayshot/examples/waymirror-egl/Cargo.toml new file mode 100644 index 00000000..eb6d9dc8 --- /dev/null +++ b/libwayshot/examples/waymirror-egl/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "waymirror-egl" +version = "0.1.0" +edition = "2021" + +[dependencies] +gl = "0.14.0" +gl_loader = "0.1.2" +khronos-egl = { version = "6.0.0",features = ["static"] } +thiserror = "1.0.58" +tracing = "0.1.40" +tracing-subscriber = "0.3.18" +wayland-backend = { version = "0.3.3", features = ["client_system"] } +wayland-client = { version = "0.31.2" } +wayland-egl = { version = "0.32.0" } +wayland-protocols = { version = "0.31.2", features = ["client"] } +libwayshot={path="../.."} +libloading="0.8.4" \ No newline at end of file diff --git a/libwayshot/examples/waymirror-egl/README.md b/libwayshot/examples/waymirror-egl/README.md new file mode 100644 index 00000000..69d4fe45 --- /dev/null +++ b/libwayshot/examples/waymirror-egl/README.md @@ -0,0 +1,8 @@ +# waymirror-egl +Example code for using the libwayshot DMA-BUF GL screencapture pipeline. + +This example sets up an EGL+OpenGL context, sets up libwayshot and renders the main display onto a rectangle after converting the screencapture into a texture. + +Adapted from https://github.com/Shinyzenith/wayland-egl-ctx, all credits to @Shinyzenith + +Run using `cargo run` inside this directory. diff --git a/libwayshot/examples/waymirror-egl/src/dispatch.rs b/libwayshot/examples/waymirror-egl/src/dispatch.rs new file mode 100644 index 00000000..2e3f38f8 --- /dev/null +++ b/libwayshot/examples/waymirror-egl/src/dispatch.rs @@ -0,0 +1,131 @@ +use crate::state::WaylandEGLState; +use wayland_client::{ + delegate_noop, + protocol::{wl_compositor, wl_registry, wl_surface}, + Connection, Dispatch, QueueHandle, +}; +use wayland_protocols::xdg::shell::client::{xdg_surface, xdg_toplevel, xdg_wm_base}; + +impl Dispatch for WaylandEGLState { + #[tracing::instrument(skip(registry, queue_handle, state), ret, level = "trace")] + fn event( + state: &mut Self, + registry: &wl_registry::WlRegistry, + event: wl_registry::Event, + _: &(), + _: &Connection, + queue_handle: &QueueHandle, + ) { + if let wl_registry::Event::Global { + name, + interface, + version, + } = event + { + match interface.as_str() { + "xdg_wm_base" => { + state.xdg_wm_base = Some(registry.bind::( + name, + version, + queue_handle, + (), + )); + } + "wl_compositor" => { + state.wl_compositor = Some(registry.bind::( + name, + version, + queue_handle, + (), + )); + } + _ => {} + } + } + } +} + +impl Dispatch for WaylandEGLState { + #[tracing::instrument(skip(xdg_wm_base), ret, level = "trace")] + fn event( + _: &mut Self, + xdg_wm_base: &xdg_wm_base::XdgWmBase, + event: xdg_wm_base::Event, + _: &(), + _: &Connection, + _: &QueueHandle, + ) { + if let xdg_wm_base::Event::Ping { serial } = event { + xdg_wm_base.pong(serial); + } + } +} + +impl Dispatch for WaylandEGLState { + #[tracing::instrument(skip(xdg_surface), ret, level = "trace")] + fn event( + _: &mut Self, + xdg_surface: &xdg_surface::XdgSurface, + event: xdg_surface::Event, + _: &(), + _: &Connection, + _: &QueueHandle, + ) { + if let xdg_surface::Event::Configure { serial } = event { + xdg_surface.ack_configure(serial); + } + } +} + +impl Dispatch for WaylandEGLState { + #[tracing::instrument(skip(), ret, level = "trace")] + fn event( + state: &mut Self, + _: &xdg_toplevel::XdgToplevel, + event: xdg_toplevel::Event, + _: &(), + _: &Connection, + _: &QueueHandle, + ) { + match event { + xdg_toplevel::Event::Configure { width, height, .. } => { + if width == 0 || height == 0 { + return; // We do not respect this configure + } + + if state.width != width || state.height != height { + state.width = width; + state.height = height; + + state + .egl_window + .clone() + .unwrap() + .resize(state.width, state.height, 0, 0); + + unsafe { + gl::Viewport(0, 0, state.width, state.height); + } + state.wl_surface.clone().unwrap().commit(); + } + } + xdg_toplevel::Event::Close {} => { + state.running = false; + } + _ => {} + } + } +} + +impl Dispatch for WaylandEGLState { + fn event( + _state: &mut Self, + _proxy: &wl_surface::WlSurface, + _event: ::Event, + _data: &(), + _conn: &Connection, + _qhandle: &QueueHandle, + ) { + } +} +delegate_noop!(WaylandEGLState: wl_compositor::WlCompositor); diff --git a/libwayshot/examples/waymirror-egl/src/error.rs b/libwayshot/examples/waymirror-egl/src/error.rs new file mode 100644 index 00000000..1860cc85 --- /dev/null +++ b/libwayshot/examples/waymirror-egl/src/error.rs @@ -0,0 +1,22 @@ +use std::result; +use thiserror::Error; + +pub type Result = result::Result; + +#[derive(Error, Debug)] +pub enum WaylandEGLStateError { + #[error("xdg_wm_base global missing")] + XdgWmBaseMissing, + + #[error("wl_compositor global missing")] + WlCompositorMissing, + + #[error("Shader compilation failed")] + GLShaderCompileFailed, + + #[error("Failed to create gl program")] + GLCreateProgramFailed, + + #[error("Failed to link gl program")] + GLLinkProgramFailed, +} diff --git a/libwayshot/examples/waymirror-egl/src/main.rs b/libwayshot/examples/waymirror-egl/src/main.rs new file mode 100644 index 00000000..969851a5 --- /dev/null +++ b/libwayshot/examples/waymirror-egl/src/main.rs @@ -0,0 +1,64 @@ +mod dispatch; +mod error; +mod state; +mod utils; + +use error::Result; +use state::WaylandEGLState; + +pub fn main() -> Result<(), Box> { + tracing_subscriber::fmt() + .with_max_level(tracing::Level::DEBUG) + .with_writer(std::io::stderr) + .init(); + + let mut state = WaylandEGLState::new()?; + let mut event_queue = state.wl_connection.new_event_queue(); + + let queue_handle = event_queue.handle(); + let _registry = state.wl_display.get_registry(&queue_handle, ()); + + event_queue.roundtrip(&mut state)?; + state.validate_globals()?; + + state.wl_surface = Some( + state + .wl_compositor + .as_ref() + .unwrap() + .create_surface(&queue_handle, ()), + ); + + state.xdg_surface = Some(state.xdg_wm_base.clone().unwrap().get_xdg_surface( + &state.wl_surface.clone().unwrap(), + &queue_handle, + (), + )); + state.xdg_toplevel = Some( + state + .xdg_surface + .clone() + .unwrap() + .get_toplevel(&queue_handle, ()), + ); + state + .xdg_toplevel + .clone() + .unwrap() + .set_title(state.title.clone()); + state.wl_surface.clone().unwrap().commit(); + + state.init_egl()?; + while state.running { + event_queue.dispatch_pending(&mut state)?; + state.draw(); + state + .egl + .swap_buffers(state.egl_display.unwrap(), state.egl_surface.unwrap())?; + + tracing::trace!("eglSwapBuffers called"); + } + state.deinit()?; + + Ok(()) +} diff --git a/libwayshot/examples/waymirror-egl/src/shaders/frag.glsl b/libwayshot/examples/waymirror-egl/src/shaders/frag.glsl new file mode 100644 index 00000000..8212a377 --- /dev/null +++ b/libwayshot/examples/waymirror-egl/src/shaders/frag.glsl @@ -0,0 +1,11 @@ +#version 300 es + +precision mediump float; +out vec4 FragColor; +uniform sampler2D uTexture; +in vec2 vTexCoord; + +void main() { + vec4 color = texture(uTexture, vTexCoord); + FragColor = vec4 ( 1.0-color.r,1.0-color.g,1.0-color.b, 1.0 ); +} diff --git a/libwayshot/examples/waymirror-egl/src/shaders/vert.glsl b/libwayshot/examples/waymirror-egl/src/shaders/vert.glsl new file mode 100644 index 00000000..9b27c568 --- /dev/null +++ b/libwayshot/examples/waymirror-egl/src/shaders/vert.glsl @@ -0,0 +1,11 @@ +#version 300 es +layout (location = 0) in vec3 aPos; +layout (location = 1) in vec2 aTexCoord; + +out vec2 vTexCoord; + +void main() +{ + gl_Position = vec4(aPos, 1.0); + vTexCoord = vec2(aTexCoord.x, aTexCoord.y); +} \ No newline at end of file diff --git a/libwayshot/examples/waymirror-egl/src/state.rs b/libwayshot/examples/waymirror-egl/src/state.rs new file mode 100644 index 00000000..6b4a8022 --- /dev/null +++ b/libwayshot/examples/waymirror-egl/src/state.rs @@ -0,0 +1,303 @@ +use crate::error::{Result, WaylandEGLStateError}; +use crate::utils::load_shader; + +use libwayshot::WayshotConnection; + +use gl::types::GLuint; +use khronos_egl::{self as egl}; +use std::{ffi::c_void, rc::Rc}; +use wayland_client::{ + protocol::{wl_compositor, wl_display::WlDisplay, wl_surface::WlSurface}, + ConnectError, Connection, Proxy, +}; +use wayland_egl::WlEglSurface; +use wayland_protocols::xdg::shell::client::{xdg_surface, xdg_toplevel, xdg_wm_base}; + +#[derive(Debug)] +pub struct WaylandEGLState { + pub width: i32, + pub height: i32, + pub running: bool, + pub title: String, + + pub wl_connection: Connection, + pub wl_display: WlDisplay, + pub wl_surface: Option, + + pub egl: egl::Instance, + pub egl_window: Option>, + pub egl_display: Option, + pub egl_surface: Option, + pub egl_context: Option, + + pub gl_program: GLuint, + pub gl_texture: GLuint, + + pub xdg_wm_base: Option, + pub xdg_surface: Option, + pub xdg_toplevel: Option, + pub wl_compositor: Option, + + wayshot: WayshotConnection, +} + +impl WaylandEGLState { + #[tracing::instrument] + pub fn new() -> Result { + let server_connection = Connection::connect_to_env()?; + + Ok(Self { + width: 1920, + height: 1080, + running: true, + title: "Waymirror-EGL".into(), + + wl_connection: server_connection.clone(), + wl_display: server_connection.display(), + wl_surface: None, + + egl: khronos_egl::Instance::new(egl::Static), + egl_window: None, + egl_display: None, + egl_surface: None, + egl_context: None, + gl_program: 0, + gl_texture: 0, + + xdg_wm_base: None, + xdg_surface: None, + xdg_toplevel: None, + wl_compositor: None, + wayshot: WayshotConnection::from_connection_with_dmabuf( + server_connection, + "/dev/dri/renderD128", + ) + .unwrap(), + }) + } + + pub fn deinit(&self) -> Result<(), Box> { + unsafe { + gl::DeleteProgram(self.gl_program); + } + + self.egl + .destroy_surface(self.egl_display.unwrap(), self.egl_surface.unwrap())?; + self.egl + .destroy_context(self.egl_display.unwrap(), self.egl_context.unwrap())?; + + self.xdg_surface.clone().unwrap().destroy(); + self.wl_surface.clone().unwrap().destroy(); + + Ok(()) + } + + pub fn init_egl(&mut self) -> Result<(), Box> { + // Init gl + gl_loader::init_gl(); + gl::load_with(|s| gl_loader::get_proc_address(s) as *const _); + + self.egl_window = Some(Rc::new(WlEglSurface::new( + self.wl_surface.clone().unwrap().id(), + self.width, + self.height, + )?)); + + self.egl_display = Some( + unsafe { + self.egl + .get_display(self.wl_display.id().as_ptr() as *mut c_void) + } + .unwrap(), + ); + + self.egl.initialize(self.egl_display.unwrap())?; + + let attributes = [ + egl::SURFACE_TYPE, + egl::WINDOW_BIT, + egl::RENDERABLE_TYPE, + egl::OPENGL_ES2_BIT, + egl::RED_SIZE, + 8, + egl::GREEN_SIZE, + 8, + egl::BLUE_SIZE, + 8, + egl::NONE, + ]; + + let config = self + .egl + .choose_first_config(self.egl_display.unwrap(), &attributes)? + .expect("unable to find an appropriate EGL configuration"); + self.egl_surface = Some(unsafe { + self.egl.create_window_surface( + self.egl_display.unwrap(), + config, + self.egl_window.clone().unwrap().ptr() as egl::NativeWindowType, + None, + )? + }); + + let context_attributes = [egl::CONTEXT_CLIENT_VERSION, 2, egl::NONE]; + self.egl_context = Some(self.egl.create_context( + self.egl_display.unwrap(), + config, + None, + &context_attributes, + )?); + + self.egl.make_current( + self.egl_display.unwrap(), + self.egl_surface, + self.egl_surface, + self.egl_context, + )?; + + self.init_program()?; + + Ok(()) + } + + fn init_program(&mut self) -> Result<()> { + let vert_shader = load_shader( + gl::VERTEX_SHADER, + include_str!("./shaders/vert.glsl").into(), + ) + .unwrap(); + + let frag_shader = load_shader( + gl::FRAGMENT_SHADER, + include_str!("./shaders/frag.glsl").into(), + ) + .unwrap(); + + unsafe { + self.gl_program = gl::CreateProgram(); + } + + if self.gl_program == 0 { + tracing::event!(tracing::Level::ERROR, "glCreateProgramFailed!"); + return Err(WaylandEGLStateError::GLCreateProgramFailed); + } + + unsafe { + gl::AttachShader(self.gl_program, vert_shader); + gl::AttachShader(self.gl_program, frag_shader); + + gl::LinkProgram(self.gl_program); + } + + let mut linked: gl::types::GLint = 1; + unsafe { gl::GetProgramiv(self.gl_program, gl::LINK_STATUS, &mut linked as *mut i32) } + + if linked > 0 { + tracing::event!(tracing::Level::INFO, "Successfully linked the program!"); + } else { + return Err(WaylandEGLStateError::GLLinkProgramFailed); + } + + let vertices: [gl::types::GLfloat; 20] = [ + // positions // texture coords + 1.0, 1.0, 0.0, 1.0, 0.0, // top right + 1.0, -1.0, 0.0, 1.0, 1.0, // bottom right + -1.0, -1.0, 0.0, 0.0, 1.0, // bottom left + -1.0, 1.0, 0.0, 0.0, 0.0, // top left + ]; + let indices: [gl::types::GLint; 6] = [ + 0, 1, 3, // first Triangle + 1, 2, 3, // second Triangle + ]; + let mut vbo: GLuint = 0; + let mut vao: GLuint = 0; + let mut ebo: GLuint = 0; + + unsafe { + gl::GenTextures(1, &mut self.gl_texture); + + self.dmabuf_to_texture(); + + gl::GenVertexArrays(1, &mut vao as *mut u32); + gl::GenBuffers(1, &mut vbo as *mut u32); + gl::GenBuffers(1, &mut ebo as *mut u32); + gl::BindVertexArray(vao); + + gl::BindBuffer(gl::ARRAY_BUFFER, vbo); + gl::BufferData( + gl::ARRAY_BUFFER, + (vertices.len() * std::mem::size_of::()) + as gl::types::GLsizeiptr, + &vertices[0] as *const f32 as *const c_void, + gl::STATIC_DRAW, + ); + + gl::BindBuffer(gl::ELEMENT_ARRAY_BUFFER, ebo); + gl::BufferData( + gl::ELEMENT_ARRAY_BUFFER, + (indices.len() * std::mem::size_of::()) + as gl::types::GLsizeiptr, + &indices[0] as *const i32 as *const c_void, + gl::STATIC_DRAW, + ); + + gl::VertexAttribPointer( + 0, + 3, + gl::FLOAT, + gl::FALSE, + 5 * std::mem::size_of::() as gl::types::GLint, + 0 as *const c_void, + ); + gl::EnableVertexAttribArray(0); + + gl::VertexAttribPointer( + 1, + 2, + gl::FLOAT, + gl::FALSE, + 5 * std::mem::size_of::() as gl::types::GLint, + (3 * std::mem::size_of::()) as *const c_void, + ); + gl::EnableVertexAttribArray(1); + gl::BindBuffer(gl::ARRAY_BUFFER, 0); + } + Ok(()) + } + + pub fn draw(&mut self) { + unsafe { + gl::ClearColor(1.0, 1.0, 0.0, 1.0); + gl::Clear(gl::COLOR_BUFFER_BIT); + // gl::DeleteTextures(1, &mut self.gl_texture); + + gl::UseProgram(self.gl_program); + gl::DrawElements(gl::TRIANGLES, 6, gl::UNSIGNED_INT, 0 as *const c_void); + } + } + + pub fn dmabuf_to_texture(&self) { + unsafe { + gl::BindTexture(gl::TEXTURE_2D, self.gl_texture); + gl::TexParameteri(gl::TEXTURE_2D, gl::TEXTURE_MIN_FILTER, gl::LINEAR as i32); + + self.wayshot + .bind_output_frame_to_gl_texture( + true, + &self.wayshot.get_all_outputs()[0].wl_output, + None, + ) + .unwrap(); + } + } + + pub fn validate_globals(&self) -> Result<()> { + if self.xdg_wm_base.is_none() { + return Err(WaylandEGLStateError::XdgWmBaseMissing); + } else if self.wl_compositor.is_none() { + return Err(WaylandEGLStateError::WlCompositorMissing); + } + + Ok(()) + } +} diff --git a/libwayshot/examples/waymirror-egl/src/utils.rs b/libwayshot/examples/waymirror-egl/src/utils.rs new file mode 100644 index 00000000..e532e2d3 --- /dev/null +++ b/libwayshot/examples/waymirror-egl/src/utils.rs @@ -0,0 +1,31 @@ +use crate::error::{Result, WaylandEGLStateError}; +use gl::types::{GLenum, GLint, GLuint}; +use std::{ffi::CString, ptr}; + +pub fn load_shader(shader_type: GLenum, src: String) -> Result { + unsafe { + let shader: GLuint = gl::CreateShader(shader_type); + + if shader == 0 { + return Err(WaylandEGLStateError::GLShaderCompileFailed); + } + + let src_c_str = CString::new(src.as_bytes()).unwrap(); + gl::ShaderSource(shader, 1, &src_c_str.as_ptr(), ptr::null()); + + //gl::ShaderSource(shader, 1, &src_c_str.as_ptr(), std::ptr::null()); + + gl::CompileShader(shader); + + let mut status: GLint = 1; + gl::GetShaderiv(shader, gl::COMPILE_STATUS, &mut status as *mut i32); + + if status > 0 { + tracing::event!(tracing::Level::INFO, "Shader compile successfull!",); + } else { + return Err(WaylandEGLStateError::GLShaderCompileFailed); + } + + Ok(shader) + } +} diff --git a/libwayshot/examples/waymirror.rs b/libwayshot/examples/waymirror.rs new file mode 100644 index 00000000..c350131a --- /dev/null +++ b/libwayshot/examples/waymirror.rs @@ -0,0 +1,205 @@ +use libwayshot::WayshotConnection; +use wayland_client::{ + delegate_noop, + protocol::{ + wl_buffer::{self}, + wl_compositor, wl_keyboard, wl_registry, wl_seat, wl_shm, wl_shm_pool, wl_surface, + }, + Connection, Dispatch, QueueHandle, WEnum, +}; + +use wayland_protocols::xdg::shell::client::{xdg_surface, xdg_toplevel, xdg_wm_base}; + +fn main() { + let conn = Connection::connect_to_env().unwrap(); + + let mut event_queue = conn.new_event_queue(); + let qhandle = event_queue.handle(); + + let display = conn.display(); + display.get_registry(&qhandle, ()); + let wayshot = + WayshotConnection::from_connection_with_dmabuf(conn, "/dev/dri/renderD128").unwrap(); + + let mut state = State { + wayshot, + running: true, + base_surface: None, + wm_base: None, + xdg_surface: None, + configured: false, + }; + + println!("Starting the example wayshot dmabuf demo app, press to quit."); + + while state.running { + event_queue.blocking_dispatch(&mut state).unwrap(); + } +} + +struct State { + wayshot: WayshotConnection, + running: bool, + base_surface: Option, + wm_base: Option, + xdg_surface: Option<(xdg_surface::XdgSurface, xdg_toplevel::XdgToplevel)>, + configured: bool, +} + +impl Dispatch for State { + fn event( + state: &mut Self, + registry: &wl_registry::WlRegistry, + event: wl_registry::Event, + _: &(), + _: &Connection, + qh: &QueueHandle, + ) { + if let wl_registry::Event::Global { + name, interface, .. + } = event + { + match &interface[..] { + "wl_compositor" => { + let compositor = + registry.bind::(name, 1, qh, ()); + let surface = compositor.create_surface(qh, ()); + state.base_surface = Some(surface); + + if state.wm_base.is_some() && state.xdg_surface.is_none() { + state.init_xdg_surface(qh); + } + } + "wl_seat" => { + registry.bind::(name, 1, qh, ()); + } + "xdg_wm_base" => { + let wm_base = registry.bind::(name, 1, qh, ()); + state.wm_base = Some(wm_base); + + if state.base_surface.is_some() && state.xdg_surface.is_none() { + state.init_xdg_surface(qh); + } + } + _ => {} + } + } + } +} + +// Ignore events from these object types in this example. +delegate_noop!(State: ignore wl_compositor::WlCompositor); +delegate_noop!(State: ignore wl_surface::WlSurface); +delegate_noop!(State: ignore wl_shm::WlShm); +delegate_noop!(State: ignore wl_shm_pool::WlShmPool); +delegate_noop!(State: ignore wl_buffer::WlBuffer); + +impl State { + fn init_xdg_surface(&mut self, qh: &QueueHandle) { + let wm_base = self.wm_base.as_ref().unwrap(); + let base_surface = self.base_surface.as_ref().unwrap(); + + let xdg_surface = wm_base.get_xdg_surface(base_surface, qh, ()); + let toplevel = xdg_surface.get_toplevel(qh, ()); + toplevel.set_title("DMABuf+wlr-screencpy example!".into()); + + base_surface.commit(); + + self.xdg_surface = Some((xdg_surface, toplevel)); + } +} + +impl Dispatch for State { + fn event( + _: &mut Self, + wm_base: &xdg_wm_base::XdgWmBase, + event: xdg_wm_base::Event, + _: &(), + _: &Connection, + _: &QueueHandle, + ) { + if let xdg_wm_base::Event::Ping { serial } = event { + wm_base.pong(serial); + } + } +} + +impl Dispatch for State { + fn event( + state: &mut Self, + xdg_surface: &xdg_surface::XdgSurface, + event: xdg_surface::Event, + _: &(), + _: &Connection, + _: &QueueHandle, + ) { + if let xdg_surface::Event::Configure { serial, .. } = event { + xdg_surface.ack_configure(serial); + state.configured = true; + let surface = state.base_surface.as_ref().unwrap(); + let (_frame_format, guard, _bo) = state + .wayshot + .capture_output_frame_dmabuf( + true, + &state.wayshot.get_all_outputs()[0].wl_output, + None, + ) + .unwrap(); + surface.attach(Some(&guard.buffer), 0, 0); + surface.commit(); + } + } +} + +impl Dispatch for State { + fn event( + state: &mut Self, + _: &xdg_toplevel::XdgToplevel, + event: xdg_toplevel::Event, + _: &(), + _: &Connection, + _: &QueueHandle, + ) { + if let xdg_toplevel::Event::Close {} = event { + state.running = false; + } + } +} + +impl Dispatch for State { + fn event( + _: &mut Self, + seat: &wl_seat::WlSeat, + event: wl_seat::Event, + _: &(), + _: &Connection, + qh: &QueueHandle, + ) { + if let wl_seat::Event::Capabilities { + capabilities: WEnum::Value(capabilities), + } = event + { + if capabilities.contains(wl_seat::Capability::Keyboard) { + seat.get_keyboard(qh, ()); + } + } + } +} + +impl Dispatch for State { + fn event( + state: &mut Self, + _: &wl_keyboard::WlKeyboard, + event: wl_keyboard::Event, + _: &(), + _: &Connection, + _: &QueueHandle, + ) { + if let wl_keyboard::Event::Key { key, .. } = event { + if key == 1 { + // ESC key + state.running = false; + } + } + } +} diff --git a/libwayshot/src/dispatch.rs b/libwayshot/src/dispatch.rs index 9b760c21..9c35fbc8 100644 --- a/libwayshot/src/dispatch.rs +++ b/libwayshot/src/dispatch.rs @@ -1,35 +1,59 @@ use std::{ - process::exit, + collections::HashSet, + os::fd::{AsFd, BorrowedFd}, sync::atomic::{AtomicBool, Ordering}, }; use wayland_client::{ delegate_noop, globals::GlobalListContents, protocol::{ - wl_buffer::WlBuffer, wl_output, wl_output::WlOutput, wl_registry, wl_registry::WlRegistry, - wl_shm::WlShm, wl_shm_pool::WlShmPool, + wl_buffer::WlBuffer, + wl_compositor::WlCompositor, + wl_output::{self, WlOutput}, + wl_registry::{self, WlRegistry}, + wl_shm::WlShm, + wl_shm_pool::WlShmPool, + wl_surface::WlSurface, }, - Connection, Dispatch, QueueHandle, WEnum, - WEnum::Value, + Connection, Dispatch, QueueHandle, + WEnum::{self, Value}, }; -use wayland_protocols::xdg::xdg_output::zv1::client::{ - zxdg_output_manager_v1::ZxdgOutputManagerV1, zxdg_output_v1, zxdg_output_v1::ZxdgOutputV1, +use wayland_protocols::{ + wp::linux_dmabuf::zv1::client::{ + zwp_linux_buffer_params_v1::{self, ZwpLinuxBufferParamsV1}, + zwp_linux_dmabuf_v1::{self, ZwpLinuxDmabufV1}, + }, + xdg::xdg_output::zv1::client::{ + zxdg_output_manager_v1::ZxdgOutputManagerV1, + zxdg_output_v1::{self, ZxdgOutputV1}, + }, +}; +use wayland_protocols_wlr::layer_shell::v1::client::{ + zwlr_layer_shell_v1::ZwlrLayerShellV1, + zwlr_layer_surface_v1::{self, ZwlrLayerSurfaceV1}, }; use wayland_protocols_wlr::screencopy::v1::client::{ zwlr_screencopy_frame_v1, zwlr_screencopy_frame_v1::ZwlrScreencopyFrameV1, zwlr_screencopy_manager_v1::ZwlrScreencopyManagerV1, }; +use wayland_protocols::wp::viewporter::client::{ + wp_viewport::WpViewport, wp_viewporter::WpViewporter, +}; + use crate::{ - output::{OutputInfo, OutputPositioning, WlOutputMode}, - screencopy::FrameFormat, + output::OutputInfo, + region::{LogicalRegion, Position, Size}, + screencopy::{DMAFrameFormat, FrameFormat}, }; +#[derive(Debug)] pub struct OutputCaptureState { pub outputs: Vec, } impl Dispatch for OutputCaptureState { + #[tracing::instrument(skip(wl_registry, qh), ret, level = "trace")] fn event( state: &mut Self, wl_registry: &WlRegistry, @@ -56,16 +80,8 @@ impl Dispatch for OutputCaptureState { name: "".to_string(), description: String::new(), transform: wl_output::Transform::Normal, - dimensions: OutputPositioning { - x: 0, - y: 0, - width: 0, - height: 0, - }, - mode: WlOutputMode { - width: 0, - height: 0, - }, + physical_size: Size::default(), + logical_region: LogicalRegion::default(), }); } else { tracing::error!("Ignoring a wl_output with version < 4."); @@ -76,6 +92,7 @@ impl Dispatch for OutputCaptureState { } impl Dispatch for OutputCaptureState { + #[tracing::instrument(skip(wl_output), ret, level = "trace")] fn event( state: &mut Self, wl_output: &WlOutput, @@ -84,11 +101,13 @@ impl Dispatch for OutputCaptureState { _: &Connection, _: &QueueHandle, ) { - let output: &mut OutputInfo = state - .outputs - .iter_mut() - .find(|x| x.wl_output == *wl_output) - .unwrap(); + let output: &mut OutputInfo = + if let Some(output) = state.outputs.iter_mut().find(|x| x.wl_output == *wl_output) { + output + } else { + tracing::error!("Received event for an output that is not registered: {event:#?}"); + return; + }; match event { wl_output::Event::Name { name } => { @@ -98,7 +117,10 @@ impl Dispatch for OutputCaptureState { output.description = description; } wl_output::Event::Mode { width, height, .. } => { - output.mode = WlOutputMode { width, height }; + output.physical_size = Size { + width: width as u32, + height: height as u32, + }; } wl_output::Event::Geometry { transform: WEnum::Value(transform), @@ -106,7 +128,9 @@ impl Dispatch for OutputCaptureState { } => { output.transform = transform; } - _ => (), + wl_output::Event::Scale { .. } => {} + wl_output::Event::Done => {} + _ => {} } } } @@ -114,6 +138,7 @@ impl Dispatch for OutputCaptureState { delegate_noop!(OutputCaptureState: ignore ZxdgOutputManagerV1); impl Dispatch for OutputCaptureState { + #[tracing::instrument(ret, level = "trace")] fn event( state: &mut Self, _: &ZxdgOutputV1, @@ -122,25 +147,34 @@ impl Dispatch for OutputCaptureState { _: &Connection, _: &QueueHandle, ) { - let output_info = state.outputs.get_mut(*index).unwrap(); + let output_info = if let Some(output_info) = state.outputs.get_mut(*index) { + output_info + } else { + tracing::error!( + "Received event for output index {index} that is not registered: {event:#?}" + ); + return; + }; match event { zxdg_output_v1::Event::LogicalPosition { x, y } => { - output_info.dimensions.x = x; - output_info.dimensions.y = y; - tracing::debug!("Logical position event fired!"); + output_info.logical_region.inner.position = Position { x, y }; } zxdg_output_v1::Event::LogicalSize { width, height } => { - output_info.dimensions.width = width; - output_info.dimensions.height = height; - tracing::debug!("Logical size event fired!"); + output_info.logical_region.inner.size = Size { + width: width as u32, + height: height as u32, + }; } + zxdg_output_v1::Event::Done => {} + zxdg_output_v1::Event::Name { .. } => {} + zxdg_output_v1::Event::Description { .. } => {} _ => {} }; } } -/// State of the frame after attemting to copy it's data to a wl_buffer. +/// State of the frame after attempting to copy it's data to a wl_buffer. #[derive(Debug, Copy, Clone, PartialEq)] pub enum FrameState { /// Compositor returned a failed event on calling `frame.copy`. @@ -151,11 +185,37 @@ pub enum FrameState { pub struct CaptureFrameState { pub formats: Vec, + pub dmabuf_formats: Vec, pub state: Option, pub buffer_done: AtomicBool, } +impl Dispatch for CaptureFrameState { + fn event( + _frame: &mut Self, + _proxy: &ZwpLinuxDmabufV1, + _event: zwp_linux_dmabuf_v1::Event, + _data: &(), + _conn: &Connection, + _qhandle: &wayland_client::QueueHandle, + ) { + } +} + +impl Dispatch for CaptureFrameState { + fn event( + _state: &mut Self, + _proxy: &ZwpLinuxBufferParamsV1, + _event: zwp_linux_buffer_params_v1::Event, + _data: &(), + _conn: &Connection, + _qhandle: &QueueHandle, + ) { + } +} + impl Dispatch for CaptureFrameState { + #[tracing::instrument(skip(frame), ret, level = "trace")] fn event( frame: &mut Self, _: &ZwlrScreencopyFrameV1, @@ -171,43 +231,43 @@ impl Dispatch for CaptureFrameState { height, stride, } => { - tracing::debug!("Received Buffer event"); if let Value(f) = format { + tracing::debug!("Received Buffer event with format: {f:?}"); frame.formats.push(FrameFormat { format: f, - width, - height, + size: Size { width, height }, stride, }) } else { tracing::debug!("Received Buffer event with unidentified format"); - exit(1); } } - zwlr_screencopy_frame_v1::Event::Flags { .. } => { - tracing::debug!("Received Flags event"); - } zwlr_screencopy_frame_v1::Event::Ready { .. } => { // If the frame is successfully copied, a “flags” and a “ready” events are sent. Otherwise, a “failed” event is sent. // This is useful when we call .copy on the frame object. - tracing::debug!("Received Ready event"); frame.state.replace(FrameState::Finished); } zwlr_screencopy_frame_v1::Event::Failed => { - tracing::debug!("Received Failed event"); frame.state.replace(FrameState::Failed); } - zwlr_screencopy_frame_v1::Event::Damage { .. } => { - tracing::debug!("Received Damage event"); - } - zwlr_screencopy_frame_v1::Event::LinuxDmabuf { .. } => { - tracing::debug!("Received LinuxDmaBuf event"); + zwlr_screencopy_frame_v1::Event::Damage { .. } => {} + zwlr_screencopy_frame_v1::Event::LinuxDmabuf { + format, + width, + height, + } => { + tracing::debug!( + "Received wlr-screencopy linux_dmabuf event with format: {format} and size {width}x{height}" + ); + frame.dmabuf_formats.push(DMAFrameFormat { + format, + size: Size { width, height }, + }); } zwlr_screencopy_frame_v1::Event::BufferDone => { - tracing::debug!("Received bufferdone event"); frame.buffer_done.store(true, Ordering::SeqCst); } - _ => unreachable!(), + _ => {} }; } } @@ -220,7 +280,7 @@ delegate_noop!(CaptureFrameState: ignore ZwlrScreencopyManagerV1); // TODO: Create a xdg-shell surface, check for the enter event, grab the output from it. pub struct WayshotState {} - +delegate_noop!(WayshotState: ignore ZwpLinuxDmabufV1); impl wayland_client::Dispatch for WayshotState { fn event( _: &mut WayshotState, @@ -232,3 +292,70 @@ impl wayland_client::Dispatch for W ) { } } + +pub struct LayerShellState { + pub configured_outputs: HashSet, +} + +delegate_noop!(LayerShellState: ignore WlCompositor); +delegate_noop!(LayerShellState: ignore WlShm); +delegate_noop!(LayerShellState: ignore WlShmPool); +delegate_noop!(LayerShellState: ignore WlBuffer); +delegate_noop!(LayerShellState: ignore ZwlrLayerShellV1); +delegate_noop!(LayerShellState: ignore WlSurface); +delegate_noop!(LayerShellState: ignore WpViewport); +delegate_noop!(LayerShellState: ignore WpViewporter); + +impl wayland_client::Dispatch for LayerShellState { + // No need to instrument here, span from lib.rs is automatically used. + fn event( + state: &mut Self, + proxy: &ZwlrLayerSurfaceV1, + event: ::Event, + data: &WlOutput, + _conn: &Connection, + _qhandle: &QueueHandle, + ) { + match event { + zwlr_layer_surface_v1::Event::Configure { + serial, + width: _, + height: _, + } => { + tracing::debug!("Acking configure"); + state.configured_outputs.insert(data.clone()); + proxy.ack_configure(serial); + tracing::trace!("Acked configure"); + } + zwlr_layer_surface_v1::Event::Closed => { + tracing::debug!("Closed") + } + _ => {} + } + } +} +pub(crate) struct Card(std::fs::File); + +/// Implementing [`AsFd`] is a prerequisite to implementing the traits found +/// in this crate. Here, we are just calling [`File::as_fd()`] on the inner +/// [`File`]. +impl AsFd for Card { + fn as_fd(&self) -> BorrowedFd<'_> { + self.0.as_fd() + } +} +impl drm::Device for Card {} +/// Simple helper methods for opening a `Card`. +impl Card { + pub fn open(path: &str) -> Self { + let mut options = std::fs::OpenOptions::new(); + options.read(true); + options.write(true); + Card(options.open(path).unwrap()) + } +} +#[derive(Debug)] +pub(crate) struct DMABUFState { + pub linux_dmabuf: ZwpLinuxDmabufV1, + pub gbmdev: gbm::Device, +} diff --git a/libwayshot/src/error.rs b/libwayshot/src/error.rs index ddeff581..10d87b4f 100644 --- a/libwayshot/src/error.rs +++ b/libwayshot/src/error.rs @@ -1,7 +1,12 @@ use std::{io, result}; +use drm::buffer::UnrecognizedFourcc; +use gbm::{DeviceDestroyedError, FdError}; use thiserror::Error; -use wayland_client::{globals::GlobalError, ConnectError, DispatchError}; +use wayland_client::{ + globals::{BindError, GlobalError}, + ConnectError, DispatchError, +}; pub type Result = result::Result; @@ -17,6 +22,8 @@ pub enum Error { Io(#[from] io::Error), #[error("dispatch error: {0}")] Dispatch(#[from] DispatchError), + #[error("bind error: {0}")] + Bind(#[from] BindError), #[error("global error: {0}")] Global(#[from] GlobalError), #[error("connect error: {0}")] @@ -27,4 +34,18 @@ pub enum Error { NoSupportedBufferFormat, #[error("Cannot find required wayland protocol")] ProtocolNotFound(String), + #[error("error occurred in freeze callback")] + FreezeCallbackError, + #[error("dmabuf configuration not initialized. Did you not use Wayshot::from_connection_with_dmabuf()?")] + NoDMAStateError, + #[error("dmabuf color format provided by compositor is invalid")] + UnrecognizedColorCode(#[from] UnrecognizedFourcc), + #[error("dmabuf device has been destroyed")] + DRMDeviceLost(#[from] DeviceDestroyedError), + #[error("obtaining gbm buffer object file descriptor failed {0}")] + GBMBoFdError(#[from] FdError), + #[error(" EGLImage import from dmabuf failed: {0}")] + EGLError(#[from] khronos_egl::Error), + #[error("No EGLImageTargetTexture2DOES function located, this extension may not be supported")] + EGLImageToTexProcNotFoundError, } diff --git a/libwayshot/src/image_util.rs b/libwayshot/src/image_util.rs index 92eb3967..fbb7068f 100644 --- a/libwayshot/src/image_util.rs +++ b/libwayshot/src/image_util.rs @@ -1,13 +1,24 @@ -use image::{DynamicImage, GenericImageView}; +use image::DynamicImage; use wayland_client::protocol::wl_output::Transform; +use crate::region::Size; + +#[tracing::instrument(skip(image))] pub(crate) fn rotate_image_buffer( image: DynamicImage, transform: Transform, - width: u32, - height: u32, + logical_size: Size, + max_scale: f64, ) -> DynamicImage { - let final_image = match transform { + // TODO Better document whether width and height are before or after the transform. + // Perhaps this should be part of a cleanup of the FrameCopy struct. + let (logical_width, _logical_height) = match transform { + Transform::_90 | Transform::_270 | Transform::Flipped90 | Transform::Flipped270 => { + (logical_size.height, logical_size.width) + } + _ => (logical_size.width, logical_size.height), + }; + let rotated_image = match transform { Transform::_90 => image::imageops::rotate90(&image).into(), Transform::_180 => image::imageops::rotate180(&image).into(), Transform::_270 => image::imageops::rotate270(&image).into(), @@ -27,14 +38,22 @@ pub(crate) fn rotate_image_buffer( _ => image, }; - if final_image.dimensions() == (width, height) { - return final_image; + let scale = rotated_image.width() as f64 / logical_width as f64; + // The amount of scaling left to perform. + let scaling_left = max_scale / scale; + if scaling_left <= 1.0 { + tracing::debug!("No scaling left to do"); + return rotated_image; } + tracing::debug!("Scaling left to do: {scaling_left}"); + let new_width = (rotated_image.width() as f64 * scaling_left).round() as u32; + let new_height = (rotated_image.height() as f64 * scaling_left).round() as u32; + tracing::debug!("Resizing image to {new_width}x{new_height}"); image::imageops::resize( - &final_image, - width, - height, + &rotated_image, + new_width, + new_height, image::imageops::FilterType::Gaussian, ) .into() diff --git a/libwayshot/src/lib.rs b/libwayshot/src/lib.rs index 6c003320..50f2f25c 100644 --- a/libwayshot/src/lib.rs +++ b/libwayshot/src/lib.rs @@ -8,39 +8,61 @@ mod dispatch; mod error; mod image_util; pub mod output; +pub mod region; mod screencopy; use std::{ - cmp, + collections::HashSet, + ffi::c_void, fs::File, - os::fd::AsFd, - process::exit, + os::fd::{AsFd, IntoRawFd, OwnedFd}, sync::atomic::{AtomicBool, Ordering}, thread, }; -use image::{imageops::overlay, DynamicImage}; +use dispatch::{DMABUFState, LayerShellState}; +use image::{imageops::replace, DynamicImage}; +use khronos_egl::{self as egl, Instance}; use memmap2::MmapMut; +use region::{EmbeddedRegion, RegionCapturer}; +use screencopy::{DMAFrameFormat, DMAFrameGuard, EGLImageGuard, FrameData, FrameGuard}; +use tracing::debug; use wayland_client::{ globals::{registry_queue_init, GlobalList}, protocol::{ + wl_compositor::WlCompositor, wl_output::{Transform, WlOutput}, wl_shm::{self, WlShm}, }, - Connection, EventQueue, + Connection, EventQueue, Proxy, }; -use wayland_protocols::xdg::xdg_output::zv1::client::{ - zxdg_output_manager_v1::ZxdgOutputManagerV1, zxdg_output_v1::ZxdgOutputV1, +use wayland_protocols::{ + wp::{ + linux_dmabuf::zv1::client::{ + zwp_linux_buffer_params_v1, zwp_linux_dmabuf_v1::ZwpLinuxDmabufV1, + }, + viewporter::client::wp_viewporter::WpViewporter, + }, + xdg::xdg_output::zv1::client::{ + zxdg_output_manager_v1::ZxdgOutputManagerV1, zxdg_output_v1::ZxdgOutputV1, + }, }; -use wayland_protocols_wlr::screencopy::v1::client::{ - zwlr_screencopy_frame_v1::ZwlrScreencopyFrameV1, - zwlr_screencopy_manager_v1::ZwlrScreencopyManagerV1, +use wayland_protocols_wlr::{ + layer_shell::v1::client::{ + zwlr_layer_shell_v1::{Layer, ZwlrLayerShellV1}, + zwlr_layer_surface_v1::Anchor, + }, + screencopy::v1::client::{ + zwlr_screencopy_frame_v1::ZwlrScreencopyFrameV1, + zwlr_screencopy_manager_v1::ZwlrScreencopyManagerV1, + }, }; use crate::{ convert::create_converter, dispatch::{CaptureFrameState, FrameState, OutputCaptureState, WayshotState}, output::OutputInfo, + region::{LogicalRegion, Size}, screencopy::{create_shm_fd, FrameCopy, FrameFormat}, }; @@ -50,41 +72,21 @@ pub mod reexport { use wayland_client::protocol::wl_output; pub use wl_output::{Transform, WlOutput}; } - -type Frame = (Vec, (i32, i32)); - -/// Struct to store region capture details. -#[derive(Debug, Copy, Clone, PartialEq, Eq)] -pub struct CaptureRegion { - /// X coordinate of the area to capture. - pub x_coordinate: i32, - /// y coordinate of the area to capture. - pub y_coordinate: i32, - /// Width of the capture area. - pub width: i32, - /// Height of the capture area. - pub height: i32, -} - -#[derive(Debug)] -struct IntersectingOutput { - output: WlOutput, - region: CaptureRegion, - transform: Transform, -} +use gbm::{BufferObject, BufferObjectFlags, Device as GBMDevice}; /// Struct to store wayland connection and globals list. /// # Example usage /// /// ``` -/// let wayshot_connection = WayshotConnection::new().unwrap(); -/// let image_buffer = wayshot_connection.screenshot_all().unwrap(); +/// let wayshot_connection = WayshotConnection::new()?; +/// let image_buffer = wayshot_connection.screenshot_all()?; /// ``` #[derive(Debug)] pub struct WayshotConnection { pub conn: Connection, pub globals: GlobalList, output_infos: Vec, + dmabuf_state: Option, } impl WayshotConnection { @@ -102,6 +104,34 @@ impl WayshotConnection { conn, globals, output_infos: Vec::new(), + dmabuf_state: None, + }; + + initial_state.refresh_outputs()?; + + Ok(initial_state) + } + + ///Create a WayshotConnection struct having DMA-BUF support + /// Using this connection is required to make use of the dmabuf functions + ///# Parameters + /// - conn: a Wayland connection + /// - device_path: string pointing to the DRI device that is to be used for creating the DMA-BUFs on. For example: "/dev/dri/renderD128" + pub fn from_connection_with_dmabuf(conn: Connection, device_path: &str) -> Result { + let (globals, evq) = registry_queue_init::(&conn)?; + let linux_dmabuf = + globals.bind(&evq.handle(), 4..=ZwpLinuxDmabufV1::interface().version, ())?; + let gpu = dispatch::Card::open(device_path); + // init a GBM device + let gbm = GBMDevice::new(gpu).unwrap(); + let mut initial_state = Self { + conn, + globals, + output_infos: Vec::new(), + dmabuf_state: Some(DMABUFState { + linux_dmabuf, + gbmdev: gbm, + }), }; initial_state.refresh_outputs()?; @@ -110,8 +140,8 @@ impl WayshotConnection { } /// Fetch all accessible wayland outputs. - pub fn get_all_outputs(&self) -> &Vec { - &self.output_infos + pub fn get_all_outputs(&self) -> &[OutputInfo] { + self.output_infos.as_slice() } /// refresh the outputs, to get new outputs @@ -139,7 +169,6 @@ impl WayshotConnection { // Fetch all outputs; when their names arrive, add them to the list let _ = self.conn.display().get_registry(&qh, ()); event_queue.roundtrip(&mut state)?; - event_queue.roundtrip(&mut state)?; // We loop over each output and request its position data. let xdg_outputs: Vec = state @@ -159,9 +188,9 @@ impl WayshotConnection { if state.outputs.is_empty() { tracing::error!("Compositor did not advertise any wl_output devices!"); - exit(1); + return Err(Error::NoOutputs); } - tracing::debug!("Outputs detected: {:#?}", state.outputs); + tracing::trace!("Outputs detected: {:#?}", state.outputs); self.output_infos = state.outputs; Ok(()) @@ -174,18 +203,256 @@ impl WayshotConnection { cursor_overlay: i32, output: &WlOutput, fd: T, - capture_region: Option, - ) -> Result { + capture_region: Option, + ) -> Result<(FrameFormat, FrameGuard)> { let (state, event_queue, frame, frame_format) = - self.capture_output_frame_get_state(cursor_overlay, output, capture_region)?; - self.capture_output_frame_inner(state, event_queue, frame, frame_format, fd) + self.capture_output_frame_get_state_shm(cursor_overlay, output, capture_region)?; + let frame_guard = + self.capture_output_frame_inner(state, event_queue, frame, frame_format, fd)?; + + Ok((frame_format, frame_guard)) } - fn capture_output_frame_get_state( + fn capture_output_frame_shm_from_file( + &self, + cursor_overlay: bool, + output: &WlOutput, + file: &File, + capture_region: Option, + ) -> Result<(FrameFormat, FrameGuard)> { + let (state, event_queue, frame, frame_format) = + self.capture_output_frame_get_state_shm(cursor_overlay as i32, output, capture_region)?; + + file.set_len(frame_format.byte_size())?; + + let frame_guard = + self.capture_output_frame_inner(state, event_queue, frame, frame_format, file)?; + + Ok((frame_format, frame_guard)) + } + /// Helper function/wrapper that uses the OpenGL extension OES_EGL_image to convert the EGLImage obtained from [`WayshotConnection::capture_output_frame_eglimage`] + /// into a OpenGL texture. + /// - The caller is supposed to setup everything required for the texture binding. An example call may look like: + /// ``` + /// gl::BindTexture(gl::TEXTURE_2D, self.gl_texture); + /// gl::TexParameteri(gl::TEXTURE_2D, gl::TEXTURE_MIN_FILTER, gl::LINEAR as i32); + /// wayshot_conn + /// .bind_output_frame_to_gl_texture( + /// true, + /// &wayshot_conn.get_all_outputs()[0].wl_output, + /// None) + ///``` + /// # Parameters + /// - `cursor_overlay`: A boolean flag indicating whether the cursor should be included in the capture. + /// - `output`: Reference to the `WlOutput` from which the frame is to be captured. + /// - `capture_region`: Optional region specifying a sub-area of the output to capture. If `None`, the entire output is captured. + /// # Returns + /// - If the function was found and called, an OK(()), note that this does not neccesarily mean that binding was successful, only that the function was called. + /// The caller may check for any OpenGL errors using the standard routes. + /// - If the function was not found, [`Error::EGLImageToTexProcNotFoundError`] is returned + pub unsafe fn bind_output_frame_to_gl_texture( + &self, + cursor_overlay: bool, + output: &WlOutput, + capture_region: Option, + ) -> Result<()> { + let egl = khronos_egl::Instance::new(egl::Static); + let eglimage_guard = + self.capture_output_frame_eglimage(&egl, cursor_overlay, output, capture_region)?; + unsafe { + let gl_egl_image_texture_target_2d_oes: unsafe extern "system" fn( + target: gl::types::GLenum, + image: gl::types::GLeglImageOES, + ) -> () = + std::mem::transmute(match egl.get_proc_address("glEGLImageTargetTexture2DOES") { + Some(f) => { + tracing::debug!("glEGLImageTargetTexture2DOES found at address {:#?}", f); + f + } + None => { + tracing::error!("glEGLImageTargetTexture2DOES not found"); + return Err(Error::EGLImageToTexProcNotFoundError); + } + }); + + gl_egl_image_texture_target_2d_oes(gl::TEXTURE_2D, eglimage_guard.image.as_ptr()); + tracing::trace!("glEGLImageTargetTexture2DOES called"); + Ok(()) + } + } + + /// Obtain a screencapture in the form of a EGLImage. + /// The display on which this image is created is obtained from the Wayland Connection. + /// Uses the dma-buf provisions of the wlr-screencopy copy protocol to avoid VRAM->RAM copies + /// It returns the captured frame as an `EGLImage`, wrapped in an `EGLImageGuard` + /// for safe handling and cleanup. + /// # Parameters + /// - `egl_instance`: Reference to an egl API instance obtained from the khronos_egl crate, which is used to create the `EGLImage`. + /// - `cursor_overlay`: A boolean flag indicating whether the cursor should be included in the capture. + /// - `output`: Reference to the `WlOutput` from which the frame is to be captured. + /// - `capture_region`: Optional region specifying a sub-area of the output to capture. If `None`, the entire output is captured. + /// + /// # Returns + /// If successful, an EGLImageGuard which contains a pointer 'image' to the created EGLImage + /// On error, the EGL [error code](https://registry.khronos.org/EGL/sdk/docs/man/html/eglGetError.xhtml) is returned via this crates Error type + pub fn capture_output_frame_eglimage<'a, T: khronos_egl::api::EGL1_5>( + &self, + egl_instance: &'a Instance, + cursor_overlay: bool, + output: &WlOutput, + capture_region: Option, + ) -> Result> { + let egl_display = unsafe { + match egl_instance.get_display(self.conn.display().id().as_ptr() as *mut c_void) { + Some(disp) => disp, + None => return Err(egl_instance.get_error().unwrap().into()), + } + }; + tracing::trace!("eglDisplay obtained from Wayland connection's display"); + + egl_instance.initialize(egl_display)?; + self.capture_output_frame_eglimage_on_display( + &egl_instance, + egl_display, + cursor_overlay, + output, + capture_region, + ) + } + + /// Obtain a screencapture in the form of a EGLImage on the given EGLDisplay. + /// + /// Uses the dma-buf provisions of the wlr-screencopy copy protocol to avoid VRAM->RAM copies + /// It returns the captured frame as an `EGLImage`, wrapped in an `EGLImageGuard` + /// for safe handling and cleanup. + /// # Parameters + /// - `egl_instance`: Reference to an `EGL1_5` instance, which is used to create the `EGLImage`. + /// - `egl_display`: The `EGLDisplay` on which the image should be created. + /// - `cursor_overlay`: A boolean flag indicating whether the cursor should be included in the capture. + /// - `output`: Reference to the `WlOutput` from which the frame is to be captured. + /// - `capture_region`: Optional region specifying a sub-area of the output to capture. If `None`, the entire output is captured. + /// + /// # Returns + /// If successful, an EGLImageGuard which contains a pointer 'image' to the created EGLImage + /// On error, the EGL [error code](https://registry.khronos.org/EGL/sdk/docs/man/html/eglGetError.xhtml) is returned via this crates Error type + pub fn capture_output_frame_eglimage_on_display<'a, T: khronos_egl::api::EGL1_5>( + &self, + egl_instance: &'a Instance, + egl_display: egl::Display, + cursor_overlay: bool, + output: &WlOutput, + capture_region: Option, + ) -> Result> { + type Attrib = egl::Attrib; + let (frame_format, _guard, bo) = + self.capture_output_frame_dmabuf(cursor_overlay, output, capture_region)?; + let modifier: u64 = bo.modifier()?.into(); + let image_attribs = [ + egl::WIDTH as Attrib, + frame_format.size.width as Attrib, + egl::HEIGHT as Attrib, + frame_format.size.height as Attrib, + 0x3271, //EGL_LINUX_DRM_FOURCC_EXT + bo.format().unwrap() as Attrib, + 0x3272, //EGL_DMA_BUF_PLANE0_FD_EXT + bo.fd_for_plane(0).unwrap().into_raw_fd() as Attrib, + 0x3273, //EGL_DMA_BUF_PLANE0_OFFSET_EXT + bo.offset(0).unwrap() as Attrib, + 0x3274, //EGL_DMA_BUF_PLANE0_PITCH_EXT + bo.stride_for_plane(0).unwrap() as Attrib, + 0x3443, //EGL_DMA_BUF_PLANE0_MODIFIER_LO_EXT + (modifier as u32) as Attrib, + 0x3444, //EGL_DMA_BUF_PLANE0_MODIFIER_HI_EXT + (modifier >> 32) as Attrib, + egl::ATTRIB_NONE as Attrib, + ]; + tracing::debug!( + "Calling eglCreateImage with attributes: {:#?}", + image_attribs + ); + unsafe { + match egl_instance.create_image( + egl_display, + khronos_egl::Context::from_ptr(egl::NO_CONTEXT), + 0x3270, // EGL_LINUX_DMA_BUF_EXT + khronos_egl::ClientBuffer::from_ptr(std::ptr::null_mut()), //NULL + &image_attribs, + ) { + Ok(image) => Ok(EGLImageGuard { + image, + egl_instance, + egl_display, + }), + Err(e) => { + tracing::error!("eglCreateImage call failed with error {e}"); + Err(e.into()) + } + } + } + } + + /// Obtain a screencapture in the form of a WlBuffer backed by a GBM Bufferobject on the GPU. + /// Uses the dma-buf provisions of the wlr-screencopy copy protocol to avoid VRAM->RAM copies + /// The captured frame is returned as a tuple containing the frame format, a guard to manage + /// the WlBuffer's cleanup on drop, and the underlying `BufferObject`. + /// - `cursor_overlay`: A boolean flag indicating whether the cursor should be included in the capture. + /// - `output`: Reference to the `WlOutput` from which the frame is to be captured. + /// - `capture_region`: Optional region specifying a sub-area of the output to capture. If `None`, the entire output is captured. + ///# Returns + /// On success, returns a tuple containing the frame format, + /// a guard to manage the frame's lifecycle, and the GPU-backed `BufferObject`. + /// # Errors + /// - Returns `NoDMAStateError` if the DMA-BUF state is not initialized a the time of initialization of this struct. + pub fn capture_output_frame_dmabuf( + &self, + cursor_overlay: bool, + output: &WlOutput, + capture_region: Option, + ) -> Result<(DMAFrameFormat, DMAFrameGuard, BufferObject<()>)> { + match &self.dmabuf_state { + Some(dmabuf_state) => { + let (state, event_queue, frame, frame_format) = self + .capture_output_frame_get_state_dmabuf( + cursor_overlay as i32, + output, + capture_region, + )?; + let gbm = &dmabuf_state.gbmdev; + let bo = gbm.create_buffer_object::<()>( + frame_format.size.width, + frame_format.size.height, + gbm::Format::try_from(frame_format.format)?, + BufferObjectFlags::RENDERING | BufferObjectFlags::LINEAR, + )?; + + let stride = bo.stride()?; + let modifier: u64 = bo.modifier()?.into(); + tracing::debug!( + "Created GBM Buffer object with input frame format {:#?}, stride {:#?} and modifier {:#?} ", + frame_format, + stride,modifier + ); + let frame_guard = self.capture_output_frame_inner_dmabuf( + state, + event_queue, + frame, + frame_format, + stride, + modifier, + bo.fd_for_plane(0)?, + )?; + + Ok((frame_format, frame_guard, bo)) + } + None => Err(Error::NoDMAStateError), + } + } + + fn capture_output_frame_get_state_shm( &self, cursor_overlay: i32, output: &WlOutput, - capture_region: Option, + capture_region: Option, ) -> Result<( CaptureFrameState, EventQueue, @@ -194,6 +461,7 @@ impl WayshotConnection { )> { let mut state = CaptureFrameState { formats: Vec::new(), + dmabuf_formats: Vec::new(), state: None, buffer_done: AtomicBool::new(false), }; @@ -216,15 +484,15 @@ impl WayshotConnection { } }; - // Capture output. - let frame: ZwlrScreencopyFrameV1 = if let Some(region) = capture_region { + tracing::debug!("Capturing output(shm buffer)..."); + let frame = if let Some(embedded_region) = capture_region { screencopy_manager.capture_output_region( cursor_overlay, output, - region.x_coordinate, - region.y_coordinate, - region.width, - region.height, + embedded_region.inner.position.x, + embedded_region.inner.position.y, + embedded_region.inner.size.width as i32, + embedded_region.inner.size.height as i32, &qh, (), ) @@ -238,7 +506,7 @@ impl WayshotConnection { event_queue.blocking_dispatch(&mut state)?; } - tracing::debug!( + tracing::trace!( "Received compositor frame buffer formats: {:#?}", state.formats ); @@ -258,7 +526,7 @@ impl WayshotConnection { ) }) .copied(); - tracing::debug!("Selected frame buffer format: {:#?}", frame_format); + tracing::trace!("Selected frame buffer format: {:#?}", frame_format); // Check if frame format exists. let frame_format = match frame_format { @@ -271,6 +539,144 @@ impl WayshotConnection { Ok((state, event_queue, frame, frame_format)) } + fn capture_output_frame_get_state_dmabuf( + &self, + cursor_overlay: i32, + output: &WlOutput, + capture_region: Option, + ) -> Result<( + CaptureFrameState, + EventQueue, + ZwlrScreencopyFrameV1, + DMAFrameFormat, + )> { + let mut state = CaptureFrameState { + formats: Vec::new(), + dmabuf_formats: Vec::new(), + state: None, + buffer_done: AtomicBool::new(false), + }; + let mut event_queue = self.conn.new_event_queue::(); + let qh = event_queue.handle(); + + // Instantiating screencopy manager. + let screencopy_manager = match self.globals.bind::( + &qh, + 3..=3, + (), + ) { + Ok(x) => x, + Err(e) => { + tracing::error!("Failed to create screencopy manager. Does your compositor implement ZwlrScreencopy?"); + tracing::error!("err: {e}"); + return Err(Error::ProtocolNotFound( + "ZwlrScreencopy Manager not found".to_string(), + )); + } + }; + + tracing::debug!("Capturing output for DMA-BUF API..."); + let frame = if let Some(embedded_region) = capture_region { + screencopy_manager.capture_output_region( + cursor_overlay, + output, + embedded_region.inner.position.x, + embedded_region.inner.position.y, + embedded_region.inner.size.width as i32, + embedded_region.inner.size.height as i32, + &qh, + (), + ) + } else { + screencopy_manager.capture_output(cursor_overlay, output, &qh, ()) + }; + + // Empty internal event buffer until buffer_done is set to true which is when the Buffer done + // event is fired, aka the capture from the compositor is succesful. + while !state.buffer_done.load(Ordering::SeqCst) { + event_queue.blocking_dispatch(&mut state)?; + } + + tracing::trace!( + "Received compositor frame buffer formats: {:#?}", + state.formats + ); + // TODO select appropriate format if there is more than one + let frame_format = state.dmabuf_formats[0]; + tracing::trace!("Selected frame buffer format: {:#?}", frame_format); + + Ok((state, event_queue, frame, frame_format)) + } + + fn capture_output_frame_inner_dmabuf( + &self, + mut state: CaptureFrameState, + mut event_queue: EventQueue, + frame: ZwlrScreencopyFrameV1, + frame_format: DMAFrameFormat, + stride: u32, + modifier: u64, + fd: OwnedFd, + ) -> Result { + match &self.dmabuf_state { + Some(dmabuf_state) => { + // Connecting to wayland environment. + let qh = event_queue.handle(); + + let linux_dmabuf = &dmabuf_state.linux_dmabuf; + let dma_width = frame_format.size.width; + let dma_height = frame_format.size.height; + + let dma_params = linux_dmabuf.create_params(&qh, ()); + + dma_params.add( + fd.as_fd(), + 0, + 0, + stride, + (modifier >> 32) as u32, + (modifier & 0xffffffff) as u32, + ); + tracing::trace!("Called ZwpLinuxBufferParamsV1::create_params "); + let dmabuf_wlbuf = dma_params.create_immed( + dma_width as i32, + dma_height as i32, + frame_format.format, + zwp_linux_buffer_params_v1::Flags::empty(), + &qh, + (), + ); + tracing::trace!("Called ZwpLinuxBufferParamsV1::create_immed to create WlBuffer "); + // Copy the pixel data advertised by the compositor into the buffer we just created. + frame.copy(&dmabuf_wlbuf); + tracing::debug!("wlr-screencopy copy() with dmabuf complete"); + + // On copy the Ready / Failed events are fired by the frame object, so here we check for them. + loop { + // Basically reads, if frame state is not None then... + if let Some(state) = state.state { + match state { + FrameState::Failed => { + tracing::error!("Frame copy failed"); + return Err(Error::FramecopyFailed); + } + FrameState::Finished => { + tracing::trace!("Frame copy finished"); + + return Ok(DMAFrameGuard { + buffer: dmabuf_wlbuf, + }); + } + } + } + + event_queue.blocking_dispatch(&mut state)?; + } + } + None => Err(Error::NoDMAStateError), + } + } + fn capture_output_frame_inner( &self, mut state: CaptureFrameState, @@ -278,20 +684,25 @@ impl WayshotConnection { frame: ZwlrScreencopyFrameV1, frame_format: FrameFormat, fd: T, - ) -> Result { + ) -> Result { // Connecting to wayland environment. let qh = event_queue.handle(); - // Bytes of data in the frame = stride * height. - let frame_bytes = frame_format.stride * frame_format.height; - // Instantiate shm global. - let shm = self.globals.bind::(&qh, 1..=1, ()).unwrap(); - let shm_pool = shm.create_pool(fd.as_fd(), frame_bytes as i32, &qh, ()); + let shm = self.globals.bind::(&qh, 1..=1, ())?; + let shm_pool = shm.create_pool( + fd.as_fd(), + frame_format + .byte_size() + .try_into() + .map_err(|_| Error::BufferTooSmall)?, + &qh, + (), + ); let buffer = shm_pool.create_buffer( 0, - frame_format.width as i32, - frame_format.height as i32, + frame_format.size.width as i32, + frame_format.size.height as i32, frame_format.stride as i32, frame_format.format, &qh, @@ -310,9 +721,8 @@ impl WayshotConnection { return Err(Error::FramecopyFailed); } FrameState::Finished => { - buffer.destroy(); - shm_pool.destroy(); - return Ok(frame_format); + tracing::trace!("Frame copy finished"); + return Ok(FrameGuard { buffer, shm_pool }); } } } @@ -321,39 +731,22 @@ impl WayshotConnection { } } - fn capture_output_frame_shm_from_file( - &self, - cursor_overlay: bool, - output: &WlOutput, - file: &File, - capture_region: Option, - ) -> Result { - let (state, event_queue, frame, frame_format) = - self.capture_output_frame_get_state(cursor_overlay as i32, output, capture_region)?; - - // Bytes of data in the frame = stride * height. - let frame_bytes = frame_format.stride * frame_format.height; - file.set_len(frame_bytes as u64)?; - - self.capture_output_frame_inner(state, event_queue, frame, frame_format, file) - } - /// Get a FrameCopy instance with screenshot pixel data for any wl_output object. - fn capture_output_frame( + #[tracing::instrument(skip_all, fields(output = format!("{output_info}"), region = capture_region.map(|r| format!("{:}", r)).unwrap_or("fullscreen".to_string())))] + fn capture_frame_copy( &self, cursor_overlay: bool, - output: &WlOutput, - transform: Transform, - capture_region: Option, - ) -> Result { + output_info: &OutputInfo, + capture_region: Option, + ) -> Result<(FrameCopy, FrameGuard)> { // Create an in memory file and return it's file descriptor. let fd = create_shm_fd()?; // Create a writeable memory map backed by a mem_file. let mem_file = File::from(fd); - let frame_format = self.capture_output_frame_shm_from_file( + let (frame_format, frame_guard) = self.capture_output_frame_shm_from_file( cursor_overlay, - output, + &output_info.wl_output, &mem_file, capture_region, )?; @@ -367,99 +760,234 @@ impl WayshotConnection { tracing::error!("You can send a feature request for the above format to the mailing list for wayshot over at https://sr.ht/~shinyzenith/wayshot."); return Err(Error::NoSupportedBufferFormat); }; - Ok(FrameCopy { + let rotated_physical_size = match output_info.transform { + Transform::_90 | Transform::_270 | Transform::Flipped90 | Transform::Flipped270 => { + Size { + width: frame_format.size.height, + height: frame_format.size.width, + } + } + _ => frame_format.size, + }; + let frame_copy = FrameCopy { frame_format, frame_color_type, - frame_mmap, - transform, - }) + frame_data: FrameData::Mmap(frame_mmap), + transform: output_info.transform, + logical_region: capture_region + .map(|capture_region| capture_region.logical()) + .unwrap_or(output_info.logical_region), + physical_size: rotated_physical_size, + }; + tracing::debug!("Created frame copy: {:#?}", frame_copy); + Ok((frame_copy, frame_guard)) } - fn create_frame_copy( + pub fn capture_frame_copies( &self, - capture_region: CaptureRegion, + output_capture_regions: &[(OutputInfo, Option)], cursor_overlay: bool, - ) -> Result { - let frame_copies = thread::scope(|scope| -> Result<_> { - let join_handles = self - .get_all_outputs() - .into_iter() - .filter_map(|output| { - let x1: i32 = cmp::max(output.dimensions.x, capture_region.x_coordinate); - let y1: i32 = cmp::max(output.dimensions.y, capture_region.y_coordinate); - let x2: i32 = cmp::min( - output.dimensions.x + output.dimensions.width, - capture_region.x_coordinate + capture_region.width, - ); - let y2: i32 = cmp::min( - output.dimensions.y + output.dimensions.height, - capture_region.y_coordinate + capture_region.height, - ); - - let width = x2 - x1; - let height = y2 - y1; - - if width <= 0 || height <= 0 { - return None; - } + ) -> Result> { + output_capture_regions + .iter() + .map(|(output_info, capture_region)| { + self.capture_frame_copy(cursor_overlay, output_info, *capture_region) + .map(|(frame_copy, frame_guard)| (frame_copy, frame_guard, output_info.clone())) + }) + .collect() + } - let true_x = capture_region.x_coordinate - output.dimensions.x; - let true_y = capture_region.y_coordinate - output.dimensions.y; - let true_region = CaptureRegion { - x_coordinate: true_x, - y_coordinate: true_y, - width: capture_region.width, - height: capture_region.height, - }; - Some(IntersectingOutput { - output: output.wl_output.clone(), - region: true_region, - transform: output.transform, - }) - }) - .map(|intersecting_output| { - scope.spawn(move || { - self.capture_output_frame( - cursor_overlay, - &intersecting_output.output, - intersecting_output.transform, - Some(intersecting_output.region), - ) - }) - }) - .collect::>(); + /// Create a layer shell surface for each output, + /// render the screen captures on them and use the callback to select a region from them + fn overlay_frames_and_select_region( + &self, + frames: &[(FrameCopy, FrameGuard, OutputInfo)], + callback: Box Result>, + ) -> Result { + let mut state = LayerShellState { + configured_outputs: HashSet::new(), + }; + let mut event_queue: EventQueue = + self.conn.new_event_queue::(); + let qh = event_queue.handle(); - join_handles - .into_iter() - .map(|join_handle| join_handle.join()) - .flatten() - .collect::>() - })?; + let compositor = match self.globals.bind::(&qh, 3..=3, ()) { + Ok(x) => x, + Err(e) => { + tracing::error!( + "Failed to create compositor Does your compositor implement WlCompositor?" + ); + tracing::error!("err: {e}"); + return Err(Error::ProtocolNotFound( + "WlCompositor not found".to_string(), + )); + } + }; + let layer_shell = match self.globals.bind::(&qh, 1..=1, ()) { + Ok(x) => x, + Err(e) => { + tracing::error!( + "Failed to create layer shell. Does your compositor implement WlrLayerShellV1?" + ); + tracing::error!("err: {e}"); + return Err(Error::ProtocolNotFound( + "WlrLayerShellV1 not found".to_string(), + )); + } + }; + let viewporter = self + .globals + .bind::(&qh, 1..=1, ()) + .unwrap(); + + let mut layer_shell_surfaces = Vec::with_capacity(frames.len()); + + for (frame_copy, frame_guard, output_info) in frames { + tracing::span!( + tracing::Level::DEBUG, + "overlay_frames::surface", + output = format!("{output_info}") + ) + .in_scope(|| -> Result<()> { + let surface = compositor.create_surface(&qh, ()); + + let layer_surface = layer_shell.get_layer_surface( + &surface, + Some(&output_info.wl_output), + Layer::Top, + "wayshot".to_string(), + &qh, + output_info.wl_output.clone(), + ); + + layer_surface.set_exclusive_zone(-1); + layer_surface.set_anchor(Anchor::Top | Anchor::Left); + layer_surface.set_size( + frame_copy.frame_format.size.width, + frame_copy.frame_format.size.height, + ); + + debug!("Committing surface creation changes."); + surface.commit(); + + debug!("Waiting for layer surface to be configured."); + while !state.configured_outputs.contains(&output_info.wl_output) { + event_queue.blocking_dispatch(&mut state)?; + } + + surface.set_buffer_transform(output_info.transform); + // surface.set_buffer_scale(output_info.scale()); + surface.attach(Some(&frame_guard.buffer), 0, 0); - Ok((frame_copies, (capture_region.width, capture_region.height))) + let viewport = viewporter.get_viewport(&surface, &qh, ()); + viewport.set_destination( + output_info.logical_region.inner.size.width as i32, + output_info.logical_region.inner.size.height as i32, + ); + + debug!("Committing surface with attached buffer."); + surface.commit(); + layer_shell_surfaces.push((surface, layer_surface)); + event_queue.blocking_dispatch(&mut state)?; + + Ok(()) + })?; + } + + let callback_result = callback(); + + debug!("Unmapping and destroying layer shell surfaces."); + for (surface, layer_shell_surface) in layer_shell_surfaces.iter() { + surface.attach(None, 0, 0); + surface.commit(); //unmap surface by committing a null buffer + layer_shell_surface.destroy(); + } + event_queue.roundtrip(&mut state)?; + + callback_result } /// Take a screenshot from the specified region. - pub fn screenshot( + #[tracing::instrument(skip_all, fields(max_scale = tracing::field::Empty))] + fn screenshot_region_capturer( &self, - capture_region: CaptureRegion, + region_capturer: RegionCapturer, cursor_overlay: bool, ) -> Result { - let (frame_copies, (width, height)) = - self.create_frame_copy(capture_region, cursor_overlay)?; + let outputs_capture_regions: Vec<(OutputInfo, Option)> = + match region_capturer { + RegionCapturer::Outputs(ref outputs) => outputs + .iter() + .map(|output_info| (output_info.clone(), None)) + .collect(), + RegionCapturer::Region(capture_region) => self + .get_all_outputs() + .iter() + .filter_map(|output_info| { + tracing::span!( + tracing::Level::DEBUG, + "filter_map", + output = format!( + "{output_info} at {region}", + output_info = format!("{output_info}"), + region = LogicalRegion::from(output_info), + ), + capture_region = format!("{}", capture_region), + ) + .in_scope(|| { + if let Some(relative_region) = + EmbeddedRegion::new(capture_region, output_info.into()) + { + tracing::debug!("Intersection found: {}", relative_region); + Some((output_info.clone(), Some(relative_region))) + } else { + tracing::debug!("No intersection found"); + None + } + }) + }) + .collect(), + RegionCapturer::Freeze(_) => self + .get_all_outputs() + .iter() + .map(|output_info| (output_info.clone(), None)) + .collect(), + }; + + let frames = self.capture_frame_copies(&outputs_capture_regions, cursor_overlay)?; + + let capture_region: LogicalRegion = match region_capturer { + RegionCapturer::Outputs(outputs) => outputs.as_slice().try_into()?, + RegionCapturer::Region(region) => region, + RegionCapturer::Freeze(callback) => { + self.overlay_frames_and_select_region(&frames, callback)? + } + }; + + // TODO When freeze was used, we can still further remove the outputs + // that don't intersect with the capture region. thread::scope(|scope| { - let rotate_join_handles = frame_copies + let max_scale = outputs_capture_regions + .iter() + .map(|(output_info, _)| output_info.scale()) + .fold(1.0, f64::max); + + tracing::Span::current().record("max_scale", max_scale); + + let rotate_join_handles = frames .into_iter() - .map(|frame_copy| { + .map(|(frame_copy, _, _)| { scope.spawn(move || { - let transform = frame_copy.transform; - let image = frame_copy.try_into()?; - Ok(image_util::rotate_image_buffer( - image, - transform, - width as u32, - height as u32, + let image = (&frame_copy).try_into()?; + Ok(( + image_util::rotate_image_buffer( + image, + frame_copy.transform, + frame_copy.logical_region.inner.size, + max_scale, + ), + frame_copy, )) }) }) @@ -467,25 +995,44 @@ impl WayshotConnection { rotate_join_handles .into_iter() - .map(|join_handle| join_handle.join()) - .flatten() + .flat_map(|join_handle| join_handle.join()) .fold( None, - |possible_overlayed_image_or_error: Option>, image: Result<_>| { - if let Some(overlayed_image_or_error) = possible_overlayed_image_or_error { - if let Ok(mut overlayed_image) = overlayed_image_or_error { - if let Ok(image) = image { - overlay(&mut overlayed_image, &image, 0, 0); - Some(Ok(overlayed_image)) - } else { - Some(image) - } - } else { - Some(image) - } - } else { - Some(image) - } + |composite_image: Option>, image: Result<_>| { + // Default to a transparent image. + let composite_image = composite_image.unwrap_or_else(|| { + Ok(DynamicImage::new_rgba8( + (capture_region.inner.size.width as f64 * max_scale) as u32, + (capture_region.inner.size.height as f64 * max_scale) as u32, + )) + }); + + Some(|| -> Result<_> { + let mut composite_image = composite_image?; + let (image, frame_copy) = image?; + let (x, y) = ( + ((frame_copy.logical_region.inner.position.x as f64 + - capture_region.inner.position.x as f64) + * max_scale) as i64, + ((frame_copy.logical_region.inner.position.y as f64 + - capture_region.inner.position.y as f64) + * max_scale) as i64, + ); + tracing::span!( + tracing::Level::DEBUG, + "replace", + frame_copy_region = format!("{}", frame_copy.logical_region), + capture_region = format!("{}", capture_region), + x = x, + y = y, + ) + .in_scope(|| { + tracing::debug!("Replacing parts of the final image"); + replace(&mut composite_image, &image, x, y); + }); + + Ok(composite_image) + }()) }, ) .ok_or_else(|| { @@ -495,58 +1042,45 @@ impl WayshotConnection { }) } + /// Take a screenshot from the specified region. + pub fn screenshot( + &self, + capture_region: LogicalRegion, + cursor_overlay: bool, + ) -> Result { + self.screenshot_region_capturer(RegionCapturer::Region(capture_region), cursor_overlay) + } + + /// Take a screenshot, overlay the screenshot, run the callback, and then + /// unfreeze the screenshot and return the selected region. + pub fn screenshot_freeze( + &self, + callback: Box Result>, + cursor_overlay: bool, + ) -> Result { + self.screenshot_region_capturer(RegionCapturer::Freeze(callback), cursor_overlay) + } /// shot one ouput pub fn screenshot_single_output( &self, output_info: &OutputInfo, cursor_overlay: bool, ) -> Result { - let frame_copy = self.capture_output_frame( - cursor_overlay, - &output_info.wl_output, - output_info.transform, - None, - )?; - frame_copy.try_into() + let (frame_copy, _) = self.capture_frame_copy(cursor_overlay, output_info, None)?; + (&frame_copy).try_into() } /// Take a screenshot from all of the specified outputs. pub fn screenshot_outputs( &self, - outputs: &Vec, + outputs: &[OutputInfo], cursor_overlay: bool, ) -> Result { if outputs.is_empty() { return Err(Error::NoOutputs); } - let x1 = outputs - .iter() - .map(|output| output.dimensions.x) - .min() - .unwrap(); - let y1 = outputs - .iter() - .map(|output| output.dimensions.y) - .min() - .unwrap(); - let x2 = outputs - .iter() - .map(|output| output.dimensions.x + output.dimensions.width) - .max() - .unwrap(); - let y2 = outputs - .iter() - .map(|output| output.dimensions.y + output.dimensions.height) - .max() - .unwrap(); - let capture_region = CaptureRegion { - x_coordinate: x1, - y_coordinate: y1, - width: x2 - x1, - height: y2 - y1, - }; - self.screenshot(capture_region, cursor_overlay) + self.screenshot_region_capturer(RegionCapturer::Outputs(outputs.to_owned()), cursor_overlay) } /// Take a screenshot from all accessible outputs. diff --git a/libwayshot/src/output.rs b/libwayshot/src/output.rs index ccca1c80..03d0a725 100644 --- a/libwayshot/src/output.rs +++ b/libwayshot/src/output.rs @@ -1,28 +1,35 @@ +use std::fmt::Display; + use wayland_client::protocol::{wl_output, wl_output::WlOutput}; +use crate::region::{LogicalRegion, Size}; + /// Represents an accessible wayland output. /// /// Do not instantiate, instead use [`crate::WayshotConnection::get_all_outputs`]. -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct OutputInfo { pub wl_output: WlOutput, pub name: String, pub description: String, pub transform: wl_output::Transform, - pub dimensions: OutputPositioning, - pub mode: WlOutputMode, + pub physical_size: Size, + pub logical_region: LogicalRegion, } -#[derive(Default, Debug, Clone, PartialEq, Eq)] -pub struct WlOutputMode { - pub width: i32, - pub height: i32, +impl Display for OutputInfo { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{name} ({description})", + name = self.name, + description = self.description + ) + } } -#[derive(Default, Debug, Clone, PartialEq, Eq)] -pub struct OutputPositioning { - pub x: i32, - pub y: i32, - pub width: i32, - pub height: i32, +impl OutputInfo { + pub(crate) fn scale(&self) -> f64 { + self.physical_size.height as f64 / self.logical_region.inner.size.height as f64 + } } diff --git a/libwayshot/src/region.rs b/libwayshot/src/region.rs new file mode 100644 index 00000000..6813ded2 --- /dev/null +++ b/libwayshot/src/region.rs @@ -0,0 +1,238 @@ +use std::cmp; + +use crate::error::{Error, Result}; +use crate::output::OutputInfo; + +/// Ways to say how a region for a screenshot should be captured. +pub enum RegionCapturer { + /// Capture all of the given outputs. + Outputs(Vec), + /// Capture an already known `LogicalRegion`. + Region(LogicalRegion), + /// The outputs will be "frozen" to the user at which point the given + /// callback is called to get the region to capture. This callback is often + /// a user interaction to let the user select a region. + Freeze(Box Result>), +} + +/// `Region` where the coordinate system is the logical coordinate system used +/// in Wayland to position outputs. Top left is (0, 0) and any transforms and +/// scaling have been applied. A unit is a logical pixel, meaning that this is +/// after scaling has been applied. +#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, Hash)] +pub struct LogicalRegion { + pub inner: Region, +} + +/// An embedded region is a region entirely inside of another (often an output). +/// +/// It can only be contained inside of another and cannot exceed its bounds. +/// +/// Example: +/// +/// ```` +/// ┌─────────────┐ +/// │ │ +/// │ ┌──────────┼──────┐ +/// │ │ │ ├──► Viewport +/// │ │ │ │ +/// │ │ ├──────┼─────────────────┐ +/// │ │ │xxxxxx│ │ +/// │ │ │xxxxx◄├─── Inner region │ +/// │ └──────────┼──────┘ │ +/// │ │ │ +/// │ │ Screen 2 ├──► Relative to +/// │ ├────────────────────────┘ +/// │ │ +/// │ Screen 1 │ +/// └─────────────┘ +/// ```` +#[derive(Debug, Copy, Clone)] +pub struct EmbeddedRegion { + /// The coordinate sysd + pub relative_to: LogicalRegion, + pub inner: Region, +} + +/// Rectangle area in an unspecified coordinate system. +/// +/// Use `LogicalRegion` or `EmbeddedRegion` instead as they convey the +/// coordinate system used. +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Default)] +pub struct Region { + /// Position of the region. + pub position: Position, + /// Size of the region. + pub size: Size, +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Default)] +pub struct Position { + /// X coordinate. + pub x: i32, + /// Y coordinate. + pub y: i32, +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Default)] +pub struct Size { + /// Width. + pub width: u32, + /// Height. + pub height: u32, +} + +impl EmbeddedRegion { + /// Given two `LogicalRegion`s, one seen as the `viewport` and the other + /// `relative_to` (think the output we want to capture), create an + /// embedded region that is entirely inside of the `relative_to` region. + /// + /// See `EmbeddedRegion` for an example ASCII visualisation. + #[tracing::instrument(ret, level = "debug")] + pub fn new(viewport: LogicalRegion, relative_to: LogicalRegion) -> Option { + let x_relative: i32 = viewport.inner.position.x - relative_to.inner.position.x; + let y_relative = viewport.inner.position.y - relative_to.inner.position.y; + + let x1 = cmp::max(x_relative, 0); + let x2 = cmp::min( + x_relative + viewport.inner.size.width as i32, + relative_to.inner.size.width as i32, + ); + let width = if let Ok(width) = (x2 - x1).try_into() { + width + } else { + return None; + }; + + let y1 = cmp::max(y_relative, 0); + let y2 = cmp::min( + y_relative + viewport.inner.size.height as i32, + relative_to.inner.size.height as i32, + ); + let height = if let Ok(height) = (y2 - y1).try_into() { + height + } else { + return None; + }; + + Some(Self { + relative_to, + inner: Region { + position: Position { x: x1, y: y1 }, + size: Size { width, height }, + }, + }) + } + + /// Return the `LogicalRegion` of the embedded region. + /// + /// Note that this remains a region of the same size, it's not the inverse + /// of `EmbeddedRegion::new` which removes the parts that are outside of + /// the `relative_to` region. + pub fn logical(&self) -> LogicalRegion { + LogicalRegion { + inner: Region { + position: Position { + x: self.relative_to.inner.position.x + self.inner.position.x, + y: self.relative_to.inner.position.y + self.inner.position.y, + }, + size: self.inner.size, + }, + } + } +} + +impl std::fmt::Display for EmbeddedRegion { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{region} relative to {relative_to}", + region = self.inner, + relative_to = self.relative_to, + ) + } +} + +impl std::fmt::Display for Position { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "({x}, {y})", x = self.x, y = self.y,) + } +} + +impl std::fmt::Display for Size { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "({width}x{height})", + width = self.width, + height = self.height, + ) + } +} + +impl std::fmt::Display for Region { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{position} {size}", + position = self.position, + size = self.size, + ) + } +} + +impl std::fmt::Display for LogicalRegion { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{inner}", inner = self.inner) + } +} + +impl From<&OutputInfo> for LogicalRegion { + fn from(output_info: &OutputInfo) -> Self { + LogicalRegion { + inner: output_info.logical_region.inner, + } + } +} + +impl TryFrom<&[OutputInfo]> for LogicalRegion { + type Error = Error; + + fn try_from(output_info: &[OutputInfo]) -> std::result::Result { + let x1 = output_info + .iter() + .map(|output| output.logical_region.inner.position.x) + .min() + .ok_or(Error::NoOutputs)?; + let y1 = output_info + .iter() + .map(|output| output.logical_region.inner.position.y) + .min() + .ok_or(Error::NoOutputs)?; + let x2 = output_info + .iter() + .map(|output| { + output.logical_region.inner.position.x + + output.logical_region.inner.size.width as i32 + }) + .max() + .ok_or(Error::NoOutputs)?; + let y2 = output_info + .iter() + .map(|output| { + output.logical_region.inner.position.y + + output.logical_region.inner.size.height as i32 + }) + .max() + .ok_or(Error::NoOutputs)?; + Ok(LogicalRegion { + inner: Region { + position: Position { x: x1, y: y1 }, + size: Size { + width: (x2 - x1) as u32, + height: (y2 - y1) as u32, + }, + }, + }) + } +} diff --git a/libwayshot/src/screencopy.rs b/libwayshot/src/screencopy.rs index e2713582..adeec7fa 100644 --- a/libwayshot/src/screencopy.rs +++ b/libwayshot/src/screencopy.rs @@ -1,67 +1,158 @@ use std::{ - ffi::CStr, - os::fd::{AsRawFd, IntoRawFd, OwnedFd}, + ffi::CString, + os::fd::OwnedFd, time::{SystemTime, UNIX_EPOCH}, }; +use gbm::BufferObject; use image::{ColorType, DynamicImage, ImageBuffer, Pixel}; use memmap2::MmapMut; -use nix::{ - fcntl, - sys::{memfd, mman, stat}, - unistd, +use rustix::{ + fs::{self, SealFlags}, + io, shm, }; -use wayland_client::protocol::{wl_output, wl_shm::Format}; +use wayland_client::protocol::{ + wl_buffer::WlBuffer, wl_output, wl_shm::Format, wl_shm_pool::WlShmPool, +}; + +use crate::{ + region::{LogicalRegion, Size}, + Error, Result, +}; + +pub struct FrameGuard { + pub buffer: WlBuffer, + pub shm_pool: WlShmPool, +} -use crate::{Error, Result}; +impl Drop for FrameGuard { + fn drop(&mut self) { + self.buffer.destroy(); + self.shm_pool.destroy(); + } +} + +pub struct DMAFrameGuard { + pub buffer: WlBuffer, +} +impl Drop for DMAFrameGuard { + fn drop(&mut self) { + self.buffer.destroy(); + } +} + +pub struct EGLImageGuard<'a, T: khronos_egl::api::EGL1_5> { + pub image: khronos_egl::Image, + pub(crate) egl_instance: &'a khronos_egl::Instance, + pub(crate) egl_display: khronos_egl::Display, +} + +impl<'a, T: khronos_egl::api::EGL1_5> Drop for EGLImageGuard<'a, T> { + fn drop(&mut self) { + self.egl_instance + .destroy_image(self.egl_display, self.image) + .unwrap_or_else(|e| { + tracing::error!("EGLimage destruction had error: {e}"); + }); + } +} /// Type of frame supported by the compositor. For now we only support Argb8888, Xrgb8888, and /// Xbgr8888. +/// +/// See `zwlr_screencopy_frame_v1::Event::Buffer` as it's retrieved from there. #[derive(Debug, Copy, Clone, PartialEq)] pub struct FrameFormat { pub format: Format, - pub width: u32, - pub height: u32, + /// Size of the frame in pixels. This will always be in "landscape" so a + /// portrait 1080x1920 frame will be 1920x1080 and will need to be rotated! + pub size: Size, + /// Stride is the number of bytes between the start of a row and the start of the next row. pub stride: u32, } +/// Type of DMABUF frame supported by the compositor +/// +/// See `zwlr_screencopy_frame_v1::Event::linux_dmabuf` as it's retrieved from there. +#[derive(Debug, Copy, Clone, PartialEq)] +pub struct DMAFrameFormat { + pub format: u32, + /// Size of the frame in pixels. This will always be in "landscape" so a + /// portrait 1080x1920 frame will be 1920x1080 and will need to be rotated! + pub size: Size, +} + +impl FrameFormat { + /// Returns the size of the frame in bytes, which is the stride * height. + pub fn byte_size(&self) -> u64 { + self.stride as u64 * self.size.height as u64 + } +} + +#[tracing::instrument(skip(frame_data))] fn create_image_buffer

( frame_format: &FrameFormat, - frame_mmap: &MmapMut, + frame_data: &FrameData, ) -> Result>> where P: Pixel, { - ImageBuffer::from_vec(frame_format.width, frame_format.height, frame_mmap.to_vec()) - .ok_or(Error::BufferTooSmall) + tracing::debug!("Creating image buffer"); + match frame_data { + FrameData::Mmap(frame_mmap) => ImageBuffer::from_vec( + frame_format.size.width, + frame_format.size.height, + frame_mmap.to_vec(), + ) + .ok_or(Error::BufferTooSmall), + FrameData::GBMBo(_) => todo!(), + } } +#[derive(Debug)] +pub enum FrameData { + Mmap(MmapMut), + GBMBo(BufferObject<()>), +} /// The copied frame comprising of the FrameFormat, ColorType (Rgba8), and a memory backed shm /// file that holds the image data in it. #[derive(Debug)] pub struct FrameCopy { pub frame_format: FrameFormat, pub frame_color_type: ColorType, - pub frame_mmap: MmapMut, + pub frame_data: FrameData, pub transform: wl_output::Transform, + /// Logical region with the transform already applied. + pub logical_region: LogicalRegion, + pub physical_size: Size, } -impl TryFrom for DynamicImage { +impl TryFrom<&FrameCopy> for DynamicImage { type Error = Error; - fn try_from(value: FrameCopy) -> Result { + fn try_from(value: &FrameCopy) -> Result { Ok(match value.frame_color_type { ColorType::Rgb8 => { - Self::ImageRgb8(create_image_buffer(&value.frame_format, &value.frame_mmap)?) + Self::ImageRgb8(create_image_buffer(&value.frame_format, &value.frame_data)?) } ColorType::Rgba8 => { - Self::ImageRgba8(create_image_buffer(&value.frame_format, &value.frame_mmap)?) + Self::ImageRgba8(create_image_buffer(&value.frame_format, &value.frame_data)?) } _ => return Err(Error::InvalidColor), }) } } +fn get_mem_file_handle() -> String { + format!( + "/libwayshot-{}", + SystemTime::now() + .duration_since(UNIX_EPOCH) + .map(|time| time.subsec_nanos().to_string()) + .unwrap_or("unknown".into()) + ) +} + /// Return a RawFd to a shm file. We use memfd create on linux and shm_open for BSD support. /// You don't need to mess around with this function, it is only used by /// capture_output_frame. @@ -70,36 +161,27 @@ pub fn create_shm_fd() -> std::io::Result { #[cfg(any(target_os = "linux", target_os = "freebsd"))] loop { // Create a file that closes on succesful execution and seal it's operations. - match memfd::memfd_create( - CStr::from_bytes_with_nul(b"libwayshot\0").unwrap(), - memfd::MemFdCreateFlag::MFD_CLOEXEC | memfd::MemFdCreateFlag::MFD_ALLOW_SEALING, + match fs::memfd_create( + CString::new("libwayshot")?.as_c_str(), + fs::MemfdFlags::CLOEXEC | fs::MemfdFlags::ALLOW_SEALING, ) { Ok(fd) => { // This is only an optimization, so ignore errors. // F_SEAL_SRHINK = File cannot be reduced in size. // F_SEAL_SEAL = Prevent further calls to fcntl(). - let _ = fcntl::fcntl( - fd.as_raw_fd(), - fcntl::F_ADD_SEALS( - fcntl::SealFlag::F_SEAL_SHRINK | fcntl::SealFlag::F_SEAL_SEAL, - ), - ); + let _ = fs::fcntl_add_seals(&fd, fs::SealFlags::SHRINK | SealFlags::SEAL); return Ok(fd); } - Err(nix::errno::Errno::EINTR) => continue, - Err(nix::errno::Errno::ENOSYS) => break, + Err(io::Errno::INTR) => continue, + Err(io::Errno::NOSYS) => break, Err(errno) => return Err(std::io::Error::from(errno)), } } // Fallback to using shm_open. - let sys_time = SystemTime::now(); - let mut mem_file_handle = format!( - "/libwayshot-{}", - sys_time.duration_since(UNIX_EPOCH).unwrap().subsec_nanos() - ); + let mut mem_file_handle = get_mem_file_handle(); loop { - match mman::shm_open( + match shm::shm_open( // O_CREAT = Create file if does not exist. // O_EXCL = Error if create and file exists. // O_RDWR = Open for reading and writing. @@ -107,28 +189,19 @@ pub fn create_shm_fd() -> std::io::Result { // S_IRUSR = Set user read permission bit . // S_IWUSR = Set user write permission bit. mem_file_handle.as_str(), - fcntl::OFlag::O_CREAT - | fcntl::OFlag::O_EXCL - | fcntl::OFlag::O_RDWR - | fcntl::OFlag::O_CLOEXEC, - stat::Mode::S_IRUSR | stat::Mode::S_IWUSR, + shm::ShmOFlags::CREATE | shm::ShmOFlags::EXCL | shm::ShmOFlags::RDWR, + fs::Mode::RUSR | fs::Mode::WUSR, ) { - Ok(fd) => match mman::shm_unlink(mem_file_handle.as_str()) { + Ok(fd) => match shm::shm_unlink(mem_file_handle.as_str()) { Ok(_) => return Ok(fd), - Err(errno) => match unistd::close(fd.into_raw_fd()) { - Ok(_) => return Err(std::io::Error::from(errno)), - Err(errno) => return Err(std::io::Error::from(errno)), - }, + Err(errno) => return Err(std::io::Error::from(errno)), }, - Err(nix::errno::Errno::EEXIST) => { + Err(io::Errno::EXIST) => { // If a file with that handle exists then change the handle - mem_file_handle = format!( - "/libwayshot-{}", - sys_time.duration_since(UNIX_EPOCH).unwrap().subsec_nanos() - ); + mem_file_handle = get_mem_file_handle(); continue; } - Err(nix::errno::Errno::EINTR) => continue, + Err(io::Errno::INTR) => continue, Err(errno) => return Err(std::io::Error::from(errno)), } } diff --git a/wayshot/Cargo.toml b/wayshot/Cargo.toml index 8d4aa528..44bc8652 100644 --- a/wayshot/Cargo.toml +++ b/wayshot/Cargo.toml @@ -12,13 +12,15 @@ repository.workspace = true [build-dependencies] flate2 = "1.0.27" +eyre = "0.6.8" + [dependencies] tracing.workspace = true libwayshot.workspace = true -clap = "4.4.6" +clap = { version = "4.4.18", features = ["derive"] } tracing-subscriber = "0.3.17" image = { version = "0.24", default-features = false, features = [ @@ -26,9 +28,15 @@ image = { version = "0.24", default-features = false, features = [ "png", "pnm", "qoi", + "webp-encoder", ] } dialoguer = { version = "0.11.0", features = ["fuzzy-select"] } +eyre = "0.6.8" +chrono = "0.4.35" + +wl-clipboard-rs = "0.8.0" +rustix = { version = "0.38", features = ["process", "runtime"] } [[bin]] name = "wayshot" diff --git a/wayshot/build.rs b/wayshot/build.rs index 14152acb..9c7c82e8 100644 --- a/wayshot/build.rs +++ b/wayshot/build.rs @@ -1,13 +1,14 @@ extern crate flate2; +use eyre::{ContextCompat, Result}; use flate2::{write::GzEncoder, Compression}; use std::{ fs::{read_dir, File, OpenOptions}, io::{copy, BufReader, ErrorKind}, path::Path, - process::{exit, Command, Stdio}, + process::{Command, Stdio}, }; -fn main() { +fn main() -> Result<()> { if let Err(e) = Command::new("scdoc") .stdin(Stdio::null()) .stdout(Stdio::null()) @@ -15,50 +16,62 @@ fn main() { .spawn() { if let ErrorKind::NotFound = e.kind() { - exit(0); + return Ok(()); } } // We just append "out" so it's easy to find all the scdoc output later in line 38. - let man_pages: Vec<(String, String)> = read_and_replace_by_ext("./docs", ".scd", ".out"); + let man_pages: Vec<(String, String)> = read_and_replace_by_ext("./docs", ".scd", ".out")?; for man_page in man_pages { let output = OpenOptions::new() .write(true) .create(true) - .open(Path::new(&man_page.1)) - .unwrap(); + .truncate(true) + .open(Path::new(&man_page.1))?; _ = Command::new("scdoc") - .stdin(Stdio::from(File::open(man_page.0).unwrap())) + .stdin(Stdio::from(File::open(man_page.0)?)) .stdout(output) .spawn(); } // Gzipping the man pages let scdoc_output_files: Vec<(String, String)> = - read_and_replace_by_ext("./docs", ".out", ".gz"); + read_and_replace_by_ext("./docs", ".out", ".gz")?; for scdoc_output in scdoc_output_files { - let mut input = BufReader::new(File::open(scdoc_output.0).unwrap()); + let mut input = BufReader::new(File::open(scdoc_output.0)?); let output = OpenOptions::new() .write(true) .create(true) - .open(Path::new(&scdoc_output.1)) - .unwrap(); + .truncate(true) + .open(Path::new(&scdoc_output.1))?; let mut encoder = GzEncoder::new(output, Compression::default()); - copy(&mut input, &mut encoder).unwrap(); - encoder.finish().unwrap(); + copy(&mut input, &mut encoder)?; + encoder.finish()?; } + + Ok(()) } -fn read_and_replace_by_ext(path: &str, search: &str, replace: &str) -> Vec<(String, String)> { +fn read_and_replace_by_ext( + path: &str, + search: &str, + replace: &str, +) -> Result> { let mut files: Vec<(String, String)> = Vec::new(); - for path in read_dir(path).unwrap() { - let path = path.unwrap(); - if path.file_type().unwrap().is_dir() { + for path in read_dir(path)? { + let path = path?; + if path.file_type()?.is_dir() { continue; } if let Some(file_name) = path.path().to_str() { - if *path.path().extension().unwrap().to_str().unwrap() != search[1..] { + if *path + .path() + .extension() + .wrap_err_with(|| format!("no extension found for {}", path.path().display()))? + .to_string_lossy() + != search[1..] + { continue; } @@ -66,5 +79,5 @@ fn read_and_replace_by_ext(path: &str, search: &str, replace: &str) -> Vec<(Stri files.push((file_name.to_string(), file)); } } - files + Ok(files) } diff --git a/wayshot/src/clap.rs b/wayshot/src/clap.rs deleted file mode 100644 index 37319334..00000000 --- a/wayshot/src/clap.rs +++ /dev/null @@ -1,67 +0,0 @@ -use clap::{arg, ArgAction, Command}; - -pub fn set_flags() -> Command { - Command::new("wayshot") - .version(env!("CARGO_PKG_VERSION")) - .author(env!("CARGO_PKG_AUTHORS")) - .about("Screenshot tool for compositors implementing zwlr_screencopy_v1.") - .arg( - arg!(-d - -debug) - .required(false) - .action(ArgAction::SetTrue) - .help("Enable debug mode"), - ) - .arg( - arg!(-s --slurp ) - .required(false) - .action(ArgAction::Set) - .help("Choose a portion of your display to screenshot using slurp"), - ) - .arg( - arg!(-f - -file ) - .required(false) - .conflicts_with("stdout") - .action(ArgAction::Set) - .help("Mention a custom file path"), - ) - .arg( - arg!(-c - -cursor) - .required(false) - .action(ArgAction::SetTrue) - .help("Enable cursor in screenshots"), - ) - .arg( - arg!(--stdout) - .required(false) - .conflicts_with("file") - .action(ArgAction::SetTrue) - .help("Output the image data to standard out"), - ) - .arg( - arg!(-e --extension ) - .required(false) - .action(ArgAction::Set) - .help("Set image encoder (Png is default)"), - ) - .arg( - arg!(-l - -listoutputs) - .required(false) - .action(ArgAction::SetTrue) - .help("List all valid outputs"), - ) - .arg( - arg!(-o --output ) - .required(false) - .action(ArgAction::Set) - .conflicts_with("slurp") - .help("Choose a particular display to screenshot"), - ) - .arg( - arg!(--chooseoutput) - .required(false) - .action(ArgAction::SetTrue) - .conflicts_with("slurp") - .conflicts_with("output") - .help("Present a fuzzy selector for outputs"), - ) -} diff --git a/wayshot/src/cli.rs b/wayshot/src/cli.rs new file mode 100644 index 00000000..9882e09d --- /dev/null +++ b/wayshot/src/cli.rs @@ -0,0 +1,54 @@ +use std::path::PathBuf; + +use clap::arg; + +use clap::Parser; +use eyre::WrapErr; + +use crate::utils::EncodingFormat; +use clap::builder::TypedValueParser; + +#[derive(Parser)] +#[command(version, about)] +pub struct Cli { + /// Custom output path can be of the following types: + /// 1. Directory (Default naming scheme is used for the image output). + /// 2. Path (Encoding is automatically inferred from the extension). + /// 3. `-` (Indicates writing to terminal [stdout]). + #[arg(value_name = "OUTPUT", verbatim_doc_comment)] + pub file: Option, + + /// Copy image to clipboard. Can be used simultaneously with [OUTPUT] or stdout. + /// Wayshot persists in the background offering the image till the clipboard is overwritten. + #[arg(long, verbatim_doc_comment)] + pub clipboard: bool, + + /// Log level to be used for printing to stderr + #[arg(long, default_value = "info", value_parser = clap::builder::PossibleValuesParser::new(["trace", "debug", "info", "warn", "error"]).map(|s| -> tracing::Level{ s.parse().wrap_err_with(|| format!("Failed to parse log level: {}", s)).unwrap()}))] + pub log_level: tracing::Level, + + /// Arguments to call slurp with for selecting a region + #[arg(short, long, value_name = "SLURP_ARGS")] + pub slurp: Option>, + + /// Enable cursor in screenshots + #[arg(short, long)] + pub cursor: bool, + + /// Set image encoder, by default uses the file extension from the OUTPUT + /// positional argument. Otherwise defaults to png. + #[arg(long, verbatim_doc_comment, visible_aliases = ["extension", "format", "output-format"], value_name = "FILE_EXTENSION")] + pub encoding: Option, + + /// List all valid outputs + #[arg(short, long, alias = "listoutputs")] + pub list_outputs: bool, + + /// Choose a particular output/display to screenshot + #[arg(short, long, conflicts_with = "slurp")] + pub output: Option, + + /// Present a fuzzy selector for output/display selection + #[arg(long, alias = "chooseoutput", conflicts_with_all = ["slurp", "output"])] + pub choose_output: bool, +} diff --git a/wayshot/src/utils.rs b/wayshot/src/utils.rs index e24caf74..feca42f8 100644 --- a/wayshot/src/utils.rs +++ b/wayshot/src/utils.rs @@ -1,56 +1,71 @@ -use std::{ - process::exit, - time::{SystemTime, UNIX_EPOCH}, -}; +use clap::ValueEnum; +use eyre::{bail, ContextCompat, Error, Result}; -use libwayshot::CaptureRegion; +use std::{fmt::Display, path::PathBuf, str::FromStr}; -pub fn parse_geometry(g: &str) -> Option { +use chrono::{DateTime, Local}; +use libwayshot::region::{LogicalRegion, Position, Region, Size}; + +pub fn parse_geometry(g: &str) -> Result { let tail = g.trim(); let x_coordinate: i32; let y_coordinate: i32; - let width: i32; - let height: i32; + let width: u32; + let height: u32; + + let validation_error = + "Invalid geometry provided.\nValid geometries:\n1) %d,%d %dx%d\n2) %d %d %d %d"; if tail.contains(',') { // this accepts: "%d,%d %dx%d" - let (head, tail) = tail.split_once(',')?; - x_coordinate = head.parse::().ok()?; - let (head, tail) = tail.split_once(' ')?; - y_coordinate = head.parse::().ok()?; - let (head, tail) = tail.split_once('x')?; - width = head.parse::().ok()?; - height = tail.parse::().ok()?; + let (head, tail) = tail.split_once(',').wrap_err(validation_error)?; + x_coordinate = head.parse::()?; + let (head, tail) = tail.split_once(' ').wrap_err(validation_error)?; + y_coordinate = head.parse::()?; + let (head, tail) = tail.split_once('x').wrap_err(validation_error)?; + width = head.parse::()?; + height = tail.parse::()?; } else { // this accepts: "%d %d %d %d" - let (head, tail) = tail.split_once(' ')?; - x_coordinate = head.parse::().ok()?; - let (head, tail) = tail.split_once(' ')?; - y_coordinate = head.parse::().ok()?; - let (head, tail) = tail.split_once(' ')?; - width = head.parse::().ok()?; - height = tail.parse::().ok()?; + let (head, tail) = tail.split_once(' ').wrap_err(validation_error)?; + x_coordinate = head.parse::()?; + let (head, tail) = tail.split_once(' ').wrap_err(validation_error)?; + y_coordinate = head.parse::()?; + let (head, tail) = tail.split_once(' ').wrap_err(validation_error)?; + width = head.parse::()?; + height = tail.parse::()?; } - Some(CaptureRegion { - x_coordinate, - y_coordinate, - width, - height, + Ok(LogicalRegion { + inner: Region { + position: Position { + x: x_coordinate, + y: y_coordinate, + }, + size: Size { width, height }, + }, }) } /// Supported image encoding formats. -#[derive(Debug, Copy, Clone, PartialEq, Eq)] +#[derive(Debug, Copy, Clone, PartialEq, Eq, ValueEnum)] pub enum EncodingFormat { - /// Jpeg / jpg encoder. + /// JPG/JPEG encoder. Jpg, - /// Png encoder. + /// PNG encoder. Png, - /// Ppm encoder. + /// PPM encoder. Ppm, - /// Qoi encoder. + /// Qut encoder. Qoi, + /// WebP encoder, + Webp, +} + +impl Default for EncodingFormat { + fn default() -> Self { + Self::Png + } } impl From for image::ImageOutputFormat { @@ -60,10 +75,38 @@ impl From for image::ImageOutputFormat { EncodingFormat::Png => image::ImageFormat::Png.into(), EncodingFormat::Ppm => image::ImageFormat::Pnm.into(), EncodingFormat::Qoi => image::ImageFormat::Qoi.into(), + EncodingFormat::Webp => image::ImageFormat::WebP.into(), } } } +impl TryFrom<&PathBuf> for EncodingFormat { + type Error = Error; + + fn try_from(value: &PathBuf) -> std::result::Result { + value + .extension() + .wrap_err_with(|| { + format!( + "no extension in {} to deduce encoding format", + value.display() + ) + }) + .and_then(|ext| { + ext.to_str().wrap_err_with(|| { + format!("extension in {} is not valid unicode", value.display()) + }) + }) + .and_then(|ext| ext.parse()) + } +} + +impl Display for EncodingFormat { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", Into::<&str>::into(*self)) + } +} + impl From for &str { fn from(format: EncodingFormat) -> Self { match format { @@ -71,18 +114,29 @@ impl From for &str { EncodingFormat::Png => "png", EncodingFormat::Ppm => "ppm", EncodingFormat::Qoi => "qoi", + EncodingFormat::Webp => "webp", } } } -pub fn get_default_file_name(extension: EncodingFormat) -> String { - let time = match SystemTime::now().duration_since(UNIX_EPOCH) { - Ok(n) => n.as_secs().to_string(), - Err(_) => { - tracing::error!("SystemTime before UNIX EPOCH!"); - exit(1); - } - }; +impl FromStr for EncodingFormat { + type Err = Error; + + fn from_str(s: &str) -> std::result::Result { + Ok(match s { + "jpg" | "jpeg" => Self::Jpg, + "png" => Self::Png, + "ppm" => Self::Ppm, + "qoi" => Self::Qoi, + "webp" => Self::Webp, + _ => bail!("unsupported extension '{s}'"), + }) + } +} + +pub fn get_default_file_name(extension: EncodingFormat) -> PathBuf { + let current_datetime: DateTime = Local::now(); + let formated_time = format!("{}", current_datetime.format("%Y_%m_%d-%H_%M_%S")); - time + "-wayshot." + extension.into() + format!("wayshot-{formated_time}.{extension}").into() } diff --git a/wayshot/src/wayshot.rs b/wayshot/src/wayshot.rs index b4b3c01f..73f524d7 100644 --- a/wayshot/src/wayshot.rs +++ b/wayshot/src/wayshot.rs @@ -1,18 +1,21 @@ use std::{ - error::Error, io::{stdout, BufWriter, Cursor, Write}, - process::exit, + process::Command, }; -use libwayshot::WayshotConnection; +use clap::Parser; +use eyre::{bail, Result}; +use libwayshot::{region::LogicalRegion, WayshotConnection}; -mod clap; +mod cli; mod utils; use dialoguer::{theme::ColorfulTheme, FuzzySelect}; -use tracing::Level; +use utils::EncodingFormat; -use crate::utils::EncodingFormat; +use wl_clipboard_rs::copy::{MimeType, Options, Source}; + +use rustix::runtime::{fork, Fork}; fn select_ouput(ouputs: &[T]) -> Option where @@ -29,104 +32,152 @@ where Some(selection) } -fn main() -> Result<(), Box> { - let args = clap::set_flags().get_matches(); - let level = if args.get_flag("debug") { - Level::TRACE - } else { - Level::INFO - }; +fn main() -> Result<()> { + let cli = cli::Cli::parse(); tracing_subscriber::fmt() - .with_max_level(level) + .with_max_level(cli.log_level) .with_writer(std::io::stderr) .init(); - let extension = if let Some(extension) = args.get_one::("extension") { - let ext = extension.trim().to_lowercase(); - tracing::debug!("Using custom extension: {:#?}", ext); - - match ext.as_str() { - "jpeg" | "jpg" => EncodingFormat::Jpg, - "png" => EncodingFormat::Png, - "ppm" => EncodingFormat::Ppm, - "qoi" => EncodingFormat::Qoi, - _ => { - tracing::error!("Invalid extension provided.\nValid extensions:\n1) jpeg\n2) jpg\n3) png\n4) ppm\n5) qoi"); - exit(1); - } + let input_encoding = cli + .file + .as_ref() + .and_then(|pathbuf| pathbuf.try_into().ok()); + let requested_encoding = cli + .encoding + .or(input_encoding) + .unwrap_or(EncodingFormat::default()); + + if let Some(input_encoding) = input_encoding { + if input_encoding != requested_encoding { + tracing::warn!( + "The encoding requested '{requested_encoding}' does not match the output file's encoding '{input_encoding}'. Still using the requested encoding however.", + ); } - } else { - EncodingFormat::Png - }; - - let mut file_is_stdout: bool = false; - let mut file_path: Option = None; - - if args.get_flag("stdout") { - file_is_stdout = true; - } else if let Some(filepath) = args.get_one::("file") { - file_path = Some(filepath.trim().to_string()); - } else { - file_path = Some(utils::get_default_file_name(extension)); } let wayshot_conn = WayshotConnection::new()?; - if args.get_flag("listoutputs") { + if cli.list_outputs { let valid_outputs = wayshot_conn.get_all_outputs(); for output in valid_outputs { tracing::info!("{:#?}", output.name); } - exit(1); + return Ok(()); } - let mut cursor_overlay = false; - if args.get_flag("cursor") { - cursor_overlay = true; - } - - let image_buffer = if let Some(slurp_region) = args.get_one::("slurp") { - if let Some(region) = utils::parse_geometry(slurp_region) { - wayshot_conn.screenshot(region, cursor_overlay)? - } else { - tracing::error!("Invalid geometry specification"); - exit(1); - } - } else if let Some(output_name) = args.get_one::("output") { + let image_buffer = if let Some(slurp_args) = cli.slurp { + let slurp_region = slurp_args.unwrap_or("".to_string()); + wayshot_conn.screenshot_freeze( + Box::new(move || { + || -> Result { + let slurp_output = Command::new("slurp") + .args(slurp_region.split(' ')) + .output()? + .stdout; + + utils::parse_geometry(&String::from_utf8(slurp_output)?) + }() + .map_err(|_| libwayshot::Error::FreezeCallbackError) + }), + cli.cursor, + )? + } else if let Some(output_name) = cli.output { let outputs = wayshot_conn.get_all_outputs(); - if let Some(output) = outputs.iter().find(|output| &output.name == output_name) { - wayshot_conn.screenshot_single_output(output, cursor_overlay)? + if let Some(output) = outputs.iter().find(|output| output.name == output_name) { + wayshot_conn.screenshot_single_output(output, cli.cursor)? } else { - tracing::error!("No output found!\n"); - exit(1); + bail!("No output found!"); } - } else if args.get_flag("chooseoutput") { + } else if cli.choose_output { let outputs = wayshot_conn.get_all_outputs(); - let output_names: Vec = outputs + let output_names: Vec<&str> = outputs .iter() - .map(|display| display.name.to_string()) + .map(|display| display.name.as_str()) .collect(); if let Some(index) = select_ouput(&output_names) { - wayshot_conn.screenshot_single_output(&outputs[index], cursor_overlay)? + wayshot_conn.screenshot_single_output(&outputs[index], cli.cursor)? } else { - tracing::error!("No output found!\n"); - exit(1); + bail!("No output found!"); } } else { - wayshot_conn.screenshot_all(cursor_overlay)? + wayshot_conn.screenshot_all(cli.cursor)? }; - if file_is_stdout { - let stdout = stdout(); - let mut buffer = Cursor::new(Vec::new()); + let mut stdout_print = false; + let file = match cli.file { + Some(mut pathbuf) => { + if pathbuf.to_string_lossy() == "-" { + stdout_print = true; + None + } else { + if pathbuf.is_dir() { + pathbuf.push(utils::get_default_file_name(requested_encoding)); + } + Some(pathbuf) + } + } + None => { + if cli.clipboard { + None + } else { + Some(utils::get_default_file_name(requested_encoding)) + } + } + }; + let mut image_buf: Option>> = None; + if let Some(file) = file { + image_buffer.save(file)?; + } else if stdout_print { + let mut buffer = Cursor::new(Vec::new()); + image_buffer.write_to(&mut buffer, requested_encoding)?; + let stdout = stdout(); let mut writer = BufWriter::new(stdout.lock()); - image_buffer.write_to(&mut buffer, extension)?; - writer.write_all(buffer.get_ref())?; - } else { - image_buffer.save(file_path.unwrap())?; + image_buf = Some(buffer); } + if cli.clipboard { + clipboard_daemonize(match image_buf { + Some(buf) => buf, + None => { + let mut buffer = Cursor::new(Vec::new()); + image_buffer.write_to(&mut buffer, requested_encoding)?; + buffer + } + })?; + } + + Ok(()) +} + +/// Daemonize and copy the given buffer containing the encoded image to the clipboard +fn clipboard_daemonize(buffer: Cursor>) -> Result<()> { + let mut opts = Options::new(); + match unsafe { fork() } { + // Having the image persistently available on the clipboard requires a wayshot process to be alive. + // Fork the process with a child detached from the main process and have the parent exit + Ok(Fork::Parent(_)) => { + return Ok(()); + } + Ok(Fork::Child(_)) => { + opts.foreground(true); // Offer the image till something else is available on the clipboard + opts.copy( + Source::Bytes(buffer.into_inner().into()), + MimeType::Autodetect, + )?; + } + Err(e) => { + tracing::warn!( + "Fork failed with error: {e}, couldn't offer image on the clipboard persistently. + Use a clipboard manager to record screenshot." + ); + opts.copy( + Source::Bytes(buffer.into_inner().into()), + MimeType::Autodetect, + )?; + } + } Ok(()) }