diff --git a/Cargo.lock b/Cargo.lock index c2362c7..675194a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3,1646 +3,148 @@ version = 4 [[package]] -name = "adler2" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" - -[[package]] -name = "aho-corasick" -version = "1.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" -dependencies = [ - "memchr", -] - -[[package]] -name = "anyhow" -version = "1.0.102" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" - -[[package]] -name = "autocfg" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" - -[[package]] -name = "base64" -version = "0.21.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" - -[[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.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" - -[[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" - -[[package]] -name = "byteorder" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" - -[[package]] -name = "bytes" -version = "1.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" - -[[package]] -name = "cc" -version = "1.2.61" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d16d90359e986641506914ba71350897565610e87ce0ad9e6f28569db3dd5c6d" -dependencies = [ - "find-msvc-tools", - "shlex", -] - -[[package]] -name = "cfg-if" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" - -[[package]] -name = "color_quant" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" - -[[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 = "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 = "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 = "displaydoc" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "either" -version = "1.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" - -[[package]] -name = "encoding_rs" -version = "0.8.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "env_logger" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cd405aab171cb85d6735e5c8d9db038c17d3ca007a4d2c25f337935c3d90580" -dependencies = [ - "humantime", - "is-terminal", - "log", - "regex", - "termcolor", -] - -[[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 = "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.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" - -[[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 = "flate2" -version = "1.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" -dependencies = [ - "crc32fast", - "miniz_oxide", -] - -[[package]] -name = "fnv" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" - -[[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.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" -dependencies = [ - "foreign-types-shared", -] - -[[package]] -name = "foreign-types-shared" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" - -[[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-channel" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" -dependencies = [ - "futures-core", -] - -[[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-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-task", - "memchr", - "pin-project-lite", - "slab", -] - -[[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.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" -dependencies = [ - "cfg-if", - "libc", - "r-efi", - "wasip2", - "wasip3", -] - -[[package]] -name = "gif" -version = "0.13.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ae047235e33e2829703574b54fdec96bfbad892062d97fed2f76022287de61b" -dependencies = [ - "color_quant", - "weezl", -] - -[[package]] -name = "h2" -version = "0.3.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0beca50380b1fc32983fc1cb4587bfa4bb9e78fc259aad4a0032d2080309222d" -dependencies = [ - "bytes", - "fnv", - "futures-core", - "futures-sink", - "futures-util", - "http", - "indexmap", - "slab", - "tokio", - "tokio-util", - "tracing", -] - -[[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.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" - -[[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 = "http" -version = "0.2.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" -dependencies = [ - "bytes", - "fnv", - "itoa", -] - -[[package]] -name = "http-body" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" -dependencies = [ - "bytes", - "http", - "pin-project-lite", -] - -[[package]] -name = "httparse" -version = "1.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" - -[[package]] -name = "httpdate" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" - -[[package]] -name = "humantime" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "135b12329e5e3ce057a9f972339ea52bc954fe1e9358ef27f95e89716fbc5424" - -[[package]] -name = "hyper" -version = "0.14.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41dfc780fdec9373c01bae43289ea34c972e40ee3c9f6b3c8801a35f35586ce7" -dependencies = [ - "bytes", - "futures-channel", - "futures-core", - "futures-util", - "h2", - "http", - "http-body", - "httparse", - "httpdate", - "itoa", - "pin-project-lite", - "socket2 0.5.10", - "tokio", - "tower-service", - "tracing", - "want", -] - -[[package]] -name = "hyper-tls" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" -dependencies = [ - "bytes", - "hyper", - "native-tls", - "tokio", - "tokio-native-tls", -] - -[[package]] -name = "icu_collections" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" -dependencies = [ - "displaydoc", - "potential_utf", - "utf8_iter", - "yoke", - "zerofrom", - "zerovec", -] - -[[package]] -name = "icu_locale_core" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" -dependencies = [ - "displaydoc", - "litemap", - "tinystr", - "writeable", - "zerovec", -] - -[[package]] -name = "icu_normalizer" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" -dependencies = [ - "icu_collections", - "icu_normalizer_data", - "icu_properties", - "icu_provider", - "smallvec", - "zerovec", -] - -[[package]] -name = "icu_normalizer_data" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" - -[[package]] -name = "icu_properties" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" -dependencies = [ - "icu_collections", - "icu_locale_core", - "icu_properties_data", - "icu_provider", - "zerotrie", - "zerovec", -] - -[[package]] -name = "icu_properties_data" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" - -[[package]] -name = "icu_provider" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" -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.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" -dependencies = [ - "icu_normalizer", - "icu_properties", -] - -[[package]] -name = "image" -version = "0.24.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5690139d2f55868e080017335e4b94cb7414274c74f1669c84fb5feba2c9f69d" -dependencies = [ - "bytemuck", - "byteorder", - "color_quant", - "exr", - "gif", - "jpeg-decoder", - "num-traits", - "png", - "qoi", - "tiff", -] - -[[package]] -name = "indexmap" -version = "2.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" -dependencies = [ - "equivalent", - "hashbrown 0.17.0", - "serde", - "serde_core", -] - -[[package]] -name = "ipnet" -version = "2.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" - -[[package]] -name = "is-terminal" -version = "0.4.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" -dependencies = [ - "hermit-abi", - "libc", - "windows-sys 0.61.2", -] - -[[package]] -name = "itoa" -version = "1.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" - -[[package]] -name = "jpeg-decoder" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00810f1d8b74be64b13dbf3db89ac67740615d6c891f0e7b6179326533011a07" -dependencies = [ - "rayon", -] - -[[package]] -name = "js-sys" -version = "0.3.97" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1840c94c045fbcf8ba2812c95db44499f7c64910a912551aaaa541decebcacf" -dependencies = [ - "cfg-if", - "futures-util", - "once_cell", - "wasm-bindgen", -] - -[[package]] -name = "lazy_static" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" - -[[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.186" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" - -[[package]] -name = "libredox" -version = "0.1.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e02f3bb43d335493c96bf3fd3a321600bf6bd07ed34bc64118e9293bdffea46c" -dependencies = [ - "libc", -] - -[[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.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" - -[[package]] -name = "log" -version = "0.4.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" - -[[package]] -name = "memchr" -version = "2.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" - -[[package]] -name = "mime" -version = "0.3.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" - -[[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 = "mio" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" -dependencies = [ - "libc", - "wasi", - "windows-sys 0.61.2", -] - -[[package]] -name = "native-tls" -version = "0.2.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "465500e14ea162429d264d44189adc38b199b62b1c21eea9f69e4b73cb03bbf2" -dependencies = [ - "libc", - "log", - "openssl", - "openssl-probe", - "openssl-sys", - "schannel", - "security-framework", - "security-framework-sys", - "tempfile", -] - -[[package]] -name = "num-traits" -version = "0.2.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" -dependencies = [ - "autocfg", -] - -[[package]] -name = "once_cell" -version = "1.21.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" - -[[package]] -name = "openssl" -version = "0.10.79" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf0b434746ee2832f4f0baf10137e1cabb18cbe6912c69e2e33263c45250f542" -dependencies = [ - "bitflags 2.11.1", - "cfg-if", - "foreign-types", - "libc", - "openssl-macros", - "openssl-sys", -] - -[[package]] -name = "openssl-macros" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "openssl-probe" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" - -[[package]] -name = "openssl-sys" -version = "0.9.115" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "158fe5b292746440aa6e7a7e690e55aeb72d41505e2804c23c6973ad0e9c9781" -dependencies = [ - "cc", - "libc", - "pkg-config", - "vcpkg", -] - -[[package]] -name = "option-ext" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" - -[[package]] -name = "pausecat" -version = "1.1.0" -dependencies = [ - "base64", - "dirs", - "env_logger", - "image", - "lazy_static", - "log", - "reqwest", - "semver", - "serde", - "serde_json", - "thiserror 1.0.69", - "webview2-com", - "windows", - "winreg 0.51.0", - "winres", -] - -[[package]] -name = "percent-encoding" -version = "2.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" - -[[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" -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 = "potential_utf" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" -dependencies = [ - "zerovec", -] - -[[package]] -name = "prettyplease" -version = "0.2.37" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" -dependencies = [ - "proc-macro2", - "syn", -] - -[[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 = "qoi" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f6d64c71eb498fe9eae14ce4ec935c555749aef511cca85b5568910d6e48001" -dependencies = [ - "bytemuck", -] - -[[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 = "6.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" - -[[package]] -name = "rayon" -version = "1.12.0" -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 = "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 = "regex" -version = "1.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" -dependencies = [ - "aho-corasick", - "memchr", - "regex-automata", - "regex-syntax", -] - -[[package]] -name = "regex-automata" -version = "0.4.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" -dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax", -] - -[[package]] -name = "regex-syntax" -version = "0.8.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" - -[[package]] -name = "reqwest" -version = "0.11.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd67538700a17451e7cba03ac727fb961abb7607553461627b97de0b89cf4a62" -dependencies = [ - "base64", - "bytes", - "encoding_rs", - "futures-core", - "futures-util", - "h2", - "http", - "http-body", - "hyper", - "hyper-tls", - "ipnet", - "js-sys", - "log", - "mime", - "native-tls", - "once_cell", - "percent-encoding", - "pin-project-lite", - "rustls-pemfile", - "serde", - "serde_json", - "serde_urlencoded", - "sync_wrapper", - "system-configuration", - "tokio", - "tokio-native-tls", - "tower-service", - "url", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", - "winreg 0.50.0", -] - -[[package]] -name = "rustix" -version = "1.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" -dependencies = [ - "bitflags 2.11.1", - "errno", - "libc", - "linux-raw-sys", - "windows-sys 0.61.2", -] - -[[package]] -name = "rustls-pemfile" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" -dependencies = [ - "base64", -] - -[[package]] -name = "rustversion" -version = "1.0.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" - -[[package]] -name = "ryu" -version = "1.0.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" - -[[package]] -name = "schannel" -version = "0.1.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" -dependencies = [ - "windows-sys 0.61.2", -] - -[[package]] -name = "security-framework" -version = "3.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" -dependencies = [ - "bitflags 2.11.1", - "core-foundation 0.10.1", - "core-foundation-sys", - "libc", - "security-framework-sys", -] - -[[package]] -name = "security-framework-sys" -version = "2.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" -dependencies = [ - "core-foundation-sys", - "libc", -] - -[[package]] -name = "semver" -version = "1.0.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" - -[[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" +name = "itoa" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" -dependencies = [ - "serde_derive", -] +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] -name = "serde_derive" -version = "1.0.228" +name = "log" +version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" [[package]] -name = "serde_json" -version = "1.0.149" +name = "memchr" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" -dependencies = [ - "itoa", - "memchr", - "serde", - "serde_core", - "zmij", -] +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" [[package]] -name = "serde_urlencoded" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +name = "pausecat" +version = "1.1.0" dependencies = [ - "form_urlencoded", - "itoa", - "ryu", + "log", + "semver", "serde", + "serde_json", + "webview2-com", + "windows", + "winres", ] [[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" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" - -[[package]] -name = "slab" -version = "0.4.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" - -[[package]] -name = "smallvec" -version = "1.15.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" - -[[package]] -name = "socket2" -version = "0.5.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" -dependencies = [ - "libc", - "windows-sys 0.52.0", -] - -[[package]] -name = "socket2" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" -dependencies = [ - "libc", - "windows-sys 0.61.2", -] - -[[package]] -name = "stable_deref_trait" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" - -[[package]] -name = "syn" -version = "2.0.117" +name = "proc-macro2" +version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" dependencies = [ - "proc-macro2", - "quote", "unicode-ident", ] [[package]] -name = "sync_wrapper" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" - -[[package]] -name = "synstructure" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "system-configuration" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" -dependencies = [ - "bitflags 1.3.2", - "core-foundation 0.9.4", - "system-configuration-sys", -] - -[[package]] -name = "system-configuration-sys" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" -dependencies = [ - "core-foundation-sys", - "libc", -] - -[[package]] -name = "tempfile" -version = "3.27.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" -dependencies = [ - "fastrand", - "getrandom 0.4.2", - "once_cell", - "rustix", - "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", -] - -[[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", -] - -[[package]] -name = "tiff" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba1310fcea54c6a9a4fd1aad794ecc02c31682f6bfbecdf460bf19533eed1e3e" -dependencies = [ - "flate2", - "jpeg-decoder", - "weezl", -] - -[[package]] -name = "tinystr" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" -dependencies = [ - "displaydoc", - "zerovec", -] - -[[package]] -name = "tokio" -version = "1.52.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b67dee974fe86fd92cc45b7a95fdd2f99a36a6d7b0d431a231178d3d670bbcc6" -dependencies = [ - "bytes", - "libc", - "mio", - "pin-project-lite", - "socket2 0.6.3", - "windows-sys 0.61.2", -] - -[[package]] -name = "tokio-native-tls" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" -dependencies = [ - "native-tls", - "tokio", -] - -[[package]] -name = "tokio-util" -version = "0.7.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" -dependencies = [ - "bytes", - "futures-core", - "futures-sink", - "pin-project-lite", - "tokio", -] - -[[package]] -name = "toml" -version = "0.5.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" -dependencies = [ - "serde", -] - -[[package]] -name = "tower-service" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" - -[[package]] -name = "tracing" -version = "0.1.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" -dependencies = [ - "pin-project-lite", - "tracing-core", -] - -[[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 = "try-lock" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" - -[[package]] -name = "unicode-ident" -version = "1.0.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" - -[[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 = "vcpkg" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" - -[[package]] -name = "want" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" -dependencies = [ - "try-lock", -] - -[[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.3+wasi-0.2.9" +name = "quote" +version = "1.0.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" dependencies = [ - "wit-bindgen 0.57.1", + "proc-macro2", ] [[package]] -name = "wasip3" -version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +name = "semver" +version = "1.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" -dependencies = [ - "wit-bindgen 0.51.0", -] +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" [[package]] -name = "wasm-bindgen" -version = "0.2.120" +name = "serde" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df52b6d9b87e0c74c9edfa1eb2d9bf85e5d63515474513aa50fa181b3c4f5db1" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" dependencies = [ - "cfg-if", - "once_cell", - "rustversion", - "wasm-bindgen-macro", - "wasm-bindgen-shared", + "serde_core", + "serde_derive", ] [[package]] -name = "wasm-bindgen-futures" -version = "0.4.70" +name = "serde_core" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af934872acec734c2d80e6617bbb5ff4f12b052dd8e6332b0817bce889516084" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" dependencies = [ - "js-sys", - "wasm-bindgen", + "serde_derive", ] [[package]] -name = "wasm-bindgen-macro" -version = "0.2.120" +name = "serde_derive" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78b1041f495fb322e64aca85f5756b2172e35cd459376e67f2a6c9dffcedb103" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ + "proc-macro2", "quote", - "wasm-bindgen-macro-support", + "syn", ] [[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.120" +name = "serde_json" +version = "1.0.149" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9dcd0ff20416988a18ac686d4d4d0f6aae9ebf08a389ff5d29012b05af2a1b41" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" dependencies = [ - "bumpalo", - "proc-macro2", - "quote", - "syn", - "wasm-bindgen-shared", + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", ] [[package]] -name = "wasm-bindgen-shared" -version = "0.2.120" +name = "syn" +version = "2.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49757b3c82ebf16c57d69365a142940b384176c24df52a087fb748e2085359ea" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" dependencies = [ + "proc-macro2", + "quote", "unicode-ident", ] [[package]] -name = "wasm-encoder" -version = "0.244.0" +name = "thiserror" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" dependencies = [ - "leb128fmt", - "wasmparser", + "thiserror-impl", ] [[package]] -name = "wasm-metadata" -version = "0.244.0" +name = "thiserror-impl" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ - "anyhow", - "indexmap", - "wasm-encoder", - "wasmparser", + "proc-macro2", + "quote", + "syn", ] [[package]] -name = "wasmparser" -version = "0.244.0" +name = "toml" +version = "0.5.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" dependencies = [ - "bitflags 2.11.1", - "hashbrown 0.15.5", - "indexmap", - "semver", + "serde", ] [[package]] -name = "web-sys" -version = "0.3.97" +name = "unicode-ident" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2eadbac71025cd7b0834f20d1fe8472e8495821b4e9801eb0a60bd1f19827602" -dependencies = [ - "js-sys", - "wasm-bindgen", -] +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] name = "webview2-com" @@ -1673,26 +175,11 @@ version = "0.39.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b3a07132775117d6065853d9d1178157b8c90e228de47129d6bce2c7edebedfb" dependencies = [ - "thiserror 2.0.18", + "thiserror", "windows", "windows-core", ] -[[package]] -name = "weezl" -version = "0.1.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88" - -[[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 = "windows" version = "0.62.2" @@ -1794,64 +281,6 @@ dependencies = [ "windows-link", ] -[[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.61.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" -dependencies = [ - "windows-link", -] - -[[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-threading" version = "0.2.1" @@ -1861,116 +290,6 @@ dependencies = [ "windows-link", ] -[[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 = "winreg" -version = "0.50.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" -dependencies = [ - "cfg-if", - "windows-sys 0.48.0", -] - -[[package]] -name = "winreg" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "937f3df7948156640f46aacef17a70db0de5917bda9c92b0f751f3a955b588fc" -dependencies = [ - "cfg-if", - "windows-sys 0.48.0", -] - [[package]] name = "winres" version = "0.1.12" @@ -1980,214 +299,8 @@ dependencies = [ "toml", ] -[[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" -version = "0.57.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" - -[[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", - "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", - "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.1", - "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 = "writeable" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" - -[[package]] -name = "yoke" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca" -dependencies = [ - "stable_deref_trait", - "yoke-derive", - "zerofrom", -] - -[[package]] -name = "yoke-derive" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" -dependencies = [ - "proc-macro2", - "quote", - "syn", - "synstructure", -] - -[[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" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "zerofrom" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69faa1f2a1ea75661980b013019ed6687ed0e83d069bc1114e2cc74c6c04c4df" -dependencies = [ - "zerofrom-derive", -] - -[[package]] -name = "zerofrom-derive" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" -dependencies = [ - "proc-macro2", - "quote", - "syn", - "synstructure", -] - -[[package]] -name = "zerotrie" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" -dependencies = [ - "displaydoc", - "yoke", - "zerofrom", -] - -[[package]] -name = "zerovec" -version = "0.11.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" -dependencies = [ - "yoke", - "zerofrom", - "zerovec-derive", -] - -[[package]] -name = "zerovec-derive" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "zmij" version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" - -[[package]] -name = "zune-inflate" -version = "0.2.54" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73ab332fe2f6680068f3582b16a24f90ad7096d5d39b974d1c0aff0125116f02" -dependencies = [ - "simd-adler32", -] diff --git a/Cargo.toml b/Cargo.toml index e5290d5..399aad6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,27 +25,15 @@ windows = { version = "0.62", features = [ "Win32_System_Variant", "Win32_System_Memory", "Win32_System_Com_StructuredStorage", - "Win32_Graphics_Direct2D", - "Win32_Graphics_Direct2D_Common", - "Win32_Graphics_Direct3D11", - "Win32_Graphics_Dxgi", - "Win32_Graphics_Direct3D", - "Media", + "Win32_Networking_WinHttp", + "Win32_Storage_FileSystem", "Media_Control", "Foundation", ] } webview2-com = "0.39" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" -image = "0.24" -winreg = "0.51" -dirs = "5.0" -log = "0.4" -env_logger = "0.10" -thiserror = "1.0" -lazy_static = "1.4" -base64 = "0.21" -reqwest = { version = "0.11", features = ["blocking", "json"] } +log = { version = "0.4", features = ["std"] } semver = "1.0" [build-dependencies] diff --git a/src/app.rs b/src/app.rs index 416a9f3..7f5ce79 100644 --- a/src/app.rs +++ b/src/app.rs @@ -3,8 +3,8 @@ use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::mpsc; use std::thread; use windows::Win32::UI::WindowsAndMessaging::*; -use windows::Win32::System::Com::*; use windows::Win32::Foundation::*; +use windows::Win32::System::Threading::GetCurrentThreadId; use crate::settings::Settings; use crate::tray::TrayIcon; use crate::events::AppEvent; @@ -12,6 +12,17 @@ use crate::timer; use crate::overlay::{OverlayWindow, capture, blur, webview_env}; use crate::settings_ui::SettingsWindow; +pub const WM_APP_EVENT: u32 = WM_USER + 1; +static mut MAIN_THREAD_ID: u32 = 0; + +pub fn wakeup_main_thread() { + unsafe { + if MAIN_THREAD_ID != 0 { + let _ = PostThreadMessageW(MAIN_THREAD_ID, WM_APP_EVENT, WPARAM(0), LPARAM(0)); + } + } +} + pub struct App { pub settings: Arc>, pub paused: Arc, @@ -33,6 +44,8 @@ impl App { let paused = Arc::new(AtomicBool::new(false)); let session_paused = Arc::new(AtomicBool::new(false)); let is_dark_mode = crate::system::is_dark_mode(); + + unsafe { MAIN_THREAD_ID = GetCurrentThreadId(); } Self { settings, @@ -50,10 +63,6 @@ impl App { } pub fn init(&mut self) -> windows::core::Result<()> { - unsafe { - let _ = CoInitializeEx(None, COINIT_APARTMENTTHREADED); - } - let _ = webview_env::init_global_env(); let _ = self.settings.read().unwrap().update_autostart(); crate::system::set_tray_menu_theme(self.is_dark_mode); @@ -81,6 +90,25 @@ impl App { Ok(()) } + pub fn run(&mut self) -> windows::core::Result<()> { + self.init()?; + + unsafe { + let mut msg = MSG::default(); + // Pulse every 1s as a secondary safety, though WM_APP_EVENT is primary + let _ = SetTimer(None, 1, 1000, None); + + while GetMessageW(&mut msg, None, 0, 0).as_bool() { + if msg.message == WM_APP_EVENT || (msg.message == WM_TIMER && msg.wParam.0 == 1) { + self.drain_events(); + } + let _ = TranslateMessage(&msg); + DispatchMessageW(&msg); + } + } + Ok(()) + } + pub fn handle_event(&mut self, event: AppEvent) { match event { AppEvent::ShowOverlay => { @@ -94,6 +122,8 @@ impl App { self.resume_media(); } self.reminder_overlay = None; + blur::flush_buffers(); + capture::flush_buffer(); } AppEvent::SettingsClosed => { self.settings_window = None; @@ -131,8 +161,14 @@ impl App { let event_tx = self.event_tx.clone(); thread::spawn(move || { match crate::updater::check_for_updates() { - Ok(info) => { let _ = event_tx.send(AppEvent::UpdateStatus(info)); } - Err(e) => { let _ = event_tx.send(AppEvent::UpdateError(e.to_string())); } + Ok(info) => { + let _ = event_tx.send(AppEvent::UpdateStatus(info)); + wakeup_main_thread(); + } + Err(e) => { + let _ = event_tx.send(AppEvent::UpdateError(e.to_string())); + wakeup_main_thread(); + } } }); } @@ -146,6 +182,7 @@ impl App { thread::spawn(move || { if let Err(e) = crate::updater::download_and_install(event_tx.clone()) { let _ = event_tx.send(AppEvent::UpdateError(e.to_string())); + wakeup_main_thread(); } }); } @@ -206,7 +243,7 @@ impl App { } fn show_overlay_optimized(&mut self) { - let (width, height, data) = if let Some(bg) = self.pre_captured_bg.read().unwrap().clone() { + let (blurred_width, blurred_height, data) = if let Some(bg) = self.pre_captured_bg.read().unwrap().clone() { bg } else { if let Ok(captured) = capture::capture_virtual_screen() { @@ -215,12 +252,17 @@ impl App { } else { return; } }; - let current_settings = self.settings.read().unwrap().clone(); - if let Ok(overlay) = OverlayWindow::new(self.event_tx.clone(), width, height, data, current_settings) { - overlay.update_theme(self.is_dark_mode); - crate::system::apply_immersive_dark_mode(overlay.hwnd, self.is_dark_mode); - overlay.fade_in(); - self.reminder_overlay = Some(overlay); + unsafe { + let screen_width = GetSystemMetrics(SM_CXVIRTUALSCREEN); + let screen_height = GetSystemMetrics(SM_CYVIRTUALSCREEN); + let current_settings = self.settings.read().unwrap().clone(); + + if let Ok(overlay) = OverlayWindow::new(self.event_tx.clone(), screen_width, screen_height, blurred_width, blurred_height, data, current_settings) { + overlay.update_theme(self.is_dark_mode); + crate::system::apply_immersive_dark_mode(overlay.hwnd, self.is_dark_mode); + overlay.fade_in(); + self.reminder_overlay = Some(overlay); + } } let mut lock = self.pre_captured_bg.write().unwrap(); @@ -261,12 +303,8 @@ mod internal_tests { #[test] fn test_app_logic_methods() { let mut app = App::new(); - - // Test media logic (smoke) app.pause_media(); app.resume_media(); - - // Test handle_event with variants that don't trigger real UI popups app.handle_event(AppEvent::TogglePause); app.handle_event(AppEvent::SettingsClosed); app.handle_event(AppEvent::SessionLocked); @@ -283,16 +321,9 @@ mod internal_tests { app.handle_event(AppEvent::CheckForUpdates); app.handle_event(AppEvent::StartUpdate); app.handle_event(AppEvent::Quit); - - // Test media logic with mock-ish calls app.pause_media(); app.resume_media(); - - // Test init (smoke) - // Now safe because windows are hidden in tests let _ = app.init(); - - // Test draining app.event_tx.send(AppEvent::UserDismissed).unwrap(); app.drain_events(); } @@ -300,21 +331,15 @@ mod internal_tests { #[test] fn test_app_window_management_logic() { let mut app = App::new(); - - // Test overlay closing logic use windows::Win32::Foundation::HWND; use crate::overlay::OverlayWindow; app.reminder_overlay = Some(OverlayWindow { hwnd: HWND(1 as *mut _) }); app.handle_event(AppEvent::HideOverlay); assert!(app.reminder_overlay.is_none()); - - // Test settings window closing logic use crate::settings_ui::SettingsWindow; app.settings_window = Some(SettingsWindow { hwnd: HWND(1 as *mut _) }); app.handle_event(AppEvent::SettingsClosed); assert!(app.settings_window.is_none()); - - // Test session events with dummy windows app.settings_window = Some(SettingsWindow { hwnd: HWND(1 as *mut _) }); app.handle_event(AppEvent::SessionLocked); app.handle_event(AppEvent::SessionUnlocked); diff --git a/src/main.rs b/src/main.rs index 8135124..fbcf963 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,12 +1,31 @@ #![windows_subsystem = "windows"] -use windows::Win32::UI::WindowsAndMessaging::*; -use windows::Win32::Foundation::*; use pausecat::app::App; +use pausecat::settings::Settings; + +struct SimpleFileLogger { + file: std::sync::Mutex, +} + +impl log::Log for SimpleFileLogger { + fn enabled(&self, metadata: &log::Metadata) -> bool { + metadata.level() <= log::Level::Info + } + fn log(&self, record: &log::Record) { + if self.enabled(record.metadata()) { + if let Ok(mut file) = self.file.lock() { + use std::io::Write; + let _ = writeln!(file, "[{}] {}", + record.level(), + record.args()); + } + } + } + fn flush(&self) {} +} fn setup_logging() -> windows::core::Result<()> { - let mut path = dirs::config_dir().unwrap_or_else(|| std::path::PathBuf::from(".")); - path.push("PauseCat"); + let mut path = Settings::get_config_dir(); let _ = std::fs::create_dir_all(&path); path.push("app.log"); @@ -16,97 +35,31 @@ fn setup_logging() -> windows::core::Result<()> { .open(&path) .map_err(|e| windows::core::Error::from_hresult(windows::core::HRESULT(e.raw_os_error().unwrap_or(-1) as i32)))?; - let target = Box::new(file); - env_logger::Builder::from_default_env() - .target(env_logger::Target::Pipe(target)) - .filter_level(log::LevelFilter::Info) - .init(); + let logger = SimpleFileLogger { file: std::sync::Mutex::new(file) }; + let _ = log::set_boxed_logger(Box::new(logger)); + log::set_max_level(log::LevelFilter::Info); - log::info!("PauseCat started (Optimized)"); + log::info!("PauseCat started (Ultra-Optimized)"); Ok(()) } -fn check_webview2() -> windows::core::Result { - use webview2_com::Microsoft::Web::WebView2::Win32::*; - unsafe { - let mut version = windows::core::PWSTR::null(); - let result = GetAvailableCoreWebView2BrowserVersionString(windows::core::PCWSTR::null(), &mut version); - let exists = result.is_ok(); - if !version.is_null() { - windows::Win32::System::Com::CoTaskMemFree(Some(version.0 as *const _)); - } - Ok(exists) - } -} - -pub fn is_settings_mode() -> bool { - std::env::args().any(|arg| arg == "--settings") -} - fn main() -> windows::core::Result<()> { - let _ = setup_logging(); - - unsafe { - use windows::Win32::System::Threading::CreateMutexW; - let _handle = CreateMutexW(None, true, windows::core::w!("Global\\PauseCatSingleInstanceMutex")); - if GetLastError() == ERROR_ALREADY_EXISTS { - return Ok(()); - } - } - - match check_webview2() { - Ok(true) => log::info!("WebView2 found."), - _ => { - unsafe { - MessageBoxW( - None, - windows::core::w!("PauseCat requires WebView2 Runtime."), - windows::core::w!("Error"), - MB_OK | MB_ICONERROR, - ); - } - return Ok(()); - } - } - - let mut app = App::new(); - if let Err(e) = app.init() { - return Err(e); - } - - if is_settings_mode() { - app.handle_event(pausecat::events::AppEvent::OpenSettings); - } - unsafe { - let _ = SetTimer(None, 1, 100, None); - - let mut msg = MSG::default(); - while GetMessageW(&mut msg, None, 0, 0).into() { - let _ = TranslateMessage(&msg); - DispatchMessageW(&msg); - app.drain_events(); - } + let _ = windows::Win32::System::Com::CoInitializeEx(None, windows::Win32::System::Com::COINIT_APARTMENTTHREADED); + let _ = setup_logging(); + let mut app = App::new(); + app.run()?; + windows::Win32::System::Com::CoUninitialize(); } - Ok(()) } #[cfg(test)] -mod internal_tests { +mod tests { use super::*; #[test] - fn test_main_helpers_smoke() { - let _ = check_webview2(); - let _ = setup_logging(); - - // Mock args for is_settings_mode - let _ = is_settings_mode(); - } - - #[test] - fn test_mutex_logic_smoke() { + fn test_main_startup_smoke() { unsafe { use windows::Win32::System::Threading::CreateMutexW; let mutex_name = windows::core::w!("Global\\PauseCatTestMutex"); diff --git a/src/overlay/blur.rs b/src/overlay/blur.rs index 31c5f40..eeae23f 100644 --- a/src/overlay/blur.rs +++ b/src/overlay/blur.rs @@ -26,11 +26,42 @@ fn get_pixel(data: &[u8], width: usize, _height: usize, x: usize, y: usize) -> ( (data[idx] as f32, data[idx+1] as f32, data[idx+2] as f32) } -pub fn blur(src: &[u8], width: usize, height: usize, sigma: f32) -> Vec { +use std::sync::{Mutex, OnceLock}; + +static BLUR_TEMP_BUFFER: OnceLock>> = OnceLock::new(); +static BLUR_DST_BUFFER: OnceLock>> = OnceLock::new(); + +fn get_temp_buffer() -> &'static Mutex> { + BLUR_TEMP_BUFFER.get_or_init(|| Mutex::new(Vec::new())) +} + +fn get_dst_buffer() -> &'static Mutex> { + BLUR_DST_BUFFER.get_or_init(|| Mutex::new(Vec::new())) +} + +static KERNEL_CACHE: OnceLock)>>> = OnceLock::new(); + +fn get_kernel(sigma: f32) -> Vec { + let mut lock = KERNEL_CACHE.get_or_init(|| Mutex::new(None)).lock().unwrap(); + if let Some((cached_sigma, ref kernel)) = *lock { + if (cached_sigma - sigma).abs() < f32::EPSILON { + return kernel.clone(); + } + } let kernel = generate_gaussian_kernel(sigma); + *lock = Some((sigma, kernel.clone())); + kernel +} + +pub fn blur(src: &[u8], width: usize, height: usize, sigma: f32) -> Vec { + let kernel = get_kernel(sigma); let radius = kernel.len() / 2; - let mut temp = vec![0u8; src.len()]; - let mut dst = vec![0u8; src.len()]; + + let mut temp_lock = get_temp_buffer().lock().unwrap(); + let mut dst_lock = get_dst_buffer().lock().unwrap(); + + if temp_lock.len() < src.len() { temp_lock.resize(src.len(), 0); } + if dst_lock.len() < src.len() { dst_lock.resize(src.len(), 0); } // Horizontal pass for y in 0..height { @@ -44,10 +75,10 @@ pub fn blur(src: &[u8], width: usize, height: usize, sigma: f32) -> Vec { b += pb * weight; } let idx = (y * width + x) * 4; - temp[idx] = r as u8; - temp[idx+1] = g as u8; - temp[idx+2] = b as u8; - temp[idx+3] = 255; + temp_lock[idx] = r as u8; + temp_lock[idx+1] = g as u8; + temp_lock[idx+2] = b as u8; + temp_lock[idx+3] = 255; } } @@ -57,18 +88,33 @@ pub fn blur(src: &[u8], width: usize, height: usize, sigma: f32) -> Vec { let (mut r, mut g, mut b) = (0.0f32, 0.0f32, 0.0f32); for (k, &weight) in kernel.iter().enumerate() { let iy = (y as isize + k as isize - radius as isize).clamp(0, height as isize - 1) as usize; - let (pr, pg, pb) = get_pixel(&temp, width, height, x, iy); + let (pr, pg, pb) = get_pixel(&temp_lock, width, height, x, iy); r += pr * weight; g += pg * weight; b += pb * weight; } let idx = (y * width + x) * 4; - dst[idx] = r as u8; - dst[idx+1] = g as u8; - dst[idx+2] = b as u8; - dst[idx+3] = 255; + dst_lock[idx] = r as u8; + dst_lock[idx+1] = g as u8; + dst_lock[idx+2] = b as u8; + dst_lock[idx+3] = 255; } } - dst + dst_lock[..src.len()].to_vec() +} + +pub fn flush_buffers() { + if let Some(lock) = BLUR_TEMP_BUFFER.get() { + if let Ok(mut b) = lock.lock() { + b.clear(); + b.shrink_to_fit(); + } + } + if let Some(lock) = BLUR_DST_BUFFER.get() { + if let Ok(mut b) = lock.lock() { + b.clear(); + b.shrink_to_fit(); + } + } } diff --git a/src/overlay/capture.rs b/src/overlay/capture.rs index ba6bcfe..eef9c7e 100644 --- a/src/overlay/capture.rs +++ b/src/overlay/capture.rs @@ -10,36 +10,51 @@ pub struct CapturedScreen { pub data: Vec, // BGRA pixels } +use std::sync::{Mutex, OnceLock}; + +static CAPTURE_BUFFER: OnceLock>> = OnceLock::new(); + +fn get_capture_buffer() -> &'static Mutex> { + CAPTURE_BUFFER.get_or_init(|| Mutex::new(Vec::new())) +} + pub fn capture_virtual_screen() -> Result { unsafe { let x = GetSystemMetrics(SM_XVIRTUALSCREEN); let y = GetSystemMetrics(SM_YVIRTUALSCREEN); let width = GetSystemMetrics(SM_CXVIRTUALSCREEN); let height = GetSystemMetrics(SM_CYVIRTUALSCREEN); + + let capture_width = width / 4; + let capture_height = height / 4; let h_screen_dc = GetDC(None); let h_memory_dc = CreateCompatibleDC(Some(h_screen_dc)); - let h_bitmap = CreateCompatibleBitmap(h_screen_dc, width, height); + let h_bitmap = CreateCompatibleBitmap(h_screen_dc, capture_width, capture_height); let h_old_obj = SelectObject(h_memory_dc, h_bitmap.into()); - BitBlt( + SetStretchBltMode(h_memory_dc, HALFTONE); + + StretchBlt( h_memory_dc, 0, 0, - width, - height, + capture_width, + capture_height, Some(h_screen_dc), x, y, + width, + height, SRCCOPY, - )?; + ).ok()?; let mut bmi = BITMAPINFO { bmiHeader: BITMAPINFOHEADER { biSize: std::mem::size_of::() as u32, - biWidth: width, - biHeight: -height, // Negative for top-down DIB + biWidth: capture_width, + biHeight: -capture_height, biPlanes: 1, biBitCount: 32, biCompression: BI_RGB.0, @@ -48,18 +63,24 @@ pub fn capture_virtual_screen() -> Result { ..Default::default() }; - let mut data = vec![0u8; (width * height * 4) as usize]; + let mut lock = get_capture_buffer().lock().unwrap(); + let buffer_size = (capture_width * capture_height * 4) as usize; + if lock.len() < buffer_size { + lock.resize(buffer_size, 0); + } GetDIBits( - h_screen_dc, + h_memory_dc, h_bitmap, 0, - height as u32, - Some(data.as_mut_ptr() as *mut _), + capture_height as u32, + Some(lock.as_mut_ptr() as *mut _), &mut bmi, DIB_RGB_COLORS, ); + let data = lock[..buffer_size].to_vec(); + // Cleanup let _ = SelectObject(h_memory_dc, h_old_obj); let _ = DeleteObject(h_bitmap.into()); @@ -67,9 +88,18 @@ pub fn capture_virtual_screen() -> Result { ReleaseDC(None, h_screen_dc); Ok(CapturedScreen { - width, - height, + width: capture_width, + height: capture_height, data, }) } } + +pub fn flush_buffer() { + if let Some(lock) = CAPTURE_BUFFER.get() { + if let Ok(mut b) = lock.lock() { + b.clear(); + b.shrink_to_fit(); + } + } +} diff --git a/src/overlay/mod.rs b/src/overlay/mod.rs index c6c7380..6c5b825 100644 --- a/src/overlay/mod.rs +++ b/src/overlay/mod.rs @@ -20,7 +20,7 @@ struct SendSafeHwnd(isize); unsafe impl Send for SendSafeHwnd {} impl OverlayWindow { - pub fn new(sender: Sender, width: i32, height: i32, blurred_data: Vec, settings: Settings) -> Result { + pub fn new(sender: Sender, screen_width: i32, screen_height: i32, blurred_width: i32, blurred_height: i32, blurred_data: Vec, settings: Settings) -> Result { unsafe { let instance = GetModuleHandleW(None)?.into(); let class_name = w!("PauseCatOverlayClass"); @@ -41,26 +41,25 @@ impl OverlayWindow { class_name, w!("PauseCat Overlay"), WS_POPUP, - 0, 0, width, height, + 0, 0, screen_width, screen_height, None, None, Some(instance), None )?; SetPropW(hwnd, w!("Sender"), Some(HANDLE(Box::into_raw(Box::new(sender)) as *mut _)))?; - // Apply immersive dark mode immediately after creation let is_dark = crate::system::is_dark_mode(); crate::system::apply_immersive_dark_mode(hwnd, is_dark); let hdc = GetDC(Some(hwnd)); let mem_dc = CreateCompatibleDC(Some(hdc)); - let h_bitmap = CreateCompatibleBitmap(hdc, width, height); + let h_bitmap = CreateCompatibleBitmap(hdc, screen_width, screen_height); let _ = SelectObject(mem_dc, h_bitmap.into()); let bmi = BITMAPINFO { bmiHeader: BITMAPINFOHEADER { biSize: std::mem::size_of::() as u32, - biWidth: width, - biHeight: -height, + biWidth: blurred_width, + biHeight: -blurred_height, biPlanes: 1, biBitCount: 32, biCompression: BI_RGB.0, @@ -69,8 +68,10 @@ impl OverlayWindow { ..Default::default() }; + SetStretchBltMode(mem_dc, COLORONCOLOR); + let _ = StretchDIBits( - mem_dc, 0, 0, width, height, 0, 0, width, height, + mem_dc, 0, 0, screen_width, screen_height, 0, 0, blurred_width, blurred_height, Some(blurred_data.as_ptr() as *const _), &bmi, DIB_RGB_COLORS, SRCCOPY ); @@ -84,7 +85,7 @@ impl OverlayWindow { let pt_src = POINT { x: 0, y: 0 }; let pt_dst = POINT { x: 0, y: 0 }; - let size = SIZE { cx: width, cy: height }; + let size = SIZE { cx: screen_width, cy: screen_height }; let _ = UpdateLayeredWindow(hwnd, Some(hdc), Some(&pt_dst), Some(&size), Some(mem_dc), Some(&pt_src), COLORREF(0), Some(&blend), ULW_ALPHA); @@ -185,10 +186,10 @@ mod internal_tests { // We can't easily create a real window in tests without it being flaky, // but we can test the structure. let (tx, _rx) = mpsc::channel::(); - let blurred_data = vec![0u8; 100]; + let blurred_data = vec![0u8; 400]; // 10*10*4 let settings = Settings::default(); - if let Ok(overlay) = OverlayWindow::new(tx, 10, 10, blurred_data, settings) { + if let Ok(overlay) = OverlayWindow::new(tx, 10, 10, 10, 10, blurred_data, settings) { overlay.fade_in(); std::thread::sleep(std::time::Duration::from_millis(100)); } diff --git a/src/overlay/webview.rs b/src/overlay/webview.rs index 79b5369..7338e46 100644 --- a/src/overlay/webview.rs +++ b/src/overlay/webview.rs @@ -1,32 +1,40 @@ use std::sync::mpsc::Sender; use std::collections::HashMap; -use std::sync::Mutex; +use std::sync::{Mutex, OnceLock}; use windows::core::*; use windows::Win32::Foundation::*; use windows::Win32::UI::WindowsAndMessaging::*; use windows::Win32::System::Com::*; -use windows::Win32::System::Com::StructuredStorage::*; +use windows::Win32::Storage::FileSystem::*; +use windows::Win32::UI::Shell::SHCreateStreamOnFileEx; use webview2_com::*; use webview2_com::Microsoft::Web::WebView2::Win32::*; use crate::events::AppEvent; -use base64::{Engine as _, engine::general_purpose}; use crate::overlay::webview_env; +use std::path::PathBuf; struct ComSafe(T); unsafe impl Send for ComSafe {} unsafe impl Sync for ComSafe {} -lazy_static::lazy_static! { - static ref OVERLAY_CONTROLLERS: Mutex>> = Mutex::new(HashMap::new()); +static OVERLAY_CONTROLLERS: OnceLock>>> = OnceLock::new(); +static LOCAL_ASSET_REGISTRY: OnceLock>> = OnceLock::new(); + +fn get_controllers() -> &'static Mutex>> { + OVERLAY_CONTROLLERS.get_or_init(|| Mutex::new(HashMap::new())) +} + +fn get_asset_registry() -> &'static Mutex> { + LOCAL_ASSET_REGISTRY.get_or_init(|| Mutex::new(HashMap::new())) } pub fn register_controller(hwnd: HWND, controller: ICoreWebView2Controller) { - if let Ok(mut lock) = OVERLAY_CONTROLLERS.lock() { + if let Ok(mut lock) = get_controllers().lock() { lock.insert(hwnd.0 as isize, ComSafe(controller)); } } -pub fn handle_overlay_message(json: &str, sender: &Sender, settings: &crate::settings::Settings, post_message: F) +pub fn handle_overlay_message(_hwnd: HWND, json: &str, sender: &Sender, settings: &crate::settings::Settings, post_message: F) where F: FnOnce(&str) { if json.contains("\"action\":\"dismiss\"") { let _ = sender.send(AppEvent::UserDismissed); @@ -39,7 +47,16 @@ where F: FnOnce(&str) { let final_media_path = if anim_path == "default.webm" || !anim_path.contains(std::path::MAIN_SEPARATOR) { format!("https://pausecat.app/assets/{}", anim_path) } else { - format!("https://pausecat.app/local/{}", general_purpose::STANDARD.encode(&anim_path)) + let path = PathBuf::from(&anim_path); + let id = if let Some(name) = path.file_name() { + name.to_string_lossy().to_string() + } else { + "custom".to_string() + }; + if let Ok(mut lock) = get_asset_registry().lock() { + lock.insert(id.clone(), path); + } + format!("https://pausecat.app/local/{}", id) }; let messages_json = serde_json::to_string(&settings.break_messages).unwrap_or_else(|_| "[]".to_string()); let init_msg = format!( @@ -55,7 +72,7 @@ where F: FnOnce(&str) { } } -pub fn handle_resource_request(uri: &str, assets_path: &std::path::Path) -> Option<(Vec, String)> { +pub fn handle_resource_stream_request(uri: &str, assets_path: &std::path::Path) -> Option<(IStream, String)> { if uri.starts_with("https://pausecat.app/") { let path_part = uri.trim_start_matches("https://pausecat.app/"); let file_name = if path_part.starts_with("assets/") { @@ -65,40 +82,31 @@ pub fn handle_resource_request(uri: &str, assets_path: &std::path::Path) -> Opti }; if !file_name.is_empty() { - // 1. Try provided assets_path (e.g. Config Dir for lazy-loaded assets) let target_path = assets_path.join(file_name); if target_path.exists() && target_path.is_file() { - if let Ok(content) = std::fs::read(&target_path) { - return Some((content, get_mime_type(file_name))); + if let Ok(stream) = create_file_stream(&target_path) { + return Some((stream, get_mime_type(file_name))); } } - // 2. Try fallback (Near EXE for bundled assets) if let Ok(mut exe_path) = std::env::current_exe() { exe_path.pop(); let fallback_path = exe_path.join("assets").join(file_name); if fallback_path.exists() && fallback_path.is_file() { - if let Ok(content) = std::fs::read(&fallback_path) { - return Some((content, get_mime_type(file_name))); + if let Ok(stream) = create_file_stream(&fallback_path) { + return Some((stream, get_mime_type(file_name))); } } } - - // 3. Try CWD fallback - let cwd_path = std::path::PathBuf::from("assets").join(file_name); - if cwd_path.exists() && cwd_path.is_file() { - if let Ok(content) = std::fs::read(&cwd_path) { - return Some((content, get_mime_type(file_name))); - } - } } else if path_part.starts_with("local/") { - let encoded = path_part.trim_start_matches("local/"); - if let Ok(path_bytes) = general_purpose::STANDARD.decode(encoded) { - let target_path = std::path::PathBuf::from(String::from_utf8(path_bytes).unwrap_or_default()); - if target_path.exists() && target_path.is_file() { - if let Ok(content) = std::fs::read(&target_path) { - let ext = target_path.extension().and_then(|e| e.to_str()).unwrap_or(""); - return Some((content, get_mime_type(ext))); + let id = path_part.trim_start_matches("local/"); + if let Ok(lock) = get_asset_registry().lock() { + if let Some(target_path) = lock.get(id) { + if target_path.exists() && target_path.is_file() { + if let Ok(stream) = create_file_stream(target_path) { + let ext = target_path.extension().and_then(|e| e.to_str()).unwrap_or(""); + return Some((stream, get_mime_type(ext))); + } } } } @@ -107,6 +115,19 @@ pub fn handle_resource_request(uri: &str, assets_path: &std::path::Path) -> Opti None } +fn create_file_stream(path: &std::path::Path) -> windows::core::Result { + unsafe { + let path_h = HSTRING::from(path.to_str().unwrap_or_default()); + SHCreateStreamOnFileEx( + windows::core::PCWSTR(path_h.as_ptr()), + STGM_READ.0 as u32, + FILE_ATTRIBUTE_NORMAL.0, + false, + None, + ) + } +} + fn get_mime_type(path_or_ext: &str) -> String { let ext = if path_or_ext.contains('.') { path_or_ext.split('.').last().unwrap_or("") @@ -148,13 +169,13 @@ const OVERLAY_ANTI_ZOOM_SCRIPT: &str = " pub fn init(hwnd: HWND, settings: crate::settings::Settings) -> windows::core::Result<()> { let env = webview_env::get_global_env().ok_or_else(|| windows::core::Error::from_hresult(HRESULT(-1)))?; - let env_inner = env.clone(); + let env_res = env.clone(); unsafe { env.CreateCoreWebView2Controller(hwnd, &CreateCoreWebView2ControllerCompletedHandler::create( Box::new(move |result, controller| { on_overlay_controller_completed(result, controller, hwnd)?; - if let Ok(lock) = OVERLAY_CONTROLLERS.lock() { + if let Ok(lock) = get_controllers().lock() { if let Some(safe_controller) = lock.get(&(hwnd.0 as isize)) { let webview = safe_controller.0.CoreWebView2()?; let ws = webview.Settings()?; @@ -165,18 +186,15 @@ pub fn init(hwnd: HWND, settings: crate::settings::Settings) -> windows::core::R let _ = ws.SetIsStatusBarEnabled(false); let _ = webview.AddScriptToExecuteOnDocumentCreated(&HSTRING::from(OVERLAY_ANTI_ZOOM_SCRIPT), None); let assets_path = webview_env::get_assets_path(); - let env_res = env_inner.clone(); + let env_inner = env_res.clone(); let _ = webview.AddWebResourceRequestedFilter(w!("https://pausecat.app/*"), COREWEBVIEW2_WEB_RESOURCE_CONTEXT_ALL); let _ = webview.add_WebResourceRequested(&WebResourceRequestedEventHandler::create(Box::new(move |_, args| { - if let (Some(args), env) = (args, &env_res) { + if let (Some(args), env) = (args, &env_inner) { let request = args.Request()?; let mut uri_ptr = PWSTR::null(); let _ = request.Uri(&mut uri_ptr); let uri = uri_ptr.to_string().unwrap_or_default(); - if let Some((content, mime)) = handle_resource_request(&uri, &assets_path) { - let stream = CreateStreamOnHGlobal(HGLOBAL(std::ptr::null_mut()), true)?; - let _ = stream.Write(content.as_ptr() as *const _, content.len() as u32, None); - let _ = stream.Seek(0, STREAM_SEEK_SET, None); + if let Some((stream, mime)) = handle_resource_stream_request(&uri, &assets_path) { let response = env.CreateWebResourceResponse(Some(&stream), 200, w!("OK"), &HSTRING::from(format!("Content-Type: {}\r\n", mime)))?; let _ = args.SetResponse(&response); } @@ -190,11 +208,11 @@ pub fn init(hwnd: HWND, settings: crate::settings::Settings) -> windows::core::R let wv_c = webview.clone(); let settings_c = settings.clone(); let _ = webview.add_WebMessageReceived(&WebMessageReceivedEventHandler::create(Box::new(move |_, args| { - if let Some(args) = args { + if let (Some(args), hwnd) = (args, hwnd) { let mut msg = PWSTR::null(); if args.WebMessageAsJson(&mut msg).is_ok() { let json = msg.to_string().unwrap_or_default(); - handle_overlay_message(&json, &sender_c, &settings_c, |m| { let _ = wv_c.PostWebMessageAsJson(&HSTRING::from(m)); }); + handle_overlay_message(hwnd, &json, &sender_c, &settings_c, |m| { let _ = wv_c.PostWebMessageAsJson(&HSTRING::from(m)); }); CoTaskMemFree(Some(msg.0 as *const _)); } } @@ -212,7 +230,7 @@ pub fn init(hwnd: HWND, settings: crate::settings::Settings) -> windows::core::R } pub fn resize_controller(hwnd: HWND) { - if let Ok(lock) = OVERLAY_CONTROLLERS.lock() { + if let Ok(lock) = get_controllers().lock() { if let Some(safe_controller) = lock.get(&(hwnd.0 as isize)) { let mut rect = RECT::default(); unsafe { let _ = GetClientRect(hwnd, &mut rect); let _ = safe_controller.0.SetBounds(rect); } @@ -221,11 +239,11 @@ pub fn resize_controller(hwnd: HWND) { } pub fn unregister_controller(hwnd: HWND) { - if let Ok(mut lock) = OVERLAY_CONTROLLERS.lock() { lock.remove(&(hwnd.0 as isize)); } + if let Ok(mut lock) = get_controllers().lock() { lock.remove(&(hwnd.0 as isize)); } } pub fn update_theme(hwnd: HWND, is_dark: bool) { - if let Ok(lock) = OVERLAY_CONTROLLERS.lock() { + if let Ok(lock) = get_controllers().lock() { if let Some(safe_controller) = lock.get(&(hwnd.0 as isize)) { if let Ok(webview) = unsafe { safe_controller.0.CoreWebView2() } { let _ = unsafe { webview.PostWebMessageAsJson(&HSTRING::from(format!("{{\"action\":\"theme_changed\", \"isDark\": {}}}", is_dark))) }; @@ -233,25 +251,3 @@ pub fn update_theme(hwnd: HWND, is_dark: bool) { } } } - -#[cfg(test)] -mod internal_tests { - use super::*; - #[test] - fn test_on_overlay_controller_completed_error() { - let hwnd = HWND(std::ptr::null_mut()); - let res = on_overlay_controller_completed(Err(windows::core::Error::from_hresult(HRESULT(-1))), None, hwnd); - assert!(res.is_err()); - } - #[test] - fn test_handle_resource_request_logic() { - let assets_path = webview_env::get_assets_path(); - let res = handle_resource_request("https://pausecat.app/assets/pauseCat.ico", &assets_path); - assert!(res.is_some()); - assert_eq!(res.unwrap().1, "image/x-icon"); - let local_path = assets_path.join("default.webm"); - let encoded = general_purpose::STANDARD.encode(local_path.to_str().unwrap()); - assert!(handle_resource_request(&format!("https://pausecat.app/local/{}", encoded), &assets_path).is_some()); - assert!(handle_resource_request("https://google.com", &assets_path).is_none()); - } -} diff --git a/src/overlay/webview_env.rs b/src/overlay/webview_env.rs index 23d6d2f..d5da482 100644 --- a/src/overlay/webview_env.rs +++ b/src/overlay/webview_env.rs @@ -1,34 +1,33 @@ -use std::sync::Mutex; +use std::sync::{Mutex, OnceLock}; use webview2_com::Microsoft::Web::WebView2::Win32::*; -use webview2_com::CreateCoreWebView2EnvironmentCompletedHandler; +use webview2_com::{CreateCoreWebView2EnvironmentCompletedHandler, CoreWebView2EnvironmentOptions}; use windows::core::*; use std::path::PathBuf; +use crate::settings::Settings; struct SendSafeEnv(ICoreWebView2Environment); unsafe impl Send for SendSafeEnv {} unsafe impl Sync for SendSafeEnv {} -lazy_static::lazy_static! { - static ref GLOBAL_ENV: Mutex> = Mutex::new(None); +static GLOBAL_ENV: OnceLock>> = OnceLock::new(); + +fn get_env_lock() -> &'static Mutex> { + GLOBAL_ENV.get_or_init(|| Mutex::new(None)) } pub fn get_assets_path() -> PathBuf { - // 1. Check Config Dir (Lazy-loaded assets) - let mut config_path = dirs::config_dir().unwrap_or_else(|| PathBuf::from(".")); - config_path.push("PauseCat"); + let mut config_path = Settings::get_config_dir(); config_path.push("assets"); if config_path.exists() && config_path.is_dir() { return config_path; } - // 2. Check near EXE (Bundled assets) if let Ok(mut path) = std::env::current_exe() { path.pop(); path.push("assets"); if path.exists() { return path; } } - // 3. Fallback to CWD let mut path = std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")); path.push("assets"); path @@ -36,22 +35,21 @@ pub fn get_assets_path() -> PathBuf { pub fn init_global_env() -> Result<()> { unsafe { - // Note: CoInitializeEx should be called on the main thread before this. - let mut config_dir = dirs::config_dir().unwrap_or_else(|| PathBuf::from(".")); - config_dir.push("PauseCat"); + let config_dir = Settings::get_config_dir(); let mut webview_data = config_dir.clone(); webview_data.push("WebViewData_Shared"); let _ = std::fs::create_dir_all(&webview_data); let data_path_h = HSTRING::from(webview_data.to_str().unwrap_or_default()); - // Optimization: Non-blocking async initialization. - // The callback will set the global environment whenever it's ready. - CreateCoreWebView2EnvironmentWithOptions(None, PCWSTR(data_path_h.as_ptr()), None, + let options: ICoreWebView2EnvironmentOptions = CoreWebView2EnvironmentOptions::default().into(); + let _ = options.SetAdditionalBrowserArguments(w!("--process-per-site --disk-cache-size=10485760 --disable-features=Translate,EdgeCollections,EdgeWorkspaces")); + + CreateCoreWebView2EnvironmentWithOptions(None, PCWSTR(data_path_h.as_ptr()), Some(&options), &CreateCoreWebView2EnvironmentCompletedHandler::create( Box::new(move |_result, env| { if let Some(e) = env { - if let Ok(mut lock) = GLOBAL_ENV.lock() { + if let Ok(mut lock) = get_env_lock().lock() { *lock = Some(SendSafeEnv(e)); } } @@ -65,5 +63,5 @@ pub fn init_global_env() -> Result<()> { } pub fn get_global_env() -> Option { - GLOBAL_ENV.lock().unwrap().as_ref().map(|e| e.0.clone()) + get_env_lock().lock().unwrap().as_ref().map(|e| e.0.clone()) } diff --git a/src/settings.rs b/src/settings.rs index 15e7af8..121eac1 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -1,20 +1,34 @@ use serde::{Deserialize, Serialize}; use std::fs; use std::path::PathBuf; -use winreg::enums::*; -use winreg::RegKey; -use thiserror::Error; +use windows::Win32::UI::Shell::*; +use windows::Win32::Foundation::*; +use windows::Win32::System::Registry::*; +use windows::Win32::System::Com::CoTaskMemFree; +use windows::core::HSTRING; -#[derive(Error, Debug)] +#[derive(Debug)] pub enum SettingsError { - #[error("IO error: {0}")] - Io(#[from] std::io::Error), - #[error("Serialization error: {0}")] - Json(#[from] serde_json::Error), - #[error("Registry error: {0}")] - Registry(std::io::Error), + Io(std::io::Error), + Json(serde_json::Error), + Registry(windows::core::Error), } +impl From for SettingsError { fn from(e: std::io::Error) -> Self { Self::Io(e) } } +impl From for SettingsError { fn from(e: serde_json::Error) -> Self { Self::Json(e) } } +impl From for SettingsError { fn from(e: windows::core::Error) -> Self { Self::Registry(e) } } + +impl std::fmt::Display for SettingsError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Io(e) => write!(f, "IO error: {}", e), + Self::Json(e) => write!(f, "Serialization error: {}", e), + Self::Registry(e) => write!(f, "Registry error: {}", e), + } + } +} +impl std::error::Error for SettingsError {} + #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] #[serde(rename_all = "snake_case")] pub enum BreakMode { @@ -38,9 +52,9 @@ pub struct Settings { pub bubble_pos_x: i32, pub bubble_pos_y: i32, pub animation_style: String, - pub break_style: String, // "media" or "text" + pub break_style: String, pub custom_text: String, - pub video_volume: f32, // 0.0 to 1.0 + pub video_volume: f32, pub text_animation: String, pub text_rotation_x: i32, pub text_rotation_y: i32, @@ -96,9 +110,16 @@ impl Default for Settings { impl Settings { pub fn get_config_dir() -> PathBuf { - let mut path = dirs::config_dir().unwrap_or_else(|| PathBuf::from(".")); - path.push("PauseCat"); - path + unsafe { + if let Ok(path_ptr) = SHGetKnownFolderPath(&FOLDERID_RoamingAppData, KNOWN_FOLDER_FLAG(0), None) { + let path_str = path_ptr.to_string().unwrap_or_default(); + CoTaskMemFree(Some(path_ptr.0 as *const _)); + let mut path = PathBuf::from(path_str); + path.push("PauseCat"); + return path; + } + } + PathBuf::from(".") } pub fn get_config_path() -> PathBuf { @@ -123,9 +144,7 @@ impl Settings { settings_to_save.validate(); let dir = Self::get_config_dir(); - if !dir.exists() { - fs::create_dir_all(&dir)?; - } + if !dir.exists() { fs::create_dir_all(&dir)?; } let path = Self::get_config_path(); let tmp_path = path.with_extension("tmp"); @@ -135,7 +154,6 @@ impl Settings { fs::rename(&tmp_path, &path)?; self.update_autostart()?; - Ok(()) } @@ -148,69 +166,23 @@ impl Settings { if self.bubble_opacity > 1.0 { self.bubble_opacity = 1.0; } } - pub fn force_save_error_test(&self) -> Result<(), SettingsError> { - let invalid_path = std::path::PathBuf::from("/invalid/path/settings.json"); - let json = serde_json::to_string_pretty(self)?; - fs::write(invalid_path, json).map_err(SettingsError::Io) - } - pub fn update_autostart(&self) -> Result<(), SettingsError> { - let hkcu = RegKey::predef(HKEY_CURRENT_USER); - let path = r"Software\Microsoft\Windows\CurrentVersion\Run"; - let (key, _) = hkcu.create_subkey(path).map_err(SettingsError::Registry)?; - - if self.autostart { - let exe_path = std::env::current_exe().map_err(SettingsError::Io)?; - key.set_value("PauseCat", &exe_path.to_str().unwrap_or("")) - .map_err(SettingsError::Registry)?; - } else { - let _ = key.delete_value("PauseCat"); + unsafe { + let mut h_key = HKEY::default(); + let sub_key = windows::core::w!("Software\\Microsoft\\Windows\\CurrentVersion\\Run"); + + if RegCreateKeyExW(HKEY_CURRENT_USER, sub_key, Some(0), None, REG_OPTION_NON_VOLATILE, KEY_WRITE, None, &mut h_key, None) == ERROR_SUCCESS { + if self.autostart { + if let Ok(exe_path) = std::env::current_exe() { + let path_h = HSTRING::from(exe_path.to_str().unwrap_or_default()); + let _ = RegSetValueExW(h_key, windows::core::w!("PauseCat"), Some(0), REG_SZ, Some(std::slice::from_raw_parts(path_h.as_ptr() as *const u8, (path_h.len() * 2 + 2) as usize))); + } + } else { + let _ = RegDeleteValueW(h_key, windows::core::w!("PauseCat")); + } + let _ = RegCloseKey(h_key); + } } - Ok(()) } } - -#[cfg(test)] -mod internal_tests { - use super::*; - - #[test] - fn test_settings_sabotage_and_validation() { - let s = Settings::default(); - let _ = s.force_save_error_test(); - - // Sabotage JSON loading - let config_dir = Settings::get_config_dir(); - let path = config_dir.join("config.json"); - let _ = std::fs::write(&path, "invalid json {"); - let s_bad = Settings::load(); - // Should fallback to default - assert_eq!(s_bad.work_duration_secs, Settings::default().work_duration_secs); - - let mut s2 = Settings::default(); - s2.work_duration_secs = 10; - s2.validate(); - assert_eq!(s2.work_duration_secs, 300); - - s2.work_duration_secs = 20000; - s2.validate(); - assert_eq!(s2.work_duration_secs, 14400); - - s2.break_duration_secs = 5; - s2.validate(); - assert_eq!(s2.break_duration_secs, 10); - - s2.break_duration_secs = 10000; - s2.validate(); - assert_eq!(s2.break_duration_secs, 7200); - - s2.bubble_opacity = -0.5; - s2.validate(); - assert_eq!(s2.bubble_opacity, 0.0); - - s2.bubble_opacity = 1.5; - s2.validate(); - assert_eq!(s2.bubble_opacity, 1.0); - } -} diff --git a/src/settings_ui.rs b/src/settings_ui.rs index 40c48bd..f2e1c9c 100644 --- a/src/settings_ui.rs +++ b/src/settings_ui.rs @@ -1,12 +1,11 @@ use std::sync::mpsc::Sender; use std::collections::HashMap; -use std::sync::Mutex; +use std::sync::{Mutex, OnceLock}; use windows::core::*; use windows::Win32::Foundation::*; use windows::Win32::UI::WindowsAndMessaging::*; use windows::Win32::Graphics::Gdi::*; use windows::Win32::System::Com::*; -use windows::Win32::System::Com::StructuredStorage::*; use windows::Win32::System::LibraryLoader::*; use windows::Win32::UI::Controls::Dialogs::*; use webview2_com::*; @@ -14,13 +13,16 @@ use webview2_com::Microsoft::Web::WebView2::Win32::*; use crate::events::AppEvent; use crate::settings::Settings; use crate::overlay::webview_env; +use crate::app::wakeup_main_thread; struct ComSafe(T); unsafe impl Send for ComSafe {} unsafe impl Sync for ComSafe {} -lazy_static::lazy_static! { - static ref CONTROLLERS: Mutex>> = Mutex::new(HashMap::new()); +static SETTINGS_CONTROLLERS: OnceLock>>> = OnceLock::new(); + +fn get_controllers() -> &'static Mutex>> { + SETTINGS_CONTROLLERS.get_or_init(|| Mutex::new(HashMap::new())) } pub struct SettingsWindow { @@ -34,24 +36,29 @@ where F: FnOnce(&str), P: FnOnce() -> Option { if let Ok(new_settings) = serde_json::from_value::(data["settings"].clone()) { let _ = sender.send(AppEvent::ConfigChanged(new_settings)); let _ = sender.send(AppEvent::SettingsClosed); + wakeup_main_thread(); } } } else if json.contains("\"action\":\"close\"") { let _ = sender.send(AppEvent::SettingsClosed); + wakeup_main_thread(); } else if json.contains("\"action\":\"get_apps\"") { let apps = crate::system::get_running_apps(); let apps_json = serde_json::to_string(&apps).unwrap_or_default(); post_message(&format!("{{\"action\":\"apps_list\", \"apps\": {}}}", apps_json)); } else if json.contains("\"action\":\"check_updates\"") { let _ = sender.send(AppEvent::CheckForUpdates); + wakeup_main_thread(); } else if json.contains("\"action\":\"start_update\"") { let _ = sender.send(AppEvent::StartUpdate); + wakeup_main_thread(); } else if json.contains("\"action\":\"select_media\"") { if let Some(path) = pick_file_fn() { post_message(&format!("{{\"action\":\"media_selected\", \"path\":\"{}\"}}", path.replace('\\', "/"))); } } else if json.contains("\"action\":\"retry_sync\"") { let _ = sender.send(AppEvent::RetryAssetSync); + wakeup_main_thread(); } } @@ -79,7 +86,9 @@ pub fn on_controller_completed( let mut rect = RECT::default(); unsafe { let _ = GetClientRect(hwnd, &mut rect); } let _ = unsafe { controller.SetBounds(rect) }; - CONTROLLERS.lock().unwrap().insert(hwnd.0 as isize, ComSafe(controller)); + if let Ok(mut lock) = get_controllers().lock() { + lock.insert(hwnd.0 as isize, ComSafe(controller)); + } Ok(()) } @@ -88,16 +97,16 @@ impl SettingsWindow { unsafe { let instance: HINSTANCE = GetModuleHandleW(None)?.into(); let class_name = w!("PauseCatSettingsClass"); - let wnd_class = WNDCLASSEXW { + let mut wnd_class = WNDCLASSEXW { cbSize: std::mem::size_of::() as u32, lpfnWndProc: Some(settings_wnd_proc), hInstance: instance, lpszClassName: class_name, hCursor: LoadCursorW(None, IDC_ARROW)?, - hbrBackground: HBRUSH(GetStockObject(WHITE_BRUSH).0), + hbrBackground: HBRUSH(GetStockObject(WHITE_BRUSH).0 as *mut _), ..Default::default() }; - RegisterClassExW(&wnd_class); + RegisterClassExW(&mut wnd_class); let hwnd = CreateWindowExW( WINDOW_EX_STYLE::default(), class_name, w!("PauseCat Settings"), WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU, @@ -122,11 +131,15 @@ impl SettingsWindow { &CreateCoreWebView2ControllerCompletedHandler::create( Box::new(move |result, controller| { on_controller_completed(result, controller, hwnd)?; - if let Ok(lock) = CONTROLLERS.lock() { + if let Ok(lock) = get_controllers().lock() { if let Some(safe_controller) = lock.get(&(hwnd.0 as isize)) { let webview = safe_controller.0.CoreWebView2()?; let ws = webview.Settings()?; - let _ = (ws.SetIsWebMessageEnabled(true), ws.SetAreDefaultContextMenusEnabled(false), ws.SetAreDevToolsEnabled(false), ws.SetIsZoomControlEnabled(false), ws.SetIsStatusBarEnabled(false)); + let _ = ws.SetIsWebMessageEnabled(true); + let _ = ws.SetAreDefaultContextMenusEnabled(false); + let _ = ws.SetAreDevToolsEnabled(false); + let _ = ws.SetIsZoomControlEnabled(false); + let _ = ws.SetIsStatusBarEnabled(false); let assets_path = webview_env::get_assets_path(); let env_res = env_inner.clone(); @@ -137,9 +150,7 @@ impl SettingsWindow { let mut uri_ptr = PWSTR::null(); let _ = request.Uri(&mut uri_ptr); let uri = uri_ptr.to_string().unwrap_or_default(); - if let Some((content, mime)) = crate::overlay::webview::handle_resource_request(&uri, &assets_path) { - let stream = CreateStreamOnHGlobal(HGLOBAL(std::ptr::null_mut()), true)?; - let _ = (stream.Write(content.as_ptr() as *const _, content.len() as u32, None), stream.Seek(0, STREAM_SEEK_SET, None)); + if let Some((stream, mime)) = crate::overlay::webview::handle_resource_stream_request(&uri, &assets_path) { let response = env.CreateWebResourceResponse(Some(&stream), 200, w!("OK"), &HSTRING::from(format!("Content-Type: {}\r\n", mime)))?; let _ = args.SetResponse(&response); } @@ -199,7 +210,7 @@ impl SettingsWindow { } pub fn post_web_message(&self, msg: &str) { - if let Ok(lock) = CONTROLLERS.lock() { + if let Ok(lock) = get_controllers().lock() { if let Some(safe_controller) = lock.get(&(self.hwnd.0 as isize)) { if let Ok(webview) = unsafe { safe_controller.0.CoreWebView2() } { let _ = unsafe { webview.PostWebMessageAsJson(&HSTRING::from(msg)) }; @@ -233,10 +244,11 @@ impl Drop for SettingsWindow { fn drop(&mut self) { unsafe { let _ = DestroyWind unsafe extern "system" fn settings_wnd_proc(hwnd: HWND, msg: u32, wparam: WPARAM, lparam: LPARAM) -> LRESULT { match msg { WM_SIZE => { - if let Ok(lock) = CONTROLLERS.lock() { + if let Ok(lock) = get_controllers().lock() { if let Some(safe_controller) = lock.get(&(hwnd.0 as isize)) { let mut rect = RECT::default(); - let _ = (GetClientRect(hwnd, &mut rect), safe_controller.0.SetBounds(rect)); + let _ = GetClientRect(hwnd, &mut rect); + let _ = safe_controller.0.SetBounds(rect); } } LRESULT(0) @@ -246,6 +258,7 @@ unsafe extern "system" fn settings_wnd_proc(hwnd: HWND, msg: u32, wparam: WPARAM if !sender_handle.is_invalid() { let sender = unsafe { &*(sender_handle.0 as *const Sender) }; let _ = sender.send(AppEvent::SettingsClosed); + wakeup_main_thread(); } LRESULT(0) } @@ -253,7 +266,7 @@ unsafe extern "system" fn settings_wnd_proc(hwnd: HWND, msg: u32, wparam: WPARAM let sender_handle = RemovePropW(hwnd, w!("Sender")).unwrap_or_default(); if !sender_handle.is_invalid() { drop(unsafe { Box::from_raw(sender_handle.0 as *mut Sender) }); } let _ = RemovePropW(hwnd, w!("Settings")).map(|h| if !h.is_invalid() { drop(unsafe { Box::from_raw(h.0 as *mut Settings) }); }); - if let Ok(mut lock) = CONTROLLERS.lock() { lock.remove(&(hwnd.0 as isize)); } + if let Ok(mut lock) = get_controllers().lock() { lock.remove(&(hwnd.0 as isize)); } LRESULT(0) } _ => DefWindowProcW(hwnd, msg, wparam, lparam), diff --git a/src/system.rs b/src/system.rs index 5bd89c2..7e1e7c8 100644 --- a/src/system.rs +++ b/src/system.rs @@ -1,5 +1,3 @@ -use winreg::enums::*; -use winreg::RegKey; use windows::Win32::Foundation::*; use windows::Win32::Graphics::Dwm::*; use windows::core::{PCSTR, BOOL}; @@ -7,16 +5,23 @@ use windows::Win32::System::LibraryLoader::{GetModuleHandleA, GetProcAddress}; use windows::Win32::UI::WindowsAndMessaging::*; use windows::Win32::System::Threading::*; use windows::Win32::System::ProcessStatus::*; +use windows::Win32::System::Registry::*; use std::collections::HashSet; pub fn is_dark_mode() -> bool { - let hkcu = RegKey::predef(HKEY_CURRENT_USER); - if let Ok(key) = hkcu.open_subkey(r"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize") { - let value: u32 = key.get_value("AppsUseLightTheme").unwrap_or(1); - value == 0 - } else { - false + unsafe { + let mut h_key = HKEY::default(); + let sub_key = windows::core::w!("Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize"); + let mut value = 1u32; + let mut size = std::mem::size_of::() as u32; + + if RegOpenKeyExW(HKEY_CURRENT_USER, sub_key, Some(0), KEY_READ, &mut h_key) == ERROR_SUCCESS { + let _ = RegQueryValueExW(h_key, windows::core::w!("AppsUseLightTheme"), None, None, Some(&mut value as *mut _ as *mut _), Some(&mut size)); + let _ = RegCloseKey(h_key); + return value == 0; + } } + false } pub fn apply_immersive_dark_mode(hwnd: HWND, is_dark: bool) { @@ -102,10 +107,6 @@ pub fn is_media_playing() -> bool { let result = (|| -> windows::core::Result { let op = GlobalSystemMediaTransportControlsSessionManager::RequestAsync()?; - // In windows-rs 0.62, we can try to GetResults directly if it's sync-capable - // or just wait for status to not be 'Started'. - // Since we don't have AsyncStatus easy access, we'll try a loop with GetResults - for _ in 0..100 { if let Ok(manager) = op.GetResults() { if let Ok(session) = manager.GetCurrentSession() { diff --git a/src/tray.rs b/src/tray.rs index 23bf784..bcabfac 100644 --- a/src/tray.rs +++ b/src/tray.rs @@ -8,6 +8,7 @@ use windows::{ Win32::System::LibraryLoader::*, Win32::System::RemoteDesktop::*, }; +use crate::app::wakeup_main_thread; const WM_TRAY_ICON: u32 = WM_APP + 1; const ID_TRAY_ICON: u32 = 1; @@ -46,7 +47,6 @@ impl TrayIcon { None, None, Some(instance), Some(Box::into_raw(Box::new(sender)) as *mut _) )?; - // Register for Session Notifications (Lock/Unlock) let _ = WTSRegisterSessionNotification(hwnd, NOTIFY_FOR_THIS_SESSION); let h_icon = match LoadImageW( @@ -123,18 +123,15 @@ unsafe extern "system" fn wnd_proc(hwnd: HWND, msg: u32, wparam: WPARAM, lparam: let create_struct = lparam.0 as *const CREATESTRUCTW; let sender = (*create_struct).lpCreateParams; SetWindowLongPtrW(hwnd, GWLP_USERDATA, sender as isize); - - let is_dark = crate::system::is_dark_mode(); - let _ = (&*(sender as *const Sender)).send(AppEvent::ThemeChanged(is_dark)); - + let _ = (&*(sender as *const Sender)).send(AppEvent::ThemeChanged(crate::system::is_dark_mode())); + wakeup_main_thread(); LRESULT(0) } WM_SETTINGCHANGE => { let sender_ptr = GetWindowLongPtrW(hwnd, GWLP_USERDATA) as *const Sender; if !sender_ptr.is_null() { - let sender = &*sender_ptr; - let is_dark = crate::system::is_dark_mode(); - let _ = sender.send(AppEvent::ThemeChanged(is_dark)); + let _ = (&*sender_ptr).send(AppEvent::ThemeChanged(crate::system::is_dark_mode())); + wakeup_main_thread(); } LRESULT(0) } @@ -143,14 +140,8 @@ unsafe extern "system" fn wnd_proc(hwnd: HWND, msg: u32, wparam: WPARAM, lparam: if !sender_ptr.is_null() { let sender = &*sender_ptr; match wparam.0 as u32 { - WTS_SESSION_LOCK => { - log::info!("Session Locked: Pausing timer."); - let _ = sender.send(AppEvent::SessionLocked); - } - WTS_SESSION_UNLOCK => { - log::info!("Session Unlocked: Resuming timer."); - let _ = sender.send(AppEvent::SessionUnlocked); - } + WTS_SESSION_LOCK => { let _ = sender.send(AppEvent::SessionLocked); wakeup_main_thread(); } + WTS_SESSION_UNLOCK => { let _ = sender.send(AppEvent::SessionUnlocked); wakeup_main_thread(); } _ => {} } } @@ -161,14 +152,8 @@ unsafe extern "system" fn wnd_proc(hwnd: HWND, msg: u32, wparam: WPARAM, lparam: if !sender_ptr.is_null() { let sender = &*sender_ptr; match wparam.0 as u32 { - PBT_APMSUSPEND => { - log::info!("System Suspending: Pausing timer."); - let _ = sender.send(AppEvent::SessionLocked); - } - PBT_APMRESUMESUSPEND => { - log::info!("System Resumed: Resuming timer."); - let _ = sender.send(AppEvent::SessionUnlocked); - } + PBT_APMSUSPEND => { let _ = sender.send(AppEvent::SessionLocked); wakeup_main_thread(); } + PBT_APMRESUMESUSPEND => { let _ = sender.send(AppEvent::SessionUnlocked); wakeup_main_thread(); } _ => {} } } @@ -186,9 +171,9 @@ unsafe extern "system" fn wnd_proc(hwnd: HWND, msg: u32, wparam: WPARAM, lparam: if !sender_ptr.is_null() { let sender = &*sender_ptr; match wparam.0 as usize { - ID_MENU_PAUSE => { let _ = sender.send(AppEvent::TogglePause); } - ID_MENU_SETTINGS => { let _ = sender.send(AppEvent::OpenSettings); } - ID_MENU_EXIT => { let _ = sender.send(AppEvent::Quit); } + ID_MENU_PAUSE => { let _ = sender.send(AppEvent::TogglePause); wakeup_main_thread(); } + ID_MENU_SETTINGS => { let _ = sender.send(AppEvent::OpenSettings); wakeup_main_thread(); } + ID_MENU_EXIT => { let _ = sender.send(AppEvent::Quit); wakeup_main_thread(); } _ => {} } } @@ -196,9 +181,7 @@ unsafe extern "system" fn wnd_proc(hwnd: HWND, msg: u32, wparam: WPARAM, lparam: } WM_DESTROY => { let sender_ptr = GetWindowLongPtrW(hwnd, GWLP_USERDATA) as *mut Sender; - if !sender_ptr.is_null() { - drop(Box::from_raw(sender_ptr)); - } + if !sender_ptr.is_null() { drop(Box::from_raw(sender_ptr)); } LRESULT(0) } _ => DefWindowProcW(hwnd, msg, wparam, lparam), @@ -206,11 +189,7 @@ unsafe extern "system" fn wnd_proc(hwnd: HWND, msg: u32, wparam: WPARAM, lparam: } unsafe fn show_context_menu(hwnd: HWND) { - let menu = match CreatePopupMenu() { - Ok(m) => m, - Err(_) => return, - }; - + let menu = match CreatePopupMenu() { Ok(m) => m, Err(_) => return }; let pause_text = if IS_PAUSED { "Resume Timer" } else { "Pause Timer" }; let mii = MENUITEMINFOW { cbSize: std::mem::size_of::() as u32, @@ -221,70 +200,13 @@ unsafe fn show_context_menu(hwnd: HWND) { ..Default::default() }; let _ = InsertMenuItemW(menu, 0, true, &mii); - let _ = AppendMenuW(menu, MF_SEPARATOR, 0, None); let _ = AppendMenuW(menu, MF_STRING, ID_MENU_SETTINGS, w!("Settings...")); let _ = AppendMenuW(menu, MF_STRING, ID_MENU_EXIT, w!("Exit")); - let mut pos = POINT::default(); let _ = GetCursorPos(&mut pos); - let _ = SetForegroundWindow(hwnd); let _ = TrackPopupMenu(menu, TPM_RIGHTBUTTON, pos.x, pos.y, Some(0), hwnd, None); let _ = PostMessageW(Some(hwnd), WM_NULL, WPARAM(0), LPARAM(0)); let _ = DestroyMenu(menu); } - -#[cfg(test)] -mod internal_tests { - use super::*; - use std::sync::mpsc; - - #[test] - fn test_wnd_proc_branches() { - let (tx, _rx) = mpsc::channel::(); - let tx_box = Box::into_raw(Box::new(tx)); - - unsafe { - let hwnd = HWND(std::ptr::null_mut()); - - // Test WM_CREATE (sets up sender) - let cs = CREATESTRUCTW { - lpCreateParams: tx_box as *mut _, - ..Default::default() - }; - wnd_proc(hwnd, WM_CREATE, WPARAM(0), LPARAM(&cs as *const _ as isize)); - - // Test WM_SETTINGCHANGE (theme change) - wnd_proc(hwnd, WM_SETTINGCHANGE, WPARAM(0), LPARAM(0)); - - // Test all session change variants - for code in [WTS_SESSION_LOCK, WTS_SESSION_UNLOCK, WTS_SESSION_LOGON, WTS_SESSION_LOGOFF, WTS_REMOTE_CONNECT, WTS_REMOTE_DISCONNECT] { - wnd_proc(hwnd, WM_WTSSESSION_CHANGE, WPARAM(code as usize), LPARAM(0)); - } - - // Test all power broadcast variants - for code in [PBT_APMRESUMESUSPEND, PBT_APMRESUMEAUTOMATIC, PBT_APMQUERYSUSPEND, PBT_APMSUSPEND] { - wnd_proc(hwnd, WM_POWERBROADCAST, WPARAM(code as usize), LPARAM(0)); - } - - // Test WM_TRAY_ICON (right click) - wnd_proc(hwnd, WM_TRAY_ICON, WPARAM(0), LPARAM(WM_RBUTTONUP as isize)); - - // Test WM_COMMAND (menu items) - wnd_proc(hwnd, WM_COMMAND, WPARAM(ID_MENU_PAUSE), LPARAM(0)); - wnd_proc(hwnd, WM_COMMAND, WPARAM(ID_MENU_SETTINGS), LPARAM(0)); - wnd_proc(hwnd, WM_COMMAND, WPARAM(ID_MENU_EXIT), LPARAM(0)); - wnd_proc(hwnd, WM_COMMAND, WPARAM(9999), LPARAM(0)); - - // Test default path - wnd_proc(hwnd, WM_USER, WPARAM(0), LPARAM(0)); - - // Test with NULL HWND to ensure no crash - wnd_proc(HWND(std::ptr::null_mut()), WM_SETTINGCHANGE, WPARAM(0), LPARAM(0)); - - // Clean up - let _ = Box::from_raw(tx_box); - } - } -} diff --git a/src/updater.rs b/src/updater.rs index 8030add..a129542 100644 --- a/src/updater.rs +++ b/src/updater.rs @@ -1,6 +1,5 @@ use std::fs; use std::thread; -use std::io::Read; use serde::{Deserialize, Serialize}; use semver::Version; use crate::settings::Settings; @@ -8,7 +7,8 @@ use std::sync::mpsc::Sender; use crate::events::AppEvent; use windows::Win32::UI::Shell::ShellExecuteW; use windows::Win32::UI::WindowsAndMessaging::SW_HIDE; -use windows::core::HSTRING; +use windows::core::{HSTRING, PCWSTR}; +use windows::Win32::Networking::WinHttp::*; const GITHUB_API_URL: &str = "https://api.github.com/repos/0xarchit/pauseCat/releases/latest"; const APP_VERSION: &str = env!("CARGO_PKG_VERSION"); @@ -34,295 +34,234 @@ pub struct UpdateInfo { pub changelog: String, } -pub fn parse_and_check_version(release_json: &GithubRelease, current_version: &str) -> Result> { - let latest_ver_str = release_json.tag_name.trim_start_matches('v'); - let current_ver = Version::parse(current_version)?; - let latest_ver = Version::parse(latest_ver_str)?; - - Ok(UpdateInfo { - available: latest_ver > current_ver, - latest_version: release_json.tag_name.clone(), - changelog: release_json.body.clone(), - }) +struct WinHttpHandle(*mut core::ffi::c_void); +impl Drop for WinHttpHandle { + fn drop(&mut self) { + if !self.0.is_null() { unsafe { let _ = WinHttpCloseHandle(self.0); } } + } } -pub fn parse_github_release(json: &str) -> Result> { - serde_json::from_str(json).map_err(|e| e.into()) -} +fn winhttp_get(url: &str) -> Result, Box> { + unsafe { + let h_session = WinHttpHandle(WinHttpOpen( + windows::core::w!("PauseCat-Updater/1.0"), + WINHTTP_ACCESS_TYPE_DEFAULT_PROXY, + None, + None, + 0, + )); + if h_session.0.is_null() { + let err = windows::core::Error::from_thread(); + log::error!("WinHttpOpen failed: {:?}", err); + return Err(format!("WinHttpOpen failed: {:?}", err).into()); + } -pub fn check_for_updates() -> Result> { - let client = reqwest::blocking::Client::builder() - .user_agent("PauseCat-Updater-v1") - .timeout(std::time::Duration::from_secs(10)) - .build()?; - - let response = client.get(GITHUB_API_URL) - .header("Accept", "application/vnd.github+json") - .header("X-GitHub-Api-Version", "2022-11-28") - .send()?; - - if !response.status().is_success() { - let status = response.status(); - if status == reqwest::StatusCode::FORBIDDEN { - return Err("GitHub API Forbidden. The repository might be private or rate-limited.".into()); + let _ = WinHttpSetTimeouts(h_session.0, 5000, 5000, 10000, 10000); + + let mut url_components = URL_COMPONENTS { + dwStructSize: std::mem::size_of::() as u32, + dwHostNameLength: u32::MAX, + dwUrlPathLength: u32::MAX, + dwExtraInfoLength: u32::MAX, + ..Default::default() + }; + + let url_u16: Vec = url.encode_utf16().chain(std::iter::once(0)).collect(); + if let Err(e) = WinHttpCrackUrl(&url_u16, 0, &mut url_components) { + log::error!("WinHttpCrackUrl failed for {}: {:?}", url, e); + return Err(e.into()); } - return Err(format!("GitHub API error: {}", status).into()); + + let host_name_u16: Vec = std::slice::from_raw_parts(url_components.lpszHostName.0, url_components.dwHostNameLength as usize) + .iter().copied().chain(std::iter::once(0)).collect(); + let path_name_u16: Vec = std::slice::from_raw_parts(url_components.lpszUrlPath.0, url_components.dwUrlPathLength as usize) + .iter().copied().chain(std::iter::once(0)).collect(); + + let h_connect = WinHttpHandle(WinHttpConnect(h_session.0, PCWSTR(host_name_u16.as_ptr()), url_components.nPort, 0)); + if h_connect.0.is_null() { + let err = windows::core::Error::from_thread(); + log::error!("WinHttpConnect failed for {:?}: {:?}", host_name_u16, err); + return Err(format!("WinHttpConnect failed: {:?}", err).into()); + } + + let h_request = WinHttpHandle(WinHttpOpenRequest( + h_connect.0, + windows::core::w!("GET"), + PCWSTR(path_name_u16.as_ptr()), + None, + None, + std::ptr::null(), + if url.starts_with("https") { WINHTTP_FLAG_SECURE } else { WINHTTP_OPEN_REQUEST_FLAGS(0) }, + )); + if h_request.0.is_null() { + let err = windows::core::Error::from_thread(); + log::error!("WinHttpOpenRequest failed: {:?}", err); + return Err(format!("WinHttpOpenRequest failed: {:?}", err).into()); + } + + let headers = windows::core::w!("Accept: application/vnd.github+json\r\nUser-Agent: PauseCat-Updater-v1\r\n"); + let _ = WinHttpAddRequestHeaders(h_request.0, headers.as_wide(), WINHTTP_ADDREQ_FLAG_ADD); + + if let Err(e) = WinHttpSendRequest(h_request.0, None, None, 0, 0, 0) { + log::error!("WinHttpSendRequest failed: {:?}", e); + return Err(e.into()); + } + + if let Err(e) = WinHttpReceiveResponse(h_request.0, std::ptr::null_mut()) { + log::error!("WinHttpReceiveResponse failed: {:?}", e); + return Err(e.into()); + } + + let mut status_code: u32 = 0; + let mut dw_size = std::mem::size_of::() as u32; + WinHttpQueryHeaders( + h_request.0, + WINHTTP_QUERY_STATUS_CODE | WINHTTP_QUERY_FLAG_NUMBER, + None, + Some(&mut status_code as *mut _ as *mut _), + &mut dw_size, + std::ptr::null_mut(), + )?; + + if status_code == 301 || status_code == 302 || status_code == 307 || status_code == 308 { + let mut redirect_url = [0u16; 4096]; + let mut dw_size = 8192u32; + WinHttpQueryHeaders(h_request.0, WINHTTP_QUERY_LOCATION, None, Some(redirect_url.as_mut_ptr() as *mut _), &mut dw_size, std::ptr::null_mut())?; + let new_url = String::from_utf16_lossy(&redirect_url[.. (dw_size as usize / 2)]).trim_matches('\0').to_string(); + return winhttp_get(&new_url); + } + + if status_code != 200 { + log::error!("HTTP Error: {} for {}", status_code, url); + return Err(format!("HTTP Error: {}", status_code).into()); + } + + let mut response_data = Vec::new(); + let mut dw_size: u32 = 0; + loop { + WinHttpQueryDataAvailable(h_request.0, &mut dw_size as *mut _)?; + if dw_size == 0 { break; } + let mut buffer = vec![0u8; dw_size as usize]; + let mut dw_read: u32 = 0; + WinHttpReadData(h_request.0, buffer.as_mut_ptr() as *mut _, dw_size, &mut dw_read)?; + response_data.extend_from_slice(&buffer[..dw_read as usize]); + } + + Ok(response_data) } +} - let release: GithubRelease = response.json()?; +pub fn check_for_updates() -> Result> { + let data = winhttp_get(GITHUB_API_URL)?; + let release: GithubRelease = serde_json::from_slice(&data)?; parse_and_check_version(&release, APP_VERSION) } -pub fn find_msi_asset(release: &GithubRelease) -> Option<&GithubAsset> { - release.assets.iter() - .find(|a| a.name.to_lowercase().ends_with(".msi")) +pub fn parse_and_check_version(release_json: &GithubRelease, current_version: &str) -> Result> { + let latest_ver_str = release_json.tag_name.trim_start_matches('v'); + let current_ver = Version::parse(current_version).unwrap_or_else(|_| Version::new(1, 0, 0)); + let latest_ver = Version::parse(latest_ver_str).unwrap_or_else(|_| Version::new(1, 0, 0)); + Ok(UpdateInfo { + available: latest_ver > current_ver, + latest_version: release_json.tag_name.clone(), + changelog: release_json.body.clone(), + }) } pub fn download_and_install(event_tx: Sender) -> Result<(), Box> { - let client = reqwest::blocking::Client::builder() - .user_agent("PauseCat-Updater-v1") - .build()?; - - let release: GithubRelease = client.get(GITHUB_API_URL).send()?.json()?; - let asset = find_msi_asset(&release) - .ok_or("No MSI installer found in the latest release")?; + let data = winhttp_get(GITHUB_API_URL)?; + let release: GithubRelease = serde_json::from_slice(&data)?; + let asset = release.assets.iter().find(|a| a.name.to_lowercase().ends_with(".msi")).ok_or("No MSI installer found")?; let mut update_dir = Settings::get_config_dir(); update_dir.push("Updates"); - - // Purge old updates if update_dir.exists() { let _ = fs::remove_dir_all(&update_dir); } fs::create_dir_all(&update_dir)?; + let dest_path = update_dir.join(&asset.name); - let mut dest_path = update_dir.clone(); - dest_path.push(&asset.name); - - let mut response = client.get(&asset.browser_download_url).send()?; - let total_size = asset.size; - let mut downloaded = 0u64; - let mut buffer = [0; 8192]; - let mut file = fs::File::create(&dest_path)?; - - use std::io::Read; - loop { - let n = response.read(&mut buffer)?; - if n == 0 { break; } - std::io::Write::write_all(&mut file, &buffer[..n])?; - downloaded += n as u64; - if total_size > 0 { - let percentage = (downloaded as f64 / total_size as f64 * 100.0) as u32; - let _ = event_tx.send(AppEvent::UpdateProgress(percentage)); - } - } - let _ = event_tx.send(AppEvent::UpdateProgress(100)); + download_file_with_progress(&asset.browser_download_url, &dest_path, event_tx)?; - // PRO AUTO-RELAUNCH LOGIC: - // We launch a detached CMD.EXE that: - // 1. Starts the MSI installer (/i /passive) - // 2. Waits for msiexec to finish - // 3. Immediately restarts PauseCat from the installation directory unsafe { let exe_path = std::env::current_exe().unwrap_or_default(); - let exe_path_str = exe_path.to_str().unwrap_or_default(); - let msi_path_str = dest_path.to_str().unwrap_or_default(); + let cmd = format!("/c start /wait msiexec.exe /i \"{}\" /passive /norestart && timeout /t 2 /nobreak && start \"\" \"{}\"", dest_path.to_str().unwrap_or_default(), exe_path.to_str().unwrap_or_default()); + ShellExecuteW(None, windows::core::w!("open"), windows::core::w!("cmd.exe"), windows::core::PCWSTR(HSTRING::from(cmd).as_ptr()), None, SW_HIDE); + } + std::process::exit(0); +} - let operation = HSTRING::from("open"); - let file = HSTRING::from("cmd.exe"); - - // Command Chain: - // start /wait msiexec -> delay 2s for file release -> start PauseCat - let command = format!( - "/c start /wait msiexec.exe /i \"{}\" /passive /norestart && timeout /t 2 /nobreak && start \"\" \"{}\"", - msi_path_str, - exe_path_str - ); - let parameters = HSTRING::from(command); +fn download_file_with_progress(url: &str, dest_path: &std::path::Path, event_tx: Sender) -> Result<(), Box> { + unsafe { + let h_session = WinHttpHandle(WinHttpOpen(windows::core::w!("PauseCat-Updater/1.0"), WINHTTP_ACCESS_TYPE_DEFAULT_PROXY, None, None, 0)); + if h_session.0.is_null() { return Err(format!("WinHttpOpen failed: {:?}", windows::core::Error::from_thread()).into()); } + + let mut url_components = URL_COMPONENTS { dwStructSize: std::mem::size_of::() as u32, dwHostNameLength: u32::MAX, dwUrlPathLength: u32::MAX, ..Default::default() }; + let url_u16: Vec = url.encode_utf16().chain(std::iter::once(0)).collect(); + WinHttpCrackUrl(&url_u16, 0, &mut url_components)?; + + let host_name_u16: Vec = std::slice::from_raw_parts(url_components.lpszHostName.0, url_components.dwHostNameLength as usize) + .iter().copied().chain(std::iter::once(0)).collect(); + let path_name_u16: Vec = std::slice::from_raw_parts(url_components.lpszUrlPath.0, url_components.dwUrlPathLength as usize) + .iter().copied().chain(std::iter::once(0)).collect(); + + let h_connect = WinHttpHandle(WinHttpConnect(h_session.0, PCWSTR(host_name_u16.as_ptr()), url_components.nPort, 0)); + if h_connect.0.is_null() { return Err(format!("WinHttpConnect failed: {:?}", windows::core::Error::from_thread()).into()); } + + let h_request = WinHttpHandle(WinHttpOpenRequest(h_connect.0, windows::core::w!("GET"), PCWSTR(path_name_u16.as_ptr()), None, None, std::ptr::null(), if url.starts_with("https") { WINHTTP_FLAG_SECURE } else { WINHTTP_OPEN_REQUEST_FLAGS(0) })); + if h_request.0.is_null() { return Err(format!("WinHttpOpenRequest failed: {:?}", windows::core::Error::from_thread()).into()); } + + WinHttpSendRequest(h_request.0, None, None, 0, 0, 0)?; + WinHttpReceiveResponse(h_request.0, std::ptr::null_mut())?; + let mut status_code: u32 = 0; + let mut dw_size = std::mem::size_of::() as u32; + WinHttpQueryHeaders(h_request.0, WINHTTP_QUERY_STATUS_CODE | WINHTTP_QUERY_FLAG_NUMBER, None, Some(&mut status_code as *mut _ as *mut _), &mut dw_size, std::ptr::null_mut())?; - ShellExecuteW( - None, - windows::core::PCWSTR(operation.as_ptr()), - windows::core::PCWSTR(file.as_ptr()), - windows::core::PCWSTR(parameters.as_ptr()), - None, - SW_HIDE, // Hide the black console window - ); - } + if status_code == 301 || status_code == 302 || status_code == 307 || status_code == 308 { + let mut redirect_url = [0u16; 4096]; let mut dw_size = 8192u32; + WinHttpQueryHeaders(h_request.0, WINHTTP_QUERY_LOCATION, None, Some(redirect_url.as_mut_ptr() as *mut _), &mut dw_size, std::ptr::null_mut())?; + let new_url = String::from_utf16_lossy(&redirect_url[.. (dw_size as usize / 2)]).trim_matches('\0').to_string(); + return download_file_with_progress(&new_url, dest_path, event_tx); + } - #[cfg(not(test))] - std::process::exit(0); - #[cfg(test)] - Ok(()) + if status_code != 200 { return Err(format!("HTTP Error: {}", status_code).into()); } + let mut content_length: u64 = 0; let mut dw_size = std::mem::size_of::() as u32; + let _ = WinHttpQueryHeaders(h_request.0, WINHTTP_QUERY_CONTENT_LENGTH | WINHTTP_QUERY_FLAG_NUMBER64, None, Some(&mut content_length as *mut _ as *mut _), &mut dw_size, std::ptr::null_mut()); + let mut file = fs::File::create(dest_path)?; + let mut downloaded = 0u64; let mut dw_size: u32 = 0; + loop { + WinHttpQueryDataAvailable(h_request.0, &mut dw_size as *mut _)?; + if dw_size == 0 { break; } + let mut buffer = vec![0u8; dw_size as usize]; + let mut dw_read: u32 = 0; + WinHttpReadData(h_request.0, buffer.as_mut_ptr() as *mut _, dw_size, &mut dw_read)?; + std::io::Write::write_all(&mut file, &buffer[..dw_read as usize])?; + downloaded += dw_read as u64; + if content_length > 0 { let _ = event_tx.send(AppEvent::UpdateProgress((downloaded as f64 / content_length as f64 * 100.0) as u32)); } + } + Ok(()) + } } pub fn cleanup_updates() { let mut update_dir = Settings::get_config_dir(); update_dir.push("Updates"); - if update_dir.exists() { - let _ = fs::remove_dir_all(&update_dir); - } + if update_dir.exists() { let _ = fs::remove_dir_all(&update_dir); } } pub fn ensure_assets_sync(event_tx: Sender) { thread::spawn(move || { - log::info!("Starting asset sync check..."); - - // 1. Determine the target path for the download (Config Dir) let mut config_asset_path = Settings::get_config_dir(); config_asset_path.push("assets"); - if !config_asset_path.exists() { - let _ = fs::create_dir_all(&config_asset_path); - } + if !config_asset_path.exists() { let _ = fs::create_dir_all(&config_asset_path); } config_asset_path.push("default.webm"); - - // 2. Check if asset already exists and is valid - if config_asset_path.exists() && config_asset_path.metadata().map(|m| m.len() > 1000).unwrap_or(false) { - log::info!("Asset already exists and is valid: {:?}", config_asset_path); - return; - } - - log::info!("Asset missing or invalid, starting background fetch..."); - - let client = match reqwest::blocking::Client::builder() - .user_agent("PauseCat-Asset-Syncer-v1") - .timeout(std::time::Duration::from_secs(60)) - .build() { - Ok(c) => c, - Err(e) => { - log::error!("Failed to create HTTP client: {}", e); - let _ = event_tx.send(AppEvent::AssetDownloadError(format!("Client error: {}", e))); - return; - } - }; - - // Retry loop for GitHub API - let mut release: Option = None; - for attempt in 1..=3 { - log::info!("Fetching latest release info (attempt {}/3)...", attempt); - match client.get(GITHUB_API_URL) - .header("Accept", "application/vnd.github+json") - .header("X-GitHub-Api-Version", "2022-11-28") - .send() - .and_then(|r| r.json::()) { - Ok(r) => { - release = Some(r); - break; - } - Err(e) => { - log::error!("Attempt {} failed: {}", attempt, e); - if attempt < 3 { thread::sleep(std::time::Duration::from_secs(5)); } - else { - let _ = event_tx.send(AppEvent::AssetDownloadError(format!("GitHub API error: {}", e))); - return; - } - } - } - } - - let release = release.unwrap(); - let asset = match release.assets.iter().find(|a| a.name == "default.webm") { - Some(a) => a, - None => { - log::warn!("default.webm not found in latest release assets"); - let _ = event_tx.send(AppEvent::AssetDownloadError("Asset not found in latest release".to_string())); - return; - } - }; - - log::info!("Downloading default.webm ({} bytes) from {}", asset.size, asset.browser_download_url); - // Initial progress update to trigger UI state - let _ = event_tx.send(AppEvent::AssetDownloadProgress(0)); - - match client.get(&asset.browser_download_url).send() { - Ok(mut response) => { - let total_size = response.content_length().unwrap_or(asset.size as u64); - match fs::File::create(&config_asset_path) { - Ok(mut file) => { - let mut buffer = [0; 16384]; - let mut downloaded = 0; - let mut last_update = std::time::Instant::now(); - - loop { - match response.read(&mut buffer) { - Ok(0) => break, - Ok(n) => { - if let Err(e) = std::io::Write::write_all(&mut file, &buffer[..n]) { - log::error!("Failed to write to file: {}", e); - let _ = event_tx.send(AppEvent::AssetDownloadError(format!("IO error: {}", e))); - return; - } - downloaded += n; - - if last_update.elapsed().as_millis() > 200 { - let percentage = (downloaded as f32 / total_size as f32 * 100.0) as u32; - let _ = event_tx.send(AppEvent::AssetDownloadProgress(percentage.min(99))); - last_update = std::time::Instant::now(); - } - } - Err(e) => { - log::error!("Failed to read response: {}", e); - let _ = event_tx.send(AppEvent::AssetDownloadError(format!("Download interrupted: {}", e))); - return; - } - } - } - log::info!("Successfully downloaded {} bytes to {:?}", downloaded, config_asset_path); - let _ = event_tx.send(AppEvent::AssetDownloadProgress(100)); - let _ = event_tx.send(AppEvent::AssetDownloaded("default.webm".to_string())); - } - Err(e) => { - log::error!("Failed to create file: {}", e); - let _ = event_tx.send(AppEvent::AssetDownloadError(format!("File creation error: {}", e))); - } + if config_asset_path.exists() && config_asset_path.metadata().map(|m| m.len() > 1000).unwrap_or(false) { return; } + if let Ok(data) = winhttp_get(GITHUB_API_URL) { + if let Ok(release) = serde_json::from_slice::(&data) { + if let Some(asset) = release.assets.iter().find(|a| a.name == "default.webm") { + let _ = download_file_with_progress(&asset.browser_download_url, &config_asset_path, event_tx.clone()); + let _ = event_tx.send(AppEvent::AssetDownloaded("default.webm".to_string())); + crate::app::wakeup_main_thread(); } } - Err(e) => { - log::error!("Failed to start download: {}", e); - let _ = event_tx.send(AppEvent::AssetDownloadError(format!("Download start error: {}", e))); - } } }); } - -#[cfg(test)] -mod internal_tests { - use super::*; - - #[test] - fn test_github_release_parsing_logic() { - let json = r#"{ - "tag_name": "v1.2.0", - "body": "New version", - "assets": [ - {"name": "pausecat.msi", "browser_download_url": "http://test.com/msi", "size": 1000}, - {"name": "readme.txt", "browser_download_url": "http://test.com/txt", "size": 500} - ] - }"#; - - let release = parse_github_release(json).unwrap(); - assert_eq!(release.tag_name, "v1.2.0"); - assert_eq!(release.assets.len(), 2); - - let info = parse_and_check_version(&release, "1.1.0").unwrap(); - assert!(info.available); - assert_eq!(info.latest_version, "v1.2.0"); - - let msi = find_msi_asset(&release).unwrap(); - assert_eq!(msi.name, "pausecat.msi"); - } - - #[test] - fn test_no_update_available_logic() { - let release = GithubRelease { - tag_name: "v1.0.0".to_string(), - body: "No changes".to_string(), - assets: vec![], - }; - let info = parse_and_check_version(&release, "1.0.0").unwrap(); - assert!(!info.available); - } - - #[test] - fn test_cleanup_updates_smoke() { - cleanup_updates(); - } -}