commit 1a50f2bf11469522b97fdcd6b30df112b0330869 Author: entailz Date: Thu Apr 9 14:04:52 2026 -0700 init diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2f7896d --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +target/ diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..0a7f9c7 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,5777 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "ab_glyph" +version = "0.2.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01c0457472c38ea5bd1c3b5ada5e368271cb550be7a4ca4a0b4634e9913f6cc2" +dependencies = [ + "ab_glyph_rasterizer", + "owned_ttf_parser", +] + +[[package]] +name = "ab_glyph_rasterizer" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "366ffbaa4442f4684d91e2cd7c5ea7c4ed8add41959a31447066e279e432b618" + +[[package]] +name = "accesskit" +version = "0.16.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99b76d84ee70e30a4a7e39ab9018e2b17a6a09e31084176cc7c0b2dec036ba45" + +[[package]] +name = "accesskit_atspi_common" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5393c75d4666f580f4cac0a968bc97c36076bb536a129f28210dac54ee127ed" +dependencies = [ + "accesskit", + "accesskit_consumer", + "atspi-common", + "serde", + "thiserror 1.0.69", + "zvariant 4.2.0", +] + +[[package]] +name = "accesskit_consumer" +version = "0.24.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a12dc159d52233c43d9fe5415969433cbdd52c3d6e0df51bda7d447427b9986" +dependencies = [ + "accesskit", + "immutable-chunkmap", +] + +[[package]] +name = "accesskit_macos" +version = "0.17.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfc6c1ecd82053d127961ad80a8beaa6004fb851a3a5b96506d7a6bd462403f6" +dependencies = [ + "accesskit", + "accesskit_consumer", + "objc2 0.5.2", + "objc2-app-kit 0.2.2", + "objc2-foundation 0.2.2", + "once_cell", +] + +[[package]] +name = "accesskit_unix" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be7f5cf6165be10a54b2655fa2e0e12b2509f38ed6fc43e11c31fdb7ee6230bb" +dependencies = [ + "accesskit", + "accesskit_atspi_common", + "async-channel", + "async-executor", + "async-task", + "atspi", + "futures-lite", + "futures-util", + "serde", + "zbus 4.4.0", +] + +[[package]] +name = "accesskit_windows" +version = "0.23.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "974e96c347384d9133427167fb8a58c340cb0496988dacceebdc1ed27071023b" +dependencies = [ + "accesskit", + "accesskit_consumer", + "paste", + "static_assertions", + "windows 0.58.0", + "windows-core 0.58.0", +] + +[[package]] +name = "accesskit_winit" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aea3522719f1c44564d03e9469a8e2f3a98b3a8a880bd66d0789c6b9c4a669dd" +dependencies = [ + "accesskit", + "accesskit_macos", + "accesskit_unix", + "accesskit_windows", + "raw-window-handle", + "winit", +] + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "getrandom 0.3.4", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "aligned" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee4508988c62edf04abd8d92897fca0c2995d907ce1dfeaf369dac3716a40685" +dependencies = [ + "as-slice", +] + +[[package]] +name = "aligned-vec" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc890384c8602f339876ded803c97ad529f3842aba97f6392b3dba0dd171769b" +dependencies = [ + "equator", +] + +[[package]] +name = "android-activity" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef6978589202a00cd7e118380c448a08b6ed394c3a8df3a430d0898e3a42d046" +dependencies = [ + "android-properties", + "bitflags 2.11.0", + "cc", + "cesu8", + "jni", + "jni-sys", + "libc", + "log", + "ndk", + "ndk-context", + "ndk-sys 0.6.0+11769913", + "num_enum", + "thiserror 1.0.69", +] + +[[package]] +name = "android-properties" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7eb209b1518d6bb87b283c20095f5228ecda460da70b44f0802523dea6da04" + +[[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.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" + +[[package]] +name = "anstyle-parse" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" + +[[package]] +name = "arboard" +version = "3.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0348a1c054491f4bfe6ab86a7b6ab1e44e45d899005de92f58b3df180b36ddaf" +dependencies = [ + "clipboard-win", + "log", + "objc2 0.6.4", + "objc2-app-kit 0.3.2", + "objc2-foundation 0.3.2", + "parking_lot", + "percent-encoding", + "windows-sys 0.60.2", + "x11rb", +] + +[[package]] +name = "arg_enum_proc_macro" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "arrayref" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + +[[package]] +name = "as-raw-xcb-connection" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175571dd1d178ced59193a6fc02dde1b972eb0bc56c892cde9beeceac5bf0f6b" + +[[package]] +name = "as-slice" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "516b6b4f0e40d50dcda9365d53964ec74560ad4284da2e7fc97122cd83174516" +dependencies = [ + "stable_deref_trait", +] + +[[package]] +name = "ash" +version = "0.38.0+1.3.281" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bb44936d800fea8f016d7f2311c6a4f97aebd5dc86f09906139ec848cf3a46f" +dependencies = [ + "libloading", +] + +[[package]] +name = "async-broadcast" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532" +dependencies = [ + "event-listener", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-channel" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" +dependencies = [ + "concurrent-queue", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-executor" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c96bf972d85afc50bf5ab8fe2d54d1586b4e0b46c97c50a0c9e71e2f7bcd812a" +dependencies = [ + "async-task", + "concurrent-queue", + "fastrand", + "futures-lite", + "pin-project-lite", + "slab", +] + +[[package]] +name = "async-fs" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8034a681df4aed8b8edbd7fbe472401ecf009251c8b40556b304567052e294c5" +dependencies = [ + "async-lock", + "blocking", + "futures-lite", +] + +[[package]] +name = "async-io" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc" +dependencies = [ + "autocfg", + "cfg-if", + "concurrent-queue", + "futures-io", + "futures-lite", + "parking", + "polling", + "rustix 1.1.4", + "slab", + "windows-sys 0.61.2", +] + +[[package]] +name = "async-lock" +version = "3.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311" +dependencies = [ + "event-listener", + "event-listener-strategy", + "pin-project-lite", +] + +[[package]] +name = "async-process" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75" +dependencies = [ + "async-channel", + "async-io", + "async-lock", + "async-signal", + "async-task", + "blocking", + "cfg-if", + "event-listener", + "futures-lite", + "rustix 1.1.4", +] + +[[package]] +name = "async-recursion" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "async-signal" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43c070bbf59cd3570b6b2dd54cd772527c7c3620fce8be898406dd3ed6adc64c" +dependencies = [ + "async-io", + "async-lock", + "atomic-waker", + "cfg-if", + "futures-core", + "futures-io", + "rustix 1.1.4", + "signal-hook-registry", + "slab", + "windows-sys 0.61.2", +] + +[[package]] +name = "async-task" +version = "4.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" + +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "atspi" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be534b16650e35237bb1ed189ba2aab86ce65e88cc84c66f4935ba38575cecbf" +dependencies = [ + "atspi-common", + "atspi-connection", + "atspi-proxies", +] + +[[package]] +name = "atspi-common" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1909ed2dc01d0a17505d89311d192518507e8a056a48148e3598fef5e7bb6ba7" +dependencies = [ + "enumflags2", + "serde", + "static_assertions", + "zbus 4.4.0", + "zbus-lockstep", + "zbus-lockstep-macros", + "zbus_names 3.0.0", + "zvariant 4.2.0", +] + +[[package]] +name = "atspi-connection" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "430c5960624a4baaa511c9c0fcc2218e3b58f5dbcc47e6190cafee344b873333" +dependencies = [ + "atspi-common", + "atspi-proxies", + "futures-lite", + "zbus 4.4.0", +] + +[[package]] +name = "atspi-proxies" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e6c5de3e524cf967569722446bcd458d5032348554d9a17d7d72b041ab7496" +dependencies = [ + "atspi-common", + "serde", + "zbus 4.4.0", + "zvariant 4.2.0", +] + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "av-scenechange" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f321d77c20e19b92c39e7471cf986812cbb46659d2af674adc4331ef3f18394" +dependencies = [ + "aligned", + "anyhow", + "arg_enum_proc_macro", + "arrayvec", + "log", + "num-rational", + "num-traits", + "pastey", + "rayon", + "thiserror 2.0.18", + "v_frame", + "y4m", +] + +[[package]] +name = "av1-grain" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cfddb07216410377231960af4fcab838eaa12e013417781b78bd95ee22077f8" +dependencies = [ + "anyhow", + "arrayvec", + "log", + "nom", + "num-rational", + "v_frame", +] + +[[package]] +name = "avif-serialize" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "375082f007bd67184fb9c0374614b29f9aaa604ec301635f72338bb65386a53d" +dependencies = [ + "arrayvec", +] + +[[package]] +name = "bit-set" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0481a0e032742109b1133a095184ee93d88f3dc9e0d28a5d033dc77a073f44f" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2c54ff287cfc0a34f38a6b832ea1bd8e448a330b3e40a50859e6488bee07f22" + +[[package]] +name = "bit_field" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e4b40c7323adcfc0a41c4b88143ed58346ff65a288fc144329c5c45e05d70c6" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" + +[[package]] +name = "bitstream-io" +version = "4.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60d4bd9d1db2c6bdf285e223a7fa369d5ce98ec767dec949c6ca62863ce61757" +dependencies = [ + "core2", +] + +[[package]] +name = "blast" +version = "0.1.0" +dependencies = [ + "ab_glyph", + "anyhow", + "clap", + "dirs", + "eframe", + "egui", + "egui_extras", + "gbm", + "image", + "libc", + "memmap2", + "notify-rust", + "png 0.17.16", + "rand 0.8.5", + "serde", + "serde_json", + "tempfile", + "thiserror 1.0.69", + "wayland-backend", + "wayland-client", + "wayland-protocols 0.31.2", + "wayland-protocols-wlr 0.2.0", + "wayland-scanner", + "wl-clipboard-rs", +] + +[[package]] +name = "block" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block2" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c132eebf10f5cad5289222520a4a058514204aed6d791f1cf4fe8088b82d15f" +dependencies = [ + "objc2 0.5.2", +] + +[[package]] +name = "block2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" +dependencies = [ + "objc2 0.6.4", +] + +[[package]] +name = "blocking" +version = "1.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21" +dependencies = [ + "async-channel", + "async-task", + "futures-io", + "futures-lite", + "piper", +] + +[[package]] +name = "built" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4ad8f11f288f48ca24471bbd51ac257aaeaaa07adae295591266b792902ae64" + +[[package]] +name = "bumpalo" +version = "3.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" + +[[package]] +name = "bytemuck" +version = "1.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" +dependencies = [ + "bytemuck_derive", +] + +[[package]] +name = "bytemuck_derive" +version = "1.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9abbd1bc6865053c427f7198e6af43bfdedc55ab791faed4fbd361d789575ff" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "byteorder-lite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" + +[[package]] +name = "calloop" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b99da2f8558ca23c71f4fd15dc57c906239752dd27ff3c00a1d56b685b7cbfec" +dependencies = [ + "bitflags 2.11.0", + "log", + "polling", + "rustix 0.38.44", + "slab", + "thiserror 1.0.69", +] + +[[package]] +name = "calloop" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4dbf9978365bac10f54d1d4b04f7ce4427e51f71d61f2fe15e3fed5166474df7" +dependencies = [ + "bitflags 2.11.0", + "polling", + "rustix 1.1.4", + "slab", + "tracing", +] + +[[package]] +name = "calloop-wayland-source" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95a66a987056935f7efce4ab5668920b5d0dac4a7c99991a67395f13702ddd20" +dependencies = [ + "calloop 0.13.0", + "rustix 0.38.44", + "wayland-backend", + "wayland-client", +] + +[[package]] +name = "calloop-wayland-source" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "138efcf0940a02ebf0cc8d1eff41a1682a46b431630f4c52450d6265876021fa" +dependencies = [ + "calloop 0.14.4", + "rustix 1.1.4", + "wayland-backend", + "wayland-client", +] + +[[package]] +name = "cc" +version = "1.2.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cfg_aliases" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "cgl" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ced0551234e87afee12411d535648dd89d2e7f34c78b753395567aff3d447ff" +dependencies = [ + "libc", +] + +[[package]] +name = "clap" +version = "4.5.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2797f34da339ce31042b27d23607e051786132987f595b02ba4f6a6dffb7030a" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24a241312cea5059b13574bb9b3861cabf758b879c15190b37b6d6fd63ab6876" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a92793da1a46a5f2a02a6f4c46c6496b28c43638adea8306fcb0caa1634f24e5" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "clap_lex" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831" + +[[package]] +name = "clipboard-win" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bde03770d3df201d4fb868f2c9c59e66a3e4e2bd06692a0fe701e7103c7e84d4" +dependencies = [ + "error-code", +] + +[[package]] +name = "codespan-reporting" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e" +dependencies = [ + "termcolor", + "unicode-width", +] + +[[package]] +name = "color_quant" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" + +[[package]] +name = "colorchoice" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" + +[[package]] +name = "com" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e17887fd17353b65b1b2ef1c526c83e26cd72e74f598a8dc1bee13a48f3d9f6" +dependencies = [ + "com_macros", +] + +[[package]] +name = "com_macros" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d375883580a668c7481ea6631fc1a8863e33cc335bf56bfad8d7e6d4b04b13a5" +dependencies = [ + "com_macros_support", + "proc-macro2", + "syn 1.0.109", +] + +[[package]] +name = "com_macros_support" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad899a1087a9296d5644792d7cb72b8e34c1bec8e7d4fbc002230169a6e8710c" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "core-graphics" +version = "0.23.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c07782be35f9e1140080c6b96f0d44b739e2278479f64e02fdab4e32dfd8b081" +dependencies = [ + "bitflags 1.3.2", + "core-foundation 0.9.4", + "core-graphics-types", + "foreign-types", + "libc", +] + +[[package]] +name = "core-graphics-types" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf" +dependencies = [ + "bitflags 1.3.2", + "core-foundation 0.9.4", + "libc", +] + +[[package]] +name = "core2" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b49ba7ef1ad6107f8824dbe97de947cbaac53c44e7f9756a1fba0d37c1eec505" +dependencies = [ + "memchr", +] + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "cursor-icon" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f27ae1dd37df86211c42e150270f82743308803d90a6f6e6651cd730d5e1732f" + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +dependencies = [ + "powerfmt", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "dirs" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.48.0", +] + +[[package]] +name = "dispatch" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" + +[[package]] +name = "dispatch2" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" +dependencies = [ + "bitflags 2.11.0", + "objc2 0.6.4", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "dlib" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab8ecd87370524b461f8557c119c405552c396ed91fc0a8eec68679eab26f94a" +dependencies = [ + "libloading", +] + +[[package]] +name = "document-features" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" +dependencies = [ + "litrs", +] + +[[package]] +name = "downcast-rs" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" + +[[package]] +name = "dpi" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76" + +[[package]] +name = "drm" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80bc8c5c6c2941f70a55c15f8d9f00f9710ebda3ffda98075f996a0e6c92756f" +dependencies = [ + "bitflags 2.11.0", + "bytemuck", + "drm-ffi", + "drm-fourcc", + "libc", + "rustix 0.38.44", +] + +[[package]] +name = "drm-ffi" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51a91c9b32ac4e8105dec255e849e0d66e27d7c34d184364fb93e469db08f690" +dependencies = [ + "drm-sys", + "rustix 1.1.4", +] + +[[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.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8e1361066d91f5ffccff060a3c3be9c3ecde15be2959c1937595f7a82a9f8" +dependencies = [ + "libc", + "linux-raw-sys 0.9.4", +] + +[[package]] +name = "ecolor" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "775cfde491852059e386c4e1deb4aef381c617dc364184c6f6afee99b87c402b" +dependencies = [ + "bytemuck", + "emath", +] + +[[package]] +name = "eframe" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ac2645a9bf4826eb4e91488b1f17b8eaddeef09396706b2f14066461338e24f" +dependencies = [ + "ahash", + "bytemuck", + "document-features", + "egui", + "egui-wgpu", + "egui-winit", + "egui_glow", + "glow 0.14.2", + "glutin", + "glutin-winit", + "image", + "js-sys", + "log", + "objc2 0.5.2", + "objc2-app-kit 0.2.2", + "objc2-foundation 0.2.2", + "parking_lot", + "percent-encoding", + "raw-window-handle", + "static_assertions", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "web-time", + "winapi", + "windows-sys 0.52.0", + "winit", +] + +[[package]] +name = "egui" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53eafabcce0cb2325a59a98736efe0bf060585b437763f8c476957fb274bb974" +dependencies = [ + "accesskit", + "ahash", + "emath", + "epaint", + "log", + "nohash-hasher", +] + +[[package]] +name = "egui-wgpu" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d00fd5d06d8405397e64a928fa0ef3934b3c30273ea7603e3dc4627b1f7a1a82" +dependencies = [ + "ahash", + "bytemuck", + "document-features", + "egui", + "epaint", + "log", + "thiserror 1.0.69", + "type-map", + "web-time", + "wgpu", + "winit", +] + +[[package]] +name = "egui-winit" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a9c430f4f816340e8e8c1b20eec274186b1be6bc4c7dfc467ed50d57abc36c6" +dependencies = [ + "accesskit_winit", + "ahash", + "arboard", + "egui", + "log", + "raw-window-handle", + "smithay-clipboard", + "web-time", + "webbrowser", + "winit", +] + +[[package]] +name = "egui_extras" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf3c1f5cd8dfe2ade470a218696c66cf556fcfd701e7830fa2e9f4428292a2a1" +dependencies = [ + "ahash", + "egui", + "enum-map", + "image", + "log", + "mime_guess2", +] + +[[package]] +name = "egui_glow" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e39bccc683cd43adab530d8f21a13eb91e80de10bcc38c3f1c16601b6f62b26" +dependencies = [ + "ahash", + "bytemuck", + "egui", + "glow 0.14.2", + "log", + "memoffset", + "wasm-bindgen", + "web-sys", + "winit", +] + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "emath" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1fe0049ce51d0fb414d029e668dd72eb30bc2b739bf34296ed97bd33df544f3" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "endi" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099" + +[[package]] +name = "enum-map" +version = "2.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6866f3bfdf8207509a033af1a75a7b08abda06bbaaeae6669323fd5a097df2e9" +dependencies = [ + "enum-map-derive", + "serde", +] + +[[package]] +name = "enum-map-derive" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f282cfdfe92516eb26c2af8589c274c7c17681f5ecc03c18255fe741c6aa64eb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "enumflags2" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef" +dependencies = [ + "enumflags2_derive", + "serde", +] + +[[package]] +name = "enumflags2_derive" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "epaint" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a32af8da821bd4f43f2c137e295459ee2e1661d87ca8779dfa0eaf45d870e20f" +dependencies = [ + "ab_glyph", + "ahash", + "bytemuck", + "ecolor", + "emath", + "epaint_default_fonts", + "log", + "nohash-hasher", + "parking_lot", +] + +[[package]] +name = "epaint_default_fonts" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "483440db0b7993cf77a20314f08311dbe95675092405518c0677aa08c151a3ea" + +[[package]] +name = "equator" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4711b213838dfee0117e3be6ac926007d7f433d7bbe33595975d4190cb07e6fc" +dependencies = [ + "equator-macro", +] + +[[package]] +name = "equator-macro" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44f23cf4b44bfce11a86ace86f8a73ffdec849c9fd00a386a53d278bd9e81fb3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "error-code" +version = "3.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dea2df4cf52843e0452895c455a1a2cfbb842a1e7329671acf418fdc53ed4c59" + +[[package]] +name = "event-listener" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" +dependencies = [ + "event-listener", + "pin-project-lite", +] + +[[package]] +name = "exr" +version = "1.74.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4300e043a56aa2cb633c01af81ca8f699a321879a7854d3896a0ba89056363be" +dependencies = [ + "bit_field", + "half", + "lebe", + "miniz_oxide", + "rayon-core", + "smallvec", + "zune-inflate", +] + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "fax" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f05de7d48f37cd6730705cbca900770cab77a89f413d23e100ad7fad7795a0ab" +dependencies = [ + "fax_derive", +] + +[[package]] +name = "fax_derive" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0aca10fb742cb43f9e7bb8467c91aa9bcb8e3ffbc6a6f7389bb93ffc920577d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "fdeflate" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "fixedbitset" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" + +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "foreign-types" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" +dependencies = [ + "foreign-types-macros", + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-macros" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "foreign-types-shared" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[package]] +name = "futures-lite" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "parking", + "pin-project-lite", +] + +[[package]] +name = "futures-macro" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "gbm" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce852e998d3ca5e4a97014fb31c940dc5ef344ec7d364984525fd11e8a547e6a" +dependencies = [ + "bitflags 2.11.0", + "drm", + "drm-fourcc", + "gbm-sys", + "libc", + "wayland-backend", + "wayland-server", +] + +[[package]] +name = "gbm-sys" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c13a5f2acc785d8fb6bf6b7ab6bfb0ef5dad4f4d97e8e70bb8e470722312f76f" +dependencies = [ + "libc", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "gethostname" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bd49230192a3797a9a4d6abe9b3eed6f7fa4c8a8a4947977c6f80025f92cbd8" +dependencies = [ + "rustix 1.1.4", + "windows-link 0.2.1", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi 5.3.0", + "wasip2", +] + +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", + "wasip2", + "wasip3", +] + +[[package]] +name = "gif" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5df2ba84018d80c213569363bdcd0c64e6933c67fe4c1d60ecf822971a3c35e" +dependencies = [ + "color_quant", + "weezl", +] + +[[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 = "glow" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd348e04c43b32574f2de31c8bb397d96c9fcfa1371bd4ca6d8bdc464ab121b1" +dependencies = [ + "js-sys", + "slotmap", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "glow" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d51fa363f025f5c111e03f13eda21162faeacb6911fe8caa0c0349f9cf0c4483" +dependencies = [ + "js-sys", + "slotmap", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "glutin" +version = "0.32.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12124de845cacfebedff80e877bb37b5b75c34c5a4c89e47e1cdd67fb6041325" +dependencies = [ + "bitflags 2.11.0", + "cfg_aliases 0.2.1", + "cgl", + "dispatch2", + "glutin_egl_sys", + "glutin_glx_sys", + "glutin_wgl_sys", + "libloading", + "objc2 0.6.4", + "objc2-app-kit 0.3.2", + "objc2-core-foundation", + "objc2-foundation 0.3.2", + "once_cell", + "raw-window-handle", + "wayland-sys", + "windows-sys 0.52.0", + "x11-dl", +] + +[[package]] +name = "glutin-winit" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85edca7075f8fc728f28cb8fbb111a96c3b89e930574369e3e9c27eb75d3788f" +dependencies = [ + "cfg_aliases 0.2.1", + "glutin", + "raw-window-handle", + "winit", +] + +[[package]] +name = "glutin_egl_sys" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c4680ba6195f424febdc3ba46e7a42a0e58743f2edb115297b86d7f8ecc02d2" +dependencies = [ + "gl_generator", + "windows-sys 0.52.0", +] + +[[package]] +name = "glutin_glx_sys" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7bb2938045a88b612499fbcba375a77198e01306f52272e692f8c1f3751185" +dependencies = [ + "gl_generator", + "x11-dl", +] + +[[package]] +name = "glutin_wgl_sys" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c4ee00b289aba7a9e5306d57c2d05499b2e5dc427f84ac708bd2c090212cf3e" +dependencies = [ + "gl_generator", +] + +[[package]] +name = "gpu-alloc" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbcd2dba93594b227a1f57ee09b8b9da8892c34d55aa332e034a228d0fe6a171" +dependencies = [ + "bitflags 2.11.0", + "gpu-alloc-types", +] + +[[package]] +name = "gpu-alloc-types" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98ff03b468aa837d70984d55f5d3f846f6ec31fe34bbb97c4f85219caeee1ca4" +dependencies = [ + "bitflags 2.11.0", +] + +[[package]] +name = "gpu-allocator" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdd4240fc91d3433d5e5b0fc5b67672d771850dc19bbee03c1381e19322803d7" +dependencies = [ + "log", + "presser", + "thiserror 1.0.69", + "winapi", + "windows 0.52.0", +] + +[[package]] +name = "gpu-descriptor" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b89c83349105e3732062a895becfc71a8f921bb71ecbbdd8ff99263e3b53a0ca" +dependencies = [ + "bitflags 2.11.0", + "gpu-descriptor-types", + "hashbrown 0.15.5", +] + +[[package]] +name = "gpu-descriptor-types" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdf242682df893b86f33a73828fb09ca4b2d3bb6cc95249707fc684d27484b91" +dependencies = [ + "bitflags 2.11.0", +] + +[[package]] +name = "half" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" +dependencies = [ + "cfg-if", + "crunchy", + "zerocopy", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + +[[package]] +name = "hassle-rs" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af2a7e73e1f34c48da31fb668a907f250794837e08faa144fd24f0b8b741e890" +dependencies = [ + "bitflags 2.11.0", + "com", + "libc", + "libloading", + "thiserror 1.0.69", + "widestring", + "winapi", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hexf-parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df" + +[[package]] +name = "icu_collections" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" + +[[package]] +name = "icu_properties" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" + +[[package]] +name = "icu_provider" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "image" +version = "0.25.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6506c6c10786659413faa717ceebcb8f70731c0a60cbae39795fdf114519c1a" +dependencies = [ + "bytemuck", + "byteorder-lite", + "color_quant", + "exr", + "gif", + "image-webp", + "moxcms", + "num-traits", + "png 0.18.1", + "qoi", + "ravif", + "rayon", + "rgb", + "tiff", + "zune-core 0.5.1", + "zune-jpeg 0.5.12", +] + +[[package]] +name = "image-webp" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "525e9ff3e1a4be2fbea1fdf0e98686a6d98b4d8f937e1bf7402245af1909e8c3" +dependencies = [ + "byteorder-lite", + "quick-error", +] + +[[package]] +name = "imgref" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c5cedc30da3a610cac6b4ba17597bdf7152cf974e8aab3afb3d54455e371c8" + +[[package]] +name = "immutable-chunkmap" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a3e98b1520e49e252237edc238a39869da9f3241f2ec19dc788c1d24694d1e4" +dependencies = [ + "arrayvec", +] + +[[package]] +name = "indexmap" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +dependencies = [ + "equivalent", + "hashbrown 0.16.1", + "serde", + "serde_core", +] + +[[package]] +name = "interpolate_name" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" + +[[package]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if", + "combine", + "jni-sys", + "log", + "thiserror 1.0.69", + "walkdir", + "windows-sys 0.45.0", +] + +[[package]] +name = "jni-sys" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" + +[[package]] +name = "jobserver" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +dependencies = [ + "getrandom 0.3.4", + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "khronos-egl" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6aae1df220ece3c0ada96b8153459b67eebe9ae9212258bb0134ae60416fdf76" +dependencies = [ + "libc", + "libloading", + "pkg-config", +] + +[[package]] +name = "khronos_api" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc" + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "lebe" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a79a3332a6609480d7d0c9eab957bca6b455b91bb84e66d19f5ff66294b85b8" + +[[package]] +name = "libc" +version = "0.2.182" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112" + +[[package]] +name = "libfuzzer-sys" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f12a681b7dd8ce12bff52488013ba614b869148d54dd79836ab85aafdd53f08d" +dependencies = [ + "arbitrary", + "cc", +] + +[[package]] +name = "libloading" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" +dependencies = [ + "cfg-if", + "windows-link 0.2.1", +] + +[[package]] +name = "libredox" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1744e39d1d6a9948f4f388969627434e31128196de472883b39f148769bfe30a" +dependencies = [ + "bitflags 2.11.0", + "libc", + "plain", + "redox_syscall 0.7.3", +] + +[[package]] +name = "linux-raw-sys" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + +[[package]] +name = "linux-raw-sys" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "litemap" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" + +[[package]] +name = "litrs" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "loop9" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fae87c125b03c1d2c0150c90365d7d6bcc53fb73a9acaef207d2d065860f062" +dependencies = [ + "imgref", +] + +[[package]] +name = "mac-notification-sys" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65fd3f75411f4725061682ed91f131946e912859d0044d39c4ec0aac818d7621" +dependencies = [ + "cc", + "objc2 0.6.4", + "objc2-foundation 0.3.2", + "time", +] + +[[package]] +name = "malloc_buf" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" +dependencies = [ + "libc", +] + +[[package]] +name = "maybe-rayon" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ea1f30cedd69f0a2954655f7188c6a834246d2bcf1e315e2ac40c4b24dc9519" +dependencies = [ + "cfg-if", + "rayon", +] + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "memmap2" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "714098028fe011992e1c3962653c96b2d578c4b4bce9036e15ff220319b1e0e3" +dependencies = [ + "libc", +] + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "metal" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ecfd3296f8c56b7c1f6fbac3c71cefa9d78ce009850c45000015f206dc7fa21" +dependencies = [ + "bitflags 2.11.0", + "block", + "core-graphics-types", + "foreign-types", + "log", + "objc", + "paste", +] + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "mime_guess2" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1706dc14a2e140dec0a7a07109d9a3d5890b81e85bd6c60b906b249a77adf0ca" +dependencies = [ + "mime", + "phf", + "phf_shared", + "unicase", +] + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "moxcms" +version = "0.7.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac9557c559cd6fc9867e122e20d2cbefc9ca29d80d027a8e39310920ed2f0a97" +dependencies = [ + "num-traits", + "pxfm", +] + +[[package]] +name = "naga" +version = "22.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8bd5a652b6faf21496f2cfd88fc49989c8db0825d1f6746b1a71a6ede24a63ad" +dependencies = [ + "arrayvec", + "bit-set", + "bitflags 2.11.0", + "cfg_aliases 0.1.1", + "codespan-reporting", + "hexf-parse", + "indexmap", + "log", + "rustc-hash 1.1.0", + "spirv", + "termcolor", + "thiserror 1.0.69", + "unicode-xid", +] + +[[package]] +name = "ndk" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" +dependencies = [ + "bitflags 2.11.0", + "jni-sys", + "log", + "ndk-sys 0.6.0+11769913", + "num_enum", + "raw-window-handle", + "thiserror 1.0.69", +] + +[[package]] +name = "ndk-context" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" + +[[package]] +name = "ndk-sys" +version = "0.5.0+25.2.9519653" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c196769dd60fd4f363e11d948139556a344e79d451aeb2fa2fd040738ef7691" +dependencies = [ + "jni-sys", +] + +[[package]] +name = "ndk-sys" +version = "0.6.0+11769913" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873" +dependencies = [ + "jni-sys", +] + +[[package]] +name = "new_debug_unreachable" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" + +[[package]] +name = "nix" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" +dependencies = [ + "bitflags 2.11.0", + "cfg-if", + "cfg_aliases 0.2.1", + "libc", + "memoffset", +] + +[[package]] +name = "nohash-hasher" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451" + +[[package]] +name = "nom" +version = "8.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405" +dependencies = [ + "memchr", +] + +[[package]] +name = "noop_proc_macro" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8" + +[[package]] +name = "notify-rust" +version = "4.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21af20a1b50be5ac5861f74af1a863da53a11c38684d9818d82f1c42f7fdc6c2" +dependencies = [ + "futures-lite", + "log", + "mac-notification-sys", + "serde", + "tauri-winrt-notification", + "zbus 5.14.0", +] + +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-conv" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050" + +[[package]] +name = "num-derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_enum" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1207a7e20ad57b847bbddc6776b968420d38292bbfe2089accff5e19e82454c" +dependencies = [ + "num_enum_derive", + "rustversion", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff32365de1b6743cb203b710788263c44a03de03802daf96092f2da4fe6ba4d7" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "objc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" +dependencies = [ + "malloc_buf", +] + +[[package]] +name = "objc-sys" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb91bdd390c7ce1a8607f35f3ca7151b65afc0ff5ff3b34fa350f7d7c7e4310" + +[[package]] +name = "objc2" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46a785d4eeff09c14c487497c162e92766fbb3e4059a71840cecc03d9a50b804" +dependencies = [ + "objc-sys", + "objc2-encode", +] + +[[package]] +name = "objc2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" +dependencies = [ + "objc2-encode", +] + +[[package]] +name = "objc2-app-kit" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4e89ad9e3d7d297152b17d39ed92cd50ca8063a89a9fa569046d41568891eff" +dependencies = [ + "bitflags 2.11.0", + "block2 0.5.1", + "libc", + "objc2 0.5.2", + "objc2-core-data", + "objc2-core-image", + "objc2-foundation 0.2.2", + "objc2-quartz-core", +] + +[[package]] +name = "objc2-app-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" +dependencies = [ + "bitflags 2.11.0", + "objc2 0.6.4", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation 0.3.2", +] + +[[package]] +name = "objc2-cloud-kit" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74dd3b56391c7a0596a295029734d3c1c5e7e510a4cb30245f8221ccea96b009" +dependencies = [ + "bitflags 2.11.0", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-core-location", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-contacts" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5ff520e9c33812fd374d8deecef01d4a840e7b41862d849513de77e44aa4889" +dependencies = [ + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-core-data" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef" +dependencies = [ + "bitflags 2.11.0", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-core-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" +dependencies = [ + "bitflags 2.11.0", + "dispatch2", + "objc2 0.6.4", +] + +[[package]] +name = "objc2-core-graphics" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" +dependencies = [ + "bitflags 2.11.0", + "dispatch2", + "objc2 0.6.4", + "objc2-core-foundation", + "objc2-io-surface", +] + +[[package]] +name = "objc2-core-image" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55260963a527c99f1819c4f8e3b47fe04f9650694ef348ffd2227e8196d34c80" +dependencies = [ + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", + "objc2-metal", +] + +[[package]] +name = "objc2-core-location" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "000cfee34e683244f284252ee206a27953279d370e309649dc3ee317b37e5781" +dependencies = [ + "block2 0.5.1", + "objc2 0.5.2", + "objc2-contacts", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + +[[package]] +name = "objc2-foundation" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" +dependencies = [ + "bitflags 2.11.0", + "block2 0.5.1", + "dispatch", + "libc", + "objc2 0.5.2", +] + +[[package]] +name = "objc2-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" +dependencies = [ + "bitflags 2.11.0", + "block2 0.6.2", + "libc", + "objc2 0.6.4", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-io-surface" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" +dependencies = [ + "bitflags 2.11.0", + "objc2 0.6.4", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-link-presentation" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1a1ae721c5e35be65f01a03b6d2ac13a54cb4fa70d8a5da293d7b0020261398" +dependencies = [ + "block2 0.5.1", + "objc2 0.5.2", + "objc2-app-kit 0.2.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-metal" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6" +dependencies = [ + "bitflags 2.11.0", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-quartz-core" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a" +dependencies = [ + "bitflags 2.11.0", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", + "objc2-metal", +] + +[[package]] +name = "objc2-symbols" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a684efe3dec1b305badae1a28f6555f6ddd3bb2c2267896782858d5a78404dc" +dependencies = [ + "objc2 0.5.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-ui-kit" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8bb46798b20cd6b91cbd113524c490f1686f4c4e8f49502431415f3512e2b6f" +dependencies = [ + "bitflags 2.11.0", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-cloud-kit", + "objc2-core-data", + "objc2-core-image", + "objc2-core-location", + "objc2-foundation 0.2.2", + "objc2-link-presentation", + "objc2-quartz-core", + "objc2-symbols", + "objc2-uniform-type-identifiers", + "objc2-user-notifications", +] + +[[package]] +name = "objc2-uniform-type-identifiers" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44fa5f9748dbfe1ca6c0b79ad20725a11eca7c2218bceb4b005cb1be26273bfe" +dependencies = [ + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-user-notifications" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76cfcbf642358e8689af64cee815d139339f3ed8ad05103ed5eaf73db8d84cb3" +dependencies = [ + "bitflags 2.11.0", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-core-location", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] +name = "orbclient" +version = "0.3.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ad2c6bae700b7aa5d1cc30c59bdd3a1c180b09dbaea51e2ae2b8e1cf211fdd" +dependencies = [ + "libc", + "libredox", +] + +[[package]] +name = "ordered-stream" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50" +dependencies = [ + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "os_pipe" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d8fae84b431384b68627d0f9b3b1245fcf9f46f6c0e3dc902e9dce64edd1967" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "owned_ttf_parser" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36820e9051aca1014ddc75770aab4d68bc1e9e632f0f5627c4086bc216fb583b" +dependencies = [ + "ttf-parser", +] + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall 0.5.18", + "smallvec", + "windows-link 0.2.1", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "pastey" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35fb2e5f958ec131621fdd531e9fc186ed768cbe395337403ae56c17a74c68ec" + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "petgraph" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8701b58ea97060d5e5b155d383a69952a60943f0e6dfe30b04c287beb0b27455" +dependencies = [ + "fixedbitset", + "hashbrown 0.15.5", + "indexmap", +] + +[[package]] +name = "phf" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" +dependencies = [ + "phf_macros", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" +dependencies = [ + "phf_shared", + "rand 0.8.5", +] + +[[package]] +name = "phf_macros" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", + "syn 2.0.117", + "unicase", +] + +[[package]] +name = "phf_shared" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" +dependencies = [ + "siphasher", + "unicase", +] + +[[package]] +name = "pin-project" +version = "1.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1749c7ed4bcaf4c3d0a3efc28538844fb29bcdd7d2b67b2be7e20ba861ff517" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b20ed30f105399776b9c883e68e536ef602a16ae6f596d2c473591d6ad64c6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "piper" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c835479a4443ded371d6c535cbfd8d31ad92c5d23ae9770a61bc155e4992a3c1" +dependencies = [ + "atomic-waker", + "fastrand", + "futures-io", +] + +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "plain" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" + +[[package]] +name = "png" +version = "0.17.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526" +dependencies = [ + "bitflags 1.3.2", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + +[[package]] +name = "png" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" +dependencies = [ + "bitflags 2.11.0", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + +[[package]] +name = "polling" +version = "3.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" +dependencies = [ + "cfg-if", + "concurrent-queue", + "hermit-abi", + "pin-project-lite", + "rustix 1.1.4", + "windows-sys 0.61.2", +] + +[[package]] +name = "potential_utf" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "presser" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8cf8e6a8aa66ce33f63993ffc4ea4271eb5b0530a9002db8455ea6050c77bfa" + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn 2.0.117", +] + +[[package]] +name = "proc-macro-crate" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +dependencies = [ + "toml_edit", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "profiling" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eb8486b569e12e2c32ad3e204dbaba5e4b5b216e9367044f25f1dba42341773" +dependencies = [ + "profiling-procmacros", +] + +[[package]] +name = "profiling-procmacros" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52717f9a02b6965224f95ca2a81e2e0c5c43baacd28ca057577988930b6c3d5b" +dependencies = [ + "quote", + "syn 2.0.117", +] + +[[package]] +name = "pxfm" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5a041e753da8b807c9255f28de81879c78c876392ff2469cde94799b2896b9d" + +[[package]] +name = "qoi" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f6d64c71eb498fe9eae14ce4ec935c555749aef511cca85b5568910d6e48001" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "quick-error" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" + +[[package]] +name = "quick-xml" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eff6510e86862b57b210fd8cbe8ed3f0d7d600b9c2863cd4549a2e033c66e956" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "quick-xml" +version = "0.37.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "331e97a1af0bf59823e6eadffe373d7b27f485be8748f71471c662c1f269b7fb" +dependencies = [ + "memchr", +] + +[[package]] +name = "quick-xml" +version = "0.39.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "958f21e8e7ceb5a1aa7fa87fab28e7c75976e0bfe7e23ff069e0a260f894067d" +dependencies = [ + "memchr", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + +[[package]] +name = "rav1e" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43b6dd56e85d9483277cde964fd1bdb0428de4fec5ebba7540995639a21cb32b" +dependencies = [ + "aligned-vec", + "arbitrary", + "arg_enum_proc_macro", + "arrayvec", + "av-scenechange", + "av1-grain", + "bitstream-io", + "built", + "cfg-if", + "interpolate_name", + "itertools", + "libc", + "libfuzzer-sys", + "log", + "maybe-rayon", + "new_debug_unreachable", + "noop_proc_macro", + "num-derive", + "num-traits", + "paste", + "profiling", + "rand 0.9.2", + "rand_chacha 0.9.0", + "simd_helpers", + "thiserror 2.0.18", + "v_frame", + "wasm-bindgen", +] + +[[package]] +name = "ravif" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef69c1990ceef18a116855938e74793a5f7496ee907562bd0857b6ac734ab285" +dependencies = [ + "avif-serialize", + "imgref", + "loop9", + "quick-error", + "rav1e", + "rayon", + "rgb", +] + +[[package]] +name = "raw-window-handle" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" + +[[package]] +name = "rayon" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[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 = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.11.0", +] + +[[package]] +name = "redox_syscall" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce70a74e890531977d37e532c34d45e9055d2409ed08ddba14529471ed0be16" +dependencies = [ + "bitflags 2.11.0", +] + +[[package]] +name = "redox_users" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" +dependencies = [ + "getrandom 0.2.17", + "libredox", + "thiserror 1.0.69", +] + +[[package]] +name = "renderdoc-sys" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832" + +[[package]] +name = "rgb" +version = "0.8.53" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b34b781b31e5d73e9fbc8689c70551fd1ade9a19e3e28cfec8580a79290cc4" + +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + +[[package]] +name = "rustix" +version = "0.38.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +dependencies = [ + "bitflags 2.11.0", + "errno", + "libc", + "linux-raw-sys 0.4.15", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags 2.11.0", + "errno", + "libc", + "linux-raw-sys 0.12.1", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "scoped-tls" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "sctk-adwaita" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6277f0217056f77f1d8f49f2950ac6c278c0d607c45f5ee99328d792ede24ec" +dependencies = [ + "ab_glyph", + "log", + "memmap2", + "smithay-client-toolkit 0.19.2", + "tiny-skia", +] + +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_repr" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "simd-adler32" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" + +[[package]] +name = "simd_helpers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95890f873bec569a0362c235787f3aca6e1e887302ba4840839bcc6459c42da6" +dependencies = [ + "quote", +] + +[[package]] +name = "siphasher" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "slotmap" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdd58c3c93c3d278ca835519292445cb4b0d4dc59ccfdf7ceadaab3f8aeb4038" +dependencies = [ + "version_check", +] + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "smithay-client-toolkit" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3457dea1f0eb631b4034d61d4d8c32074caa6cd1ab2d59f2327bd8461e2c0016" +dependencies = [ + "bitflags 2.11.0", + "calloop 0.13.0", + "calloop-wayland-source 0.3.0", + "cursor-icon", + "libc", + "log", + "memmap2", + "rustix 0.38.44", + "thiserror 1.0.69", + "wayland-backend", + "wayland-client", + "wayland-csd-frame", + "wayland-cursor", + "wayland-protocols 0.32.11", + "wayland-protocols-wlr 0.3.11", + "wayland-scanner", + "xkeysym", +] + +[[package]] +name = "smithay-client-toolkit" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0512da38f5e2b31201a93524adb8d3136276fa4fe4aafab4e1f727a82b534cc0" +dependencies = [ + "bitflags 2.11.0", + "calloop 0.14.4", + "calloop-wayland-source 0.4.1", + "cursor-icon", + "libc", + "log", + "memmap2", + "rustix 1.1.4", + "thiserror 2.0.18", + "wayland-backend", + "wayland-client", + "wayland-csd-frame", + "wayland-cursor", + "wayland-protocols 0.32.11", + "wayland-protocols-experimental", + "wayland-protocols-misc", + "wayland-protocols-wlr 0.3.11", + "wayland-scanner", + "xkeysym", +] + +[[package]] +name = "smithay-clipboard" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71704c03f739f7745053bde45fa203a46c58d25bc5c4efba1d9a60e9dba81226" +dependencies = [ + "libc", + "smithay-client-toolkit 0.20.0", + "wayland-backend", +] + +[[package]] +name = "smol_str" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd538fb6910ac1099850255cf94a94df6551fbdd602454387d0adb2d1ca6dead" +dependencies = [ + "serde", +] + +[[package]] +name = "spirv" +version = "0.3.0+sdk-1.3.268.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eda41003dc44290527a59b13432d4a0379379fa074b70174882adfbdfd917844" +dependencies = [ + "bitflags 2.11.0", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "strict-num" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "tauri-winrt-notification" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b1e66e07de489fe43a46678dd0b8df65e0c973909df1b60ba33874e297ba9b9" +dependencies = [ + "quick-xml 0.37.5", + "thiserror 2.0.18", + "windows 0.61.3", + "windows-version", +] + +[[package]] +name = "tempfile" +version = "3.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82a72c767771b47409d2345987fda8628641887d5466101319899796367354a0" +dependencies = [ + "fastrand", + "getrandom 0.4.2", + "once_cell", + "rustix 1.1.4", + "windows-sys 0.61.2", +] + +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl 2.0.18", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "tiff" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af9605de7fee8d9551863fd692cce7637f548dbd9db9180fcc07ccc6d26c336f" +dependencies = [ + "fax", + "flate2", + "half", + "quick-error", + "weezl", + "zune-jpeg 0.4.21", +] + +[[package]] +name = "time" +version = "0.3.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" +dependencies = [ + "deranged", + "num-conv", + "powerfmt", + "serde_core", + "time-core", +] + +[[package]] +name = "time-core" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" + +[[package]] +name = "tiny-skia" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83d13394d44dae3207b52a326c0c85a8bf87f1541f23b0d143811088497b09ab" +dependencies = [ + "arrayref", + "arrayvec", + "bytemuck", + "cfg-if", + "log", + "tiny-skia-path", +] + +[[package]] +name = "tiny-skia-path" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c9e7fc0c2e86a30b117d0462aa261b72b7a99b7ebd7deb3a14ceda95c5bdc93" +dependencies = [ + "arrayref", + "bytemuck", + "strict-num", +] + +[[package]] +name = "tinystr" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "toml_datetime" +version = "1.0.0+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32c2555c699578a4f59f0cc68e5116c8d7cabbd45e1409b989d4be085b53f13e" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.25.4+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7193cbd0ce53dc966037f54351dbbcf0d5a642c7f0038c382ef9e677ce8c13f2" +dependencies = [ + "indexmap", + "toml_datetime", + "toml_parser", + "winnow", +] + +[[package]] +name = "toml_parser" +version = "1.0.9+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "702d4415e08923e7e1ef96cd5727c0dfed80b4d2fa25db9647fe5eb6f7c5a4c4" +dependencies = [ + "winnow", +] + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "tree_magic_mini" +version = "3.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8765b90061cba6c22b5831f675da109ae5561588290f9fa2317adab2714d5a6" +dependencies = [ + "memchr", + "nom", + "petgraph", +] + +[[package]] +name = "ttf-parser" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31" + +[[package]] +name = "type-map" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb30dbbd9036155e74adad6812e9898d03ec374946234fbcebd5dfc7b9187b90" +dependencies = [ + "rustc-hash 2.1.1", +] + +[[package]] +name = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + +[[package]] +name = "uds_windows" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51b70b87d15e91f553711b40df3048faf27a7a04e01e0ddc0cf9309f0af7c2ca" +dependencies = [ + "memoffset", + "tempfile", + "windows-sys 0.61.2", +] + +[[package]] +name = "unicase" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + +[[package]] +name = "unicode-width" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "uuid" +version = "1.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b672338555252d43fd2240c714dc444b8c6fb0a5c5335e65a07bba7742735ddb" +dependencies = [ + "js-sys", + "serde_core", + "wasm-bindgen", +] + +[[package]] +name = "v_frame" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "666b7727c8875d6ab5db9533418d7c764233ac9c0cff1d469aec8fa127597be2" +dependencies = [ + "aligned-vec", + "num-traits", + "wasm-bindgen", +] + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.2+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9c5522b3a28661442748e09d40924dfb9ca614b21c00d3fd135720e48b67db8" +dependencies = [ + "cfg-if", + "futures-util", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.117", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags 2.11.0", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + +[[package]] +name = "wayland-backend" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eaa6143502b9a87f759cb6a649ca801a226f77740eb54f3951cba2227790afeb" +dependencies = [ + "cc", + "downcast-rs", + "rustix 1.1.4", + "scoped-tls", + "smallvec", + "wayland-sys", +] + +[[package]] +name = "wayland-client" +version = "0.31.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab51d9f7c071abeee76007e2b742499e535148035bb835f97aaed1338cf516c3" +dependencies = [ + "bitflags 2.11.0", + "rustix 1.1.4", + "wayland-backend", + "wayland-scanner", +] + +[[package]] +name = "wayland-csd-frame" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625c5029dbd43d25e6aa9615e88b829a5cad13b2819c4ae129fdbb7c31ab4c7e" +dependencies = [ + "bitflags 2.11.0", + "cursor-icon", + "wayland-backend", +] + +[[package]] +name = "wayland-cursor" +version = "0.31.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b3298683470fbdc6ca40151dfc48c8f2fd4c41a26e13042f801f85002384091" +dependencies = [ + "rustix 1.1.4", + "wayland-client", + "xcursor", +] + +[[package]] +name = "wayland-protocols" +version = "0.31.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f81f365b8b4a97f422ac0e8737c438024b5951734506b0e1d775c73030561f4" +dependencies = [ + "bitflags 2.11.0", + "wayland-backend", + "wayland-client", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols" +version = "0.32.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b23b5df31ceff1328f06ac607591d5ba360cf58f90c8fad4ac8d3a55a3c4aec7" +dependencies = [ + "bitflags 2.11.0", + "wayland-backend", + "wayland-client", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-experimental" +version = "20250721.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40a1f863128dcaaec790d7b4b396cc9b9a7a079e878e18c47e6c2d2c5a8dcbb1" +dependencies = [ + "bitflags 2.11.0", + "wayland-backend", + "wayland-client", + "wayland-protocols 0.32.11", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-misc" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "429b99200febaf95d4f4e46deff6fe4382bcff3280ee16a41cf887b3c3364984" +dependencies = [ + "bitflags 2.11.0", + "wayland-backend", + "wayland-client", + "wayland-protocols 0.32.11", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-plasma" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d392fc283a87774afc9beefcd6f931582bb97fe0e6ced0b306a62cb1d026527c" +dependencies = [ + "bitflags 2.11.0", + "wayland-backend", + "wayland-client", + "wayland-protocols 0.32.11", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-wlr" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad1f61b76b6c2d8742e10f9ba5c3737f6530b4c243132c2a2ccc8aa96fe25cd6" +dependencies = [ + "bitflags 2.11.0", + "wayland-backend", + "wayland-client", + "wayland-protocols 0.31.2", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-wlr" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78248e4cc0eff8163370ba5c158630dcae1f3497a586b826eca2ef5f348d6235" +dependencies = [ + "bitflags 2.11.0", + "wayland-backend", + "wayland-client", + "wayland-protocols 0.32.11", + "wayland-scanner", +] + +[[package]] +name = "wayland-scanner" +version = "0.31.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c86287151a309799b821ca709b7345a048a2956af05957c89cb824ab919fa4e3" +dependencies = [ + "proc-macro2", + "quick-xml 0.39.2", + "quote", +] + +[[package]] +name = "wayland-server" +version = "0.31.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63736a4a73e781cf6a736aa32c5d6773c3eb5389197562742a8c611b49b5e359" +dependencies = [ + "bitflags 2.11.0", + "downcast-rs", + "rustix 1.1.4", + "wayland-backend", + "wayland-scanner", +] + +[[package]] +name = "wayland-sys" +version = "0.31.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81d2bd69b1dadd601d0e98ef2fc9339a1b1e00cec5ee7545a77b5a0f52a90394" +dependencies = [ + "dlib", + "libc", + "log", + "memoffset", + "once_cell", + "pkg-config", +] + +[[package]] +name = "web-sys" +version = "0.3.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "854ba17bb104abfb26ba36da9729addc7ce7f06f5c0f90f3c391f8461cca21f9" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webbrowser" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f00bb839c1cf1e3036066614cbdcd035ecf215206691ea646aa3c60a24f68f2" +dependencies = [ + "core-foundation 0.10.1", + "jni", + "log", + "ndk-context", + "objc2 0.6.4", + "objc2-foundation 0.3.2", + "url", + "web-sys", +] + +[[package]] +name = "weezl" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88" + +[[package]] +name = "wgpu" +version = "22.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1d1c4ba43f80542cf63a0a6ed3134629ae73e8ab51e4b765a67f3aa062eb433" +dependencies = [ + "arrayvec", + "cfg_aliases 0.1.1", + "document-features", + "js-sys", + "log", + "parking_lot", + "profiling", + "raw-window-handle", + "smallvec", + "static_assertions", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "wgpu-core", + "wgpu-hal", + "wgpu-types", +] + +[[package]] +name = "wgpu-core" +version = "22.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0348c840d1051b8e86c3bcd31206080c5e71e5933dabd79be1ce732b0b2f089a" +dependencies = [ + "arrayvec", + "bit-vec", + "bitflags 2.11.0", + "cfg_aliases 0.1.1", + "document-features", + "indexmap", + "log", + "naga", + "once_cell", + "parking_lot", + "profiling", + "raw-window-handle", + "rustc-hash 1.1.0", + "smallvec", + "thiserror 1.0.69", + "wgpu-hal", + "wgpu-types", +] + +[[package]] +name = "wgpu-hal" +version = "22.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6bbf4b4de8b2a83c0401d9e5ae0080a2792055f25859a02bf9be97952bbed4f" +dependencies = [ + "android_system_properties", + "arrayvec", + "ash", + "bitflags 2.11.0", + "cfg_aliases 0.1.1", + "core-graphics-types", + "glow 0.13.1", + "glutin_wgl_sys", + "gpu-alloc", + "gpu-allocator", + "gpu-descriptor", + "hassle-rs", + "js-sys", + "khronos-egl", + "libc", + "libloading", + "log", + "metal", + "naga", + "ndk-sys 0.5.0+25.2.9519653", + "objc", + "once_cell", + "parking_lot", + "profiling", + "raw-window-handle", + "renderdoc-sys", + "rustc-hash 1.1.0", + "smallvec", + "thiserror 1.0.69", + "wasm-bindgen", + "web-sys", + "wgpu-types", + "winapi", +] + +[[package]] +name = "wgpu-types" +version = "22.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc9d91f0e2c4b51434dfa6db77846f2793149d8e73f800fa2e41f52b8eac3c5d" +dependencies = [ + "bitflags 2.11.0", + "js-sys", + "web-sys", +] + +[[package]] +name = "widestring" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72069c3113ab32ab29e5584db3c6ec55d416895e60715417b5b883a357c3e471" + +[[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-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[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" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e48a53791691ab099e5e2ad123536d0fff50652600abaf43bbf952894110d0be" +dependencies = [ + "windows-core 0.52.0", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd04d41d93c4992d421894c18c8b43496aa748dd4c081bac0dc93eb0489272b6" +dependencies = [ + "windows-core 0.58.0", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows" +version = "0.61.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" +dependencies = [ + "windows-collections", + "windows-core 0.61.2", + "windows-future", + "windows-link 0.1.3", + "windows-numerics", +] + +[[package]] +name = "windows-collections" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" +dependencies = [ + "windows-core 0.61.2", +] + +[[package]] +name = "windows-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-core" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ba6d44ec8c2591c134257ce647b7ea6b20335bf6379a27dac5f1641fcf59f99" +dependencies = [ + "windows-implement 0.58.0", + "windows-interface 0.58.0", + "windows-result 0.2.0", + "windows-strings 0.1.0", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-core" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +dependencies = [ + "windows-implement 0.60.2", + "windows-interface 0.59.3", + "windows-link 0.1.3", + "windows-result 0.3.4", + "windows-strings 0.4.2", +] + +[[package]] +name = "windows-future" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" +dependencies = [ + "windows-core 0.61.2", + "windows-link 0.1.3", + "windows-threading", +] + +[[package]] +name = "windows-implement" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "windows-interface" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "windows-link" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-numerics" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" +dependencies = [ + "windows-core 0.61.2", + "windows-link 0.1.3", +] + +[[package]] +name = "windows-result" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-result" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-strings" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" +dependencies = [ + "windows-result 0.2.0", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-strings" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +dependencies = [ + "windows-link 0.1.3", +] + +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link 0.2.1", +] + +[[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", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +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", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link 0.2.1", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + +[[package]] +name = "windows-threading" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-version" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4060a1da109b9d0326b7262c8e12c84df67cc0dbc9e33cf49e01ccc2eb63631" +dependencies = [ + "windows-link 0.2.1", +] + +[[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.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.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.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + +[[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.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "winit" +version = "0.30.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6755fa58a9f8350bd1e472d4c3fcc25f824ec358933bba33306d0b63df5978d" +dependencies = [ + "ahash", + "android-activity", + "atomic-waker", + "bitflags 2.11.0", + "block2 0.5.1", + "bytemuck", + "calloop 0.13.0", + "cfg_aliases 0.2.1", + "concurrent-queue", + "core-foundation 0.9.4", + "core-graphics", + "cursor-icon", + "dpi", + "js-sys", + "libc", + "memmap2", + "ndk", + "objc2 0.5.2", + "objc2-app-kit 0.2.2", + "objc2-foundation 0.2.2", + "objc2-ui-kit", + "orbclient", + "percent-encoding", + "pin-project", + "raw-window-handle", + "redox_syscall 0.4.1", + "rustix 0.38.44", + "sctk-adwaita", + "smithay-client-toolkit 0.19.2", + "smol_str", + "tracing", + "unicode-segmentation", + "wasm-bindgen", + "wasm-bindgen-futures", + "wayland-backend", + "wayland-client", + "wayland-protocols 0.32.11", + "wayland-protocols-plasma", + "web-sys", + "web-time", + "windows-sys 0.52.0", + "x11-dl", + "x11rb", + "xkbcommon-dl", +] + +[[package]] +name = "winnow" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" +dependencies = [ + "memchr", +] + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn 2.0.117", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn 2.0.117", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags 2.11.0", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "wl-clipboard-rs" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9651471a32e87d96ef3a127715382b2d11cc7c8bb9822ded8a7cc94072eb0a3" +dependencies = [ + "libc", + "log", + "os_pipe", + "rustix 1.1.4", + "thiserror 2.0.18", + "tree_magic_mini", + "wayland-backend", + "wayland-client", + "wayland-protocols 0.32.11", + "wayland-protocols-wlr 0.3.11", +] + +[[package]] +name = "writeable" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" + +[[package]] +name = "x11-dl" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38735924fedd5314a6e548792904ed8c6de6636285cb9fec04d5b1db85c1516f" +dependencies = [ + "libc", + "once_cell", + "pkg-config", +] + +[[package]] +name = "x11rb" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9993aa5be5a26815fe2c3eacfc1fde061fc1a1f094bf1ad2a18bf9c495dd7414" +dependencies = [ + "as-raw-xcb-connection", + "gethostname", + "libc", + "libloading", + "once_cell", + "rustix 1.1.4", + "x11rb-protocol", +] + +[[package]] +name = "x11rb-protocol" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea6fc2961e4ef194dcbfe56bb845534d0dc8098940c7e5c012a258bfec6701bd" + +[[package]] +name = "xcursor" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bec9e4a500ca8864c5b47b8b482a73d62e4237670e5b5f1d6b9e3cae50f28f2b" + +[[package]] +name = "xdg-home" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec1cdab258fb55c0da61328dc52c8764709b249011b2cad0454c72f0bf10a1f6" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + +[[package]] +name = "xkbcommon-dl" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d039de8032a9a8856a6be89cea3e5d12fdd82306ab7c94d74e6deab2460651c5" +dependencies = [ + "bitflags 2.11.0", + "dlib", + "log", + "once_cell", + "xkeysym", +] + +[[package]] +name = "xkeysym" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9cc00251562a284751c9973bace760d86c0276c471b4be569fe6b068ee97a56" + +[[package]] +name = "xml-rs" +version = "0.8.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ae8337f8a065cfc972643663ea4279e04e7256de865aa66fe25cec5fb912d3f" + +[[package]] +name = "y4m" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a5a4b21e1a62b67a2970e6831bc091d7b87e119e7f9791aef9702e3bef04448" + +[[package]] +name = "yoke" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", + "synstructure", +] + +[[package]] +name = "zbus" +version = "4.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb97012beadd29e654708a0fdb4c84bc046f537aecfde2c3ee0a9e4b4d48c725" +dependencies = [ + "async-broadcast", + "async-executor", + "async-fs", + "async-io", + "async-lock", + "async-process", + "async-recursion", + "async-task", + "async-trait", + "blocking", + "enumflags2", + "event-listener", + "futures-core", + "futures-sink", + "futures-util", + "hex", + "nix", + "ordered-stream", + "rand 0.8.5", + "serde", + "serde_repr", + "sha1", + "static_assertions", + "tracing", + "uds_windows", + "windows-sys 0.52.0", + "xdg-home", + "zbus_macros 4.4.0", + "zbus_names 3.0.0", + "zvariant 4.2.0", +] + +[[package]] +name = "zbus" +version = "5.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca82f95dbd3943a40a53cfded6c2d0a2ca26192011846a1810c4256ef92c60bc" +dependencies = [ + "async-broadcast", + "async-executor", + "async-io", + "async-lock", + "async-process", + "async-recursion", + "async-task", + "async-trait", + "blocking", + "enumflags2", + "event-listener", + "futures-core", + "futures-lite", + "hex", + "libc", + "ordered-stream", + "rustix 1.1.4", + "serde", + "serde_repr", + "tracing", + "uds_windows", + "uuid", + "windows-sys 0.61.2", + "winnow", + "zbus_macros 5.14.0", + "zbus_names 4.3.1", + "zvariant 5.10.0", +] + +[[package]] +name = "zbus-lockstep" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca2c5dceb099bddaade154055c926bb8ae507a18756ba1d8963fd7b51d8ed1d" +dependencies = [ + "zbus_xml", + "zvariant 4.2.0", +] + +[[package]] +name = "zbus-lockstep-macros" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "709ab20fc57cb22af85be7b360239563209258430bccf38d8b979c5a2ae3ecce" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", + "zbus-lockstep", + "zbus_xml", + "zvariant 4.2.0", +] + +[[package]] +name = "zbus_macros" +version = "4.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "267db9407081e90bbfa46d841d3cbc60f59c0351838c4bc65199ecd79ab1983e" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.117", + "zvariant_utils 2.1.0", +] + +[[package]] +name = "zbus_macros" +version = "5.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897e79616e84aac4b2c46e9132a4f63b93105d54fe8c0e8f6bffc21fa8d49222" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.117", + "zbus_names 4.3.1", + "zvariant 5.10.0", + "zvariant_utils 3.3.0", +] + +[[package]] +name = "zbus_names" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b9b1fef7d021261cc16cba64c351d291b715febe0fa10dc3a443ac5a5022e6c" +dependencies = [ + "serde", + "static_assertions", + "zvariant 4.2.0", +] + +[[package]] +name = "zbus_names" +version = "4.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffd8af6d5b78619bab301ff3c560a5bd22426150253db278f164d6cf3b72c50f" +dependencies = [ + "serde", + "winnow", + "zvariant 5.10.0", +] + +[[package]] +name = "zbus_xml" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab3f374552b954f6abb4bd6ce979e6c9b38fb9d0cd7cc68a7d796e70c9f3a233" +dependencies = [ + "quick-xml 0.30.0", + "serde", + "static_assertions", + "zbus_names 3.0.0", + "zvariant 4.2.0", +] + +[[package]] +name = "zerocopy" +version = "0.8.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a789c6e490b576db9f7e6b6d661bcc9799f7c0ac8352f56ea20193b2681532e5" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f65c489a7071a749c849713807783f70672b28094011623e200cb86dcb835953" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", + "synstructure", +] + +[[package]] +name = "zerotrie" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" + +[[package]] +name = "zune-core" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f423a2c17029964870cfaabb1f13dfab7d092a62a29a89264f4d36990ca414a" + +[[package]] +name = "zune-core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb8a0807f7c01457d0379ba880ba6322660448ddebc890ce29bb64da71fb40f9" + +[[package]] +name = "zune-inflate" +version = "0.2.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ab332fe2f6680068f3582b16a24f90ad7096d5d39b974d1c0aff0125116f02" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "zune-jpeg" +version = "0.4.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29ce2c8a9384ad323cf564b67da86e21d3cfdff87908bc1223ed5c99bc792713" +dependencies = [ + "zune-core 0.4.12", +] + +[[package]] +name = "zune-jpeg" +version = "0.5.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "410e9ecef634c709e3831c2cfdb8d9c32164fae1c67496d5b68fff728eec37fe" +dependencies = [ + "zune-core 0.5.1", +] + +[[package]] +name = "zvariant" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2084290ab9a1c471c38fc524945837734fbf124487e105daec2bb57fd48c81fe" +dependencies = [ + "endi", + "enumflags2", + "serde", + "static_assertions", + "zvariant_derive 4.2.0", +] + +[[package]] +name = "zvariant" +version = "5.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5708299b21903bbe348e94729f22c49c55d04720a004aa350f1f9c122fd2540b" +dependencies = [ + "endi", + "enumflags2", + "serde", + "winnow", + "zvariant_derive 5.10.0", + "zvariant_utils 3.3.0", +] + +[[package]] +name = "zvariant_derive" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73e2ba546bda683a90652bac4a279bc146adad1386f25379cf73200d2002c449" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.117", + "zvariant_utils 2.1.0", +] + +[[package]] +name = "zvariant_derive" +version = "5.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b59b012ebe9c46656f9cc08d8da8b4c726510aef12559da3e5f1bf72780752c" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.117", + "zvariant_utils 3.3.0", +] + +[[package]] +name = "zvariant_utils" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c51bcff7cc3dbb5055396bcf774748c3dab426b4b8659046963523cee4808340" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "zvariant_utils" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f75c23a64ef8f40f13a6989991e643554d9bef1d682a281160cf0c1bc389c5e9" +dependencies = [ + "proc-macro2", + "quote", + "serde", + "syn 2.0.117", + "winnow", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..2d45c2e --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,41 @@ +[workspace] + +[package] +name = "blast" +version = "0.1.0" +edition = "2021" +description = "Hyprland screenshot tool" + +[[bin]] +name = "blast" +path = "src/main.rs" + +[[bin]] +name = "blast-gui" +path = "src/gui/main.rs" + +[dependencies] +ab_glyph = "0.2" +clap = { version = "4", features = ["derive"] } +serde = { version = "1", features = ["derive"] } +serde_json = "1" +wl-clipboard-rs = "0.9.3" +notify-rust = "4" +wayland-client = "0.31" +wayland-protocols-wlr = { version = "0.2", features = ["client"] } +wayland-protocols = { version = "0.31", features = ["client", "staging", "unstable"] } +wayland-scanner = "0.31" +wayland-backend = "0.3" +image = { version = "0.25", features = ["png"] } +png = "0.17" +dirs = "5" +rand = "0.8" +thiserror = "1" +anyhow = "1" +libc = "0.2" +tempfile = "3" +gbm = "0.18" +memmap2 = "0.9" +eframe = { version = "0.29", features = ["wayland"] } +egui = "0.29" +egui_extras = { version = "0.29", features = ["image"] } diff --git a/README.md b/README.md new file mode 100644 index 0000000..a7ae42e --- /dev/null +++ b/README.md @@ -0,0 +1,98 @@ +## Blast + +A pretty Wayland screenshot tool supporting multiple capture modes, including window-aware capture. + +│ Note +│ This tool includes a proof-of-concept implementation of the +│ xx-foreign-toplevel-geometry-v1 protocol for tracking client geometry. +│ It is experimental and may be incomplete or subject to change. + +## Supported Compositors +- Hyprland +- Wry + +## Features + +### Actions + + +- **`copy`** - Copy to clipboard +- **`save`** - Save to file (or - for stdout) +- **`copysave`** - Copy and save, save path defaults to home directory +- **`edit`** - Open in image editor +- **`check`** - Verify protocol support + +### Subjects + +- **`screen`** - All monitors stitched into a single image at logical resolution +- **`output`** - The currently focused monitor +- **`active`** - The currently focused window +- **`area`** - Interactive selection with window snap hints (like `slurp -w`) +- **`region`** - Free-form interactive selection with no snap hints + +### Fine Tune + +``` +--no-shadow Disable shadow and rounding entirely +--radius px Corner radius +--shadow-offset X,Y Shadow offset +--shadow-alpha 0-255 Shadow opacity +--shadow-spread PX Shadow spread distance +--shadow-blur PX Shadow blur radius +``` + +### Freeze overlay + +`-f / --freeze` captures the current screen state before the selection UI appears and renders it as a static layer shell overlay. The screen appears frozen while you draw your selection and is captured directly from the compositor. + +The screenshot is taken while the freeze is still active, so what you see is exactly what you get. + +### Cursor inclusion + +`-c / --cursor` includes the cursor in the screenshot. + +### Wait + +`-w / --wait N` waits N seconds before capturing. Useful for capturing tooltips or hover states. + +### Notifications + +`-n / --notify` sends a desktop notification on completion with the saved file path as the notification body. + +### Editor integration + +`blast edit` saves the screenshot to `/tmp/.png` and opens it in your configured editor. Set `$BLAST_EDITOR` to override the default (`gimp`). + +### Save path resolution + +Output paths follow XDG priority: `$XDG_SCREENSHOTS_DIR` → `$XDG_PICTURES_DIR` → `$HOME`. Default filenames are `_.png`. + +--- + +## Usage examples + +```sh +# Copy a window-snapping region selection to clipboard, with freeze and shadow +blast -fn copy area + +# Save the active window to ~/Pictures +blast save active + +# Copy full screen of all monitors to clipboard +blast copy screen + +# Save a free-form selection to a specific path +blast save region ~/screenshots/test.png + +# Copy to clipboard and save, no shadow +blast --no-shadow copysave area + +# Open focused monitor screenshot in editor +blast edit output +``` + +## Building + +```sh +cargo build --release +``` diff --git a/protocols/xx-foreign-toplevel-geometry-v1.xml b/protocols/xx-foreign-toplevel-geometry-v1.xml new file mode 100644 index 0000000..131f1c1 --- /dev/null +++ b/protocols/xx-foreign-toplevel-geometry-v1.xml @@ -0,0 +1,120 @@ + + + + Copyright © 2026 outfoxxed + + Permission to use, copy, modify, distribute, and sell this + software and its documentation for any purpose is hereby granted + without fee, provided that the above copyright notice appear in + all copies and that both that copyright notice and this permission + notice appear in supporting documentation, and that the name of + the copyright holders not be used in advertising or publicity + pertaining to distribution of the software without specific, + written prior permission. The copyright holders make no + representations about the suitability of this software for any + purpose. It is provided "as is" without express or implied + warranty. + + THE COPYRIGHT HOLDERS DISCLAIM ALL WARRANTIES WITH REGARD TO THIS + SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND + FITNESS, IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY + SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN + AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, + ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF + THIS SOFTWARE. + + + + This protocol allows clients to track the geometry of toplevels relative + to all relevant outputs. + + This protocol is privileged and should be limited to trusted clients. + + The key words "must", "must not", "required", "shall", "shall not", + "should", "should not", "recommended", "may", and "optional" in this + document are to be interpreted as described in IETF RFC 2119. + + + + + This global can be used to create foreign toplevel geometry trackers. + + + + + Destroy the manager. Any objects created by the manager are not affected. + + + + + + Creates a toplevel geometry tracker for the given foreign toplevel handle. + + + + + + + + + + This object tracks the geometry of a foreign toplevel relative to all outputs + it is present on. + + Upon creation, a set of 'geometry' events must be sent. Additional event sets + should be sent whenever the toplevel is moved. + + + + + This request destroys the geometry tracker object. + + + + + + This event indicates no more geometry events will be sent for this tracker. + The client should destroy the tracker. + + + + + + This event marks the end of a set of 'geometry' events for a toplevel. + + The event set may be empty, indicating the toplevel is not present on + any output. + + Following this event, 'ext_foreign_toplevel_handle_v1.done' must be sent + to allow toplevel geometry updates to be used atomically with other foreign + toplevel state changes. + + + + + + This event reports the geometry of the toplevel relative to the output in the + output's hardware coordinate space, as reported by 'wl_output.mode'. Geometry + should not be clipped to the bounds of the output even if the output does not + fully contain the toplevel. + + Clients may use the logical coordinate information reported by `xdg_output` + to derive a global position for a toplevel. + + One event may be sent for each output the toplevel is present on, followed by + a single 'done' event. + + A series of geometry events should be resent whenever the toplevel is moved, + followed by a 'done' event. + + + + + + + + + + + diff --git a/src/capture.rs b/src/capture.rs new file mode 100644 index 0000000..5be796e --- /dev/null +++ b/src/capture.rs @@ -0,0 +1,1134 @@ +//! Screenshot capture via wlr-screencopy-unstable-v1. +//! +//! 1. Request a frame (capture_output or capture_output_region). +//! 2. The compositor sends one or more Buffer events listing supported +//! wl_shm formats, plus optional LinuxDmabuf events, followed by BufferDone. +//! 3a. (DMA-buf path) Allocate a LINEAR GBM buffer object for the offered DRM +//! format, create a wl_buffer via zwp_linux_dmabuf_v1, and call copy(). +//! After Ready, mmap the exported fd and decode the pixels on the CPU. +//! 3b. (SHM fallback) Pick the best 8-bit wl_shm format, allocate a SHM file, +//! and call copy(). After Ready, read and decode the SHM file. +//! 4. The compositor fills the buffer and sends Ready. + +use std::{ + fs::File, + os::unix::io::{AsFd, AsRawFd, FromRawFd, IntoRawFd, OwnedFd}, +}; + +use gbm::{BufferObjectFlags, Device as GbmDevice}; +use image::{DynamicImage, ImageBuffer, Rgba}; +use wayland_client::{ + delegate_noop, + globals::{registry_queue_init, GlobalListContents}, + protocol::{wl_buffer, wl_output, wl_registry, wl_shm, wl_shm_pool}, + Connection, Dispatch, EventQueue, QueueHandle, +}; +use wayland_protocols::wp::linux_dmabuf::zv1::client::{ + zwp_linux_buffer_params_v1::{self, ZwpLinuxBufferParamsV1}, + zwp_linux_dmabuf_v1::ZwpLinuxDmabufV1, +}; +use wayland_protocols::xdg::xdg_output::zv1::client::{ + zxdg_output_manager_v1::ZxdgOutputManagerV1, + zxdg_output_v1::{self, ZxdgOutputV1}, +}; +use wayland_protocols_wlr::screencopy::v1::client::{ + zwlr_screencopy_frame_v1, zwlr_screencopy_manager_v1, +}; + +use crate::{ + error::{BlastError, Result}, + hyprland::Geometry, +}; + +#[derive(Debug, Default, Clone)] +pub struct CaptureOptions { + pub include_cursor: bool, + #[allow(dead_code)] + pub scale: Option, +} + +/// Wayland native output geometry (logical coords + hw pixel size). +/// Collected via wl_output + zxdg_output_v1 so we don't need Hyprland IPC. +struct OutputInfo { + name: String, + logical_x: i32, + logical_y: i32, + logical_w: i32, + logical_h: i32, + hw_w: i32, + hw_h: i32, + handle: wl_output::WlOutput, +} + +impl OutputInfo { + /// Scale = hw / logical. Falls back to 1.0 if logical size is unknown. + fn scale(&self) -> f64 { + if self.logical_w > 0 && self.logical_h > 0 { + self.hw_w as f64 / self.logical_w as f64 + } else { + 1.0 + } + } + + /// True if the geometry overlaps this output's logical bounds. + fn contains_geometry(&self, g: &Geometry) -> bool { + let ox = self.logical_x as i64; + let oy = self.logical_y as i64; + let ow = self.logical_w as i64; + let oh = self.logical_h as i64; + g.x < ox + ow && g.x + g.w > ox && g.y < oy + oh && g.y + g.h > oy + } +} + +pub fn capture_region(geom: Geometry, opts: &CaptureOptions) -> Result { + AppState::connect()?.run_capture_region(geom, opts) +} + +pub fn capture_output(output_name: &str, opts: &CaptureOptions) -> Result { + AppState::connect()?.run_capture_output(output_name, opts) +} + +pub fn capture_screen(opts: &CaptureOptions) -> Result { + AppState::connect()?.run_capture_all(opts) +} + +#[derive(Clone)] +struct BufferOffer { + format: wl_shm::Format, + width: u32, + height: u32, + stride: u32, +} + +/// DRM fourcc format + dimensions from screencopy's LinuxDmabuf event. +#[derive(Clone)] +struct DmabufOffer { + format: u32, // DRM fourcc + width: u32, + height: u32, +} + +/// The DMA-buf allocation we actually used for a capture. +struct DmabufChosen { + format: u32, // DRM fourcc + width: u32, + height: u32, + stride: u32, // from bo.stride() after allocation +} + +#[derive(Debug, PartialEq)] +enum FrameState { + Negotiating, + Copying, + Ready, + /// compositor rejected the frame. + Failed, +} + +struct AppState { + #[allow(dead_code)] + conn: Connection, + qh: QueueHandle, + queue: Option>, + + shm: Option, + shm_formats: Vec, + screencopy_mgr: Option, + /// Present when the compositor supports zwp_linux_dmabuf_v1. + linux_dmabuf: Option, + outputs: Vec, + /// Kept alive so the compositor continues to send geometry events until roundtrip. + _xdg_outputs: Vec, + + // per-capture + frame_state: FrameState, + buffer_offers: Vec, + dmabuf_offers: Vec, + chosen_offer: Option, + chosen_dmabuf: Option, + frame: Option, + shm_file: Option, + shm_pool: Option, + wl_buffer: Option, + /// Keeps the GBM BO alive until read_pixels() has finished mmapping it. + gbm_bo: Option>, +} + +impl AppState { + fn connect() -> Result { + let conn = Connection::connect_to_env() + .map_err(|e| BlastError::Capture(format!("Wayland connect: {e}")))?; + let (globals, queue) = registry_queue_init::(&conn) + .map_err(|e| BlastError::Capture(format!("registry init: {e}")))?; + let qh = queue.handle(); + + let shm: wl_shm::WlShm = globals + .bind(&qh, 1..=1, ()) + .map_err(|e| BlastError::Capture(format!("bind wl_shm: {e}")))?; + let screencopy_mgr: zwlr_screencopy_manager_v1::ZwlrScreencopyManagerV1 = + globals.bind(&qh, 1..=3, ()).map_err(|e| { + BlastError::Capture(format!( + "bind zwlr_screencopy_manager_v1: {e} - is wlr-screencopy supported?" + )) + })?; + + // Always attempt dmabuf; falls back to SHM if unavailable. + let linux_dmabuf: Option = globals.bind(&qh, 2..=4, ()).ok(); + // Optional: xdg_output for Wayland-native logical monitor geometry. + let xdg_mgr: Option = globals.bind(&qh, 2..=3, ()).ok(); + + let mut state = AppState { + conn, + qh: qh.clone(), + queue: Some(queue), + shm: Some(shm), + shm_formats: Vec::new(), + screencopy_mgr: Some(screencopy_mgr), + linux_dmabuf, + outputs: Vec::new(), + _xdg_outputs: Vec::new(), + frame_state: FrameState::Negotiating, + buffer_offers: Vec::new(), + dmabuf_offers: Vec::new(), + chosen_offer: None, + chosen_dmabuf: None, + frame: None, + shm_file: None, + shm_pool: None, + wl_buffer: None, + gbm_bo: None, + }; + + let out_globals: Vec<(u32, u32)> = globals + .contents() + .clone_list() + .iter() + .filter(|g| g.interface == "wl_output") + .map(|g| (g.name, g.version.min(4))) + .collect(); + + for (i, (gname, version)) in out_globals.iter().enumerate() { + let wl_out = globals + .registry() + .bind::(*gname, *version, &qh, i); + state.outputs.push(OutputInfo { + name: String::new(), + logical_x: 0, + logical_y: 0, + logical_w: 0, + logical_h: 0, + hw_w: 0, + hw_h: 0, + handle: wl_out.clone(), + }); + if let Some(mgr) = &xdg_mgr { + let xdg_out = mgr.get_xdg_output(&wl_out, &qh, i); + state._xdg_outputs.push(xdg_out); + } + } + + state.roundtrip()?; + if xdg_mgr.is_some() { + // Second roundtrip ensures all zxdg_output_v1 events have arrived. + state.roundtrip()?; + } + + Ok(state) + } + + fn run_capture_region(mut self, geom: Geometry, opts: &CaptureOptions) -> Result { + // Try Hyprland IPC first, fall back to Wayland-native output geometry. + let (output, mon_x, mon_y, scale) = + if let Ok(mon) = crate::hyprland::monitor_for_geometry(&geom) { + let wl = self + .outputs + .iter() + .find(|o| o.name == mon.name) + .or_else(|| self.outputs.first()) + .map(|o| o.handle.clone()) + .ok_or_else(|| BlastError::Capture("no Wayland outputs".into()))?; + (wl, mon.x, mon.y, mon.scale) + } else { + let out = self + .outputs + .iter() + .find(|o| o.contains_geometry(&geom)) + .or_else(|| self.outputs.first()) + .ok_or_else(|| BlastError::Capture("no Wayland outputs".into()))?; + let scale = out.scale(); + ( + out.handle.clone(), + out.logical_x as i64, + out.logical_y as i64, + scale, + ) + }; + + let overlay = if opts.include_cursor { 1 } else { 0 }; + let frame = + self.screencopy_mgr + .as_ref() + .unwrap() + .capture_output(overlay, &output, &self.qh, ()); + self.frame = Some(frame); + self.frame_state = FrameState::Negotiating; + + self.dispatch_to_ready()?; + let full = self.read_pixels()?; + + let rx = ((geom.x - mon_x) as f64 * scale).round() as u32; + let ry = ((geom.y - mon_y) as f64 * scale).round() as u32; + let rw = (geom.w as f64 * scale).round() as u32; + let rh = (geom.h as f64 * scale).round() as u32; + + let img_w = full.width(); + let img_h = full.height(); + let rx = rx.min(img_w.saturating_sub(1)); + let ry = ry.min(img_h.saturating_sub(1)); + let rw = rw.min(img_w - rx); + let rh = rh.min(img_h - ry); + + Ok(full.crop_imm(rx, ry, rw, rh)) + } + + fn run_capture_output(mut self, name: &str, opts: &CaptureOptions) -> Result { + let output = self + .outputs + .iter() + .find(|o| o.name == name) + .ok_or_else(|| BlastError::Capture(format!("output '{name}' not found")))? + .handle + .clone(); + + let overlay = if opts.include_cursor { 1 } else { 0 }; + let frame = + self.screencopy_mgr + .as_ref() + .unwrap() + .capture_output(overlay, &output, &self.qh, ()); + self.frame = Some(frame); + self.frame_state = FrameState::Negotiating; + + self.dispatch_to_ready()?; + self.read_pixels() + } + + fn run_capture_all(self, opts: &CaptureOptions) -> Result { + if let Ok(mons) = crate::hyprland::monitors() { + if !mons.is_empty() { + let logical_total_w = mons + .iter() + .map(|m| m.x + (m.width as f64 / m.scale).round() as i64) + .max() + .unwrap_or(0) as u32; + let logical_total_h = mons + .iter() + .map(|m| m.y + (m.height as f64 / m.scale).round() as i64) + .max() + .unwrap_or(0) as u32; + let mut canvas = image::RgbaImage::new(logical_total_w, logical_total_h); + for mon in &mons { + let state = AppState::connect()?; + let out = state + .outputs + .iter() + .find(|o| o.name == mon.name) + .map(|o| o.handle.clone()); + let out = match out { + Some(o) => o, + None => continue, + }; + let overlay = if opts.include_cursor { 1 } else { 0 }; + let mut state = state; + let frame = state.screencopy_mgr.as_ref().unwrap().capture_output( + overlay, + &out, + &state.qh, + (), + ); + state.frame = Some(frame); + state.frame_state = FrameState::Negotiating; + state.dispatch_to_ready()?; + let mon_img = state.read_pixels()?; + let logical_w = (mon.width as f64 / mon.scale).round() as u32; + let logical_h = (mon.height as f64 / mon.scale).round() as u32; + let scaled = mon_img.resize_exact( + logical_w, + logical_h, + image::imageops::FilterType::Lanczos3, + ); + image::imageops::overlay( + &mut canvas, + &scaled.to_rgba8(), + mon.x as i64, + mon.y as i64, + ); + } + return Ok(image::DynamicImage::ImageRgba8(canvas)); + } + } + + if self.outputs.is_empty() { + return Err(BlastError::Capture("no monitors found".into())); + } + let logical_total_w = self + .outputs + .iter() + .map(|o| o.logical_x + o.logical_w) + .max() + .unwrap_or(0) + .max(1) as u32; + let logical_total_h = self + .outputs + .iter() + .map(|o| o.logical_y + o.logical_h) + .max() + .unwrap_or(0) + .max(1) as u32; + let mut canvas = image::RgbaImage::new(logical_total_w, logical_total_h); + + let out_info: Vec<(String, i32, i32, i32, i32)> = self + .outputs + .iter() + .map(|o| { + ( + o.name.clone(), + o.logical_x, + o.logical_y, + o.logical_w, + o.logical_h, + ) + }) + .collect(); + + for (name, lx, ly, lw, lh) in &out_info { + let state = AppState::connect()?; + let out = state + .outputs + .iter() + .find(|o| &o.name == name) + .or_else(|| state.outputs.first()) + .map(|o| o.handle.clone()); + let out = match out { + Some(o) => o, + None => continue, + }; + let overlay = if opts.include_cursor { 1 } else { 0 }; + let mut state = state; + let frame = + state + .screencopy_mgr + .as_ref() + .unwrap() + .capture_output(overlay, &out, &state.qh, ()); + state.frame = Some(frame); + state.frame_state = FrameState::Negotiating; + state.dispatch_to_ready()?; + let mon_img = state.read_pixels()?; + let lw = (*lw as u32).max(1); + let lh = (*lh as u32).max(1); + let scaled = mon_img.resize_exact(lw, lh, image::imageops::FilterType::Lanczos3); + image::imageops::overlay(&mut canvas, &scaled.to_rgba8(), *lx as i64, *ly as i64); + } + + Ok(image::DynamicImage::ImageRgba8(canvas)) + } + + fn roundtrip(&mut self) -> Result<()> { + let mut q = self + .queue + .take() + .ok_or_else(|| BlastError::Capture("queue missing".into()))?; + let r = q + .roundtrip(self) + .map_err(|e| BlastError::Capture(format!("roundtrip: {e}"))); + self.queue = Some(q); + r.map(|_| ()) + } + + fn dispatch_once(&mut self) -> Result<()> { + let mut q = self + .queue + .take() + .ok_or_else(|| BlastError::Capture("queue missing".into()))?; + let r = q + .blocking_dispatch(self) + .map_err(|e| BlastError::Capture(format!("dispatch: {e}"))); + self.queue = Some(q); + r.map(|_| ()) + } + + /// Drive the event loop through the full Buffer->BufferDone->copy->Ready sequence. + fn dispatch_to_ready(&mut self) -> Result<()> { + loop { + self.dispatch_once()?; + match self.frame_state { + FrameState::Ready => return Ok(()), + FrameState::Failed => { + return Err(BlastError::Capture("compositor rejected frame".into())) + } + _ => {} + } + } + } + + /// Pick the best offer from what the compositor sent. + /// Prefers formats we know how to decode, in priority order. + /// Returns None if none of the offered formats are supported by wl_shm. + fn pick_offer(&self) -> Option { + const PREFERRED: &[wl_shm::Format] = &[ + wl_shm::Format::Argb8888, + wl_shm::Format::Xrgb8888, + wl_shm::Format::Abgr8888, + wl_shm::Format::Xbgr8888, + wl_shm::Format::Rgba8888, + wl_shm::Format::Bgra8888, + ]; + for &fmt in PREFERRED { + if let Some(o) = self + .buffer_offers + .iter() + .find(|o| o.format == fmt && self.shm_formats.contains(&o.format)) + { + return Some(o.clone()); + } + } + // Last resort: anything wl_shm supports (may fail in read_pixels). + self.buffer_offers + .iter() + .find(|o| self.shm_formats.contains(&o.format)) + .cloned() + } + + fn alloc_and_copy(&mut self) -> bool { + // DMA-buf path: try LINEAR allocation so we can mmap the result on CPU. + // Any failure (unsupported format, no render node, etc.) falls through + // to the SHM path below. + if try_alloc_capture_dmabuf(self) { + return true; + } + + // SHM path. + let offer = match self.pick_offer() { + Some(o) => o, + None => { + eprintln!( + "blast: no supported shm format in offers: {:?}", + self.buffer_offers + .iter() + .map(|o| o.format) + .collect::>() + ); + return false; + } + }; + + let size = (offer.stride * offer.height) as usize; + let file = match create_shm_file(size) { + Ok(f) => f, + Err(e) => { + eprintln!("blast: shm alloc: {e}"); + return false; + } + }; + let fd = match file.try_clone() { + Ok(f) => f.into_raw_fd(), + Err(e) => { + eprintln!("blast: fd clone: {e}"); + return false; + } + }; + + let pool = self.shm.as_ref().unwrap().create_pool( + unsafe { std::os::unix::io::BorrowedFd::borrow_raw(fd) }, + size as i32, + &self.qh, + (), + ); + let buf = pool.create_buffer( + 0, + offer.width as i32, + offer.height as i32, + offer.stride as i32, + offer.format, + &self.qh, + (), + ); + + self.chosen_offer = Some(offer); + self.shm_file = Some(file); + self.shm_pool = Some(pool); + self.wl_buffer = Some(buf.clone()); + + if let Some(frame) = &self.frame { + frame.copy(&buf); + true + } else { + false + } + } + + fn read_pixels(mut self) -> Result { + // DMA-buf path: mmap the exported fd and decode. + if let Some(bo) = self.gbm_bo.take() { + let chosen = self + .chosen_dmabuf + .take() + .ok_or_else(|| BlastError::Capture("no dmabuf chosen offer".into()))?; + + let dma_fd: OwnedFd = bo + .fd() + .map_err(|e| BlastError::Capture(format!("dmabuf fd export: {e}")))?; + + let size = (chosen.stride * chosen.height) as usize; + let rgba = { + let ptr = unsafe { + libc::mmap( + std::ptr::null_mut(), + size, + libc::PROT_READ, + libc::MAP_SHARED, + dma_fd.as_raw_fd(), + 0, + ) + }; + if ptr == libc::MAP_FAILED { + return Err(BlastError::Capture(format!( + "dmabuf mmap failed: {}", + std::io::Error::last_os_error() + ))); + } + let raw = unsafe { std::slice::from_raw_parts(ptr as *const u8, size) }; + let result = decode_drm_pixels( + raw, + chosen.width, + chosen.height, + chosen.stride, + chosen.format, + ); + unsafe { libc::munmap(ptr, size) }; + result? + }; + // dma_fd and bo drop here. + + let img = ImageBuffer::from_raw(chosen.width, chosen.height, rgba) + .ok_or_else(|| BlastError::Capture("image buffer construction failed".into()))?; + return Ok(DynamicImage::ImageRgba8(img)); + } + + // SHM path. + let offer = self + .chosen_offer + .take() + .ok_or_else(|| BlastError::Capture("no chosen offer after ready".into()))?; + let mut file = self + .shm_file + .take() + .ok_or_else(|| BlastError::Capture("no shm file after ready".into()))?; + + use std::io::{Read, Seek, SeekFrom}; + file.seek(SeekFrom::Start(0)) + .map_err(|e| BlastError::Capture(format!("shm seek: {e}")))?; + + let byte_count = (offer.stride * offer.height) as usize; + let mut raw = vec![0u8; byte_count]; + file.read_exact(&mut raw) + .map_err(|e| BlastError::Capture(format!("shm read: {e}")))?; + + let (width, height, stride) = (offer.width, offer.height, offer.stride); + let mut rgba: Vec = Vec::with_capacity((width * height * 4) as usize); + + // All formats below are packed 32-bit little-endian values. + // The DRM name describes channel order from MSB to LSB, so in memory + // (little-endian) the channel order is reversed. + // e.g. ARGB8888 = [31:0] A:R:G:B -> memory bytes [B, G, R, A] + // ABGR8888 = [31:0] A:B:G:R -> memory bytes [R, G, B, A] + match offer.format { + wl_shm::Format::Argb8888 => { + // memory: [B, G, R, A] + for row in 0..height { + for col in 0..width { + let base = (row * stride + col * 4) as usize; + rgba.push(raw[base + 2]); // R + rgba.push(raw[base + 1]); // G + rgba.push(raw[base]); // B + rgba.push(raw[base + 3]); // A + } + } + } + wl_shm::Format::Xrgb8888 => { + // memory: [B, G, R, X] + for row in 0..height { + for col in 0..width { + let base = (row * stride + col * 4) as usize; + rgba.push(raw[base + 2]); // R + rgba.push(raw[base + 1]); // G + rgba.push(raw[base]); // B + rgba.push(255); // A + } + } + } + wl_shm::Format::Abgr8888 => { + // memory: [R, G, B, A] + for row in 0..height { + for col in 0..width { + let base = (row * stride + col * 4) as usize; + rgba.push(raw[base]); // R + rgba.push(raw[base + 1]); // G + rgba.push(raw[base + 2]); // B + rgba.push(raw[base + 3]); // A + } + } + } + wl_shm::Format::Xbgr8888 => { + // memory: [R, G, B, X] + for row in 0..height { + for col in 0..width { + let base = (row * stride + col * 4) as usize; + rgba.push(raw[base]); // R + rgba.push(raw[base + 1]); // G + rgba.push(raw[base + 2]); // B + rgba.push(255); // A + } + } + } + wl_shm::Format::Rgba8888 => { + // memory: [A, B, G, R] + for row in 0..height { + for col in 0..width { + let base = (row * stride + col * 4) as usize; + rgba.push(raw[base + 3]); // R + rgba.push(raw[base + 2]); // G + rgba.push(raw[base + 1]); // B + rgba.push(raw[base]); // A + } + } + } + wl_shm::Format::Bgra8888 => { + // memory: [A, R, G, B] + for row in 0..height { + for col in 0..width { + let base = (row * stride + col * 4) as usize; + rgba.push(raw[base + 1]); // R + rgba.push(raw[base + 2]); // G + rgba.push(raw[base + 3]); // B + rgba.push(raw[base]); // A + } + } + } + other => { + return Err(BlastError::Capture(format!( + "unhandled pixel format {other:?} ->>> please report this" + ))); + } + } + + let img: ImageBuffer, Vec> = ImageBuffer::from_raw(width, height, rgba) + .ok_or_else(|| BlastError::Capture("image buffer construction failed".into()))?; + Ok(DynamicImage::ImageRgba8(img)) + } +} + +// DMA-buf capture helpers +/// Try to allocate a LINEAR GBM buffer for the screencopy DMA-buf offer, +/// create a wl_buffer, and kick off the copy. Returns true on success. +/// On any failure, returns false silently so the caller falls back to SHM. +fn try_alloc_capture_dmabuf(state: &mut AppState) -> bool { + let offer = match state.dmabuf_offers.first().cloned() { + Some(o) => o, + None => return false, + }; + let linux_dmabuf = match state.linux_dmabuf.clone() { + Some(d) => d, + None => return false, + }; + + let drm_file = match find_drm_render_node() { + Some(f) => f, + None => return false, + }; + let gbm = match GbmDevice::new(drm_file) { + Ok(d) => d, + Err(_) => return false, + }; + let fmt = match gbm::Format::try_from(offer.format) { + Ok(f) => f, + Err(_) => return false, + }; + + // LINEAR is required so we can mmap the buffer for CPU readback. + // If the driver doesn't support it for this format, fall back to SHM. + let bo = match gbm.create_buffer_object::<()>( + offer.width, + offer.height, + fmt, + BufferObjectFlags::RENDERING | BufferObjectFlags::LINEAR, + ) { + Ok(b) => b, + Err(_) => return false, + }; + + let stride = bo.stride(); + let dma_fd: OwnedFd = match bo.fd() { + Ok(f) => f, + Err(_) => return false, + }; + + let params = linux_dmabuf.create_params(&state.qh, ()); + // LINEAR modifier = 0; offset = 0; single plane. + params.add(dma_fd.as_fd(), 0, 0, stride, 0, 0); + + let wl_buf = params.create_immed( + offer.width as i32, + offer.height as i32, + offer.format, + zwp_linux_buffer_params_v1::Flags::empty(), + &state.qh, + (), + ); + + let frame = match state.frame.as_ref() { + Some(f) => f, + None => return false, + }; + frame.copy(&wl_buf); + + state.chosen_dmabuf = Some(DmabufChosen { + format: offer.format, + width: offer.width, + height: offer.height, + stride, + }); + state.gbm_bo = Some(bo); + state.wl_buffer = Some(wl_buf); + // dma_fd drops here; the compositor dup'd it inside params.add(). + true +} + +/// Walk /dev/dri/renderD128..135 and return the first one we can open. +fn find_drm_render_node() -> Option { + for n in 128..=135u32 { + if let Ok(f) = std::fs::OpenOptions::new() + .read(true) + .write(true) + .open(format!("/dev/dri/renderD{n}")) + { + return Some(f); + } + } + None +} + +// DRM fourcc constants used by decode_drm_pixels. +// fourcc_code(a,b,c,d) = a | (b<<8) | (c<<16) | (d<<24) +const DRM_FORMAT_ARGB8888: u32 = 0x34325241; // fourcc('A','R','2','4') +const DRM_FORMAT_XRGB8888: u32 = 0x34325258; // fourcc('X','R','2','4') +const DRM_FORMAT_ABGR8888: u32 = 0x34324241; // fourcc('A','B','2','4') +const DRM_FORMAT_XBGR8888: u32 = 0x34324258; // fourcc('X','B','2','4') +const DRM_FORMAT_RGBA8888: u32 = 0x34324152; // fourcc('R','A','2','4') +const DRM_FORMAT_BGRA8888: u32 = 0x34324142; // fourcc('B','A','2','4') +const DRM_FORMAT_XRGB2101010: u32 = 0x30335258; // fourcc('X','R','3','0') +const DRM_FORMAT_ARGB2101010: u32 = 0x30335241; // fourcc('A','R','3','0') +const DRM_FORMAT_XBGR2101010: u32 = 0x30334258; // fourcc('X','B','3','0') +const DRM_FORMAT_ABGR2101010: u32 = 0x30334241; // fourcc('A','B','3','0') + +/// Decode a LINEAR DMA-buf pixel buffer into RGBA8 using the DRM fourcc format. +/// Supports the common 8-bit and 10-bit packed formats the compositor may offer. +fn decode_drm_pixels( + raw: &[u8], + width: u32, + height: u32, + stride: u32, + fourcc: u32, +) -> Result> { + let mut rgba: Vec = Vec::with_capacity((width * height * 4) as usize); + + match fourcc { + DRM_FORMAT_ARGB8888 => { + for row in 0..height { + for col in 0..width { + let b = (row * stride + col * 4) as usize; + rgba.push(raw[b + 2]); + rgba.push(raw[b + 1]); + rgba.push(raw[b]); + rgba.push(raw[b + 3]); + } + } + } + DRM_FORMAT_XRGB8888 => { + for row in 0..height { + for col in 0..width { + let b = (row * stride + col * 4) as usize; + rgba.push(raw[b + 2]); + rgba.push(raw[b + 1]); + rgba.push(raw[b]); + rgba.push(255); + } + } + } + DRM_FORMAT_ABGR8888 => { + for row in 0..height { + for col in 0..width { + let b = (row * stride + col * 4) as usize; + rgba.push(raw[b]); + rgba.push(raw[b + 1]); + rgba.push(raw[b + 2]); + rgba.push(raw[b + 3]); + } + } + } + DRM_FORMAT_XBGR8888 => { + for row in 0..height { + for col in 0..width { + let b = (row * stride + col * 4) as usize; + rgba.push(raw[b]); + rgba.push(raw[b + 1]); + rgba.push(raw[b + 2]); + rgba.push(255); + } + } + } + DRM_FORMAT_RGBA8888 => { + for row in 0..height { + for col in 0..width { + let b = (row * stride + col * 4) as usize; + rgba.push(raw[b + 3]); + rgba.push(raw[b + 2]); + rgba.push(raw[b + 1]); + rgba.push(raw[b]); + } + } + } + DRM_FORMAT_BGRA8888 => { + for row in 0..height { + for col in 0..width { + let b = (row * stride + col * 4) as usize; + rgba.push(raw[b + 1]); + rgba.push(raw[b + 2]); + rgba.push(raw[b + 3]); + rgba.push(raw[b]); + } + } + } + DRM_FORMAT_XRGB2101010 | DRM_FORMAT_ARGB2101010 => { + for row in 0..height { + for col in 0..width { + let b = (row * stride + col * 4) as usize; + let p = u32::from_le_bytes([raw[b], raw[b + 1], raw[b + 2], raw[b + 3]]); + rgba.push((((p >> 20) & 0x3FF) >> 2) as u8); + rgba.push((((p >> 10) & 0x3FF) >> 2) as u8); + rgba.push(((p & 0x3FF) >> 2) as u8); + rgba.push(255); + } + } + } + DRM_FORMAT_XBGR2101010 | DRM_FORMAT_ABGR2101010 => { + for row in 0..height { + for col in 0..width { + let b = (row * stride + col * 4) as usize; + let p = u32::from_le_bytes([raw[b], raw[b + 1], raw[b + 2], raw[b + 3]]); + rgba.push(((p & 0x3FF) >> 2) as u8); + rgba.push((((p >> 10) & 0x3FF) >> 2) as u8); + rgba.push((((p >> 20) & 0x3FF) >> 2) as u8); + rgba.push(255); + } + } + } + other => { + return Err(BlastError::Capture(format!( + "unhandled DRM format 0x{other:08x} in dmabuf capture -> please report this" + ))); + } + } + + Ok(rgba) +} + +// Wayland dispatch + +impl Dispatch for AppState { + fn event( + _: &mut Self, + _: &wl_registry::WlRegistry, + _: wl_registry::Event, + _: &GlobalListContents, + _: &Connection, + _: &QueueHandle, + ) { + } +} + +/// Collect the formats wl_shm supports so we can pick one the compositor offers. +impl Dispatch for AppState { + fn event( + state: &mut Self, + _: &wl_shm::WlShm, + event: wl_shm::Event, + _: &(), + _: &Connection, + _: &QueueHandle, + ) { + if let wl_shm::Event::Format { format } = event { + if let wayland_client::WEnum::Value(f) = format { + state.shm_formats.push(f); + } + } + } +} + +impl Dispatch for AppState { + fn event( + state: &mut Self, + _proxy: &wl_output::WlOutput, + event: wl_output::Event, + idx: &usize, + _: &Connection, + _: &QueueHandle, + ) { + let Some(out) = state.outputs.get_mut(*idx) else { + return; + }; + match event { + wl_output::Event::Name { name } => out.name = name, + wl_output::Event::Mode { + flags, + width, + height, + .. + } => { + if let wayland_client::WEnum::Value(f) = flags { + if f.contains(wl_output::Mode::Current) { + out.hw_w = width; + out.hw_h = height; + } + } + } + _ => {} + } + } +} + +impl Dispatch for AppState { + fn event( + state: &mut Self, + _proxy: &ZxdgOutputV1, + event: zxdg_output_v1::Event, + idx: &usize, + _: &Connection, + _: &QueueHandle, + ) { + let Some(out) = state.outputs.get_mut(*idx) else { + return; + }; + match event { + zxdg_output_v1::Event::LogicalPosition { x, y } => { + out.logical_x = x; + out.logical_y = y; + } + zxdg_output_v1::Event::LogicalSize { width, height } => { + out.logical_w = width; + out.logical_h = height; + } + _ => {} + } + } +} + +impl Dispatch for AppState { + fn event( + state: &mut Self, + _frame: &zwlr_screencopy_frame_v1::ZwlrScreencopyFrameV1, + event: zwlr_screencopy_frame_v1::Event, + _: &(), + _: &Connection, + _: &QueueHandle, + ) { + match event { + zwlr_screencopy_frame_v1::Event::Buffer { + format, + width, + height, + stride, + } => { + if let wayland_client::WEnum::Value(fmt) = format { + state.buffer_offers.push(BufferOffer { + format: fmt, + width, + height, + stride, + }); + } + } + + zwlr_screencopy_frame_v1::Event::LinuxDmabuf { + format, + width, + height, + } => { + state.dmabuf_offers.push(DmabufOffer { + format, + width, + height, + }); + } + + zwlr_screencopy_frame_v1::Event::BufferDone => { + if state.alloc_and_copy() { + state.frame_state = FrameState::Copying; + } else { + state.frame_state = FrameState::Failed; + } + } + + zwlr_screencopy_frame_v1::Event::Ready { .. } => { + state.frame_state = FrameState::Ready; + } + + zwlr_screencopy_frame_v1::Event::Failed => { + state.frame_state = FrameState::Failed; + } + + _ => {} + } + } +} + +impl Dispatch for AppState { + fn event( + _: &mut Self, + _: &zwlr_screencopy_manager_v1::ZwlrScreencopyManagerV1, + _: zwlr_screencopy_manager_v1::Event, + _: &(), + _: &Connection, + _: &QueueHandle, + ) { + } +} + +// wl_shm is dispatched above, not noop +delegate_noop!(AppState: ignore wl_shm_pool::WlShmPool); +delegate_noop!(AppState: ignore wl_buffer::WlBuffer); +delegate_noop!(AppState: ignore ZwpLinuxDmabufV1); +delegate_noop!(AppState: ignore ZwpLinuxBufferParamsV1); +delegate_noop!(AppState: ignore ZxdgOutputManagerV1); + +fn create_shm_file(size: usize) -> std::io::Result { + #[cfg(target_os = "linux")] + { + use std::ffi::CStr; + let fd = unsafe { + libc::memfd_create( + CStr::from_bytes_with_nul(b"blast-shm\0").unwrap().as_ptr(), + libc::MFD_CLOEXEC, + ) + }; + if fd >= 0 { + let file = unsafe { File::from_raw_fd(fd) }; + file.set_len(size as u64)?; + return Ok(file); + } + } + let f = tempfile::tempfile()?; + f.set_len(size as u64)?; + Ok(f) +} diff --git a/src/clipboard.rs b/src/clipboard.rs new file mode 100644 index 0000000..e45ec7f --- /dev/null +++ b/src/clipboard.rs @@ -0,0 +1,16 @@ +use std::thread; + +use wl_clipboard_rs::copy::{MimeType, Options, Source}; + +use crate::error::{BlastError, Result}; + +pub fn copy_png(png_bytes: Vec) -> Result<()> { + Options::new() + .copy( + Source::Bytes(png_bytes.into()), + MimeType::Specific("image/png".into()), + ) + .map_err(|e| BlastError::Clipboard(format!("{e}")))?; + thread::sleep(std::time::Duration::from_millis(50)); + Ok(()) +} diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..9395546 --- /dev/null +++ b/src/error.rs @@ -0,0 +1,35 @@ +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum BlastError { + #[error("Hyprland IPC error: {0}")] + Hyprland(String), + + #[error("Screenshot capture error: {0}")] + Capture(String), + + #[error("Region selection cancelled")] + SelectionCancelled, + + #[error("Region selection error: {0}")] + Selection(String), + + #[error("Clipboard error: {0}")] + Clipboard(String), + + #[error("Notification error: {0}")] + #[allow(dead_code)] + Notify(String), + + #[error("Image processing error: {0}")] + Image(String), + + #[error("IO error: {0}")] + Io(#[from] std::io::Error), + + #[error("{0}")] + #[allow(dead_code)] + Other(String), +} + +pub type Result = std::result::Result; diff --git a/src/freeze.rs b/src/freeze.rs new file mode 100644 index 0000000..68a2051 --- /dev/null +++ b/src/freeze.rs @@ -0,0 +1,911 @@ +//! 1. For every output, screencopy-capture one frame into a SHM buffer. +//! 2. Create a zwlr_layer_shell_v1 surface at the OVERLAY layer covering +//! the full output with exclusive input (so the compositor stops rendering +//! live content underneath). +//! 3. Paint the captured SHM pixels onto the layer surface's wl_buffer and +//! commit. +//! 4. When FreezeGuard::drop() is called, destroy all layer surfaces and +//! the compositor resumes normal rendering. +//! +//! With --freeze-dmabuf, step 1 allocates a GBM buffer object instead of a +//! SHM file. The compositor fills the DMA-buf directly and the same object is +//! attached to the layer surface, keeping everything on-GPU and avoiding the +//! 10-bit bit-packing misinterpretation that occurs with the SHM path. + +use std::{ + fs::File, + io::{Read, Seek, SeekFrom, Write}, + os::unix::io::{AsFd, FromRawFd, IntoRawFd, OwnedFd}, +}; + +use gbm::{BufferObjectFlags, Device as GbmDevice}; +use wayland_client::{ + delegate_noop, + globals::{registry_queue_init, GlobalListContents}, + protocol::{wl_buffer, wl_compositor, wl_output, wl_registry, wl_shm, wl_shm_pool, wl_surface}, + Connection, Dispatch, EventQueue, QueueHandle, +}; +use wayland_protocols::wp::linux_dmabuf::zv1::client::{ + zwp_linux_buffer_params_v1::{self, ZwpLinuxBufferParamsV1}, + zwp_linux_dmabuf_v1::ZwpLinuxDmabufV1, +}; +use wayland_protocols_wlr::{ + layer_shell::v1::client::{ + zwlr_layer_shell_v1::{self, ZwlrLayerShellV1}, + zwlr_layer_surface_v1::{self, ZwlrLayerSurfaceV1}, + }, + screencopy::v1::client::{zwlr_screencopy_frame_v1, zwlr_screencopy_manager_v1}, +}; + +use crate::error::{BlastError, Result}; + +pub struct FreezeGuard { + inner: Option, + conn: Option, +} + +struct FreezeInner { + _conn: Connection, + _layer_surfaces: Vec, + _wl_surfaces: Vec, +} + +impl FreezeGuard { + pub fn spawn() -> Result { + match FreezeState::run() { + Ok((inner, conn)) => Ok(FreezeGuard { + inner: Some(inner), + conn, + }), + Err(e) => { + eprintln!("blast: freeze failed ({e}), continuing without freeze"); + Ok(FreezeGuard { + inner: None, + conn: None, + }) + } + } + } + + pub fn none() -> Self { + FreezeGuard { + inner: None, + conn: None, + } + } + + pub fn kill(&mut self) { + self.inner = None; + if let Some(conn) = self.conn.take() { + let _ = conn.flush(); + } + } +} + +impl Drop for FreezeGuard { + fn drop(&mut self) { + self.kill(); + } +} + +#[derive(Debug, PartialEq)] +enum FrameState { + Pending, + Ready, + Failed, +} + +#[derive(Clone)] +struct BufferOffer { + format: wl_shm::Format, + width: u32, + height: u32, + stride: u32, +} + +/// DRM fourcc format + dimensions from screencopy's linux_dmabuf event. +#[derive(Clone)] +struct DmabufOffer { + format: u32, // DRM fourcc + width: u32, + height: u32, +} + +struct OutputCapture { + output: wl_output::WlOutput, + output_name: String, + buffer_offers: Vec, + dmabuf_offers: Vec, + shm_file: Option, + shm_pool: Option, + buffer: Option, + /// Keeps the GBM buffer object alive for the lifetime of the overlay surface. + /// The wl_buffer references the underlying DMA-buf fd; the BO must not drop + /// before the compositor is done with it. + _gbm_bo: Option>, + frame_state: FrameState, + frame_width: u32, + frame_height: u32, + frame_stride: u32, + frame_format: wl_shm::Format, +} + +// state machine +struct FreezeState { + conn: Connection, + qh: QueueHandle, + queue: Option>, + + compositor: Option, + shm: Option, + shm_formats: Vec, + layer_shell: Option, + screencopy_mgr: Option, + /// Present when the compositor supports zwp_linux_dmabuf_v1. + linux_dmabuf: Option, + + captures: Vec, + + layer_surfaces: Vec, + wl_surfaces: Vec, + configured_count: usize, +} + +impl FreezeState { + fn run() -> Result<(FreezeInner, Option)> { + let conn = Connection::connect_to_env() + .map_err(|e| BlastError::Capture(format!("freeze: connect: {e}")))?; + + let (globals, queue) = registry_queue_init::(&conn) + .map_err(|e| BlastError::Capture(format!("freeze: registry: {e}")))?; + + let qh = queue.handle(); + + let compositor: wl_compositor::WlCompositor = globals + .bind(&qh, 4..=6, ()) + .map_err(|e| BlastError::Capture(format!("freeze: compositor: {e}")))?; + + let shm: wl_shm::WlShm = globals + .bind(&qh, 1..=1, ()) + .map_err(|e| BlastError::Capture(format!("freeze: shm: {e}")))?; + + let layer_shell: ZwlrLayerShellV1 = globals + .bind(&qh, 1..=4, ()) + .map_err(|e| BlastError::Capture(format!("freeze: layer_shell: {e}")))?; + + let screencopy_mgr: zwlr_screencopy_manager_v1::ZwlrScreencopyManagerV1 = globals + .bind(&qh, 1..=3, ()) + .map_err(|e| BlastError::Capture(format!("freeze: screencopy_mgr: {e}")))?; + + // Always attempt dmabuf; falls back to SHM if unavailable. + let linux_dmabuf: Option = globals.bind(&qh, 2..=4, ()).ok(); + + let raw_outputs: Vec = globals + .contents() + .clone_list() + .iter() + .filter(|g| g.interface == "wl_output") + .map(|g| { + globals.registry().bind::( + g.name, + g.version.min(4), + &qh, + (), + ) + }) + .collect(); + + let captures: Vec = raw_outputs + .into_iter() + .map(|o| OutputCapture { + output: o, + output_name: String::new(), + buffer_offers: Vec::new(), + dmabuf_offers: Vec::new(), + shm_file: None, + shm_pool: None, + buffer: None, + _gbm_bo: None, + frame_state: FrameState::Pending, + frame_width: 0, + frame_height: 0, + frame_stride: 0, + frame_format: wl_shm::Format::Argb8888, + }) + .collect(); + + let mut state = FreezeState { + conn, + qh, + queue: Some(queue), + compositor: Some(compositor), + shm: Some(shm), + shm_formats: Vec::new(), + layer_shell: Some(layer_shell), + screencopy_mgr: Some(screencopy_mgr), + linux_dmabuf, + captures, + layer_surfaces: Vec::new(), + wl_surfaces: Vec::new(), + configured_count: 0, + }; + + state.roundtrip()?; + + for i in 0..state.captures.len() { + let output = state.captures[i].output.clone(); + let _frame = state + .screencopy_mgr + .as_ref() + .unwrap() + .capture_output(0, &output, &state.qh, i); + } + + state.dispatch_until_frames_ready()?; + + state.create_overlay_surfaces()?; + + state.roundtrip()?; + + let kill_conn = Connection::connect_to_env().ok(); + + Ok(( + FreezeInner { + _conn: state.conn, + _layer_surfaces: state.layer_surfaces, + _wl_surfaces: state.wl_surfaces, + }, + kill_conn, + )) + } + + fn roundtrip(&mut self) -> Result<()> { + let mut q = self + .queue + .take() + .ok_or_else(|| BlastError::Capture("freeze: queue missing".into()))?; + let r = q + .roundtrip(self) + .map_err(|e| BlastError::Capture(format!("freeze: roundtrip: {e}"))); + self.queue = Some(q); + r?; + Ok(()) + } + + fn dispatch_once(&mut self) -> Result<()> { + let mut q = self + .queue + .take() + .ok_or_else(|| BlastError::Capture("freeze: queue missing".into()))?; + let r = q + .blocking_dispatch(self) + .map_err(|e| BlastError::Capture(format!("freeze: dispatch: {e}"))); + self.queue = Some(q); + r?; + Ok(()) + } + + fn dispatch_until_frames_ready(&mut self) -> Result<()> { + loop { + if self + .captures + .iter() + .all(|c| c.frame_state != FrameState::Pending) + { + break; + } + self.dispatch_once()?; + } + Ok(()) + } + + fn dispatch_until_configured(&mut self) -> Result<()> { + let expected = self + .captures + .iter() + .filter(|c| c.frame_state == FrameState::Ready) + .count(); + while self.configured_count < expected { + self.dispatch_once()?; + } + Ok(()) + } + + fn alloc_shm(size: usize) -> Result { + #[cfg(target_os = "linux")] + { + use std::ffi::CStr; + let fd = unsafe { + libc::memfd_create( + CStr::from_bytes_with_nul(b"blast-freeze\0") + .unwrap() + .as_ptr(), + libc::MFD_CLOEXEC, + ) + }; + if fd >= 0 { + let f = unsafe { File::from_raw_fd(fd) }; + f.set_len(size as u64) + .map_err(|e| BlastError::Capture(format!("freeze: set_len: {e}")))?; + return Ok(f); + } + } + let f = tempfile::tempfile() + .map_err(|e| BlastError::Capture(format!("freeze: tempfile: {e}")))?; + f.set_len(size as u64) + .map_err(|e| BlastError::Capture(format!("freeze: set_len: {e}")))?; + Ok(f) + } + + fn create_overlay_surfaces(&mut self) -> Result<()> { + use wayland_protocols_wlr::layer_shell::v1::client::{ + zwlr_layer_shell_v1::Layer, zwlr_layer_surface_v1::Anchor, + }; + + let mons = crate::hyprland::monitors().unwrap_or_default(); + + for i in 0..self.captures.len() { + if self.captures[i].frame_state != FrameState::Ready { + continue; + } + + let output = self.captures[i].output.clone(); + let phys_w = self.captures[i].frame_width; + let phys_h = self.captures[i].frame_height; + + let scale = mons + .iter() + .find(|m| m.name == self.captures[i].output_name) + .map(|m| m.scale) + .unwrap_or(1.0); + + let logical_w = (phys_w as f64 / scale).round() as u32; + let logical_h = (phys_h as f64 / scale).round() as u32; + + let wl_surf = self + .compositor + .as_ref() + .unwrap() + .create_surface(&self.qh, ()); + + let layer_surf = self.layer_shell.as_ref().unwrap().get_layer_surface( + &wl_surf, + Some(&output), + Layer::Overlay, + "blast-freeze".to_string(), + &self.qh, + i, + ); + + layer_surf.set_size(0, 0); + layer_surf.set_exclusive_zone(-1); + layer_surf.set_anchor(Anchor::Top | Anchor::Bottom | Anchor::Left | Anchor::Right); + layer_surf + .set_keyboard_interactivity(zwlr_layer_surface_v1::KeyboardInteractivity::None); + + let int_scale = scale.ceil() as i32; + wl_surf.set_buffer_scale(int_scale); + + // Empty input region so the cursor passes through without + // triggering compositor input processing (which causes lag). + let empty_region = self + .compositor + .as_ref() + .unwrap() + .create_region(&self.qh, ()); + wl_surf.set_input_region(Some(&empty_region)); + empty_region.destroy(); + + wl_surf.commit(); + + self.layer_surfaces.push(layer_surf); + self.wl_surfaces.push(wl_surf); + + let _ = logical_w; + let _ = logical_h; + } + + self.dispatch_until_configured()?; + + let mut surf_idx = 0; + for i in 0..self.captures.len() { + if self.captures[i].frame_state != FrameState::Ready { + continue; + } + if let (Some(surf), Some(buf)) = ( + self.wl_surfaces.get(surf_idx), + self.captures[i].buffer.as_ref(), + ) { + surf.attach(Some(buf), 0, 0); + surf.damage_buffer(0, 0, i32::MAX, i32::MAX); + surf.commit(); + } + surf_idx += 1; + } + + Ok(()) + } +} + +// Format selection helpers +/// Pick the best 8-bit SHM format from screencopy offers, constrained to what +/// wl_shm advertised. Mirrors the priority list used in capture.rs. +fn pick_offer(offers: &[BufferOffer], shm_formats: &[wl_shm::Format]) -> Option { + const PREFERRED: &[wl_shm::Format] = &[ + wl_shm::Format::Argb8888, + wl_shm::Format::Xrgb8888, + wl_shm::Format::Abgr8888, + wl_shm::Format::Xbgr8888, + wl_shm::Format::Rgba8888, + wl_shm::Format::Bgra8888, + ]; + for &fmt in PREFERRED { + if let Some(o) = offers + .iter() + .find(|o| o.format == fmt && shm_formats.contains(&o.format)) + { + return Some(o.clone()); + } + } + // Last resort: anything wl_shm supports (may still be 10-bit). + offers + .iter() + .find(|o| shm_formats.contains(&o.format)) + .cloned() +} + +fn is_10bit(fmt: wl_shm::Format) -> bool { + matches!( + fmt, + wl_shm::Format::Xrgb2101010 + | wl_shm::Format::Argb2101010 + | wl_shm::Format::Xbgr2101010 + | wl_shm::Format::Abgr2101010 + | wl_shm::Format::Rgbx1010102 + | wl_shm::Format::Rgba1010102 + | wl_shm::Format::Bgrx1010102 + | wl_shm::Format::Bgra1010102 + ) +} + +// DMA-buf path + +/// Walk /dev/dri/renderD128..135 and return the first one we can open. +fn find_drm_render_node() -> Option { + for n in 128..=135u32 { + if let Ok(f) = std::fs::OpenOptions::new() + .read(true) + .write(true) + .open(format!("/dev/dri/renderD{n}")) + { + return Some(f); + } + } + None +} + +/// Allocate a GBM buffer object for the given DMA-buf offer, export its fd, +/// create a wl_buffer via zwp_linux_dmabuf_v1, and kick off the screencopy +/// copy. Stores the resulting wl_buffer and BO in captures[i]. +/// +/// Returns true on success. On failure prints a diagnostic and returns false +/// so the caller can fall back to the SHM path. +fn alloc_dmabuf_capture( + state: &mut FreezeState, + i: usize, + frame: &zwlr_screencopy_frame_v1::ZwlrScreencopyFrameV1, + qh: &QueueHandle, +) -> bool { + let offer = match state.captures[i].dmabuf_offers.first().cloned() { + Some(o) => o, + None => { + eprintln!("blast: freeze: dmabuf: no linux_dmabuf offers received from compositor"); + return false; + } + }; + + let linux_dmabuf = match state.linux_dmabuf.clone() { + Some(d) => d, + None => return false, + }; + + let drm_file = match find_drm_render_node() { + Some(f) => f, + None => { + eprintln!( + "blast: freeze: dmabuf: no DRM render node found under /dev/dri/renderD128-135" + ); + return false; + } + }; + + let gbm = match GbmDevice::new(drm_file) { + Ok(d) => d, + Err(e) => { + eprintln!("blast: freeze: dmabuf: GBM device: {e}"); + return false; + } + }; + + let fmt = match gbm::Format::try_from(offer.format) { + Ok(f) => f, + Err(_) => { + eprintln!( + "blast: freeze: dmabuf: unrecognised DRM fourcc 0x{:08x}", + offer.format + ); + return false; + } + }; + + // RENDERING | SCANOUT: we need a buffer the compositor can both write into + // (screencopy) and display (layer surface attachment). LINEAR is intentionally + // omitted - drivers may not support it for a given format, causing EINVAL. + // Instead we read the actual modifier back from the allocated BO and forward + // it to zwp_linux_buffer_params_v1 so the compositor interprets the tiling + // correctly. + let bo = match gbm.create_buffer_object::<()>( + offer.width, + offer.height, + fmt, + BufferObjectFlags::RENDERING | BufferObjectFlags::SCANOUT, + ) { + Ok(b) => b, + Err(e) => { + eprintln!("blast: freeze: dmabuf: GBM alloc: {e}"); + return false; + } + }; + + let stride = bo.stride(); + let modifier = u64::from(bo.modifier()); + let modifier_hi = (modifier >> 32) as u32; + let modifier_lo = (modifier & 0xFFFF_FFFF) as u32; + + let dma_fd: OwnedFd = match bo.fd() { + Ok(f) => f, + Err(e) => { + eprintln!("blast: freeze: dmabuf: GBM fd export: {e}"); + return false; + } + }; + + let params = linux_dmabuf.create_params(qh, ()); + params.add(dma_fd.as_fd(), 0, 0, stride, modifier_hi, modifier_lo); + // dma_fd drops here; the compositor has already dup'd it inside add(). + + let wl_buf = params.create_immed( + offer.width as i32, + offer.height as i32, + offer.format, + zwp_linux_buffer_params_v1::Flags::empty(), + qh, + (), + ); + + frame.copy(&wl_buf); + + state.captures[i].buffer = Some(wl_buf); + state.captures[i].frame_width = offer.width; + state.captures[i].frame_height = offer.height; + state.captures[i].frame_stride = stride; + // _gbm_bo keeps the DMA-buf alive until the overlay surface is destroyed. + // The GBM Device is kept alive internally by the BO's Arc reference. + state.captures[i]._gbm_bo = Some(bo); + + true +} + +// SHM 10-bit fallback conversion + +/// Called after screencopy Ready when a 10-bit SHM format was the only option. +/// Reads the raw data, converts each pixel to XRGB8888, and replaces the +/// capture's pool/buffer with a new 8-bit one safe for wl_surface attachment. +fn convert_capture_to_8bit( + state: &mut FreezeState, + i: usize, + qh: &QueueHandle, +) -> std::result::Result<(), String> { + let w = state.captures[i].frame_width; + let h = state.captures[i].frame_height; + let stride = state.captures[i].frame_stride; + let fmt = state.captures[i].frame_format; + + let file = state.captures[i].shm_file.as_mut().ok_or("no shm file")?; + file.seek(SeekFrom::Start(0)).map_err(|e| e.to_string())?; + let mut raw = vec![0u8; (stride * h) as usize]; + file.read_exact(&mut raw).map_err(|e| e.to_string())?; + + let out_stride = w * 4; + let mut out = vec![0u8; (out_stride * h) as usize]; + for row in 0..h { + for col in 0..w { + let src = (row * stride + col * 4) as usize; + let pixel = u32::from_le_bytes([raw[src], raw[src + 1], raw[src + 2], raw[src + 3]]); + let (r, g, b) = match fmt { + // A/X[31:30] R[29:20] G[19:10] B[9:0] + wl_shm::Format::Xrgb2101010 | wl_shm::Format::Argb2101010 => ( + ((pixel >> 20) & 0x3FF) >> 2, + ((pixel >> 10) & 0x3FF) >> 2, + (pixel & 0x3FF) >> 2, + ), + // A/X[31:30] B[29:20] G[19:10] R[9:0] + wl_shm::Format::Xbgr2101010 | wl_shm::Format::Abgr2101010 => ( + (pixel & 0x3FF) >> 2, + ((pixel >> 10) & 0x3FF) >> 2, + ((pixel >> 20) & 0x3FF) >> 2, + ), + // R[31:22] G[21:12] B[11:2] A/X[1:0] + wl_shm::Format::Rgbx1010102 | wl_shm::Format::Rgba1010102 => ( + ((pixel >> 22) & 0x3FF) >> 2, + ((pixel >> 12) & 0x3FF) >> 2, + ((pixel >> 2) & 0x3FF) >> 2, + ), + // B[31:22] G[21:12] R[11:2] A/X[1:0] + wl_shm::Format::Bgrx1010102 | wl_shm::Format::Bgra1010102 => ( + ((pixel >> 2) & 0x3FF) >> 2, + ((pixel >> 12) & 0x3FF) >> 2, + ((pixel >> 22) & 0x3FF) >> 2, + ), + _ => (0, 0, 0), + }; + let dst = (row * out_stride + col * 4) as usize; + // XRGB8888 memory layout: [B, G, R, X] + out[dst] = b as u8; + out[dst + 1] = g as u8; + out[dst + 2] = r as u8; + out[dst + 3] = 0xFF; + } + } + + let new_size = out.len(); + let mut new_file = FreezeState::alloc_shm(new_size).map_err(|e| e.to_string())?; + new_file.write_all(&out).map_err(|e| e.to_string())?; + + let fd = new_file + .try_clone() + .map_err(|e| e.to_string())? + .into_raw_fd(); + let new_pool = state.shm.as_ref().unwrap().create_pool( + unsafe { std::os::unix::io::BorrowedFd::borrow_raw(fd) }, + new_size as i32, + qh, + (), + ); + let new_buf = new_pool.create_buffer( + 0, + w as i32, + h as i32, + out_stride as i32, + wl_shm::Format::Xrgb8888, + qh, + (), + ); + + state.captures[i].shm_file = Some(new_file); + state.captures[i].shm_pool = Some(new_pool); + state.captures[i].buffer = Some(new_buf); + state.captures[i].frame_stride = out_stride; + state.captures[i].frame_format = wl_shm::Format::Xrgb8888; + + Ok(()) +} + +impl Dispatch for FreezeState { + fn event( + _: &mut Self, + _: &wl_registry::WlRegistry, + _: wl_registry::Event, + _: &GlobalListContents, + _: &Connection, + _: &QueueHandle, + ) { + } +} + +impl Dispatch for FreezeState { + fn event( + state: &mut Self, + proxy: &wl_output::WlOutput, + event: wl_output::Event, + _: &(), + _: &Connection, + _: &QueueHandle, + ) { + if let wl_output::Event::Name { name } = event { + if let Some(cap) = state.captures.iter_mut().find(|c| &c.output == proxy) { + cap.output_name = name; + } + } + } +} + +impl Dispatch for FreezeState { + fn event( + state: &mut Self, + _: &wl_shm::WlShm, + event: wl_shm::Event, + _: &(), + _: &Connection, + _: &QueueHandle, + ) { + if let wl_shm::Event::Format { format } = event { + if let wayland_client::WEnum::Value(f) = format { + state.shm_formats.push(f); + } + } + } +} + +impl Dispatch for FreezeState { + fn event( + state: &mut Self, + frame: &zwlr_screencopy_frame_v1::ZwlrScreencopyFrameV1, + event: zwlr_screencopy_frame_v1::Event, + idx: &usize, + _: &Connection, + qh: &QueueHandle, + ) { + let i = *idx; + match event { + zwlr_screencopy_frame_v1::Event::Buffer { + format, + width, + height, + stride, + } => { + if let wayland_client::WEnum::Value(fmt) = format { + state.captures[i].buffer_offers.push(BufferOffer { + format: fmt, + width, + height, + stride, + }); + } + } + zwlr_screencopy_frame_v1::Event::LinuxDmabuf { + format, + width, + height, + } => { + state.captures[i].dmabuf_offers.push(DmabufOffer { + format, + width, + height, + }); + } + zwlr_screencopy_frame_v1::Event::BufferDone => { + // Always prefer DMA-buf; fall back to SHM on any failure. + let used_dmabuf = + state.linux_dmabuf.is_some() && alloc_dmabuf_capture(state, i, frame, qh); + + if !used_dmabuf { + let offer = + match pick_offer(&state.captures[i].buffer_offers, &state.shm_formats) { + Some(o) => o, + None => { + eprintln!( + "blast: freeze: no supported shm format in offers: {:?}", + state.captures[i] + .buffer_offers + .iter() + .map(|o| o.format) + .collect::>() + ); + state.captures[i].frame_state = FrameState::Failed; + return; + } + }; + + let size = (offer.stride * offer.height) as usize; + let file = match FreezeState::alloc_shm(size) { + Ok(f) => f, + Err(e) => { + eprintln!("blast: freeze: shm alloc: {e}"); + state.captures[i].frame_state = FrameState::Failed; + return; + } + }; + + let fd = match file.try_clone() { + Ok(f) => f.into_raw_fd(), + Err(e) => { + eprintln!("blast: freeze: fd clone: {e}"); + state.captures[i].frame_state = FrameState::Failed; + return; + } + }; + + let pool = state.shm.as_ref().unwrap().create_pool( + unsafe { std::os::unix::io::BorrowedFd::borrow_raw(fd) }, + size as i32, + qh, + (), + ); + let buf = pool.create_buffer( + 0, + offer.width as i32, + offer.height as i32, + offer.stride as i32, + offer.format, + qh, + (), + ); + + state.captures[i].shm_file = Some(file); + state.captures[i].shm_pool = Some(pool); + state.captures[i].buffer = Some(buf.clone()); + state.captures[i].frame_width = offer.width; + state.captures[i].frame_height = offer.height; + state.captures[i].frame_stride = offer.stride; + state.captures[i].frame_format = offer.format; + + frame.copy(&buf); + } + } + zwlr_screencopy_frame_v1::Event::Ready { .. } => { + // DMA-buf path: buffer is already on-GPU, no conversion needed. + // SHM path: convert 10-bit data to XRGB8888 if necessary. + let is_dmabuf = state.captures[i]._gbm_bo.is_some(); + if !is_dmabuf && is_10bit(state.captures[i].frame_format) { + if let Err(e) = convert_capture_to_8bit(state, i, qh) { + eprintln!("blast: freeze: 10-bit conversion failed: {e}"); + } + } + state.captures[i].frame_state = FrameState::Ready; + } + zwlr_screencopy_frame_v1::Event::Failed => { + state.captures[i].frame_state = FrameState::Failed; + } + _ => {} + } + } +} + +impl Dispatch for FreezeState { + fn event( + state: &mut Self, + surf: &ZwlrLayerSurfaceV1, + event: zwlr_layer_surface_v1::Event, + _: &usize, + _: &Connection, + _: &QueueHandle, + ) { + if let zwlr_layer_surface_v1::Event::Configure { serial, .. } = event { + surf.ack_configure(serial); + state.configured_count += 1; + } + } +} + +impl Dispatch for FreezeState { + fn event( + _: &mut Self, + _: &ZwlrLayerShellV1, + _: zwlr_layer_shell_v1::Event, + _: &(), + _: &Connection, + _: &QueueHandle, + ) { + } +} + +impl Dispatch for FreezeState { + fn event( + _: &mut Self, + _: &zwlr_screencopy_manager_v1::ZwlrScreencopyManagerV1, + _: zwlr_screencopy_manager_v1::Event, + _: &(), + _: &Connection, + _: &QueueHandle, + ) { + } +} + +delegate_noop!(FreezeState: ignore wl_compositor::WlCompositor); +delegate_noop!(FreezeState: ignore wl_surface::WlSurface); +delegate_noop!(FreezeState: ignore wayland_client::protocol::wl_region::WlRegion); +delegate_noop!(FreezeState: ignore wl_shm_pool::WlShmPool); +delegate_noop!(FreezeState: ignore wl_buffer::WlBuffer); +delegate_noop!(FreezeState: ignore ZwpLinuxDmabufV1); +delegate_noop!(FreezeState: ignore ZwpLinuxBufferParamsV1); diff --git a/src/gui/annotations.rs b/src/gui/annotations.rs new file mode 100644 index 0000000..2396faa --- /dev/null +++ b/src/gui/annotations.rs @@ -0,0 +1,1091 @@ +//! Annotation layer for blast-gui preview. +//! +//! All coordinates are stored in **image-space** (pixels of the source image) +//! so they are zoom/pan independent and can be flattened onto export. + +use eframe::egui::{self, Color32, Pos2, Rect, Stroke}; +use image::{DynamicImage, GenericImageView, Rgba}; + +// Tool selection + +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum Tool { + Select, + Rect, // outline; fill controlled by AnnotationLayer::rect_filled + Arrow, + Text, + Highlight, + Pixelate, + Pencil, +} + +impl Tool { + pub const ALL: &'static [Tool] = &[ + Tool::Select, + Tool::Rect, + Tool::Arrow, + Tool::Text, + Tool::Highlight, + Tool::Pixelate, + Tool::Pencil, + ]; + + pub fn label(self) -> &'static str { + match self { + Tool::Select => "✦ Select", + Tool::Rect => "▭ Box", + Tool::Arrow => "➜ Arrow", + Tool::Text => "T Text", + Tool::Highlight => "▮ Hi-lite", + Tool::Pixelate => "⊞ Censor", + Tool::Pencil => "✏ Pencil", + } + } +} + +// Annotation data + +#[derive(Debug, Clone)] +pub enum Annotation { + Rect { + r: [f32; 4], // x0,y0,x1,y1 in image-space + color: Color32, + thickness: f32, + filled: bool, + }, + Arrow { + from: [f32; 2], + to: [f32; 2], + color: Color32, + thickness: f32, + }, + Text { + pos: [f32; 2], + text: String, + color: Color32, + size: f32, + }, + Highlight { + r: [f32; 4], + color: Color32, + }, + Pixelate { + r: [f32; 4], + block: u32, + }, + Pencil { + points: Vec<[f32; 2]>, + color: Color32, + thickness: f32, + }, +} + +impl Annotation { + /// Returns true if the image-space point "p" hits this annotation. + pub fn hit_test(&self, p: Pos2, tolerance: f32) -> bool { + match self { + Annotation::Rect { r, .. } + | Annotation::Highlight { r, .. } + | Annotation::Pixelate { r, .. } => { + let outer = Rect::from_min_max( + Pos2::new(r[0] - tolerance, r[1] - tolerance), + Pos2::new(r[2] + tolerance, r[3] + tolerance), + ); + outer.contains(p) + } + Annotation::Arrow { from, to, .. } => { + point_seg_dist(p, Pos2::new(from[0], from[1]), Pos2::new(to[0], to[1])) < tolerance + } + Annotation::Text { + pos, text, size, .. + } => { + // Estimate bounding box: ~0.55 * size per character wide, size tall + let est_w = text.len() as f32 * size * 0.55; + let est_h = *size; + let r = Rect::from_min_max( + Pos2::new(pos[0] - tolerance, pos[1] - tolerance), + Pos2::new(pos[0] + est_w + tolerance, pos[1] + est_h + tolerance), + ); + r.contains(p) + } + Annotation::Pencil { points, .. } => points.windows(2).any(|w| { + point_seg_dist(p, Pos2::new(w[0][0], w[0][1]), Pos2::new(w[1][0], w[1][1])) + < tolerance + }), + } + } +} + +fn point_seg_dist(p: Pos2, a: Pos2, b: Pos2) -> f32 { + let ab = b - a; + let ap = p - a; + let len_sq = ab.length_sq(); + if len_sq < 1e-6 { + return ap.length(); + } + let t = (ap.dot(ab) / len_sq).clamp(0.0, 1.0); + (p - (a + ab * t)).length() +} + +// Drag + +#[derive(Debug, Clone)] +pub enum DragState { + None, + Dragging { + start: Pos2, + current: Pos2, + }, + /// Dragging an existing annotation: idx = which one, last = previous image-space pointer pos + Moving { + idx: usize, + last: Pos2, + }, + PencilStroke { + points: Vec, + }, + TextInput { + pos: [f32; 2], + buf: String, + }, +} + +// Annotation Layer + +pub struct AnnotationLayer { + pub tool: Tool, + pub color: Color32, + pub thickness: f32, + pub text_size: f32, + pub pixel_block: u32, + pub rect_filled: bool, + pub annotations: Vec, + pub drag: DragState, + pub text_pending: Option<[f32; 2]>, + pub selected: Option, +} + +impl Default for AnnotationLayer { + fn default() -> Self { + Self { + tool: Tool::Rect, + color: Color32::from_rgb(230, 60, 60), + thickness: 2.5, + text_size: 40.0, + pixel_block: 12, + rect_filled: false, + annotations: Vec::new(), + drag: DragState::None, + text_pending: None, + selected: None, + } + } +} + +impl AnnotationLayer { + pub fn clear(&mut self) { + self.annotations.clear(); + self.drag = DragState::None; + self.text_pending = None; + self.selected = None; + } + + pub fn undo(&mut self) { + self.annotations.pop(); + self.selected = None; + } + + pub fn delete_selected(&mut self) { + if let Some(idx) = self.selected.take() { + if idx < self.annotations.len() { + self.annotations.remove(idx); + } + } + } + + /// Handle pointer + keyboard events on the canvas. + pub fn handle_input( + &mut self, + ui: &egui::Ui, + canvas_rect: Rect, + to_img: impl Fn(Pos2) -> Pos2, + ) { + let pointer = ui.input(|i| i.pointer.clone()); + let hover = pointer.hover_pos(); + let pressed = pointer.primary_pressed(); + let down = pointer.primary_down(); + let released = pointer.primary_released(); + + // Select + if self.tool == Tool::Select { + if ui.input(|i| i.key_pressed(egui::Key::Delete) || i.key_pressed(egui::Key::Backspace)) + { + self.delete_selected(); + return; + } + + let double_click = ui.input(|i| { + i.pointer + .button_double_clicked(egui::PointerButton::Primary) + }); + + if let Some(pos) = hover { + if canvas_rect.contains(pos) { + let ip = to_img(pos); + + if pressed { + // Find topmost hit + let hit = self + .annotations + .iter() + .enumerate() + .rev() + .find(|(_, ann)| ann.hit_test(ip, 8.0)) + .map(|(i, _)| i); + self.selected = hit; + + // Start a move drag if something is selected + if self.selected.is_some() { + self.drag = DragState::Moving { + idx: self.selected.unwrap(), + last: ip, + }; + } + } + + if double_click { + if let Some(idx) = self.selected { + if idx < self.annotations.len() { + if let Annotation::Text { + pos, text, size, .. + } = self.annotations[idx].clone() + { + self.annotations.remove(idx); + self.selected = None; + self.text_size = size; // already image-space, matches slider + self.text_pending = Some(pos); + self.drag = DragState::TextInput { pos, buf: text }; + return; + } + } + } + } + + // Accumulate move delta while button held + if down { + if let DragState::Moving { idx, ref mut last } = self.drag { + let delta = ip - *last; + if delta.length() > 0.5 && idx < self.annotations.len() { + translate_annotation(&mut self.annotations[idx], delta); + *last = ip; + } + } + } + } + } + + if released { + self.drag = DragState::None; + } + return; + } + + if self.tool == Tool::Text { + if let Some(pos) = hover { + if canvas_rect.contains(pos) && pressed { + let ip = to_img(pos); + self.text_pending = Some([ip.x, ip.y]); + self.drag = DragState::None; + } + } + return; + } + + // Pencil + if self.tool == Tool::Pencil { + if let Some(pos) = hover { + if canvas_rect.contains(pos) { + if pressed { + self.drag = DragState::PencilStroke { + points: vec![to_img(pos)], + }; + } else if down { + if let DragState::PencilStroke { ref mut points } = self.drag { + let ip = to_img(pos); + if points + .last() + .map_or(true, |last: &Pos2| (*last - ip).length() > 1.5) + { + points.push(ip); + } + } + } + } + } + if released { + if let DragState::PencilStroke { ref points } = self.drag { + if points.len() >= 2 { + self.annotations.push(Annotation::Pencil { + points: points.iter().map(|p| [p.x, p.y]).collect(), + color: self.color, + thickness: self.thickness, + }); + } + } + self.drag = DragState::None; + } + return; + } + + // Drag to draw + if let Some(pos) = hover { + if canvas_rect.contains(pos) && pressed { + self.drag = DragState::Dragging { + start: to_img(pos), + current: to_img(pos), + }; + } + } + if down { + if let Some(pos) = hover { + if let DragState::Dragging { + ref mut current, .. + } = self.drag + { + *current = to_img(pos); + } + } + } + if released { + if let DragState::Dragging { start, current } = self.drag { + self.commit_drag(start, current); + } + self.drag = DragState::None; + } + } + + fn commit_drag(&mut self, start: Pos2, end: Pos2) { + let r = norm_rect(start, end); + if (r[2] - r[0]).abs() < 3.0 && (r[3] - r[1]).abs() < 3.0 { + return; + } + match self.tool { + Tool::Rect => self.annotations.push(Annotation::Rect { + r, + color: self.color, + thickness: self.thickness, + filled: self.rect_filled, + }), + Tool::Arrow => self.annotations.push(Annotation::Arrow { + from: [start.x, start.y], + to: [end.x, end.y], + color: self.color, + thickness: self.thickness, + }), + Tool::Highlight => self.annotations.push(Annotation::Highlight { + r, + color: Color32::from_rgba_unmultiplied( + self.color.r(), + self.color.g(), + self.color.b(), + 80, + ), + }), + Tool::Pixelate => self.annotations.push(Annotation::Pixelate { + r, + block: self.pixel_block, + }), + _ => {} + } + } + + pub fn commit_text(&mut self) { + if let Some(pos) = self.text_pending.take() { + let buf = match &self.drag { + DragState::TextInput { buf, .. } => buf.clone(), + _ => String::new(), + }; + if !buf.trim().is_empty() { + // text_size is stored in image-space pixels, same coordinate system + // as pos, so it scales with zoom/resize just like every other annotation. + // !NOTE this is still bugged + self.annotations.push(Annotation::Text { + pos, + text: buf, + color: self.color, + size: self.text_size, + }); + } + self.drag = DragState::None; + } + } + + /// Draw all annotations + in-progress ghost. + /// display_scale = screen logical pts per image pixel (zoom * fit_scale). + pub fn paint( + &self, + painter: &egui::Painter, + to_screen: impl Fn(Pos2) -> Pos2 + Copy, + image: &DynamicImage, + display_scale: f32, + ) { + for (i, ann) in self.annotations.iter().enumerate() { + paint_annotation(ann, painter, to_screen, image, display_scale); + if self.selected == Some(i) { + paint_selection(ann, painter, to_screen); + } + } + + match &self.drag { + DragState::Dragging { start, current } => { + if let Some(g) = self.ghost(*start, *current) { + paint_annotation(&g, painter, to_screen, image, display_scale); + } + } + DragState::PencilStroke { points } => { + for w in points.windows(2) { + painter.line_segment( + [to_screen(w[0]), to_screen(w[1])], + Stroke::new(self.thickness, self.color), + ); + } + } + DragState::TextInput { pos, buf } => { + // Live preview while typing: size * display_scale gives correct screen pts + let screen_size = self.text_size * display_scale; + let sp = to_screen(Pos2::new(pos[0], pos[1])); + painter.text( + sp + egui::vec2(1.0, 1.0), + egui::Align2::LEFT_TOP, + format!("{buf}|"), + egui::FontId::proportional(screen_size), + Color32::from_black_alpha(160), + ); + painter.text( + sp, + egui::Align2::LEFT_TOP, + format!("{buf}|"), + egui::FontId::proportional(screen_size), + self.color, + ); + } + DragState::Moving { .. } => {} + DragState::None => {} + } + } + + fn ghost(&self, start: Pos2, end: Pos2) -> Option { + let r = norm_rect(start, end); + match self.tool { + Tool::Rect => Some(Annotation::Rect { + r, + color: self.color, + thickness: self.thickness, + filled: self.rect_filled, + }), + Tool::Arrow => Some(Annotation::Arrow { + from: [start.x, start.y], + to: [end.x, end.y], + color: self.color, + thickness: self.thickness, + }), + Tool::Highlight => Some(Annotation::Highlight { + r, + color: Color32::from_rgba_unmultiplied( + self.color.r(), + self.color.g(), + self.color.b(), + 80, + ), + }), + Tool::Pixelate => Some(Annotation::Pixelate { + r, + block: self.pixel_block, + }), + _ => None, + } + } +} + +fn norm_rect(a: Pos2, b: Pos2) -> [f32; 4] { + [a.x.min(b.x), a.y.min(b.y), a.x.max(b.x), a.y.max(b.y)] +} + +/// Translate an annotation by delta (image-space pixels). +fn translate_annotation(ann: &mut Annotation, delta: egui::Vec2) { + match ann { + Annotation::Rect { r, .. } + | Annotation::Highlight { r, .. } + | Annotation::Pixelate { r, .. } => { + r[0] += delta.x; + r[1] += delta.y; + r[2] += delta.x; + r[3] += delta.y; + } + Annotation::Arrow { from, to, .. } => { + from[0] += delta.x; + from[1] += delta.y; + to[0] += delta.x; + to[1] += delta.y; + } + Annotation::Text { pos, .. } => { + pos[0] += delta.x; + pos[1] += delta.y; + } + Annotation::Pencil { points, .. } => { + for p in points.iter_mut() { + p[0] += delta.x; + p[1] += delta.y; + } + } + } +} + +// Screen space painting + +fn paint_annotation( + ann: &Annotation, + painter: &egui::Painter, + to_screen: impl Fn(Pos2) -> Pos2, + image: &DynamicImage, + display_scale: f32, +) { + match ann { + Annotation::Rect { + r, + color, + thickness, + filled, + } => { + let tl = to_screen(Pos2::new(r[0], r[1])); + let br = to_screen(Pos2::new(r[2], r[3])); + let rect = Rect::from_min_max(tl, br); + if *filled { + painter.rect_filled(rect, 0.0, *color); + } else { + painter.rect_stroke(rect, 0.0, Stroke::new(*thickness, *color)); + } + } + Annotation::Arrow { + from, + to, + color, + thickness, + } => { + let sp = to_screen(Pos2::new(from[0], from[1])); + let ep = to_screen(Pos2::new(to[0], to[1])); + painter.arrow(sp, ep - sp, Stroke::new(*thickness, *color)); + } + Annotation::Text { + pos, + text, + color, + size, + } => { + // size is in image-space pixels; multiply by display_scale -> screen logical pts + let screen_size = size * display_scale; + let sp = to_screen(Pos2::new(pos[0], pos[1])); + painter.text( + sp + egui::vec2(1.0, 1.0), + egui::Align2::LEFT_TOP, + text, + egui::FontId::proportional(screen_size), + Color32::from_black_alpha(160), + ); + painter.text( + sp, + egui::Align2::LEFT_TOP, + text, + egui::FontId::proportional(screen_size), + *color, + ); + } + Annotation::Highlight { r, color } => { + let tl = to_screen(Pos2::new(r[0], r[1])); + let br = to_screen(Pos2::new(r[2], r[3])); + painter.rect_filled(Rect::from_min_max(tl, br), 0.0, *color); + } + Annotation::Pixelate { r, block } => { + paint_pixelate_preview(r, *block, painter, to_screen, image); + } + Annotation::Pencil { + points, + color, + thickness, + } => { + for w in points.windows(2) { + painter.line_segment( + [ + to_screen(Pos2::new(w[0][0], w[0][1])), + to_screen(Pos2::new(w[1][0], w[1][1])), + ], + Stroke::new(*thickness, *color), + ); + } + } + } +} + +/// Sample average block colors from the source image and draw them as filled +/// rectangles, creating a pixelated preview. +fn paint_pixelate_preview( + r: &[f32; 4], + block: u32, + painter: &egui::Painter, + to_screen: impl Fn(Pos2) -> Pos2, + image: &DynamicImage, +) { + let (iw, ih) = image.dimensions(); + let rgba = image.to_rgba8(); + + let x0 = r[0].round() as u32; + let y0 = r[1].round() as u32; + let x1 = (r[2].round() as u32).min(iw); + let y1 = (r[3].round() as u32).min(ih); + let b = block.max(1); + + if x1 <= x0 || y1 <= y0 { + return; + } + + let mut bx = x0; + while bx < x1 { + let bx_end = (bx + b).min(x1); + let mut by = y0; + while by < y1 { + let by_end = (by + b).min(y1); + + let mut rr = 0u32; + let mut gg = 0u32; + let mut bb = 0u32; + let mut n = 0u32; + for py in by..by_end { + for px in bx..bx_end { + let p = rgba.get_pixel(px, py); + rr += p[0] as u32; + gg += p[1] as u32; + bb += p[2] as u32; + n += 1; + } + } + let avg = if n > 0 { + Color32::from_rgb((rr / n) as u8, (gg / n) as u8, (bb / n) as u8) + } else { + Color32::BLACK + }; + + let tl = to_screen(Pos2::new(bx as f32, by as f32)); + let br = to_screen(Pos2::new(bx_end as f32, by_end as f32)); + painter.rect_filled(Rect::from_min_max(tl, br), 0.0, avg); + + by = by_end; + } + bx = bx_end; + } +} + +fn paint_selection(ann: &Annotation, painter: &egui::Painter, to_screen: impl Fn(Pos2) -> Pos2) { + let dash = Color32::from_rgba_unmultiplied(255, 220, 0, 220); + match ann { + Annotation::Rect { r, .. } + | Annotation::Highlight { r, .. } + | Annotation::Pixelate { r, .. } => { + let tl = to_screen(Pos2::new(r[0] - 3.0, r[1] - 3.0)); + let br = to_screen(Pos2::new(r[2] + 3.0, r[3] + 3.0)); + painter.rect_stroke(Rect::from_min_max(tl, br), 0.0, Stroke::new(1.5, dash)); + } + Annotation::Arrow { from, to, .. } => { + painter.circle_stroke( + to_screen(Pos2::new(from[0], from[1])), + 5.0, + Stroke::new(1.5, dash), + ); + painter.circle_stroke( + to_screen(Pos2::new(to[0], to[1])), + 5.0, + Stroke::new(1.5, dash), + ); + } + Annotation::Text { pos, .. } => { + painter.circle_filled(to_screen(Pos2::new(pos[0] - 2.0, pos[1] - 2.0)), 4.0, dash); + } + Annotation::Pencil { points, .. } => { + if let Some(p) = points.first() { + painter.circle_stroke( + to_screen(Pos2::new(p[0], p[1])), + 5.0, + Stroke::new(1.5, dash), + ); + } + } + } +} + +// Flatten annotations on to DynamicImage + +pub fn flatten(base: &DynamicImage, annotations: &[Annotation]) -> DynamicImage { + let mut out = base.to_rgba8(); + let (iw, ih) = out.dimensions(); + + for ann in annotations { + match ann { + Annotation::Rect { + r, + color, + thickness, + filled, + } => { + let x0 = r[0].round() as i32; + let y0 = r[1].round() as i32; + let x1 = r[2].round() as i32; + let y1 = r[3].round() as i32; + let t = (*thickness).round().max(1.0) as i32; + if *filled { + fill_rect_img(&mut out, x0, y0, x1, y1, to_rgba(*color), iw, ih); + } else { + draw_rect_border(&mut out, x0, y0, x1, y1, t, to_rgba(*color), iw, ih); + } + } + Annotation::Arrow { + from, + to, + color, + thickness, + } => { + let c = to_rgba(*color); + draw_line_img( + &mut out, from[0], from[1], to[0], to[1], *thickness, c, iw, ih, + ); + draw_arrowhead( + &mut out, + from[0], + from[1], + to[0], + to[1], + *thickness * 4.0, + c, + iw, + ih, + ); + } + Annotation::Text { + pos, + text, + color, + size, + } => { + draw_text_img(&mut out, pos[0], pos[1], text, *size, *color, iw, ih); + } + Annotation::Highlight { r, color } => { + let x0 = r[0].round() as i32; + let y0 = r[1].round() as i32; + let x1 = r[2].round() as i32; + let y1 = r[3].round() as i32; + fill_rect_img(&mut out, x0, y0, x1, y1, to_rgba(*color), iw, ih); + } + Annotation::Pixelate { r, block } => { + pixelate_region(&mut out, r[0], r[1], r[2], r[3], *block, iw, ih); + } + Annotation::Pencil { + points, + color, + thickness, + } => { + let c = to_rgba(*color); + for w in points.windows(2) { + draw_line_img( + &mut out, w[0][0], w[0][1], w[1][0], w[1][1], *thickness, c, iw, ih, + ); + } + } + } + } + + DynamicImage::ImageRgba8(out) +} + +// Pixel helpers + +fn to_rgba(c: Color32) -> Rgba { + Rgba([c.r(), c.g(), c.b(), c.a()]) +} + +fn blend_px(dst: Rgba, src: Rgba) -> Rgba { + let sa = src[3] as f32 / 255.0; + let da = dst[3] as f32 / 255.0; + let oa = sa + da * (1.0 - sa); + if oa < 1e-6 { + return Rgba([0, 0, 0, 0]); + } + let r = (src[0] as f32 * sa + dst[0] as f32 * da * (1.0 - sa)) / oa; + let g = (src[1] as f32 * sa + dst[1] as f32 * da * (1.0 - sa)) / oa; + let b = (src[2] as f32 * sa + dst[2] as f32 * da * (1.0 - sa)) / oa; + Rgba([r as u8, g as u8, b as u8, (oa * 255.0) as u8]) +} + +fn set_px(img: &mut image::RgbaImage, x: i32, y: i32, c: Rgba, w: u32, h: u32) { + if x >= 0 && y >= 0 && (x as u32) < w && (y as u32) < h { + let (px, py) = (x as u32, y as u32); + img.put_pixel(px, py, blend_px(*img.get_pixel(px, py), c)); + } +} + +fn draw_line_img( + img: &mut image::RgbaImage, + x0: f32, + y0: f32, + x1: f32, + y1: f32, + thickness: f32, + color: Rgba, + w: u32, + h: u32, +) { + let t = (thickness / 2.0).ceil() as i32; + let steps = (x1 - x0).abs().max((y1 - y0).abs()).ceil() as usize + 1; + for i in 0..=steps { + let frac = if steps == 0 { + 0.0 + } else { + i as f32 / steps as f32 + }; + let px = (x0 + frac * (x1 - x0)).round() as i32; + let py = (y0 + frac * (y1 - y0)).round() as i32; + for dy in -t..=t { + for dx in -t..=t { + if dx * dx + dy * dy <= t * t { + set_px(img, px + dx, py + dy, color, w, h); + } + } + } + } +} + +fn draw_rect_border( + img: &mut image::RgbaImage, + x0: i32, + y0: i32, + x1: i32, + y1: i32, + t: i32, + color: Rgba, + w: u32, + h: u32, +) { + for o in 0..t { + for x in (x0 - o)..=(x1 + o) { + set_px(img, x, y0 - o, color, w, h); + set_px(img, x, y1 + o, color, w, h); + } + for y in (y0 - o)..=(y1 + o) { + set_px(img, x0 - o, y, color, w, h); + set_px(img, x1 + o, y, color, w, h); + } + } +} + +fn fill_rect_img( + img: &mut image::RgbaImage, + x0: i32, + y0: i32, + x1: i32, + y1: i32, + color: Rgba, + w: u32, + h: u32, +) { + for y in y0..=y1 { + for x in x0..=x1 { + set_px(img, x, y, color, w, h); + } + } +} + +fn draw_arrowhead( + img: &mut image::RgbaImage, + x0: f32, + y0: f32, + x1: f32, + y1: f32, + size: f32, + color: Rgba, + w: u32, + h: u32, +) { + let dx = x1 - x0; + let dy = y1 - y0; + let len = (dx * dx + dy * dy).sqrt().max(1.0); + let (ux, uy) = (dx / len, dy / len); + draw_line_img( + img, + x1, + y1, + x1 - size * ux + size * 0.5 * (-uy), + y1 - size * uy + size * 0.5 * ux, + 2.0, + color, + w, + h, + ); + draw_line_img( + img, + x1, + y1, + x1 - size * ux - size * 0.5 * (-uy), + y1 - size * uy - size * 0.5 * ux, + 2.0, + color, + w, + h, + ); +} + +// Text rasterization via ab_glyph + +// Font candidate paths tried in order at runtime (Linux/Wayland/Hyprland systems). +// !TODO Still unsure about this, might make a font selector / preview soontm. +const FONT_PATHS: &[&str] = &[ + "/usr/share/fonts/noto/NotoSans-Regular.ttf", + "/usr/share/fonts/truetype/noto/NotoSans-Regular.ttf", + "/usr/share/fonts/google-noto/NotoSans-Regular.ttf", + "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", + "/usr/share/fonts/liberation/LiberationSans-Regular.ttf", + "/usr/share/fonts/truetype/liberation/LiberationSans-Regular.ttf", +]; + +fn load_font_bytes() -> Option> { + for path in FONT_PATHS { + if let Ok(bytes) = std::fs::read(path) { + return Some(bytes); + } + } + None +} + +// !FIXME placement is borked in final rendered image. +/// Rasterize text onto img at image-space position (x, y) using ab_glyph. +fn draw_text_img( + img: &mut image::RgbaImage, + x: f32, + y: f32, + text: &str, + size_pts: f32, + color: Color32, + iw: u32, + ih: u32, +) { + use ab_glyph::{Font, FontVec, PxScale, ScaleFont}; + + let bytes = match load_font_bytes() { + Some(b) => b, + None => return, // no system font found, skip silently + }; + let font = match FontVec::try_from_vec(bytes) { + Ok(f) => f, + Err(_) => return, + }; + let scale = PxScale::from(size_pts); + let ascent = font.as_scaled(scale).ascent(); + + // Shadow pass (1 px down-right, semi-transparent black) + render_glyphs_vec( + img, + &font, + text, + scale, + x + 1.0, + y + ascent + 1.0, + Color32::from_black_alpha(160), + iw, + ih, + ); + // Foreground pass + render_glyphs_vec(img, &font, text, scale, x, y + ascent, color, iw, ih); +} + +fn render_glyphs_vec( + img: &mut image::RgbaImage, + font: &ab_glyph::FontVec, + text: &str, + scale: ab_glyph::PxScale, + mut pen_x: f32, + pen_y: f32, + color: Color32, + iw: u32, + ih: u32, +) { + use ab_glyph::{Font, GlyphId, ScaleFont}; + let scaled = font.as_scaled(scale); + let mut prev: Option = None; + + for ch in text.chars() { + let gid = scaled.glyph_id(ch); + if let Some(p) = prev { + pen_x += scaled.kern(p, gid); + } + let glyph = gid.with_scale_and_position(scale, ab_glyph::point(pen_x, pen_y)); + pen_x += scaled.h_advance(gid); + + if let Some(og) = font.outline_glyph(glyph) { + let b = og.px_bounds(); + og.draw(|gx, gy, cov| { + let px = b.min.x as i32 + gx as i32; + let py = b.min.y as i32 + gy as i32; + if px >= 0 && py >= 0 && (px as u32) < iw && (py as u32) < ih { + let alpha = (cov * color.a() as f32) as u8; + let src = Rgba([color.r(), color.g(), color.b(), alpha]); + let dst = *img.get_pixel(px as u32, py as u32); + img.put_pixel(px as u32, py as u32, blend_px(dst, src)); + } + }); + } + prev = Some(gid); + } +} + +fn pixelate_region( + img: &mut image::RgbaImage, + x0: f32, + y0: f32, + x1: f32, + y1: f32, + block: u32, + w: u32, + h: u32, +) { + let rx0 = (x0.round() as u32).min(w.saturating_sub(1)); + let ry0 = (y0.round() as u32).min(h.saturating_sub(1)); + let rx1 = (x1.round() as u32).min(w.saturating_sub(1)); + let ry1 = (y1.round() as u32).min(h.saturating_sub(1)); + if block == 0 || rx1 <= rx0 || ry1 <= ry0 { + return; + } + let mut bx = rx0; + while bx < rx1 { + let bx_end = (bx + block).min(rx1); + let mut by = ry0; + while by < ry1 { + let by_end = (by + block).min(ry1); + let (mut r, mut g, mut b, mut n) = (0u32, 0u32, 0u32, 0u32); + for py in by..by_end { + for px in bx..bx_end { + let p = img.get_pixel(px, py); + r += p[0] as u32; + g += p[1] as u32; + b += p[2] as u32; + n += 1; + } + } + if n > 0 { + let avg = Rgba([(r / n) as u8, (g / n) as u8, (b / n) as u8, 255]); + for py in by..by_end { + for px in bx..bx_end { + img.put_pixel(px, py, avg); + } + } + } + by = by_end; + } + bx = bx_end; + } +} diff --git a/src/gui/app.rs b/src/gui/app.rs new file mode 100644 index 0000000..b4ed1a6 --- /dev/null +++ b/src/gui/app.rs @@ -0,0 +1,929 @@ +//! Main application state for blast-gui. +//! +//! State machine: +//! Launcher -> (capture runs, window hides) -> Preview +//! Preview -> copy / save / copysave / discard -> Launcher + +use std::{path::PathBuf, thread, time::Duration}; + +use eframe::egui::{self, ColorImage, Context, Pos2, TextureHandle, TextureOptions, Vec2}; +use image::DynamicImage; + +use super::annotations::{AnnotationLayer, DragState, Tool}; +use super::{theme, widgets}; +use crate::{ + capture::{self, CaptureOptions}, + clipboard, + error::BlastError, + freeze, hyprland, paths, region, select, wayland_windows, + shhh::{self, ShadowOptions}, +}; + +#[derive(Debug, Clone, Copy, PartialEq)] +enum Subject { + Screen, + Active, + Output, + Area, + Region, +} + +impl Subject { + const ALL: &'static [Subject] = &[ + Subject::Screen, + Subject::Active, + Subject::Output, + Subject::Area, + Subject::Region, + ]; + + fn label(self) -> &'static str { + match self { + Subject::Screen => "🖥 Screen", + Subject::Active => "🪟 Active", + Subject::Output => "📺 Output", + Subject::Area => "⊡ Area", + Subject::Region => "✂ Region", + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq)] +enum Action { + Copy, + Save, + Copysave, +} + +impl Action { + const ALL: &'static [Action] = &[Action::Copy, Action::Save, Action::Copysave]; + + fn label(self) -> &'static str { + match self { + Action::Copy => "📋 Copy", + Action::Save => "💾 Save", + Action::Copysave => "📋+💾 Copy & Save", + } + } +} + +#[derive(Debug, Clone)] +struct ShadowConfig { + enabled: bool, + radius: f32, + offset_x: f32, + offset_y: f32, + alpha: f32, + spread: f32, + blur: f32, +} + +impl Default for ShadowConfig { + fn default() -> Self { + let bs = hyprland::border_size().unwrap_or(0) as f32; + let rnd = hyprland::rounding().unwrap_or(8) as f32; + Self { + enabled: true, + radius: rnd + bs, + offset_x: -20.0, + offset_y: -20.0, + alpha: 150.0, + spread: 26.0, + blur: 5.0, + } + } +} + +impl ShadowConfig { + fn to_opts(&self) -> Option { + if !self.enabled { + return None; + } + Some(ShadowOptions { + corner_radius: self.radius as u32, + offset: (self.offset_x as i32, self.offset_y as i32), + shadow_alpha: self.alpha as u8, + spread: self.spread as u32, + blur_radius: self.blur as u32, + }) + } +} + +enum AppState { + Launcher, + Capturing, + Preview { + image: DynamicImage, + texture: TextureHandle, + label: String, + status: Option<(String, bool)>, // (message, is_error) + zoom: f32, // 1.0 = fit, >1.0 = zoomed in + annotations: AnnotationLayer, + }, +} + +pub struct BlastApp { + state: AppState, + subject: Subject, + action: Action, + shadow: ShadowConfig, + delay: u32, + freeze: bool, + notify: bool, + // save path override + save_path: String, +} + +impl BlastApp { + pub fn new(cc: &eframe::CreationContext<'_>) -> Self { + theme::apply(&cc.egui_ctx); + Self { + state: AppState::Launcher, + subject: Subject::Screen, + action: Action::Copysave, + shadow: ShadowConfig::default(), + delay: 0, + freeze: false, + notify: false, + save_path: String::new(), + } + } + + fn do_capture(&mut self, ctx: &Context) { + self.state = AppState::Capturing; + + ctx.send_viewport_cmd(egui::ViewportCommand::Minimized(true)); + + thread::sleep(Duration::from_millis(150)); + + let subject = self.subject; + let freeze = self.freeze; + let delay = self.delay; + let shadow_opts = self.shadow.to_opts(); + let action = self.action; + let save_path = if self.save_path.trim().is_empty() { + None + } else { + Some(PathBuf::from(self.save_path.trim())) + }; + + let result: std::result::Result<(DynamicImage, String), String> = (|| { + if delay > 0 { + thread::sleep(Duration::from_secs(delay as u64)); + } + + let opts = CaptureOptions { + include_cursor: false, + scale: None, + }; + + let mut freeze_guard = if freeze && matches!(subject, Subject::Area | Subject::Region) { + freeze::FreezeGuard::spawn().unwrap_or_else(|_| freeze::FreezeGuard::none()) + } else { + freeze::FreezeGuard::none() + }; + + let (raw_img, label): (DynamicImage, String) = match subject { + Subject::Screen => { + let img = capture::capture_screen(&opts).map_err(|e| e.to_string())?; + (img, "Screen".into()) + } + Subject::Active => { + let win = hyprland::active_window().map_err(|e| e.to_string())?; + let bs = hyprland::border_size().unwrap_or(0); + let img = capture::capture_region(win.to_geometry(bs, 0), &opts) + .map_err(|e| e.to_string())?; + (img, format!("{} window", win.class)) + } + Subject::Output => { + let mon = hyprland::focused_monitor().map_err(|e| e.to_string())?; + let img = + capture::capture_output(&mon.name, &opts).map_err(|e| e.to_string())?; + (img, mon.name.clone()) + } + Subject::Area => { + let geom = hyprland::with_animations_disabled(|bs| { + let boxes = wayland_windows::visible_window_hints().unwrap_or_else(|| { + let bar = hyprland::get_option_int("plugin:hyprbars:bar_height") + .unwrap_or(0); + hyprland::visible_windows() + .map(|wins| { + wins.iter() + .map(|w| { + let g = w.to_geometry(bs, bar); + select::HintBox { + x: g.x as i32, y: g.y as i32, + w: g.w as i32, h: g.h as i32, + label: w.class.clone(), + } + }) + .collect() + }) + .unwrap_or_default() + }); + region::select_area_boxes(boxes, None) + }) + .map_err(|e| e.to_string())?; + freeze_guard.kill(); + let img = capture::capture_region(geom, &opts).map_err(|e| e.to_string())?; + (img, "Area".into()) + } + Subject::Region => { + let geom = region::select_free_region(None).map_err(|e| e.to_string())?; + freeze_guard.kill(); + let img = capture::capture_region(geom, &opts).map_err(|e| e.to_string())?; + (img, "Region".into()) + } + }; + + Ok((raw_img, label)) + })(); + + ctx.send_viewport_cmd(egui::ViewportCommand::Minimized(false)); + ctx.send_viewport_cmd(egui::ViewportCommand::Focus); + + match result { + Err(e) => { + self.state = AppState::Launcher; + eprintln!("blast-gui: capture failed: {e}"); + } + Ok((raw_img, label)) => { + let processed: DynamicImage = match &shadow_opts { + Some(opts) => shhh::apply_effects(&raw_img, opts).unwrap_or(raw_img.clone()), + None => raw_img.clone(), + }; + + // Execute action. + let status: Option<(String, bool)> = match action { + Action::Copy => { + let png = shhh::encode_png(&processed); + match png.and_then(|b| { + clipboard::copy_png(b) + .map_err(|e| crate::error::BlastError::Image(e.to_string())) + }) { + Ok(_) => Some(("Copied to clipboard".into(), false)), + Err(e) => Some((format!("Copy failed: {e}"), true)), + } + } + Action::Save => { + let dest = save_path.clone().unwrap_or_else(|| { + let prefix = hyprland::active_window() + .ok() + .map(|w| w.class) + .unwrap_or_default(); + paths::default_save_path(Some(&prefix)) + }); + let png = shhh::encode_png(&processed); + match png.and_then(|b| std::fs::write(&dest, b).map_err(BlastError::Io)) { + Ok(_) => Some((format!("Saved to {}", dest.display()), false)), + Err(e) => Some((format!("Save failed: {e}"), true)), + } + } + Action::Copysave => { + let dest = save_path.clone().unwrap_or_else(|| { + let prefix = hyprland::active_window() + .ok() + .map(|w| w.class) + .unwrap_or_default(); + paths::default_save_path(Some(&prefix)) + }); + let result = shhh::encode_png(&processed).and_then(|png| { + clipboard::copy_png(png.clone()) + .map_err(|e| BlastError::Image(e.to_string()))?; + std::fs::write(&dest, &png).map_err(BlastError::Io)?; + Ok(dest.display().to_string()) + }); + match result { + Ok(path) => Some((format!("Copied & saved to {path}"), false)), + Err(e) => Some((format!("Failed: {e}"), true)), + } + } + }; + + let texture = image_to_texture(ctx, &processed, "preview"); + self.state = AppState::Preview { + image: processed, + texture, + label, + status, + zoom: 1.0, + annotations: AnnotationLayer::default(), + }; + } + } + } +} + +impl eframe::App for BlastApp { + fn clear_color(&self, _visuals: &egui::Visuals) -> [f32; 4] { + [0.0, 0.0, 0.0, 0.0] + } + + fn update(&mut self, ctx: &Context, _frame: &mut eframe::Frame) { + if ctx.input(|i| i.key_pressed(egui::Key::Escape)) { + match &self.state { + AppState::Preview { .. } => self.state = AppState::Launcher, + _ => ctx.send_viewport_cmd(egui::ViewportCommand::Close), + } + } + + egui::CentralPanel::default() + .frame( + egui::Frame::none() + .fill(theme::BG) + .rounding(egui::Rounding::same(theme::ROUNDING)) + .inner_margin(egui::Margin::same(0.0)), + ) + .show(ctx, |ui| { + // Title / drag bar. + ui.add_space(4.0); + widgets::drag_area(ui, egui::Id::new("title_bar")); + ui.add_space(4.0); + + // Close button, top-right corner. + let close_rect = { + let r = ui.max_rect(); + egui::Rect::from_min_size( + egui::pos2(r.right() - 32.0, r.top() + 4.0), + egui::vec2(24.0, 24.0), + ) + }; + let close_resp = ui.put( + close_rect, + egui::Button::new(egui::RichText::new("✕").color(theme::FG_DIM).size(12.0)) + .fill(egui::Color32::TRANSPARENT) + .stroke(egui::Stroke::NONE), + ); + if close_resp.clicked() { + ctx.send_viewport_cmd(egui::ViewportCommand::Close); + } + + match &self.state { + AppState::Launcher | AppState::Capturing => { + self.show_launcher(ui); + } + AppState::Preview { .. } => { + self.show_preview(ui); + } + } + }); + + // Trigger capture on the next frame after entering Capturing state + // (so the window has had a chance to hide). + // Use a flag via egui's request_repaint mechanism. + if matches!(self.state, AppState::Capturing) {} + } +} + +impl BlastApp { + fn show_launcher(&mut self, ui: &mut egui::Ui) { + let mut do_capture = false; + + egui::ScrollArea::vertical() + .auto_shrink([false, false]) + .show(ui, |ui| { + ui.add_space(4.0); + + egui::Frame::none() + .inner_margin(egui::Margin::symmetric(20.0, 8.0)) + .show(ui, |ui| { + widgets::section_heading(ui, "SUBJECT"); + ui.add_space(4.0); + ui.horizontal_wrapped(|ui| { + for &subj in Subject::ALL { + let sel = self.subject == subj; + if widgets::pill_button(ui, subj.label(), sel).clicked() { + self.subject = subj; + } + } + }); + + ui.add_space(12.0); + widgets::separator(ui); + + widgets::section_heading(ui, "ACTION"); + ui.add_space(4.0); + ui.horizontal_wrapped(|ui| { + for &act in Action::ALL { + let sel = self.action == act; + if widgets::pill_button(ui, act.label(), sel).clicked() { + self.action = act; + } + } + }); + + // Save path, shown when action involves saving. + if matches!(self.action, Action::Save | Action::Copysave) { + ui.add_space(8.0); + ui.horizontal(|ui| { + ui.label( + egui::RichText::new("Path:").color(theme::FG_DIM).size(12.5), + ); + ui.add( + egui::TextEdit::singleline(&mut self.save_path) + .hint_text("~/Pictures/.png") + .desired_width(260.0), + ); + }); + } + + ui.add_space(12.0); + widgets::separator(ui); + + // Shadow + widgets::section_heading(ui, "SHADOW & ROUNDING"); + ui.add_space(4.0); + ui.horizontal(|ui| { + ui.checkbox(&mut self.shadow.enabled, ""); + ui.label( + egui::RichText::new("Enable shadow & corner rounding") + .color(if self.shadow.enabled { + theme::FG + } else { + theme::FG_DIM + }) + .size(13.5), + ); + }); + + if self.shadow.enabled { + ui.add_space(6.0); + widgets::labelled_slider( + ui, + "Corner radius", + &mut self.shadow.radius, + 0.0..=40.0, + ); + widgets::labelled_slider( + ui, + "Spread ", + &mut self.shadow.spread, + 0.0..=60.0, + ); + widgets::labelled_slider( + ui, + "Blur ", + &mut self.shadow.blur, + 0.0..=30.0, + ); + widgets::labelled_slider( + ui, + "Opacity ", + &mut self.shadow.alpha, + 0.0..=255.0, + ); + widgets::labelled_slider( + ui, + "Offset X ", + &mut self.shadow.offset_x, + -60.0..=60.0, + ); + widgets::labelled_slider( + ui, + "Offset Y ", + &mut self.shadow.offset_y, + -60.0..=60.0, + ); + } + + ui.add_space(12.0); + widgets::separator(ui); + + widgets::section_heading(ui, "OPTIONS"); + ui.add_space(4.0); + ui.horizontal(|ui| { + ui.checkbox(&mut self.freeze, ""); + ui.label( + egui::RichText::new("Freeze screen during selection") + .color(theme::FG) + .size(13.5), + ); + }); + ui.horizontal(|ui| { + ui.checkbox(&mut self.notify, ""); + ui.label( + egui::RichText::new("Desktop notification on complete") + .color(theme::FG) + .size(13.5), + ); + }); + ui.horizontal(|ui| { + ui.label( + egui::RichText::new("Delay (s):") + .color(theme::FG_DIM) + .size(12.5), + ); + ui.add( + egui::DragValue::new(&mut self.delay) + .range(0..=30) + .speed(0.1), + ); + }); + + ui.add_space(20.0); + + ui.with_layout(egui::Layout::bottom_up(egui::Align::Center), |ui| { + ui.add_space(8.0); + if widgets::primary_button(ui, " Capture ").clicked() { + do_capture = true; + } + ui.add_space(4.0); + ui.label( + egui::RichText::new("Esc to close") + .color(theme::FG_DIM) + .size(11.0), + ); + }); + }); + }); + + if do_capture { + // Clone context so we can pass it into do_capture. + let ctx = ui.ctx().clone(); + self.do_capture(&ctx); + } + } +} + +impl BlastApp { + fn show_preview(&mut self, ui: &mut egui::Ui) { + let (label, status) = match &self.state { + AppState::Preview { label, status, .. } => (label.clone(), status.clone()), + _ => return, + }; + + let mut go_back = false; + let mut do_copy = false; + let mut do_save = false; + let mut do_undo = false; + let mut do_clear = false; + let mut commit_text = false; + + egui::Frame::none() + .inner_margin(egui::Margin::symmetric(12.0, 8.0)) + .show(ui, |ui| { + + ui.horizontal(|ui| { + ui.label( + egui::RichText::new(format!("Preview - {label}")) + .color(theme::FG) + .size(15.0) + .strong(), + ); + ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { + small_icon_btn(ui, "1:1", "Reset zoom").map(|_| { + if let AppState::Preview { zoom, .. } = &mut self.state { *zoom = 1.0; } + }); + small_icon_btn(ui, "−", "Zoom out").map(|_| { + if let AppState::Preview { zoom, .. } = &mut self.state { + *zoom = (*zoom / 1.25).max(0.1); + } + }); + let zoom_pct = if let AppState::Preview { zoom, .. } = &self.state { + format!("{:.0}%", zoom * 100.0) + } else { "100%".into() }; + ui.label(egui::RichText::new(zoom_pct).color(theme::FG_DIM).size(11.5)); + small_icon_btn(ui, "+", "Zoom in").map(|_| { + if let AppState::Preview { zoom, .. } = &mut self.state { + *zoom = (*zoom * 1.25).min(8.0); + } + }); + ui.label(egui::RichText::new("🔍").color(theme::FG_DIM).size(12.0)); + }); + }); + + ui.add_space(6.0); + + egui::Frame::none() + .fill(theme::BG_PANEL) + .rounding(egui::Rounding::same(6.0)) + .inner_margin(egui::Margin::symmetric(6.0, 4.0)) + .show(ui, |ui| { + ui.horizontal_wrapped(|ui| { + // Tool pills + for &tool in Tool::ALL { + let sel = matches!(&self.state, + AppState::Preview { annotations, .. } if annotations.tool == tool); + if widgets::pill_button(ui, tool.label(), sel).clicked() { + if let AppState::Preview { annotations, .. } = &mut self.state { + annotations.tool = tool; + } + } + } + + ui.separator(); + + // Fill checkbox, only shown when Box tool is active + let is_rect = matches!(&self.state, + AppState::Preview { annotations, .. } if annotations.tool == Tool::Rect); + if is_rect { + if let AppState::Preview { annotations, .. } = &mut self.state { + ui.checkbox(&mut annotations.rect_filled, ""); + ui.label(egui::RichText::new("Fill").color(theme::FG_DIM).size(11.5)); + } + ui.separator(); + } + + // Color picker + if let AppState::Preview { annotations, .. } = &mut self.state { + ui.color_edit_button_srgba(&mut annotations.color); + ui.label(egui::RichText::new("Color").color(theme::FG_DIM).size(11.5)); + } + + // Tool-specific size control: + // - Rect / Arrow / Pencil -> line thickness in px + // - Text -> font size + // - Select / Highlight / Pixelate -> nothing + let active_tool = if let AppState::Preview { annotations, .. } = &self.state { + Some(annotations.tool) + } else { None }; + + match active_tool { + Some(Tool::Rect) | Some(Tool::Arrow) | Some(Tool::Pencil) => { + ui.separator(); + if let AppState::Preview { annotations, .. } = &mut self.state { + ui.add( + egui::Slider::new(&mut annotations.thickness, 1.0..=12.0) + .text("px") + .show_value(true) + .trailing_fill(true), + ); + } + } + Some(Tool::Text) => { + ui.separator(); + if let AppState::Preview { annotations, .. } = &mut self.state { + ui.add( + egui::Slider::new(&mut annotations.text_size, 10.0..=200.0) + .text("px") + .show_value(true) + .trailing_fill(true), + ); + } + } + _ => {} + } + + ui.separator(); + + // Undo / Clear + if widgets::secondary_button(ui, "↩ Undo").clicked() { do_undo = true; } + if widgets::secondary_button(ui, "🗑 Clear").clicked() { do_clear = true; } + }); + }); + + ui.add_space(4.0); + + // Text input box (shown when text tool active & placement chosen) + let show_text_input = matches!(&self.state, + AppState::Preview { annotations, .. } + if annotations.tool == Tool::Text && annotations.text_pending.is_some()); + + if show_text_input { + ui.horizontal(|ui| { + ui.label(egui::RichText::new("Text:").color(theme::FG_DIM).size(12.5)); + if let AppState::Preview { annotations, .. } = &mut self.state { + // Ensure drag state is TextInput + if let Some(pos) = annotations.text_pending { + if !matches!(annotations.drag, DragState::TextInput { .. }) { + annotations.drag = DragState::TextInput { pos, buf: String::new() }; + } + if let DragState::TextInput { ref mut buf, .. } = annotations.drag { + let resp = ui.add( + egui::TextEdit::singleline(buf) + .desired_width(240.0) + .hint_text("Type annotation…") + ); + resp.request_focus(); + if ui.input(|i| i.key_pressed(egui::Key::Enter)) { + commit_text = true; + } + } + } + } + if widgets::secondary_button(ui, "✓ Place").clicked() { + commit_text = true; + } + }); + ui.add_space(4.0); + } + + let _canvas_resp = if let AppState::Preview { texture, zoom, annotations, image, .. } = &mut self.state { + let avail_w = ui.available_width(); + let avail_h = (ui.available_height() - 80.0).max(160.0); + let [tw, th] = [texture.size()[0] as f32, texture.size()[1] as f32]; + let fit_scale = (avail_w / tw).min(avail_h / th).min(1.0); + let display_scale = fit_scale * *zoom; + let img_size = Vec2::new(tw * display_scale, th * display_scale); + + let scroll_out = egui::ScrollArea::both() + .auto_shrink([false, false]) + .max_height(avail_h) + .show(ui, |ui| { + let (rect, resp) = ui.allocate_exact_size( + img_size, + egui::Sense::click_and_drag(), + ); + + ui.painter().image( + texture.id(), + rect, + egui::Rect::from_min_max(egui::pos2(0.0, 0.0), egui::pos2(1.0, 1.0)), + egui::Color32::WHITE, + ); + + let to_img = |sp: Pos2| -> Pos2 { + Pos2::new( + (sp.x - rect.min.x) / display_scale, + (sp.y - rect.min.y) / display_scale, + ) + }; + let to_screen = |ip: Pos2| -> Pos2 { + Pos2::new( + rect.min.x + ip.x * display_scale, + rect.min.y + ip.y * display_scale, + ) + }; + + annotations.handle_input(ui, rect, to_img); + annotations.paint(ui.painter(), to_screen, image, display_scale); + + (rect, resp) + }); + + let hover_pos = ui.input(|i| i.pointer.hover_pos().unwrap_or_default()); + if scroll_out.inner_rect.contains(hover_pos) { + let delta = ui.input(|i| i.smooth_scroll_delta.y); + if delta != 0.0 { + let factor = if delta > 0.0 { 1.08 } else { 1.0 / 1.08 }; + *zoom = (*zoom * factor).clamp(0.1, 8.0); + ui.ctx().request_repaint(); + } + } + + Some(scroll_out.inner.1) // canvas response + } else { + None + }; + + ui.add_space(6.0); + + if let Some((msg, is_err)) = &status { + widgets::status_label(ui, msg, *is_err); + ui.add_space(4.0); + } + + widgets::separator(ui); + + ui.horizontal(|ui| { + if widgets::secondary_button(ui, "← Back").clicked() { go_back = true; } + ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { + if widgets::primary_button(ui, "📋 Copy").clicked() { do_copy = true; } + ui.add_space(4.0); + if widgets::secondary_button(ui, "💾 Save").clicked() { do_save = true; } + }); + }); + }); + + if go_back { + self.state = AppState::Launcher; + return; + } + if do_undo { + if let AppState::Preview { annotations, .. } = &mut self.state { + annotations.undo(); + } + } + if do_clear { + if let AppState::Preview { annotations, .. } = &mut self.state { + annotations.clear(); + } + } + if commit_text { + if let AppState::Preview { annotations, .. } = &mut self.state { + annotations.commit_text(); + } + } + + // Build a flattened image (base + annotations) for copy/save + let flat_image = if do_copy || do_save { + if let AppState::Preview { + image, annotations, .. + } = &self.state + { + Some(super::annotations::flatten(image, &annotations.annotations)) + } else { + None + } + } else { + None + }; + + if do_copy { + let img = + flat_image + .as_ref() + .or(if let AppState::Preview { image, .. } = &self.state { + Some(image) + } else { + None + }); + if let Some(img) = img { + let img = img.clone(); + let opts = self.shadow.to_opts(); + let result = (|| -> std::result::Result<(), String> { + let png = match &opts { + Some(o) => shhh::apply_and_encode(&img, o), + None => shhh::encode_png(&img), + } + .map_err(|e| e.to_string())?; + clipboard::copy_png(png).map_err(|e| e.to_string())?; + Ok(()) + })(); + let status = match result { + Ok(_) => ("Copied to clipboard".into(), false), + Err(e) => (format!("Copy failed: {e}"), true), + }; + if let AppState::Preview { status: s, .. } = &mut self.state { + *s = Some(status); + } + } + } + + if do_save { + let img = + flat_image + .as_ref() + .or(if let AppState::Preview { image, .. } = &self.state { + Some(image) + } else { + None + }); + if let Some(img) = img { + let img = img.clone(); + let opts = self.shadow.to_opts(); + let save_path = if self.save_path.trim().is_empty() { + None + } else { + Some(PathBuf::from(self.save_path.trim())) + }; + let dest = save_path.unwrap_or_else(|| { + let prefix = hyprland::active_window() + .ok() + .map(|w| w.class) + .unwrap_or_default(); + paths::default_save_path(Some(&prefix)) + }); + let result = (|| -> std::result::Result { + let png = match &opts { + Some(o) => shhh::apply_and_encode(&img, o), + None => shhh::encode_png(&img), + } + .map_err(|e| e.to_string())?; + std::fs::write(&dest, &png).map_err(|e| e.to_string())?; + Ok(dest.display().to_string()) + })(); + let status = match result { + Ok(p) => (format!("Saved to {p}"), false), + Err(e) => (format!("Save failed: {e}"), true), + }; + if let AppState::Preview { status: s, .. } = &mut self.state { + *s = Some(status); + } + } + } + } +} + +fn small_icon_btn(ui: &mut egui::Ui, label: &str, _tooltip: &str) -> Option { + let resp = ui.add( + egui::Button::new(egui::RichText::new(label).color(theme::FG_DIM).size(13.0)) + .fill(theme::BG_PANEL) + .stroke(egui::Stroke::new(1.0, theme::ACCENT_DIM)) + .rounding(egui::Rounding::same(theme::ROUNDING)) + .min_size(egui::Vec2::new(28.0, 22.0)), + ); + if resp.clicked() { + Some(resp) + } else { + None + } +} + +fn image_to_texture(ctx: &Context, img: &DynamicImage, name: &str) -> TextureHandle { + let rgba = img.to_rgba8(); + let (w, h) = rgba.dimensions(); + let pixels: Vec = rgba + .pixels() + .map(|p| egui::Color32::from_rgba_unmultiplied(p[0], p[1], p[2], p[3])) + .collect(); + + ctx.load_texture( + name, + ColorImage { + size: [w as usize, h as usize], + pixels, + }, + TextureOptions::LINEAR, + ) +} diff --git a/src/gui/main.rs b/src/gui/main.rs new file mode 100644 index 0000000..20e1b4c --- /dev/null +++ b/src/gui/main.rs @@ -0,0 +1,52 @@ +// blast egui launcher + post-capture preview + +// Pull in shared backend modules. +#[path = "../capture.rs"] +mod capture; +#[path = "../clipboard.rs"] +mod clipboard; +#[path = "../error.rs"] +mod error; +#[path = "../freeze.rs"] +mod freeze; +#[path = "../hyprland.rs"] +mod hyprland; +#[path = "../notify.rs"] +mod notify; +#[path = "../paths.rs"] +mod paths; +#[path = "../region.rs"] +mod region; +#[path = "../select.rs"] +mod select; +#[path = "../shhh.rs"] +mod shhh; +#[path = "../wayland_windows.rs"] +mod wayland_windows; + +mod annotations; +mod app; +mod theme; +mod widgets; + +use eframe::egui; + +fn main() { + let options = eframe::NativeOptions { + viewport: egui::ViewportBuilder::default() + .with_title("blast") + .with_inner_size([520.0, 560.0]) + .with_min_inner_size([400.0, 440.0]) + .with_resizable(true) + .with_decorations(false) // borderless + .with_transparent(true), + ..Default::default() + }; + + eframe::run_native( + "blast", + options, + Box::new(|cc| Ok(Box::new(app::BlastApp::new(cc)))), + ) + .expect("failed to start blast-gui"); +} diff --git a/src/gui/theme.rs b/src/gui/theme.rs new file mode 100644 index 0000000..6e001d7 --- /dev/null +++ b/src/gui/theme.rs @@ -0,0 +1,97 @@ +//! Palette derived from neomodern neovim theme. +//! https://github.com/casedami/neomodern.nvim +use eframe::egui::{self, Color32, FontId, Rounding, Stroke, Style, Visuals}; + +// Base surfaces +pub const BG: Color32 = Color32::from_rgb(0x17, 0x17, 0x19); +pub const BG_PANEL: Color32 = Color32::from_rgb(0x1F, 0x1F, 0x21); +pub const BG_HOVER: Color32 = Color32::from_rgb(0x2A, 0x2A, 0x2B); + +// Interactive states +pub const BG_ACTIVE: Color32 = Color32::from_rgb(0x6C, 0x81, 0xB8); +pub const ACCENT: Color32 = Color32::from_rgb(0xAB, 0xBD, 0xEE); +pub const ACCENT_DIM: Color32 = Color32::from_rgb(0x6E, 0x6D, 0xA7); + +// Text +pub const FG: Color32 = Color32::from_rgb(0xA0, 0xA0, 0xAF); +pub const FG_DIM: Color32 = Color32::from_rgb(0x61, 0x61, 0x6A); + +// Semantic +pub const SUCCESS: Color32 = Color32::from_rgb(0x73, 0x89, 0x80); +pub const ERROR: Color32 = Color32::from_rgb(0xA6, 0x7E, 0x97); + +pub const ROUNDING: f32 = 0.0; + +pub fn apply(ctx: &egui::Context) { + let mut style = Style::default(); + style.visuals = Visuals { + dark_mode: true, + window_fill: BG, + panel_fill: BG_PANEL, + faint_bg_color: BG_PANEL, + extreme_bg_color: BG, + window_rounding: Rounding::same(ROUNDING), + window_stroke: Stroke::new(1.0, ACCENT_DIM), + widgets: { + let mut w = egui::style::Widgets::default(); + // inactive + w.inactive.bg_fill = BG_PANEL; + w.inactive.weak_bg_fill = BG_PANEL; + w.inactive.bg_stroke = Stroke::new(1.0, FG_DIM); + w.inactive.fg_stroke = Stroke::new(1.0, FG_DIM); + w.inactive.rounding = Rounding::same(ROUNDING); + // hovered + w.hovered.bg_fill = BG_HOVER; + w.hovered.weak_bg_fill = BG_HOVER; + w.hovered.bg_stroke = Stroke::new(1.0, ACCENT); + w.hovered.fg_stroke = Stroke::new(1.5, FG); + w.hovered.rounding = Rounding::same(ROUNDING); + // active (pressed) + w.active.bg_fill = BG_ACTIVE; + w.active.weak_bg_fill = BG_ACTIVE; + w.active.bg_stroke = Stroke::new(1.5, ACCENT); + w.active.fg_stroke = Stroke::new(2.0, Color32::WHITE); + w.active.rounding = Rounding::same(ROUNDING); + // open (selected/toggled) + w.open.bg_fill = BG_ACTIVE; + w.open.weak_bg_fill = BG_ACTIVE; + w.open.bg_stroke = Stroke::new(1.5, ACCENT); + w.open.fg_stroke = Stroke::new(2.0, Color32::WHITE); + w.open.rounding = Rounding::same(ROUNDING); + // noninteractive + w.noninteractive.bg_fill = BG_PANEL; + w.noninteractive.weak_bg_fill = BG_PANEL; + w.noninteractive.bg_stroke = Stroke::new(1.0, FG_DIM); + w.noninteractive.fg_stroke = Stroke::new(1.0, FG); + w.noninteractive.rounding = Rounding::same(ROUNDING); + w + }, + selection: egui::style::Selection { + bg_fill: BG_HOVER, + stroke: Stroke::new(1.0, ACCENT), + }, + hyperlink_color: ACCENT, + override_text_color: Some(FG), + ..Visuals::dark() + }; + style.spacing.item_spacing = egui::vec2(8.0, 6.0); + style.spacing.button_padding = egui::vec2(12.0, 7.0); + style.spacing.window_margin = egui::Margin::same(16.0); + style.spacing.scroll = egui::style::ScrollStyle::solid(); + style + .text_styles + .insert(egui::TextStyle::Body, FontId::proportional(14.0)); + style + .text_styles + .insert(egui::TextStyle::Button, FontId::proportional(14.0)); + style + .text_styles + .insert(egui::TextStyle::Heading, FontId::proportional(18.0)); + style + .text_styles + .insert(egui::TextStyle::Small, FontId::proportional(11.0)); + style + .text_styles + .insert(egui::TextStyle::Monospace, FontId::monospace(13.0)); + ctx.set_style(style); +} diff --git a/src/gui/widgets.rs b/src/gui/widgets.rs new file mode 100644 index 0000000..53aee1b --- /dev/null +++ b/src/gui/widgets.rs @@ -0,0 +1,130 @@ +use eframe::egui::{self, Color32, Response, RichText, Sense, Ui, Vec2}; + +use super::theme; + +/// A pill shaped toggle button. Returns true when clicked. +pub fn pill_button(ui: &mut Ui, label: &str, selected: bool) -> Response { + let (bg, fg) = if selected { + (theme::BG_ACTIVE, Color32::WHITE) + } else { + (theme::BG_PANEL, theme::FG_DIM) + }; + + let text = RichText::new(label).color(fg).size(13.5); + let btn = egui::Button::new(text) + .fill(bg) + .stroke(egui::Stroke::new( + if selected { 1.5 } else { 1.0 }, + if selected { + theme::ACCENT + } else { + theme::ACCENT_DIM + }, + )) + .rounding(egui::Rounding::same(theme::ROUNDING)) + .min_size(Vec2::new(80.0, 30.0)); + + ui.add(btn) +} + +/// Large action button (e.g. "Capture") +pub fn primary_button(ui: &mut Ui, label: &str) -> Response { + let text = RichText::new(label) + .color(Color32::WHITE) + .size(15.0) + .strong(); + + ui.add( + egui::Button::new(text) + .fill(theme::ACCENT) + .stroke(egui::Stroke::new(1.0, theme::ACCENT)) + .rounding(egui::Rounding::same(theme::ROUNDING)) + .min_size(Vec2::new(120.0, 36.0)), + ) +} + +/// Secondary action button (e.g. "Copy", "Save") +pub fn secondary_button(ui: &mut Ui, label: &str) -> Response { + let text = RichText::new(label).color(theme::FG).size(13.5); + ui.add( + egui::Button::new(text) + .fill(theme::BG_PANEL) + .stroke(egui::Stroke::new(1.0, theme::ACCENT_DIM)) + .rounding(egui::Rounding::same(theme::ROUNDING)) + .min_size(Vec2::new(90.0, 32.0)), + ) +} + +/// Heading label +pub fn section_heading(ui: &mut Ui, label: &str) { + ui.label( + RichText::new(label) + .color(theme::FG_DIM) + .size(11.0) + .strong(), + ); +} + +/// Status badge: green for success, red for error, dimmed for info. +pub fn status_label(ui: &mut Ui, msg: &str, is_error: bool) { + let color = if is_error { + theme::ERROR + } else { + theme::SUCCESS + }; + ui.label(RichText::new(msg).color(color).size(12.5)); +} + +/// Thin horizontal rule. +pub fn separator(ui: &mut Ui) { + let rect = ui.available_rect_before_wrap(); + let y = ui.cursor().min.y + 4.0; + ui.painter().hline( + rect.left()..=rect.right(), + y, + egui::Stroke::new(1.0, theme::ACCENT_DIM), + ); + ui.add_space(10.0); +} + +/// Labelled slider returning the new value. +pub fn labelled_slider( + ui: &mut Ui, + label: &str, + value: &mut f32, + range: std::ops::RangeInclusive, +) { + ui.horizontal(|ui| { + ui.label(RichText::new(label).color(theme::FG_DIM).size(12.5)); + ui.add( + egui::Slider::new(value, range) + .show_value(true) + .trailing_fill(true), + ); + }); +} + +/// Drag area for a borderless window. +/// Call this at the very top of your central panel. Returns the response +/// Caller can check for right-click etc. +pub fn drag_area(ui: &mut Ui, _id: egui::Id) -> Response { + let (rect, resp) = ui.allocate_exact_size( + egui::vec2(ui.available_width(), 24.0), + Sense::click_and_drag(), + ); + + if resp.dragged() { + ui.ctx().send_viewport_cmd(egui::ViewportCommand::StartDrag); + } + + // Title bar label + ui.painter().text( + rect.center(), + egui::Align2::CENTER_CENTER, + "blasted", + egui::FontId::proportional(13.0), + theme::FG_DIM, + ); + + resp +} diff --git a/src/hyprland.rs b/src/hyprland.rs new file mode 100644 index 0000000..121e607 --- /dev/null +++ b/src/hyprland.rs @@ -0,0 +1,268 @@ +//! Direct Hyprland IPC client. + +use std::{ + env, + io::{Read, Write}, + os::unix::net::UnixStream, + path::PathBuf, + time::Duration, +}; + +use serde::Deserialize; + +use crate::error::{BlastError, Result}; + +fn his() -> Result { + env::var("HYPRLAND_INSTANCE_SIGNATURE").map_err(|_| { + BlastError::Hyprland("HYPRLAND_INSTANCE_SIGNATURE not set ->>> is Hyprland running?".into()) + }) +} + +fn runtime_dir() -> PathBuf { + env::var("XDG_RUNTIME_DIR") + .map(PathBuf::from) + .unwrap_or_else(|_| PathBuf::from("/run/user/1000")) +} + +pub fn socket1_path() -> Result { + Ok(runtime_dir().join("hypr").join(his()?).join(".socket.sock")) +} + +#[allow(dead_code)] +pub fn socket2_path() -> Result { + Ok(runtime_dir() + .join("hypr") + .join(his()?) + .join(".socket2.sock")) +} + +// raw IPC + +pub fn ipc_request(command: &str) -> Result { + let path = socket1_path()?; + let mut stream = UnixStream::connect(&path) + .map_err(|e| BlastError::Hyprland(format!("connect to {path:?}: {e}")))?; + stream + .write_all(command.as_bytes()) + .map_err(|e| BlastError::Hyprland(format!("write: {e}")))?; + stream.set_read_timeout(Some(Duration::from_secs(5))).ok(); + let mut response = String::new(); + stream + .read_to_string(&mut response) + .map_err(|e| BlastError::Hyprland(format!("read: {e}")))?; + Ok(response) +} + +pub fn ipc_json(command: &str) -> Result { + ipc_request(&format!("j/{command}")) +} + +#[derive(Debug, Deserialize)] +pub struct WindowInfo { + pub at: [i64; 2], + pub size: [i64; 2], + pub class: String, + #[allow(dead_code)] + pub title: String, + pub workspace: WorkspaceRef, +} + +#[derive(Debug, Deserialize, Clone)] +pub struct WorkspaceRef { + pub id: i64, + #[allow(dead_code)] + pub name: String, +} + +/// Full monitor description from j/monitors. +#[derive(Debug, Deserialize, Clone)] +pub struct MonitorInfo { + pub name: String, + pub focused: bool, + /// Logical X origin of this monitor in the global compositor space. + pub x: i64, + /// Logical Y origin of this monitor in the global compositor space. + pub y: i64, + /// Physical pixel width (raw resolution, before scale). + pub width: i64, + /// Physical pixel height (raw resolution, before scale). + pub height: i64, + /// Fractional scale factor (e.g. 1.5, 2.0). + pub scale: f64, + #[serde(rename = "activeWorkspace")] + pub active_workspace: WorkspaceRef, +} + +impl MonitorInfo { + /// Logical width = physical ÷ scale. This is the coordinate space + pub fn logical_width(&self) -> i64 { + (self.width as f64 / self.scale).round() as i64 + } + + /// Logical height = physical ÷ scale. + pub fn logical_height(&self) -> i64 { + (self.height as f64 / self.scale).round() as i64 + } + + /// True if the logical point (px, py) lies within this monitor's logical bounds. + pub fn contains_point(&self, px: i64, py: i64) -> bool { + px >= self.x + && px < self.x + self.logical_width() + && py >= self.y + && py < self.y + self.logical_height() + } + + /// True if any corner of the geometry overlaps this monitor's logical bounds. + pub fn contains_geometry(&self, g: &Geometry) -> bool { + self.contains_point(g.x, g.y) + || self.contains_point(g.x + g.w - 1, g.y) + || self.contains_point(g.x, g.y + g.h - 1) + || self.contains_point(g.x + g.w - 1, g.y + g.h - 1) + } +} + +#[derive(Debug, Deserialize)] +#[allow(dead_code)] +pub struct AnimationEntry { + pub name: String, + pub enabled: bool, + pub speed: f64, + pub bezier: String, +} + +#[derive(Debug, Deserialize)] +struct OptionInt { + int: i64, +} + +pub fn active_window() -> Result { + let json = ipc_json("activewindow")?; + serde_json::from_str(&json) + .map_err(|e| BlastError::Hyprland(format!("parse activewindow: {e}\nraw: {json}"))) +} + +pub fn monitors() -> Result> { + let json = ipc_json("monitors")?; + serde_json::from_str(&json).map_err(|e| BlastError::Hyprland(format!("parse monitors: {e}"))) +} + +pub fn focused_monitor() -> Result { + monitors()? + .into_iter() + .find(|m| m.focused) + .ok_or_else(|| BlastError::Hyprland("no focused monitor found".into())) +} + +pub fn monitor_for_geometry(g: &Geometry) -> Result { + let mons = monitors()?; + // try monitor that contains the top-left corner. + if let Some(m) = mons.iter().find(|m| m.contains_geometry(g)) { + return Ok(m.clone()); + } + // Fallback: focused, then first. + mons.iter() + .find(|m| m.focused) + .or_else(|| mons.first()) + .cloned() + .ok_or_else(|| BlastError::Hyprland("no monitors found".into())) +} + +pub fn clients() -> Result> { + let json = ipc_json("clients")?; + serde_json::from_str(&json).map_err(|e| BlastError::Hyprland(format!("parse clients: {e}"))) +} + +pub fn active_workspace_ids() -> Result> { + Ok(monitors()?.iter().map(|m| m.active_workspace.id).collect()) +} + +pub fn visible_windows() -> Result> { + let active_ids = active_workspace_ids()?; + Ok(clients()? + .into_iter() + .filter(|w| active_ids.contains(&w.workspace.id)) + .collect()) +} + +pub fn get_option_int(option: &str) -> Result { + let json = ipc_json(&format!("getoption {option}"))?; + let opt: OptionInt = serde_json::from_str(&json) + .map_err(|e| BlastError::Hyprland(format!("parse option {option}: {e}")))?; + Ok(opt.int) +} + +pub fn border_size() -> Result { + get_option_int("general:border_size") +} +pub fn rounding() -> Result { + get_option_int("decoration:rounding") +} + +pub fn set_keyword(keyword: &str, value: &str) -> Result<()> { + ipc_request(&format!("keyword {keyword} {value}"))?; + Ok(()) +} + +pub fn get_fade_animations() -> Result<(String, String)> { + let json = ipc_json("animations")?; + let outer: Vec> = serde_json::from_str(&json) + .map_err(|e| BlastError::Hyprland(format!("parse animations: {e}")))?; + let entries: Vec = outer.into_iter().flatten().collect(); + let find = |name: &str| -> Option { + entries.iter().find(|e| e["name"] == name).map(|e| { + let enabled = if e["enabled"].as_bool().unwrap_or(false) { + "1" + } else { + "0" + }; + let speed = e["speed"].as_f64().unwrap_or(1.0).floor() as i64; + let bezier = e["bezier"].as_str().unwrap_or("default"); + format!("{name},{enabled},{speed},{bezier}") + }) + }; + Ok(( + find("fade").unwrap_or_else(|| "fade,1,3,default".into()), + find("fadeOut").unwrap_or_else(|| "fadeOut,1,3,default".into()), + )) +} + +pub fn with_animations_disabled(f: F) -> Result +where + F: FnOnce(i64) -> Result, +{ + let (fade, fade_out) = get_fade_animations()?; + let bs = border_size()?; + set_keyword("animation", "fade,0,1,default")?; + set_keyword("animation", "fadeOut,0,1,default")?; + let result = f(bs); + let _ = set_keyword("animation", &fade); + let _ = set_keyword("animation", &fade_out); + result +} + +// Geom +#[derive(Debug, Clone, Copy)] +pub struct Geometry { + pub x: i64, + pub y: i64, + pub w: i64, + pub h: i64, +} + +impl Geometry { + #[allow(dead_code)] + pub fn to_geom_string(self) -> String { + format!("{},{} {}x{}", self.x, self.y, self.w, self.h) + } +} + +impl WindowInfo { + pub fn to_geometry(&self, border_size: i64, bar_height: i64) -> Geometry { + Geometry { + x: self.at[0] - border_size, + y: self.at[1] - border_size - bar_height, + w: self.size[0] + border_size * 2, + h: self.size[1] + border_size * 2 + bar_height, + } + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..7fdc402 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,285 @@ +//! corner rounding and dropshadow for RGBA images. + +use image::{imageops, DynamicImage, GenericImageView, ImageBuffer, ImageError, Rgba}; +use png::{BitDepth, ColorType, Encoder}; +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum ShhhError { + #[error("image decode error: {0}")] + Decode(#[from] ImageError), + #[error("PNG encode error: {0}")] + Encode(#[from] png::EncodingError), + #[error("IO error: {0}")] + Io(#[from] std::io::Error), + #[error("format detection failed: {0}")] + Format(String), +} + +#[derive(Debug, Clone)] +pub struct ShadowOptions { + /// Corner rounding radius in pixels (default 8). + pub corner_radius: u32, + /// Shadow offset as (x, y) pixels (default (-20, -20)). + pub offset: (i32, i32), + /// Shadow opacity 0–255 (default 150). + pub shadow_alpha: u8, + /// Shadow spread distance in pixels (default 26). + pub spread: u32, + /// Internal blur radius applied to the shadow layer (default 5). + pub blur_radius: u32, +} + +impl Default for ShadowOptions { + fn default() -> Self { + Self { + corner_radius: 8, + offset: (-20, -20), + shadow_alpha: 150, + spread: 26, + blur_radius: 5, + } + } +} + +/// Process raw PNG bytes: round corners, add drop shadow, return PNG bytes. +pub fn process_image_bytes(input: &[u8], opts: &ShadowOptions) -> Result, ShhhError> { + let img = decode_image(input)?; + let processed = apply_effects(&img, opts)?; + encode_png(&processed) +} + +/// Process an already-decoded [DynamicImage] and return a new one. +/// +/// Hold an image in memory and want to avoid an extra encode/decode roundtrip. +pub fn apply_effects(img: &DynamicImage, opts: &ShadowOptions) -> Result { + let rounded = round_corners(img, opts.corner_radius); + let shadowed = add_rounded_drop_shadow( + &rounded, + opts.offset.0, + opts.offset.1, + opts.blur_radius, + opts.spread, + opts.shadow_alpha, + )?; + Ok(shadowed) +} + +/// Decode raw bytes into a [DynamicImage]. +pub fn decode_image(bytes: &[u8]) -> Result { + use image::ImageReader; + let reader = ImageReader::new(std::io::Cursor::new(bytes)) + .with_guessed_format() + .map_err(|e| ShhhError::Format(e.to_string()))?; + Ok(reader.decode()?) +} + +/// Encode a [DynamicImage] to PNG bytes (RGBA8). +pub fn encode_png(img: &DynamicImage) -> Result, ShhhError> { + let rgba = img.to_rgba8(); + let (width, height) = rgba.dimensions(); + let mut out: Vec = Vec::new(); + { + let mut encoder = Encoder::new(&mut out, width, height); + encoder.set_color(ColorType::Rgba); + encoder.set_depth(BitDepth::Eight); + let mut writer = encoder.write_header()?; + writer.write_image_data(rgba.as_raw())?; + } + Ok(out) +} + +// Image post-processing internals + +/// Apply anti-aliased corner rounding to an image. +pub fn round_corners(img: &DynamicImage, radius: u32) -> DynamicImage { + let (width, height) = img.dimensions(); + let mut rounded: ImageBuffer, Vec> = ImageBuffer::new(width, height); + let r = radius as f32; + + for (x, y, pixel) in img.to_rgba8().enumerate_pixels() { + // Determine which corner quadrant, if any, this pixel falls into. + let corner: Option<(f32, f32)> = if x < radius && y < radius { + Some((r - x as f32, r - y as f32)) + } else if x >= width - radius && y < radius { + Some((x as f32 - (width as f32 - r - 1.0), r - y as f32)) + } else if x < radius && y >= height - radius { + Some((r - x as f32, y as f32 - (height as f32 - r - 1.0))) + } else if x >= width - radius && y >= height - radius { + Some(( + x as f32 - (width as f32 - r - 1.0), + y as f32 - (height as f32 - r - 1.0), + )) + } else { + None + }; + + match corner { + None => rounded.put_pixel(x, y, *pixel), + Some((dx, dy)) => { + let dist = (dx * dx + dy * dy).sqrt(); + if dist <= r { + rounded.put_pixel(x, y, *pixel); + } else { + // Anti-alias: fade alpha over the last pixel. + let alpha = ((r + 1.0 - dist).max(0.0) * 255.0) as u8; + rounded.put_pixel( + x, + y, + Rgba([pixel[0], pixel[1], pixel[2], alpha.min(pixel[3])]), + ); + } + } + } + } + + DynamicImage::ImageRgba8(rounded) +} + +/// Composite a dropshadow behind a rounded image. +pub fn add_rounded_drop_shadow( + img: &DynamicImage, + offset_x: i32, + offset_y: i32, + blur_radius: u32, + spread: u32, + shadow_alpha: u8, +) -> Result { + let (width, height) = img.dimensions(); + + let padding = spread + blur_radius * 2; + let total_w = (width as i32 + offset_x.abs() + padding as i32 * 2) as u32; + let total_h = (height as i32 + offset_y.abs() + padding as i32 * 2) as u32; + + let mut output: ImageBuffer, Vec> = ImageBuffer::new(total_w, total_h); + + let shadow = create_shadow(img, blur_radius, spread, shadow_alpha); + + // Shadow position: pushed back relative to the image. + let shadow_x: i64 = if offset_x >= 0 { + padding as i64 + } else { + padding as i64 + offset_x as i64 + }; + let shadow_y: i64 = if offset_y >= 0 { + padding as i64 + } else { + padding as i64 + offset_y as i64 + }; + imageops::overlay(&mut output, &shadow, shadow_x, shadow_y); + + // Image position: pushed forward relative to the shadow. + let image_x: i64 = if offset_x >= 0 { + padding as i64 + offset_x as i64 + } else { + padding as i64 + }; + let image_y: i64 = if offset_y >= 0 { + padding as i64 + offset_y as i64 + } else { + padding as i64 + }; + imageops::overlay(&mut output, img, image_x, image_y); + + Ok(DynamicImage::ImageRgba8(output)) +} + +/// Build the blurred shadow layer for a given image. +fn create_shadow( + img: &DynamicImage, + blur_radius: u32, + spread: u32, + shadow_alpha: u8, +) -> DynamicImage { + let (width, height) = img.dimensions(); + let padding = spread + blur_radius * 2; + let new_w = width + padding * 2; + let new_h = height + padding * 2; + + let mut shadow: ImageBuffer, Vec> = ImageBuffer::new(new_w, new_h); + imageops::overlay(&mut shadow, &img.to_rgba8(), padding as i64, padding as i64); + + // Flatten to a solid black mask preserving original alpha. + for pixel in shadow.pixels_mut() { + let alpha = pixel[3] as f32 / 255.0 * shadow_alpha as f32; + *pixel = Rgba([0, 0, 0, alpha as u8]); + } + + let adjusted_blur = blur_radius + (spread as f32 / 2.0) as u32; + let blurred = imageops::blur(&shadow, adjusted_blur as f32); + + // Soften the shadow edges with a sqrt gamma curve. + let mut cleaned: ImageBuffer, Vec> = ImageBuffer::new(new_w, new_h); + for (x, y, pixel) in blurred.enumerate_pixels() { + if pixel[3] > 0 { + let factor = (pixel[3] as f32 / 255.0).powf(0.5); + cleaned.put_pixel( + x, + y, + Rgba([ + (pixel[0] as f32 * factor) as u8, + (pixel[1] as f32 * factor) as u8, + (pixel[2] as f32 * factor) as u8, + (pixel[3] as f32 * factor) as u8, + ]), + ); + } + } + + DynamicImage::ImageRgba8(cleaned) +} + +// tests + +#[cfg(test)] +mod tests { + use super::*; + use image::{ImageBuffer, Rgba}; + + fn solid_rgba(w: u32, h: u32, color: Rgba) -> DynamicImage { + DynamicImage::ImageRgba8(ImageBuffer::from_pixel(w, h, color)) + } + + #[test] + fn round_corners_shrinks_corners() { + let img = solid_rgba(100, 100, Rgba([255, 0, 0, 255])); + let rounded = round_corners(&img, 10); + let px = rounded.to_rgba8().get_pixel(0, 0).clone(); + assert_eq!(px[3], 0, "corner pixel alpha should be 0"); + let center = rounded.to_rgba8().get_pixel(50, 50).clone(); + assert_eq!(center[3], 255); + } + + #[test] + fn shadow_expands_canvas() { + let img = solid_rgba(100, 100, Rgba([255, 255, 255, 255])); + let opts = ShadowOptions::default(); + let result = apply_effects(&img, &opts).unwrap(); + let (w, h) = result.dimensions(); + assert!(w > 100, "shadow should expand canvas width"); + assert!(h > 100, "shadow should expand canvas height"); + } + + #[test] + fn encode_decode_roundtrip() { + let img = solid_rgba(64, 64, Rgba([128, 64, 32, 255])); + let bytes = encode_png(&img).unwrap(); + let decoded = decode_image(&bytes).unwrap(); + assert_eq!(decoded.dimensions(), (64, 64)); + } + + #[test] + fn process_image_bytes_end_to_end() { + let img = solid_rgba(50, 50, Rgba([200, 100, 50, 255])); + let input_bytes = encode_png(&img).unwrap(); + let opts = ShadowOptions { + corner_radius: 5, + ..ShadowOptions::default() + }; + let output = process_image_bytes(&input_bytes, &opts).unwrap(); + assert!(!output.is_empty()); + let decoded = decode_image(&output).unwrap(); + let (w, h) = decoded.dimensions(); + assert!(w > 50 && h > 50); + } +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..fcecbd1 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,490 @@ +mod capture; +mod clipboard; +mod error; +mod freeze; +mod hyprland; +mod notify; +mod paths; +mod region; +mod select; +mod shhh; +mod wayland_windows; + +use std::{ + io::{self, Write}, + path::PathBuf, + process, +}; + +use clap::{Args, Parser, Subcommand, ValueEnum}; +use image::DynamicImage; +use shhh::{encode_png, ShadowOptions}; + +use capture::CaptureOptions; +use error::{BlastError, Result}; + +#[derive(Debug, Parser)] +#[command(name = "blast", about = "Hyprland screenshot tool")] +struct Cli { + #[arg(short = 'n', long)] + notify: bool, + + #[arg(short = 'c', long)] + cursor: bool, + + #[arg(short = 'f', long)] + freeze: bool, + + #[arg(short = 'w', long, value_name = "N")] + wait: Option, + + #[arg(short = 's', long, value_name = "SCALE")] + scale: Option, + + /// Hint-box highlight colour for area/region selection (RRGGBBAA hex, e.g. ff550080). + #[arg(long = "hint-color", value_name = "RRGGBBAA", value_parser = parse_color)] + hint_color: Option, + + #[command(flatten)] + shadow: ShadowArgs, + + #[command(subcommand)] + action: Option, +} + +#[derive(Debug, Args, Clone)] +struct ShadowArgs { + #[arg(long = "no-shadow")] + no_shadow: bool, + + #[arg(long = "radius", value_name = "PX")] + radius: Option, + + #[arg(long = "shadow-offset", value_name = "X,Y", value_parser = parse_offset)] + offset: Option<(i32, i32)>, + + #[arg(long = "shadow-alpha", value_name = "0-255")] + alpha: Option, + + #[arg(long = "shadow-spread", value_name = "PX")] + spread: Option, + + #[arg(long = "shadow-blur", value_name = "PX")] + blur: Option, +} + +fn parse_color(s: &str) -> std::result::Result { + let s = s.trim_start_matches('#'); + let s: std::borrow::Cow = if s.len() == 6 { + format!("{s}ff").into() + } else { + s.into() + }; + if s.len() != 8 { + return Err(format!("expected RRGGBBAA (6 or 8 hex digits), got '{s}'")); + } + u32::from_str_radix(&s, 16).map_err(|e| format!("invalid color '{s}': {e}")) +} + +fn parse_offset(s: &str) -> std::result::Result<(i32, i32), String> { + let parts: Vec<&str> = s.splitn(2, ',').collect(); + if parts.len() != 2 { + return Err(format!("expected X,Y e.g. '-20,-20', got '{s}'")); + } + let x = parts[0] + .trim() + .parse::() + .map_err(|e| format!("bad X: {e}"))?; + let y = parts[1] + .trim() + .parse::() + .map_err(|e| format!("bad Y: {e}"))?; + Ok((x, y)) +} + +impl ShadowArgs { + /// Resolve into a ShadowOptions, pulling the corner radius from Hyprland + /// IPC if not explicitly provided. + fn resolve(&self) -> Option { + if self.no_shadow { + return None; + } + + let corner_radius = self.radius.unwrap_or_else(|| { + let bs = hyprland::border_size().unwrap_or(0); + let rnd = hyprland::rounding().unwrap_or(8); + (rnd + bs) as u32 + }); + + Some(ShadowOptions { + corner_radius, + offset: self.offset.unwrap_or((-20, -20)), + shadow_alpha: self.alpha.unwrap_or(150), + spread: self.spread.unwrap_or(26), + blur_radius: self.blur.unwrap_or(5), + }) + } +} + +#[derive(Debug, Subcommand)] +enum Action { + Copy { + #[arg(value_enum, default_value_t = Subject::Screen)] + subject: Subject, + }, + Save { + #[arg(value_enum, default_value_t = Subject::Screen)] + subject: Subject, + /// Output file path, or '-' for stdout. + file: Option, + }, + Copysave { + #[arg(value_enum, default_value_t = Subject::Screen)] + subject: Subject, + #[arg(short = 'o', long = "output")] + output: Option, + #[arg(hide = true)] + file: Option, + }, + Edit { + #[arg(value_enum, default_value_t = Subject::Screen)] + subject: Subject, + /// Output file path (defaults to /tmp/.png). + file: Option, + }, + Check, +} + +#[derive(Debug, Clone, ValueEnum)] +enum Subject { + Active, + Screen, + Output, + Area, + Region, +} + +fn main() { + let cli = Cli::parse(); + + let action = match cli.action { + Some(a) => a, + None => { + print_usage(); + process::exit(0); + } + }; + + let ctx = Context { + notify: cli.notify, + cursor: cli.cursor, + freeze: cli.freeze, + wait_secs: cli.wait, + scale: cli.scale, + hint_color: cli.hint_color, + shadow: cli.shadow, + }; + + if let Err(e) = run(action, ctx) { + match e { + BlastError::SelectionCancelled => process::exit(1), + other => { + eprintln!("blast: error: {other}"); + process::exit(2); + } + } + } +} + +fn print_usage() { + println!("Usage:"); + println!( + " blast [FLAGS] (copy|save|copysave|edit) [active|screen|output|area|region] [FILE|-]" + ); + println!(" blast check"); + println!(); + println!("Flags:"); + println!(" -n, --notify Desktop notification on completion"); + println!(" -c, --cursor Include cursor in screenshot"); + println!(" -f, --freeze Freeze screen during area selection"); + println!(" -w, --wait N Wait N seconds before capture"); + println!(" -s, --scale SCALE Scale factor"); + println!(" --hint-color RRGGBBAA Hint-box colour for area/region (e.g. ff550080)"); + println!(" --no-shadow Disable drop shadow and corner rounding"); + println!(" --radius px Corner radius (default: from Hyprland config or 0)"); + println!(" --shadow-offset x,y Shadow offset (default: -20,-20)"); + println!(" --shadow-alpha N Shadow opacity 0-255 (default: 150)"); + println!(" --shadow-spread px Shadow spread (default: 26)"); + println!(" --shadow-blur px Shadow blur radius (default: 5)"); +} + +struct Context { + notify: bool, + cursor: bool, + freeze: bool, + wait_secs: Option, + scale: Option, + hint_color: Option, + shadow: ShadowArgs, +} + +impl Context { + fn capture_opts(&self) -> CaptureOptions { + CaptureOptions { + include_cursor: self.cursor, + scale: self.scale, + } + } + + fn maybe_wait(&self) { + if let Some(secs) = self.wait_secs { + std::thread::sleep(std::time::Duration::from_secs(secs)); + } + } + + fn notify_ok(&self, summary: &str, body: Option<&str>, icon: Option<&str>) { + if self.notify { + let _ = notify::notify_ok(summary, body, icon); + } + } + + fn notify_err(&self, summary: &str, body: Option<&str>) { + if self.notify { + let _ = notify::notify_error(summary, body); + } else { + eprintln!("blast: {}: {}", summary, body.unwrap_or("")); + } + } + + /// Encode image to PNG, applying shadow/rounding if enabled. + fn finalize_png(&self, img: &DynamicImage) -> Result> { + match self.shadow.resolve() { + Some(opts) => shhh::apply_and_encode(img, &opts), + None => encode_png(img), + } + } +} + +fn run(action: Action, ctx: Context) -> Result<()> { + match action { + Action::Check => run_check(), + Action::Copy { subject } => run_copy(subject, ctx), + Action::Save { subject, file } => run_save(subject, file, ctx), + Action::Copysave { + subject, + output, + file, + } => { + // --output takes precedence over the positional file arg. + run_copysave(subject, output.or(file), ctx) + } + Action::Edit { subject, file } => run_edit(subject, file, ctx), + } +} + +fn run_check() -> Result<()> { + println!("Checking Hyprland IPC..."); + match hyprland::active_window() { + Ok(w) => println!(" hyprland IPC: OK (active: {})", w.class), + Err(e) => println!(" hyprland IPC: FAIL ({e})"), + } + + println!("Checking wlr-screencopy..."); + match capture::capture_screen(&CaptureOptions::default()) { + Ok(_) => println!(" wlr-screencopy: OK"), + Err(e) => println!(" wlr-screencopy: FAIL ({e})"), + } + + println!("Checking shhh image processing..."); + { + use image::{DynamicImage, ImageBuffer, Rgba}; + let img = DynamicImage::ImageRgba8(ImageBuffer::from_pixel(4, 4, Rgba([255u8, 0, 0, 255]))); + match shhh::apply_effects(&img, &ShadowOptions::default()) { + Ok(_) => println!(" shhh: OK"), + Err(e) => println!(" shhh: FAIL ({e})"), + } + } + + println!(" wl-clipboard-rs: OK (linked)"); + println!(" notify-rust: OK (linked)"); + println!(" freeze (layer-shell):OK (native)"); + + Ok(()) +} + +fn run_copy(subject: Subject, ctx: Context) -> Result<()> { + let (img, what) = capture_subject(&subject, &ctx)?; + let png = ctx.finalize_png(&img)?; + clipboard::copy_png(png).map_err(|e| { + ctx.notify_err("Clipboard error", Some(&e.to_string())); + e + })?; + ctx.notify_ok(&format!("{what} copied to clipboard"), None, None); + Ok(()) +} + +fn run_save(subject: Subject, file: Option, ctx: Context) -> Result<()> { + let (img, what) = capture_subject(&subject, &ctx)?; + + match file.as_deref() { + Some("-") => { + let png = ctx.finalize_png(&img)?; + io::stdout() + .lock() + .write_all(&png) + .map_err(BlastError::Io)?; + } + path => { + let dest = resolve_dest(path); + let png = ctx.finalize_png(&img)?; + std::fs::write(&dest, &png).map_err(BlastError::Io)?; + let name = dest.display().to_string(); + ctx.notify_ok(&format!("Screenshot of {what}"), Some(&name), Some(&name)); + println!("{name}"); + } + } + + Ok(()) +} + +fn run_copysave(subject: Subject, file: Option, ctx: Context) -> Result<()> { + let (img, what) = capture_subject(&subject, &ctx)?; + let png = ctx.finalize_png(&img)?; + + clipboard::copy_png(png.clone()).map_err(|e| { + ctx.notify_err("Clipboard error", Some(&e.to_string())); + e + })?; + + let dest = resolve_dest(file.as_deref()); + std::fs::write(&dest, &png).map_err(BlastError::Io)?; + + let name = dest.display().to_string(); + ctx.notify_ok( + &format!("{what} copied and saved"), + Some(&name), + Some(&name), + ); + println!("{name}"); + Ok(()) +} + +fn run_edit(subject: Subject, file: Option, ctx: Context) -> Result<()> { + let (img, what) = capture_subject(&subject, &ctx)?; + + let dest = file + .map(PathBuf::from) + .unwrap_or_else(paths::default_editor_path); + + let png = ctx.finalize_png(&img)?; + std::fs::write(&dest, &png).map_err(BlastError::Io)?; + + let name = dest.display().to_string(); + ctx.notify_ok( + &format!("Screenshot of {what}"), + Some("Opening in image editor"), + Some(&name), + ); + + let editor = paths::resolve_editor(); + std::process::Command::new(&editor) + .arg(&dest) + .spawn() + .map_err(|e| BlastError::Other(format!("spawn {editor}: {e}")))?; + + println!("{name}"); + Ok(()) +} + +fn capture_subject(subject: &Subject, ctx: &Context) -> Result<(DynamicImage, String)> { + match subject { + Subject::Screen => { + ctx.maybe_wait(); + let img = capture::capture_screen(&ctx.capture_opts())?; + Ok((img, "Screen".into())) + } + Subject::Active => { + ctx.maybe_wait(); + let win = hyprland::active_window()?; + let bs = hyprland::border_size().unwrap_or(0); + let geom = win.to_geometry(bs, 0); + let img = capture::capture_region(geom, &ctx.capture_opts())?; + Ok((img, format!("{} window", win.class))) + } + Subject::Output => { + ctx.maybe_wait(); + let mon = hyprland::focused_monitor()?; + let img = capture::capture_output(&mon.name, &ctx.capture_opts())?; + Ok((img, mon.name.clone())) + } + Subject::Area => capture_area(ctx), + Subject::Region => capture_region_free(ctx), + } +} + +fn capture_area(ctx: &Context) -> Result<(DynamicImage, String)> { + let mut freeze_guard = if ctx.freeze { + freeze::FreezeGuard::spawn()? + } else { + freeze::FreezeGuard::none() + }; + + let on_hyprland = std::env::var("HYPRLAND_INSTANCE_SIGNATURE").is_ok(); + + let geom = if on_hyprland { + hyprland::with_animations_disabled(|border_size| { + // Prefer Wayland-native geometry; fall back to Hyprland IPC. + let boxes = wayland_windows::visible_window_hints().unwrap_or_else(|| { + let bar = hyprland::get_option_int("plugin:hyprbars:bar_height").unwrap_or(0); + hyprland::visible_windows() + .map(|wins| { + wins.iter() + .map(|w| { + let g = w.to_geometry(border_size, bar); + select::HintBox { + x: g.x as i32, + y: g.y as i32, + w: g.w as i32, + h: g.h as i32, + label: w.class.clone(), + } + }) + .collect() + }) + .unwrap_or_default() + }); + region::select_area_boxes(boxes, ctx.hint_color) + })? + } else { + let boxes = wayland_windows::visible_window_hints().unwrap_or_default(); + region::select_area_boxes(boxes, ctx.hint_color)? + }; + + let img = capture::capture_region(geom, &ctx.capture_opts())?; + freeze_guard.kill(); + Ok((img, "Area".into())) +} + +fn capture_region_free(ctx: &Context) -> Result<(DynamicImage, String)> { + let mut freeze_guard = if ctx.freeze { + freeze::FreezeGuard::spawn()? + } else { + freeze::FreezeGuard::none() + }; + + let geom = region::select_free_region(ctx.hint_color)?; + + let img = capture::capture_region(geom, &ctx.capture_opts())?; + freeze_guard.kill(); + Ok((img, "Region".into())) +} + +fn resolve_dest(path: Option<&str>) -> PathBuf { + path.map(PathBuf::from).unwrap_or_else(|| { + let prefix = hyprland::active_window() + .ok() + .map(|w| w.class) + .unwrap_or_default(); + paths::default_save_path(Some(&prefix)) + }) +} diff --git a/src/notify.rs b/src/notify.rs new file mode 100644 index 0000000..03ccfe4 --- /dev/null +++ b/src/notify.rs @@ -0,0 +1,37 @@ +use notify_rust::{Notification, Urgency}; + +use crate::error::{BlastError, Result}; + +#[allow(dead_code)] +pub fn notify_ok(summary: &str, body: Option<&str>, icon: Option<&str>) -> Result<()> { + let mut n = Notification::new(); + n.appname("blast").summary(summary).timeout(3000); + + if let Some(b) = body { + n.body(b); + } + if let Some(i) = icon { + n.icon(i); + } + + n.show().map_err(|e| BlastError::Notify(e.to_string()))?; + + Ok(()) +} + +#[allow(dead_code)] +pub fn notify_error(summary: &str, body: Option<&str>) -> Result<()> { + let mut n = Notification::new(); + n.appname("blast") + .summary(summary) + .urgency(Urgency::Critical) + .timeout(5000); + + if let Some(b) = body { + n.body(b); + } + + n.show().map_err(|e| BlastError::Notify(e.to_string()))?; + + Ok(()) +} diff --git a/src/paths.rs b/src/paths.rs new file mode 100644 index 0000000..18f6c63 --- /dev/null +++ b/src/paths.rs @@ -0,0 +1,62 @@ +//! File path helpers: XDG screenshot dir, random names, editor tmp dir. + +use std::{env, path::PathBuf}; + +use rand::{distributions::Alphanumeric, Rng}; + +/// Resolve the target directory for saving screenshots. +/// +/// Priority: $XDG_SCREENSHOTS_DIR -> $XDG_PICTURES_DIR -> $HOME. +pub fn target_directory() -> PathBuf { + if let Ok(dir) = env::var("XDG_SCREENSHOTS_DIR") { + if !dir.is_empty() { + return PathBuf::from(dir); + } + } + if let Ok(dir) = env::var("XDG_PICTURES_DIR") { + if !dir.is_empty() { + return PathBuf::from(dir); + } + } + dirs::home_dir().unwrap_or_else(|| PathBuf::from(".")) +} + +#[allow(dead_code)] +pub fn tmp_editor_directory() -> PathBuf { + PathBuf::from("/tmp") +} + +pub fn random_filename(prefix: Option<&str>) -> String { + let rand_part: String = rand::thread_rng() + .sample_iter(&Alphanumeric) + .take(13) + .map(char::from) + .collect(); + + match prefix { + Some(p) if !p.is_empty() => format!("{p}_{rand_part}.png"), + _ => format!("{rand_part}.png"), + } +} + +pub fn default_save_path(prefix: Option<&str>) -> PathBuf { + target_directory().join(random_filename(prefix)) +} + +#[allow(dead_code)] +pub fn default_editor_path() -> PathBuf { + use std::time::{SystemTime, UNIX_EPOCH}; + let ns = SystemTime::now() + .duration_since(UNIX_EPOCH) + .map(|d| d.as_nanos()) + .unwrap_or(0); + tmp_editor_directory().join(format!("{ns}.png")) +} + +/// Resolve the editor binary. +/// +/// Uses $BLAST_EDITOR if set, otherwise falls back to "gimp". +#[allow(dead_code)] +pub fn resolve_editor() -> String { + env::var("BLAST_EDITOR").unwrap_or_else(|_| "gimp".into()) +} diff --git a/src/region.rs b/src/region.rs new file mode 100644 index 0000000..de79b3c --- /dev/null +++ b/src/region.rs @@ -0,0 +1,43 @@ +use crate::{ + error::Result, + hyprland::{Geometry, WindowInfo}, + select::{self, HintBox}, +}; + +/// Area selection from a pre-built list of hint boxes (logical coords). +pub fn select_area_boxes(boxes: Vec, hint_rgba: Option) -> Result { + if boxes.is_empty() { + select::select_region(vec![], false, hint_rgba) + } else { + select::select_region(boxes, true, hint_rgba) + } +} + +pub fn select_free_region(hint_rgba: Option) -> Result { + select::select_region(vec![], false, hint_rgba) +} +#[allow(dead_code)] +pub fn select_area( + windows: &[WindowInfo], + border_size: i64, + bar_height: i64, + hint_rgba: Option, +) -> Result { + if windows.is_empty() { + return select::select_region(vec![], false, hint_rgba); + } + let boxes = windows + .iter() + .map(|w| { + let g = w.to_geometry(border_size, bar_height); + HintBox { + x: g.x as i32, + y: g.y as i32, + w: g.w as i32, + h: g.h as i32, + label: w.class.clone(), + } + }) + .collect(); + select::select_region(boxes, true, hint_rgba) +} diff --git a/src/select.rs b/src/select.rs new file mode 100644 index 0000000..f94ecd7 --- /dev/null +++ b/src/select.rs @@ -0,0 +1,873 @@ +//! Native Wayland region/area selector. + +use std::{ + fs::File, + os::unix::io::{AsFd, FromRawFd}, +}; + +use memmap2::{MmapMut, MmapOptions}; +use wayland_client::{ + delegate_noop, + globals::{registry_queue_init, GlobalListContents}, + protocol::{ + wl_buffer, wl_callback, wl_compositor, wl_keyboard, wl_output, wl_pointer, wl_region, + wl_registry, wl_seat, wl_shm, wl_shm_pool, wl_surface, + }, + Connection, Dispatch, EventQueue, Proxy, QueueHandle, WEnum, +}; +use wayland_protocols::wp::cursor_shape::v1::client::{ + wp_cursor_shape_device_v1::{Shape as CursorShape, WpCursorShapeDeviceV1}, + wp_cursor_shape_manager_v1::WpCursorShapeManagerV1, +}; +use wayland_protocols_wlr::layer_shell::v1::client::{ + zwlr_layer_shell_v1::ZwlrLayerShellV1, + zwlr_layer_surface_v1::{self, ZwlrLayerSurfaceV1}, +}; + +use crate::{ + error::{BlastError, Result}, + hyprland::Geometry, +}; + +const C_BG: [u8; 4] = [0x26, 0x26, 0x26, 0x26]; +const C_SEL: [u8; 4] = [0x00, 0x00, 0x00, 0x00]; +const C_BDR: [u8; 4] = [0x00, 0x00, 0x00, 0xFF]; + +const DEFAULT_HINT: [u8; 4] = [0x40, 0x40, 0x40, 0x40]; +const DEFAULT_HOV: [u8; 4] = [0x80, 0x80, 0x80, 0x80]; + +fn rgba_to_bgra_premult(rgba: u32) -> [u8; 4] { + let r = ((rgba >> 24) & 0xFF) as u8; + let g = ((rgba >> 16) & 0xFF) as u8; + let b = ((rgba >> 8) & 0xFF) as u8; + let a = (rgba & 0xFF) as u8; + let pm = |c: u8| -> u8 { ((c as u32 * a as u32) / 255) as u8 }; + [pm(b), pm(g), pm(r), a] +} + +fn derive_hover(rgba: u32) -> [u8; 4] { + let r = ((rgba >> 24) & 0xFF) as u32; + let g = ((rgba >> 16) & 0xFF) as u32; + let b = ((rgba >> 8) & 0xFF) as u32; + let a = ((rgba & 0xFF) as u32 * 2).min(255); + let pm = |c: u32| -> u8 { ((c * a) / 255) as u8 }; + [pm(b), pm(g), pm(r), a as u8] +} + +const BDR: i32 = 2; + +pub struct HintBox { + pub x: i32, + pub y: i32, + pub w: i32, + pub h: i32, + pub label: String, +} + +struct PendingOut { + wl: wl_output::WlOutput, + name: Option, + ox: i32, + oy: i32, + scale: i32, +} + +struct Surf { + wl: wl_surface::WlSurface, + layer: ZwlrLayerSurfaceV1, + configured: bool, + lx: i32, + ly: i32, + lw: i32, + lh: i32, + scale: i32, + pw: i32, + ph: i32, + stride: usize, + buf_size: usize, + _file: Option, + _pool: Option, + // Two buffers for double-buffering + bufs: [Option; 2], + mmap: Option, + buf_busy: [bool; 2], + // per-surface render state + frame_pending: bool, + needs_repaint: bool, +} + +struct St { + conn: Connection, + qh: QueueHandle, + queue: Option>, + + compositor: Option, + shm: Option, + shell: Option, + seat: Option, + pointer: Option, + keyboard: Option, + cursor_shape: Option, + + pending: Vec, + surfs: Vec, + configured: usize, // count of configure events received + + on_surf: Option, // index into surfs for current pointer output + cx: i32, + cy: i32, // cursor position (global logical) + + anchor_x: i32, + anchor_y: i32, + dragging: bool, + + boxes: Vec, + hover: Option, + restrict: bool, + c_hint: [u8; 4], + c_hov: [u8; 4], + + running: bool, + result: Option, +} + +fn fill(data: &mut [u8], stride: usize, x0: i32, y0: i32, x1: i32, y1: i32, c: [u8; 4]) { + for y in y0..y1 { + let row = y as usize * stride; + for x in x0..x1 { + let i = row + x as usize * 4; + data[i..i + 4].copy_from_slice(&c); + } + } +} + +fn draw_border( + data: &mut [u8], + stride: usize, + pw: i32, + ph: i32, + x0: i32, + y0: i32, + x1: i32, + y1: i32, + bw: i32, + c: [u8; 4], +) { + let strips = [ + (x0.max(0), y0.max(0), x1.min(pw), (y0 + bw).min(y1).min(ph)), + (x0.max(0), (y1 - bw).max(y0).max(0), x1.min(pw), y1.min(ph)), + ( + x0.max(0), + (y0 + bw).max(0), + (x0 + bw).min(x1).min(pw), + (y1 - bw).min(ph), + ), + ( + (x1 - bw).max(x0).max(0), + (y0 + bw).max(0), + x1.min(pw), + (y1 - bw).min(ph), + ), + ]; + for (ax0, ay0, ax1, ay1) in strips { + if ax0 < ax1 && ay0 < ay1 { + fill(data, stride, ax0, ay0, ax1, ay1, c); + } + } +} + +fn rect_union( + a: Option<(i32, i32, i32, i32)>, + b: Option<(i32, i32, i32, i32)>, +) -> Option<(i32, i32, i32, i32)> { + match (a, b) { + (None, None) => None, + (Some(r), None) | (None, Some(r)) => Some(r), + (Some(a), Some(b)) => { + let x0 = a.0.min(b.0); + let y0 = a.1.min(b.1); + let x1 = (a.0 + a.2).max(b.0 + b.2); + let y1 = (a.1 + a.3).max(b.1 + b.3); + Some((x0, y0, x1 - x0, y1 - y0)) + } + } +} + +fn expand(r: Option<(i32, i32, i32, i32)>, pad: i32) -> Option<(i32, i32, i32, i32)> { + r.map(|(x, y, w, h)| (x - pad, y - pad, w + 2 * pad, h + 2 * pad)) +} + +fn alloc_shm(size: usize) -> std::io::Result { + #[cfg(target_os = "linux")] + unsafe { + let fd = libc::memfd_create(b"blast-sel\0".as_ptr().cast(), libc::MFD_CLOEXEC); + if fd >= 0 { + let f = File::from_raw_fd(fd); + f.set_len(size as u64)?; + return Ok(f); + } + } + let f = tempfile::tempfile()?; + f.set_len(size as u64)?; + Ok(f) +} + +impl St { + fn roundtrip(&mut self) -> Result<()> { + let mut q = self.queue.take().unwrap(); + let r = q + .roundtrip(self) + .map_err(|e| BlastError::Selection(format!("roundtrip: {e}"))); + self.queue = Some(q); + r?; + Ok(()) + } + + fn dispatch_once(&mut self) -> Result<()> { + let mut q = self.queue.take().unwrap(); + let r = q + .blocking_dispatch(self) + .map_err(|e| BlastError::Selection(format!("dispatch: {e}"))); + self.queue = Some(q); + r?; + Ok(()) + } + + fn flush(&self) { + if let Some(q) = &self.queue { + let _ = q.flush(); + } + } + + fn create_surfaces(&mut self) -> Result<()> { + use wayland_protocols_wlr::layer_shell::v1::client::{ + zwlr_layer_shell_v1::Layer, zwlr_layer_surface_v1::Anchor, + }; + + let qh = self.qh.clone(); + let mut surf_idx = 0usize; + + for p in &self.pending { + let (lx, ly, scale) = (p.ox, p.oy, p.scale); + + let wl_surf = self.compositor.as_ref().unwrap().create_surface(&qh, ()); + let layer = self.shell.as_ref().unwrap().get_layer_surface( + &wl_surf, + Some(&p.wl), + Layer::Overlay, + "blast-select".to_string(), + &qh, + surf_idx, + ); + + layer.set_size(0, 0); + layer.set_exclusive_zone(-1); + layer.set_anchor(Anchor::Top | Anchor::Bottom | Anchor::Left | Anchor::Right); + // Exclusive keyboard focus so ESC events are delivered to us. + layer.set_keyboard_interactivity( + zwlr_layer_surface_v1::KeyboardInteractivity::Exclusive, + ); + wl_surf.set_buffer_scale(scale); + wl_surf.commit(); + + self.surfs.push(Surf { + wl: wl_surf, + layer, + configured: false, + lx, + ly, + lw: 0, + lh: 0, + scale, + pw: 0, + ph: 0, + stride: 0, + buf_size: 0, + _file: None, + _pool: None, + bufs: [None, None], + mmap: None, + buf_busy: [false, false], + frame_pending: false, + needs_repaint: false, + }); + + surf_idx += 1; + } + + Ok(()) + } + + fn curr_sel(&self) -> Option<(i32, i32, i32, i32)> { + if !self.dragging { + return None; + } + let x0 = self.anchor_x.min(self.cx); + let y0 = self.anchor_y.min(self.cy); + let x1 = self.anchor_x.max(self.cx) + 1; + let y1 = self.anchor_y.max(self.cy) + 1; + Some((x0, y0, x1 - x0, y1 - y0)) + } + + fn update_hover(&mut self) { + if !self.restrict { + return; + } + let (cx, cy) = (self.cx, self.cy); + self.hover = self + .boxes + .iter() + .position(|b| cx >= b.x && cx < b.x + b.w && cy >= b.y && cy < b.y + b.h); + } + + fn mark_all_repaint(&mut self) { + for s in &mut self.surfs { + s.needs_repaint = true; + } + } + + /// Request a frame callback for surface idx if one is not already pending. + /// An empty commit is used to schedule the callback. + fn request_frame(&mut self, idx: usize) { + let s = &mut self.surfs[idx]; + if s.frame_pending { + return; + } + let qh = self.qh.clone(); + let s = &mut self.surfs[idx]; + s.wl.frame(&qh, idx); + s.wl.commit(); + s.frame_pending = true; + } + + fn render_surf(&mut self, idx: usize) { + let sel = self.curr_sel(); + let hover = if self.restrict { self.hover } else { None }; + + // Pick a free buffer slot + let buf_slot = { + let s = &self.surfs[idx]; + if !s.configured || s.mmap.is_none() { + return; + } + if !s.buf_busy[0] { + 0 + } else if !s.buf_busy[1] { + 1 + } else { + return; + } // both in use compositor hasn't released either yet + }; + + let s = &self.surfs[idx]; + let lx = s.lx; + let ly = s.ly; + let scale = s.scale; + let pw = s.pw; + let ph = s.ph; + let stride = s.stride; + let buf_size = s.buf_size; + + let c_hint = self.c_hint; + let c_hov = self.c_hov; + let box_data: Vec<(i32, i32, i32, i32, bool)> = self + .boxes + .iter() + .enumerate() + .map(|(i, b)| (b.x, b.y, b.w, b.h, hover == Some(i))) + .collect(); + + let s = &mut self.surfs[idx]; + let offset = buf_slot * buf_size; + let data = &mut s.mmap.as_mut().unwrap()[offset..offset + buf_size]; + + // Always repaint the full surface with double-buffering the "other" slot + // may have stale content, and a full repaint keeps both slots consistent. + + fill(data, stride, 0, 0, pw, ph, C_BG); + + for (bx, by, bw, bh, is_hov) in &box_data { + let bx0 = ((bx - lx) * scale).max(0).min(pw); + let by0 = ((by - ly) * scale).max(0).min(ph); + let bx1 = ((bx + bw - lx) * scale).max(0).min(pw); + let by1 = ((by + bh - ly) * scale).max(0).min(ph); + if bx0 < bx1 && by0 < by1 { + fill( + data, + stride, + bx0, + by0, + bx1, + by1, + if *is_hov { c_hov } else { c_hint }, + ); + if *is_hov { + draw_border(data, stride, pw, ph, bx0, by0, bx1, by1, BDR * scale, C_BDR); + } + } + } + + if let Some((gx, gy, gw, gh)) = sel { + let x0 = ((gx - lx) * scale).max(0).min(pw); + let y0 = ((gy - ly) * scale).max(0).min(ph); + let x1 = ((gx + gw - lx) * scale).max(0).min(pw); + let y1 = ((gy + gh - ly) * scale).max(0).min(ph); + if x0 < x1 && y0 < y1 { + fill(data, stride, x0, y0, x1, y1, C_SEL); + draw_border(data, stride, pw, ph, x0, y0, x1, y1, BDR * scale, C_BDR); + } + } + + // Attach buffer, damage the full surface, and commit. + s.wl.attach(Some(s.bufs[buf_slot].as_ref().unwrap()), 0, 0); + s.wl.damage_buffer(0, 0, pw, ph); + s.wl.commit(); + s.buf_busy[buf_slot] = true; + s.needs_repaint = false; + } +} + +impl Dispatch for St { + fn event( + _: &mut Self, + _: &wl_registry::WlRegistry, + _: wl_registry::Event, + _: &GlobalListContents, + _: &Connection, + _: &QueueHandle, + ) { + } +} + +impl Dispatch for St { + fn event( + state: &mut Self, + wl: &wl_output::WlOutput, + event: wl_output::Event, + _: &(), + _: &Connection, + _: &QueueHandle, + ) { + let entry = state.pending.iter_mut().find(|p| p.wl.id() == wl.id()); + let Some(p) = entry else { return }; + match event { + wl_output::Event::Name { name } => p.name = Some(name), + wl_output::Event::Geometry { x, y, .. } => { + p.ox = x; + p.oy = y; + } + wl_output::Event::Scale { factor } => p.scale = factor, + _ => {} + } + } +} + +impl Dispatch for St { + fn event( + state: &mut Self, + seat: &wl_seat::WlSeat, + event: wl_seat::Event, + _: &(), + _: &Connection, + qh: &QueueHandle, + ) { + if let wl_seat::Event::Capabilities { + capabilities: WEnum::Value(caps), + } = event + { + if caps.contains(wl_seat::Capability::Pointer) && state.pointer.is_none() { + state.pointer = Some(seat.get_pointer(qh, ())); + } + if caps.contains(wl_seat::Capability::Keyboard) && state.keyboard.is_none() { + state.keyboard = Some(seat.get_keyboard(qh, ())); + } + } + } +} + +impl Dispatch for St { + fn event( + state: &mut Self, + pointer: &wl_pointer::WlPointer, + event: wl_pointer::Event, + _: &(), + _: &Connection, + qh: &QueueHandle, + ) { + match event { + wl_pointer::Event::Enter { + serial, + surface, + surface_x, + surface_y, + } => { + let idx = state.surfs.iter().position(|s| s.wl.id() == surface.id()); + state.on_surf = idx; + if let Some(i) = idx { + let s = &state.surfs[i]; + state.cx = s.lx + surface_x as i32; + state.cy = s.ly + surface_y as i32; + } + if let Some(mgr) = &state.cursor_shape { + let device = mgr.get_pointer(pointer, qh, ()); + device.set_shape(serial, CursorShape::Crosshair); + } + state.update_hover(); + state.mark_all_repaint(); + let n = state.surfs.len(); + for idx in 0..n { + if state.surfs[idx].needs_repaint && !state.surfs[idx].frame_pending { + state.request_frame(idx); + } + } + } + wl_pointer::Event::Leave { .. } => { + state.on_surf = None; + } + wl_pointer::Event::Motion { + surface_x, + surface_y, + .. + } => { + if let Some(i) = state.on_surf { + let s = &state.surfs[i]; + state.cx = s.lx + surface_x as i32; + state.cy = s.ly + surface_y as i32; + } + state.update_hover(); + state.mark_all_repaint(); + let n = state.surfs.len(); + for idx in 0..n { + if state.surfs[idx].needs_repaint && !state.surfs[idx].frame_pending { + state.request_frame(idx); + } + } + } + wl_pointer::Event::Button { + button, + state: WEnum::Value(btn_state), + .. + } => { + if button != 0x110 { + return; + } // BTN_LEFT = 0x110 + match btn_state { + wl_pointer::ButtonState::Pressed => { + if state.restrict { + // handled on release + } else { + state.dragging = true; + state.anchor_x = state.cx; + state.anchor_y = state.cy; + } + } + wl_pointer::ButtonState::Released => { + if state.restrict { + if let Some(hi) = state.hover { + let b = &state.boxes[hi]; + state.result = Some(Geometry { + x: b.x as i64, + y: b.y as i64, + w: b.w as i64, + h: b.h as i64, + }); + state.running = false; + } + } else if state.dragging { + if let Some((x, y, w, h)) = state.curr_sel() { + if w > 0 && h > 0 { + state.result = Some(Geometry { + x: x as i64, + y: y as i64, + w: w as i64, + h: h as i64, + }); + state.running = false; + } + } + state.dragging = false; + } + } + _ => {} + } + } + _ => {} + } + } +} + +impl Dispatch for St { + fn event( + state: &mut Self, + _: &wl_keyboard::WlKeyboard, + event: wl_keyboard::Event, + _: &(), + _: &Connection, + _: &QueueHandle, + ) { + if let wl_keyboard::Event::Key { + key, + state: WEnum::Value(wl_keyboard::KeyState::Pressed), + .. + } = event + { + if key == 1 { + // ESC scancode + state.running = false; + } + } + } +} + +impl Dispatch for St { + fn event( + state: &mut Self, + layer: &ZwlrLayerSurfaceV1, + event: zwlr_layer_surface_v1::Event, + idx: &usize, + _: &Connection, + qh: &QueueHandle, + ) { + let idx = *idx; + if let zwlr_layer_surface_v1::Event::Configure { + serial, + width, + height, + } = event + { + layer.ack_configure(serial); + + let s = &mut state.surfs[idx]; + if s.configured { + return; + } // ignore reconfigures + + s.lw = width as i32; + s.lh = height as i32; + s.pw = s.lw * s.scale; + s.ph = s.lh * s.scale; + s.stride = s.pw as usize * 4; + let buf_size = s.stride * s.ph as usize; + s.buf_size = buf_size; + + let file = match alloc_shm(2 * buf_size) { + Ok(f) => f, + Err(e) => { + eprintln!("blast: select: alloc_shm: {e}"); + return; + } + }; + let mmap = match unsafe { MmapOptions::new().len(2 * buf_size).map_mut(&file) } { + Ok(m) => m, + Err(e) => { + eprintln!("blast: select: mmap: {e}"); + return; + } + }; + let shm = state.shm.as_ref().unwrap(); + let pool = shm.create_pool(file.as_fd(), (2 * buf_size) as i32, qh, ()); + // User-data encodes both surface index and buffer slot: idx*2+slot. + let buf0 = pool.create_buffer( + 0, + s.pw, + s.ph, + s.stride as i32, + wl_shm::Format::Argb8888, + qh, + idx * 2, + ); + let buf1 = pool.create_buffer( + buf_size as i32, + s.pw, + s.ph, + s.stride as i32, + wl_shm::Format::Argb8888, + qh, + idx * 2 + 1, + ); + + let s = &mut state.surfs[idx]; + s._file = Some(file); + s._pool = Some(pool); + s.bufs = [Some(buf0), Some(buf1)]; + s.mmap = Some(mmap); + s.configured = true; + s.needs_repaint = true; + + state.configured += 1; + } + } +} + +impl Dispatch for St { + fn event( + state: &mut Self, + _: &wl_buffer::WlBuffer, + event: wl_buffer::Event, + slot: &usize, + _: &Connection, + _: &QueueHandle, + ) { + if let wl_buffer::Event::Release = event { + let surf_idx = slot / 2; + let buf_slot = slot % 2; + state.surfs[surf_idx].buf_busy[buf_slot] = false; + } + } +} + +impl Dispatch for St { + fn event( + state: &mut Self, + _: &wl_callback::WlCallback, + _: wl_callback::Event, + idx: &usize, + _: &Connection, + _: &QueueHandle, + ) { + let idx = *idx; + state.surfs[idx].frame_pending = false; + + if state.surfs[idx].needs_repaint { + state.render_surf(idx); + // If still needs another frame (e.g. buf was busy), re-request. + if state.surfs[idx].needs_repaint { + state.request_frame(idx); + } + } + } +} + +impl Dispatch for St { + fn event( + _: &mut Self, + _: &wl_shm::WlShm, + _: wl_shm::Event, + _: &(), + _: &Connection, + _: &QueueHandle, + ) { + } +} + +delegate_noop!(St: ignore wl_compositor::WlCompositor); +delegate_noop!(St: ignore wl_surface::WlSurface); +delegate_noop!(St: ignore wl_shm_pool::WlShmPool); +delegate_noop!(St: ignore ZwlrLayerShellV1); +delegate_noop!(St: ignore wl_region::WlRegion); +delegate_noop!(St: ignore WpCursorShapeManagerV1); +delegate_noop!(St: ignore WpCursorShapeDeviceV1); + +pub fn select_region( + boxes: Vec, + restrict: bool, + hint_rgba: Option, +) -> Result { + let conn = + Connection::connect_to_env().map_err(|e| BlastError::Selection(format!("connect: {e}")))?; + + let (globals, queue) = registry_queue_init::(&conn) + .map_err(|e| BlastError::Selection(format!("registry: {e}")))?; + let qh = queue.handle(); + + let compositor = globals + .bind::(&qh, 4..=6, ()) + .map_err(|e| BlastError::Selection(format!("compositor: {e}")))?; + let shm = globals + .bind::(&qh, 1..=1, ()) + .map_err(|e| BlastError::Selection(format!("shm: {e}")))?; + let shell = globals + .bind::(&qh, 1..=4, ()) + .map_err(|e| BlastError::Selection(format!("layer_shell: {e}")))?; + let seat = globals + .bind::(&qh, 1..=8, ()) + .map_err(|e| BlastError::Selection(format!("seat: {e}")))?; + + let pending: Vec = globals + .contents() + .clone_list() + .iter() + .filter(|g| g.interface == "wl_output") + .map(|g| PendingOut { + wl: globals.registry().bind::( + g.name, + g.version.min(4), + &qh, + (), + ), + name: None, + ox: 0, + oy: 0, + scale: 1, + }) + .collect(); + + let cursor_shape = globals + .bind::(&qh, 1..=1, ()) + .ok(); + + let (c_hint, c_hov) = match hint_rgba { + Some(rgba) => (rgba_to_bgra_premult(rgba), derive_hover(rgba)), + None => (DEFAULT_HINT, DEFAULT_HOV), + }; + + let mut st = St { + conn, + qh, + queue: Some(queue), + compositor: Some(compositor), + shm: Some(shm), + shell: Some(shell), + seat: Some(seat), + pointer: None, + keyboard: None, + cursor_shape, + pending, + surfs: Vec::new(), + configured: 0, + on_surf: None, + cx: 0, + cy: 0, + anchor_x: 0, + anchor_y: 0, + dragging: false, + boxes, + hover: None, + restrict, + c_hint, + c_hov, + running: true, + result: None, + }; + + st.roundtrip()?; + st.roundtrip()?; // second pass to get pointer/keyboard from capabilities + + st.create_surfaces()?; + + let expected = st.surfs.len(); + while st.configured < expected { + st.dispatch_once()?; + } + + for idx in 0..st.surfs.len() { + st.render_surf(idx); + st.request_frame(idx); + } + st.flush(); + + while st.running { + st.dispatch_once()?; + } + + for s in &st.surfs { + s.layer.destroy(); + s.wl.destroy(); + } + st.roundtrip()?; + // Give the compositor one frame period to repaint without the overlay. + std::thread::sleep(std::time::Duration::from_millis(32)); + st.roundtrip()?; + + st.result.ok_or(BlastError::SelectionCancelled) +} diff --git a/src/shhh.rs b/src/shhh.rs new file mode 100644 index 0000000..37b214b --- /dev/null +++ b/src/shhh.rs @@ -0,0 +1,205 @@ +use image::{imageops, DynamicImage, GenericImageView, ImageBuffer, Rgba}; +use png::{BitDepth, ColorType, Encoder}; + +use crate::error::{BlastError, Result}; + +#[derive(Debug, Clone)] +pub struct ShadowOptions { + pub corner_radius: u32, + pub offset: (i32, i32), + pub shadow_alpha: u8, + pub spread: u32, + pub blur_radius: u32, +} + +impl Default for ShadowOptions { + fn default() -> Self { + Self { + corner_radius: 8, + offset: (-20, -20), + shadow_alpha: 150, + spread: 26, + blur_radius: 5, + } + } +} + +/// Process a DynamicImage: round corners then add drop shadow. +pub fn apply_effects(img: &DynamicImage, opts: &ShadowOptions) -> Result { + let rounded = round_corners(img, opts.corner_radius); + add_rounded_drop_shadow( + &rounded, + opts.offset.0, + opts.offset.1, + opts.blur_radius, + opts.spread, + opts.shadow_alpha, + ) +} + +pub fn encode_png(img: &DynamicImage) -> Result> { + let rgba = img.to_rgba8(); + let (width, height) = rgba.dimensions(); + let mut out: Vec = Vec::new(); + { + let mut encoder = Encoder::new(&mut out, width, height); + encoder.set_color(ColorType::Rgba); + encoder.set_depth(BitDepth::Eight); + let mut writer = encoder + .write_header() + .map_err(|e| BlastError::Image(e.to_string()))?; + writer + .write_image_data(rgba.as_raw()) + .map_err(|e| BlastError::Image(e.to_string()))?; + } + Ok(out) +} + +pub fn apply_and_encode(img: &DynamicImage, opts: &ShadowOptions) -> Result> { + let result = apply_effects(img, opts)?; + encode_png(&result) +} + +/// Process raw PNG bytes +// pub fn process_png_bytes(input: &[u8], opts: &ShadowOptions) -> Result> { +// use image::ImageReader; +// let reader = ImageReader::new(std::io::Cursor::new(input)) +// .with_guessed_format() +// .map_err(|e| BlastError::Image(format!("format detection: {e}")))?; +// let img = reader +// .decode() +// .map_err(|e| BlastError::Image(e.to_string()))?; +// let result = apply_effects(&img, opts)?; +// encode_png(&result) +// } + +// image processing, logic from shhh + +pub fn round_corners(img: &DynamicImage, radius: u32) -> DynamicImage { + let (width, height) = img.dimensions(); + let mut rounded: ImageBuffer, Vec> = ImageBuffer::new(width, height); + let r = radius as f32; + + for (x, y, pixel) in img.to_rgba8().enumerate_pixels() { + let corner: Option<(f32, f32)> = if x < radius && y < radius { + Some((r - x as f32, r - y as f32)) + } else if x >= width - radius && y < radius { + Some((x as f32 - (width as f32 - r - 1.0), r - y as f32)) + } else if x < radius && y >= height - radius { + Some((r - x as f32, y as f32 - (height as f32 - r - 1.0))) + } else if x >= width - radius && y >= height - radius { + Some(( + x as f32 - (width as f32 - r - 1.0), + y as f32 - (height as f32 - r - 1.0), + )) + } else { + None + }; + + match corner { + None => rounded.put_pixel(x, y, *pixel), + Some((dx, dy)) => { + let dist = (dx * dx + dy * dy).sqrt(); + if dist <= r { + rounded.put_pixel(x, y, *pixel); + } else { + let alpha = ((r + 1.0 - dist).max(0.0) * 255.0) as u8; + rounded.put_pixel( + x, + y, + Rgba([pixel[0], pixel[1], pixel[2], alpha.min(pixel[3])]), + ); + } + } + } + } + + DynamicImage::ImageRgba8(rounded) +} + +pub fn add_rounded_drop_shadow( + img: &DynamicImage, + offset_x: i32, + offset_y: i32, + blur_radius: u32, + spread: u32, + shadow_alpha: u8, +) -> Result { + let (width, height) = img.dimensions(); + + let padding = spread + blur_radius * 2; + let total_w = (width as i32 + offset_x.abs() + padding as i32 * 2) as u32; + let total_h = (height as i32 + offset_y.abs() + padding as i32 * 2) as u32; + + let mut output: ImageBuffer, Vec> = ImageBuffer::new(total_w, total_h); + + let shadow = create_shadow(img, blur_radius, spread, shadow_alpha); + + let shadow_x: i64 = if offset_x >= 0 { + padding as i64 + } else { + padding as i64 + offset_x as i64 + }; + let shadow_y: i64 = if offset_y >= 0 { + padding as i64 + } else { + padding as i64 + offset_y as i64 + }; + imageops::overlay(&mut output, &shadow, shadow_x, shadow_y); + + let image_x: i64 = if offset_x >= 0 { + padding as i64 + offset_x as i64 + } else { + padding as i64 + }; + let image_y: i64 = if offset_y >= 0 { + padding as i64 + offset_y as i64 + } else { + padding as i64 + }; + imageops::overlay(&mut output, img, image_x, image_y); + + Ok(DynamicImage::ImageRgba8(output)) +} + +fn create_shadow( + img: &DynamicImage, + blur_radius: u32, + spread: u32, + shadow_alpha: u8, +) -> DynamicImage { + let (width, height) = img.dimensions(); + let padding = spread + blur_radius * 2; + let new_w = width + padding * 2; + let new_h = height + padding * 2; + + let mut shadow: ImageBuffer, Vec> = ImageBuffer::new(new_w, new_h); + imageops::overlay(&mut shadow, &img.to_rgba8(), padding as i64, padding as i64); + + for pixel in shadow.pixels_mut() { + let alpha = pixel[3] as f32 / 255.0 * shadow_alpha as f32; + *pixel = Rgba([0, 0, 0, alpha as u8]); + } + + let adjusted_blur = blur_radius + (spread as f32 / 2.0) as u32; + let blurred = imageops::blur(&shadow, adjusted_blur as f32); + + let mut cleaned: ImageBuffer, Vec> = ImageBuffer::new(new_w, new_h); + for (x, y, pixel) in blurred.enumerate_pixels() { + if pixel[3] > 0 { + let factor = (pixel[3] as f32 / 255.0).powf(0.5); + cleaned.put_pixel( + x, + y, + Rgba([ + (pixel[0] as f32 * factor) as u8, + (pixel[1] as f32 * factor) as u8, + (pixel[2] as f32 * factor) as u8, + (pixel[3] as f32 * factor) as u8, + ]), + ); + } + } + + DynamicImage::ImageRgba8(cleaned) +} diff --git a/src/wayland_windows.rs b/src/wayland_windows.rs new file mode 100644 index 0000000..4d42757 --- /dev/null +++ b/src/wayland_windows.rs @@ -0,0 +1,363 @@ +//! Window geometry enumeration. +//! +//! Uses ext_foreign_toplevel_list_v1 + the custom +//! xx_foreign_toplevel_geometry_tracking_v1 protocol to enumerate visible +//! windows and their logical screen positions without Hyprland IPC. +//! +//! Returns None if either protocol is not advertised by the compositor + +use std::collections::HashMap; + +use wayland_client::{ + delegate_noop, event_created_child, + globals::{registry_queue_init, GlobalListContents}, + protocol::{wl_output, wl_registry}, + Connection, Dispatch, EventQueue, Proxy, QueueHandle, WEnum, +}; +use wayland_protocols::ext::foreign_toplevel_list::v1::client::{ + ext_foreign_toplevel_handle_v1::{self, ExtForeignToplevelHandleV1}, + ext_foreign_toplevel_list_v1::{self, ExtForeignToplevelListV1}, +}; +use wayland_protocols::xdg::xdg_output::zv1::client::{ + zxdg_output_manager_v1::ZxdgOutputManagerV1, + zxdg_output_v1::{self, ZxdgOutputV1}, +}; + +use crate::select::HintBox; + +// Protocol bindings generated from xx_foreign_toplevel_geometry_tracking_v1.xml (yet to be named, will not use ext namespace most +// likely but we will use as a POC) + +mod protocol { + #![allow( + dead_code, + non_camel_case_types, + unused_unsafe, + unused_variables, + non_upper_case_globals, + non_snake_case, + unused_imports, + clippy::all + )] + pub mod client { + use wayland_client; + use wayland_client::protocol::*; + use wayland_protocols::ext::foreign_toplevel_list::v1::client::*; + + pub mod __interfaces { + use wayland_client::protocol::__interfaces::*; + use wayland_protocols::ext::foreign_toplevel_list::v1::client::__interfaces::*; + wayland_scanner::generate_interfaces!("protocols/xx-foreign-toplevel-geometry-v1.xml"); + } + use self::__interfaces::*; + + wayland_scanner::generate_client_code!("protocols/xx-foreign-toplevel-geometry-v1.xml"); + } +} + +use protocol::client::{ + xx_foreign_toplevel_geometry_tracker_v1::{self, XxForeignToplevelGeometryTrackerV1}, + xx_foreign_toplevel_geometry_tracking_manager_v1::XxForeignToplevelGeometryTrackingManagerV1, +}; + +struct OutputInfo { + global_name: u32, + logical_x: i32, + logical_y: i32, + hw_width: i32, + hw_height: i32, + logical_width: i32, + logical_height: i32, +} + +impl OutputInfo { + /// Convert hardware space coords to global logical. + fn hw_to_logical(&self, x: i32, y: i32, w: i32, h: i32) -> (i32, i32, i32, i32) { + let (sx, sy) = if self.logical_width > 0 && self.logical_height > 0 { + ( + self.hw_width as f64 / self.logical_width as f64, + self.hw_height as f64 / self.logical_height as f64, + ) + } else { + (1.0, 1.0) + }; + let lx = self.logical_x + (x as f64 / sx).round() as i32; + let ly = self.logical_y + (y as f64 / sy).round() as i32; + let lw = (w as f64 / sx).round() as i32; + let lh = (h as f64 / sy).round() as i32; + (lx, ly, lw, lh) + } +} + +struct ToplevelInfo { + handle: ExtForeignToplevelHandleV1, + app_id: String, + geometry: Option<(u32, i32, i32, i32, i32)>, +} + +struct St { + queue: Option>, + outputs: Vec, + out_idx: HashMap, + toplevels: Vec, + _trackers: Vec, +} + +impl St { + fn roundtrip(&mut self) -> Option<()> { + let mut q = self.queue.take().unwrap(); + let ok = q.roundtrip(self).is_ok(); + self.queue = Some(q); + ok.then_some(()) + } +} + +impl Dispatch for St { + fn event( + _: &mut Self, + _: &wl_registry::WlRegistry, + _: wl_registry::Event, + _: &GlobalListContents, + _: &Connection, + _: &QueueHandle, + ) { + } +} + +impl Dispatch for St { + fn event( + state: &mut Self, + _: &wl_output::WlOutput, + event: wl_output::Event, + gname: &u32, + _: &Connection, + _: &QueueHandle, + ) { + if let wl_output::Event::Mode { + flags: WEnum::Value(flags), + width, + height, + .. + } = event + { + if flags.contains(wl_output::Mode::Current) { + let idx = match state.out_idx.get(gname) { + Some(&i) => i, + None => { + let i = state.outputs.len(); + state.outputs.push(OutputInfo { + global_name: *gname, + logical_x: 0, + logical_y: 0, + hw_width: 0, + hw_height: 0, + logical_width: 0, + logical_height: 0, + }); + state.out_idx.insert(*gname, i); + i + } + }; + state.outputs[idx].hw_width = width; + state.outputs[idx].hw_height = height; + } + } + } +} + +impl Dispatch for St { + fn event( + state: &mut Self, + _: &ZxdgOutputV1, + event: zxdg_output_v1::Event, + gname: &u32, + _: &Connection, + _: &QueueHandle, + ) { + let Some(&idx) = state.out_idx.get(gname) else { + return; + }; + match event { + zxdg_output_v1::Event::LogicalPosition { x, y } => { + state.outputs[idx].logical_x = x; + state.outputs[idx].logical_y = y; + } + zxdg_output_v1::Event::LogicalSize { width, height } => { + state.outputs[idx].logical_width = width; + state.outputs[idx].logical_height = height; + } + _ => {} + } + } +} + +/// The list sends toplevel events, each new handle needs () user data. +impl Dispatch for St { + fn event( + state: &mut Self, + _: &ExtForeignToplevelListV1, + event: ext_foreign_toplevel_list_v1::Event, + _: &(), + _: &Connection, + _: &QueueHandle, + ) { + if let ext_foreign_toplevel_list_v1::Event::Toplevel { toplevel } = event { + state.toplevels.push(ToplevelInfo { + handle: toplevel, + app_id: String::new(), + geometry: None, + }); + } + } + // opcode 0 = toplevel event (server creates a new ExtForeignToplevelHandleV1) + event_created_child!(St, ExtForeignToplevelListV1, [ + 0 => (ExtForeignToplevelHandleV1, ()) + ]); +} + +impl Dispatch for St { + fn event( + state: &mut Self, + handle: &ExtForeignToplevelHandleV1, + event: ext_foreign_toplevel_handle_v1::Event, + _: &(), + _: &Connection, + _: &QueueHandle, + ) { + let id = handle.id(); + let Some(tl) = state.toplevels.iter_mut().find(|t| t.handle.id() == id) else { + return; + }; + if let ext_foreign_toplevel_handle_v1::Event::AppId { app_id } = event { + tl.app_id = app_id; + } + } +} + +/// User data is an index into state.toplevels. +impl Dispatch for St { + fn event( + state: &mut Self, + _: &XxForeignToplevelGeometryTrackerV1, + event: xx_foreign_toplevel_geometry_tracker_v1::Event, + idx: &usize, + _: &Connection, + _: &QueueHandle, + ) { + if let xx_foreign_toplevel_geometry_tracker_v1::Event::Geometry { + output, + x, + y, + width, + height, + } = event + { + let Some(tl) = state.toplevels.get_mut(*idx) else { + return; + }; + let area = width as i64 * height as i64; + let prev = tl + .geometry + .map(|(_, _, _, w, h)| w as i64 * h as i64) + .unwrap_or(-1); + if area > prev { + tl.geometry = Some((output, x, y, width as i32, height as i32)); + } + } + } +} + +delegate_noop!(St: ignore ZxdgOutputManagerV1); +delegate_noop!(St: ignore XxForeignToplevelGeometryTrackingManagerV1); + +// Public entry point +// +/// Try to enumerate visible windows via Wayland protocols. +/// +/// Returns None if ext_foreign_toplevel_list_v1 or +/// xx_foreign_toplevel_geometry_tracking_v1 are not advertised, so the +/// caller can fall back to Hyprland IPC. +pub fn visible_window_hints() -> Option> { + let conn = Connection::connect_to_env().ok()?; + let (globals, queue) = registry_queue_init::(&conn).ok()?; + let qh = queue.handle(); + + // Both protocols are required, return None if either is missing. + let tl_list = globals + .bind::(&qh, 1..=1, ()) + .ok()?; + let geo_mgr = globals + .bind::(&qh, 1..=1, ()) + .ok()?; + let xdg_mgr = globals + .bind::(&qh, 2..=3, ()) + .ok()?; + + // Bind wl_outputs with their registry global names as user data. + let out_globals: Vec<(u32, wl_output::WlOutput)> = globals + .contents() + .clone_list() + .iter() + .filter(|g| g.interface == "wl_output") + .map(|g| { + let o = globals.registry().bind::( + g.name, + g.version.min(4), + &qh, + g.name, + ); + (g.name, o) + }) + .collect(); + + // Create one zxdg_output per wl_output (kept alive for the roundtrip). + let _xdg_outputs: Vec = out_globals + .iter() + .map(|(gname, wl_out)| xdg_mgr.get_xdg_output(wl_out, &qh, *gname)) + .collect(); + + let mut st = St { + queue: Some(queue), + outputs: Vec::new(), + out_idx: HashMap::new(), + toplevels: Vec::new(), + _trackers: Vec::new(), + }; + + st.roundtrip()?; + st.roundtrip()?; + + for idx in 0..st.toplevels.len() { + let handle = st.toplevels[idx].handle.clone(); + let tracker = geo_mgr.get_geometry_tracker(&handle, &qh, idx); + st._trackers.push(tracker); + } + + st.roundtrip()?; + + let hints = st + .toplevels + .iter() + .filter_map(|tl| { + let (out_name, x, y, w, h) = tl.geometry?; + let out = st.outputs.iter().find(|o| o.global_name == out_name)?; + let (lx, ly, lw, lh) = out.hw_to_logical(x, y, w, h); + if lw <= 0 || lh <= 0 { + return None; + } + Some(HintBox { + x: lx, + y: ly, + w: lw, + h: lh, + label: tl.app_id.clone(), + }) + }) + .collect(); + + drop(_xdg_outputs); + drop(out_globals); + drop(tl_list); + + Some(hints) +}