From 775c222448d7eeee12b9d66e8f5e38ebaa0e54dd Mon Sep 17 00:00:00 2001 From: inorishio Date: Wed, 27 May 2026 14:33:18 +0200 Subject: [PATCH] winZoom it works similar to hyprland zoom but with typecursor follow --- Cargo.lock | 1696 ++++++++++++++++++++++++++++------------------ Cargo.toml | 19 +- _ul | 1 + src/caret.rs | 121 ++++ src/config.rs | 45 -- src/effects.rs | 288 -------- src/hook.rs | 100 +++ src/magnifier.rs | 76 +++ src/main.rs | 358 +++++----- src/state.rs | 192 ++++++ src/tray.rs | 70 ++ 11 files changed, 1760 insertions(+), 1206 deletions(-) create mode 100644 _ul create mode 100644 src/caret.rs delete mode 100644 src/config.rs delete mode 100644 src/effects.rs create mode 100644 src/hook.rs create mode 100644 src/magnifier.rs create mode 100644 src/state.rs create mode 100644 src/tray.rs diff --git a/Cargo.lock b/Cargo.lock index c5e7fe8..df3afc3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9,65 +9,26 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" [[package]] -name = "aligned" -version = "0.4.3" +name = "atk" +version = "0.18.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee4508988c62edf04abd8d92897fca0c2995d907ce1dfeaf369dac3716a40685" +checksum = "241b621213072e993be4f6f3a9e4b45f65b7e6faad43001be957184b7bb1824b" dependencies = [ - "as-slice", + "atk-sys", + "glib", + "libc", ] [[package]] -name = "aligned-vec" -version = "0.6.4" +name = "atk-sys" +version = "0.18.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc890384c8602f339876ded803c97ad529f3842aba97f6392b3dba0dd171769b" +checksum = "c5e48b684b0ca77d2bbadeef17424c2ea3c897d44d566a1617e7e8f30614d086" dependencies = [ - "equator", -] - -[[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 = "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", -] - -[[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-slice" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "516b6b4f0e40d50dcda9365d53964ec74560ad4284da2e7fc97122cd83174516" -dependencies = [ - "stable_deref_trait", + "glib-sys", + "gobject-sys", + "libc", + "system-deps", ] [[package]] @@ -76,55 +37,6 @@ 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", - "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_field" -version = "0.10.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e4b40c7323adcfc0a41c4b88143ed58346ff65a288fc144329c5c45e05d70c6" - [[package]] name = "bitflags" version = "1.3.2" @@ -136,50 +48,58 @@ name = "bitflags" version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" - -[[package]] -name = "bitstream-io" -version = "4.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7eff00be299a18769011411c9def0d827e8f2d7bf0c3dbf53633147a8867fd1f" dependencies = [ - "no_std_io2", + "serde_core", ] [[package]] -name = "built" -version = "0.8.0" +name = "block" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4ad8f11f288f48ca24471bbd51ac257aaeaaa07adae295591266b792902ae64" +checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" [[package]] -name = "bumpalo" -version = "3.20.2" +name = "block2" +version = "0.5.1" 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" - -[[package]] -name = "byteorder-lite" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" - -[[package]] -name = "cc" -version = "1.2.61" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d16d90359e986641506914ba71350897565610e87ce0ad9e6f28569db3dd5c6d" +checksum = "2c132eebf10f5cad5289222520a4a058514204aed6d791f1cf4fe8088b82d15f" dependencies = [ - "find-msvc-tools", - "jobserver", + "objc2", +] + +[[package]] +name = "cairo-rs" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ca26ef0159422fb77631dc9d17b102f253b876fe1586b03b803e63a309b4ee2" +dependencies = [ + "bitflags 2.11.1", + "cairo-sys-rs", + "glib", "libc", - "shlex", + "once_cell", + "thiserror", +] + +[[package]] +name = "cairo-sys-rs" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "685c9fa8e590b8b3d678873528d83411db17242a73fccaed827770ea0fedda51" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + +[[package]] +name = "cfg-expr" +version = "0.15.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02" +dependencies = [ + "smallvec", + "target-lexicon", ] [[package]] @@ -189,10 +109,73 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] -name = "color_quant" -version = "1.1.0" +name = "cocoa" +version = "0.26.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" +checksum = "ad36507aeb7e16159dfe68db81ccc27571c3ccd4b76fb2fb72fc59e7a4b1b64c" +dependencies = [ + "bitflags 2.11.1", + "block", + "cocoa-foundation", + "core-foundation", + "core-graphics", + "foreign-types", + "libc", + "objc", +] + +[[package]] +name = "cocoa-foundation" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81411967c50ee9a1fc11365f8c585f863a22a9697c89239c452292c40ba79b0d" +dependencies = [ + "bitflags 2.11.1", + "block", + "core-foundation", + "core-graphics-types", + "objc", +] + +[[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.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa95a34622365fa5bbf40b20b75dba8dfa8c94c734aea8ac9a5ca38af14316f1" +dependencies = [ + "bitflags 2.11.1", + "core-foundation", + "core-graphics-types", + "foreign-types", + "libc", +] + +[[package]] +name = "core-graphics-types" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" +dependencies = [ + "bitflags 2.11.1", + "core-foundation", + "libc", +] [[package]] name = "crc32fast" @@ -204,20 +187,10 @@ dependencies = [ ] [[package]] -name = "crossbeam-deque" -version = "0.8.6" +name = "crossbeam-channel" +version = "0.5.15" 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" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" dependencies = [ "crossbeam-utils", ] @@ -229,57 +202,37 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" [[package]] -name = "crunchy" -version = "0.2.4" +name = "dirs" +version = "5.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" - -[[package]] -name = "either" -version = "1.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" - -[[package]] -name = "equator" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4711b213838dfee0117e3be6ac926007d7f433d7bbe33595975d4190cb07e6fc" +checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" dependencies = [ - "equator-macro", + "dirs-sys", ] [[package]] -name = "equator-macro" -version = "0.4.2" +name = "dirs-sys" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44f23cf4b44bfce11a86ace86f8a73ffdec849c9fd00a386a53d278bd9e81fb3" +checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" dependencies = [ - "proc-macro2", - "quote", - "syn", + "libc", + "option-ext", + "redox_users", + "windows-sys 0.48.0", ] [[package]] -name = "exr" -version = "1.74.0" +name = "dpi" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4300e043a56aa2cb633c01af81ca8f699a321879a7854d3896a0ba89056363be" -dependencies = [ - "bit_field", - "half", - "lebe", - "miniz_oxide", - "rayon-core", - "smallvec", - "zune-inflate", -] +checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76" [[package]] -name = "fax" -version = "0.2.7" +name = "equivalent" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "caf1079563223d5d59d83c85886a56e586cfd5c1a26292e971a0fa266531ac5a" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "fdeflate" @@ -291,10 +244,14 @@ dependencies = [ ] [[package]] -name = "find-msvc-tools" -version = "0.1.9" +name = "field-offset" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" +checksum = "38e2275cc4e4fc009b0669731a1e5ab7ebf11f469eaede2bab9309a5b4d6057f" +dependencies = [ + "memoffset", + "rustc_version", +] [[package]] name = "flate2" @@ -307,130 +264,375 @@ dependencies = [ ] [[package]] -name = "getrandom" -version = "0.3.4" +name = "foreign-types" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" dependencies = [ - "cfg-if", - "libc", - "r-efi", - "wasip2", + "foreign-types-macros", + "foreign-types-shared", ] [[package]] -name = "gif" -version = "0.14.2" +name = "foreign-types-macros" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee8cfcc411d9adbbaba82fb72661cc1bcca13e8bba98b364e62b2dba8f960159" -dependencies = [ - "color_quant", - "weezl", -] - -[[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 = "image" -version = "0.25.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104" -dependencies = [ - "bytemuck", - "byteorder-lite", - "color_quant", - "exr", - "gif", - "image-webp", - "moxcms", - "num-traits", - "png 0.18.1", - "qoi", - "ravif", - "rayon", - "rgb", - "tiff", - "zune-core", - "zune-jpeg", -] - -[[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.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40fac9d56ed6437b198fddba683305e8e2d651aa42647f00f5ae542e7f5c94a2" - -[[package]] -name = "interpolate_name" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60" +checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] -name = "itertools" -version = "0.14.0" +name = "foreign-types-shared" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" dependencies = [ - "either", + "futures-core", ] [[package]] -name = "itoa" -version = "1.0.18" +name = "futures-core" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-executor" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[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-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-macro", + "futures-task", + "pin-project-lite", + "slab", +] + +[[package]] +name = "gdk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9f245958c627ac99d8e529166f9823fb3b838d1d41fd2b297af3075093c2691" +dependencies = [ + "cairo-rs", + "gdk-pixbuf", + "gdk-sys", + "gio", + "glib", + "libc", + "pango", +] + +[[package]] +name = "gdk-pixbuf" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50e1f5f1b0bfb830d6ccc8066d18db35c487b1b2b1e8589b5dfe9f07e8defaec" +dependencies = [ + "gdk-pixbuf-sys", + "gio", + "glib", + "libc", + "once_cell", +] + +[[package]] +name = "gdk-pixbuf-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9839ea644ed9c97a34d129ad56d38a25e6756f99f3a88e15cd39c20629caf7" +dependencies = [ + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gdk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c2d13f38594ac1e66619e188c6d5a1adb98d11b2fcf7894fc416ad76aa2f3f7" +dependencies = [ + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "pkg-config", + "system-deps", +] + +[[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 = "gio" +version = "0.18.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4fc8f532f87b79cbc51a79748f16a6828fb784be93145a322fa14d06d354c73" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "gio-sys", + "glib", + "libc", + "once_cell", + "pin-project-lite", + "smallvec", + "thiserror", +] + +[[package]] +name = "gio-sys" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37566df850baf5e4cb0dfb78af2e4b9898d817ed9263d1090a2df958c64737d2" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", + "winapi", +] + +[[package]] +name = "glib" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "233daaf6e83ae6a12a52055f568f9d7cf4671dabb78ff9560ab6da230ce00ee5" +dependencies = [ + "bitflags 2.11.1", + "futures-channel", + "futures-core", + "futures-executor", + "futures-task", + "futures-util", + "gio-sys", + "glib-macros", + "glib-sys", + "gobject-sys", + "libc", + "memchr", + "once_cell", + "smallvec", + "thiserror", +] + +[[package]] +name = "glib-macros" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bb0228f477c0900c880fd78c8759b95c7636dbd7842707f49e132378aa2acdc" +dependencies = [ + "heck 0.4.1", + "proc-macro-crate 2.0.2", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "glib-sys" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "063ce2eb6a8d0ea93d2bf8ba1957e78dbab6be1c2220dd3daca57d5a9d869898" +dependencies = [ + "libc", + "system-deps", +] + +[[package]] +name = "gobject-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0850127b514d1c4a4654ead6dedadb18198999985908e6ffe4436f53c785ce44" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gtk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd56fb197bfc42bd5d2751f4f017d44ff59fbb58140c6b49f9b3b2bdab08506a" +dependencies = [ + "atk", + "cairo-rs", + "field-offset", + "futures-channel", + "gdk", + "gdk-pixbuf", + "gio", + "glib", + "gtk-sys", + "gtk3-macros", + "libc", + "pango", + "pkg-config", +] + +[[package]] +name = "gtk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f29a1c21c59553eb7dd40e918be54dccd60c52b049b75119d5d96ce6b624414" +dependencies = [ + "atk-sys", + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gdk-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "system-deps", +] + +[[package]] +name = "gtk3-macros" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ff3c5b21f14f0736fed6dcfc0bfb4225ebf5725f3c0209edeec181e4d73e9d" +dependencies = [ + "proc-macro-crate 1.3.1", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown", +] [[package]] name = "iwaku" version = "0.1.0" dependencies = [ - "anyhow", - "image", - "serde", - "serde_json", - "tiny-skia", + "tray-icon", + "windows", ] [[package]] -name = "jobserver" -version = "0.1.34" +name = "keyboard-types" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +checksum = "b750dcadc39a09dbadd74e118f6dd6598df77fa01df0cfcdc52c28dece74528a" dependencies = [ - "getrandom", - "libc", + "bitflags 2.11.1", + "serde", + "unicode-segmentation", ] [[package]] -name = "lebe" -version = "0.5.3" +name = "libappindicator" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a79a3332a6609480d7d0c9eab957bca6b455b91bb84e66d19f5ff66294b85b8" +checksum = "03589b9607c868cc7ae54c0b2a22c8dc03dd41692d48f2d7df73615c6a95dc0a" +dependencies = [ + "glib", + "gtk", + "gtk-sys", + "libappindicator-sys", + "log", +] + +[[package]] +name = "libappindicator-sys" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e9ec52138abedcc58dc17a7c6c0c00a2bdb4f3427c7f63fa97fd0d859155caf" +dependencies = [ + "gtk-sys", + "libloading", + "once_cell", +] [[package]] name = "libc" @@ -439,13 +641,41 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" [[package]] -name = "libfuzzer-sys" -version = "0.4.12" +name = "libloading" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f12a681b7dd8ce12bff52488013ba614b869148d54dd79836ab85aafdd53f08d" +checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f" dependencies = [ - "arbitrary", - "cc", + "cfg-if", + "winapi", +] + +[[package]] +name = "libredox" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e02f3bb43d335493c96bf3fd3a321600bf6bd07ed34bc64118e9293bdffea46c" +dependencies = [ + "libc", +] + +[[package]] +name = "libxdo" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00333b8756a3d28e78def82067a377de7fa61b24909000aeaa2b446a948d14db" +dependencies = [ + "libxdo-sys", +] + +[[package]] +name = "libxdo-sys" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db23b9e7e2b7831bbd8aac0bbeeeb7b68cbebc162b227e7052e8e55829a09212" +dependencies = [ + "libc", + "x11", ] [[package]] @@ -455,22 +685,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" [[package]] -name = "loop9" -version = "0.1.5" +name = "malloc_buf" +version = "0.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fae87c125b03c1d2c0150c90365d7d6bcc53fb73a9acaef207d2d065860f062" +checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" dependencies = [ - "imgref", -] - -[[package]] -name = "maybe-rayon" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ea1f30cedd69f0a2954655f7188c6a834246d2bcf1e315e2ac40c4b24dc9519" -dependencies = [ - "cfg-if", - "rayon", + "libc", ] [[package]] @@ -479,6 +699,15 @@ version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + [[package]] name = "miniz_oxide" version = "0.8.9" @@ -490,93 +719,130 @@ dependencies = [ ] [[package]] -name = "moxcms" -version = "0.8.1" +name = "muda" +version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb85c154ba489f01b25c0d36ae69a87e4a1c73a72631fc6c0eb6dde34a73e44b" +checksum = "ba8ac4080fb1e097c2c22acae467e46e4da72d941f02e82b67a87a2a89fa38b1" dependencies = [ - "num-traits", - "pxfm", + "cocoa", + "crossbeam-channel", + "dpi", + "gtk", + "keyboard-types", + "libxdo", + "objc", + "once_cell", + "png", + "thiserror", + "windows-sys 0.59.0", ] [[package]] -name = "new_debug_unreachable" -version = "1.0.6" +name = "objc" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" - -[[package]] -name = "no_std_io2" -version = "0.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b51ed7824b6e07d354605f4abb3d9d300350701299da96642ee084f5ce631550" +checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" dependencies = [ - "memchr", + "malloc_buf", ] [[package]] -name = "nom" -version = "8.0.0" +name = "objc-sys" +version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405" +checksum = "cdb91bdd390c7ce1a8607f35f3ca7151b65afc0ff5ff3b34fa350f7d7c7e4310" + +[[package]] +name = "objc2" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46a785d4eeff09c14c487497c162e92766fbb3e4059a71840cecc03d9a50b804" dependencies = [ - "memchr", + "objc-sys", + "objc2-encode", ] [[package]] -name = "noop_proc_macro" -version = "0.3.0" +name = "objc2-app-kit" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8" - -[[package]] -name = "num-bigint" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +checksum = "e4e89ad9e3d7d297152b17d39ed92cd50ca8063a89a9fa569046d41568891eff" dependencies = [ - "num-integer", - "num-traits", + "bitflags 2.11.1", + "block2", + "libc", + "objc2", + "objc2-core-data", + "objc2-core-image", + "objc2-foundation", + "objc2-quartz-core", ] [[package]] -name = "num-derive" -version = "0.4.2" +name = "objc2-core-data" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" +checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef" dependencies = [ - "proc-macro2", - "quote", - "syn", + "bitflags 2.11.1", + "block2", + "objc2", + "objc2-foundation", ] [[package]] -name = "num-integer" -version = "0.1.46" +name = "objc2-core-image" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +checksum = "55260963a527c99f1819c4f8e3b47fe04f9650694ef348ffd2227e8196d34c80" dependencies = [ - "num-traits", + "block2", + "objc2", + "objc2-foundation", + "objc2-metal", ] [[package]] -name = "num-rational" -version = "0.4.2" +name = "objc2-encode" +version = "4.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + +[[package]] +name = "objc2-foundation" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" dependencies = [ - "num-bigint", - "num-integer", - "num-traits", + "bitflags 2.11.1", + "block2", + "libc", + "objc2", ] [[package]] -name = "num-traits" -version = "0.2.19" +name = "objc2-metal" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6" dependencies = [ - "autocfg", + "bitflags 2.11.1", + "block2", + "objc2", + "objc2-foundation", +] + +[[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.1", + "block2", + "objc2", + "objc2-foundation", + "objc2-metal", ] [[package]] @@ -586,16 +852,47 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" [[package]] -name = "paste" -version = "1.0.15" +name = "option-ext" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" [[package]] -name = "pastey" -version = "0.1.1" +name = "pango" +version = "0.18.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35fb2e5f958ec131621fdd531e9fc186ed768cbe395337403ae56c17a74c68ec" +checksum = "7ca27ec1eb0457ab26f3036ea52229edbdb74dee1edd29063f5b9b010e7ebee4" +dependencies = [ + "gio", + "glib", + "libc", + "once_cell", + "pango-sys", +] + +[[package]] +name = "pango-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "436737e391a843e5933d6d9aa102cb126d501e815b83601365a948a518555dc5" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pkg-config" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" [[package]] name = "png" @@ -611,25 +908,47 @@ dependencies = [ ] [[package]] -name = "png" -version = "0.18.1" +name = "proc-macro-crate" +version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" +checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" dependencies = [ - "bitflags 2.11.1", - "crc32fast", - "fdeflate", - "flate2", - "miniz_oxide", + "once_cell", + "toml_edit 0.19.15", ] [[package]] -name = "ppv-lite86" -version = "0.2.21" +name = "proc-macro-crate" +version = "2.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +checksum = "b00f26d3400549137f92511a46ac1cd8ce37cb5598a96d382381458b992a5d24" dependencies = [ - "zerocopy", + "toml_datetime", + "toml_edit 0.20.2", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", ] [[package]] @@ -641,46 +960,6 @@ 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", -] - -[[package]] -name = "pxfm" -version = "0.1.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0c5ccf5294c6ccd63a74f1565028353830a9c2f5eb0c682c355c471726a6e3f" - -[[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 = "quote" version = "1.0.45" @@ -691,121 +970,30 @@ dependencies = [ ] [[package]] -name = "r-efi" -version = "5.3.0" +name = "redox_users" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" - -[[package]] -name = "rand" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" -dependencies = [ - "rand_chacha", - "rand_core", -] - -[[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", -] - -[[package]] -name = "rand_core" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" dependencies = [ "getrandom", -] - -[[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", - "rand_chacha", - "simd_helpers", + "libredox", "thiserror", - "v_frame", - "wasm-bindgen", ] [[package]] -name = "ravif" -version = "0.13.0" +name = "rustc_version" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e52310197d971b0f5be7fe6b57530dcd27beb35c1b013f29d66c1ad73fbbcc45" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" dependencies = [ - "avif-serialize", - "imgref", - "loop9", - "quick-error", - "rav1e", - "rayon", - "rgb", + "semver", ] [[package]] -name = "rayon" -version = "1.12.0" +name = "semver" +version = "1.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" -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 = "rgb" -version = "0.8.53" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47b34b781b31e5d73e9fbc8689c70551fd1ade9a19e3e28cfec8580a79290cc4" - -[[package]] -name = "rustversion" -version = "1.0.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" [[package]] name = "serde" @@ -834,28 +1022,18 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] -name = "serde_json" -version = "1.0.149" +name = "serde_spanned" +version = "0.6.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" dependencies = [ - "itoa", - "memchr", "serde", - "serde_core", - "zmij", ] -[[package]] -name = "shlex" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" - [[package]] name = "simd-adler32" version = "0.3.9" @@ -863,13 +1041,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" [[package]] -name = "simd_helpers" -version = "0.1.0" +name = "slab" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95890f873bec569a0362c235787f3aca6e1e887302ba4840839bcc6459c42da6" -dependencies = [ - "quote", -] +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" [[package]] name = "smallvec" @@ -878,16 +1053,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" [[package]] -name = "stable_deref_trait" -version = "1.2.1" +name = "syn" +version = "1.0.109" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" - -[[package]] -name = "strict-num" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "unicode-ident", +] [[package]] name = "syn" @@ -901,63 +1074,107 @@ dependencies = [ ] [[package]] -name = "thiserror" -version = "2.0.18" +name = "system-deps" +version = "6.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349" +dependencies = [ + "cfg-expr", + "heck 0.5.0", + "pkg-config", + "toml", + "version-compare", +] + +[[package]] +name = "target-lexicon" +version = "0.12.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "2.0.18" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] -name = "tiff" -version = "0.11.3" +name = "toml" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b63feaf3343d35b6ca4d50483f94843803b0f51634937cc2ec519fc32232bc52" +checksum = "185d8ab0dfbb35cf1399a6344d8484209c088f75f8f68230da55d48d95d43e3d" dependencies = [ - "fax", - "flate2", - "half", - "quick-error", - "weezl", - "zune-jpeg", + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit 0.20.2", ] [[package]] -name = "tiny-skia" -version = "0.11.4" +name = "toml_datetime" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83d13394d44dae3207b52a326c0c85a8bf87f1541f23b0d143811088497b09ab" +checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" dependencies = [ - "arrayref", - "arrayvec", - "bytemuck", - "cfg-if", - "log", - "png 0.17.16", - "tiny-skia-path", + "serde", ] [[package]] -name = "tiny-skia-path" -version = "0.11.4" +name = "toml_edit" +version = "0.19.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c9e7fc0c2e86a30b117d0462aa261b72b7a99b7ebd7deb3a14ceda95c5bdc93" +checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" dependencies = [ - "arrayref", - "bytemuck", - "strict-num", + "indexmap", + "toml_datetime", + "winnow", +] + +[[package]] +name = "toml_edit" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "winnow", +] + +[[package]] +name = "tray-icon" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "044d7738b3d50f288ddef035b793228740ad4d927f5466b0af55dc15e7e03cfe" +dependencies = [ + "core-graphics", + "crossbeam-channel", + "dirs", + "libappindicator", + "muda", + "objc2", + "objc2-app-kit", + "objc2-foundation", + "once_cell", + "png", + "thiserror", + "windows-sys 0.59.0", ] [[package]] @@ -967,134 +1184,269 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] -name = "v_frame" +name = "unicode-segmentation" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c" + +[[package]] +name = "version-compare" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03c2856837ef78f57382f06b2b8563a2f512f7185d732608fd9176cb3b8edf0e" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "winapi" version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "666b7727c8875d6ab5db9533418d7c764233ac9c0cff1d469aec8fa127597be2" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" dependencies = [ - "aligned-vec", - "num-traits", - "wasm-bindgen", + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", ] [[package]] -name = "wasip2" -version = "1.0.3+wasi-0.2.9" +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd04d41d93c4992d421894c18c8b43496aa748dd4c081bac0dc93eb0489272b6" dependencies = [ - "wit-bindgen", + "windows-core", + "windows-targets 0.52.6", ] [[package]] -name = "wasm-bindgen" -version = "0.2.120" +name = "windows-core" +version = "0.58.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df52b6d9b87e0c74c9edfa1eb2d9bf85e5d63515474513aa50fa181b3c4f5db1" +checksum = "6ba6d44ec8c2591c134257ce647b7ea6b20335bf6379a27dac5f1641fcf59f99" dependencies = [ - "cfg-if", - "once_cell", - "rustversion", - "wasm-bindgen-macro", - "wasm-bindgen-shared", + "windows-implement", + "windows-interface", + "windows-result", + "windows-strings", + "windows-targets 0.52.6", ] [[package]] -name = "wasm-bindgen-macro" -version = "0.2.120" +name = "windows-implement" +version = "0.58.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78b1041f495fb322e64aca85f5756b2172e35cd459376e67f2a6c9dffcedb103" -dependencies = [ - "quote", - "wasm-bindgen-macro-support", -] - -[[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.120" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9dcd0ff20416988a18ac686d4d4d0f6aae9ebf08a389ff5d29012b05af2a1b41" -dependencies = [ - "bumpalo", - "proc-macro2", - "quote", - "syn", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-shared" -version = "0.2.120" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49757b3c82ebf16c57d69365a142940b384176c24df52a087fb748e2085359ea" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "weezl" -version = "0.1.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88" - -[[package]] -name = "wit-bindgen" -version = "0.57.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" - -[[package]] -name = "y4m" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a5a4b21e1a62b67a2970e6831bc091d7b87e119e7f9791aef9702e3bef04448" - -[[package]] -name = "zerocopy" -version = "0.8.48" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" -dependencies = [ - "zerocopy-derive", -] - -[[package]] -name = "zerocopy-derive" -version = "0.8.48" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" +checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] -name = "zmij" -version = "1.0.21" +name = "windows-interface" +version = "0.58.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" - -[[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" +checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515" dependencies = [ - "simd-adler32", + "proc-macro2", + "quote", + "syn 2.0.117", ] [[package]] -name = "zune-jpeg" -version = "0.5.15" +name = "windows-result" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27bc9d5b815bc103f142aa054f561d9187d191692ec7c2d1e2b4737f8dbd7296" +checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" dependencies = [ - "zune-core", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-strings" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" +dependencies = [ + "windows-result", + "windows-targets 0.52.6", +] + +[[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.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[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", + "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_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_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_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_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[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_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_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_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 = "winnow" +version = "0.5.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" +dependencies = [ + "memchr", +] + +[[package]] +name = "x11" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "502da5464ccd04011667b11c435cb992822c2c0dbde1770c988480d312a0db2e" +dependencies = [ + "libc", + "pkg-config", ] diff --git a/Cargo.toml b/Cargo.toml index d74c3eb..eae7bc4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,11 +8,20 @@ name = "iwaku" path = "src/main.rs" [dependencies] -image = { version = "0.25", features = ["png"] } -tiny-skia = "0.11" -serde = { version = "1", features = ["derive"] } -anyhow = "1" -serde_json = "1.0.149" +windows = { version = "0.58", features = [ + "Win32_Foundation", + "Win32_Graphics_Gdi", + "Win32_System_Com", + "Win32_System_LibraryLoader", + "Win32_System_Ole", + "Win32_UI_Accessibility", + "Win32_UI_HiDpi", + "Win32_UI_Input_KeyboardAndMouse", + "Win32_UI_Magnification", + "Win32_UI_Shell", + "Win32_UI_WindowsAndMessaging", +] } +tray-icon = "0.17" [profile.release] opt-level = 3 diff --git a/_ul b/_ul new file mode 100644 index 0000000..42de59f --- /dev/null +++ b/_ul @@ -0,0 +1 @@ +/usr/bin/bash: line 1: del: command not found diff --git a/src/caret.rs b/src/caret.rs new file mode 100644 index 0000000..827c1b7 --- /dev/null +++ b/src/caret.rs @@ -0,0 +1,121 @@ +use std::cell::RefCell; + +use windows::Win32::Foundation::{HWND, POINT}; +use windows::Win32::Graphics::Gdi::ClientToScreen; +use windows::Win32::System::Com::SAFEARRAY; +use windows::Win32::System::Com::{CoCreateInstance, CLSCTX_INPROC_SERVER}; +use windows::Win32::System::Ole::{SafeArrayGetElement, SafeArrayGetLBound, SafeArrayGetUBound}; +use windows::Win32::UI::Accessibility::{ + CUIAutomation, IUIAutomation, IUIAutomationElement, IUIAutomationTextPattern, UIA_TextPatternId, +}; +use windows::Win32::UI::WindowsAndMessaging::{ + GetForegroundWindow, GetGUIThreadInfo, GetWindowThreadProcessId, GUITHREADINFO, +}; + +thread_local! { + static AUTOMATION: RefCell> = RefCell::new(None); +} + +fn get_automation() -> Option { + AUTOMATION.with(|cell| { + let mut borrow = cell.borrow_mut(); + if borrow.is_none() { + unsafe { + *borrow = CoCreateInstance(&CUIAutomation, None, CLSCTX_INPROC_SERVER).ok(); + } + } + borrow.clone() + }) +} + +pub fn get_caret_pos() -> Option { + // 1. Win32 GUI-thread caret — fast, no COM. Works for classic edit controls, + // Notepad, Office dialogs, conhost-based terminals, etc. + if let Some(pt) = get_caret_via_gui_thread() { + return Some(pt); + } + // 2. UI Automation — works for Word, modern browsers, WPF, UWP apps. + get_caret_via_uia() +} + +fn get_caret_via_gui_thread() -> Option { + unsafe { + let hwnd: HWND = GetForegroundWindow(); + if hwnd.0.is_null() { + return None; + } + let thread_id = GetWindowThreadProcessId(hwnd, None); + let mut gti = GUITHREADINFO { + cbSize: std::mem::size_of::() as u32, + ..Default::default() + }; + GetGUIThreadInfo(thread_id, &mut gti).ok()?; + if gti.hwndCaret.0.is_null() { + return None; + } + let mut pt = POINT { + x: gti.rcCaret.left, + y: gti.rcCaret.bottom, + }; + let _ = ClientToScreen(gti.hwndCaret, &mut pt); + Some(pt) + } +} + +fn get_caret_via_uia() -> Option { + unsafe { + let automation = get_automation()?; + let element = automation.GetFocusedElement().ok()?; + + // Try the focused element and walk up to 6 ancestors. + // Some apps expose TextPattern on a parent rather than the focused leaf. + let mut current: Option = Some(element); + for _ in 0..7 { + let el = current.as_ref()?; + if let Some(sa) = try_text_pattern(el) { + return point_from_safearray(sa); + } + current = automation + .ControlViewWalker() + .ok() + .and_then(|w| w.GetParentElement(el).ok()); + } + None + } +} + +unsafe fn try_text_pattern(el: &IUIAutomationElement) -> Option<*mut SAFEARRAY> { + unsafe { + let pattern = el + .GetCurrentPatternAs::(UIA_TextPatternId) + .ok()?; + let ranges = pattern.GetSelection().ok()?; + if ranges.Length().ok()? == 0 { + return None; + } + ranges.GetElement(0).ok()?.GetBoundingRectangles().ok() + } +} + +unsafe fn point_from_safearray(sa: *mut SAFEARRAY) -> Option { + unsafe { + if sa.is_null() { + return None; + } + let lb = SafeArrayGetLBound(sa, 1).ok()?; + let ub = SafeArrayGetUBound(sa, 1).ok()?; + if ub - lb < 3 { + return None; + } + let mut left: f64 = 0.0; + let mut top: f64 = 0.0; + let mut idx0 = lb; + let mut idx1 = lb + 1; + SafeArrayGetElement(sa, &mut idx0, &mut left as *mut f64 as *mut _).ok()?; + SafeArrayGetElement(sa, &mut idx1, &mut top as *mut f64 as *mut _).ok()?; + Some(POINT { + x: left as i32, + y: top as i32, + }) + } +} diff --git a/src/config.rs b/src/config.rs deleted file mode 100644 index 3b502f1..0000000 --- a/src/config.rs +++ /dev/null @@ -1,45 +0,0 @@ -use anyhow::{Context, Result}; -use serde::{Deserialize, Serialize}; -use std::path::PathBuf; - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Config { - #[serde(rename = "screenshot")] - pub screenshot: EffectsConfig, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct EffectsConfig { - pub mode: String, - pub rounded_corners: bool, - pub corner_radius: f32, - pub drop_shadow: bool, - pub shadow_blur_radius: f32, - pub shadow_offset_x: f32, - pub shadow_offset_y: f32, - pub shadow_color: [u8; 4], -} - -impl Config { - pub fn config_path() -> Option { - let home = std::env::var("HOME").ok()?; - Some( - PathBuf::from(home) - .join(".config") - .join("zshell") - .join("config.json"), - ) - } - - pub fn load() -> Result { - let path = Self::config_path().context("Could not determine HOME directory")?; - Self::load_from(&path) - } - - pub fn load_from(path: &PathBuf) -> Result { - let raw = std::fs::read_to_string(path) - .with_context(|| format!("Failed to read config at {}", path.display()))?; - serde_json::from_str(&raw) - .with_context(|| format!("Failed to parse JSON config at {}", path.display())) - } -} diff --git a/src/effects.rs b/src/effects.rs deleted file mode 100644 index e6d41f6..0000000 --- a/src/effects.rs +++ /dev/null @@ -1,288 +0,0 @@ -use crate::config::EffectsConfig; -use image::RgbaImage; -use tiny_skia::{ - BlendMode, Color, FillRule, Paint, Path, PathBuilder, Pixmap, PixmapPaint, Transform, -}; - -pub fn apply_effects(img: RgbaImage, cfg: &EffectsConfig) -> RgbaImage { - let img = if cfg.rounded_corners { - apply_rounded_corners(img, cfg.corner_radius) - } else { - img - }; - if cfg.drop_shadow { - apply_drop_shadow( - img, - cfg.shadow_blur_radius, - cfg.shadow_offset_x, - cfg.shadow_offset_y, - cfg.shadow_color, - ) - } else { - img - } -} - -pub fn apply_rounded_corners(img: RgbaImage, radius: f32) -> RgbaImage { - let (w, h) = img.dimensions(); - let mut mask = Pixmap::new(w, h).expect("mask pixmap"); - let path = rounded_rect_path(0.0, 0.0, w as f32, h as f32, radius); - let mut paint = Paint::default(); - paint.set_color(Color::WHITE); - paint.anti_alias = true; - mask.fill_path( - &path, - &paint, - FillRule::Winding, - Transform::identity(), - None, - ); - - let mut pixmap = rgba_image_to_pixmap(&img); - let mut dst_paint = PixmapPaint::default(); - dst_paint.blend_mode = BlendMode::DestinationIn; - pixmap.draw_pixmap(0, 0, mask.as_ref(), &dst_paint, Transform::identity(), None); - pixmap_to_rgba_image(pixmap) -} - -pub fn apply_drop_shadow( - img: RgbaImage, - blur_radius: f32, - offset_x: f32, - offset_y: f32, - shadow_color: [u8; 4], -) -> RgbaImage { - let (iw, ih) = img.dimensions(); - let br = blur_radius.ceil() as u32; - - let extra_left = br.saturating_sub((-offset_x).max(0.0) as u32); - let extra_top = br.saturating_sub((-offset_y).max(0.0) as u32); - let extra_right = br + offset_x.max(0.0) as u32; - let extra_bottom = br + offset_y.max(0.0) as u32; - - let canvas_w = iw + extra_left + extra_right; - let canvas_h = ih + extra_top + extra_bottom; - - let mut shadow_pixmap = Pixmap::new(canvas_w, canvas_h).expect("shadow pixmap"); - let img_pixmap = rgba_image_to_pixmap(&img); - let shadow_x = (extra_left as f32 + offset_x) as i32; - let shadow_y = (extra_top as f32 + offset_y) as i32; - - let mut sp = PixmapPaint::default(); - sp.blend_mode = BlendMode::Source; - shadow_pixmap.draw_pixmap( - shadow_x, - shadow_y, - img_pixmap.as_ref(), - &sp, - Transform::identity(), - None, - ); - - tint_pixmap_as_shadow(&mut shadow_pixmap, shadow_color); - - let shadow_img = pixmap_to_rgba_image(shadow_pixmap); - let blurred = box_blur_rgba(&shadow_img, br); - let blurred_pixmap = rgba_image_to_pixmap(&blurred); - - let mut canvas = Pixmap::new(canvas_w, canvas_h).expect("canvas pixmap"); - let mut p = PixmapPaint::default(); - p.blend_mode = BlendMode::Source; - canvas.draw_pixmap( - 0, - 0, - blurred_pixmap.as_ref(), - &p, - Transform::identity(), - None, - ); - - let mut p2 = PixmapPaint::default(); - p2.blend_mode = BlendMode::SourceOver; - canvas.draw_pixmap( - extra_left as i32, - extra_top as i32, - img_pixmap.as_ref(), - &p2, - Transform::identity(), - None, - ); - - pixmap_to_rgba_image(canvas) -} - -fn rounded_rect_path(x: f32, y: f32, w: f32, h: f32, r: f32) -> Path { - let r = r.min(w / 2.0).min(h / 2.0); - let mut pb = PathBuilder::new(); - pb.move_to(x + r, y); - pb.line_to(x + w - r, y); - pb.quad_to(x + w, y, x + w, y + r); - pb.line_to(x + w, y + h - r); - pb.quad_to(x + w, y + h, x + w - r, y + h); - pb.line_to(x + r, y + h); - pb.quad_to(x, y + h, x, y + h - r); - pb.line_to(x, y + r); - pb.quad_to(x, y, x + r, y); - pb.close(); - pb.finish().expect("rounded rect path") -} - -fn rgba_image_to_pixmap(img: &RgbaImage) -> Pixmap { - let (w, h) = img.dimensions(); - let mut pixmap = Pixmap::new(w, h).expect("pixmap alloc"); - let pixels = pixmap.pixels_mut(); - for (i, px) in img.pixels().enumerate() { - let [r, g, b, a] = px.0; - let af = a as f32 / 255.0; - pixels[i] = tiny_skia::PremultipliedColorU8::from_rgba( - (r as f32 * af) as u8, - (g as f32 * af) as u8, - (b as f32 * af) as u8, - a, - ) - .unwrap_or(tiny_skia::PremultipliedColorU8::TRANSPARENT); - } - pixmap -} - -fn pixmap_to_rgba_image(pixmap: Pixmap) -> RgbaImage { - let (w, h) = (pixmap.width(), pixmap.height()); - let mut out = RgbaImage::new(w, h); - for (i, px) in pixmap.pixels().iter().enumerate() { - let x = (i as u32) % w; - let y = (i as u32) / w; - let a = px.alpha(); - let (r, g, b) = if a == 0 { - (0, 0, 0) - } else { - let af = a as f32 / 255.0; - ( - (px.red() as f32 / af).round().min(255.0) as u8, - (px.green() as f32 / af).round().min(255.0) as u8, - (px.blue() as f32 / af).round().min(255.0) as u8, - ) - }; - out.put_pixel(x, y, image::Rgba([r, g, b, a])); - } - out -} - -fn tint_pixmap_as_shadow(pixmap: &mut Pixmap, color: [u8; 4]) { - let [sr, sg, sb, _] = color; - for px in pixmap.pixels_mut() { - let a = px.alpha(); - if a > 0 { - let af = a as f32 / 255.0; - *px = tiny_skia::PremultipliedColorU8::from_rgba( - (sr as f32 * af) as u8, - (sg as f32 * af) as u8, - (sb as f32 * af) as u8, - a, - ) - .unwrap_or(tiny_skia::PremultipliedColorU8::TRANSPARENT); - } - } -} - -fn box_blur_rgba(img: &RgbaImage, radius: u32) -> RgbaImage { - if radius == 0 { - return img.clone(); - } - let mut buf = sliding_horizontal(img, radius); - buf = sliding_vertical(&buf, radius); - buf = sliding_horizontal(&buf, radius); - buf = sliding_vertical(&buf, radius); - buf -} - -fn sliding_horizontal(img: &RgbaImage, radius: u32) -> RgbaImage { - let (w, h) = img.dimensions(); - let r = radius as i32; - let diam = (2 * r + 1) as u32; - let mut out = RgbaImage::new(w, h); - - for y in 0..h { - let mut sr = 0u32; - let mut sg = 0u32; - let mut sb = 0u32; - let mut sa = 0u32; - - for dx in -r..=r { - let sx = dx.clamp(0, w as i32 - 1) as u32; - let p = img.get_pixel(sx, y).0; - sr += p[0] as u32; - sg += p[1] as u32; - sb += p[2] as u32; - sa += p[3] as u32; - } - - for x in 0..w { - out.put_pixel( - x, - y, - image::Rgba([ - (sr / diam) as u8, - (sg / diam) as u8, - (sb / diam) as u8, - (sa / diam) as u8, - ]), - ); - - let remove_x = (x as i32 - r).clamp(0, w as i32 - 1) as u32; - let add_x = (x as i32 + r + 1).clamp(0, w as i32 - 1) as u32; - let rp = img.get_pixel(remove_x, y).0; - let ap = img.get_pixel(add_x, y).0; - sr = sr.saturating_sub(rp[0] as u32) + ap[0] as u32; - sg = sg.saturating_sub(rp[1] as u32) + ap[1] as u32; - sb = sb.saturating_sub(rp[2] as u32) + ap[2] as u32; - sa = sa.saturating_sub(rp[3] as u32) + ap[3] as u32; - } - } - out -} - -fn sliding_vertical(img: &RgbaImage, radius: u32) -> RgbaImage { - let (w, h) = img.dimensions(); - let r = radius as i32; - let diam = (2 * r + 1) as u32; - let mut out = RgbaImage::new(w, h); - - for x in 0..w { - let mut sr = 0u32; - let mut sg = 0u32; - let mut sb = 0u32; - let mut sa = 0u32; - - for dy in -r..=r { - let sy = dy.clamp(0, h as i32 - 1) as u32; - let p = img.get_pixel(x, sy).0; - sr += p[0] as u32; - sg += p[1] as u32; - sb += p[2] as u32; - sa += p[3] as u32; - } - - for y in 0..h { - out.put_pixel( - x, - y, - image::Rgba([ - (sr / diam) as u8, - (sg / diam) as u8, - (sb / diam) as u8, - (sa / diam) as u8, - ]), - ); - - let remove_y = (y as i32 - r).clamp(0, h as i32 - 1) as u32; - let add_y = (y as i32 + r + 1).clamp(0, h as i32 - 1) as u32; - let rp = img.get_pixel(x, remove_y).0; - let ap = img.get_pixel(x, add_y).0; - sr = sr.saturating_sub(rp[0] as u32) + ap[0] as u32; - sg = sg.saturating_sub(rp[1] as u32) + ap[1] as u32; - sb = sb.saturating_sub(rp[2] as u32) + ap[2] as u32; - sa = sa.saturating_sub(rp[3] as u32) + ap[3] as u32; - } - } - out -} diff --git a/src/hook.rs b/src/hook.rs new file mode 100644 index 0000000..193574d --- /dev/null +++ b/src/hook.rs @@ -0,0 +1,100 @@ +use windows::Win32::Foundation::{LPARAM, LRESULT, WPARAM}; +use windows::Win32::UI::Input::KeyboardAndMouse::{ + GetAsyncKeyState, SendInput, INPUT, INPUT_KEYBOARD, KEYBDINPUT, KEYEVENTF_KEYUP, VIRTUAL_KEY, + VK_F24, VK_LWIN, VK_RWIN, +}; +use windows::Win32::UI::WindowsAndMessaging::{ + CallNextHookEx, SetWindowsHookExW, UnhookWindowsHookEx, HHOOK, MSLLHOOKSTRUCT, WH_MOUSE_LL, + WM_MOUSEWHEEL, +}; + +use std::sync::atomic::{AtomicIsize, Ordering}; + +pub const WM_APP_ZOOM_IN: u32 = 0x8001; +pub const WM_APP_ZOOM_OUT: u32 = 0x8002; + +static MOUSE_HOOK: AtomicIsize = AtomicIsize::new(0); +static MAIN_HWND: AtomicIsize = AtomicIsize::new(0); + +pub fn install(main_hwnd: windows::Win32::Foundation::HWND) { + MAIN_HWND.store(main_hwnd.0 as isize, Ordering::SeqCst); + unsafe { + let mh = SetWindowsHookExW(WH_MOUSE_LL, Some(mouse_hook_proc), None, 0) + .expect("Failed to install WH_MOUSE_LL hook"); + MOUSE_HOOK.store(mh.0 as isize, Ordering::SeqCst); + } +} + +pub fn uninstall() { + let raw = MOUSE_HOOK.swap(0, Ordering::SeqCst); + if raw != 0 { + unsafe { + let _ = UnhookWindowsHookEx(HHOOK(raw as *mut _)); + } + } +} + +unsafe extern "system" fn mouse_hook_proc(code: i32, wparam: WPARAM, lparam: LPARAM) -> LRESULT { + if code >= 0 && wparam.0 as u32 == WM_MOUSEWHEEL { + unsafe { + let lwin = GetAsyncKeyState(VK_LWIN.0 as i32); + let rwin = GetAsyncKeyState(VK_RWIN.0 as i32); + if (lwin < 0) || (rwin < 0) { + let info = &*(lparam.0 as *const MSLLHOOKSTRUCT); + let delta = (info.mouseData >> 16) as i16; + + let hwnd_raw = MAIN_HWND.load(Ordering::SeqCst); + if hwnd_raw != 0 { + let hwnd = windows::Win32::Foundation::HWND(hwnd_raw as *mut _); + let msg = if delta > 0 { + WM_APP_ZOOM_IN + } else { + WM_APP_ZOOM_OUT + }; + let _ = windows::Win32::UI::WindowsAndMessaging::PostMessageW( + hwnd, + msg, + WPARAM(0), + LPARAM(0), + ); + } + + // Inject a dummy F24 down+up between the Win key-down and the + // upcoming Win key-up. Windows only opens the Start menu when + // Win is the *sole* key in a press/release cycle; seeing any + // other key in between suppresses it — no stuck key, no Start + // menu, no keyboard hook required. + inject_f24(); + + return LRESULT(1); // consume the scroll event + } + } + } + unsafe { CallNextHookEx(None, code, wparam, lparam) } +} + +fn inject_f24() { + let inputs = [make_key(VK_F24, false), make_key(VK_F24, true)]; + unsafe { + SendInput(&inputs, std::mem::size_of::() as i32); + } +} + +fn make_key(vk: VIRTUAL_KEY, key_up: bool) -> INPUT { + INPUT { + r#type: INPUT_KEYBOARD, + Anonymous: windows::Win32::UI::Input::KeyboardAndMouse::INPUT_0 { + ki: KEYBDINPUT { + wVk: vk, + wScan: 0, + dwFlags: if key_up { + KEYEVENTF_KEYUP + } else { + Default::default() + }, + time: 0, + dwExtraInfo: 0, + }, + }, + } +} diff --git a/src/magnifier.rs b/src/magnifier.rs new file mode 100644 index 0000000..bc77e56 --- /dev/null +++ b/src/magnifier.rs @@ -0,0 +1,76 @@ +use windows::Win32::Foundation::HWND; +use windows::Win32::Graphics::Gdi::{ + GetMonitorInfoW, MonitorFromWindow, MONITORINFO, MONITOR_DEFAULTTOPRIMARY, +}; +use windows::Win32::UI::Magnification::{ + MagInitialize, MagSetFullscreenTransform, MagUninitialize, +}; + +use crate::state::AppState; + +pub struct Magnifier { + screen_w: i32, + screen_h: i32, +} + +impl Magnifier { + pub fn new(screen_w: i32, screen_h: i32) -> Self { + Self { screen_w, screen_h } + } + + /// Apply or clear the fullscreen magnification transform. + pub fn update(&self, state: &mut AppState, just_activated: bool) { + unsafe { + if !state.active { + let _ = MagSetFullscreenTransform(1.0, 0, 0); + return; + } + + let z = state.zoom; + let src_w = self.screen_w as f32 / z; + let src_h = self.screen_h as f32 / z; + + state.update_viewport( + src_w, + src_h, + self.screen_w as f32, + self.screen_h as f32, + just_activated, + ); + + let x_off = state.viewport_x as i32; + let y_off = state.viewport_y as i32; + + let _ = MagSetFullscreenTransform(z, x_off, y_off); + } + } +} + +pub fn create() -> Magnifier { + unsafe { + MagInitialize().expect("MagInitialize failed"); + let (sw, sh) = monitor_dimensions(); + Magnifier::new(sw, sh) + } +} + +pub fn shutdown() { + unsafe { + let _ = MagSetFullscreenTransform(1.0, 0, 0); + let _ = MagUninitialize(); + } +} + +fn monitor_dimensions() -> (i32, i32) { + unsafe { + let hmon = MonitorFromWindow(HWND(std::ptr::null_mut()), MONITOR_DEFAULTTOPRIMARY); + let mut info = MONITORINFO { + cbSize: std::mem::size_of::() as u32, + ..Default::default() + }; + let _ = GetMonitorInfoW(hmon, &mut info); + let sw = info.rcMonitor.right - info.rcMonitor.left; + let sh = info.rcMonitor.bottom - info.rcMonitor.top; + (sw, sh) + } +} diff --git a/src/main.rs b/src/main.rs index b142628..a8b964b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,211 +1,177 @@ -mod config; -mod effects; +#![windows_subsystem = "windows"] -use anyhow::{bail, Context, Result}; -use std::io::Write as _; -use std::process::{Command, Stdio}; +mod caret; +mod hook; +mod magnifier; +mod state; +mod tray; -/// CLI overrides that map 1:1 to `EffectsConfig` fields. -/// All fields are `Option` so we can tell "not supplied" from any concrete value. -#[derive(Default)] -struct CliOverrides { - rounded_corners: Option, - corner_radius: Option, - drop_shadow: Option, - shadow_blur_radius: Option, - shadow_offset_x: Option, - shadow_offset_y: Option, - /// Accepted as four comma-separated u8 values, e.g. `255,0,0,200` - shadow_color: Option<[u8; 4]>, -} +use state::AppState; +use windows::core::w; +use windows::Win32::Foundation::{HINSTANCE, HWND, LPARAM, LRESULT, WPARAM}; +use windows::Win32::System::Com::{CoInitializeEx, CoUninitialize, COINIT_APARTMENTTHREADED}; +use windows::Win32::UI::HiDpi::{ + SetProcessDpiAwarenessContext, DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2, +}; +use windows::Win32::UI::WindowsAndMessaging::GetCursorPos; +use windows::Win32::UI::WindowsAndMessaging::WINDOW_EX_STYLE; +use windows::Win32::UI::WindowsAndMessaging::{ + CreateWindowExW, DefWindowProcW, DispatchMessageW, GetMessageW, PostQuitMessage, + RegisterClassExW, SetTimer, TranslateMessage, CS_HREDRAW, CS_VREDRAW, MSG, WM_DESTROY, + WM_TIMER, WNDCLASSEXW, WS_OVERLAPPEDWINDOW, +}; -fn parse_bool(s: &str) -> Result { - match s.to_lowercase().as_str() { - "true" | "1" | "yes" => Ok(true), - "false" | "0" | "no" => Ok(false), - other => bail!("Expected a boolean (true/false), got '{other}'"), - } -} +static mut APP_STATE: Option = None; +static mut MAGNIFIER: Option = None; -fn parse_shadow_color(s: &str) -> Result<[u8; 4]> { - let parts: Vec<&str> = s.split(',').collect(); - if parts.len() != 4 { - bail!("--shadow_color expects four comma-separated u8 values, e.g. 255,0,0,200"); - } - let r = parts[0] - .trim() - .parse::() - .context("shadow_color red channel")?; - let g = parts[1] - .trim() - .parse::() - .context("shadow_color green channel")?; - let b = parts[2] - .trim() - .parse::() - .context("shadow_color blue channel")?; - let a = parts[3] - .trim() - .parse::() - .context("shadow_color alpha channel")?; - Ok([r, g, b, a]) -} +const TIMER_ID: usize = 1; +const TIMER_MS: u32 = 16; // ~60 fps -fn main() -> Result<()> { - let args: Vec = std::env::args().skip(1).collect(); +fn main() { + unsafe { + // DPI awareness — must be set before any window is created. + let _ = SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2); - let mut image_path: Option = None; - let mut overrides = CliOverrides::default(); + // COM initialisation needed for UI Automation (caret tracking). + let _ = CoInitializeEx(None, COINIT_APARTMENTTHREADED); + APP_STATE = Some(AppState::new()); - let mut i = 0; - while i < args.len() { - match args[i].as_str() { - "--image" => { - i += 1; - image_path = Some( - args.get(i) - .cloned() - .context("Expected a path after --image")?, - ); - } - "--rounded_corners" => { - i += 1; - let val = args - .get(i) - .context("Expected true/false after --rounded_corners")?; - overrides.rounded_corners = Some(parse_bool(val)?); - } - "--corner_radius" => { - i += 1; - let val = args - .get(i) - .context("Expected a number after --corner_radius")?; - overrides.corner_radius = Some( - val.parse::() - .context("--corner_radius must be a number")?, - ); - } - "--drop_shadow" => { - i += 1; - let val = args - .get(i) - .context("Expected true/false after --drop_shadow")?; - overrides.drop_shadow = Some(parse_bool(val)?); - } - "--shadow_blur_radius" => { - i += 1; - let val = args - .get(i) - .context("Expected a number after --shadow_blur_radius")?; - overrides.shadow_blur_radius = Some( - val.parse::() - .context("--shadow_blur_radius must be a number")?, - ); - } - "--shadow_offset_x" => { - i += 1; - let val = args - .get(i) - .context("Expected a number after --shadow_offset_x")?; - overrides.shadow_offset_x = Some( - val.parse::() - .context("--shadow_offset_x must be a number")?, - ); - } - "--shadow_offset_y" => { - i += 1; - let val = args - .get(i) - .context("Expected a number after --shadow_offset_y")?; - overrides.shadow_offset_y = Some( - val.parse::() - .context("--shadow_offset_y must be a number")?, - ); - } - "--shadow_color" => { - i += 1; - let val = args - .get(i) - .context("Expected r,g,b,a after --shadow_color")?; - overrides.shadow_color = Some(parse_shadow_color(val)?); - } - unknown => bail!("Unknown argument: {unknown}"), - } - i += 1; - } + let instance = get_instance(); + register_msg_class(instance); - let image_path = image_path.context("Missing --image ")?; - - let config = config::Config::load().context("Failed to load config")?; - - let mut effects = config.screenshot; - if effects.mode == "auto" { - if let Some(v) = overrides.rounded_corners { - effects.rounded_corners = v; - } - if let Some(v) = overrides.corner_radius { - effects.corner_radius = v; - } - if let Some(v) = overrides.drop_shadow { - effects.drop_shadow = v; - } - if let Some(v) = overrides.shadow_blur_radius { - effects.shadow_blur_radius = v; - } - if let Some(v) = overrides.shadow_offset_x { - effects.shadow_offset_x = v; - } - if let Some(v) = overrides.shadow_offset_y { - effects.shadow_offset_y = v; - } - if let Some(v) = overrides.shadow_color { - effects.shadow_color = v; - } - } - - if let Err(e) = process_image(&image_path, &effects) { - eprintln!("Error processing '{}': {e:#}", image_path); - } - - Ok(()) -} - -fn process_image(path: &str, effects: &config::EffectsConfig) -> Result<()> { - let img = image::open(path) - .with_context(|| format!("Failed to open image '{path}'"))? - .into_rgba8(); - - let processed = effects::apply_effects(img, effects); - - let mut png_bytes: Vec = Vec::new(); - image::DynamicImage::ImageRgba8(processed) - .write_to( - &mut std::io::Cursor::new(&mut png_bytes), - image::ImageFormat::Png, + // Invisible message-only window to receive WM_APP zoom messages and WM_TIMER. + let msg_hwnd = CreateWindowExW( + WINDOW_EX_STYLE(0), + w!("IwakuMsg"), + w!(""), + WS_OVERLAPPEDWINDOW, + 0, + 0, + 0, + 0, + None, + None, + instance, + None, ) - .context("Failed to encode processed image as PNG")?; + .expect("Failed to create message window"); - let mut child = Command::new("swappy") - .args(["-f", "-"]) - .stdin(Stdio::piped()) - .spawn() - .context("Failed to spawn swappy. Is it installed and in PATH?")?; + // Global Win+Scroll hook. + hook::install(msg_hwnd); - child - .stdin - .take() - .context("Failed to get swappy stdin")? - .write_all(&png_bytes) - .context("Failed to write image data to swappy")?; + // Fullscreen magnifier — no overlay window needed with MagSetFullscreenTransform. + MAGNIFIER = Some(magnifier::create()); - let status = child.wait().context("Failed to wait for swappy")?; + // System tray icon. + let tray = tray::Tray::new(); - if !status.success() { - eprintln!( - "swappy exited with non-zero status for '{}': {}", - path, status - ); + // 16 ms refresh timer. + SetTimer(msg_hwnd, TIMER_ID, TIMER_MS, None); + + // Message loop. + let mut msg = MSG::default(); + loop { + if tray.process_events() { + break; + } + + let ret = GetMessageW(&mut msg, None, 0, 0); + if ret.0 == 0 || ret.0 == -1 { + break; + } + let _ = TranslateMessage(&msg); + DispatchMessageW(&msg); + } + + hook::uninstall(); + magnifier::shutdown(); + CoUninitialize(); + } +} + +/// Window procedure for the invisible message window. +unsafe extern "system" fn wnd_proc( + hwnd: HWND, + msg: u32, + wparam: WPARAM, + lparam: LPARAM, +) -> LRESULT { + unsafe { + match msg { + WM_TIMER => { + if wparam.0 == TIMER_ID { + tick(); + } + LRESULT(0) + } + m if m == hook::WM_APP_ZOOM_IN => { + if let Some(state) = (*std::ptr::addr_of_mut!(APP_STATE)).as_mut() { + state.zoom_in(); + } + LRESULT(0) + } + m if m == hook::WM_APP_ZOOM_OUT => { + if let Some(state) = (*std::ptr::addr_of_mut!(APP_STATE)).as_mut() { + state.zoom_out(); + } + LRESULT(0) + } + WM_DESTROY => { + PostQuitMessage(0); + LRESULT(0) + } + _ => DefWindowProcW(hwnd, msg, wparam, lparam), + } + } +} + +/// Called every ~16 ms: update inputs, recalculate priority, push to magnifier. +unsafe fn tick() { + unsafe { + let state = match (*std::ptr::addr_of_mut!(APP_STATE)).as_mut() { + Some(s) => s, + None => return, + }; + + // Remember whether the magnifier was active before this tick so we + // can detect the exact frame it turns on and snap the smooth position. + let was_active = state.active; + + // 1. Update cursor and caret. + let mut pt = windows::Win32::Foundation::POINT::default(); + if GetCursorPos(&mut pt).is_ok() { + state.cursor = pt; + } + state.caret = caret::get_caret_pos(); + + // 2. Advance state. + let just_activated = state.active && !was_active; + state.update(just_activated); + + // 3. Push the new transform. + if let Some(mag) = (*std::ptr::addr_of_mut!(MAGNIFIER)).as_mut() { + mag.update(state, just_activated); + } + } +} + +unsafe fn get_instance() -> HINSTANCE { + unsafe { + use windows::Win32::System::LibraryLoader::GetModuleHandleW; + GetModuleHandleW(None).unwrap().into() + } +} + +unsafe fn register_msg_class(instance: HINSTANCE) { + unsafe { + let wc = WNDCLASSEXW { + cbSize: std::mem::size_of::() as u32, + style: CS_HREDRAW | CS_VREDRAW, + lpfnWndProc: Some(wnd_proc), + hInstance: instance, + lpszClassName: w!("IwakuMsg"), + ..Default::default() + }; + RegisterClassExW(&wc); } - - Ok(()) } diff --git a/src/state.rs b/src/state.rs new file mode 100644 index 0000000..9f6bfeb --- /dev/null +++ b/src/state.rs @@ -0,0 +1,192 @@ +use windows::Win32::Foundation::POINT; + +pub const ZOOM_MIN: f32 = 1.5; +pub const ZOOM_MAX: f32 = 10.0; +pub const ZOOM_STEP: f32 = 0.5; + +const EDGE_MARGIN: f32 = 0.10; +const PAN_ALPHA: f32 = 0.20; +const ZOOM_ALPHA: f32 = 0.18; +const ZOOM_EPSILON: f32 = 0.01; +const CURSOR_ALPHA: f32 = 0.35; +const MOUSE_MOVE_THRESHOLD_SQ: i32 = 6 * 6; +const MOUSE_TAKEOVER_TICKS: u32 = 4; +/// Ticks of mouse stillness before caret re-takes focus (if caret present). ~240 ms. +const CARET_REFOCUS_TICKS: u32 = 15; + +pub struct AppState { + pub zoom: f32, + target_zoom: f32, + prev_zoom: f32, + pub active: bool, + + pub cursor: POINT, + prev_cursor: POINT, + smooth_cursor_x: f32, + smooth_cursor_y: f32, + + pub caret: Option, + prev_caret: Option, + pub caret_active: bool, + mouse_move_ticks: u32, + mouse_still_ticks: u32, + + pub viewport_x: f32, + pub viewport_y: f32, +} + +impl AppState { + pub fn new() -> Self { + Self { + zoom: 1.0, + target_zoom: 1.0, + prev_zoom: 1.0, + active: false, + cursor: POINT { x: 0, y: 0 }, + prev_cursor: POINT { x: 0, y: 0 }, + smooth_cursor_x: 0.0, + smooth_cursor_y: 0.0, + caret: None, + prev_caret: None, + caret_active: false, + mouse_move_ticks: 0, + mouse_still_ticks: 0, + viewport_x: 0.0, + viewport_y: 0.0, + } + } + + pub fn update(&mut self, _just_activated: bool) { + // Animate zoom. + if self.active { + let diff = self.target_zoom - self.zoom; + if diff.abs() < ZOOM_EPSILON { + self.zoom = self.target_zoom; + if self.zoom <= 1.0 { + self.zoom = 1.0; + self.active = false; + } + } else { + self.zoom += diff * ZOOM_ALPHA; + } + } + + // Smooth cursor. + self.smooth_cursor_x += (self.cursor.x as f32 - self.smooth_cursor_x) * CURSOR_ALPHA; + self.smooth_cursor_y += (self.cursor.y as f32 - self.smooth_cursor_y) * CURSOR_ALPHA; + + // Caret / mouse priority. + let dx = self.cursor.x - self.prev_cursor.x; + let dy = self.cursor.y - self.prev_cursor.y; + let intentional = dx * dx + dy * dy >= MOUSE_MOVE_THRESHOLD_SQ; + + let caret_moved = match (self.caret, self.prev_caret) { + (Some(c), Some(p)) => c.x != p.x || c.y != p.y, + (Some(_), None) => true, + _ => false, + }; + + if intentional { + self.mouse_move_ticks += 1; + self.mouse_still_ticks = 0; + } else { + self.mouse_move_ticks = 0; + self.mouse_still_ticks += 1; + } + + if self.mouse_move_ticks >= MOUSE_TAKEOVER_TICKS { + self.caret_active = false; + self.mouse_move_ticks = 0; + } + // Re-engage caret once mouse has been still long enough. + if self.mouse_still_ticks >= CARET_REFOCUS_TICKS && self.caret.is_some() { + self.caret_active = true; + } + if caret_moved { + self.caret_active = true; + self.mouse_move_ticks = 0; + } + + self.prev_cursor = self.cursor; + self.prev_caret = self.caret; + } + + pub fn update_viewport( + &mut self, + src_w: f32, + src_h: f32, + screen_w: f32, + screen_h: f32, + just_activated: bool, + ) { + let (fx, fy) = self.focus_point(); + let zoom_changed = (self.zoom - self.prev_zoom).abs() > f32::EPSILON; + self.prev_zoom = self.zoom; + + if just_activated { + self.viewport_x = (fx - src_w / 2.0).clamp(0.0, (screen_w - src_w).max(0.0)); + self.viewport_y = (fy - src_h / 2.0).clamp(0.0, (screen_h - src_h).max(0.0)); + return; + } + + let (target_x, target_y) = if self.caret_active { + // Centre on caret while typing. + let tx = (fx - src_w / 2.0).clamp(0.0, (screen_w - src_w).max(0.0)); + let ty = (fy - src_h / 2.0).clamp(0.0, (screen_h - src_h).max(0.0)); + (tx, ty) + } else if zoom_changed { + let tx = (fx - src_w / 2.0).clamp(0.0, (screen_w - src_w).max(0.0)); + let ty = (fy - src_h / 2.0).clamp(0.0, (screen_h - src_h).max(0.0)); + (tx, ty) + } else { + let mx = EDGE_MARGIN * src_w; + let my = EDGE_MARGIN * src_h; + let tx = self + .viewport_x + .clamp(fx - src_w + mx, fx - mx) + .clamp(0.0, (screen_w - src_w).max(0.0)); + let ty = self + .viewport_y + .clamp(fy - src_h + my, fy - my) + .clamp(0.0, (screen_h - src_h).max(0.0)); + (tx, ty) + }; + + self.viewport_x += (target_x - self.viewport_x) * PAN_ALPHA; + self.viewport_y += (target_y - self.viewport_y) * PAN_ALPHA; + + // Hard-clamp every tick so the viewport never overshoots screen bounds + // even while the lerp is still catching up (e.g. fast zoom-out). + self.viewport_x = self.viewport_x.clamp(0.0, (screen_w - src_w).max(0.0)); + self.viewport_y = self.viewport_y.clamp(0.0, (screen_h - src_h).max(0.0)); + } + + pub fn zoom_in(&mut self) { + if !self.active { + self.target_zoom = ZOOM_MIN; + self.zoom = 1.0; + self.active = true; + } else { + self.target_zoom = (self.target_zoom + ZOOM_STEP).min(ZOOM_MAX); + } + } + + pub fn zoom_out(&mut self) { + if !self.active { + return; + } + self.target_zoom -= ZOOM_STEP; + if self.target_zoom < ZOOM_MIN { + self.target_zoom = 1.0; + } + } + + fn focus_point(&self) -> (f32, f32) { + if self.caret_active { + if let Some(c) = self.caret { + return (c.x as f32, c.y as f32); + } + } + (self.smooth_cursor_x, self.smooth_cursor_y) + } +} diff --git a/src/tray.rs b/src/tray.rs new file mode 100644 index 0000000..cde1808 --- /dev/null +++ b/src/tray.rs @@ -0,0 +1,70 @@ +use tray_icon::{ + menu::{Menu, MenuEvent, MenuId, MenuItem}, + TrayIcon, TrayIconBuilder, +}; + +pub struct Tray { + _icon: TrayIcon, + exit_id: MenuId, +} + +impl Tray { + pub fn new() -> Self { + let toggle_item = MenuItem::new("Toggle", true, None); + let exit_item = MenuItem::new("Exit", true, None); + + let exit_id = exit_item.id().clone(); + + let menu = Menu::new(); + menu.append(&toggle_item) + .expect("Failed to append toggle item"); + menu.append(&exit_item).expect("Failed to append exit item"); + + let icon = make_icon(); + + let tray = TrayIconBuilder::new() + .with_menu(Box::new(menu)) + .with_tooltip("iwaku magnifier") + .with_icon(icon) + .build() + .expect("Failed to build tray icon"); + + Self { + _icon: tray, + exit_id, + } + } + + /// Returns `true` if the app should exit. + pub fn process_events(&self) -> bool { + if let Ok(event) = MenuEvent::receiver().try_recv() { + if event.id == self.exit_id { + return true; + } + } + false + } +} + +fn make_icon() -> tray_icon::Icon { + const SIZE: usize = 16; + let mut rgba = vec![0u8; SIZE * SIZE * 4]; + + for y in 0..SIZE { + for x in 0..SIZE { + let idx = (y * SIZE + x) * 4; + let cx = x as f32 - 7.5; + let cy = y as f32 - 7.5; + let dist = (cx * cx + cy * cy).sqrt(); + if (dist - 5.5).abs() < 1.5 { + rgba[idx] = 255; + rgba[idx + 1] = 255; + rgba[idx + 2] = 255; + rgba[idx + 3] = 255; + } + } + } + + tray_icon::Icon::from_rgba(rgba, SIZE as u32, SIZE as u32) + .expect("Failed to create tray icon from RGBA") +}