From 029232a269816ae88c9c9be6e08e594a5610131b Mon Sep 17 00:00:00 2001 From: H-Chris233 Date: Sat, 9 May 2026 07:44:57 +0800 Subject: [PATCH 1/5] Make backend state tests runnable without Tauri Windows cannot reliably execute the full Tauri lib test harness in CI because the process exits before Rust tests start in the desktop runtime loader path. Split the coordinator session state transitions into a Tauri-free Rust module used by coordinator.rs, and add a small Rust-only backend test crate that path-includes coordinator/hotkey/recorder/insertion logic for real Windows execution. Constraint: User scoped this follow-up to backend-only Rust-to-Rust tests, not app/runtime smoke tests. Constraint: Existing Windows CI still keeps full lib --no-run cfg/link coverage. Rejected: Keep trying to execute cargo test --lib on Windows | it starts the full Tauri-linked test binary and fails before unit tests run. Rejected: Rework release/runtime DLL handling | outside the backend unit-test scope. Confidence: high Scope-risk: moderate Directive: Keep Windows actual unit execution on the Rust-only backend-tests manifest unless the full Tauri lib harness is proven runnable on clean Windows CI. Tested: cargo test --manifest-path openless-all/app/src-tauri/backend-tests/Cargo.toml Tested: cargo test --manifest-path openless-all/app/src-tauri/Cargo.toml --lib Tested: cargo check --manifest-path openless-all/app/src-tauri/Cargo.toml Tested: python yaml parse for .github/workflows/ci.yml; git diff --check Not-tested: Windows runner execution; intended to be verified by GitHub Actions. Related: #295 Co-authored-by: OmX --- .github/workflows/ci.yml | 8 +- .../app/src-tauri/backend-tests/Cargo.lock | 2242 +++++++++++++++++ .../app/src-tauri/backend-tests/Cargo.toml | 28 + .../backend-tests/tests/backend_rust.rs | 38 + openless-all/app/src-tauri/src/coordinator.rs | 192 +- .../app/src-tauri/src/coordinator_state.rs | 484 ++++ openless-all/app/src-tauri/src/lib.rs | 1 + 7 files changed, 2828 insertions(+), 165 deletions(-) create mode 100644 openless-all/app/src-tauri/backend-tests/Cargo.lock create mode 100644 openless-all/app/src-tauri/backend-tests/Cargo.toml create mode 100644 openless-all/app/src-tauri/backend-tests/tests/backend_rust.rs create mode 100644 openless-all/app/src-tauri/src/coordinator_state.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6f7453e3..788d6f91 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -98,10 +98,16 @@ jobs: - name: Compile Rust backend unit tests (Windows) # Windows runner 能链接 lib test binary,但干净镜像缺少可选 native runtime # DLL entrypoint 时,进程会在 test harness 启动前退出。这里保留 cfg/link - # 覆盖;共享单测在 macOS / Linux 上实际执行。 + # 覆盖;Rust-only 后端单测由下一步实际执行。 if: runner.os == 'Windows' run: cargo test --manifest-path src-tauri/Cargo.toml --lib --no-run + - name: Run Rust-only backend unit tests (Windows) + # 只把纯 Rust 后端模块编进独立 test crate,不链接完整 Tauri app lib。 + # 这样 Windows CI 能实际执行 coordinator/hotkey/recorder/insertion 逻辑单测。 + if: runner.os == 'Windows' + run: cargo test --manifest-path src-tauri/backend-tests/Cargo.toml + - name: Verify version sync across all 5 files # 两个平台都跑这个校验:Windows runner 自带 git-bash,跨 shell 表现一致。 # 一旦版本号 drift 立刻 fail,避免发版时再发现漏改。 diff --git a/openless-all/app/src-tauri/backend-tests/Cargo.lock b/openless-all/app/src-tauri/backend-tests/Cargo.lock new file mode 100644 index 00000000..a10b693e --- /dev/null +++ b/openless-all/app/src-tauri/backend-tests/Cargo.lock @@ -0,0 +1,2242 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[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 = "alsa" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed7572b7ba83a31e20d1b48970ee402d2e3e0537dcfe0a3ff4d6eb7508617d43" +dependencies = [ + "alsa-sys", + "bitflags 2.11.1", + "cfg-if", + "libc", +] + +[[package]] +name = "alsa-sys" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db8fee663d06c4e303404ef5f40488a53e062f89ba8bfed81f42325aafad1527" +dependencies = [ + "libc", + "pkg-config", +] + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "arboard" +version = "3.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0348a1c054491f4bfe6ab86a7b6ab1e44e45d899005de92f58b3df180b36ddaf" +dependencies = [ + "clipboard-win", + "image", + "log", + "objc2 0.6.4", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation", + "parking_lot", + "percent-encoding", + "windows-sys 0.60.2", + "x11rb", +] + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "bindgen" +version = "0.72.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" +dependencies = [ + "bitflags 2.11.1", + "cexpr", + "clang-sys", + "itertools", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", + "syn", +] + +[[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" +dependencies = [ + "serde_core", +] + +[[package]] +name = "block" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" + +[[package]] +name = "block-sys" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae85a0696e7ea3b835a453750bf002770776609115e6d25c6d2ff28a8200f7e7" +dependencies = [ + "objc-sys", +] + +[[package]] +name = "block2" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e58aa60e59d8dbfcc36138f5f18be5f24394d33b38b24f7fd0b1caa33095f22f" +dependencies = [ + "block-sys", + "objc2 0.5.2", +] + +[[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-lite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" + +[[package]] +name = "cc" +version = "1.2.62" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1dce859f0832a7d088c4f1119888ab94ef4b5d6795d1ce05afb7fe159d79f98" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "clang-sys" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" +dependencies = [ + "glob", + "libc", + "libloading", +] + +[[package]] +name = "clipboard-win" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bde03770d3df201d4fb868f2c9c59e66a3e4e2bd06692a0fe701e7103c7e84d4" +dependencies = [ + "error-code", +] + +[[package]] +name = "cocoa" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "667fdc068627a2816b9ff831201dd9864249d6ee8d190b9532357f1fc0f61ea7" +dependencies = [ + "bitflags 1.3.2", + "block", + "core-foundation 0.9.4", + "core-graphics 0.21.0", + "foreign-types 0.3.2", + "libc", + "objc", +] + +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "core-foundation" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57d24c7a13c43e870e37c1556b74555437870a04514f7685f5b354e090567171" +dependencies = [ + "core-foundation-sys 0.7.0", + "libc", +] + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys 0.8.7", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3a71ab494c0b5b860bdc8407ae08978052417070c2ced38573a9157ad75b8ac" + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "core-graphics" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3889374e6ea6ab25dba90bb5d96202f61108058361f6dc72e8b03e6f8bbe923" +dependencies = [ + "bitflags 1.3.2", + "core-foundation 0.7.0", + "foreign-types 0.3.2", + "libc", +] + +[[package]] +name = "core-graphics" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52a67c4378cf203eace8fb6567847eb641fd6ff933c1145a115c6ee820ebb978" +dependencies = [ + "bitflags 1.3.2", + "core-foundation 0.9.4", + "foreign-types 0.3.2", + "libc", +] + +[[package]] +name = "core-graphics" +version = "0.23.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c07782be35f9e1140080c6b96f0d44b739e2278479f64e02fdab4e32dfd8b081" +dependencies = [ + "bitflags 1.3.2", + "core-foundation 0.9.4", + "core-graphics-types", + "foreign-types 0.5.0", + "libc", +] + +[[package]] +name = "core-graphics-types" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf" +dependencies = [ + "bitflags 1.3.2", + "core-foundation 0.9.4", + "libc", +] + +[[package]] +name = "coreaudio-rs" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "321077172d79c662f64f5071a03120748d5bb652f5231570141be24cfcd2bace" +dependencies = [ + "bitflags 1.3.2", + "core-foundation-sys 0.8.7", + "coreaudio-sys", +] + +[[package]] +name = "coreaudio-sys" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ceec7a6067e62d6f931a2baf6f3a751f4a892595bcec1461a3c94ef9949864b6" +dependencies = [ + "bindgen", +] + +[[package]] +name = "cpal" +version = "0.15.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "873dab07c8f743075e57f524c583985fbaf745602acbe916a01539364369a779" +dependencies = [ + "alsa", + "core-foundation-sys 0.8.7", + "coreaudio-rs", + "dasp_sample", + "jni", + "js-sys", + "libc", + "mach2", + "ndk", + "ndk-context", + "oboe", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "windows 0.54.0", +] + +[[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-channel" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +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 = "dasp_sample" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c87e182de0887fd5361989c677c4e8f5000cd9491d6d563161a8f3a5519fc7f" + +[[package]] +name = "dispatch2" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" +dependencies = [ + "bitflags 2.11.1", + "objc2 0.6.4", +] + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "enigo" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0087a01fc8591217447d28005379fb5a183683cc83f0a4707af28cc6603f70fb" +dependencies = [ + "core-graphics 0.23.2", + "foreign-types-shared 0.3.1", + "icrate", + "libc", + "log", + "objc2 0.5.2", + "windows 0.56.0", + "xkbcommon", + "xkeysym", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "error-code" +version = "3.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dea2df4cf52843e0452895c455a1a2cfbb842a1e7329671acf418fdc53ed4c59" + +[[package]] +name = "fax" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caf1079563223d5d59d83c85886a56e586cfd5c1a26292e971a0fa266531ac5a" + +[[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 = "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 0.1.1", +] + +[[package]] +name = "foreign-types" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" +dependencies = [ + "foreign-types-macros", + "foreign-types-shared 0.3.1", +] + +[[package]] +name = "foreign-types-macros" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "foreign-types-shared" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[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-task", + "pin-project-lite", + "slab", +] + +[[package]] +name = "gethostname" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bd49230192a3797a9a4d6abe9b3eed6f7fa4c8a8a4947977c6f80025f92cbd8" +dependencies = [ + "rustix", + "windows-link", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi 5.3.0", + "wasip2", +] + +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", + "wasip2", + "wasip3", +] + +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + +[[package]] +name = "global-hotkey" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41fbb3a4e56c901ee66c190fdb3fa08344e6d09593cc6c61f8eb9add7144b271" +dependencies = [ + "crossbeam-channel", + "keyboard-types", + "objc2 0.6.4", + "objc2-app-kit", + "once_cell", + "thiserror 2.0.18", + "windows-sys 0.59.0", + "x11-dl", +] + +[[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 = "icrate" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fb69199826926eb864697bddd27f73d9fddcffc004f5733131e15b465e30642" +dependencies = [ + "block2", + "objc2 0.5.2", +] + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "image" +version = "0.25.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104" +dependencies = [ + "bytemuck", + "byteorder-lite", + "moxcms", + "num-traits", + "png", + "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 = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if", + "combine", + "jni-sys 0.3.1", + "log", + "thiserror 1.0.69", + "walkdir", + "windows-sys 0.45.0", +] + +[[package]] +name = "jni-sys" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258" +dependencies = [ + "jni-sys 0.4.1", +] + +[[package]] +name = "jni-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" +dependencies = [ + "jni-sys-macros", +] + +[[package]] +name = "jni-sys-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "jobserver" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +dependencies = [ + "getrandom 0.3.4", + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.98" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67df7112613f8bfd9150013a0314e196f4800d3201ae742489d999db2f979f08" +dependencies = [ + "cfg-if", + "futures-util", + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "keyboard-types" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b750dcadc39a09dbadd74e118f6dd6598df77fa01df0cfcdc52c28dece74528a" +dependencies = [ + "bitflags 2.11.1", + "serde", + "unicode-segmentation", +] + +[[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 = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "libloading" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" +dependencies = [ + "cfg-if", + "windows-link", +] + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "mach2" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d640282b302c0bb0a2a8e0233ead9035e3bed871f0b7e81fe4a1ec829765db44" +dependencies = [ + "libc", +] + +[[package]] +name = "malloc_buf" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" +dependencies = [ + "libc", +] + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "memmap2" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43a5a03cefb0d953ec0be133036f14e109412fa594edc2f77227249db66cc3ed" +dependencies = [ + "libc", +] + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "moxcms" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb85c154ba489f01b25c0d36ae69a87e4a1c73a72631fc6c0eb6dde34a73e44b" +dependencies = [ + "num-traits", + "pxfm", +] + +[[package]] +name = "ndk" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2076a31b7010b17a38c01907c45b945e8f11495ee4dd588309718901b1f7a5b7" +dependencies = [ + "bitflags 2.11.1", + "jni-sys 0.3.1", + "log", + "ndk-sys", + "num_enum", + "thiserror 1.0.69", +] + +[[package]] +name = "ndk-context" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" + +[[package]] +name = "ndk-sys" +version = "0.5.0+25.2.9519653" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c196769dd60fd4f363e11d948139556a344e79d451aeb2fa2fd040738ef7691" +dependencies = [ + "jni-sys 0.3.1", +] + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "num-derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_enum" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26" +dependencies = [ + "num_enum_derive", + "rustversion", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "objc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" +dependencies = [ + "malloc_buf", +] + +[[package]] +name = "objc-sys" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb91bdd390c7ce1a8607f35f3ca7151b65afc0ff5ff3b34fa350f7d7c7e4310" + +[[package]] +name = "objc2" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46a785d4eeff09c14c487497c162e92766fbb3e4059a71840cecc03d9a50b804" +dependencies = [ + "objc-sys", + "objc2-encode", +] + +[[package]] +name = "objc2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" +dependencies = [ + "objc2-encode", +] + +[[package]] +name = "objc2-app-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" +dependencies = [ + "bitflags 2.11.1", + "objc2 0.6.4", + "objc2-core-graphics", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" +dependencies = [ + "bitflags 2.11.1", + "dispatch2", + "objc2 0.6.4", +] + +[[package]] +name = "objc2-core-graphics" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" +dependencies = [ + "bitflags 2.11.1", + "dispatch2", + "objc2 0.6.4", + "objc2-core-foundation", + "objc2-io-surface", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + +[[package]] +name = "objc2-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" +dependencies = [ + "bitflags 2.11.1", + "objc2 0.6.4", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-io-surface" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" +dependencies = [ + "bitflags 2.11.1", + "objc2 0.6.4", + "objc2-core-foundation", +] + +[[package]] +name = "oboe" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8b61bebd49e5d43f5f8cc7ee2891c16e0f41ec7954d36bcb6c14c5e0de867fb" +dependencies = [ + "jni", + "ndk", + "ndk-context", + "num-derive", + "num-traits", + "oboe-sys", +] + +[[package]] +name = "oboe-sys" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c8bb09a4a2b1d668170cfe0a7d5bc103f8999fb316c98099b6a9939c9f2e79d" +dependencies = [ + "cc", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "openless-backend-tests" +version = "0.1.0" +dependencies = [ + "arboard", + "cpal", + "enigo", + "global-hotkey", + "log", + "once_cell", + "parking_lot", + "rdev", + "serde", + "serde_json", + "thiserror 1.0.69", + "uuid", + "windows 0.58.0", +] + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[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.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" +dependencies = [ + "bitflags 2.11.1", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + +[[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-macro-crate" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +dependencies = [ + "toml_edit", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "pxfm" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0c5ccf5294c6ccd63a74f1565028353830a9c2f5eb0c682c355c471726a6e3f" + +[[package]] +name = "quick-error" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rdev" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00552ca2dc2f93b84cd7b5581de49549411e4e41d89e1c691bcb93dc4be360c3" +dependencies = [ + "cocoa", + "core-foundation 0.7.0", + "core-foundation-sys 0.7.0", + "core-graphics 0.19.2", + "lazy_static", + "libc", + "winapi", + "x11", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.11.1", +] + +[[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 = "rustc-hash" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" + +[[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 = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "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 = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "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.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b63feaf3343d35b6ca4d50483f94843803b0f51634937cc2ec519fc32232bc52" +dependencies = [ + "fax", + "flate2", + "half", + "quick-error", + "weezl", + "zune-jpeg", +] + +[[package]] +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.25.11+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b59c4d22ed448339746c59b905d24568fcbb3ab65a500494f7b8c3e97739f2b" +dependencies = [ + "indexmap", + "toml_datetime", + "toml_parser", + "winnow", +] + +[[package]] +name = "toml_parser" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" +dependencies = [ + "winnow", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-segmentation" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "uuid" +version = "1.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd74a9687298c6858e9b88ec8935ec45d22e8fd5e6394fa1bd4e99a87789c76" +dependencies = [ + "getrandom 0.4.2", + "js-sys", + "serde_core", + "wasm-bindgen", +] + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "wasip2" +version = "1.0.3+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" +dependencies = [ + "wit-bindgen 0.57.1", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen 0.51.0", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.121" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49ace1d07c165b0864824eee619580c4689389afa9dc9ed3a4c75040d82e6790" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.71" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96492d0d3ffba25305a7dc88720d250b1401d7edca02cc3bcd50633b424673b8" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.121" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e68e6f4afd367a562002c05637acb8578ff2dea1943df76afb9e83d177c8578" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.121" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d95a9ec35c64b2a7cb35d3fead40c4238d0940c86d107136999567a4703259f2" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.121" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4e0100b01e9f0d03189a92b96772a1fb998639d981193d7dbab487302513441" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags 2.11.1", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + +[[package]] +name = "web-sys" +version = "0.3.98" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b572dff8bcf38bad0fa19729c89bb5748b2b9b1d8be70cf90df697e3a8f32aa" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "weezl" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows" +version = "0.54.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9252e5725dbed82865af151df558e754e4a3c2c30818359eb17465f1346a1b49" +dependencies = [ + "windows-core 0.54.0", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows" +version = "0.56.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1de69df01bdf1ead2f4ac895dc77c9351aefff65b2f3db429a343f9cbf05e132" +dependencies = [ + "windows-core 0.56.0", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd04d41d93c4992d421894c18c8b43496aa748dd4c081bac0dc93eb0489272b6" +dependencies = [ + "windows-core 0.58.0", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-core" +version = "0.54.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12661b9c89351d684a50a8a643ce5f608e20243b9fb84687800163429f161d65" +dependencies = [ + "windows-result 0.1.2", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-core" +version = "0.56.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4698e52ed2d08f8658ab0c39512a7c00ee5fe2688c65f8c0a4f06750d729f2a6" +dependencies = [ + "windows-implement 0.56.0", + "windows-interface 0.56.0", + "windows-result 0.1.2", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-core" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ba6d44ec8c2591c134257ce647b7ea6b20335bf6379a27dac5f1641fcf59f99" +dependencies = [ + "windows-implement 0.58.0", + "windows-interface 0.58.0", + "windows-result 0.2.0", + "windows-strings", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-implement" +version = "0.56.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6fc35f58ecd95a9b71c4f2329b911016e6bec66b3f2e6a4aad86bd2e99e2f9b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-implement" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.56.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08990546bf4edef8f431fa6326e032865f27138718c587dc21bc0265bbcb57cc" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-result" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e383302e8ec8515204254685643de10811af0ed97ea37210dc26fb0032647f8" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-result" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-strings" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" +dependencies = [ + "windows-result 0.2.0", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "winnow" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ee1708bef14716a11bae175f579062d4554d95be2c6829f518df847b7b3fdd0" +dependencies = [ + "memchr", +] + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen" +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 = "x11" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "502da5464ccd04011667b11c435cb992822c2c0dbde1770c988480d312a0db2e" +dependencies = [ + "libc", + "pkg-config", +] + +[[package]] +name = "x11-dl" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38735924fedd5314a6e548792904ed8c6de6636285cb9fec04d5b1db85c1516f" +dependencies = [ + "libc", + "once_cell", + "pkg-config", +] + +[[package]] +name = "x11rb" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9993aa5be5a26815fe2c3eacfc1fde061fc1a1f094bf1ad2a18bf9c495dd7414" +dependencies = [ + "gethostname", + "rustix", + "x11rb-protocol", +] + +[[package]] +name = "x11rb-protocol" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea6fc2961e4ef194dcbfe56bb845534d0dc8098940c7e5c012a258bfec6701bd" + +[[package]] +name = "xkbcommon" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13867d259930edc7091a6c41b4ce6eee464328c6ff9659b7e4c668ca20d4c91e" +dependencies = [ + "libc", + "memmap2", + "xkeysym", +] + +[[package]] +name = "xkeysym" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9cc00251562a284751c9973bace760d86c0276c471b4be569fe6b068ee97a56" + +[[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 = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" + +[[package]] +name = "zune-core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb8a0807f7c01457d0379ba880ba6322660448ddebc890ce29bb64da71fb40f9" + +[[package]] +name = "zune-jpeg" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27bc9d5b815bc103f142aa054f561d9187d191692ec7c2d1e2b4737f8dbd7296" +dependencies = [ + "zune-core", +] diff --git a/openless-all/app/src-tauri/backend-tests/Cargo.toml b/openless-all/app/src-tauri/backend-tests/Cargo.toml new file mode 100644 index 00000000..8b8b5e47 --- /dev/null +++ b/openless-all/app/src-tauri/backend-tests/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "openless-backend-tests" +version = "0.1.0" +edition = "2021" +rust-version = "1.77" +publish = false + +[dependencies] +arboard = "3" +cpal = "0.15" +enigo = "0.2" +global-hotkey = "0.6" +log = "0.4" +once_cell = "1" +parking_lot = "0.12" +rdev = "0.5" +serde = { version = "1", features = ["derive"] } +serde_json = "1" +thiserror = "1" +uuid = { version = "1", features = ["v4", "serde"] } + +[target.'cfg(target_os = "windows")'.dependencies] +windows = { version = "0.58", features = [ + "Win32_Foundation", + "Win32_System_Threading", + "Win32_UI_Input_KeyboardAndMouse", + "Win32_UI_WindowsAndMessaging", +] } diff --git a/openless-all/app/src-tauri/backend-tests/tests/backend_rust.rs b/openless-all/app/src-tauri/backend-tests/tests/backend_rust.rs new file mode 100644 index 00000000..23e75a81 --- /dev/null +++ b/openless-all/app/src-tauri/backend-tests/tests/backend_rust.rs @@ -0,0 +1,38 @@ +//! Rust-only backend unit harness. +//! +//! 这个测试 crate 只把纯 Rust 后端模块按源码路径编进来,不链接完整 Tauri +//! `openless_lib`,避免 Windows CI 在 test harness 启动前被桌面运行时 DLL 拦截。 + +#![allow(dead_code, unused_variables)] + +mod asr { + pub mod local { + pub mod foundry { + pub const DEFAULT_MODEL_ALIAS: &str = "whisper-large-v3-turbo"; + pub const PROVIDER_ID: &str = "foundry-local-whisper"; + } + + pub mod foundry_native { + pub fn normalize_runtime_source_str(value: &str) -> String { + match value.trim() { + "nuget" | "ort-nightly" => value.trim().to_string(), + _ => "auto".to_string(), + } + } + } + } +} + +#[path = "../../src/coordinator_state.rs"] +mod coordinator_state; +#[path = "../../src/hotkey.rs"] +mod hotkey; +#[cfg(not(target_os = "macos"))] +#[path = "../../src/insertion.rs"] +mod insertion; +#[path = "../../src/recorder.rs"] +mod recorder; +#[path = "../../src/shortcut_binding.rs"] +mod shortcut_binding; +#[path = "../../src/types.rs"] +mod types; diff --git a/openless-all/app/src-tauri/src/coordinator.rs b/openless-all/app/src-tauri/src/coordinator.rs index fd85a4cf..8cd0d2f3 100644 --- a/openless-all/app/src-tauri/src/coordinator.rs +++ b/openless-all/app/src-tauri/src/coordinator.rs @@ -16,16 +16,6 @@ use parking_lot::Mutex; use tauri::{async_runtime, AppHandle, Emitter, Manager}; use uuid::Uuid; -type SessionId = Uuid; - -fn new_session_id() -> SessionId { - Uuid::new_v4() -} - -fn initial_session_id() -> SessionId { - Uuid::nil() -} - #[cfg(target_os = "windows")] use crate::asr::local::{foundry, FoundryLocalRuntime, FoundryLocalWhisperAsr}; use crate::asr::{ @@ -33,6 +23,13 @@ use crate::asr::{ WhisperBatchASR, }; use crate::combo_hotkey::{ComboHotkeyError, ComboHotkeyEvent, ComboHotkeyMonitor}; +use crate::coordinator_state::{ + begin_cancel_session_state, begin_recording_abort_before_restore, begin_session_state, + finish_cancel_session_state, finish_starting_session_state, initial_session_id, new_session_id, + publish_abort_idle_after_restore, request_stop_during_starting_state, + start_processing_if_listening, startup_race_status, BeginOutcome, SessionId, SessionPhase, + SessionState, StartupRaceStatus, +}; use crate::hotkey::{HotkeyEvent, HotkeyMonitor}; use crate::insertion::TextInserter; use crate::persistence::{ @@ -53,19 +50,6 @@ use crate::windows_ime_ipc::ImeSubmitTarget; #[cfg(target_os = "windows")] use crate::windows_ime_session::{PreparedWindowsImeSession, WindowsImeSessionController}; -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -enum SessionPhase { - Idle, - Starting, - Listening, - Processing, - /// 已经过了最后一次 cancel 检查、即将 / 正在调用 inserter.insert 的窗口。 - /// cancel_session 在此阶段拒绝介入:Cmd+V 模拟点击已开始或已发出, - /// 无法撤销,硬把 cancelled=true 也救不回来,只会让 UI 出现 cancelled - /// 但实际还是插入了的诡异状态。详见 PR 修 Codex audit HIGH #2。 - Inserting, -} - enum ActiveAsr { Volcengine(Arc), Whisper(Arc), @@ -102,25 +86,6 @@ impl SessionResource { } } -struct SessionState { - phase: SessionPhase, - started_at: Instant, - /// Starting 阶段(ASR 握手中)按下 stop 边沿(toggle 第二次按 / hold 松开)→ - /// 等握手完成 phase=Listening 后立刻 end_session,不丢边沿。issue #51。 - pending_stop: bool, - /// 用户在 Processing 阶段按 Esc 取消:end_session 在 polish/insert 检查点跳过插入 + - /// 跳过 history.append。issue #52。 - cancelled: bool, - focus_target: Option, - /// 每次 begin_session 生成新的 UUID session id。 - /// recorder error monitor 持有 captured id,处理时若与当前不等说明 - /// 是上一 session 的迟到错误,必须 drop,不要 abort 当前 active session。 - session_id: SessionId, - /// 用户开始 dictation 时所处的前台 app 标签("Mail (com.apple.mail)" / Windows 窗口标题)。 - /// 用作 LLM polish/translate 的上下文前提,让模型按 app 调风格。详见 issue #116。 - front_app: Option, -} - struct SharedRecordingMuteState { guard: Option, holders: u32, @@ -135,20 +100,6 @@ impl SharedRecordingMuteState { } } -impl Default for SessionState { - fn default() -> Self { - Self { - phase: SessionPhase::Idle, - started_at: Instant::now(), - pending_stop: false, - cancelled: false, - focus_target: None, - session_id: initial_session_id(), - front_app: None, - } - } -} - pub struct Coordinator { inner: Arc, } @@ -1704,10 +1655,9 @@ async fn handle_released(inner: &Arc) { fn request_stop_during_starting(inner: &Arc, reason: &str) { { let mut state = inner.state.lock(); - if state.phase != SessionPhase::Starting { + if !request_stop_during_starting_state(&mut state) { return; } - state.pending_stop = true; } log::info!("[coord] {reason} during Starting — queued"); stop_recorder_if_pending_start_stop(inner); @@ -1956,23 +1906,15 @@ fn window_key_matches_trigger(trigger: crate::types::HotkeyTrigger, key: &str, c async fn begin_session(inner: &Arc) -> Result<(), String> { let current_session_id = { let mut state = inner.state.lock(); - if state.phase != SessionPhase::Idle { + let Some(session_id) = + begin_session_state(&mut state, capture_focus_target(), capture_frontmost_app()) + else { return Ok(()); - } - state.phase = SessionPhase::Starting; - state.started_at = Instant::now(); - // 新会话清掉旧 pending_stop / cancelled,避免上一会话遗留触发奇怪行为 - state.pending_stop = false; - state.cancelled = false; - state.focus_target = capture_focus_target(); - // 新建 UUID session_id;spawn 出去的 recorder error monitor 会捕获这个值, - // 如果迟到错误到达时 id 已不匹配就 drop,不会误中止后续 session。 - state.session_id = new_session_id(); - state.front_app = capture_frontmost_app(); + }; if let Some(label) = state.front_app.as_deref() { log::info!("[coord] front_app captured: {label}"); } - state.session_id + session_id }; #[cfg(target_os = "windows")] { @@ -2356,33 +2298,6 @@ fn abort_recording_with_error(inner: &Arc, message: String) { schedule_capsule_idle(inner, CAPSULE_AUTO_HIDE_DELAY_MS); } -struct RecordingAbort { - elapsed: u64, - session_id: SessionId, -} - -fn begin_recording_abort_before_restore(state: &mut SessionState) -> Option { - if state.cancelled - || !matches!( - state.phase, - SessionPhase::Starting | SessionPhase::Listening - ) - { - return None; - } - state.cancelled = true; - Some(RecordingAbort { - elapsed: state.started_at.elapsed().as_millis() as u64, - session_id: state.session_id, - }) -} - -fn publish_abort_idle_after_restore(state: &mut SessionState, session_id: SessionId) { - if state.session_id == session_id { - state.phase = SessionPhase::Idle; - } -} - async fn start_recorder_and_enter_listening( inner: &Arc, session_id: SessionId, @@ -2400,19 +2315,7 @@ async fn finish_starting_session(inner: &Arc, session_id: SessionId) { // 反向覆盖回 Listening → 用户的 cancel 边沿被吞掉。 let outcome = { let mut state = inner.state.lock(); - if state.session_id != session_id { - BeginOutcome::StaleContinuation - } else if state.cancelled || state.phase != SessionPhase::Starting { - BeginOutcome::CancelRaced - } else { - state.phase = SessionPhase::Listening; - let pending = std::mem::replace(&mut state.pending_stop, false); - if pending { - BeginOutcome::PendingStop - } else { - BeginOutcome::Started - } - } + finish_starting_session_state(&mut state, session_id) }; match outcome { BeginOutcome::StaleContinuation => { @@ -2441,11 +2344,10 @@ async fn finish_starting_session(inner: &Arc, session_id: SessionId) { async fn end_session(inner: &Arc) -> Result<(), String> { let current_session_id = { let mut state = inner.state.lock(); - if state.phase != SessionPhase::Listening { + let Some(session_id) = start_processing_if_listening(&mut state) else { return Ok(()); - } - state.phase = SessionPhase::Processing; - state.session_id + }; + session_id }; let elapsed = inner.state.lock().started_at.elapsed().as_millis() as u64; @@ -2946,37 +2848,29 @@ fn dictation_error_code( } fn cancel_session(inner: &Arc) { - let (phase, session_id) = { + let Some(decision) = ({ let mut state = inner.state.lock(); let phase = state.phase; - if phase == SessionPhase::Idle { - return; - } - // Inserting 阶段已经过了最后一次 cancel 检查 + 锁内转换,inserter.insert 即将 - // 或正在执行 → Cmd+V 已发出无法撤销。这里硬设 cancelled=true 只会让 UI 显示 - // "已取消" 但文本仍被插入,与用户预期相反。直接拒绝,让本次 session 走完。 + let decision = begin_cancel_session_state(&mut state); if phase == SessionPhase::Inserting { log::info!("[coord] cancel ignored — already in Inserting phase, can't undo paste"); - return; } - // Processing 阶段 cancel 不能直接干掉 in-flight polish task(已经 await 了), - // 但可以打 cancelled 标记,让 end_session 在插入前检查并丢弃结果。 - state.cancelled = true; - (phase, state.session_id) + decision + }) else { + return; }; - stop_recorder_for_session(inner, session_id); - cancel_asr_for_session(inner, session_id); - restore_prepared_windows_ime_session(inner, session_id); + stop_recorder_for_session(inner, decision.session_id); + cancel_asr_for_session(inner, decision.session_id); + restore_prepared_windows_ime_session(inner, decision.session_id); // Processing 阶段保持 phase=Processing 让 end_session 自己走完检查 + 收尾; // 其他阶段直接转 Idle。 - if phase != SessionPhase::Processing { + if decision.phase != SessionPhase::Processing { let mut state = inner.state.lock(); - state.phase = SessionPhase::Idle; - state.focus_target = None; + finish_cancel_session_state(&mut state, decision); } emit_capsule(inner, CapsuleState::Cancelled, 0.0, 0, None, None); - log::info!("[coord] session cancelled (was {phase:?})"); + log::info!("[coord] session cancelled (was {:?})", decision.phase); schedule_capsule_idle(inner, CAPSULE_AUTO_HIDE_DELAY_MS); } @@ -4701,36 +4595,6 @@ fn foundry_audio_transcribe_timeout_duration() -> std::time::Duration { std::time::Duration::from_secs(COORDINATOR_GLOBAL_TIMEOUT_SECS) } -/// begin_session 中各 await 之间的 cancel race 检查结果。 -enum BeginOutcome { - /// 启动 continuation 属于旧 session;不能改动当前 session 状态。 - StaleContinuation, - /// 正常进入 Listening。 - Started, - /// Starting 阶段积累了 pending_stop 边沿,应立即 end_session(hold 快速松开 / toggle 快速双击)。 - PendingStop, - /// 期间 cancel_session 触发(cancelled=true 或 phase 被外部改回 Idle)。 - /// 必须回滚 recorder + ASR 资源,不进 Listening。 - CancelRaced, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -enum StartupRaceStatus { - ActiveStarting, - CancelRaced, - StaleContinuation, -} - -fn startup_race_status(state: &SessionState, captured_session_id: SessionId) -> StartupRaceStatus { - if state.session_id != captured_session_id { - StartupRaceStatus::StaleContinuation - } else if state.cancelled || state.phase != SessionPhase::Starting { - StartupRaceStatus::CancelRaced - } else { - StartupRaceStatus::ActiveStarting - } -} - /// 检查 begin_session 的 await 间隙是否被 cancel_session 打断。 /// 必须在持有 state lock 的瞬间读,结果一拿就过期,所以用 helper 名字提醒只在 /// 「准备做下一步副作用前」用。 diff --git a/openless-all/app/src-tauri/src/coordinator_state.rs b/openless-all/app/src-tauri/src/coordinator_state.rs new file mode 100644 index 00000000..70733327 --- /dev/null +++ b/openless-all/app/src-tauri/src/coordinator_state.rs @@ -0,0 +1,484 @@ +//! Coordinator 的纯状态转移层。 +//! +//! 这里不依赖 Tauri / 音频 / 系统剪贴板,只描述 dictation session 的 Rust +//! 状态机。这样 Windows CI 可以在不启动完整 Tauri test harness 的情况下实际运行 +//! 后端单测。 + +use std::time::Instant; + +use uuid::Uuid; + +pub type SessionId = Uuid; + +pub fn new_session_id() -> SessionId { + Uuid::new_v4() +} + +pub fn initial_session_id() -> SessionId { + Uuid::nil() +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum SessionPhase { + Idle, + Starting, + Listening, + Processing, + /// 已经过了最后一次 cancel 检查、即将 / 正在调用 inserter.insert 的窗口。 + /// cancel_session 在此阶段拒绝介入:Cmd+V 模拟点击已开始或已发出, + /// 无法撤销,硬把 cancelled=true 也救不回来,只会让 UI 出现 cancelled + /// 但实际还是插入了的诡异状态。详见 PR 修 Codex audit HIGH #2。 + Inserting, +} + +pub(crate) struct SessionState { + pub(crate) phase: SessionPhase, + pub(crate) started_at: Instant, + /// Starting 阶段(ASR 握手中)按下 stop 边沿(toggle 第二次按 / hold 松开)→ + /// 等握手完成 phase=Listening 后立刻 end_session,不丢边沿。issue #51。 + pub(crate) pending_stop: bool, + /// 用户在 Processing 阶段按 Esc 取消:end_session 在 polish/insert 检查点跳过插入 + + /// 跳过 history.append。issue #52。 + pub(crate) cancelled: bool, + pub(crate) focus_target: Option, + /// 每次 begin_session 生成新的 UUID session id。 + /// recorder error monitor 持有 captured id,处理时若与当前不等说明 + /// 是上一 session 的迟到错误,必须 drop,不要 abort 当前 active session。 + pub(crate) session_id: SessionId, + /// 用户开始 dictation 时所处的前台 app 标签("Mail (com.apple.mail)" / Windows 窗口标题)。 + /// 用作 LLM polish/translate 的上下文前提,让模型按 app 调风格。详见 issue #116。 + pub(crate) front_app: Option, +} + +impl Default for SessionState { + fn default() -> Self { + Self { + phase: SessionPhase::Idle, + started_at: Instant::now(), + pending_stop: false, + cancelled: false, + focus_target: None, + session_id: initial_session_id(), + front_app: None, + } + } +} + +/// begin_session 的锁内转移:只有 Idle 能进入 Starting,并生成新 session id。 +pub(crate) fn begin_session_state( + state: &mut SessionState, + focus_target: Option, + front_app: Option, +) -> Option { + if state.phase != SessionPhase::Idle { + return None; + } + state.phase = SessionPhase::Starting; + state.started_at = Instant::now(); + state.pending_stop = false; + state.cancelled = false; + state.focus_target = focus_target; + state.session_id = new_session_id(); + state.front_app = front_app; + Some(state.session_id) +} + +/// stop_dictation / hold release 在 Starting 阶段只记录 pending_stop,等待启动完成后处理。 +pub(crate) fn request_stop_during_starting_state(state: &mut SessionState) -> bool { + if state.phase != SessionPhase::Starting { + return false; + } + state.pending_stop = true; + true +} + +/// begin_session 中各 await 之间的 cancel race 检查结果。 +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum BeginOutcome { + /// 启动 continuation 属于旧 session;不能改动当前 session 状态。 + StaleContinuation, + /// 正常进入 Listening。 + Started, + /// Starting 阶段积累了 pending_stop 边沿,应立即 end_session(hold 快速松开 / toggle 快速双击)。 + PendingStop, + /// 期间 cancel_session 触发(cancelled=true 或 phase 被外部改回 Idle)。 + /// 必须回滚 recorder + ASR 资源,不进 Listening。 + CancelRaced, +} + +pub(crate) fn finish_starting_session_state( + state: &mut SessionState, + session_id: SessionId, +) -> BeginOutcome { + if state.session_id != session_id { + BeginOutcome::StaleContinuation + } else if state.cancelled || state.phase != SessionPhase::Starting { + BeginOutcome::CancelRaced + } else { + state.phase = SessionPhase::Listening; + let pending = std::mem::replace(&mut state.pending_stop, false); + if pending { + BeginOutcome::PendingStop + } else { + BeginOutcome::Started + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum StartupRaceStatus { + ActiveStarting, + CancelRaced, + StaleContinuation, +} + +pub(crate) fn startup_race_status( + state: &SessionState, + captured_session_id: SessionId, +) -> StartupRaceStatus { + if state.session_id != captured_session_id { + StartupRaceStatus::StaleContinuation + } else if state.cancelled || state.phase != SessionPhase::Starting { + StartupRaceStatus::CancelRaced + } else { + StartupRaceStatus::ActiveStarting + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) struct CancelDecision { + pub(crate) phase: SessionPhase, + pub(crate) session_id: SessionId, +} + +/// cancel_session 的锁内前半段。Idle / Inserting 不可取消;其他阶段设置 cancelled。 +pub(crate) fn begin_cancel_session_state(state: &mut SessionState) -> Option { + let phase = state.phase; + if matches!(phase, SessionPhase::Idle | SessionPhase::Inserting) { + return None; + } + state.cancelled = true; + Some(CancelDecision { + phase, + session_id: state.session_id, + }) +} + +/// cancel_session 外部资源清理后的锁内收尾。Processing 交给 end_session 自己收尾。 +pub(crate) fn finish_cancel_session_state(state: &mut SessionState, decision: CancelDecision) { + if decision.phase != SessionPhase::Processing { + state.phase = SessionPhase::Idle; + state.focus_target = None; + } +} + +pub(crate) fn start_processing_if_listening(state: &mut SessionState) -> Option { + if state.phase != SessionPhase::Listening { + return None; + } + state.phase = SessionPhase::Processing; + Some(state.session_id) +} + +pub(crate) struct RecordingAbort { + pub(crate) elapsed: u64, + pub(crate) session_id: SessionId, +} + +pub(crate) fn begin_recording_abort_before_restore( + state: &mut SessionState, +) -> Option { + if state.cancelled + || !matches!( + state.phase, + SessionPhase::Starting | SessionPhase::Listening + ) + { + return None; + } + state.cancelled = true; + Some(RecordingAbort { + elapsed: state.started_at.elapsed().as_millis() as u64, + session_id: state.session_id, + }) +} + +pub(crate) fn publish_abort_idle_after_restore(state: &mut SessionState, session_id: SessionId) { + if state.session_id == session_id { + state.phase = SessionPhase::Idle; + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn session_id(n: u128) -> SessionId { + Uuid::from_u128(n) + } + + #[test] + fn begin_session_enters_starting_and_clears_stale_edges() { + let mut state = SessionState { + pending_stop: true, + cancelled: true, + ..Default::default() + }; + + let id = begin_session_state( + &mut state, + Some(7), + Some("Terminal (com.apple.Terminal)".into()), + ) + .unwrap(); + + assert_eq!(state.phase, SessionPhase::Starting); + assert!(!state.pending_stop); + assert!(!state.cancelled); + assert_eq!(state.focus_target, Some(7)); + assert_eq!( + state.front_app.as_deref(), + Some("Terminal (com.apple.Terminal)") + ); + assert_eq!(state.session_id, id); + assert_ne!(id, initial_session_id()); + } + + #[test] + fn begin_session_ignores_non_idle_phase() { + let mut state = SessionState { + phase: SessionPhase::Processing, + session_id: session_id(99), + ..Default::default() + }; + + assert!(begin_session_state(&mut state, Some(1), Some("Mail".into())).is_none()); + + assert_eq!(state.phase, SessionPhase::Processing); + assert_eq!(state.session_id, session_id(99)); + assert!(state.focus_target.is_none()); + assert!(state.front_app.is_none()); + } + + #[test] + fn stop_during_starting_sets_pending_stop_only_for_starting() { + let mut state = SessionState { + phase: SessionPhase::Starting, + ..Default::default() + }; + + assert!(request_stop_during_starting_state(&mut state)); + assert!(state.pending_stop); + + state.phase = SessionPhase::Listening; + state.pending_stop = false; + assert!(!request_stop_during_starting_state(&mut state)); + assert!(!state.pending_stop); + } + + #[test] + fn finish_starting_is_table_driven_for_pending_cancel_and_stale_edges() { + let cases = [ + ( + SessionPhase::Starting, + false, + false, + session_id(7), + BeginOutcome::Started, + SessionPhase::Listening, + ), + ( + SessionPhase::Starting, + false, + true, + session_id(7), + BeginOutcome::PendingStop, + SessionPhase::Listening, + ), + ( + SessionPhase::Starting, + true, + false, + session_id(7), + BeginOutcome::CancelRaced, + SessionPhase::Starting, + ), + ( + SessionPhase::Idle, + false, + false, + session_id(7), + BeginOutcome::CancelRaced, + SessionPhase::Idle, + ), + ( + SessionPhase::Starting, + false, + false, + session_id(8), + BeginOutcome::StaleContinuation, + SessionPhase::Starting, + ), + ]; + + for (phase, cancelled, pending_stop, actual_id, expected, expected_phase) in cases { + let mut state = SessionState { + phase, + cancelled, + pending_stop, + session_id: actual_id, + ..Default::default() + }; + + assert_eq!( + finish_starting_session_state(&mut state, session_id(7)), + expected, + "phase={phase:?} cancelled={cancelled} pending_stop={pending_stop} actual_id={actual_id}" + ); + assert_eq!(state.phase, expected_phase); + if expected == BeginOutcome::PendingStop { + assert!(!state.pending_stop); + } + } + } + + #[test] + fn cancel_session_state_machine_is_table_driven() { + let cases = [ + (SessionPhase::Idle, SessionPhase::Idle, false), + (SessionPhase::Starting, SessionPhase::Idle, true), + (SessionPhase::Listening, SessionPhase::Idle, true), + (SessionPhase::Processing, SessionPhase::Processing, true), + (SessionPhase::Inserting, SessionPhase::Inserting, false), + ]; + + for (initial, expected_phase, expected_cancelled) in cases { + let mut state = SessionState { + phase: initial, + cancelled: false, + focus_target: Some(1), + session_id: session_id(42), + ..Default::default() + }; + + if let Some(decision) = begin_cancel_session_state(&mut state) { + finish_cancel_session_state(&mut state, decision); + } + + assert_eq!(state.phase, expected_phase, "initial={initial:?}"); + assert_eq!(state.cancelled, expected_cancelled, "initial={initial:?}"); + if matches!(initial, SessionPhase::Starting | SessionPhase::Listening) { + assert!(state.focus_target.is_none(), "initial={initial:?}"); + } + } + } + + #[test] + fn stop_dictation_from_listening_enters_processing_once() { + let mut state = SessionState { + phase: SessionPhase::Listening, + session_id: session_id(123), + ..Default::default() + }; + + assert_eq!( + start_processing_if_listening(&mut state), + Some(session_id(123)) + ); + assert_eq!(state.phase, SessionPhase::Processing); + assert_eq!(start_processing_if_listening(&mut state), None); + assert_eq!(state.phase, SessionPhase::Processing); + } + + #[test] + fn startup_race_check_is_table_driven_for_begin_session_edges() { + let cases = [ + ( + SessionPhase::Starting, + false, + session_id(7), + StartupRaceStatus::ActiveStarting, + ), + ( + SessionPhase::Starting, + true, + session_id(7), + StartupRaceStatus::CancelRaced, + ), + ( + SessionPhase::Idle, + false, + session_id(7), + StartupRaceStatus::CancelRaced, + ), + ( + SessionPhase::Listening, + false, + session_id(7), + StartupRaceStatus::CancelRaced, + ), + ( + SessionPhase::Starting, + false, + session_id(8), + StartupRaceStatus::StaleContinuation, + ), + ]; + + for (phase, cancelled, actual_session_id, expected) in cases { + let state = SessionState { + phase, + cancelled, + session_id: actual_session_id, + ..Default::default() + }; + + assert_eq!( + startup_race_status(&state, session_id(7)), + expected, + "phase={phase:?} cancelled={cancelled} actual_session={actual_session_id}" + ); + } + } + + #[test] + fn recording_abort_keeps_session_non_idle_until_restore_can_run() { + let mut state = SessionState { + phase: SessionPhase::Listening, + cancelled: false, + session_id: session_id(7), + ..Default::default() + }; + + let abort = begin_recording_abort_before_restore(&mut state).unwrap(); + + assert_eq!(abort.session_id, session_id(7)); + assert!(state.cancelled); + assert_eq!(state.phase, SessionPhase::Listening); + + publish_abort_idle_after_restore(&mut state, abort.session_id); + + assert_eq!(state.phase, SessionPhase::Idle); + } + + #[test] + fn recording_abort_is_noop_after_prior_cancel_or_idle() { + let cases = [ + (SessionPhase::Idle, false), + (SessionPhase::Processing, false), + (SessionPhase::Listening, true), + ]; + + for (phase, cancelled) in cases { + let mut state = SessionState { + phase, + cancelled, + ..Default::default() + }; + + assert!(begin_recording_abort_before_restore(&mut state).is_none()); + assert_eq!(state.phase, phase); + assert_eq!(state.cancelled, cancelled); + } + } +} diff --git a/openless-all/app/src-tauri/src/lib.rs b/openless-all/app/src-tauri/src/lib.rs index 7bb94ea4..1e47e16a 100644 --- a/openless-all/app/src-tauri/src/lib.rs +++ b/openless-all/app/src-tauri/src/lib.rs @@ -15,6 +15,7 @@ mod audio_mute; mod combo_hotkey; mod commands; mod coordinator; +mod coordinator_state; mod global_hotkey_runtime; mod hotkey; mod insertion; From f339209394775ec78c937468f043a4629acc1021 Mon Sep 17 00:00:00 2001 From: H-Chris233 Date: Sat, 9 May 2026 08:00:21 +0800 Subject: [PATCH 2/5] Align shortcut tests with platform modifier semantics Windows intentionally normalizes cmd/command shortcut modifiers to ctrl, while macOS and Linux keep the super bit. The Rust-only backend test crate now executes these source tests on Windows, so the assertion must follow the same platform contract as production parsing instead of assuming the non-Windows result.\n\nConstraint: Keep the fix inside Rust backend tests and avoid changing shortcut behavior.\nRejected: Change normalize_modifier_tag to keep cmd as super on Windows | would alter user-facing hotkey semantics.\nConfidence: high\nScope-risk: narrow\nTested: cargo test --manifest-path openless-all/app/src-tauri/backend-tests/Cargo.toml\nTested: cargo test --manifest-path openless-all/app/src-tauri/Cargo.toml --lib\nNot-tested: GitHub Windows runner rerun result pending after push.\n\nCo-authored-by: OmX --- openless-all/app/src-tauri/src/shortcut_binding.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/openless-all/app/src-tauri/src/shortcut_binding.rs b/openless-all/app/src-tauri/src/shortcut_binding.rs index 731f8532..bc2bd85b 100644 --- a/openless-all/app/src-tauri/src/shortcut_binding.rs +++ b/openless-all/app/src-tauri/src/shortcut_binding.rs @@ -204,6 +204,9 @@ mod tests { modifiers: vec!["cmd".into(), "shift".into()], }; let parsed = parse_global_hotkey(&combo).expect("combo parses"); + #[cfg(target_os = "windows")] + assert!(parsed.mods.contains(Modifiers::CONTROL)); + #[cfg(not(target_os = "windows"))] assert!(parsed.mods.contains(Modifiers::SUPER)); assert!(parsed.mods.contains(Modifiers::SHIFT)); assert_eq!(parsed.key, Code::KeyD); From 02ccac2dd50beb9bbaf1d57bebe68e67bc39654e Mon Sep 17 00:00:00 2001 From: H-Chris233 Date: Sat, 9 May 2026 08:30:23 +0800 Subject: [PATCH 3/5] Make Rust-only backend test target explicit The backend-tests crate is intentionally an integration-test harness, but declaring the target explicitly makes Cargo discovery and review intent unambiguous. The harness comment also records that path-included modules run their own cfg(test) unit tests for hotkey, recorder, and insertion without linking the full Tauri library.\n\nConstraint: Stay within Rust-only backend tests; do not add a Tauri lib/runtime target.\nRejected: Add src/lib.rs just to satisfy target discovery | would introduce an unused crate surface instead of the actual test harness.\nConfidence: high\nScope-risk: narrow\nTested: cargo test --manifest-path openless-all/app/src-tauri/backend-tests/Cargo.toml -- --list | grep -E '^(hotkey|recorder|insertion)::'\nTested: cargo test --manifest-path openless-all/app/src-tauri/backend-tests/Cargo.toml\nTested: git diff --check\nNot-tested: GitHub Windows runner rerun result pending after push.\n\nCo-authored-by: OmX --- openless-all/app/src-tauri/backend-tests/Cargo.toml | 4 ++++ .../app/src-tauri/backend-tests/tests/backend_rust.rs | 2 ++ 2 files changed, 6 insertions(+) diff --git a/openless-all/app/src-tauri/backend-tests/Cargo.toml b/openless-all/app/src-tauri/backend-tests/Cargo.toml index 8b8b5e47..d3dc05c1 100644 --- a/openless-all/app/src-tauri/backend-tests/Cargo.toml +++ b/openless-all/app/src-tauri/backend-tests/Cargo.toml @@ -5,6 +5,10 @@ edition = "2021" rust-version = "1.77" publish = false +[[test]] +name = "backend_rust" +path = "tests/backend_rust.rs" + [dependencies] arboard = "3" cpal = "0.15" diff --git a/openless-all/app/src-tauri/backend-tests/tests/backend_rust.rs b/openless-all/app/src-tauri/backend-tests/tests/backend_rust.rs index 23e75a81..08ef2f2a 100644 --- a/openless-all/app/src-tauri/backend-tests/tests/backend_rust.rs +++ b/openless-all/app/src-tauri/backend-tests/tests/backend_rust.rs @@ -2,6 +2,8 @@ //! //! 这个测试 crate 只把纯 Rust 后端模块按源码路径编进来,不链接完整 Tauri //! `openless_lib`,避免 Windows CI 在 test harness 启动前被桌面运行时 DLL 拦截。 +//! Cargo 以 `cfg(test)` 编译这些 path-included 模块,所以各模块自己的 +//! `#[cfg(test)]` 单测会在这里实际执行(见 hotkey / recorder / insertion)。 #![allow(dead_code, unused_variables)] From 044799e7057ca78d1f4202c805ac509916e79855 Mon Sep 17 00:00:00 2001 From: H-Chris233 Date: Sat, 9 May 2026 10:07:38 +0800 Subject: [PATCH 4/5] Cover backend edge tests in Rust-only harness The Rust-only backend harness already ran existing hotkey, recorder, and insertion unit tests, but the review could not see target edge coverage directly in this diff. Add focused edge tests for modifier trigger filtering/re-emission, recorder callback no-op inputs, and insertion fallback status so the coverage is explicit in the changed files.\n\nConstraint: Keep scope to backend Rust unit tests and avoid app/runtime integration.\nRejected: Add UI or Tauri runtime tests | conflicts with the Rust-only backend boundary.\nConfidence: high\nScope-risk: narrow\nTested: cargo test --manifest-path openless-all/app/src-tauri/backend-tests/Cargo.toml\nTested: cargo test --manifest-path openless-all/app/src-tauri/Cargo.toml --lib\nTested: git diff --check\nNot-tested: GitHub Windows runner result after push.\n\nCo-authored-by: OmX --- openless-all/app/src-tauri/src/hotkey.rs | 62 +++++++++++++++++++++ openless-all/app/src-tauri/src/insertion.rs | 11 +++- openless-all/app/src-tauri/src/recorder.rs | 29 ++++++++++ 3 files changed, 100 insertions(+), 2 deletions(-) diff --git a/openless-all/app/src-tauri/src/hotkey.rs b/openless-all/app/src-tauri/src/hotkey.rs index 38bc516f..6babad78 100644 --- a/openless-all/app/src-tauri/src/hotkey.rs +++ b/openless-all/app/src-tauri/src/hotkey.rs @@ -1042,6 +1042,27 @@ mod platform { ); } + #[test] + fn windows_modifier_edges_ignore_unrelated_keys_and_reemit_after_release() { + let shared = shared(HotkeyTrigger::RightControl); + let (ctx, rx) = callback_context(shared); + + assert!(!dispatch_keyboard_event(&ctx, VK_LCONTROL, WM_KEYDOWN)); + assert!(dispatch_keyboard_event(&ctx, VK_RCONTROL, WM_KEYUP)); + assert!(dispatch_keyboard_event(&ctx, VK_RCONTROL, WM_KEYDOWN)); + assert!(dispatch_keyboard_event(&ctx, VK_RCONTROL, WM_KEYUP)); + assert!(dispatch_keyboard_event(&ctx, VK_RCONTROL, WM_KEYDOWN)); + + assert_eq!( + drain(&rx), + vec![ + HotkeyEvent::Pressed, + HotkeyEvent::Released, + HotkeyEvent::Pressed + ] + ); + } + #[test] fn windows_optional_modifier_shortcuts_use_independent_latches() { let shared = shared(HotkeyTrigger::RightControl); @@ -1352,6 +1373,47 @@ mod platform { ); } + #[test] + fn rdev_modifier_edges_ignore_unrelated_keys_and_reemit_after_release() { + let shared = shared(HotkeyTrigger::RightControl); + let (tx, rx) = mpsc::channel(); + + dispatch_event( + &shared, + &tx, + key_event(EventType::KeyPress(Key::ControlLeft)), + ); + dispatch_event( + &shared, + &tx, + key_event(EventType::KeyRelease(Key::ControlRight)), + ); + dispatch_event( + &shared, + &tx, + key_event(EventType::KeyPress(Key::ControlRight)), + ); + dispatch_event( + &shared, + &tx, + key_event(EventType::KeyRelease(Key::ControlRight)), + ); + dispatch_event( + &shared, + &tx, + key_event(EventType::KeyPress(Key::ControlRight)), + ); + + assert_eq!( + drain(&rx), + vec![ + HotkeyEvent::Pressed, + HotkeyEvent::Released, + HotkeyEvent::Pressed + ] + ); + } + #[test] fn rdev_optional_modifier_shortcuts_use_independent_latches() { let shared = shared(HotkeyTrigger::RightControl); diff --git a/openless-all/app/src-tauri/src/insertion.rs b/openless-all/app/src-tauri/src/insertion.rs index da36ccd4..ecfc4490 100644 --- a/openless-all/app/src-tauri/src/insertion.rs +++ b/openless-all/app/src-tauri/src/insertion.rs @@ -479,6 +479,13 @@ mod tests { let inserter = TextInserter::new(); assert_eq!(inserter.insert("", true), InsertStatus::CopiedFallback); + #[cfg(not(target_os = "macos"))] + { + assert_eq!( + inserter.insert_via_clipboard_fallback("", true), + InsertStatus::CopiedFallback + ); + } assert_eq!(inserter.copy_fallback(""), InsertStatus::CopiedFallback); } @@ -520,13 +527,13 @@ mod tests { #[test] #[cfg(target_os = "macos")] - fn macos_paste_failure_keeps_copied_fallback_available() { + fn macos_direct_write_or_paste_failure_keeps_copied_fallback_available() { assert_eq!( macos_insert_status_after_paste(Ok(())), InsertStatus::Inserted ); assert_eq!( - macos_insert_status_after_paste(Err("AX write unavailable".to_string())), + macos_insert_status_after_paste(Err("AX direct write unavailable".to_string())), InsertStatus::CopiedFallback ); } diff --git a/openless-all/app/src-tauri/src/recorder.rs b/openless-all/app/src-tauri/src/recorder.rs index c135a115..bac0c8d7 100644 --- a/openless-all/app/src-tauri/src/recorder.rs +++ b/openless-all/app/src-tauri/src/recorder.rs @@ -678,4 +678,33 @@ mod tests { assert!(state.last_callback_time.lock().is_some()); assert_eq!(state.callback_count.load(Ordering::Relaxed), 1); } + + #[test] + fn process_callback_ignores_empty_or_zero_channel_input_without_liveness_marker() { + let consumer = RecordingConsumer::default(); + let levels = Arc::new(StdMutex::new(Vec::new())); + let levels_for_handler = Arc::clone(&levels); + let state = StreamState::new(); + + process_callback( + &[], + 1, + TARGET_SAMPLE_RATE, + &consumer, + &move |level| levels_for_handler.lock().unwrap().push(level), + &state, + ); + process_callback( + &[0.25, -0.25], + 0, + TARGET_SAMPLE_RATE, + &consumer, + &move |level| levels.lock().unwrap().push(level), + &state, + ); + + assert!(consumer.chunks.lock().unwrap().is_empty()); + assert!(state.last_callback_time.lock().is_none()); + assert_eq!(state.callback_count.load(Ordering::Relaxed), 0); + } } From c63c69fb2f68433fc9e6d44a82ef50d0cfbdb1e3 Mon Sep 17 00:00:00 2001 From: H-Chris233 Date: Sat, 9 May 2026 11:40:39 +0800 Subject: [PATCH 5/5] Lock recorder resampling and RMS coverage The backend-only test PR needs to cover the concrete recorder paths named in the ticket, not just empty input handling. This adds focused Rust unit coverage for non-target sample-rate resampling, emitted PCM shape, scaled RMS levels, and peak tracking while keeping the production recorder unchanged.\n\nConstraint: Scope is Rust-only backend tests; do not launch or link extra Tauri runtime paths for Windows CI.\nConfidence: high\nScope-risk: narrow\nTested: cargo test --manifest-path openless-all/app/src-tauri/backend-tests/Cargo.toml recorder:: -- --nocapture\nTested: cargo test --manifest-path openless-all/app/src-tauri/backend-tests/Cargo.toml\nTested: cargo test --manifest-path openless-all/app/src-tauri/Cargo.toml --lib\nCo-authored-by: OmX --- openless-all/app/src-tauri/src/recorder.rs | 64 ++++++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/openless-all/app/src-tauri/src/recorder.rs b/openless-all/app/src-tauri/src/recorder.rs index bac0c8d7..eb1b1d49 100644 --- a/openless-all/app/src-tauri/src/recorder.rs +++ b/openless-all/app/src-tauri/src/recorder.rs @@ -619,6 +619,13 @@ mod tests { } } + fn decode_i16_le(bytes: &[u8]) -> Vec { + bytes + .chunks_exact(2) + .map(|chunk| i16::from_le_bytes([chunk[0], chunk[1]])) + .collect() + } + #[test] fn downmix_to_mono_averages_complete_interleaved_frames() { let mono = downmix_to_mono(&[1.0, -1.0, 0.5, 0.25, 0.0], 2); @@ -655,6 +662,63 @@ mod tests { assert_eq!(*state.resample_phase.lock(), 0.5); } + #[test] + fn resample_upsamples_with_linear_interpolation_and_tail_state() { + let state = StreamState::new(); + + let out = resample_to_target(&[0.0, 1.0], 8_000, TARGET_SAMPLE_RATE, &state); + + assert_eq!(out, vec![0.0, 0.5, 1.0]); + assert_eq!(*state.last_sample.lock(), 1.0); + assert_eq!(*state.resample_phase.lock(), 0.0); + } + + #[test] + fn process_callback_resamples_non_target_input_before_emitting_pcm() { + let consumer = RecordingConsumer::default(); + let levels = Arc::new(StdMutex::new(Vec::new())); + let levels_for_handler = Arc::clone(&levels); + let state = StreamState::new(); + + process_callback( + &[0.0, 1.0], + 1, + 8_000, + &consumer, + &move |level| levels_for_handler.lock().unwrap().push(level), + &state, + ); + + let chunks = consumer.chunks.lock().unwrap(); + assert_eq!(chunks.len(), 1); + assert_eq!(decode_i16_le(&chunks[0]), vec![0, 16383, 32767]); + assert_eq!(*levels.lock().unwrap(), vec![1.0]); + assert_eq!(state.callback_count.load(Ordering::Relaxed), 1); + } + + #[test] + fn process_callback_reports_scaled_rms_level_and_peaks() { + let consumer = RecordingConsumer::default(); + let levels = Arc::new(StdMutex::new(Vec::new())); + let levels_for_handler = Arc::clone(&levels); + let state = StreamState::new(); + + process_callback( + &[0.125, -0.125], + 1, + TARGET_SAMPLE_RATE, + &consumer, + &move |level| levels_for_handler.lock().unwrap().push(level), + &state, + ); + + let levels = levels.lock().unwrap(); + assert_eq!(levels.len(), 1); + assert!((levels[0] - 0.5).abs() < 0.0001); + assert_eq!(state.peak_input_rms_milli.load(Ordering::Relaxed), 125); + assert_eq!(state.peak_output_rms_milli.load(Ordering::Relaxed), 125); + } + #[test] fn process_callback_emits_pcm_level_and_liveness_marker() { let consumer = RecordingConsumer::default();