commit 32ee3a6d2632f684c7a6656b2412763eee494a61 Author: Mae PUGIN <48982737+mornepousse@users.noreply.github.com> Date: Mon Apr 6 20:40:34 2026 +0200 feat: Complete KeSp Controller — Slint UI port Full port of the KaSe/KeSp split keyboard configurator from egui to Slint: - 6 tabs: Keymap, Advanced, Macros, Stats, Settings, Flash - Responsive keyboard view with scale-to-fit and key rotations - Key selector popup with categorized grid, MT/LT builders, hex input - Combo key picker with inline keyboard visual - Macro step builder with visual tags - Serial communication via background threads + mpsc polling - Heatmap overlay with blue-yellow-red gradient - OTA flasher with prog port VID filtering and partition selector - WPM polling, Tamagotchi, Autoshift controls - Dracula theme matching egui version Co-Authored-By: Claude Opus 4.6 (1M context) diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..41df5da --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,6511 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "ab_glyph" +version = "0.2.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01c0457472c38ea5bd1c3b5ada5e368271cb550be7a4ca4a0b4634e9913f6cc2" +dependencies = [ + "ab_glyph_rasterizer", + "owned_ttf_parser", +] + +[[package]] +name = "ab_glyph_rasterizer" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "366ffbaa4442f4684d91e2cd7c5ea7c4ed8add41959a31447066e279e432b618" + +[[package]] +name = "accesskit" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eca13c82f9a5cd813120b2e9b6a5d10532c6e4cd140c295cebd1f770095c8a5" + +[[package]] +name = "accesskit_atspi_common" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eb9cc46b7fb6987c4f891f0301b230b29d9e69b4854f060a0cf41fbc407ab77" +dependencies = [ + "accesskit", + "accesskit_consumer", + "atspi-common", + "serde", + "zvariant", +] + +[[package]] +name = "accesskit_consumer" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69d880a613f29621c90e801feec40f5dd61d837d7e20bf9b67676d45e7364a36" +dependencies = [ + "accesskit", + "hashbrown 0.16.1", +] + +[[package]] +name = "accesskit_macos" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b0ddfc3fe3d457d11cc1c4989105986a03583a1d54d0c25053118944b62e100" +dependencies = [ + "accesskit", + "accesskit_consumer", + "hashbrown 0.16.1", + "objc2 0.5.2", + "objc2-app-kit 0.2.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "accesskit_unix" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5d552169ef018149966ed139bb0311c6947b3343e9140d1b9f88d69da9528fd" +dependencies = [ + "accesskit", + "accesskit_atspi_common", + "async-channel", + "async-executor", + "async-task", + "atspi", + "futures-lite", + "futures-util", + "serde", + "zbus", +] + +[[package]] +name = "accesskit_windows" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d277279d0a3b0c0021dd110b55aa1fe326b09ee2cbc338df28f847c7daf94e25" +dependencies = [ + "accesskit", + "accesskit_consumer", + "hashbrown 0.16.1", + "static_assertions", + "windows 0.61.3", + "windows-core 0.61.2", +] + +[[package]] +name = "accesskit_winit" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db08dff285306264a1de127ea07bb9e7a1ed71bd8593c168d0731caa782516c9" +dependencies = [ + "accesskit", + "accesskit_macos", + "accesskit_unix", + "accesskit_windows", + "raw-window-handle", + "winit", +] + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "getrandom 0.3.4", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "aligned" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee4508988c62edf04abd8d92897fca0c2995d907ce1dfeaf369dac3716a40685" +dependencies = [ + "as-slice", +] + +[[package]] +name = "aligned-vec" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc890384c8602f339876ded803c97ad529f3842aba97f6392b3dba0dd171769b" +dependencies = [ + "equator", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "android-activity" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f2a1bb052857d5dd49572219344a7332b31b76405648eabac5bc68978251bcd" +dependencies = [ + "android-properties", + "bitflags 2.11.0", + "cc", + "jni", + "libc", + "log", + "ndk", + "ndk-context", + "ndk-sys", + "num_enum", + "thiserror 2.0.18", +] + +[[package]] +name = "android-properties" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7eb209b1518d6bb87b283c20095f5228ecda460da70b44f0802523dea6da04" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "annotate-snippets" +version = "0.12.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22b669bf35e50f130e98212b486b0df78d93e285963344e58937692705e1a21a" +dependencies = [ + "anstyle", + "memchr", + "unicode-width", +] + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" + +[[package]] +name = "arg_enum_proc_macro" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "arrayref" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + +[[package]] +name = "as-raw-xcb-connection" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175571dd1d178ced59193a6fc02dde1b972eb0bc56c892cde9beeceac5bf0f6b" + +[[package]] +name = "as-slice" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "516b6b4f0e40d50dcda9365d53964ec74560ad4284da2e7fc97122cd83174516" +dependencies = [ + "stable_deref_trait", +] + +[[package]] +name = "ashpd" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2f3f79755c74fd155000314eb349864caa787c6592eace6c6882dad873d9c39" +dependencies = [ + "async-fs", + "async-net", + "enumflags2", + "futures-channel", + "futures-util", + "rand", + "raw-window-handle", + "serde", + "serde_repr", + "url", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "zbus", +] + +[[package]] +name = "async-broadcast" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532" +dependencies = [ + "event-listener", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-channel" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" +dependencies = [ + "concurrent-queue", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-executor" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c96bf972d85afc50bf5ab8fe2d54d1586b4e0b46c97c50a0c9e71e2f7bcd812a" +dependencies = [ + "async-task", + "concurrent-queue", + "fastrand", + "futures-lite", + "pin-project-lite", + "slab", +] + +[[package]] +name = "async-fs" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8034a681df4aed8b8edbd7fbe472401ecf009251c8b40556b304567052e294c5" +dependencies = [ + "async-lock", + "blocking", + "futures-lite", +] + +[[package]] +name = "async-io" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc" +dependencies = [ + "autocfg", + "cfg-if", + "concurrent-queue", + "futures-io", + "futures-lite", + "parking", + "polling", + "rustix 1.1.4", + "slab", + "windows-sys 0.61.2", +] + +[[package]] +name = "async-lock" +version = "3.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311" +dependencies = [ + "event-listener", + "event-listener-strategy", + "pin-project-lite", +] + +[[package]] +name = "async-net" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b948000fad4873c1c9339d60f2623323a0cfd3816e5181033c6a5cb68b2accf7" +dependencies = [ + "async-io", + "blocking", + "futures-lite", +] + +[[package]] +name = "async-process" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75" +dependencies = [ + "async-channel", + "async-io", + "async-lock", + "async-signal", + "async-task", + "blocking", + "cfg-if", + "event-listener", + "futures-lite", + "rustix 1.1.4", +] + +[[package]] +name = "async-recursion" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "async-signal" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43c070bbf59cd3570b6b2dd54cd772527c7c3620fce8be898406dd3ed6adc64c" +dependencies = [ + "async-io", + "async-lock", + "atomic-waker", + "cfg-if", + "futures-core", + "futures-io", + "rustix 1.1.4", + "signal-hook-registry", + "slab", + "windows-sys 0.61.2", +] + +[[package]] +name = "async-task" +version = "4.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" + +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "atspi" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c77886257be21c9cd89a4ae7e64860c6f0eefca799bb79127913052bd0eefb3d" +dependencies = [ + "atspi-common", + "atspi-proxies", +] + +[[package]] +name = "atspi-common" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20c5617155740c98003016429ad13fe43ce7a77b007479350a9f8bf95a29f63d" +dependencies = [ + "enumflags2", + "serde", + "static_assertions", + "zbus", + "zbus-lockstep", + "zbus-lockstep-macros", + "zbus_names", + "zvariant", +] + +[[package]] +name = "atspi-proxies" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2230e48787ed3eb4088996eab66a32ca20c0b67bbd4fd6cdfe79f04f1f04c9fc" +dependencies = [ + "atspi-common", + "serde", + "zbus", +] + +[[package]] +name = "auto_enums" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65398a2893f41bce5c9259f6e1a4f03fbae40637c1bdc755b4f387f48c613b03" +dependencies = [ + "derive_utils", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "av-scenechange" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f321d77c20e19b92c39e7471cf986812cbb46659d2af674adc4331ef3f18394" +dependencies = [ + "aligned", + "anyhow", + "arg_enum_proc_macro", + "arrayvec", + "log", + "num-rational", + "num-traits", + "pastey", + "rayon", + "thiserror 2.0.18", + "v_frame", + "y4m", +] + +[[package]] +name = "av1-grain" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cfddb07216410377231960af4fcab838eaa12e013417781b78bd95ee22077f8" +dependencies = [ + "anyhow", + "arrayvec", + "log", + "nom 8.0.0", + "num-rational", + "v_frame", +] + +[[package]] +name = "avif-serialize" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "375082f007bd67184fb9c0374614b29f9aaa604ec301635f72338bb65386a53d" +dependencies = [ + "arrayvec", +] + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bincode" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36eaf5d7b090263e8150820482d5d93cd964a81e4019913c972f4edcc6edb740" +dependencies = [ + "serde", + "unty", +] + +[[package]] +name = "bindgen" +version = "0.72.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895" +dependencies = [ + "bitflags 2.11.0", + "cexpr", + "clang-sys", + "itertools 0.13.0", + "log", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "rustc-hash 2.1.2", + "shlex", + "syn", +] + +[[package]] +name = "bit_field" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e4b40c7323adcfc0a41c4b88143ed58346ff65a288fc144329c5c45e05d70c6" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" +dependencies = [ + "serde_core", +] + +[[package]] +name = "bitstream-io" +version = "4.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60d4bd9d1db2c6bdf285e223a7fa369d5ce98ec767dec949c6ca62863ce61757" +dependencies = [ + "core2", +] + +[[package]] +name = "block2" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c132eebf10f5cad5289222520a4a058514204aed6d791f1cf4fe8088b82d15f" +dependencies = [ + "objc2 0.5.2", +] + +[[package]] +name = "block2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" +dependencies = [ + "objc2 0.6.4", +] + +[[package]] +name = "blocking" +version = "1.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21" +dependencies = [ + "async-channel", + "async-task", + "futures-io", + "futures-lite", + "piper", +] + +[[package]] +name = "borsh" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfd1e3f8955a5d7de9fab72fc8373fade9fb8a703968cb200ae3dc6cf08e185a" +dependencies = [ + "bytes", + "cfg_aliases", +] + +[[package]] +name = "built" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4ad8f11f288f48ca24471bbd51ac257aaeaaa07adae295591266b792902ae64" + +[[package]] +name = "bumpalo" +version = "3.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" + +[[package]] +name = "by_address" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64fa3c856b712db6612c019f14756e64e4bcea13337a6b33b696333a9eaa2d06" + +[[package]] +name = "bytemuck" +version = "1.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" +dependencies = [ + "bytemuck_derive", +] + +[[package]] +name = "bytemuck_derive" +version = "1.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9abbd1bc6865053c427f7198e6af43bfdedc55ab791faed4fbd361d789575ff" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "byteorder-lite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" + +[[package]] +name = "calloop" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b99da2f8558ca23c71f4fd15dc57c906239752dd27ff3c00a1d56b685b7cbfec" +dependencies = [ + "bitflags 2.11.0", + "log", + "polling", + "rustix 0.38.44", + "slab", + "thiserror 1.0.69", +] + +[[package]] +name = "calloop" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4dbf9978365bac10f54d1d4b04f7ce4427e51f71d61f2fe15e3fed5166474df7" +dependencies = [ + "bitflags 2.11.0", + "polling", + "rustix 1.1.4", + "slab", + "tracing", +] + +[[package]] +name = "calloop-wayland-source" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95a66a987056935f7efce4ab5668920b5d0dac4a7c99991a67395f13702ddd20" +dependencies = [ + "calloop 0.13.0", + "rustix 0.38.44", + "wayland-backend", + "wayland-client", +] + +[[package]] +name = "calloop-wayland-source" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "138efcf0940a02ebf0cc8d1eff41a1682a46b431630f4c52450d6265876021fa" +dependencies = [ + "calloop 0.14.4", + "rustix 1.1.4", + "wayland-backend", + "wayland-client", +] + +[[package]] +name = "cc" +version = "1.2.59" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7a4d3ec6524d28a329fc53654bbadc9bdd7b0431f5d65f1a56ffb28a1ee5283" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom 7.1.3", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "cgl" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ced0551234e87afee12411d535648dd89d2e7f34c78b753395567aff3d447ff" +dependencies = [ + "libc", +] + +[[package]] +name = "chrono" +version = "0.4.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-link 0.2.1", +] + +[[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 = "clru" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "197fd99cb113a8d5d9b6376f3aa817f32c1078f2343b714fff7d2ca44fdf67d5" +dependencies = [ + "hashbrown 0.16.1", +] + +[[package]] +name = "color_quant" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" + +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "const-field-offset" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91fcde4ca1211b5a94b573083c472ee19e86b19a441913f66e1cc5c41daf0255" +dependencies = [ + "const-field-offset-macro", + "field-offset", +] + +[[package]] +name = "const-field-offset-macro" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5387f5bbc9e9e6c96436ea125afa12614cebf8ac67f49abc08c1e7a891466c90" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "convert_case" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "copypasta" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e6811e17f81fe246ef2bc553f76b6ee6ab41a694845df1d37e52a92b7bbd38a" +dependencies = [ + "clipboard-win", + "objc2 0.5.2", + "objc2-app-kit 0.2.2", + "objc2-foundation 0.2.2", + "smithay-clipboard", + "x11-clipboard", +] + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "core-graphics" +version = "0.23.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c07782be35f9e1140080c6b96f0d44b739e2278479f64e02fdab4e32dfd8b081" +dependencies = [ + "bitflags 1.3.2", + "core-foundation 0.9.4", + "core-graphics-types", + "foreign-types", + "libc", +] + +[[package]] +name = "core-graphics-types" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf" +dependencies = [ + "bitflags 1.3.2", + "core-foundation 0.9.4", + "libc", +] + +[[package]] +name = "core2" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b49ba7ef1ad6107f8824dbe97de947cbaac53c44e7f9756a1fba0d37c1eec505" +dependencies = [ + "memchr", +] + +[[package]] +name = "core_maths" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77745e017f5edba1a9c1d854f6f3a52dac8a12dd5af5d2f54aecf61e43d80d30" +dependencies = [ + "libm", +] + +[[package]] +name = "countme" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7704b5fdd17b18ae31c4c1da5a2e0305a2bf17b5249300a9ee9ed7b72114c636" + +[[package]] +name = "cpp" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f36bcac3d8234c1fb813358e83d1bb6b0290a3d2b3b5efc6b88bfeaf9d8eec17" +dependencies = [ + "cpp_macros", +] + +[[package]] +name = "cpp_build" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27f8638c97fbd79cc6fc80b616e0e74b49bac21014faed590bbc89b7e2676c90" +dependencies = [ + "cc", + "cpp_common", + "lazy_static", + "proc-macro2", + "regex", + "syn", + "unicode-xid", +] + +[[package]] +name = "cpp_common" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25fcfea2ee05889597d35e986c2ad0169694320ae5cc8f6d2640a4bb8a884560" +dependencies = [ + "lazy_static", + "proc-macro2", + "syn", +] + +[[package]] +name = "cpp_macros" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d156158fe86e274820f5a53bc9edb0885a6e7113909497aa8d883b69dd171871" +dependencies = [ + "aho-corasick", + "byteorder", + "cpp_common", + "lazy_static", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "critical-section" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" + +[[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-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + +[[package]] +name = "ctor-lite" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e162d0c2e2068eb736b71e5597eff0b9944e6b973cd9f37b6a288ab9bf20e300" + +[[package]] +name = "cursor-icon" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f27ae1dd37df86211c42e150270f82743308803d90a6f6e6651cd730d5e1732f" + +[[package]] +name = "data-url" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be1e0bca6c3637f992fc1cc7cbc52a78c1ef6db076dbf1059c4323d6a2048376" + +[[package]] +name = "derive_more" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version", + "syn", + "unicode-xid", +] + +[[package]] +name = "derive_utils" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "362f47930db19fe7735f527e6595e4900316b893ebf6d48ad3d31be928d57dd6" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "dispatch" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" + +[[package]] +name = "dispatch2" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" +dependencies = [ + "bitflags 2.11.0", + "block2 0.6.2", + "libc", + "objc2 0.6.4", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "dlib" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab8ecd87370524b461f8557c119c405552c396ed91fc0a8eec68679eab26f94a" +dependencies = [ + "libloading", +] + +[[package]] +name = "downcast-rs" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" + +[[package]] +name = "dpi" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76" + +[[package]] +name = "drm" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80bc8c5c6c2941f70a55c15f8d9f00f9710ebda3ffda98075f996a0e6c92756f" +dependencies = [ + "bitflags 2.11.0", + "bytemuck", + "drm-ffi", + "drm-fourcc", + "libc", + "rustix 0.38.44", +] + +[[package]] +name = "drm-ffi" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51a91c9b32ac4e8105dec255e849e0d66e27d7c34d184364fb93e469db08f690" +dependencies = [ + "drm-sys", + "rustix 1.1.4", +] + +[[package]] +name = "drm-fourcc" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0aafbcdb8afc29c1a7ee5fbe53b5d62f4565b35a042a662ca9fecd0b54dae6f4" + +[[package]] +name = "drm-sys" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8e1361066d91f5ffccff060a3c3be9c3ecde15be2959c1937595f7a82a9f8" +dependencies = [ + "libc", + "linux-raw-sys 0.9.4", +] + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "endi" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099" + +[[package]] +name = "enumflags2" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef" +dependencies = [ + "enumflags2_derive", + "serde", +] + +[[package]] +name = "enumflags2_derive" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "equator" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4711b213838dfee0117e3be6ac926007d7f433d7bbe33595975d4190cb07e6fc" +dependencies = [ + "equator-macro", +] + +[[package]] +name = "equator-macro" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44f23cf4b44bfce11a86ace86f8a73ffdec849c9fd00a386a53d278bd9e81fb3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[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 = "euclid" +version = "0.22.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1a05365e3b1c6d1650318537c7460c6923f1abdd272ad6842baa2b509957a06" +dependencies = [ + "num-traits", +] + +[[package]] +name = "event-listener" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" +dependencies = [ + "event-listener", + "pin-project-lite", +] + +[[package]] +name = "exr" +version = "1.74.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4300e043a56aa2cb633c01af81ca8f699a321879a7854d3896a0ba89056363be" +dependencies = [ + "bit_field", + "half", + "lebe", + "miniz_oxide", + "rayon-core", + "smallvec", + "zune-inflate", +] + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "fax" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f05de7d48f37cd6730705cbca900770cab77a89f413d23e100ad7fad7795a0ab" +dependencies = [ + "fax_derive", +] + +[[package]] +name = "fax_derive" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0aca10fb742cb43f9e7bb8467c91aa9bcb8e3ffbc6a6f7389bb93ffc920577d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "fdeflate" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "femtovg" +version = "0.20.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35695993a8264f5dfa8facc135c003965cea40d83705b49e0f8c3a3b632a2171" +dependencies = [ + "bitflags 2.11.0", + "bytemuck", + "fnv", + "glow", + "image", + "imgref", + "itertools 0.14.0", + "log", + "rgb", + "slotmap", + "ttf-parser 0.25.1", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "field-offset" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38e2275cc4e4fc009b0669731a1e5ab7ebf11f469eaede2bab9309a5b4d6057f" +dependencies = [ + "memoffset", + "rustc_version", +] + +[[package]] +name = "filetime" +version = "0.2.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f98844151eee8917efc50bd9e8318cb963ae8b297431495d3f758616ea5c57db" +dependencies = [ + "cfg-if", + "libc", + "libredox", +] + +[[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 = "float-cmp" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "font-types" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39a654f404bbcbd48ea58c617c2993ee91d1cb63727a37bf2323a4edeed1b8c5" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "font-types" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d9237c6d82152100c691fb77ea18037b402bcc7257d2c876a4ffac81bc22a1c" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "fontdb" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "457e789b3d1202543297a350643cf459f836cade38934e7a4cf6a39e7cde2905" +dependencies = [ + "log", + "slotmap", + "tinyvec", + "ttf-parser 0.25.1", +] + +[[package]] +name = "fontdue" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e57e16b3fe8ff4364c0661fdaac543fb38b29ea9bc9c2f45612d90adf931d2b" +dependencies = [ + "hashbrown 0.15.5", + "rayon", + "ttf-parser 0.21.1", +] + +[[package]] +name = "fontique" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30bbc252c93499b6d3635d692f892a637db0dbb130ce9b32bf20b28e0dcc470b" +dependencies = [ + "bytemuck", + "hashbrown 0.16.1", + "icu_locale_core", + "linebender_resource_handle", + "memmap2", + "objc2 0.6.4", + "objc2-core-foundation", + "objc2-core-text", + "objc2-foundation 0.3.2", + "read-fonts 0.35.0", + "roxmltree", + "smallvec", + "windows 0.58.0", + "windows-core 0.58.0", + "yeslogic-fontconfig-sys", +] + +[[package]] +name = "foreign-types" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" +dependencies = [ + "foreign-types-macros", + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-macros" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "foreign-types-shared" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-executor" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[package]] +name = "futures-lite" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "parking", + "pin-project-lite", +] + +[[package]] +name = "futures-macro" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "gbm" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce852e998d3ca5e4a97014fb31c940dc5ef344ec7d364984525fd11e8a547e6a" +dependencies = [ + "bitflags 2.11.0", + "drm", + "drm-fourcc", + "gbm-sys", + "libc", +] + +[[package]] +name = "gbm-sys" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c13a5f2acc785d8fb6bf6b7ab6bfb0ef5dad4f4d97e8e70bb8e470722312f76f" +dependencies = [ + "libc", +] + +[[package]] +name = "gethostname" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bd49230192a3797a9a4d6abe9b3eed6f7fa4c8a8a4947977c6f80025f92cbd8" +dependencies = [ + "rustix 1.1.4", + "windows-link 0.2.1", +] + +[[package]] +name = "getopts" +version = "0.2.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfe4fbac503b8d1f88e6676011885f34b7174f46e59956bba534ba83abded4df" +dependencies = [ + "unicode-width", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi 5.3.0", + "wasip2", +] + +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", + "wasip2", + "wasip3", +] + +[[package]] +name = "gif" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5df2ba84018d80c213569363bdcd0c64e6933c67fe4c1d60ecf822971a3c35e" +dependencies = [ + "color_quant", + "weezl", +] + +[[package]] +name = "gl_generator" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a95dfc23a2b4a9a2f5ab41d194f8bfda3cabec42af4e39f08c339eb2a0c124d" +dependencies = [ + "khronos_api", + "log", + "xml-rs", +] + +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + +[[package]] +name = "glow" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5e5ea60d70410161c8bf5da3fdfeaa1c72ed2c15f8bbb9d19fe3a4fad085f08" +dependencies = [ + "js-sys", + "slotmap", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "glutin" +version = "0.32.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12124de845cacfebedff80e877bb37b5b75c34c5a4c89e47e1cdd67fb6041325" +dependencies = [ + "bitflags 2.11.0", + "cfg_aliases", + "cgl", + "dispatch2", + "glutin_egl_sys", + "glutin_glx_sys", + "glutin_wgl_sys", + "libloading", + "objc2 0.6.4", + "objc2-app-kit 0.3.2", + "objc2-core-foundation", + "objc2-foundation 0.3.2", + "once_cell", + "raw-window-handle", + "wayland-sys", + "windows-sys 0.52.0", + "x11-dl", +] + +[[package]] +name = "glutin-winit" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85edca7075f8fc728f28cb8fbb111a96c3b89e930574369e3e9c27eb75d3788f" +dependencies = [ + "cfg_aliases", + "glutin", + "raw-window-handle", + "winit", +] + +[[package]] +name = "glutin_egl_sys" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c4680ba6195f424febdc3ba46e7a42a0e58743f2edb115297b86d7f8ecc02d2" +dependencies = [ + "gl_generator", + "windows-sys 0.52.0", +] + +[[package]] +name = "glutin_glx_sys" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7bb2938045a88b612499fbcba375a77198e01306f52272e692f8c1f3751185" +dependencies = [ + "gl_generator", + "x11-dl", +] + +[[package]] +name = "glutin_wgl_sys" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c4ee00b289aba7a9e5306d57c2d05499b2e5dc427f84ac708bd2c090212cf3e" +dependencies = [ + "gl_generator", +] + +[[package]] +name = "half" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" +dependencies = [ + "cfg-if", + "crunchy", + "zerocopy", +] + +[[package]] +name = "harfrust" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92c020db12c71d8a12a3fe7607873cade3a01a6287e29d540c8723276221b9d8" +dependencies = [ + "bitflags 2.11.0", + "bytemuck", + "core_maths", + "read-fonts 0.35.0", + "smallvec", +] + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash 0.1.5", + "rayon", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash 0.2.0", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hermit-abi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" + +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "htmlparser" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48ce8546b993eaf241d69ded33b1be6d205dd9857ec879d9d18bd05d3676e144" + +[[package]] +name = "i-slint-backend-linuxkms" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b8827952ecfbbf76c8cb5bc3388ca9124c34f2b4fe5dffcfe57800d2a484885" +dependencies = [ + "bytemuck", + "calloop 0.14.4", + "drm", + "gbm", + "glutin", + "i-slint-common", + "i-slint-core", + "i-slint-renderer-femtovg", + "i-slint-renderer-software", + "input", + "memmap2", + "nix 0.30.1", + "raw-window-handle", + "xkbcommon", +] + +[[package]] +name = "i-slint-backend-qt" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac9d5db3221f453439ec5ad9b6ac3bb8d2b4825b2f8734f0cde4b67d7336c3da" +dependencies = [ + "const-field-offset", + "cpp", + "cpp_build", + "i-slint-common", + "i-slint-core", + "i-slint-core-macros", + "lyon_path", + "pin-project", + "pin-weak", + "qttypes", + "vtable", +] + +[[package]] +name = "i-slint-backend-selector" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce5a7e591a7257096e1f3da1bbb9ad6a140c307d0eee74f008a0b412fdb20dec" +dependencies = [ + "cfg-if", + "i-slint-backend-linuxkms", + "i-slint-backend-qt", + "i-slint-backend-winit", + "i-slint-common", + "i-slint-core", + "i-slint-core-macros", + "i-slint-renderer-femtovg", +] + +[[package]] +name = "i-slint-backend-winit" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbbf4789191740f939c9563b8850379122d7b5c1ceb09f9297b50ad53e408787" +dependencies = [ + "accesskit", + "accesskit_winit", + "block2 0.6.2", + "bytemuck", + "cfg-if", + "cfg_aliases", + "copypasta", + "derive_more", + "futures", + "glutin", + "glutin-winit", + "i-slint-common", + "i-slint-core", + "i-slint-core-macros", + "i-slint-renderer-femtovg", + "i-slint-renderer-skia", + "i-slint-renderer-software", + "imgref", + "lyon_path", + "muda", + "objc2 0.6.4", + "objc2-app-kit 0.3.2", + "objc2-foundation 0.3.2", + "objc2-quartz-core 0.3.2", + "objc2-ui-kit 0.3.2", + "pin-weak", + "raw-window-handle", + "rgb", + "scoped-tls-hkt", + "scopeguard", + "softbuffer", + "strum", + "vtable", + "wasm-bindgen", + "web-sys", + "windows 0.62.2", + "winit", + "zbus", +] + +[[package]] +name = "i-slint-common" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7659797fd28d4df3ed275ff95bf730bdf4a88d253f07e1ee8d0032d70138c3a" +dependencies = [ + "fontique", + "ttf-parser 0.25.1", +] + +[[package]] +name = "i-slint-compiler" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a6f358d0d5389869d67cd6ab6f5acf98fe31827264a696593e9687213cff682" +dependencies = [ + "annotate-snippets", + "by_address", + "derive_more", + "fontdue", + "i-slint-common", + "image", + "itertools 0.14.0", + "linked_hash_set", + "lyon_extra", + "lyon_path", + "num_enum", + "proc-macro2", + "quote", + "rayon", + "resvg", + "rowan", + "rspolib", + "smol_str 0.3.6", + "strum", + "typed-index-collections", + "url", +] + +[[package]] +name = "i-slint-core" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a95591ff85f8e2ff11c8d26ea8429768c2b77866e0c7e7fd49348f23ad108b5c" +dependencies = [ + "auto_enums", + "bitflags 2.11.0", + "cfg-if", + "chrono", + "clru", + "const-field-offset", + "derive_more", + "euclid", + "htmlparser", + "i-slint-common", + "i-slint-core-macros", + "image", + "lyon_algorithms", + "lyon_extra", + "lyon_geom", + "lyon_path", + "num-traits", + "once_cell", + "parley", + "pin-project", + "pin-weak", + "portable-atomic", + "pulldown-cmark", + "raw-window-handle", + "resvg", + "rgb", + "scoped-tls-hkt", + "scopeguard", + "skrifa 0.37.0", + "slab", + "strum", + "sys-locale", + "thiserror 2.0.18", + "unicode-linebreak", + "unicode-script", + "unicode-segmentation", + "vtable", + "wasm-bindgen", + "web-sys", + "web-time", +] + +[[package]] +name = "i-slint-core-macros" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cc5f2f71682787dd5c6299555c0de635009eb269bbc54d6198e0d225b69fae4" +dependencies = [ + "quote", + "serde_json", + "syn", +] + +[[package]] +name = "i-slint-renderer-femtovg" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb6eccda447999bc6222988500b841b64c953988986af182334e7ba9a30f0edd" +dependencies = [ + "cfg-if", + "const-field-offset", + "derive_more", + "femtovg", + "glow", + "i-slint-common", + "i-slint-core", + "i-slint-core-macros", + "imgref", + "lyon_path", + "pin-weak", + "rgb", + "ttf-parser 0.25.1", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "i-slint-renderer-skia" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a64546232c0370f291e65fc92a4f4fc777ea78d5f48467873cb968b1de52e9ab" +dependencies = [ + "bytemuck", + "cfg-if", + "cfg_aliases", + "const-field-offset", + "derive_more", + "glow", + "glutin", + "i-slint-common", + "i-slint-core", + "i-slint-core-macros", + "lyon_path", + "objc2 0.6.4", + "objc2-app-kit 0.3.2", + "objc2-core-foundation", + "objc2-foundation 0.3.2", + "objc2-metal 0.3.2", + "objc2-quartz-core 0.3.2", + "pin-weak", + "raw-window-handle", + "raw-window-metal", + "read-fonts 0.35.0", + "scoped-tls-hkt", + "skia-safe", + "softbuffer", + "unicode-segmentation", + "vtable", + "windows 0.62.2", + "write-fonts", +] + +[[package]] +name = "i-slint-renderer-software" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a59be6c34935c4f8e41aa67a63518d5c59219c8eeb1d07af420bed8334fa31d7" +dependencies = [ + "bytemuck", + "clru", + "derive_more", + "euclid", + "fontdue", + "i-slint-common", + "i-slint-core", + "integer-sqrt", + "lyon_path", + "num-traits", + "skrifa 0.37.0", + "zeno", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core 0.62.2", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "icu_collections" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" +dependencies = [ + "displaydoc", + "litemap", + "serde", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" + +[[package]] +name = "icu_properties" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" + +[[package]] +name = "icu_provider" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "image" +version = "0.25.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104" +dependencies = [ + "bytemuck", + "byteorder-lite", + "color_quant", + "exr", + "gif", + "image-webp", + "moxcms", + "num-traits", + "png 0.18.1", + "qoi", + "ravif", + "rayon", + "rgb", + "tiff", + "zune-core", + "zune-jpeg", +] + +[[package]] +name = "image-webp" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "525e9ff3e1a4be2fbea1fdf0e98686a6d98b4d8f937e1bf7402245af1909e8c3" +dependencies = [ + "byteorder-lite", + "quick-error", +] + +[[package]] +name = "imagesize" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09e54e57b4c48b40f7aec75635392b12b3421fa26fe8b4332e63138ed278459c" + +[[package]] +name = "imgref" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c5cedc30da3a610cac6b4ba17597bdf7152cf974e8aab3afb3d54455e371c8" + +[[package]] +name = "indexmap" +version = "2.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45a8a2b9cb3e0b0c1803dbb0758ffac5de2f425b23c28f518faabd9d805342ff" +dependencies = [ + "equivalent", + "hashbrown 0.16.1", + "serde", + "serde_core", +] + +[[package]] +name = "input" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbdc09524a91f9cacd26f16734ff63d7dc650daffadd2b6f84d17a285bd875a9" +dependencies = [ + "bitflags 2.11.0", + "input-sys", + "libc", + "log", + "udev", +] + +[[package]] +name = "input-sys" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd4f5b4d1c00331c5245163aacfe5f20be75b564c7112d45893d4ae038119eb0" + +[[package]] +name = "integer-sqrt" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "276ec31bcb4a9ee45f58bec6f9ec700ae4cf4f4f8f2fa7e06cb406bd5ffdd770" +dependencies = [ + "num-traits", +] + +[[package]] +name = "interpolate_name" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "io-kit-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "617ee6cf8e3f66f3b4ea67a4058564628cde41901316e19f559e14c7c72c5e7b" +dependencies = [ + "core-foundation-sys", + "mach2", +] + +[[package]] +name = "io-lifetimes" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" +dependencies = [ + "hermit-abi 0.3.9", + "libc", + "windows-sys 0.48.0", +] + +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "jni" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498" +dependencies = [ + "cfg-if", + "combine", + "jni-macros", + "jni-sys 0.4.1", + "log", + "simd_cesu8", + "thiserror 2.0.18", + "walkdir", + "windows-link 0.2.1", +] + +[[package]] +name = "jni-macros" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version", + "simd_cesu8", + "syn", +] + +[[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.94" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e04e2ef80ce82e13552136fabeef8a5ed1f985a96805761cbb9a2c34e7664d9" +dependencies = [ + "cfg-if", + "futures-util", + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "kase-controller" +version = "0.6.0" +dependencies = [ + "rfd", + "serde", + "serde_json", + "serialport", + "slint", + "slint-build", +] + +[[package]] +name = "keyboard-types" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b750dcadc39a09dbadd74e118f6dd6598df77fa01df0cfcdc52c28dece74528a" +dependencies = [ + "bitflags 2.11.0", + "serde", + "unicode-segmentation", +] + +[[package]] +name = "khronos_api" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc" + +[[package]] +name = "kurbo" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce9729cc38c18d86123ab736fd2e7151763ba226ac2490ec092d1dd148825e32" +dependencies = [ + "arrayvec", + "euclid", + "smallvec", +] + +[[package]] +name = "kurbo" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7564e90fe3c0d5771e1f0bc95322b21baaeaa0d9213fa6a0b61c99f8b17b3bfb" +dependencies = [ + "arrayvec", + "euclid", + "smallvec", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "lebe" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a79a3332a6609480d7d0c9eab957bca6b455b91bb84e66d19f5ff66294b85b8" + +[[package]] +name = "libc" +version = "0.2.184" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48f5d2a454e16a5ea0f4ced81bd44e4cfc7bd3a507b61887c99fd3538b28e4af" + +[[package]] +name = "libfuzzer-sys" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f12a681b7dd8ce12bff52488013ba614b869148d54dd79836ab85aafdd53f08d" +dependencies = [ + "arbitrary", + "cc", +] + +[[package]] +name = "libloading" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" +dependencies = [ + "cfg-if", + "windows-link 0.2.1", +] + +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[package]] +name = "libredox" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ddbf48fd451246b1f8c2610bd3b4ac0cc6e149d89832867093ab69a17194f08" +dependencies = [ + "bitflags 2.11.0", + "libc", + "plain", + "redox_syscall 0.7.3", +] + +[[package]] +name = "libudev" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78b324152da65df7bb95acfcaab55e3097ceaab02fb19b228a9eb74d55f135e0" +dependencies = [ + "libc", + "libudev-sys", +] + +[[package]] +name = "libudev-sys" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c8469b4a23b962c1396b9b451dda50ef5b283e8dd309d69033475fa9b334324" +dependencies = [ + "libc", + "pkg-config", +] + +[[package]] +name = "linebender_resource_handle" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4a5ff6bcca6c4867b1c4fd4ef63e4db7436ef363e0ad7531d1558856bae64f4" + +[[package]] +name = "linked-hash-map" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" + +[[package]] +name = "linked_hash_set" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "984fb35d06508d1e69fc91050cceba9c0b748f983e6739fa2c7a9237154c52c8" +dependencies = [ + "linked-hash-map", +] + +[[package]] +name = "linux-raw-sys" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + +[[package]] +name = "linux-raw-sys" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "litemap" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "loop9" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fae87c125b03c1d2c0150c90365d7d6bcc53fb73a9acaef207d2d065860f062" +dependencies = [ + "imgref", +] + +[[package]] +name = "lyon_algorithms" +version = "1.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9815fac08e6fd96733a11dce4f9d15a3f338e96a2e2311ee21e1b738efc2bc0f" +dependencies = [ + "lyon_path", + "num-traits", +] + +[[package]] +name = "lyon_extra" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7755f08423275157ad1680aaecc9ccb7e0cc633da3240fea2d1522935cc15c72" +dependencies = [ + "lyon_path", + "thiserror 2.0.18", +] + +[[package]] +name = "lyon_geom" +version = "1.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4336502e29e32af93cf2dad2214ed6003c17ceb5bd499df77b1de663b9042b92" +dependencies = [ + "arrayvec", + "euclid", + "num-traits", +] + +[[package]] +name = "lyon_path" +version = "1.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c463f9c428b7fc5ec885dcd39ce4aa61e29111d0e33483f6f98c74e89d8621e" +dependencies = [ + "lyon_geom", + "num-traits", +] + +[[package]] +name = "mach2" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d640282b302c0bb0a2a8e0233ead9035e3bed871f0b7e81fe4a1ec829765db44" +dependencies = [ + "libc", +] + +[[package]] +name = "maybe-rayon" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ea1f30cedd69f0a2954655f7188c6a834246d2bcf1e315e2ac40c4b24dc9519" +dependencies = [ + "cfg-if", + "rayon", +] + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "memmap2" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "714098028fe011992e1c3962653c96b2d578c4b4bce9036e15ff220319b1e0e3" +dependencies = [ + "libc", +] + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "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 = "muda" +version = "0.17.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c9fec5a4e89860383d778d10563a605838f8f0b2f9303868937e5ff32e86177" +dependencies = [ + "crossbeam-channel", + "dpi", + "keyboard-types", + "objc2 0.6.4", + "objc2-app-kit 0.3.2", + "objc2-core-foundation", + "objc2-foundation 0.3.2", + "once_cell", + "png 0.17.16", + "thiserror 2.0.18", + "windows-sys 0.60.2", +] + +[[package]] +name = "natord" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "308d96db8debc727c3fd9744aac51751243420e46edf401010908da7f8d5e57c" + +[[package]] +name = "ndk" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" +dependencies = [ + "bitflags 2.11.0", + "jni-sys 0.3.1", + "log", + "ndk-sys", + "num_enum", + "raw-window-handle", + "thiserror 1.0.69", +] + +[[package]] +name = "ndk-context" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" + +[[package]] +name = "ndk-sys" +version = "0.6.0+11769913" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873" +dependencies = [ + "jni-sys 0.3.1", +] + +[[package]] +name = "new_debug_unreachable" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" + +[[package]] +name = "nix" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "598beaf3cc6fdd9a5dfb1630c2800c7acd31df7aaf0f565796fba2b53ca1af1b" +dependencies = [ + "bitflags 1.3.2", + "cfg-if", + "libc", +] + +[[package]] +name = "nix" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" +dependencies = [ + "bitflags 2.11.0", + "cfg-if", + "cfg_aliases", + "libc", +] + +[[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 = "nom" +version = "8.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405" +dependencies = [ + "memchr", +] + +[[package]] +name = "noop_proc_macro" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8" + +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-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-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", + "libm", +] + +[[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-sys" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb91bdd390c7ce1a8607f35f3ca7151b65afc0ff5ff3b34fa350f7d7c7e4310" + +[[package]] +name = "objc2" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46a785d4eeff09c14c487497c162e92766fbb3e4059a71840cecc03d9a50b804" +dependencies = [ + "objc-sys", + "objc2-encode", +] + +[[package]] +name = "objc2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" +dependencies = [ + "objc2-encode", +] + +[[package]] +name = "objc2-app-kit" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4e89ad9e3d7d297152b17d39ed92cd50ca8063a89a9fa569046d41568891eff" +dependencies = [ + "bitflags 2.11.0", + "block2 0.5.1", + "libc", + "objc2 0.5.2", + "objc2-core-data 0.2.2", + "objc2-core-image 0.2.2", + "objc2-foundation 0.2.2", + "objc2-quartz-core 0.2.2", +] + +[[package]] +name = "objc2-app-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" +dependencies = [ + "bitflags 2.11.0", + "block2 0.6.2", + "libc", + "objc2 0.6.4", + "objc2-cloud-kit 0.3.2", + "objc2-core-data 0.3.2", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-core-image 0.3.2", + "objc2-core-text", + "objc2-core-video", + "objc2-foundation 0.3.2", + "objc2-quartz-core 0.3.2", +] + +[[package]] +name = "objc2-cloud-kit" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74dd3b56391c7a0596a295029734d3c1c5e7e510a4cb30245f8221ccea96b009" +dependencies = [ + "bitflags 2.11.0", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-core-location", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-cloud-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ad74d880bb43877038da939b7427bba67e9dd42004a18b809ba7d87cee241c" +dependencies = [ + "bitflags 2.11.0", + "objc2 0.6.4", + "objc2-foundation 0.3.2", +] + +[[package]] +name = "objc2-contacts" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5ff520e9c33812fd374d8deecef01d4a840e7b41862d849513de77e44aa4889" +dependencies = [ + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-core-data" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef" +dependencies = [ + "bitflags 2.11.0", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-core-data" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b402a653efbb5e82ce4df10683b6b28027616a2715e90009947d50b8dd298fa" +dependencies = [ + "bitflags 2.11.0", + "objc2 0.6.4", + "objc2-foundation 0.3.2", +] + +[[package]] +name = "objc2-core-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" +dependencies = [ + "bitflags 2.11.0", + "dispatch2", + "objc2 0.6.4", +] + +[[package]] +name = "objc2-core-graphics" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" +dependencies = [ + "bitflags 2.11.0", + "dispatch2", + "objc2 0.6.4", + "objc2-core-foundation", + "objc2-io-surface", +] + +[[package]] +name = "objc2-core-image" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55260963a527c99f1819c4f8e3b47fe04f9650694ef348ffd2227e8196d34c80" +dependencies = [ + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", + "objc2-metal 0.2.2", +] + +[[package]] +name = "objc2-core-image" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5d563b38d2b97209f8e861173de434bd0214cf020e3423a52624cd1d989f006" +dependencies = [ + "objc2 0.6.4", + "objc2-foundation 0.3.2", +] + +[[package]] +name = "objc2-core-location" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "000cfee34e683244f284252ee206a27953279d370e309649dc3ee317b37e5781" +dependencies = [ + "block2 0.5.1", + "objc2 0.5.2", + "objc2-contacts", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-core-text" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cde0dfb48d25d2b4862161a4d5fcc0e3c24367869ad306b0c9ec0073bfed92d" +dependencies = [ + "bitflags 2.11.0", + "objc2 0.6.4", + "objc2-core-foundation", + "objc2-core-graphics", +] + +[[package]] +name = "objc2-core-video" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d425caf1df73233f29fd8a5c3e5edbc30d2d4307870f802d18f00d83dc5141a6" +dependencies = [ + "bitflags 2.11.0", + "objc2 0.6.4", + "objc2-core-foundation", + "objc2-core-graphics", + "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.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" +dependencies = [ + "bitflags 2.11.0", + "block2 0.5.1", + "dispatch", + "libc", + "objc2 0.5.2", +] + +[[package]] +name = "objc2-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" +dependencies = [ + "bitflags 2.11.0", + "block2 0.6.2", + "objc2 0.6.4", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-io-surface" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" +dependencies = [ + "bitflags 2.11.0", + "objc2 0.6.4", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-link-presentation" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1a1ae721c5e35be65f01a03b6d2ac13a54cb4fa70d8a5da293d7b0020261398" +dependencies = [ + "block2 0.5.1", + "objc2 0.5.2", + "objc2-app-kit 0.2.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-metal" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6" +dependencies = [ + "bitflags 2.11.0", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-metal" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0125f776a10d00af4152d74616409f0d4a2053a6f57fa5b7d6aa2854ac04794" +dependencies = [ + "bitflags 2.11.0", + "objc2 0.6.4", + "objc2-foundation 0.3.2", +] + +[[package]] +name = "objc2-quartz-core" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a" +dependencies = [ + "bitflags 2.11.0", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", + "objc2-metal 0.2.2", +] + +[[package]] +name = "objc2-quartz-core" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f" +dependencies = [ + "bitflags 2.11.0", + "objc2 0.6.4", + "objc2-core-foundation", + "objc2-foundation 0.3.2", + "objc2-metal 0.3.2", +] + +[[package]] +name = "objc2-symbols" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a684efe3dec1b305badae1a28f6555f6ddd3bb2c2267896782858d5a78404dc" +dependencies = [ + "objc2 0.5.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-ui-kit" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8bb46798b20cd6b91cbd113524c490f1686f4c4e8f49502431415f3512e2b6f" +dependencies = [ + "bitflags 2.11.0", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-cloud-kit 0.2.2", + "objc2-core-data 0.2.2", + "objc2-core-image 0.2.2", + "objc2-core-location", + "objc2-foundation 0.2.2", + "objc2-link-presentation", + "objc2-quartz-core 0.2.2", + "objc2-symbols", + "objc2-uniform-type-identifiers", + "objc2-user-notifications", +] + +[[package]] +name = "objc2-ui-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22" +dependencies = [ + "bitflags 2.11.0", + "block2 0.6.2", + "objc2 0.6.4", + "objc2-core-foundation", + "objc2-foundation 0.3.2", + "objc2-quartz-core 0.3.2", +] + +[[package]] +name = "objc2-uniform-type-identifiers" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44fa5f9748dbfe1ca6c0b79ad20725a11eca7c2218bceb4b005cb1be26273bfe" +dependencies = [ + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-user-notifications" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76cfcbf642358e8689af64cee815d139339f3ed8ad05103ed5eaf73db8d84cb3" +dependencies = [ + "bitflags 2.11.0", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-core-location", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" +dependencies = [ + "critical-section", + "portable-atomic", +] + +[[package]] +name = "orbclient" +version = "0.3.51" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59aed3b33578edcfa1bc96a321d590d31832b6ad55a26f0313362ce687e9abd6" +dependencies = [ + "libc", + "libredox", +] + +[[package]] +name = "ordered-stream" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50" +dependencies = [ + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "owned_ttf_parser" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36820e9051aca1014ddc75770aab4d68bc1e9e632f0f5627c4086bc216fb583b" +dependencies = [ + "ttf-parser 0.25.1", +] + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + +[[package]] +name = "parley" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ada5338c3a9794af7342e6f765b6e78740db37378aced034d7bf72c96b94ed94" +dependencies = [ + "fontique", + "harfrust", + "hashbrown 0.16.1", + "linebender_resource_handle", + "skrifa 0.37.0", + "swash", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "pastey" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35fb2e5f958ec131621fdd531e9fc186ed768cbe395337403ae56c17a74c68ec" + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pico-args" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315" + +[[package]] +name = "pin-project" +version = "1.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1749c7ed4bcaf4c3d0a3efc28538844fb29bcdd7d2b67b2be7e20ba861ff517" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b20ed30f105399776b9c883e68e536ef602a16ae6f596d2c473591d6ad64c6" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pin-weak" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b330c9d1b92dfe68442ca20b009c717d5f0b1e3cf4965e62f704c3c6e95a1305" + +[[package]] +name = "piper" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c835479a4443ded371d6c535cbfd8d31ad92c5d23ae9770a61bc155e4992a3c1" +dependencies = [ + "atomic-waker", + "fastrand", + "futures-io", +] + +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "plain" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" + +[[package]] +name = "png" +version = "0.17.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526" +dependencies = [ + "bitflags 1.3.2", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + +[[package]] +name = "png" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" +dependencies = [ + "bitflags 2.11.0", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + +[[package]] +name = "polling" +version = "3.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" +dependencies = [ + "cfg-if", + "concurrent-queue", + "hermit-abi 0.5.2", + "pin-project-lite", + "rustix 1.1.4", + "windows-sys 0.61.2", +] + +[[package]] +name = "pollster" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f3a9f18d041e6d0e102a0a46750538147e5e8992d3b4873aaafee2520b00ce3" + +[[package]] +name = "portable-atomic" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" +dependencies = [ + "critical-section", +] + +[[package]] +name = "potential_utf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" +dependencies = [ + "zerovec", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[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 0.25.10+spec-1.1.0", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "profiling" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eb8486b569e12e2c32ad3e204dbaba5e4b5b216e9367044f25f1dba42341773" +dependencies = [ + "profiling-procmacros", +] + +[[package]] +name = "profiling-procmacros" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52717f9a02b6965224f95ca2a81e2e0c5c43baacd28ca057577988930b6c3d5b" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "pulldown-cmark" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c3a14896dfa883796f1cb410461aef38810ea05f2b2c33c5aded3649095fdad" +dependencies = [ + "bitflags 2.11.0", + "getopts", + "memchr", + "pulldown-cmark-escape", + "unicase", +] + +[[package]] +name = "pulldown-cmark-escape" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "007d8adb5ddab6f8e3f491ac63566a7d5002cc7ed73901f72057943fa71ae1ae" + +[[package]] +name = "pxfm" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5a041e753da8b807c9255f28de81879c78c876392ff2469cde94799b2896b9d" + +[[package]] +name = "qoi" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f6d64c71eb498fe9eae14ce4ec935c555749aef511cca85b5568910d6e48001" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "qttypes" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7edf5b38c97ad8900ad2a8418ee44b4adceaa866a4a3405e2f1c909871d7ebd" +dependencies = [ + "cpp", + "cpp_build", + "semver", +] + +[[package]] +name = "quick-error" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" + +[[package]] +name = "quick-xml" +version = "0.38.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b66c2058c55a409d601666cffe35f04333cf1013010882cec174a7467cd4e21c" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "quick-xml" +version = "0.39.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "958f21e8e7ceb5a1aa7fa87fab28e7c75976e0bfe7e23ff069e0a260f894067d" +dependencies = [ + "memchr", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rand" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +dependencies = [ + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + +[[package]] +name = "rav1e" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43b6dd56e85d9483277cde964fd1bdb0428de4fec5ebba7540995639a21cb32b" +dependencies = [ + "aligned-vec", + "arbitrary", + "arg_enum_proc_macro", + "arrayvec", + "av-scenechange", + "av1-grain", + "bitstream-io", + "built", + "cfg-if", + "interpolate_name", + "itertools 0.14.0", + "libc", + "libfuzzer-sys", + "log", + "maybe-rayon", + "new_debug_unreachable", + "noop_proc_macro", + "num-derive", + "num-traits", + "paste", + "profiling", + "rand", + "rand_chacha", + "simd_helpers", + "thiserror 2.0.18", + "v_frame", + "wasm-bindgen", +] + +[[package]] +name = "ravif" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e52310197d971b0f5be7fe6b57530dcd27beb35c1b013f29d66c1ad73fbbcc45" +dependencies = [ + "avif-serialize", + "imgref", + "loop9", + "quick-error", + "rav1e", + "rayon", + "rgb", +] + +[[package]] +name = "raw-window-handle" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" + +[[package]] +name = "raw-window-metal" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40d213455a5f1dc59214213c7330e074ddf8114c9a42411eb890c767357ce135" +dependencies = [ + "objc2 0.6.4", + "objc2-core-foundation", + "objc2-foundation 0.3.2", + "objc2-quartz-core 0.3.2", +] + +[[package]] +name = "rayon" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "read-fonts" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6717cf23b488adf64b9d711329542ba34de147df262370221940dfabc2c91358" +dependencies = [ + "bytemuck", + "core_maths", + "font-types 0.10.1", +] + +[[package]] +name = "read-fonts" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b634fabf032fab15307ffd272149b622260f55974d9fad689292a5d33df02e5" +dependencies = [ + "bytemuck", + "font-types 0.11.2", +] + +[[package]] +name = "redox_syscall" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.11.0", +] + +[[package]] +name = "redox_syscall" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce70a74e890531977d37e532c34d45e9055d2409ed08ddba14529471ed0be16" +dependencies = [ + "bitflags 2.11.0", +] + +[[package]] +name = "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 = "resvg" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b563218631706d614e23059436526d005b50ab5f2d506b55a17eb65c5eb83419" +dependencies = [ + "gif", + "image-webp", + "log", + "pico-args", + "rgb", + "svgtypes", + "tiny-skia", + "usvg", + "zune-jpeg", +] + +[[package]] +name = "rfd" +version = "0.15.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef2bee61e6cffa4635c72d7d81a84294e28f0930db0ddcb0f66d10244674ebed" +dependencies = [ + "ashpd", + "block2 0.6.2", + "dispatch2", + "js-sys", + "log", + "objc2 0.6.4", + "objc2-app-kit 0.3.2", + "objc2-core-foundation", + "objc2-foundation 0.3.2", + "pollster", + "raw-window-handle", + "urlencoding", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "windows-sys 0.59.0", +] + +[[package]] +name = "rgb" +version = "0.8.53" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b34b781b31e5d73e9fbc8689c70551fd1ade9a19e3e28cfec8580a79290cc4" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "rowan" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "417a3a9f582e349834051b8a10c8d71ca88da4211e4093528e36b9845f6b5f21" +dependencies = [ + "countme", + "hashbrown 0.14.5", + "rustc-hash 1.1.0", + "text-size", +] + +[[package]] +name = "roxmltree" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1964b10c76125c36f8afe190065a4bf9a87bf324842c05701330bba9f1cacbb" +dependencies = [ + "memchr", +] + +[[package]] +name = "rspolib" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fda9a7796aff63a7b1b39ccc93fffaaf65e20042984b4843041a49ca4677535" +dependencies = [ + "lazy_static", + "natord", + "snafu", + "unicode-linebreak", + "unicode-width", +] + +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + +[[package]] +name = "rustc-hash" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "0.38.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +dependencies = [ + "bitflags 2.11.0", + "errno", + "libc", + "linux-raw-sys 0.4.15", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags 2.11.0", + "errno", + "libc", + "linux-raw-sys 0.12.1", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "rustybuzz" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd3c7c96f8a08ee34eff8857b11b49b07d71d1c3f4e88f8a88d4c9e9f90b1702" +dependencies = [ + "bitflags 2.11.0", + "bytemuck", + "core_maths", + "log", + "smallvec", + "ttf-parser 0.25.1", + "unicode-bidi-mirroring", + "unicode-ccc", + "unicode-properties", + "unicode-script", +] + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "scoped-tls" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" + +[[package]] +name = "scoped-tls-hkt" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9603871ffe5df3ac39cb624790c296dbd47a400d202f56bf3e414045099524d" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "sctk-adwaita" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6277f0217056f77f1d8f49f2950ac6c278c0d607c45f5ee99328d792ede24ec" +dependencies = [ + "ab_glyph", + "log", + "memmap2", + "smithay-client-toolkit 0.19.2", + "tiny-skia", +] + +[[package]] +name = "semver" +version = "1.0.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 = "serde_repr" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_spanned" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serialport" +version = "4.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4d91116f97173694f1642263b2ff837f80d933aa837e2314969f6728f661df3" +dependencies = [ + "bitflags 2.11.0", + "cfg-if", + "core-foundation 0.10.1", + "core-foundation-sys", + "io-kit-sys", + "libudev", + "mach2", + "nix 0.26.4", + "scopeguard", + "unescaper", + "windows-sys 0.52.0", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "simd-adler32" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" + +[[package]] +name = "simd_cesu8" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94f90157bb87cddf702797c5dadfa0be7d266cdf49e22da2fcaa32eff75b2c33" +dependencies = [ + "rustc_version", + "simdutf8", +] + +[[package]] +name = "simd_helpers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95890f873bec569a0362c235787f3aca6e1e887302ba4840839bcc6459c42da6" +dependencies = [ + "quote", +] + +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + +[[package]] +name = "simplecss" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a9c6883ca9c3c7c90e888de77b7a5c849c779d25d74a1269b0218b14e8b136c" +dependencies = [ + "log", +] + +[[package]] +name = "siphasher" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e" + +[[package]] +name = "skia-bindings" +version = "0.90.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f6f96e00735f14a781aac8a6870c862b8cc831df6d8e4ad77ab78e11411b9af" +dependencies = [ + "bindgen", + "cc", + "flate2", + "heck", + "pkg-config", + "regex", + "serde_json", + "tar", + "toml", +] + +[[package]] +name = "skia-safe" +version = "0.90.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a71c01d325d40b1031dee67d251a5e0132e79e2a9ec272149a4f4a0d4b8b3be" +dependencies = [ + "bitflags 2.11.0", + "skia-bindings", + "windows 0.62.2", +] + +[[package]] +name = "skrifa" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c31071dedf532758ecf3fed987cdb4bd9509f900e026ab684b4ecb81ea49841" +dependencies = [ + "bytemuck", + "read-fonts 0.35.0", +] + +[[package]] +name = "skrifa" +version = "0.40.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fbdfe3d2475fbd7ddd1f3e5cf8288a30eb3e5f95832829570cd88115a7434ac" +dependencies = [ + "bytemuck", + "read-fonts 0.37.0", +] + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "slint" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d25b87d458205e79efb30545cae083aec2ccb1b192c46a55ae6d54403cdacb33" +dependencies = [ + "const-field-offset", + "i-slint-backend-qt", + "i-slint-backend-selector", + "i-slint-common", + "i-slint-core", + "i-slint-core-macros", + "i-slint-renderer-femtovg", + "i-slint-renderer-software", + "num-traits", + "once_cell", + "pin-weak", + "slint-macros", + "unicode-segmentation", + "vtable", +] + +[[package]] +name = "slint-build" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "064ef470cc8ab046319db94d0727f080fbd05322d07d774eb6de607d97defb8d" +dependencies = [ + "derive_more", + "fontique", + "i-slint-compiler", + "spin_on", + "toml_edit 0.24.1+spec-1.1.0", +] + +[[package]] +name = "slint-macros" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ecc09bbc42c780d5b7ed7d41f7573dfd67343e11cdac27c07b88a8f933958e6" +dependencies = [ + "i-slint-compiler", + "proc-macro2", + "quote", + "spin_on", +] + +[[package]] +name = "slotmap" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdd58c3c93c3d278ca835519292445cb4b0d4dc59ccfdf7ceadaab3f8aeb4038" +dependencies = [ + "version_check", +] + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "smithay-client-toolkit" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3457dea1f0eb631b4034d61d4d8c32074caa6cd1ab2d59f2327bd8461e2c0016" +dependencies = [ + "bitflags 2.11.0", + "calloop 0.13.0", + "calloop-wayland-source 0.3.0", + "cursor-icon", + "libc", + "log", + "memmap2", + "rustix 0.38.44", + "thiserror 1.0.69", + "wayland-backend", + "wayland-client", + "wayland-csd-frame", + "wayland-cursor", + "wayland-protocols", + "wayland-protocols-wlr", + "wayland-scanner", + "xkeysym", +] + +[[package]] +name = "smithay-client-toolkit" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0512da38f5e2b31201a93524adb8d3136276fa4fe4aafab4e1f727a82b534cc0" +dependencies = [ + "bitflags 2.11.0", + "calloop 0.14.4", + "calloop-wayland-source 0.4.1", + "cursor-icon", + "libc", + "log", + "memmap2", + "rustix 1.1.4", + "thiserror 2.0.18", + "wayland-backend", + "wayland-client", + "wayland-csd-frame", + "wayland-cursor", + "wayland-protocols", + "wayland-protocols-experimental", + "wayland-protocols-misc", + "wayland-protocols-wlr", + "wayland-scanner", + "xkeysym", +] + +[[package]] +name = "smithay-clipboard" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71704c03f739f7745053bde45fa203a46c58d25bc5c4efba1d9a60e9dba81226" +dependencies = [ + "libc", + "smithay-client-toolkit 0.20.0", + "wayland-backend", +] + +[[package]] +name = "smol_str" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd538fb6910ac1099850255cf94a94df6551fbdd602454387d0adb2d1ca6dead" +dependencies = [ + "serde", +] + +[[package]] +name = "smol_str" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4aaa7368fcf4852a4c2dd92df0cace6a71f2091ca0a23391ce7f3a31833f1523" +dependencies = [ + "borsh", + "serde_core", +] + +[[package]] +name = "snafu" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e84b3f4eacbf3a1ce05eac6763b4d629d60cbc94d632e4092c54ade71f1e1a2" +dependencies = [ + "snafu-derive", +] + +[[package]] +name = "snafu-derive" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1c97747dbf44bb1ca44a561ece23508e99cb592e862f22222dcf42f51d1e451" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "softbuffer" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aac18da81ebbf05109ab275b157c22a653bb3c12cf884450179942f81bcbf6c3" +dependencies = [ + "as-raw-xcb-connection", + "bytemuck", + "fastrand", + "js-sys", + "memmap2", + "ndk", + "objc2 0.6.4", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation 0.3.2", + "objc2-quartz-core 0.3.2", + "raw-window-handle", + "redox_syscall 0.5.18", + "rustix 1.1.4", + "tiny-xlib", + "tracing", + "wasm-bindgen", + "wayland-backend", + "wayland-client", + "wayland-sys", + "web-sys", + "windows-sys 0.61.2", + "x11rb", +] + +[[package]] +name = "spin_on" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "076e103ed41b9864aa838287efe5f4e3a7a0362dd00671ae62a212e5e4612da2" +dependencies = [ + "pin-utils", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "strict-num" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731" +dependencies = [ + "float-cmp", +] + +[[package]] +name = "strum" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "svgtypes" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "695b5790b3131dafa99b3bbfd25a216edb3d216dad9ca208d4657bfb8f2abc3d" +dependencies = [ + "kurbo 0.13.0", + "siphasher", +] + +[[package]] +name = "swash" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "842f3cd369c2ba38966204f983eaa5e54a8e84a7d7159ed36ade2b6c335aae64" +dependencies = [ + "skrifa 0.40.0", + "yazi", + "zeno", +] + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "sys-locale" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eab9a99a024a169fe8a903cf9d4a3b3601109bcc13bd9e3c6fff259138626c4" +dependencies = [ + "js-sys", + "libc", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "tar" +version = "0.4.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22692a6476a21fa75fdfc11d452fda482af402c008cdbaf3476414e122040973" +dependencies = [ + "filetime", + "libc", + "xattr", +] + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.2", + "once_cell", + "rustix 1.1.4", + "windows-sys 0.61.2", +] + +[[package]] +name = "text-size" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f18aa187839b2bdb1ad2fa35ead8c4c2976b64e4363c386d45ac0f7ee85c9233" + +[[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 = "tiny-skia" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83d13394d44dae3207b52a326c0c85a8bf87f1541f23b0d143811088497b09ab" +dependencies = [ + "arrayref", + "arrayvec", + "bytemuck", + "cfg-if", + "log", + "png 0.17.16", + "tiny-skia-path", +] + +[[package]] +name = "tiny-skia-path" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c9e7fc0c2e86a30b117d0462aa261b72b7a99b7ebd7deb3a14ceda95c5bdc93" +dependencies = [ + "arrayref", + "bytemuck", + "strict-num", +] + +[[package]] +name = "tiny-xlib" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0324504befd01cab6e0c994f34b2ffa257849ee019d3fb3b64fb2c858887d89e" +dependencies = [ + "as-raw-xcb-connection", + "ctor-lite", + "libloading", + "pkg-config", + "tracing", +] + +[[package]] +name = "tinystr" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" +dependencies = [ + "displaydoc", + "serde_core", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "toml" +version = "0.9.12+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863" +dependencies = [ + "indexmap", + "serde_core", + "serde_spanned", + "toml_datetime 0.7.5+spec-1.1.0", + "toml_parser", + "toml_writer", + "winnow 0.7.15", +] + +[[package]] +name = "toml_datetime" +version = "0.7.5+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" +dependencies = [ + "serde_core", +] + +[[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.24.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01f2eadbbc6b377a847be05f60791ef1058d9f696ecb51d2c07fe911d8569d8e" +dependencies = [ + "indexmap", + "toml_datetime 0.7.5+spec-1.1.0", + "toml_parser", + "toml_writer", + "winnow 0.7.15", +] + +[[package]] +name = "toml_edit" +version = "0.25.10+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a82418ca169e235e6c399a84e395ab6debeb3bc90edc959bf0f48647c6a32d1b" +dependencies = [ + "indexmap", + "toml_datetime 1.1.1+spec-1.1.0", + "toml_parser", + "winnow 1.0.1", +] + +[[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 1.0.1", +] + +[[package]] +name = "toml_writer" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "ttf-parser" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c591d83f69777866b9126b24c6dd9a18351f177e49d625920d19f989fd31cf8" + +[[package]] +name = "ttf-parser" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31" +dependencies = [ + "core_maths", +] + +[[package]] +name = "typed-index-collections" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "898160f1dfd383b4e92e17f0512a7d62f3c51c44937b23b6ffc3a1614a8eaccd" +dependencies = [ + "bincode", + "serde", +] + +[[package]] +name = "udev" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af4e37e9ea4401fc841ff54b9ddfc9be1079b1e89434c1a6a865dd68980f7e9f" +dependencies = [ + "io-lifetimes", + "libc", + "libudev-sys", + "pkg-config", +] + +[[package]] +name = "uds_windows" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e" +dependencies = [ + "memoffset", + "tempfile", + "windows-sys 0.61.2", +] + +[[package]] +name = "unescaper" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4064ed685c487dbc25bd3f0e9548f2e34bab9d18cefc700f9ec2dba74ba1138e" +dependencies = [ + "thiserror 2.0.18", +] + +[[package]] +name = "unicase" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" + +[[package]] +name = "unicode-bidi" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" + +[[package]] +name = "unicode-bidi-mirroring" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dfa6e8c60bb66d49db113e0125ee8711b7647b5579dc7f5f19c42357ed039fe" + +[[package]] +name = "unicode-ccc" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce61d488bcdc9bc8b5d1772c404828b17fc481c0a582b5581e95fb233aef503e" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-linebreak" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f" + +[[package]] +name = "unicode-properties" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" + +[[package]] +name = "unicode-script" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "383ad40bb927465ec0ce7720e033cb4ca06912855fc35db31b5755d0de75b1ee" + +[[package]] +name = "unicode-segmentation" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c" + +[[package]] +name = "unicode-vo" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1d386ff53b415b7fe27b50bb44679e2cc4660272694b7b6f3326d8480823a94" + +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "unty" +version = "0.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d49784317cd0d1ee7ec5c716dd598ec5b4483ea832a2dced265471cc0f690ae" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", + "serde_derive", +] + +[[package]] +name = "urlencoding" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + +[[package]] +name = "usvg" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e419dff010bb12512b0ae9e3d2f318dfbdf0167fde7eb05465134d4e8756076f" +dependencies = [ + "base64", + "data-url", + "flate2", + "fontdb", + "imagesize", + "kurbo 0.13.0", + "log", + "pico-args", + "roxmltree", + "rustybuzz", + "simplecss", + "siphasher", + "strict-num", + "svgtypes", + "tiny-skia-path", + "unicode-bidi", + "unicode-script", + "unicode-vo", + "xmlwriter", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "uuid" +version = "1.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ac8b6f42ead25368cf5b098aeb3dc8a1a2c05a3eee8a9a1a68c640edbfc79d9" +dependencies = [ + "js-sys", + "serde_core", + "wasm-bindgen", +] + +[[package]] +name = "v_frame" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "666b7727c8875d6ab5db9533418d7c764233ac9c0cff1d469aec8fa127597be2" +dependencies = [ + "aligned-vec", + "num-traits", + "wasm-bindgen", +] + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "vtable" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "753be81c38dff787d177b5939af1fa16f72f0d0d21a6b7d74ae56e29cd26f2a6" +dependencies = [ + "const-field-offset", + "portable-atomic", + "stable_deref_trait", + "vtable-macro", +] + +[[package]] +name = "vtable-macro" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cfcf6171aa2b0f85718ca5888ca32f6edf61d1849f8e4b3786ad890e5b68f68" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[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.2+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0551fc1bb415591e3372d0bc4780db7e587d84e2a7e79da121051c5c4b89d0b0" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.67" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03623de6905b7206edd0a75f69f747f134b7f0a2323392d664448bf2d3c5d87e" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fbdf9a35adf44786aecd5ff89b4563a90325f9da0923236f6104e603c7e86be" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dca9693ef2bab6d4e6707234500350d8dad079eb508dca05530c85dc3a529ff2" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39129a682a6d2d841b6c429d0c51e5cb0ed1a03829d8b3d1e69a011e62cb3d3b" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags 2.11.0", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + +[[package]] +name = "wayland-backend" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2857dd20b54e916ec7253b3d6b4d5c4d7d4ca2c33c2e11c6c76a99bd8744755d" +dependencies = [ + "cc", + "downcast-rs", + "rustix 1.1.4", + "scoped-tls", + "smallvec", + "wayland-sys", +] + +[[package]] +name = "wayland-client" +version = "0.31.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "645c7c96bb74690c3189b5c9cb4ca1627062bb23693a4fad9d8c3de958260144" +dependencies = [ + "bitflags 2.11.0", + "rustix 1.1.4", + "wayland-backend", + "wayland-scanner", +] + +[[package]] +name = "wayland-csd-frame" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625c5029dbd43d25e6aa9615e88b829a5cad13b2819c4ae129fdbb7c31ab4c7e" +dependencies = [ + "bitflags 2.11.0", + "cursor-icon", + "wayland-backend", +] + +[[package]] +name = "wayland-cursor" +version = "0.31.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a52d18780be9b1314328a3de5f930b73d2200112e3849ca6cb11822793fb34d" +dependencies = [ + "rustix 1.1.4", + "wayland-client", + "xcursor", +] + +[[package]] +name = "wayland-protocols" +version = "0.32.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "563a85523cade2429938e790815fd7319062103b9f4a2dc806e9b53b95982d8f" +dependencies = [ + "bitflags 2.11.0", + "wayland-backend", + "wayland-client", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-experimental" +version = "20250721.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40a1f863128dcaaec790d7b4b396cc9b9a7a079e878e18c47e6c2d2c5a8dcbb1" +dependencies = [ + "bitflags 2.11.0", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-misc" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e9567599ef23e09b8dad6e429e5738d4509dfc46b3b21f32841a304d16b29c8" +dependencies = [ + "bitflags 2.11.0", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-plasma" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b6d8cf1eb2c1c31ed1f5643c88a6e53538129d4af80030c8cabd1f9fa884d91" +dependencies = [ + "bitflags 2.11.0", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-wlr" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb04e52f7836d7c7976c78ca0250d61e33873c34156a2a1fc9474828ec268234" +dependencies = [ + "bitflags 2.11.0", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-scanner", +] + +[[package]] +name = "wayland-scanner" +version = "0.31.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c324a910fd86ebdc364a3e61ec1f11737d3b1d6c273c0239ee8ff4bc0d24b4a" +dependencies = [ + "proc-macro2", + "quick-xml 0.39.2", + "quote", +] + +[[package]] +name = "wayland-sys" +version = "0.31.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8eab23fefc9e41f8e841df4a9c707e8a8c4ed26e944ef69297184de2785e3be" +dependencies = [ + "dlib", + "log", + "once_cell", + "pkg-config", +] + +[[package]] +name = "web-sys" +version = "0.3.94" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd70027e39b12f0849461e08ffc50b9cd7688d942c1c8e3c7b22273236b4dd0a" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "weezl" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88" + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "windows" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd04d41d93c4992d421894c18c8b43496aa748dd4c081bac0dc93eb0489272b6" +dependencies = [ + "windows-core 0.58.0", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows" +version = "0.61.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" +dependencies = [ + "windows-collections 0.2.0", + "windows-core 0.61.2", + "windows-future 0.2.1", + "windows-link 0.1.3", + "windows-numerics 0.2.0", +] + +[[package]] +name = "windows" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580" +dependencies = [ + "windows-collections 0.3.2", + "windows-core 0.62.2", + "windows-future 0.3.2", + "windows-numerics 0.3.1", +] + +[[package]] +name = "windows-collections" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" +dependencies = [ + "windows-core 0.61.2", +] + +[[package]] +name = "windows-collections" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610" +dependencies = [ + "windows-core 0.62.2", +] + +[[package]] +name = "windows-core" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ba6d44ec8c2591c134257ce647b7ea6b20335bf6379a27dac5f1641fcf59f99" +dependencies = [ + "windows-implement 0.58.0", + "windows-interface 0.58.0", + "windows-result 0.2.0", + "windows-strings 0.1.0", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-core" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +dependencies = [ + "windows-implement 0.60.2", + "windows-interface 0.59.3", + "windows-link 0.1.3", + "windows-result 0.3.4", + "windows-strings 0.4.2", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement 0.60.2", + "windows-interface 0.59.3", + "windows-link 0.2.1", + "windows-result 0.4.1", + "windows-strings 0.5.1", +] + +[[package]] +name = "windows-future" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" +dependencies = [ + "windows-core 0.61.2", + "windows-link 0.1.3", + "windows-threading 0.1.0", +] + +[[package]] +name = "windows-future" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb" +dependencies = [ + "windows-core 0.62.2", + "windows-link 0.2.1", + "windows-threading 0.2.1", +] + +[[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-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +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-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-link" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-numerics" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" +dependencies = [ + "windows-core 0.61.2", + "windows-link 0.1.3", +] + +[[package]] +name = "windows-numerics" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26" +dependencies = [ + "windows-core 0.62.2", + "windows-link 0.2.1", +] + +[[package]] +name = "windows-result" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-result" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-strings" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" +dependencies = [ + "windows-result 0.2.0", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-strings" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link 0.2.1", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + +[[package]] +name = "windows-threading" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-threading" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "winit" +version = "0.30.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6755fa58a9f8350bd1e472d4c3fcc25f824ec358933bba33306d0b63df5978d" +dependencies = [ + "ahash", + "android-activity", + "atomic-waker", + "bitflags 2.11.0", + "block2 0.5.1", + "bytemuck", + "calloop 0.13.0", + "cfg_aliases", + "concurrent-queue", + "core-foundation 0.9.4", + "core-graphics", + "cursor-icon", + "dpi", + "js-sys", + "libc", + "memmap2", + "ndk", + "objc2 0.5.2", + "objc2-app-kit 0.2.2", + "objc2-foundation 0.2.2", + "objc2-ui-kit 0.2.2", + "orbclient", + "percent-encoding", + "pin-project", + "raw-window-handle", + "redox_syscall 0.4.1", + "rustix 0.38.44", + "sctk-adwaita", + "smithay-client-toolkit 0.19.2", + "smol_str 0.2.2", + "tracing", + "unicode-segmentation", + "wasm-bindgen", + "wasm-bindgen-futures", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-protocols-plasma", + "web-sys", + "web-time", + "windows-sys 0.52.0", + "x11-dl", + "x11rb", + "xkbcommon-dl", +] + +[[package]] +name = "winnow" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" +dependencies = [ + "memchr", +] + +[[package]] +name = "winnow" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09dac053f1cd375980747450bfc7250c264eaae0583872e845c0c7cd578872b5" +dependencies = [ + "memchr", +] + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn", + "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.0", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "write-fonts" +version = "0.43.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "886614b5ce857341226aa091f3c285e450683894acaaa7887f366c361efef79d" +dependencies = [ + "font-types 0.10.1", + "indexmap", + "kurbo 0.12.0", + "log", + "read-fonts 0.35.0", +] + +[[package]] +name = "writeable" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" + +[[package]] +name = "x11-clipboard" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "662d74b3d77e396b8e5beb00b9cad6a9eccf40b2ef68cc858784b14c41d535a3" +dependencies = [ + "libc", + "x11rb", +] + +[[package]] +name = "x11-dl" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38735924fedd5314a6e548792904ed8c6de6636285cb9fec04d5b1db85c1516f" +dependencies = [ + "libc", + "once_cell", + "pkg-config", +] + +[[package]] +name = "x11rb" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9993aa5be5a26815fe2c3eacfc1fde061fc1a1f094bf1ad2a18bf9c495dd7414" +dependencies = [ + "as-raw-xcb-connection", + "gethostname", + "libc", + "libloading", + "once_cell", + "rustix 1.1.4", + "x11rb-protocol", +] + +[[package]] +name = "x11rb-protocol" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea6fc2961e4ef194dcbfe56bb845534d0dc8098940c7e5c012a258bfec6701bd" + +[[package]] +name = "xattr" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156" +dependencies = [ + "libc", + "rustix 1.1.4", +] + +[[package]] +name = "xcursor" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bec9e4a500ca8864c5b47b8b482a73d62e4237670e5b5f1d6b9e3cae50f28f2b" + +[[package]] +name = "xkbcommon" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7a974f48060a14e95705c01f24ad9c3345022f4d97441b8a36beb7ed5c4a02d" +dependencies = [ + "libc", + "memmap2", + "xkeysym", +] + +[[package]] +name = "xkbcommon-dl" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d039de8032a9a8856a6be89cea3e5d12fdd82306ab7c94d74e6deab2460651c5" +dependencies = [ + "bitflags 2.11.0", + "dlib", + "log", + "once_cell", + "xkeysym", +] + +[[package]] +name = "xkeysym" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9cc00251562a284751c9973bace760d86c0276c471b4be569fe6b068ee97a56" + +[[package]] +name = "xml-rs" +version = "0.8.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ae8337f8a065cfc972643663ea4279e04e7256de865aa66fe25cec5fb912d3f" + +[[package]] +name = "xmlwriter" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec7a2a501ed189703dba8b08142f057e887dfc4b2cc4db2d343ac6376ba3e0b9" + +[[package]] +name = "y4m" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a5a4b21e1a62b67a2970e6831bc091d7b87e119e7f9791aef9702e3bef04448" + +[[package]] +name = "yazi" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01738255b5a16e78bbb83e7fbba0a1e7dd506905cfc53f4622d89015a03fbb5" + +[[package]] +name = "yeslogic-fontconfig-sys" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "503a066b4c037c440169d995b869046827dbc71263f6e8f3be6d77d4f3229dbd" +dependencies = [ + "dlib", + "once_cell", + "pkg-config", +] + +[[package]] +name = "yoke" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zbus" +version = "5.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca82f95dbd3943a40a53cfded6c2d0a2ca26192011846a1810c4256ef92c60bc" +dependencies = [ + "async-broadcast", + "async-executor", + "async-io", + "async-lock", + "async-process", + "async-recursion", + "async-task", + "async-trait", + "blocking", + "enumflags2", + "event-listener", + "futures-core", + "futures-lite", + "hex", + "libc", + "ordered-stream", + "rustix 1.1.4", + "serde", + "serde_repr", + "tracing", + "uds_windows", + "uuid", + "windows-sys 0.61.2", + "winnow 0.7.15", + "zbus_macros", + "zbus_names", + "zvariant", +] + +[[package]] +name = "zbus-lockstep" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6998de05217a084b7578728a9443d04ea4cd80f2a0839b8d78770b76ccd45863" +dependencies = [ + "zbus_xml", + "zvariant", +] + +[[package]] +name = "zbus-lockstep-macros" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10da05367f3a7b7553c8cdf8fa91aee6b64afebe32b51c95177957efc47ca3a0" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "zbus-lockstep", + "zbus_xml", + "zvariant", +] + +[[package]] +name = "zbus_macros" +version = "5.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897e79616e84aac4b2c46e9132a4f63b93105d54fe8c0e8f6bffc21fa8d49222" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn", + "zbus_names", + "zvariant", + "zvariant_utils", +] + +[[package]] +name = "zbus_names" +version = "4.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffd8af6d5b78619bab301ff3c560a5bd22426150253db278f164d6cf3b72c50f" +dependencies = [ + "serde", + "winnow 0.7.15", + "zvariant", +] + +[[package]] +name = "zbus_xml" +version = "5.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "441a0064125265655bccc3a6af6bef56814d9277ac83fce48b1cd7e160b80eac" +dependencies = [ + "quick-xml 0.38.4", + "serde", + "zbus_names", + "zvariant", +] + +[[package]] +name = "zeno" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6df3dc4292935e51816d896edcd52aa30bc297907c26167fec31e2b0c6a32524" + +[[package]] +name = "zerocopy" +version = "0.8.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zerofrom" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69faa1f2a1ea75661980b013019ed6687ed0e83d069bc1114e2cc74c6c04c4df" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerotrie" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" +dependencies = [ + "serde", + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" + +[[package]] +name = "zune-core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb8a0807f7c01457d0379ba880ba6322660448ddebc890ce29bb64da71fb40f9" + +[[package]] +name = "zune-inflate" +version = "0.2.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ab332fe2f6680068f3582b16a24f90ad7096d5d39b974d1c0aff0125116f02" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "zune-jpeg" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27bc9d5b815bc103f142aa054f561d9187d191692ec7c2d1e2b4737f8dbd7296" +dependencies = [ + "zune-core", +] + +[[package]] +name = "zvariant" +version = "5.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5708299b21903bbe348e94729f22c49c55d04720a004aa350f1f9c122fd2540b" +dependencies = [ + "endi", + "enumflags2", + "serde", + "url", + "winnow 0.7.15", + "zvariant_derive", + "zvariant_utils", +] + +[[package]] +name = "zvariant_derive" +version = "5.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b59b012ebe9c46656f9cc08d8da8b4c726510aef12559da3e5f1bf72780752c" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn", + "zvariant_utils", +] + +[[package]] +name = "zvariant_utils" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f75c23a64ef8f40f13a6989991e643554d9bef1d682a281160cf0c1bc389c5e9" +dependencies = [ + "proc-macro2", + "quote", + "serde", + "syn", + "winnow 0.7.15", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..27b5243 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "kase-controller" +version = "0.6.0" +edition = "2021" + +[dependencies] +slint = "1" +serde = { version = "1", features = ["derive"] } +serde_json = "1" +serialport = "4" +rfd = "0.15" + +[build-dependencies] +slint-build = "1" + +[profile.release] +opt-level = "s" +lto = true +strip = true diff --git a/build.rs b/build.rs new file mode 100644 index 0000000..c34d653 --- /dev/null +++ b/build.rs @@ -0,0 +1,3 @@ +fn main() { + slint_build::compile("ui/main.slint").unwrap(); +} diff --git a/default.json b/default.json new file mode 100644 index 0000000..b9f6ac7 --- /dev/null +++ b/default.json @@ -0,0 +1,260 @@ +{ + "Group": { + "Children": [ + { + "Line": { + "Orientation": "Horizontal", + "Children": [ + { + "Group": { + "Children": [ + { + "Line": { + "Orientation": "Horizontal", + "Children": [ + { + "Line": { + "Children": [ + { "Keycap": { "Column": 0, "Row": 4 } }, + { "Keycap": { "Column": 0, "Row": 0 } }, + { "Keycap": { "Column": 0, "Row": 1 } }, + { "Keycap": { "Column": 0, "Row": 2 } }, + { "Keycap": { "Column": 0, "Row": 3 } } + ] + } + }, + { + "Line": { + "Children": [ + { "Keycap": { "Column": 1, "Row": 4 } }, + { "Keycap": { "Column": 1, "Row": 0 } }, + { "Keycap": { "Column": 1, "Row": 1 } }, + { "Keycap": { "Column": 1, "Row": 2 } }, + { "Keycap": { "Column": 1, "Row": 3 } } + ] + } + }, + { + "Line": { + "Margin": "8,25,0,0", + "RenderTransform": { "RotateTransform": { "Angle": 5 } }, + "Children": [ + { "Keycap": { "Column": 2, "Row": 0 } }, + { "Keycap": { "Column": 2, "Row": 1 } }, + { "Keycap": { "Column": 2, "Row": 2 } }, + { "Keycap": { "Column": 2, "Row": 3 } } + ] + } + }, + { + "Line": { + "Margin": "10,5,0,0", + "RenderTransform": { "RotateTransform": { "Angle": 10 } }, + "Children": [ + { "Keycap": { "Column": 3, "Row": 0 } }, + { "Keycap": { "Column": 3, "Row": 1 } }, + { "Keycap": { "Column": 3, "Row": 2 } }, + { "Keycap": { "Column": 3, "Row": 3 } } + ] + } + }, + { + "Line": { + "Margin": "0,25,0,0", + "RenderTransform": { "RotateTransform": { "Angle": 10 } }, + "Children": [ + { "Keycap": { "Column": 4, "Row": 0 } }, + { "Keycap": { "Column": 4, "Row": 1 } }, + { "Keycap": { "Column": 4, "Row": 2 } }, + { "Keycap": { "Column": 4, "Row": 3 } } + ] + } + }, + { + "Line": { + "Margin": "0,38,0,0", + "RenderTransform": { "RotateTransform": { "Angle": 10 } }, + "Children": [ + { "Keycap": { "Column": 5, "Row": 0 } }, + { "Keycap": { "Column": 5, "Row": 1 } }, + { "Keycap": { "Column": 5, "Row": 2 } }, + { "Keycap": { "Column": 5, "Row": 3 } } + ] + } + }, + { + "Keycap": { + "Column": 6, + "Row": 0, + "Margin": "-8,170,0,0", + "RenderTransform": { "RotateTransform": { "Angle": 10 } } + } + } + ] + } + }, + { "Keycap": { "Column": 2, "Row": 4, "Width": 60, "Margin": "110,228,0,0" } }, + { + "Keycap": { + "Column": 3, + "Row": 4, + "Margin": "174,225,0,0", + "RenderTransform": { "RotateTransform": { "Angle": 10 } } + } + }, + { + "Keycap": { + "Column": 4, + "Row": 4, + "Margin": "228,240,0,0", + "RenderTransform": { "RotateTransform": { "Angle": 20 } } + } + }, + { + "Keycap": { + "Column": 5, + "Row": 4, + "Margin": "280,270,0,0", + "RenderTransform": { "RotateTransform": { "Angle": 40 } } + } + }, + { + "Keycap": { + "Column": 6, + "Row": 4, + "Margin": "320,305,0,0", + "RenderTransform": { "RotateTransform": { "Angle": 40 } } + } + } + ] + } + }, + { + "Group": { + "Margin": "420,0,0,0", + "HorizontalAlignment": "Right", + "Children": [ + { + "Line": { + "Orientation": "Horizontal", + "Margin": "0,290,0,0", + "RenderTransform": { "RotateTransform": { "Angle": -40 } }, + "Children": [ + { "Keycap": { "Column": 6, "Row": 3 } }, + { "Keycap": { "Column": 7, "Row": 4 } } + ] + } + }, + { + "Keycap": { + "Column": 8, + "Row": 4, + "Margin": "95,245,0,0", + "RenderTransform": { "RotateTransform": { "Angle": -20 } } + } + }, + { + "Keycap": { + "Column": 9, + "Row": 4, + "Margin": "145,230,0,0", + "RenderTransform": { "RotateTransform": { "Angle": -10 } } + } + }, + { "Keycap": { "Column": 10, "Row": 4, "Width": 60, "Margin": "200,228,0,0" } }, + { + "Line": { + "Orientation": "Horizontal", + "Children": [ + { + "Keycap": { + "Column": 6, + "Row": 2, + "Margin": "0,170,-8,0", + "RenderTransform": { "RotateTransform": { "Angle": -10 } } + } + }, + { + "Line": { + "Margin": "0,38,0,0", + "RenderTransform": { "RotateTransform": { "Angle": -10 } }, + "Children": [ + { "Keycap": { "Column": 7, "Row": 0 } }, + { "Keycap": { "Column": 7, "Row": 1 } }, + { "Keycap": { "Column": 7, "Row": 2 } }, + { "Keycap": { "Column": 7, "Row": 3 } } + ] + } + }, + { + "Line": { + "Margin": "0,25,0,0", + "RenderTransform": { "RotateTransform": { "Angle": -10 } }, + "Children": [ + { "Keycap": { "Column": 8, "Row": 0 } }, + { "Keycap": { "Column": 8, "Row": 1 } }, + { "Keycap": { "Column": 8, "Row": 2 } }, + { "Keycap": { "Column": 8, "Row": 3 } } + ] + } + }, + { + "Line": { + "Margin": "0,5,10,0", + "RenderTransform": { "RotateTransform": { "Angle": -10 } }, + "Children": [ + { "Keycap": { "Column": 9, "Row": 0 } }, + { "Keycap": { "Column": 9, "Row": 1 } }, + { "Keycap": { "Column": 9, "Row": 2 } }, + { "Keycap": { "Column": 9, "Row": 3 } } + ] + } + }, + { + "Line": { + "Margin": "0,25,8,0", + "RenderTransform": { "RotateTransform": { "Angle": -5 } }, + "Children": [ + { "Keycap": { "Column": 10, "Row": 0 } }, + { "Keycap": { "Column": 10, "Row": 1 } }, + { "Keycap": { "Column": 10, "Row": 2 } }, + { "Keycap": { "Column": 10, "Row": 3 } } + ] + } + }, + { + "Line": { + "Children": [ + { "Keycap": { "Column": 11, "Row": 4 } }, + { "Keycap": { "Column": 11, "Row": 0 } }, + { "Keycap": { "Column": 11, "Row": 1 } }, + { "Keycap": { "Column": 11, "Row": 2 } }, + { "Keycap": { "Column": 11, "Row": 3 } } + + ] + } + }, + { + "Line": { + "Children": [ + { "Keycap": { "Column": 12, "Row": 4 } }, + { "Keycap": { "Column": 12, "Row": 0 } }, + { "Keycap": { "Column": 12, "Row": 1 } }, + { "Keycap": { "Column": 12, "Row": 2 } }, + { "Keycap": { "Column": 12, "Row": 3 } } + + ] + } + } + ] + } + } + ] + } + } + ] + } + } + ] + } +} \ No newline at end of file diff --git a/original-src/binary_protocol.rs b/original-src/binary_protocol.rs new file mode 100644 index 0000000..fa34b85 --- /dev/null +++ b/original-src/binary_protocol.rs @@ -0,0 +1,259 @@ +/// KaSe Binary CDC Protocol v2 +/// Frame: KS(2) + cmd(1) + len(2 LE) + payload(N) + crc8(1) +/// Response: KR(2) + cmd(1) + status(1) + len(2 LE) + payload(N) + crc8(1) + +#[allow(dead_code)] +pub mod cmd { + // System + pub const VERSION: u8 = 0x01; + pub const FEATURES: u8 = 0x02; + pub const DFU: u8 = 0x03; + pub const PING: u8 = 0x04; + + // Keymap + pub const SETLAYER: u8 = 0x10; + pub const SETKEY: u8 = 0x11; + pub const KEYMAP_CURRENT: u8 = 0x12; + pub const KEYMAP_GET: u8 = 0x13; + pub const LAYER_INDEX: u8 = 0x14; + pub const LAYER_NAME: u8 = 0x15; + + // Layout + pub const SET_LAYOUT_NAME: u8 = 0x20; + pub const LIST_LAYOUTS: u8 = 0x21; + pub const GET_LAYOUT_JSON: u8 = 0x22; + + // Macros + pub const LIST_MACROS: u8 = 0x30; + pub const MACRO_ADD: u8 = 0x31; + pub const MACRO_ADD_SEQ: u8 = 0x32; + pub const MACRO_DELETE: u8 = 0x33; + + // Statistics + pub const KEYSTATS_BIN: u8 = 0x40; + pub const KEYSTATS_RESET: u8 = 0x42; + pub const BIGRAMS_BIN: u8 = 0x43; + pub const BIGRAMS_RESET: u8 = 0x45; + + // Tap Dance + pub const TD_SET: u8 = 0x50; + pub const TD_LIST: u8 = 0x51; + pub const TD_DELETE: u8 = 0x52; + + // Combos + pub const COMBO_SET: u8 = 0x60; + pub const COMBO_LIST: u8 = 0x61; + pub const COMBO_DELETE: u8 = 0x62; + + // Leader + pub const LEADER_SET: u8 = 0x70; + pub const LEADER_LIST: u8 = 0x71; + pub const LEADER_DELETE: u8 = 0x72; + + // Bluetooth + pub const BT_QUERY: u8 = 0x80; + pub const BT_SWITCH: u8 = 0x81; + pub const BT_PAIR: u8 = 0x82; + pub const BT_DISCONNECT: u8 = 0x83; + pub const BT_NEXT: u8 = 0x84; + pub const BT_PREV: u8 = 0x85; + + // Features + pub const AUTOSHIFT_TOGGLE: u8 = 0x90; + pub const KO_SET: u8 = 0x91; + pub const KO_LIST: u8 = 0x92; + pub const KO_DELETE: u8 = 0x93; + pub const WPM_QUERY: u8 = 0x94; + pub const TRILAYER_SET: u8 = 0x94; + + // Tamagotchi + pub const TAMA_QUERY: u8 = 0xA0; + pub const TAMA_ENABLE: u8 = 0xA1; + pub const TAMA_DISABLE: u8 = 0xA2; + pub const TAMA_FEED: u8 = 0xA3; + pub const TAMA_PLAY: u8 = 0xA4; + pub const TAMA_SLEEP: u8 = 0xA5; + pub const TAMA_MEDICINE: u8 = 0xA6; + pub const TAMA_SAVE: u8 = 0xA7; + + // OTA + pub const OTA_START: u8 = 0xF0; + pub const OTA_DATA: u8 = 0xF1; + pub const OTA_ABORT: u8 = 0xF2; +} + +#[allow(dead_code)] +pub mod status { + pub const OK: u8 = 0x00; + pub const ERR_UNKNOWN: u8 = 0x01; + pub const ERR_CRC: u8 = 0x02; + pub const ERR_INVALID: u8 = 0x03; + pub const ERR_RANGE: u8 = 0x04; + pub const ERR_BUSY: u8 = 0x05; + pub const ERR_OVERFLOW: u8 = 0x06; +} + +/// CRC-8/MAXIM (polynomial 0x31, init 0x00) +pub fn crc8(data: &[u8]) -> u8 { + let mut crc: u8 = 0x00; + for &b in data { + crc ^= b; + for _ in 0..8 { + crc = if crc & 0x80 != 0 { + (crc << 1) ^ 0x31 + } else { + crc << 1 + }; + } + } + crc +} + +/// Build a KS request frame. +pub fn ks_frame(cmd_id: u8, payload: &[u8]) -> Vec { + let len = payload.len() as u16; + let mut frame = Vec::with_capacity(6 + payload.len()); + frame.push(0x4B); // 'K' + frame.push(0x53); // 'S' + frame.push(cmd_id); + frame.push((len & 0xFF) as u8); + frame.push((len >> 8) as u8); + frame.extend_from_slice(payload); + frame.push(crc8(payload)); + frame +} + +/// Build MACRO_ADD_SEQ payload: [slot][name_len][name...][step_count][{kc,mod}...] +pub fn macro_add_seq_payload(slot: u8, name: &str, steps_hex: &str) -> Vec { + let name_bytes = name.as_bytes(); + let name_len = name_bytes.len().min(255) as u8; + + // Parse hex steps "06:01,FF:0A,19:01" into (kc, mod) pairs + let mut step_pairs: Vec<(u8, u8)> = Vec::new(); + if !steps_hex.is_empty() { + for part in steps_hex.split(',') { + let trimmed = part.trim(); + let kv: Vec<&str> = trimmed.split(':').collect(); + let has_two = kv.len() == 2; + if !has_two { + continue; + } + let kc = u8::from_str_radix(kv[0].trim(), 16).unwrap_or(0); + let md = u8::from_str_radix(kv[1].trim(), 16).unwrap_or(0); + step_pairs.push((kc, md)); + } + } + let step_count = step_pairs.len().min(255) as u8; + + let mut payload = Vec::new(); + payload.push(slot); + payload.push(name_len); + payload.extend_from_slice(&name_bytes[..name_len as usize]); + payload.push(step_count); + for (kc, md) in &step_pairs { + payload.push(*kc); + payload.push(*md); + } + payload +} + +/// Build MACRO_DELETE payload: [slot] +pub fn macro_delete_payload(slot: u8) -> Vec { + vec![slot] +} + +/// Build COMBO_SET payload: [index][r1][c1][r2][c2][result] +pub fn combo_set_payload(index: u8, r1: u8, c1: u8, r2: u8, c2: u8, result: u8) -> Vec { + vec![index, r1, c1, r2, c2, result] +} + +/// Build TD_SET payload: [index][a1][a2][a3][a4] +pub fn td_set_payload(index: u8, actions: &[u16; 4]) -> Vec { + vec![index, actions[0] as u8, actions[1] as u8, actions[2] as u8, actions[3] as u8] +} + +/// Build KO_SET payload: [index][trigger_key][trigger_mod][result_key][result_mod] +pub fn ko_set_payload(index: u8, trig_key: u8, trig_mod: u8, res_key: u8, res_mod: u8) -> Vec { + vec![index, trig_key, trig_mod, res_key, res_mod] +} + +/// Build LEADER_SET payload: [index][seq_len][seq...][result][result_mod] +pub fn leader_set_payload(index: u8, sequence: &[u8], result: u8, result_mod: u8) -> Vec { + let seq_len = sequence.len().min(4) as u8; + let mut payload = Vec::with_capacity(4 + sequence.len()); + payload.push(index); + payload.push(seq_len); + payload.extend_from_slice(&sequence[..seq_len as usize]); + payload.push(result); + payload.push(result_mod); + payload +} + +/// Parsed KR response. +#[derive(Debug)] +pub struct KrResponse { + #[allow(dead_code)] + pub cmd: u8, + pub status: u8, + pub payload: Vec, +} + +impl KrResponse { + pub fn is_ok(&self) -> bool { + self.status == status::OK + } + + pub fn status_name(&self) -> &str { + match self.status { + status::OK => "OK", + status::ERR_UNKNOWN => "ERR_UNKNOWN", + status::ERR_CRC => "ERR_CRC", + status::ERR_INVALID => "ERR_INVALID", + status::ERR_RANGE => "ERR_RANGE", + status::ERR_BUSY => "ERR_BUSY", + status::ERR_OVERFLOW => "ERR_OVERFLOW", + _ => "UNKNOWN", + } + } +} + +/// Parse a KR response from raw bytes. Returns (response, bytes_consumed). +pub fn parse_kr(data: &[u8]) -> Result<(KrResponse, usize), String> { + // Find KR magic + let pos = data + .windows(2) + .position(|w| w[0] == 0x4B && w[1] == 0x52) + .ok_or("No KR header found")?; + + if data.len() < pos + 7 { + return Err("Response too short".into()); + } + + let cmd = data[pos + 2]; + let status = data[pos + 3]; + let plen = data[pos + 4] as u16 | ((data[pos + 5] as u16) << 8); + let payload_start = pos + 6; + let payload_end = payload_start + plen as usize; + + if data.len() < payload_end + 1 { + return Err(format!( + "Incomplete response: need {} bytes, got {}", + payload_end + 1, + data.len() + )); + } + + let payload = data[payload_start..payload_end].to_vec(); + let expected_crc = data[payload_end]; + let actual_crc = crc8(&payload); + + if expected_crc != actual_crc { + return Err(format!( + "CRC mismatch: expected 0x{:02X}, got 0x{:02X}", + expected_crc, actual_crc + )); + } + + let consumed = payload_end + 1 - pos; + Ok((KrResponse { cmd, status, payload }, consumed)) +} diff --git a/original-src/flasher.rs b/original-src/flasher.rs new file mode 100644 index 0000000..8a688f8 --- /dev/null +++ b/original-src/flasher.rs @@ -0,0 +1,501 @@ +/// ESP32 ROM bootloader flasher via serial (CH340/CP2102 programming port). +/// Implements minimal SLIP-framed bootloader protocol for firmware flashing +/// without requiring esptool. + +#[cfg(not(target_arch = "wasm32"))] +use serialport::SerialPort; +#[cfg(not(target_arch = "wasm32"))] +use std::sync::mpsc; +#[cfg(not(target_arch = "wasm32"))] +use std::time::{Duration, Instant}; + +// ==================== SLIP framing ==================== + +const SLIP_END: u8 = 0xC0; +const SLIP_ESC: u8 = 0xDB; +const SLIP_ESC_END: u8 = 0xDC; +const SLIP_ESC_ESC: u8 = 0xDD; + +#[cfg(not(target_arch = "wasm32"))] +fn slip_encode(data: &[u8]) -> Vec { + let mut frame = Vec::with_capacity(data.len() + 10); + frame.push(SLIP_END); + for &byte in data { + match byte { + SLIP_END => { + frame.push(SLIP_ESC); + frame.push(SLIP_ESC_END); + } + SLIP_ESC => { + frame.push(SLIP_ESC); + frame.push(SLIP_ESC_ESC); + } + _ => frame.push(byte), + } + } + frame.push(SLIP_END); + frame +} + +#[cfg(not(target_arch = "wasm32"))] +fn slip_decode(frame: &[u8]) -> Vec { + let mut data = Vec::with_capacity(frame.len()); + let mut escaped = false; + for &byte in frame { + if escaped { + match byte { + SLIP_ESC_END => data.push(SLIP_END), + SLIP_ESC_ESC => data.push(SLIP_ESC), + _ => data.push(byte), + } + escaped = false; + } else if byte == SLIP_ESC { + escaped = true; + } else if byte != SLIP_END { + data.push(byte); + } + } + data +} + +// ==================== Bootloader commands ==================== + +const CMD_SYNC: u8 = 0x08; +const CMD_CHANGE_BAUDRATE: u8 = 0x0F; +const CMD_SPI_ATTACH: u8 = 0x0D; +const CMD_FLASH_BEGIN: u8 = 0x02; +const CMD_FLASH_DATA: u8 = 0x03; +const CMD_FLASH_END: u8 = 0x04; + +const FLASH_BLOCK_SIZE: u32 = 1024; +const INITIAL_BAUD: u32 = 115200; +const FLASH_BAUD: u32 = 460800; + +#[cfg(not(target_arch = "wasm32"))] +fn xor_checksum(data: &[u8]) -> u32 { + let mut chk: u8 = 0xEF; + for &b in data { + chk ^= b; + } + chk as u32 +} + +/// Build a bootloader command packet (before SLIP encoding). +/// Format: [0x00][cmd][size:u16 LE][checksum:u32 LE][data...] +#[cfg(not(target_arch = "wasm32"))] +fn build_command(cmd: u8, data: &[u8], checksum: u32) -> Vec { + let size = data.len() as u16; + let mut pkt = Vec::with_capacity(8 + data.len()); + pkt.push(0x00); // direction: command + pkt.push(cmd); + pkt.push((size & 0xFF) as u8); + pkt.push((size >> 8) as u8); + pkt.push((checksum & 0xFF) as u8); + pkt.push(((checksum >> 8) & 0xFF) as u8); + pkt.push(((checksum >> 16) & 0xFF) as u8); + pkt.push(((checksum >> 24) & 0xFF) as u8); + pkt.extend_from_slice(data); + pkt +} + +/// Extract complete SLIP frames from a byte buffer. +/// Returns (frames, remaining_bytes_not_consumed). +#[cfg(not(target_arch = "wasm32"))] +fn extract_slip_frames(raw: &[u8]) -> Vec> { + let mut frames = Vec::new(); + let mut in_frame = false; + let mut current = Vec::new(); + + for &byte in raw { + if byte == SLIP_END { + if in_frame && !current.is_empty() { + // End of frame + frames.push(current.clone()); + current.clear(); + in_frame = false; + } else { + // Start of frame (or consecutive 0xC0) + in_frame = true; + current.clear(); + } + } else if in_frame { + current.push(byte); + } + // If !in_frame and byte != SLIP_END, it's garbage — skip + } + frames +} + +/// Send a command and receive a valid response. +/// Handles boot log garbage and multiple SYNC responses. +#[cfg(not(target_arch = "wasm32"))] +fn send_command( + port: &mut Box, + cmd: u8, + data: &[u8], + checksum: u32, + timeout_ms: u64, +) -> Result, String> { + let pkt = build_command(cmd, data, checksum); + let frame = slip_encode(&pkt); + + port.write_all(&frame) + .map_err(|e| format!("Write error: {}", e))?; + port.flush() + .map_err(|e| format!("Flush error: {}", e))?; + + // Read bytes and extract SLIP frames, looking for a valid response + let mut raw = Vec::new(); + let mut buf = [0u8; 512]; + let start = Instant::now(); + let timeout = Duration::from_millis(timeout_ms); + + loop { + let elapsed = start.elapsed(); + if elapsed > timeout { + let got = if raw.is_empty() { + "nothing".to_string() + } else { + format!("{} raw bytes, no valid response", raw.len()) + }; + return Err(format!("Response timeout (got {})", got)); + } + + let read_result = port.read(&mut buf); + match read_result { + Ok(n) if n > 0 => { + raw.extend_from_slice(&buf[..n]); + } + _ => { + std::thread::sleep(Duration::from_millis(1)); + if raw.is_empty() { + continue; + } + } + } + + // Try to find a valid response in accumulated data + let frames = extract_slip_frames(&raw); + for slip_data in &frames { + let decoded = slip_decode(slip_data); + + if decoded.len() < 8 { + continue; + } + + let direction = decoded[0]; + let resp_cmd = decoded[1]; + + if direction != 0x01 || resp_cmd != cmd { + continue; + } + + // ROM bootloader status is at offset 8 (right after 8-byte header) + // Format: [dir][cmd][size:u16][value:u32][status][error][pad][pad] + if decoded.len() >= 10 { + let status = decoded[8]; + let error = decoded[9]; + if status != 0 { + return Err(format!("Bootloader error: cmd=0x{:02X} status={}, error={} (0x{:02X})", + cmd, status, error, error)); + } + } + + return Ok(decoded); + } + } +} + +// ==================== Bootloader entry ==================== + +/// Toggle DTR/RTS to reset ESP32 into bootloader mode. +/// Standard auto-reset circuit: DTR→EN, RTS→GPIO0. +#[cfg(not(target_arch = "wasm32"))] +fn enter_bootloader(port: &mut Box) -> Result<(), String> { + // Hold GPIO0 low (RTS=true) while pulsing EN (DTR) + port.write_data_terminal_ready(false) + .map_err(|e| format!("DTR error: {}", e))?; + port.write_request_to_send(true) + .map_err(|e| format!("RTS error: {}", e))?; + std::thread::sleep(Duration::from_millis(100)); + + // Release EN (DTR=true) while keeping GPIO0 low + port.write_data_terminal_ready(true) + .map_err(|e| format!("DTR error: {}", e))?; + port.write_request_to_send(false) + .map_err(|e| format!("RTS error: {}", e))?; + std::thread::sleep(Duration::from_millis(50)); + + // Release all + port.write_data_terminal_ready(false) + .map_err(|e| format!("DTR error: {}", e))?; + + // Drain any boot message + let _ = port.clear(serialport::ClearBuffer::All); + std::thread::sleep(Duration::from_millis(200)); + + Ok(()) +} + +// ==================== High-level commands ==================== + +#[cfg(not(target_arch = "wasm32"))] +fn sync(port: &mut Box) -> Result<(), String> { + // SYNC payload: [0x07, 0x07, 0x12, 0x20] + 32 x 0x55 + let mut payload = vec![0x07, 0x07, 0x12, 0x20]; + payload.extend_from_slice(&[0x55; 32]); + + for attempt in 0..10 { + let result = send_command(port, CMD_SYNC, &payload, 0, 500); + match result { + Ok(_) => return Ok(()), + Err(_) if attempt < 9 => { + // Drain any pending data before retry + let _ = port.clear(serialport::ClearBuffer::Input); + std::thread::sleep(Duration::from_millis(50)); + } + Err(e) => return Err(format!("SYNC failed after 10 attempts: {}", e)), + } + } + Err("SYNC failed".into()) +} + +/// Tell the bootloader to switch to a faster baud rate, then reconnect. +#[cfg(not(target_arch = "wasm32"))] +fn change_baudrate(port: &mut Box, new_baud: u32) -> Result<(), String> { + // Payload: [new_baud:u32 LE][old_baud:u32 LE] (old_baud=0 means "current") + let mut payload = Vec::with_capacity(8); + payload.extend_from_slice(&new_baud.to_le_bytes()); + payload.extend_from_slice(&0u32.to_le_bytes()); + + send_command(port, CMD_CHANGE_BAUDRATE, &payload, 0, 3000)?; + + // Switch host side to new baud + port.set_baud_rate(new_baud) + .map_err(|e| format!("Set baud error: {}", e))?; + + // Small delay for baud switch to take effect + std::thread::sleep(Duration::from_millis(50)); + let _ = port.clear(serialport::ClearBuffer::All); + + Ok(()) +} + +#[cfg(not(target_arch = "wasm32"))] +fn spi_attach(port: &mut Box) -> Result<(), String> { + let payload = [0u8; 8]; + send_command(port, CMD_SPI_ATTACH, &payload, 0, 3000)?; + Ok(()) +} + +#[cfg(not(target_arch = "wasm32"))] +fn flash_begin( + port: &mut Box, + offset: u32, + total_size: u32, + block_size: u32, +) -> Result<(), String> { + let num_blocks = (total_size + block_size - 1) / block_size; + + let mut payload = Vec::with_capacity(20); + // erase_size + payload.extend_from_slice(&total_size.to_le_bytes()); + // num_blocks + payload.extend_from_slice(&num_blocks.to_le_bytes()); + // block_size + payload.extend_from_slice(&block_size.to_le_bytes()); + // offset + payload.extend_from_slice(&offset.to_le_bytes()); + // encrypted (ESP32-S3 requires this 5th field — 0 = not encrypted) + payload.extend_from_slice(&0u32.to_le_bytes()); + + // FLASH_BEGIN can take a while (flash erase) — long timeout + send_command(port, CMD_FLASH_BEGIN, &payload, 0, 30_000)?; + Ok(()) +} + +#[cfg(not(target_arch = "wasm32"))] +fn flash_data( + port: &mut Box, + seq: u32, + data: &[u8], +) -> Result<(), String> { + let data_len = data.len() as u32; + + let mut payload = Vec::with_capacity(16 + data.len()); + // data length + payload.extend_from_slice(&data_len.to_le_bytes()); + // sequence number + payload.extend_from_slice(&seq.to_le_bytes()); + // reserved (2 x u32) + payload.extend_from_slice(&0u32.to_le_bytes()); + payload.extend_from_slice(&0u32.to_le_bytes()); + // data + payload.extend_from_slice(data); + + let checksum = xor_checksum(data); + send_command(port, CMD_FLASH_DATA, &payload, checksum, 10_000)?; + Ok(()) +} + +#[cfg(not(target_arch = "wasm32"))] +fn flash_end(port: &mut Box, reboot: bool) -> Result<(), String> { + let flag: u32 = if reboot { 0 } else { 1 }; + let payload = flag.to_le_bytes(); + // FLASH_END might not get a response if device reboots + let _ = send_command(port, CMD_FLASH_END, &payload, 0, 2000); + + if reboot { + // Hard reset: toggle RTS to pulse EN pin (like esptool --after hard_reset) + std::thread::sleep(Duration::from_millis(100)); + port.write_request_to_send(true) + .map_err(|e| format!("RTS error: {}", e))?; + std::thread::sleep(Duration::from_millis(100)); + port.write_request_to_send(false) + .map_err(|e| format!("RTS error: {}", e))?; + } + + Ok(()) +} + +// ==================== Main entry point ==================== + +/// Flash firmware to ESP32 via programming port (CH340/CP2102). +/// Sends progress updates via the channel as (progress_0_to_1, status_message). +#[cfg(not(target_arch = "wasm32"))] +pub fn flash_firmware( + port_name: &str, + firmware: &[u8], + offset: u32, + tx: &mpsc::Sender, +) -> Result<(), String> { + let send_progress = |progress: f32, msg: String| { + let _ = tx.send(super::ui::BgResult::OtaProgress(progress, msg)); + }; + + send_progress(0.0, "Opening port...".into()); + + let builder = serialport::new(port_name, INITIAL_BAUD); + let builder_timeout = builder.timeout(Duration::from_millis(500)); + let mut port = builder_timeout.open() + .map_err(|e| format!("Cannot open {}: {}", port_name, e))?; + + // Step 1: Enter bootloader + send_progress(0.0, "Resetting into bootloader...".into()); + enter_bootloader(&mut port)?; + + // Step 2: Sync at 115200 + send_progress(0.0, "Syncing with bootloader...".into()); + sync(&mut port)?; + send_progress(0.02, "Bootloader sync OK".into()); + + // Step 3: Switch to 460800 baud for faster flashing + send_progress(0.03, format!("Switching to {} baud...", FLASH_BAUD)); + change_baudrate(&mut port, FLASH_BAUD)?; + send_progress(0.04, format!("Baud: {}", FLASH_BAUD)); + + // Step 4: SPI attach + send_progress(0.05, "Attaching SPI flash...".into()); + spi_attach(&mut port)?; + + // Step 5: Flash begin (this erases the flash — can take several seconds) + let total_size = firmware.len() as u32; + let num_blocks = (total_size + FLASH_BLOCK_SIZE - 1) / FLASH_BLOCK_SIZE; + send_progress(0.05, format!("Erasing flash ({} KB)...", total_size / 1024)); + flash_begin(&mut port, offset, total_size, FLASH_BLOCK_SIZE)?; + send_progress(0.10, "Flash erased, writing...".into()); + + // Step 6: Flash data blocks + for (i, chunk) in firmware.chunks(FLASH_BLOCK_SIZE as usize).enumerate() { + // Pad last block to block_size + let mut block = chunk.to_vec(); + let pad_needed = FLASH_BLOCK_SIZE as usize - block.len(); + if pad_needed > 0 { + block.extend(std::iter::repeat(0xFF).take(pad_needed)); + } + + flash_data(&mut port, i as u32, &block)?; + + let blocks_done = (i + 1) as f32; + let total_blocks = num_blocks as f32; + let progress = 0.10 + 0.85 * (blocks_done / total_blocks); + let msg = format!("Writing block {}/{} ({} KB / {} KB)", + i + 1, num_blocks, + ((i + 1) as u32 * FLASH_BLOCK_SIZE).min(total_size) / 1024, + total_size / 1024); + send_progress(progress, msg); + } + + // Step 7: Flash end + reboot + send_progress(0.97, "Finalizing...".into()); + flash_end(&mut port, true)?; + + send_progress(1.0, format!("Flash OK — {} KB written at 0x{:X}", total_size / 1024, offset)); + Ok(()) +} + +// ==================== Tests ==================== + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn slip_encode_no_special() { + let data = vec![0x01, 0x02, 0x03]; + let encoded = slip_encode(&data); + assert_eq!(encoded, vec![0xC0, 0x01, 0x02, 0x03, 0xC0]); + } + + #[test] + fn slip_encode_with_end_byte() { + let data = vec![0x01, 0xC0, 0x03]; + let encoded = slip_encode(&data); + assert_eq!(encoded, vec![0xC0, 0x01, 0xDB, 0xDC, 0x03, 0xC0]); + } + + #[test] + fn slip_encode_with_esc_byte() { + let data = vec![0x01, 0xDB, 0x03]; + let encoded = slip_encode(&data); + assert_eq!(encoded, vec![0xC0, 0x01, 0xDB, 0xDD, 0x03, 0xC0]); + } + + #[test] + fn slip_roundtrip() { + let original = vec![0xC0, 0xDB, 0x00, 0xFF, 0xC0]; + let encoded = slip_encode(&original); + let decoded = slip_decode(&encoded); + assert_eq!(decoded, original); + } + + #[test] + fn xor_checksum_basic() { + let data = vec![0x01, 0x02, 0x03]; + let chk = xor_checksum(&data); + let expected = 0xEF ^ 0x01 ^ 0x02 ^ 0x03; + assert_eq!(chk, expected as u32); + } + + #[test] + fn xor_checksum_empty() { + let chk = xor_checksum(&[]); + assert_eq!(chk, 0xEF); + } + + #[test] + fn build_command_format() { + let data = vec![0xAA, 0xBB]; + let pkt = build_command(0x08, &data, 0x12345678); + assert_eq!(pkt[0], 0x00); // direction + assert_eq!(pkt[1], 0x08); // command + assert_eq!(pkt[2], 0x02); // size low + assert_eq!(pkt[3], 0x00); // size high + assert_eq!(pkt[4], 0x78); // checksum byte 0 + assert_eq!(pkt[5], 0x56); // checksum byte 1 + assert_eq!(pkt[6], 0x34); // checksum byte 2 + assert_eq!(pkt[7], 0x12); // checksum byte 3 + assert_eq!(pkt[8], 0xAA); // data + assert_eq!(pkt[9], 0xBB); + } +} diff --git a/original-src/keycode.rs b/original-src/keycode.rs new file mode 100644 index 0000000..6709ee3 --- /dev/null +++ b/original-src/keycode.rs @@ -0,0 +1,390 @@ +/// Decode a raw 16-bit keycode into a human-readable string. +/// +/// Covers all KaSe firmware keycode ranges: HID basic keys, layer switches, +/// macros, Bluetooth, one-shot, mod-tap, layer-tap, tap-dance, and more. +pub fn decode_keycode(raw: u16) -> String { + // --- HID basic keycodes 0x00..=0xE7 --- + if raw <= 0x00E7 { + return hid_key_name(raw as u8); + } + + // --- MO (Momentary Layer): 0x0100..=0x0A00, low byte == 0 --- + if raw >= 0x0100 && raw <= 0x0A00 && (raw & 0xFF) == 0 { + let layer = (raw >> 8) - 1; + return format!("MO {layer}"); + } + + // --- TO (Toggle Layer): 0x0B00..=0x1400, low byte == 0 --- + if raw >= 0x0B00 && raw <= 0x1400 && (raw & 0xFF) == 0 { + let layer = (raw >> 8) - 0x0B; + return format!("TO {layer}"); + } + + // --- MACRO: 0x1500..=0x2800, low byte == 0 --- + if raw >= 0x1500 && raw <= 0x2800 && (raw & 0xFF) == 0 { + let idx = (raw >> 8) - 0x14; + return format!("M{idx}"); + } + + // --- BT keycodes --- + match raw { + 0x2900 => return "BT Next".into(), + 0x2A00 => return "BT Prev".into(), + 0x2B00 => return "BT Pair".into(), + 0x2C00 => return "BT Disc".into(), + 0x2E00 => return "USB/BT".into(), + 0x2F00 => return "BT On/Off".into(), + _ => {} + } + + // --- OSM (One-Shot Mod): 0x3000..=0x30FF --- + if raw >= 0x3000 && raw <= 0x30FF { + let mods = (raw & 0xFF) as u8; + return format!("OSM {}", mod_name(mods)); + } + + // --- OSL (One-Shot Layer): 0x3100..=0x310F --- + if raw >= 0x3100 && raw <= 0x310F { + let layer = raw & 0x0F; + return format!("OSL {layer}"); + } + + // --- Fixed special codes --- + match raw { + 0x3200 => return "Caps Word".into(), + 0x3300 => return "Repeat".into(), + 0x3400 => return "Leader".into(), + 0x3500 => return "Feed".into(), + 0x3600 => return "Play".into(), + 0x3700 => return "Sleep".into(), + 0x3800 => return "Meds".into(), + 0x3900 => return "GEsc".into(), + 0x3A00 => return "Layer Lock".into(), + 0x3C00 => return "AS Toggle".into(), + _ => {} + } + + // --- KO (Key Override) slots: 0x3D00..=0x3DFF --- + if raw >= 0x3D00 && raw <= 0x3DFF { + let slot = raw & 0xFF; + return format!("KO {slot}"); + } + + // --- LT (Layer-Tap): 0x4000..=0x4FFF --- + // layout: 0x4LKK where L = layer (0..F), KK = HID keycode + if raw >= 0x4000 && raw <= 0x4FFF { + let layer = (raw >> 8) & 0x0F; + let kc = (raw & 0xFF) as u8; + return format!("LT {} {}", layer, hid_key_name(kc)); + } + + // --- MT (Mod-Tap): 0x5000..=0x5FFF --- + // layout: 0x5MKK where M = mod nibble (4 bits), KK = HID keycode + if raw >= 0x5000 && raw <= 0x5FFF { + let mods = ((raw >> 8) & 0x0F) as u8; + let kc = (raw & 0xFF) as u8; + return format!("MT {} {}", mod_name(mods), hid_key_name(kc)); + } + + // --- TD (Tap Dance): 0x6000..=0x6FFF --- + if raw >= 0x6000 && raw <= 0x6FFF { + let index = (raw >> 8) & 0x0F; + return format!("TD {index}"); + } + + // --- Unknown --- + format!("0x{raw:04X}") +} + +/// Decode a modifier bitmask into a human-readable string. +/// +/// Bits: 0x01=Ctrl, 0x02=Shift, 0x04=Alt, 0x08=GUI, +/// 0x10=RCtrl, 0x20=RShift, 0x40=RAlt, 0x80=RGUI. +/// Multiple modifiers are joined with "+". +pub fn mod_name(mod_mask: u8) -> String { + let mut parts = Vec::new(); + if mod_mask & 0x01 != 0 { parts.push("Ctrl"); } + if mod_mask & 0x02 != 0 { parts.push("Shift"); } + if mod_mask & 0x04 != 0 { parts.push("Alt"); } + if mod_mask & 0x08 != 0 { parts.push("GUI"); } + if mod_mask & 0x10 != 0 { parts.push("RCtrl"); } + if mod_mask & 0x20 != 0 { parts.push("RShift"); } + if mod_mask & 0x40 != 0 { parts.push("RAlt"); } + if mod_mask & 0x80 != 0 { parts.push("RGUI"); } + if parts.is_empty() { + format!("0x{mod_mask:02X}") + } else { + parts.join("+") + } +} + +/// Map a single HID usage code (0x00..=0xE7) to a short readable name. +pub fn hid_key_name(code: u8) -> String { + match code { + // No key / transparent + 0x00 => "None", + // 0x01 = ErrorRollOver, 0x02 = POSTFail, 0x03 = ErrorUndefined (not user-facing) + 0x01 => "ErrRollOver", + 0x02 => "POSTFail", + 0x03 => "ErrUndef", + + // Letters + 0x04 => "A", + 0x05 => "B", + 0x06 => "C", + 0x07 => "D", + 0x08 => "E", + 0x09 => "F", + 0x0A => "G", + 0x0B => "H", + 0x0C => "I", + 0x0D => "J", + 0x0E => "K", + 0x0F => "L", + 0x10 => "M", + 0x11 => "N", + 0x12 => "O", + 0x13 => "P", + 0x14 => "Q", + 0x15 => "R", + 0x16 => "S", + 0x17 => "T", + 0x18 => "U", + 0x19 => "V", + 0x1A => "W", + 0x1B => "X", + 0x1C => "Y", + 0x1D => "Z", + + // Number row + 0x1E => "1", + 0x1F => "2", + 0x20 => "3", + 0x21 => "4", + 0x22 => "5", + 0x23 => "6", + 0x24 => "7", + 0x25 => "8", + 0x26 => "9", + 0x27 => "0", + + // Common control keys + 0x28 => "Enter", + 0x29 => "Esc", + 0x2A => "Backspace", + 0x2B => "Tab", + 0x2C => "Space", + + // Punctuation / symbols + 0x2D => "-", + 0x2E => "=", + 0x2F => "[", + 0x30 => "]", + 0x31 => "\\", + 0x32 => "Europe1", + 0x33 => ";", + 0x34 => "'", + 0x35 => "`", + 0x36 => ",", + 0x37 => ".", + 0x38 => "/", + + // Caps Lock + 0x39 => "Caps Lock", + + // Function keys + 0x3A => "F1", + 0x3B => "F2", + 0x3C => "F3", + 0x3D => "F4", + 0x3E => "F5", + 0x3F => "F6", + 0x40 => "F7", + 0x41 => "F8", + 0x42 => "F9", + 0x43 => "F10", + 0x44 => "F11", + 0x45 => "F12", + + // Navigation / editing cluster + 0x46 => "PrtSc", + 0x47 => "ScrLk", + 0x48 => "Pause", + 0x49 => "Ins", + 0x4A => "Home", + 0x4B => "PgUp", + 0x4C => "Del", + 0x4D => "End", + 0x4E => "PgDn", + + // Arrow keys + 0x4F => "Right", + 0x50 => "Left", + 0x51 => "Down", + 0x52 => "Up", + + // Keypad + 0x53 => "NumLk", + 0x54 => "Num /", + 0x55 => "Num *", + 0x56 => "Num -", + 0x57 => "Num +", + 0x58 => "Num Enter", + 0x59 => "Num 1", + 0x5A => "Num 2", + 0x5B => "Num 3", + 0x5C => "Num 4", + 0x5D => "Num 5", + 0x5E => "Num 6", + 0x5F => "Num 7", + 0x60 => "Num 8", + 0x61 => "Num 9", + 0x62 => "Num 0", + 0x63 => "Num .", + 0x64 => "Europe2", + 0x65 => "Menu", + 0x66 => "Power", + 0x67 => "Num =", + + // F13-F24 + 0x68 => "F13", + 0x69 => "F14", + 0x6A => "F15", + 0x6B => "F16", + 0x6C => "F17", + 0x6D => "F18", + 0x6E => "F19", + 0x6F => "F20", + 0x70 => "F21", + 0x71 => "F22", + 0x72 => "F23", + 0x73 => "F24", + + // Misc system keys + 0x74 => "Execute", + 0x75 => "Help", + 0x76 => "Menu2", + 0x77 => "Select", + 0x78 => "Stop", + 0x79 => "Again", + 0x7A => "Undo", + 0x7B => "Cut", + 0x7C => "Copy", + 0x7D => "Paste", + 0x7E => "Find", + 0x7F => "Mute", + 0x80 => "Vol Up", + 0x81 => "Vol Down", + + // Locking keys + 0x82 => "Lock Caps", + 0x83 => "Lock Num", + 0x84 => "Lock Scroll", + + // Keypad extras + 0x85 => "Num ,", + 0x86 => "Num =2", + + // International / Kanji + 0x87 => "Kanji1", + 0x88 => "Kanji2", + 0x89 => "Kanji3", + 0x8A => "Kanji4", + 0x8B => "Kanji5", + 0x8C => "Kanji6", + 0x8D => "Kanji7", + 0x8E => "Kanji8", + 0x8F => "Kanji9", + + // Language keys + 0x90 => "Lang1", + 0x91 => "Lang2", + 0x92 => "Lang3", + 0x93 => "Lang4", + 0x94 => "Lang5", + 0x95 => "Lang6", + 0x96 => "Lang7", + 0x97 => "Lang8", + 0x98 => "Lang9", + + // Rare system keys + 0x99 => "Alt Erase", + 0x9A => "SysReq", + 0x9B => "Cancel", + 0x9C => "Clear", + 0x9D => "Prior", + 0x9E => "Return", + 0x9F => "Separator", + 0xA0 => "Out", + 0xA1 => "Oper", + 0xA2 => "Clear Again", + 0xA3 => "CrSel", + 0xA4 => "ExSel", + + // 0xA5..=0xAF reserved / not defined in standard HID tables + + // Extended keypad + 0xB0 => "Num 00", + 0xB1 => "Num 000", + 0xB2 => "Thousands Sep", + 0xB3 => "Decimal Sep", + 0xB4 => "Currency", + 0xB5 => "Currency Sub", + 0xB6 => "Num (", + 0xB7 => "Num )", + 0xB8 => "Num {", + 0xB9 => "Num }", + 0xBA => "Num Tab", + 0xBB => "Num Bksp", + 0xBC => "Num A", + 0xBD => "Num B", + 0xBE => "Num C", + 0xBF => "Num D", + 0xC0 => "Num E", + 0xC1 => "Num F", + 0xC2 => "Num XOR", + 0xC3 => "Num ^", + 0xC4 => "Num %", + 0xC5 => "Num <", + 0xC6 => "Num >", + 0xC7 => "Num &", + 0xC8 => "Num &&", + 0xC9 => "Num |", + 0xCA => "Num ||", + 0xCB => "Num :", + 0xCC => "Num #", + 0xCD => "Num Space", + 0xCE => "Num @", + 0xCF => "Num !", + 0xD0 => "Num M Store", + 0xD1 => "Num M Recall", + 0xD2 => "Num M Clear", + 0xD3 => "Num M+", + 0xD4 => "Num M-", + 0xD5 => "Num M*", + 0xD6 => "Num M/", + 0xD7 => "Num +/-", + 0xD8 => "Num Clear", + 0xD9 => "Num ClrEntry", + 0xDA => "Num Binary", + 0xDB => "Num Octal", + 0xDC => "Num Decimal", + 0xDD => "Num Hex", + + // 0xDE..=0xDF reserved + + // Modifier keys + 0xE0 => "LCtrl", + 0xE1 => "LShift", + 0xE2 => "LAlt", + 0xE3 => "LGUI", + 0xE4 => "RCtrl", + 0xE5 => "RShift", + 0xE6 => "RAlt", + 0xE7 => "RGUI", + + // Anything else in 0x00..=0xFF not covered above + _ => return format!("0x{code:02X}"), + } + .into() +} diff --git a/original-src/layout.rs b/original-src/layout.rs new file mode 100644 index 0000000..6197a31 --- /dev/null +++ b/original-src/layout.rs @@ -0,0 +1,286 @@ +use serde_json::Value; + +/// A keycap with computed absolute position. +#[derive(Clone, Debug, PartialEq)] +pub struct KeycapPos { + pub row: usize, + pub col: usize, + pub x: f32, + pub y: f32, + pub w: f32, + pub h: f32, + pub angle: f32, // degrees +} + +const KEY_SIZE: f32 = 50.0; +const KEY_GAP: f32 = 4.0; + +/// Parse a layout JSON string into absolute key positions. +pub fn parse_json(json: &str) -> Result, String> { + let val: Value = serde_json::from_str(json) + .map_err(|e| format!("Invalid layout JSON: {}", e))?; + let mut keys = Vec::new(); + walk(&val, 0.0, 0.0, 0.0, &mut keys); + if keys.is_empty() { + return Err("No keys found in layout".into()); + } + Ok(keys) +} + +/// Default layout embedded at compile time. +pub fn default_layout() -> Vec { + let json = include_str!("../default.json"); + parse_json(json).unwrap_or_default() +} + +fn walk(node: &Value, ox: f32, oy: f32, parent_angle: f32, out: &mut Vec) { + let obj = match node.as_object() { + Some(o) => o, + None => return, + }; + + for (key, val) in obj { + let key_str = key.as_str(); + match key_str { + "Group" => walk_group(val, ox, oy, parent_angle, out), + "Line" => walk_line(val, ox, oy, parent_angle, out), + "Keycap" => walk_keycap(val, ox, oy, parent_angle, out), + _ => {} + } + } +} + +fn parse_margin(val: &Value) -> (f32, f32, f32, f32) { + let as_str = val.as_str(); + if let Some(s) = as_str { + let split = s.split(','); + let parts: Vec = split + .filter_map(|p| { + let trimmed = p.trim(); + let parsed = trimmed.parse().ok(); + parsed + }) + .collect(); + let has_four_parts = parts.len() == 4; + if has_four_parts { + return (parts[0], parts[1], parts[2], parts[3]); + } + } + (0.0, 0.0, 0.0, 0.0) +} + +fn parse_angle(val: &Value) -> f32 { + let rotate_transform = val.get("RotateTransform"); + let angle_val = rotate_transform + .and_then(|rt| rt.get("Angle")); + let angle_f64 = angle_val + .and_then(|a| a.as_f64()); + let angle = angle_f64.unwrap_or(0.0) as f32; + angle +} + +fn walk_group(val: &Value, ox: f32, oy: f32, parent_angle: f32, out: &mut Vec) { + let obj = match val.as_object() { + Some(o) => o, + None => return, + }; + + let margin_val = obj.get("Margin"); + let (ml, mt, _, _) = margin_val + .map(parse_margin) + .unwrap_or_default(); + let transform_val = obj.get("RenderTransform"); + let angle = transform_val + .map(parse_angle) + .unwrap_or(0.0); + + let gx = ox + ml; + let gy = oy + mt; + + let children_val = obj.get("Children"); + let children_array = children_val + .and_then(|c| c.as_array()); + if let Some(children) = children_array { + let combined_angle = parent_angle + angle; + for child in children { + walk(child, gx, gy, combined_angle, out); + } + } +} + +fn walk_line(val: &Value, ox: f32, oy: f32, parent_angle: f32, out: &mut Vec) { + let obj = match val.as_object() { + Some(o) => o, + None => return, + }; + + let margin_val = obj.get("Margin"); + let (ml, mt, _, _) = margin_val + .map(parse_margin) + .unwrap_or_default(); + let transform_val = obj.get("RenderTransform"); + let angle = transform_val + .map(parse_angle) + .unwrap_or(0.0); + let total_angle = parent_angle + angle; + + let orientation_val = obj.get("Orientation"); + let orientation_str = orientation_val + .and_then(|o| o.as_str()) + .unwrap_or("Vertical"); + let horiz = orientation_str == "Horizontal"; + + let lx = ox + ml; + let ly = oy + mt; + + let rad = total_angle.to_radians(); + let cos_a = rad.cos(); + let sin_a = rad.sin(); + + let mut cursor = 0.0f32; + + let children_val = obj.get("Children"); + let children_array = children_val + .and_then(|c| c.as_array()); + if let Some(children) = children_array { + for child in children { + let (cx, cy) = if horiz { + let x = lx + cursor * cos_a; + let y = ly + cursor * sin_a; + (x, y) + } else { + let x = lx - cursor * sin_a; + let y = ly + cursor * cos_a; + (x, y) + }; + + let child_size = measure(child, horiz); + walk(child, cx, cy, total_angle, out); + cursor += child_size; + } + } +} + +/// Measure a child's extent along the parent's main axis. +fn measure(node: &Value, horiz: bool) -> f32 { + let obj = match node.as_object() { + Some(o) => o, + None => return 0.0, + }; + + for (key, val) in obj { + let key_str = key.as_str(); + match key_str { + "Keycap" => { + let width_val = val.get("Width"); + let width_f64 = width_val + .and_then(|v| v.as_f64()); + let w = width_f64.unwrap_or(KEY_SIZE as f64) as f32; + let extent = if horiz { + w + KEY_GAP + } else { + KEY_SIZE + KEY_GAP + }; + return extent; + } + "Line" => { + let sub = match val.as_object() { + Some(o) => o, + None => return 0.0, + }; + let sub_orientation = sub.get("Orientation"); + let sub_orient_str = sub_orientation + .and_then(|o| o.as_str()) + .unwrap_or("Vertical"); + let sub_horiz = sub_orient_str == "Horizontal"; + + let sub_children_val = sub.get("Children"); + let sub_children_array = sub_children_val + .and_then(|c| c.as_array()); + let children = sub_children_array + .map(|a| a.as_slice()) + .unwrap_or(&[]); + + let same_direction = sub_horiz == horiz; + let content: f32 = if same_direction { + // Same direction: sum + children + .iter() + .map(|c| measure(c, sub_horiz)) + .sum() + } else { + // Cross direction: max + children + .iter() + .map(|c| measure(c, horiz)) + .fold(0.0f32, f32::max) + }; + + return content; + } + "Group" => { + let sub = match val.as_object() { + Some(o) => o, + None => return 0.0, + }; + let sub_children_val = sub.get("Children"); + let sub_children_array = sub_children_val + .and_then(|c| c.as_array()); + let children = sub_children_array + .map(|a| a.as_slice()) + .unwrap_or(&[]); + let max_extent = children + .iter() + .map(|c| measure(c, horiz)) + .fold(0.0f32, f32::max); + return max_extent; + } + _ => {} + } + } + 0.0 +} + +fn walk_keycap(val: &Value, ox: f32, oy: f32, parent_angle: f32, out: &mut Vec) { + let obj = match val.as_object() { + Some(o) => o, + None => return, + }; + + let col_val = obj.get("Column"); + let col_u64 = col_val + .and_then(|v| v.as_u64()); + let col = col_u64.unwrap_or(0) as usize; + + let row_val = obj.get("Row"); + let row_u64 = row_val + .and_then(|v| v.as_u64()); + let row = row_u64.unwrap_or(0) as usize; + + let width_val = obj.get("Width"); + let width_f64 = width_val + .and_then(|v| v.as_f64()); + let w = width_f64.unwrap_or(KEY_SIZE as f64) as f32; + + let margin_val = obj.get("Margin"); + let (ml, mt, _, _) = margin_val + .map(parse_margin) + .unwrap_or_default(); + + let transform_val = obj.get("RenderTransform"); + let angle = transform_val + .map(parse_angle) + .unwrap_or(0.0); + + let total_angle = parent_angle + angle; + + out.push(KeycapPos { + row, + col, + x: ox + ml, + y: oy + mt, + w, + h: KEY_SIZE, + angle: total_angle, + }); +} diff --git a/original-src/layout_remap.rs b/original-src/layout_remap.rs new file mode 100644 index 0000000..023f97d --- /dev/null +++ b/original-src/layout_remap.rs @@ -0,0 +1,339 @@ +/// Remaps HID key names to their visual representation based on the user's +/// keyboard layout (language). Ported from the C# `KeyConverter.LayoutOverrides`. + +// Display implementation for the keyboard layout picker in settings. +impl std::fmt::Display for KeyboardLayout { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.name()) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum KeyboardLayout { + Qwerty, + Azerty, + Qwertz, + Dvorak, + Colemak, + Bepo, + QwertyEs, + QwertyPt, + QwertyIt, + QwertyNordic, + QwertyBr, + QwertyTr, + QwertyUk, +} + +impl KeyboardLayout { + /// All known layout variants. + pub fn all() -> &'static [KeyboardLayout] { + &[ + KeyboardLayout::Qwerty, + KeyboardLayout::Azerty, + KeyboardLayout::Qwertz, + KeyboardLayout::Dvorak, + KeyboardLayout::Colemak, + KeyboardLayout::Bepo, + KeyboardLayout::QwertyEs, + KeyboardLayout::QwertyPt, + KeyboardLayout::QwertyIt, + KeyboardLayout::QwertyNordic, + KeyboardLayout::QwertyBr, + KeyboardLayout::QwertyTr, + KeyboardLayout::QwertyUk, + ] + } + + /// Human-readable display name. + pub fn name(&self) -> &str { + match self { + KeyboardLayout::Qwerty => "QWERTY", + KeyboardLayout::Azerty => "AZERTY", + KeyboardLayout::Qwertz => "QWERTZ", + KeyboardLayout::Dvorak => "DVORAK", + KeyboardLayout::Colemak => "COLEMAK", + KeyboardLayout::Bepo => "BEPO", + KeyboardLayout::QwertyEs => "QWERTY_ES", + KeyboardLayout::QwertyPt => "QWERTY_PT", + KeyboardLayout::QwertyIt => "QWERTY_IT", + KeyboardLayout::QwertyNordic => "QWERTY_NORDIC", + KeyboardLayout::QwertyBr => "QWERTY_BR", + KeyboardLayout::QwertyTr => "QWERTY_TR", + KeyboardLayout::QwertyUk => "QWERTY_UK", + } + } + + /// Parse a layout name (case-insensitive). Falls back to `Qwerty`. + pub fn from_name(s: &str) -> Self { + match s.to_ascii_uppercase().as_str() { + "QWERTY" => KeyboardLayout::Qwerty, + "AZERTY" => KeyboardLayout::Azerty, + "QWERTZ" => KeyboardLayout::Qwertz, + "DVORAK" => KeyboardLayout::Dvorak, + "COLEMAK" => KeyboardLayout::Colemak, + "BEPO" | "BÉPO" => KeyboardLayout::Bepo, + "QWERTY_ES" => KeyboardLayout::QwertyEs, + "QWERTY_PT" => KeyboardLayout::QwertyPt, + "QWERTY_IT" => KeyboardLayout::QwertyIt, + "QWERTY_NORDIC" => KeyboardLayout::QwertyNordic, + "QWERTY_BR" => KeyboardLayout::QwertyBr, + "QWERTY_TR" => KeyboardLayout::QwertyTr, + "QWERTY_UK" => KeyboardLayout::QwertyUk, + _ => KeyboardLayout::Qwerty, + } + } +} + +/// Given a `layout` and an HID key name (e.g. `"A"`, `"COMMA"`, `"SEMICOLON"`), +/// returns the visual label for that key on the given layout, or `None` when no +/// override exists (meaning the default / QWERTY label applies). +/// +/// The lookup is **case-insensitive** on `hid_name`. +pub fn remap_key_label(layout: &KeyboardLayout, hid_name: &str) -> Option<&'static str> { + // Normalise to uppercase for matching. + let key = hid_name.to_ascii_uppercase(); + let key = key.as_str(); + + match layout { + // QWERTY has no overrides — it *is* the reference layout. + KeyboardLayout::Qwerty => None, + + KeyboardLayout::Azerty => match key { + "COMMA" | "COMM" | "COMA" => Some(";"), + "SEMICOLON" | "SCOLON" | "SCLN" => Some(","), + "PERIOD" | "DOT" => Some(":"), + "SLASH" | "SLSH" | "/" => Some("!"), + "M" => Some(","), + "W" => Some("Z"), + "Z" => Some("W"), + "Q" => Some("A"), + "A" => Some("Q"), + "," => Some(";"), + "." => Some(":"), + ";" => Some("M"), + "-" | "MINUS" | "MIN" => Some(")"), + "BRACKET_LEFT" | "LBRCKT" | "LBRC" | "[" => Some("^"), + "BRACKET_RIGHT" | "RBRCKT" | "RBRC" | "]" => Some("$"), + "BACKSLASH" | "BSLSH" | "\\" => Some("<"), + "APOSTROPHE" | "APO" | "QUOT" | "'" => Some("\u{00f9}"), // ù + "1" => Some("& 1"), + "2" => Some("\u{00e9} 2 ~"), // é 2 ~ + "3" => Some("\" 3 #"), + "4" => Some("' 4 }"), + "5" => Some("( 5 ["), + "6" => Some("- 6 |"), + "7" => Some("\u{00e8} 7 `"), // è 7 ` + "8" => Some("_ 8 \\"), + "9" => Some("\u{00e7} 9 ^"), // ç 9 ^ + "0" => Some("\u{00e0} 0 @"), // à 0 @ + _ => None, + }, + + KeyboardLayout::Qwertz => match key { + "Y" => Some("Z"), + "Z" => Some("Y"), + "MINUS" | "MIN" => Some("\u{00df}"), // ß + "EQUAL" | "EQL" => Some("'"), + "BRACKET_LEFT" | "LBRCKT" | "LBRC" => Some("\u{00fc}"), // ü + "BRACKET_RIGHT" | "RBRCKT" | "RBRC" => Some("+"), + "SEMICOLON" | "SCOLON" | "SCLN" => Some("\u{00f6}"), // ö + "APOSTROPHE" | "APO" | "QUOT" => Some("\u{00e4}"), // ä + "GRAVE" | "GRV" => Some("^"), + "SLASH" | "SLSH" => Some("-"), + "BACKSLASH" | "BSLSH" => Some("#"), + _ => None, + }, + + KeyboardLayout::Dvorak => match key { + "Q" => Some("'"), + "W" => Some(","), + "E" => Some("."), + "R" => Some("P"), + "T" => Some("Y"), + "Y" => Some("F"), + "U" => Some("G"), + "I" => Some("C"), + "O" => Some("R"), + "P" => Some("L"), + "A" => Some("A"), + "S" => Some("O"), + "D" => Some("E"), + "F" => Some("U"), + "G" => Some("I"), + "H" => Some("D"), + "J" => Some("H"), + "K" => Some("T"), + "L" => Some("N"), + "SEMICOLON" | "SCOLON" | "SCLN" => Some("S"), + "APOSTROPHE" | "APO" | "QUOT" => Some("-"), + "Z" => Some(";"), + "X" => Some("Q"), + "C" => Some("J"), + "V" => Some("K"), + "B" => Some("X"), + "N" => Some("B"), + "M" => Some("M"), + "COMMA" | "COMM" | "COMA" => Some("W"), + "PERIOD" | "DOT" => Some("V"), + "SLASH" | "SLSH" => Some("Z"), + "MINUS" | "MIN" => Some("["), + "EQUAL" | "EQL" => Some("]"), + "BRACKET_LEFT" | "LBRCKT" | "LBRC" => Some("/"), + "BRACKET_RIGHT" | "RBRCKT" | "RBRC" => Some("="), + _ => None, + }, + + KeyboardLayout::Colemak => match key { + "E" => Some("F"), + "R" => Some("P"), + "T" => Some("G"), + "Y" => Some("J"), + "U" => Some("L"), + "I" => Some("U"), + "O" => Some("Y"), + "P" => Some(";"), + "S" => Some("R"), + "D" => Some("S"), + "F" => Some("T"), + "G" => Some("D"), + "J" => Some("N"), + "K" => Some("E"), + "L" => Some("I"), + "SEMICOLON" | "SCOLON" | "SCLN" => Some("O"), + "N" => Some("K"), + _ => None, + }, + + KeyboardLayout::Bepo => match key { + "Q" => Some("B"), + "W" => Some("E"), + "E" => Some("P"), + "R" => Some("O"), + "T" => Some("E"), + "Y" => Some("^"), + "U" => Some("V"), + "I" => Some("D"), + "O" => Some("L"), + "P" => Some("J"), + "A" => Some("A"), + "S" => Some("U"), + "D" => Some("I"), + "F" => Some("E"), + "G" => Some(","), + "H" => Some("C"), + "J" => Some("T"), + "K" => Some("S"), + "L" => Some("R"), + "SEMICOLON" | "SCOLON" | "SCLN" => Some("N"), + "Z" => Some("A"), + "X" => Some("Y"), + "C" => Some("X"), + "V" => Some("."), + "B" => Some("K"), + "N" => Some("'"), + "M" => Some("Q"), + "COMMA" | "COMM" | "COMA" => Some("G"), + "PERIOD" | "DOT" => Some("H"), + "SLASH" | "SLSH" => Some("F"), + "1" => Some("\""), + "2" => Some("<"), + "3" => Some(">"), + "4" => Some("("), + "5" => Some(")"), + "6" => Some("@"), + "7" => Some("+"), + "8" => Some("-"), + "9" => Some("/"), + "0" => Some("*"), + _ => None, + }, + + KeyboardLayout::QwertyEs => match key { + "MINUS" | "MIN" => Some("'"), + "EQUAL" | "EQL" => Some("\u{00a1}"), // ¡ + "BRACKET_LEFT" | "LBRCKT" | "LBRC" => Some("`"), + "BRACKET_RIGHT" | "RBRCKT" | "RBRC" => Some("+"), + "SEMICOLON" | "SCOLON" | "SCLN" => Some("\u{00f1}"), // ñ + "APOSTROPHE" | "APO" | "QUOT" => Some("'"), + "GRAVE" | "GRV" => Some("\u{00ba}"), // º + "SLASH" | "SLSH" => Some("-"), + "BACKSLASH" | "BSLSH" => Some("\u{00e7}"), // ç + _ => None, + }, + + KeyboardLayout::QwertyPt => match key { + "MINUS" | "MIN" => Some("'"), + "EQUAL" | "EQL" => Some("\u{00ab}"), // « + "BRACKET_LEFT" | "LBRCKT" | "LBRC" => Some("+"), + "BRACKET_RIGHT" | "RBRCKT" | "RBRC" => Some("'"), + "SEMICOLON" | "SCOLON" | "SCLN" => Some("\u{00e7}"), // ç + "APOSTROPHE" | "APO" | "QUOT" => Some("\u{00ba}"), // º + "GRAVE" | "GRV" => Some("\\"), + "SLASH" | "SLSH" => Some("-"), + "BACKSLASH" | "BSLSH" => Some("~"), + _ => None, + }, + + KeyboardLayout::QwertyIt => match key { + "MINUS" | "MIN" => Some("'"), + "EQUAL" | "EQL" => Some("\u{00ec}"), // ì + "BRACKET_LEFT" | "LBRCKT" | "LBRC" => Some("\u{00e8}"), // è + "BRACKET_RIGHT" | "RBRCKT" | "RBRC" => Some("+"), + "SEMICOLON" | "SCOLON" | "SCLN" => Some("\u{00f2}"), // ò + "APOSTROPHE" | "APO" | "QUOT" => Some("\u{00e0}"), // à + "GRAVE" | "GRV" => Some("\\"), + "SLASH" | "SLSH" => Some("-"), + "BACKSLASH" | "BSLSH" => Some("\u{00f9}"), // ù + _ => None, + }, + + KeyboardLayout::QwertyNordic => match key { + "MINUS" | "MIN" => Some("+"), + "EQUAL" | "EQL" => Some("'"), + "BRACKET_LEFT" | "LBRCKT" | "LBRC" => Some("\u{00e5}"), // å + "BRACKET_RIGHT" | "RBRCKT" | "RBRC" => Some("\u{00a8}"), // ¨ + "SEMICOLON" | "SCOLON" | "SCLN" => Some("\u{00f6}"), // ö + "APOSTROPHE" | "APO" | "QUOT" => Some("\u{00e4}"), // ä + "GRAVE" | "GRV" => Some("\u{00a7}"), // § + "SLASH" | "SLSH" => Some("-"), + "BACKSLASH" | "BSLSH" => Some("'"), + _ => None, + }, + + KeyboardLayout::QwertyBr => match key { + "MINUS" | "MIN" => Some("-"), + "EQUAL" | "EQL" => Some("="), + "BRACKET_LEFT" | "LBRCKT" | "LBRC" => Some("'"), + "BRACKET_RIGHT" | "RBRCKT" | "RBRC" => Some("["), + "SEMICOLON" | "SCOLON" | "SCLN" => Some("\u{00e7}"), // ç + "APOSTROPHE" | "APO" | "QUOT" => Some("~"), + "GRAVE" | "GRV" => Some("'"), + "SLASH" | "SLSH" => Some(";"), + "BACKSLASH" | "BSLSH" => Some("]"), + _ => None, + }, + + KeyboardLayout::QwertyTr => match key { + "MINUS" | "MIN" => Some("*"), + "EQUAL" | "EQL" => Some("-"), + "BRACKET_LEFT" | "LBRCKT" | "LBRC" => Some("\u{011f}"), // ğ + "BRACKET_RIGHT" | "RBRCKT" | "RBRC" => Some("\u{00fc}"), // ü + "SEMICOLON" | "SCOLON" | "SCLN" => Some("\u{015f}"), // ş + "APOSTROPHE" | "APO" | "QUOT" => Some("i"), + "GRAVE" | "GRV" => Some("\""), + "SLASH" | "SLSH" => Some("."), + "BACKSLASH" | "BSLSH" => Some(","), + _ => None, + }, + + KeyboardLayout::QwertyUk => match key { + "GRAVE" | "GRV" => Some("`"), + "MINUS" | "MIN" => Some("-"), + "EQUAL" | "EQL" => Some("="), + "BACKSLASH" | "BSLSH" => Some("#"), + "APOSTROPHE" | "APO" | "QUOT" => Some("'"), + _ => None, + }, + } +} diff --git a/original-src/parsers.rs b/original-src/parsers.rs new file mode 100644 index 0000000..6fed54a --- /dev/null +++ b/original-src/parsers.rs @@ -0,0 +1,900 @@ +/// Parsing functions for firmware text and binary responses. +/// Separated for testability. + +/// Keyboard physical dimensions (must match firmware). +pub const ROWS: usize = 5; +pub const COLS: usize = 13; + +/// Parse "TD0: 04,05,06,29" lines into an array of 8 tap dance slots. +/// Each slot has 4 actions: [1-tap, 2-tap, 3-tap, hold]. +pub fn parse_td_lines(lines: &[String]) -> Vec<[u16; 4]> { + let mut result = vec![[0u16; 4]; 8]; + + for line in lines { + // Only process lines starting with "TD" + let starts_with_td = line.starts_with("TD"); + if !starts_with_td { + continue; + } + + // Find the colon separator: "TD0: ..." + let colon = match line.find(':') { + Some(i) => i, + None => continue, + }; + + // Extract the index between "TD" and ":" + let index_str = &line[2..colon]; + let idx: usize = match index_str.parse() { + Ok(i) if i < 8 => i, + _ => continue, + }; + + // Parse the comma-separated hex values after the colon + let after_colon = &line[colon + 1..]; + let trimmed_values = after_colon.trim(); + let split_parts = trimmed_values.split(','); + let vals: Vec = split_parts + .filter_map(|s| { + let trimmed_part = s.trim(); + u16::from_str_radix(trimmed_part, 16).ok() + }) + .collect(); + + // We need exactly 4 values + let has_four_values = vals.len() == 4; + if has_four_values { + result[idx] = [vals[0], vals[1], vals[2], vals[3]]; + } + } + + result +} + +/// Parse KO (Key Override) lines into arrays of [trigger, mod, result, res_mod]. +/// Format: "KO0: trigger=2A mod=02 -> result=4C resmod=00" +pub fn parse_ko_lines(lines: &[String]) -> Vec<[u8; 4]> { + let mut result = Vec::new(); + + for line in lines { + // Only process lines starting with "KO" + let starts_with_ko = line.starts_with("KO"); + if !starts_with_ko { + continue; + } + + // Helper: extract hex value after a keyword like "trigger=" + let parse_hex = |key: &str| -> u8 { + let key_position = line.find(key); + + let after_key = match key_position { + Some(i) => { + let rest = &line[i + key.len()..]; + let first_token = rest.split_whitespace().next(); + first_token + } + None => None, + }; + + let parsed_value = match after_key { + Some(s) => { + let without_prefix = s.trim_start_matches("0x"); + u8::from_str_radix(without_prefix, 16).ok() + } + None => None, + }; + + parsed_value.unwrap_or(0) + }; + + let trigger = parse_hex("trigger="); + let modifier = parse_hex("mod="); + let result_key = parse_hex("result="); + let result_mod = parse_hex("resmod="); + + result.push([trigger, modifier, result_key, result_mod]); + } + + result +} + +/// Parse heatmap lines (KEYSTATS? response). +/// Format: "R0: 100 50 30 20 10 5 0 15 25 35 45 55 65" +/// Returns (data[5][13], max_value). +pub fn parse_heatmap_lines(lines: &[String]) -> (Vec>, u32) { + let mut data: Vec> = vec![vec![0u32; COLS]; ROWS]; + let mut max = 0u32; + + for line in lines { + let trimmed = line.trim(); + + // Only process lines starting with "R" + if !trimmed.starts_with('R') { + continue; + } + + // Find the colon + let colon = match trimmed.find(':') { + Some(i) => i, + None => continue, + }; + + // Extract row number between "R" and ":" + let row_str = &trimmed[1..colon]; + let row: usize = match row_str.parse() { + Ok(r) if r < ROWS => r, + _ => continue, + }; + + // Parse space-separated values after the colon + let values_str = &trimmed[colon + 1..]; + for (col, token) in values_str.split_whitespace().enumerate() { + if col >= COLS { + break; + } + let count: u32 = token.parse().unwrap_or(0); + data[row][col] = count; + if count > max { + max = count; + } + } + } + + (data, max) +} + +/// Parsed combo: [index, row1, col1, row2, col2, result_keycode] +#[derive(Clone)] +pub struct ComboEntry { + pub index: u8, + pub r1: u8, + pub c1: u8, + pub r2: u8, + pub c2: u8, + pub result: u16, +} + +/// Parse "COMBO0: r3c3+r3c4=29" lines. +pub fn parse_combo_lines(lines: &[String]) -> Vec { + let mut result = Vec::new(); + + for line in lines { + let starts_with_combo = line.starts_with("COMBO"); + if !starts_with_combo { + continue; + } + + // Find colon: "COMBO0: ..." + let colon = match line.find(':') { + Some(i) => i, + None => continue, + }; + + // Index between "COMBO" and ":" + let index_str = &line[5..colon]; + let index: u8 = match index_str.parse() { + Ok(i) => i, + _ => continue, + }; + + // After colon: "r3c3+r3c4=29" + let rest = line[colon + 1..].trim(); + + // Split by "=" + let eq_parts: Vec<&str> = rest.split('=').collect(); + let has_two_parts = eq_parts.len() == 2; + if !has_two_parts { + continue; + } + + // Split left side by "+" + let key_parts: Vec<&str> = eq_parts[0].split('+').collect(); + let has_two_keys = key_parts.len() == 2; + if !has_two_keys { + continue; + } + + // Parse "r3c4" format + let pos1 = parse_rc(key_parts[0].trim()); + let pos2 = parse_rc(key_parts[1].trim()); + + let (r1, c1) = match pos1 { + Some(rc) => rc, + None => continue, + }; + let (r2, c2) = match pos2 { + Some(rc) => rc, + None => continue, + }; + + // Parse result keycode (hex) + let result_str = eq_parts[1].trim(); + let result_code: u16 = match u16::from_str_radix(result_str, 16) { + Ok(v) => v, + _ => continue, + }; + + result.push(ComboEntry { + index, + r1, + c1, + r2, + c2, + result: result_code, + }); + } + + result +} + +/// Parse "r3c4" into (row, col). +fn parse_rc(s: &str) -> Option<(u8, u8)> { + let lower = s.to_lowercase(); + + let r_pos = lower.find('r'); + let c_pos = lower.find('c'); + + let r_idx = match r_pos { + Some(i) => i, + None => return None, + }; + let c_idx = match c_pos { + Some(i) => i, + None => return None, + }; + + let row_str = &lower[r_idx + 1..c_idx]; + let col_str = &lower[c_idx + 1..]; + + let row: u8 = match row_str.parse() { + Ok(v) => v, + _ => return None, + }; + let col: u8 = match col_str.parse() { + Ok(v) => v, + _ => return None, + }; + + Some((row, col)) +} + +/// Parsed leader sequence entry. +#[derive(Clone)] +pub struct LeaderEntry { + pub index: u8, + pub sequence: Vec, // HID keycodes + pub result: u8, + pub result_mod: u8, +} + +/// Parse "LEADER0: 04,->29+00" lines. +pub fn parse_leader_lines(lines: &[String]) -> Vec { + let mut result = Vec::new(); + + for line in lines { + let starts_with_leader = line.starts_with("LEADER"); + if !starts_with_leader { + continue; + } + + let colon = match line.find(':') { + Some(i) => i, + None => continue, + }; + + let index_str = &line[6..colon]; + let index: u8 = match index_str.parse() { + Ok(i) => i, + _ => continue, + }; + + // After colon: "04,->29+00" + let rest = line[colon + 1..].trim(); + + // Split by "->" + let arrow_parts: Vec<&str> = rest.split("->").collect(); + let has_two_parts = arrow_parts.len() == 2; + if !has_two_parts { + continue; + } + + // Sequence: comma-separated hex keycodes (trailing comma OK) + let seq_str = arrow_parts[0].trim().trim_end_matches(','); + let sequence: Vec = seq_str + .split(',') + .filter_map(|s| { + let trimmed = s.trim(); + u8::from_str_radix(trimmed, 16).ok() + }) + .collect(); + + // Result: "29+00" = keycode + modifier + let result_parts: Vec<&str> = arrow_parts[1].trim().split('+').collect(); + let has_result = result_parts.len() == 2; + if !has_result { + continue; + } + + let result_key = match u8::from_str_radix(result_parts[0].trim(), 16) { + Ok(v) => v, + _ => continue, + }; + let result_mod = match u8::from_str_radix(result_parts[1].trim(), 16) { + Ok(v) => v, + _ => continue, + }; + + result.push(LeaderEntry { + index, + sequence, + result: result_key, + result_mod, + }); + } + + result +} + +/// A single macro step: keycode + modifier, or delay. +#[derive(Clone)] +pub struct MacroStep { + pub keycode: u8, + pub modifier: u8, +} + +impl MacroStep { + /// Returns true if this step is a delay (keycode 0xFF). + pub fn is_delay(&self) -> bool { + self.keycode == 0xFF + } + + /// Delay in milliseconds (modifier * 10). + pub fn delay_ms(&self) -> u32 { + self.modifier as u32 * 10 + } +} + +/// A parsed macro entry. +#[derive(Clone)] +pub struct MacroEntry { + pub slot: u8, + pub name: String, + pub steps: Vec, +} + +/// Parse MACROS? text response. +/// Lines can be like: +/// "MACRO 0: CopyPaste [06:01,FF:0A,19:01]" +/// "M0: name=CopyPaste steps=06:01,FF:0A,19:01" +/// or just raw text lines +pub fn parse_macro_lines(lines: &[String]) -> Vec { + let mut result = Vec::new(); + + for line in lines { + // Try format: "MACRO 0: name [steps]" or "M0: ..." + let trimmed = line.trim(); + + // Skip empty or header lines + let is_empty = trimmed.is_empty(); + if is_empty { + continue; + } + + // Try to find slot number + let has_macro_prefix = trimmed.starts_with("MACRO") || trimmed.starts_with("M"); + if !has_macro_prefix { + continue; + } + + let colon = match trimmed.find(':') { + Some(i) => i, + None => continue, + }; + + // Extract slot number from prefix + let prefix_end = trimmed[..colon].trim(); + let digits_start = prefix_end + .find(|c: char| c.is_ascii_digit()) + .unwrap_or(prefix_end.len()); + let slot_str = &prefix_end[digits_start..]; + let slot: u8 = match slot_str.trim().parse() { + Ok(s) => s, + _ => continue, + }; + + let after_colon = trimmed[colon + 1..].trim(); + + // Try to parse name and steps from brackets: "CopyPaste [06:01,FF:0A,19:01]" + let bracket_start = after_colon.find('['); + let bracket_end = after_colon.find(']'); + + let (name, steps_str) = match (bracket_start, bracket_end) { + (Some(bs), Some(be)) => { + let name_part = after_colon[..bs].trim().to_string(); + let steps_part = &after_colon[bs + 1..be]; + (name_part, steps_part.to_string()) + } + _ => { + // Try "name=X steps=Y" format + let name_eq = after_colon.find("name="); + let steps_eq = after_colon.find("steps="); + match (name_eq, steps_eq) { + (Some(ni), Some(si)) => { + let name_start = ni + 5; + let name_end = si; + let n = after_colon[name_start..name_end].trim().to_string(); + let s = after_colon[si + 6..].trim().to_string(); + (n, s) + } + _ => { + // Just use the whole thing as name, no steps + (after_colon.to_string(), String::new()) + } + } + } + }; + + // Parse steps: "06:01,FF:0A,19:01" + let mut steps = Vec::new(); + let has_steps = !steps_str.is_empty(); + if has_steps { + let step_parts = steps_str.split(','); + for part in step_parts { + let step_trimmed = part.trim(); + let kv: Vec<&str> = step_trimmed.split(':').collect(); + let has_two = kv.len() == 2; + if !has_two { + continue; + } + let key_byte = u8::from_str_radix(kv[0].trim(), 16).unwrap_or(0); + let mod_byte = u8::from_str_radix(kv[1].trim(), 16).unwrap_or(0); + steps.push(MacroStep { + keycode: key_byte, + modifier: mod_byte, + }); + } + } + + result.push(MacroEntry { + slot, + name, + steps, + }); + } + + result +} + +// --------------------------------------------------------------------------- +// Binary payload parsers (protocol v2) +// --------------------------------------------------------------------------- +// These functions parse the *payload* bytes extracted from a KR response frame. +// They produce the same data types as the text parsers above. + +/// Parse TD_LIST (0x51) binary payload. +/// Format: [count:u8] then count entries of [idx:u8][a1:u8][a2:u8][a3:u8][a4:u8]. +/// Returns Vec<[u16; 4]> with 8 slots (same shape as parse_td_lines). +pub fn parse_td_binary(payload: &[u8]) -> Vec<[u16; 4]> { + let mut result = vec![[0u16; 4]; 8]; + + // Need at least 1 byte for count + let has_count = !payload.is_empty(); + if !has_count { + return result; + } + + let count = payload[0] as usize; + let entry_size = 5; // idx(1) + actions(4) + let mut offset = 1; + + for _ in 0..count { + // Bounds check: need 5 bytes for this entry + let remaining = payload.len().saturating_sub(offset); + let enough_bytes = remaining >= entry_size; + if !enough_bytes { + break; + } + + let idx = payload[offset] as usize; + let action1 = payload[offset + 1] as u16; + let action2 = payload[offset + 2] as u16; + let action3 = payload[offset + 3] as u16; + let action4 = payload[offset + 4] as u16; + + let valid_index = idx < 8; + if valid_index { + result[idx] = [action1, action2, action3, action4]; + } + + offset += entry_size; + } + + result +} + +/// Parse COMBO_LIST (0x61) binary payload. +/// Format: [count:u8] then count entries of [idx:u8][r1:u8][c1:u8][r2:u8][c2:u8][result:u8]. +pub fn parse_combo_binary(payload: &[u8]) -> Vec { + let mut result = Vec::new(); + + let has_count = !payload.is_empty(); + if !has_count { + return result; + } + + let count = payload[0] as usize; + let entry_size = 6; // idx + r1 + c1 + r2 + c2 + result + let mut offset = 1; + + for _ in 0..count { + let remaining = payload.len().saturating_sub(offset); + let enough_bytes = remaining >= entry_size; + if !enough_bytes { + break; + } + + let index = payload[offset]; + let r1 = payload[offset + 1]; + let c1 = payload[offset + 2]; + let r2 = payload[offset + 3]; + let c2 = payload[offset + 4]; + let result_code = payload[offset + 5] as u16; + + result.push(ComboEntry { + index, + r1, + c1, + r2, + c2, + result: result_code, + }); + + offset += entry_size; + } + + result +} + +/// Parse LEADER_LIST (0x71) binary payload. +/// Format: [count:u8] then per entry: [idx:u8][seq_len:u8][seq: seq_len bytes][result:u8][result_mod:u8]. +pub fn parse_leader_binary(payload: &[u8]) -> Vec { + let mut result = Vec::new(); + + let has_count = !payload.is_empty(); + if !has_count { + return result; + } + + let count = payload[0] as usize; + let mut offset = 1; + + for _ in 0..count { + // Need at least idx(1) + seq_len(1) + let remaining = payload.len().saturating_sub(offset); + let enough_for_header = remaining >= 2; + if !enough_for_header { + break; + } + + let index = payload[offset]; + let seq_len = payload[offset + 1] as usize; + offset += 2; + + // Need seq_len bytes for sequence + 2 bytes for result+result_mod + let remaining_after_header = payload.len().saturating_sub(offset); + let enough_for_body = remaining_after_header >= seq_len + 2; + if !enough_for_body { + break; + } + + let sequence = payload[offset..offset + seq_len].to_vec(); + offset += seq_len; + + let result_key = payload[offset]; + let result_mod = payload[offset + 1]; + offset += 2; + + result.push(LeaderEntry { + index, + sequence, + result: result_key, + result_mod, + }); + } + + result +} + +/// Parse KO_LIST (0x92) binary payload. +/// Format: [count:u8] then count entries of [idx:u8][trigger_key:u8][trigger_mod:u8][result_key:u8][result_mod:u8]. +/// Returns Vec<[u8; 4]> = [trigger_key, trigger_mod, result_key, result_mod] (same as parse_ko_lines). +pub fn parse_ko_binary(payload: &[u8]) -> Vec<[u8; 4]> { + let mut result = Vec::new(); + + let has_count = !payload.is_empty(); + if !has_count { + return result; + } + + let count = payload[0] as usize; + let entry_size = 5; // idx + trigger_key + trigger_mod + result_key + result_mod + let mut offset = 1; + + for _ in 0..count { + let remaining = payload.len().saturating_sub(offset); + let enough_bytes = remaining >= entry_size; + if !enough_bytes { + break; + } + + // idx is payload[offset], but we skip it (not stored in output) + let trigger_key = payload[offset + 1]; + let trigger_mod = payload[offset + 2]; + let result_key = payload[offset + 3]; + let result_mod = payload[offset + 4]; + + result.push([trigger_key, trigger_mod, result_key, result_mod]); + + offset += entry_size; + } + + result +} + +/// Parse BT_QUERY (0x80) binary payload. +/// Format: [active_slot:u8][initialized:u8][connected:u8][pairing:u8] +/// then 3 slot entries: [slot_idx:u8][valid:u8][addr:6 bytes][name_len:u8][name: name_len bytes] +/// Returns Vec of text lines compatible with the UI (same shape as legacy text parsing). +pub fn parse_bt_binary(payload: &[u8]) -> Vec { + let mut lines = Vec::new(); + + // Need at least 4 bytes for the global state header + let enough_for_header = payload.len() >= 4; + if !enough_for_header { + return lines; + } + + let active_slot = payload[0]; + let initialized = payload[1]; + let connected = payload[2]; + let pairing = payload[3]; + + let status_line = format!( + "BT: slot={} init={} conn={} pairing={}", + active_slot, initialized, connected, pairing + ); + lines.push(status_line); + + let mut offset = 4; + let slot_count = 3; + + for _ in 0..slot_count { + // Each slot: slot_idx(1) + valid(1) + addr(6) + name_len(1) = 9 bytes minimum + let remaining = payload.len().saturating_sub(offset); + let enough_for_slot_header = remaining >= 9; + if !enough_for_slot_header { + break; + } + + let slot_idx = payload[offset]; + let valid = payload[offset + 1]; + let addr_bytes = &payload[offset + 2..offset + 8]; + let name_len = payload[offset + 8] as usize; + offset += 9; + + // Read the name string + let remaining_for_name = payload.len().saturating_sub(offset); + let enough_for_name = remaining_for_name >= name_len; + if !enough_for_name { + break; + } + + let name_bytes = &payload[offset..offset + name_len]; + let name = String::from_utf8_lossy(name_bytes).to_string(); + offset += name_len; + + // Format address as "XX:XX:XX:XX:XX:XX" + let addr_str = format!( + "{:02X}:{:02X}:{:02X}:{:02X}:{:02X}:{:02X}", + addr_bytes[0], addr_bytes[1], addr_bytes[2], + addr_bytes[3], addr_bytes[4], addr_bytes[5] + ); + + let slot_line = format!( + "BT slot {}: valid={} addr={} name={}", + slot_idx, valid, addr_str, name + ); + lines.push(slot_line); + } + + lines +} + +/// Parse TAMA_QUERY (0xA0) binary payload (22 bytes fixed). +/// Format: [enabled:u8][state:u8][hunger:u16 LE][happiness:u16 LE][energy:u16 LE] +/// [health:u16 LE][level:u16 LE][xp:u16 LE][total_keys:u32 LE][max_kpm:u32 LE] +/// Returns Vec with one summary line. +pub fn parse_tama_binary(payload: &[u8]) -> Vec { + let expected_size = 22; + let enough_bytes = payload.len() >= expected_size; + if !enough_bytes { + return vec!["TAMA: invalid payload".to_string()]; + } + + let enabled = payload[0]; + let _state = payload[1]; + + let hunger = u16::from_le_bytes([payload[2], payload[3]]); + let happiness = u16::from_le_bytes([payload[4], payload[5]]); + let energy = u16::from_le_bytes([payload[6], payload[7]]); + let health = u16::from_le_bytes([payload[8], payload[9]]); + let level = u16::from_le_bytes([payload[10], payload[11]]); + let _xp = u16::from_le_bytes([payload[12], payload[13]]); + let total_keys = u32::from_le_bytes([payload[14], payload[15], payload[16], payload[17]]); + let _max_kpm = u32::from_le_bytes([payload[18], payload[19], payload[20], payload[21]]); + + let line = format!( + "TAMA: Lv{} hunger={} happy={} energy={} health={} keys={} enabled={}", + level, hunger, happiness, energy, health, total_keys, enabled + ); + + vec![line] +} + +/// Parse WPM_QUERY (0x93) binary payload (2 bytes fixed). +/// Format: [wpm:u16 LE] +pub fn parse_wpm_binary(payload: &[u8]) -> String { + let enough_bytes = payload.len() >= 2; + if !enough_bytes { + return "WPM: 0".to_string(); + } + + let wpm = u16::from_le_bytes([payload[0], payload[1]]); + + format!("WPM: {}", wpm) +} + +/// Parse LIST_MACROS (0x30) binary payload. +/// Format: [count:u8] then per entry: +/// [idx:u8][keycode:u16 LE][name_len:u8][name: name_len bytes] +/// [keys_len:u8][keys: keys_len bytes][step_count:u8][{kc:u8,mod:u8}... step_count*2 bytes] +pub fn parse_macros_binary(payload: &[u8]) -> Vec { + let mut result = Vec::new(); + + let has_count = !payload.is_empty(); + if !has_count { + return result; + } + + let count = payload[0] as usize; + let mut offset = 1; + + for _ in 0..count { + // Need at least: idx(1) + keycode(2) + name_len(1) = 4 + let remaining = payload.len().saturating_sub(offset); + let enough_for_prefix = remaining >= 4; + if !enough_for_prefix { + break; + } + + let slot = payload[offset]; + // keycode is stored but not used in MacroEntry (it's the trigger keycode) + let _keycode = u16::from_le_bytes([payload[offset + 1], payload[offset + 2]]); + let name_len = payload[offset + 3] as usize; + offset += 4; + + // Read name + let remaining_for_name = payload.len().saturating_sub(offset); + let enough_for_name = remaining_for_name >= name_len; + if !enough_for_name { + break; + } + + let name_bytes = &payload[offset..offset + name_len]; + let name = String::from_utf8_lossy(name_bytes).to_string(); + offset += name_len; + + // Read keys_len + keys (raw key bytes, skipped for MacroEntry) + let remaining_for_keys_len = payload.len().saturating_sub(offset); + let enough_for_keys_len = remaining_for_keys_len >= 1; + if !enough_for_keys_len { + break; + } + + let keys_len = payload[offset] as usize; + offset += 1; + + let remaining_for_keys = payload.len().saturating_sub(offset); + let enough_for_keys = remaining_for_keys >= keys_len; + if !enough_for_keys { + break; + } + + // Skip the raw keys bytes + offset += keys_len; + + // Read step_count + steps + let remaining_for_step_count = payload.len().saturating_sub(offset); + let enough_for_step_count = remaining_for_step_count >= 1; + if !enough_for_step_count { + break; + } + + let step_count = payload[offset] as usize; + offset += 1; + + let steps_byte_size = step_count * 2; + let remaining_for_steps = payload.len().saturating_sub(offset); + let enough_for_steps = remaining_for_steps >= steps_byte_size; + if !enough_for_steps { + break; + } + + let mut steps = Vec::with_capacity(step_count); + for i in 0..step_count { + let step_offset = offset + i * 2; + let kc = payload[step_offset]; + let md = payload[step_offset + 1]; + steps.push(MacroStep { + keycode: kc, + modifier: md, + }); + } + offset += steps_byte_size; + + result.push(MacroEntry { + slot, + name, + steps, + }); + } + + result +} + +/// Parse KEYSTATS_BIN (0x40) binary payload. +/// Format: [rows:u8][cols:u8][counts: rows*cols * u32 LE] +/// Returns (heatmap_data, max_value) — same shape as parse_heatmap_lines. +pub fn parse_keystats_binary(payload: &[u8]) -> (Vec>, u32) { + // Need at least 2 bytes for rows and cols + let enough_for_header = payload.len() >= 2; + if !enough_for_header { + return (vec![], 0); + } + + let rows = payload[0] as usize; + let cols = payload[1] as usize; + let total_cells = rows * cols; + let data_byte_size = total_cells * 4; // each count is u32 LE + + let remaining = payload.len().saturating_sub(2); + let enough_for_data = remaining >= data_byte_size; + if !enough_for_data { + return (vec![], 0); + } + + let mut data: Vec> = vec![vec![0u32; cols]; rows]; + let mut max_value = 0u32; + let mut offset = 2; + + for row in 0..rows { + for col in 0..cols { + let count = u32::from_le_bytes([ + payload[offset], + payload[offset + 1], + payload[offset + 2], + payload[offset + 3], + ]); + data[row][col] = count; + + let is_new_max = count > max_value; + if is_new_max { + max_value = count; + } + + offset += 4; + } + } + + (data, max_value) +} diff --git a/original-src/protocol.rs b/original-src/protocol.rs new file mode 100644 index 0000000..03f9b63 --- /dev/null +++ b/original-src/protocol.rs @@ -0,0 +1,65 @@ +#![allow(dead_code)] +/// CDC protocol command helpers for KaSe keyboard firmware. + +// Text-based query commands +pub const CMD_TAP_DANCE: &str = "TD?"; +pub const CMD_COMBOS: &str = "COMBO?"; +pub const CMD_LEADER: &str = "LEADER?"; +pub const CMD_KEY_OVERRIDE: &str = "KO?"; +pub const CMD_BT_STATUS: &str = "BT?"; +pub const CMD_WPM: &str = "WPM?"; +pub const CMD_TAMA: &str = "TAMA?"; +pub const CMD_MACROS_TEXT: &str = "MACROS?"; +pub const CMD_FEATURES: &str = "FEATURES?"; +pub const CMD_KEYSTATS: &str = "KEYSTATS?"; +pub const CMD_BIGRAMS: &str = "BIGRAMS?"; + +pub fn cmd_set_key(layer: u8, row: u8, col: u8, keycode: u16) -> String { + format!("SETKEY {},{},{},{:04X}", layer, row, col, keycode) +} + +pub fn cmd_set_layer_name(layer: u8, name: &str) -> String { + format!("LAYOUTNAME{}:{}", layer, name) +} + +pub fn cmd_bt_switch(slot: u8) -> String { + format!("BT SWITCH {}", slot) +} + +pub fn cmd_trilayer(l1: u8, l2: u8, l3: u8) -> String { + format!("TRILAYER {},{},{}", l1, l2, l3) +} + +pub fn cmd_macroseq(slot: u8, name: &str, steps: &str) -> String { + format!("MACROSEQ {};{};{}", slot, name, steps) +} + +pub fn cmd_macro_del(slot: u8) -> String { + format!("MACRODEL {}", slot) +} + +pub fn cmd_comboset(index: u8, r1: u8, c1: u8, r2: u8, c2: u8, result: u8) -> String { + format!("COMBOSET {};{},{},{},{},{:02X}", index, r1, c1, r2, c2, result) +} + +pub fn cmd_combodel(index: u8) -> String { + format!("COMBODEL {}", index) +} + +pub fn cmd_koset(index: u8, trig_key: u8, trig_mod: u8, res_key: u8, res_mod: u8) -> String { + format!("KOSET {};{:02X},{:02X},{:02X},{:02X}", index, trig_key, trig_mod, res_key, res_mod) +} + +pub fn cmd_kodel(index: u8) -> String { + format!("KODEL {}", index) +} + +pub fn cmd_leaderset(index: u8, sequence: &[u8], result: u8, result_mod: u8) -> String { + let seq_hex: Vec = sequence.iter().map(|k| format!("{:02X}", k)).collect(); + let seq_str = seq_hex.join(","); + format!("LEADERSET {};{};{:02X},{:02X}", index, seq_str, result, result_mod) +} + +pub fn cmd_leaderdel(index: u8) -> String { + format!("LEADERDEL {}", index) +} diff --git a/original-src/serial/mod.rs b/original-src/serial/mod.rs new file mode 100644 index 0000000..cfc2f59 --- /dev/null +++ b/original-src/serial/mod.rs @@ -0,0 +1,15 @@ +/// Serial communication module. +/// Dispatches to native (serialport crate) or web (WebSerial API) +/// depending on the target architecture. + +#[cfg(not(target_arch = "wasm32"))] +mod native; + +#[cfg(target_arch = "wasm32")] +mod web; + +#[cfg(not(target_arch = "wasm32"))] +pub use native::*; + +#[cfg(target_arch = "wasm32")] +pub use web::*; diff --git a/original-src/serial/native.rs b/original-src/serial/native.rs new file mode 100644 index 0000000..ebc91ec --- /dev/null +++ b/original-src/serial/native.rs @@ -0,0 +1,559 @@ +use serialport::SerialPort; +use std::io::{BufRead, BufReader, Read, Write}; +use std::sync::{Arc, Mutex}; +use std::time::{Duration, Instant}; + +use crate::binary_protocol::{self as bp, KrResponse}; +use crate::parsers::{ROWS, COLS}; + +const BAUD_RATE: u32 = 115200; +const CONNECT_TIMEOUT_MS: u64 = 300; +const QUERY_TIMEOUT_MS: u64 = 800; +const BINARY_READ_TIMEOUT_MS: u64 = 1500; +const LEGACY_BINARY_SETTLE_MS: u64 = 50; +const BINARY_SETTLE_MS: u64 = 30; +const JSON_TIMEOUT_SECS: u64 = 3; + +pub struct SerialManager { + port: Option>, + pub port_name: String, + pub connected: bool, + pub v2: bool, // true if firmware supports binary protocol v2 +} + +impl SerialManager { + pub fn new() -> Self { + Self { + port: None, + port_name: String::new(), + connected: false, + v2: false, + } + } + + #[allow(dead_code)] + pub fn list_ports() -> Vec { + let available = serialport::available_ports(); + let ports = available.unwrap_or_default(); + let port_iter = ports.into_iter(); + let name_iter = port_iter.map(|p| p.port_name); + let names: Vec = name_iter.collect(); + names + } + + pub fn connect(&mut self, port_name: &str) -> Result<(), String> { + let builder = serialport::new(port_name, BAUD_RATE); + let builder_with_timeout = builder.timeout(Duration::from_millis(CONNECT_TIMEOUT_MS)); + let open_result = builder_with_timeout.open(); + let port = open_result.map_err(|e| format!("Failed to open {}: {}", port_name, e))?; + + self.port = Some(port); + self.port_name = port_name.to_string(); + self.connected = true; + self.v2 = false; + + // Detect v2: try PING + if let Some(p) = self.port.as_mut() { + let _ = p.clear(serialport::ClearBuffer::All); + } + std::thread::sleep(Duration::from_millis(LEGACY_BINARY_SETTLE_MS)); + + let ping_result = self.send_binary(bp::cmd::PING, &[]); + if ping_result.is_ok() { + self.v2 = true; + } + + Ok(()) + } + + pub fn auto_connect(&mut self) -> Result { + let port_name = Self::find_kase_port()?; + self.connect(&port_name)?; + Ok(port_name) + } + + pub fn find_kase_port() -> Result { + const TARGET_VID: u16 = 0xCAFE; + const TARGET_PID: u16 = 0x4001; + + let available = serialport::available_ports(); + let ports = available.unwrap_or_default(); + if ports.is_empty() { + return Err("No serial ports found".into()); + } + + // First pass: check USB VID/PID and product name + for port in &ports { + let port_type = &port.port_type; + match port_type { + serialport::SerialPortType::UsbPort(usb) => { + let vid_matches = usb.vid == TARGET_VID; + let pid_matches = usb.pid == TARGET_PID; + if vid_matches && pid_matches { + return Ok(port.port_name.clone()); + } + + match &usb.product { + Some(product) => { + let is_kase = product.contains("KaSe"); + let is_kesp = product.contains("KeSp"); + if is_kase || is_kesp { + return Ok(port.port_name.clone()); + } + } + None => {} + } + } + _ => {} + } + } + + // Second pass (Linux only): check udevadm info + #[cfg(target_os = "linux")] + for port in &ports { + let udevadm_result = std::process::Command::new("udevadm") + .args(["info", "-n", &port.port_name]) + .output(); + + match udevadm_result { + Ok(output) => { + let stdout_bytes = &output.stdout; + let text = String::from_utf8_lossy(stdout_bytes); + let has_kase = text.contains("KaSe"); + let has_kesp = text.contains("KeSp"); + if has_kase || has_kesp { + return Ok(port.port_name.clone()); + } + } + Err(_) => {} + } + } + + let scanned_count = ports.len(); + Err(format!("No KaSe keyboard found ({} port(s) scanned)", scanned_count)) + } + + pub fn port_mut(&mut self) -> Option<&mut Box> { + self.port.as_mut() + } + + pub fn disconnect(&mut self) { + self.port = None; + self.port_name.clear(); + self.connected = false; + self.v2 = false; + } + + // ==================== LOW-LEVEL: ASCII LEGACY ==================== + + pub fn send_command(&mut self, cmd: &str) -> Result<(), String> { + let port = self.port.as_mut().ok_or("Not connected")?; + let data = format!("{}\r\n", cmd); + let bytes = data.as_bytes(); + + let write_result = port.write_all(bytes); + write_result.map_err(|e| format!("Write: {}", e))?; + + let flush_result = port.flush(); + flush_result.map_err(|e| format!("Flush: {}", e))?; + + Ok(()) + } + + pub fn query_command(&mut self, cmd: &str) -> Result, String> { + self.send_command(cmd)?; + + let port = self.port.as_mut().ok_or("Not connected")?; + let cloned_port = port.try_clone(); + let port_clone = cloned_port.map_err(|e| e.to_string())?; + let mut reader = BufReader::new(port_clone); + + let mut lines = Vec::new(); + let start = Instant::now(); + let max_wait = Duration::from_millis(QUERY_TIMEOUT_MS); + + loop { + let elapsed = start.elapsed(); + if elapsed > max_wait { + break; + } + + let mut line = String::new(); + let read_result = reader.read_line(&mut line); + + match read_result { + Ok(0) => break, + Ok(_) => { + let trimmed = line.trim().to_string(); + let is_terminal = trimmed == "OK" || trimmed == "ERROR"; + if is_terminal { + break; + } + let is_not_empty = !trimmed.is_empty(); + if is_not_empty { + lines.push(trimmed); + } + } + Err(_) => break, + } + } + + Ok(lines) + } + + fn read_raw(&mut self, timeout_ms: u64) -> Result, String> { + let port = self.port.as_mut().ok_or("Not connected")?; + let mut buf = vec![0u8; 4096]; + let mut result = Vec::new(); + let start = Instant::now(); + let timeout = Duration::from_millis(timeout_ms); + + while start.elapsed() < timeout { + let read_result = port.read(&mut buf); + + match read_result { + Ok(n) if n > 0 => { + let received_bytes = &buf[..n]; + result.extend_from_slice(received_bytes); + std::thread::sleep(Duration::from_millis(5)); + } + _ => { + let got_something = !result.is_empty(); + if got_something { + break; + } + std::thread::sleep(Duration::from_millis(5)); + } + } + } + + Ok(result) + } + + /// Legacy C> binary protocol + fn query_legacy_binary(&mut self, cmd: &str) -> Result<(u8, Vec), String> { + self.send_command(cmd)?; + std::thread::sleep(Duration::from_millis(LEGACY_BINARY_SETTLE_MS)); + let raw = self.read_raw(BINARY_READ_TIMEOUT_MS)?; + + // Look for the "C>" header in the raw bytes + let mut windows = raw.windows(2); + let header_search = windows.position(|w| w == b"C>"); + let pos = header_search.ok_or("No C> header found")?; + + let min_packet_size = pos + 5; + if raw.len() < min_packet_size { + return Err("Packet too short".into()); + } + + let cmd_type = raw[pos + 2]; + let low_byte = raw[pos + 3] as u16; + let high_byte = (raw[pos + 4] as u16) << 8; + let data_len = low_byte | high_byte; + + let data_start = pos + 5; + let data_end = data_start.checked_add(data_len as usize) + .ok_or("Data length overflow")?; + + if raw.len() < data_end { + return Err(format!("Incomplete: need {}, got {}", data_end, raw.len())); + } + + let payload = raw[data_start..data_end].to_vec(); + Ok((cmd_type, payload)) + } + + // ==================== LOW-LEVEL: BINARY V2 ==================== + + /// Send a KS frame, read KR response. + pub fn send_binary(&mut self, cmd_id: u8, payload: &[u8]) -> Result { + let frame = bp::ks_frame(cmd_id, payload); + let port = self.port.as_mut().ok_or("Not connected")?; + + let write_result = port.write_all(&frame); + write_result.map_err(|e| format!("Write: {}", e))?; + + let flush_result = port.flush(); + flush_result.map_err(|e| format!("Flush: {}", e))?; + + std::thread::sleep(Duration::from_millis(BINARY_SETTLE_MS)); + let raw = self.read_raw(BINARY_READ_TIMEOUT_MS)?; + + let (resp, _remaining) = bp::parse_kr(&raw)?; + let firmware_ok = resp.is_ok(); + if !firmware_ok { + let status = resp.status_name(); + return Err(format!("Firmware error: {}", status)); + } + + Ok(resp) + } + + // ==================== HIGH-LEVEL: AUTO V2/LEGACY ==================== + + pub fn get_firmware_version(&mut self) -> Option { + // Try v2 binary first + if self.v2 { + let binary_result = self.send_binary(bp::cmd::VERSION, &[]); + match binary_result { + Ok(resp) => { + let raw_bytes = &resp.payload; + let lossy_string = String::from_utf8_lossy(raw_bytes); + let version = lossy_string.to_string(); + return Some(version); + } + Err(_) => {} + } + } + + // Legacy fallback + let query_result = self.query_command("VERSION?"); + let lines = match query_result { + Ok(l) => l, + Err(_) => return None, + }; + let mut line_iter = lines.into_iter(); + let first_line = line_iter.next(); + first_line + } + + pub fn get_keymap(&mut self, layer: u8) -> Result>, String> { + // Try v2 binary first + if self.v2 { + let resp = self.send_binary(bp::cmd::KEYMAP_GET, &[layer])?; + let keymap = self.parse_keymap_payload(&resp.payload)?; + return Ok(keymap); + } + + // Legacy + let cmd = format!("KEYMAP{}", layer); + let (cmd_type, data) = self.query_legacy_binary(&cmd)?; + + if cmd_type != 1 { + return Err(format!("Unexpected cmd type: {}", cmd_type)); + } + if data.len() < 2 { + return Err("Data too short".into()); + } + + // skip 2-byte layer index in legacy + let data_without_header = &data[2..]; + self.parse_keymap_payload(data_without_header) + } + + fn parse_keymap_payload(&self, data: &[u8]) -> Result>, String> { + // v2: [layer:u8][keycodes...] -- skip first byte + // legacy: already stripped + let expected_with_layer_byte = 1 + ROWS * COLS * 2; + let has_layer_byte = data.len() >= expected_with_layer_byte; + let offset = if has_layer_byte { 1 } else { 0 }; + let kc_data = &data[offset..]; + + let needed_bytes = ROWS * COLS * 2; + if kc_data.len() < needed_bytes { + return Err(format!("Keymap data too short: {} bytes (need {})", kc_data.len(), needed_bytes)); + } + + let mut keymap = Vec::with_capacity(ROWS); + + for row_index in 0..ROWS { + let mut row = Vec::with_capacity(COLS); + + for col_index in 0..COLS { + let idx = (row_index * COLS + col_index) * 2; + let low_byte = kc_data[idx] as u16; + let high_byte = (kc_data[idx + 1] as u16) << 8; + let keycode = low_byte | high_byte; + row.push(keycode); + } + + keymap.push(row); + } + + Ok(keymap) + } + + pub fn get_layer_names(&mut self) -> Result, String> { + // Try v2 binary first + if self.v2 { + let resp = self.send_binary(bp::cmd::LIST_LAYOUTS, &[])?; + + let payload = &resp.payload; + if payload.is_empty() { + return Err("Empty response".into()); + } + + let count = payload[0] as usize; + let mut names = Vec::with_capacity(count); + let mut i = 1; + + for _ in 0..count { + let remaining = payload.len(); + let need_header = i + 2; + if need_header > remaining { + break; + } + + let _layer_index = payload[i]; + let name_len = payload[i + 1] as usize; + i += 2; + + let need_name = i + name_len; + if need_name > payload.len() { + break; + } + + let name_bytes = &payload[i..i + name_len]; + let name_lossy = String::from_utf8_lossy(name_bytes); + let name = name_lossy.to_string(); + names.push(name); + i += name_len; + } + + let found_names = !names.is_empty(); + if found_names { + return Ok(names); + } + } + + // Legacy fallback: try C> binary protocol + let legacy_result = self.query_legacy_binary("LAYOUTS?"); + match legacy_result { + Ok((cmd_type, data)) => { + let is_layout_type = cmd_type == 4; + let has_data = !data.is_empty(); + + if is_layout_type && has_data { + let text = String::from_utf8_lossy(&data); + let parts = text.split(';'); + let non_empty = parts.filter(|s| !s.is_empty()); + let trimmed_names = non_empty.map(|s| { + let long_enough = s.len() > 1; + if long_enough { + s[1..].to_string() + } else { + s.to_string() + } + }); + let names: Vec = trimmed_names.collect(); + + let found_names = !names.is_empty(); + if found_names { + return Ok(names); + } + } + } + Err(_) => {} + } + + // Last resort: raw text + self.send_command("LAYOUTS?")?; + std::thread::sleep(Duration::from_millis(LEGACY_BINARY_SETTLE_MS * 2)); + let raw = self.read_raw(BINARY_READ_TIMEOUT_MS)?; + + let text = String::from_utf8_lossy(&raw); + let split_by_delimiters = text.split(|c: char| c == ';' || c == '\n'); + + let cleaned = split_by_delimiters.map(|s| { + let step1 = s.trim(); + let step2 = step1.trim_matches(|c: char| c.is_control() || c == '"'); + step2 + }); + + let valid_names = cleaned.filter(|s| { + let is_not_empty = !s.is_empty(); + let is_short_enough = s.len() < 30; + let no_header_marker = !s.contains("C>"); + let not_ok = *s != "OK"; + is_not_empty && is_short_enough && no_header_marker && not_ok + }); + + let as_strings = valid_names.map(|s| s.to_string()); + let names: Vec = as_strings.collect(); + + let found_any = !names.is_empty(); + if found_any { + Ok(names) + } else { + Err("No layer names found".into()) + } + } + + pub fn get_layout_json(&mut self) -> Result { + // Try v2 binary first + if self.v2 { + let resp = self.send_binary(bp::cmd::GET_LAYOUT_JSON, &[])?; + let has_payload = !resp.payload.is_empty(); + if has_payload { + let raw_bytes = &resp.payload; + let lossy_string = String::from_utf8_lossy(raw_bytes); + let json = lossy_string.to_string(); + return Ok(json); + } + } + + // Legacy: text brace-counting + self.send_command("LAYOUT?")?; + std::thread::sleep(Duration::from_millis(LEGACY_BINARY_SETTLE_MS)); + + let port = self.port.as_mut().ok_or("Not connected")?; + let mut result = String::new(); + let mut buf = [0u8; 4096]; + let start = Instant::now(); + let max_wait = Duration::from_secs(JSON_TIMEOUT_SECS); + let mut brace_count: i32 = 0; + let mut started = false; + + while start.elapsed() < max_wait { + let read_result = port.read(&mut buf); + + match read_result { + Ok(n) if n > 0 => { + let received_bytes = &buf[..n]; + let chunk = String::from_utf8_lossy(received_bytes); + + for ch in chunk.chars() { + let is_open_brace = ch == '{'; + if is_open_brace { + started = true; + brace_count += 1; + } + + if started { + result.push(ch); + } + + let is_close_brace = ch == '}'; + if is_close_brace && started { + brace_count -= 1; + let json_complete = brace_count == 0; + if json_complete { + return Ok(result); + } + } + } + } + _ => { + std::thread::sleep(Duration::from_millis(10)); + } + } + } + + let got_nothing = result.is_empty(); + if got_nothing { + Err("No JSON".into()) + } else { + Err("Incomplete JSON".into()) + } + } +} + +/// Thread-safe wrapper +pub type SharedSerial = Arc>; + +pub fn new_shared() -> SharedSerial { + let manager = SerialManager::new(); + let mutex = Mutex::new(manager); + let shared = Arc::new(mutex); + shared +} diff --git a/original-src/settings.rs b/original-src/settings.rs new file mode 100644 index 0000000..531200f --- /dev/null +++ b/original-src/settings.rs @@ -0,0 +1,118 @@ +/// Persistent settings for KaSe Controller. +/// +/// - **Native**: `kase_settings.json` next to the executable. +/// - **WASM**: `localStorage` under key `"kase_settings"`. +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Settings { + #[serde(default = "default_layout")] + pub keyboard_layout: String, +} + +fn default_layout() -> String { + "QWERTY".to_string() +} + +impl Default for Settings { + fn default() -> Self { + Self { + keyboard_layout: default_layout(), + } + } +} + +// ============================================================================= +// Native: JSON file next to executable +// ============================================================================= + +#[cfg(not(target_arch = "wasm32"))] +mod native_settings { + use super::Settings; + use std::path::PathBuf; + + /// Config file path: next to the executable. + fn settings_path() -> PathBuf { + let exe = std::env::current_exe().unwrap_or_default(); + let parent_dir = exe.parent().unwrap_or(std::path::Path::new(".")); + parent_dir.join("kase_settings.json") + } + + pub fn load() -> Settings { + let path = settings_path(); + let json_content = std::fs::read_to_string(&path).ok(); + let parsed = json_content.and_then(|s| serde_json::from_str(&s).ok()); + parsed.unwrap_or_default() + } + + pub fn save(settings: &Settings) { + let path = settings_path(); + let json_result = serde_json::to_string_pretty(settings); + if let Ok(json) = json_result { + let _ = std::fs::write(path, json); + } + } +} + +// ============================================================================= +// WASM: browser localStorage +// ============================================================================= + +#[cfg(target_arch = "wasm32")] +mod web_settings { + use super::Settings; + + const STORAGE_KEY: &str = "kase_settings"; + + /// Get localStorage. Returns None if not in a browser context. + fn get_storage() -> Option { + let window = web_sys::window()?; + window.local_storage().ok().flatten() + } + + pub fn load() -> Settings { + let storage = match get_storage() { + Some(s) => s, + None => return Settings::default(), + }; + + let json_option = storage.get_item(STORAGE_KEY).ok().flatten(); + let parsed = json_option.and_then(|s| serde_json::from_str(&s).ok()); + parsed.unwrap_or_default() + } + + pub fn save(settings: &Settings) { + let storage = match get_storage() { + Some(s) => s, + None => return, + }; + + let json_result = serde_json::to_string(settings); + if let Ok(json) = json_result { + let _ = storage.set_item(STORAGE_KEY, &json); + } + } +} + +// ============================================================================= +// Public interface +// ============================================================================= + +/// Load settings from persistent storage. +/// Returns `Settings::default()` if none found. +pub fn load() -> Settings { + #[cfg(not(target_arch = "wasm32"))] + return native_settings::load(); + + #[cfg(target_arch = "wasm32")] + return web_settings::load(); +} + +/// Save settings to persistent storage. Fails silently. +pub fn save(settings: &Settings) { + #[cfg(not(target_arch = "wasm32"))] + native_settings::save(settings); + + #[cfg(target_arch = "wasm32")] + web_settings::save(settings); +} diff --git a/original-src/stats_analyzer.rs b/original-src/stats_analyzer.rs new file mode 100644 index 0000000..a14e755 --- /dev/null +++ b/original-src/stats_analyzer.rs @@ -0,0 +1,335 @@ +/// Stats analysis: hand balance, finger load, row usage, top keys, bigrams. +/// Transforms raw heatmap data into structured analysis for the stats tab. + +use crate::keycode; +use crate::parsers::ROWS; + +/// Which hand a column belongs to. +#[derive(Clone, Copy, PartialEq)] +pub enum Hand { + Left, + Right, +} + +/// Which finger a column belongs to. +#[derive(Clone, Copy, PartialEq)] +pub enum Finger { + Pinky, + Ring, + Middle, + Index, + Thumb, +} + +/// Row names for the 5 rows. +const ROW_NAMES: [&str; 5] = ["Number", "Upper", "Home", "Lower", "Thumb"]; + +/// Finger names (French). +const FINGER_NAMES: [&str; 5] = ["Pinky", "Ring", "Middle", "Index", "Thumb"]; + +/// Map column index → (Hand, Finger). +/// KaSe layout: cols 0-5 = left hand, cols 6 (gap), cols 7-12 = right hand. +fn col_to_hand_finger(col: usize) -> (Hand, Finger) { + match col { + 0 => (Hand::Left, Finger::Pinky), + 1 => (Hand::Left, Finger::Ring), + 2 => (Hand::Left, Finger::Middle), + 3 => (Hand::Left, Finger::Index), + 4 => (Hand::Left, Finger::Index), // inner column, still index + 5 => (Hand::Left, Finger::Thumb), + 6 => (Hand::Left, Finger::Thumb), // center / gap + 7 => (Hand::Right, Finger::Thumb), + 8 => (Hand::Right, Finger::Index), + 9 => (Hand::Right, Finger::Index), // inner column + 10 => (Hand::Right, Finger::Middle), + 11 => (Hand::Right, Finger::Ring), + 12 => (Hand::Right, Finger::Pinky), + _ => (Hand::Left, Finger::Pinky), + } +} + +/// Hand balance result. +#[allow(dead_code)] +pub struct HandBalance { + pub left_count: u32, + pub right_count: u32, + pub total: u32, + pub left_pct: f32, + pub right_pct: f32, +} + +/// Finger load for one finger. +#[allow(dead_code)] +pub struct FingerLoad { + pub name: String, + pub hand: Hand, + pub count: u32, + pub pct: f32, +} + +/// Row usage for one row. +#[allow(dead_code)] +pub struct RowUsage { + pub name: String, + pub row: usize, + pub count: u32, + pub pct: f32, +} + +/// A key in the top keys ranking. +#[allow(dead_code)] +pub struct TopKey { + pub name: String, + pub finger: String, + pub count: u32, + pub pct: f32, +} + +/// Compute hand balance from heatmap data. +pub fn hand_balance(heatmap: &[Vec]) -> HandBalance { + let mut left: u32 = 0; + let mut right: u32 = 0; + + for row in heatmap { + for (c, &count) in row.iter().enumerate() { + let (hand, _) = col_to_hand_finger(c); + match hand { + Hand::Left => left += count, + Hand::Right => right += count, + } + } + } + + let total = left + right; + let left_pct = if total > 0 { left as f32 / total as f32 * 100.0 } else { 0.0 }; + let right_pct = if total > 0 { right as f32 / total as f32 * 100.0 } else { 0.0 }; + + HandBalance { left_count: left, right_count: right, total, left_pct, right_pct } +} + +/// Compute finger load (10 fingers: 5 left + 5 right). +pub fn finger_load(heatmap: &[Vec]) -> Vec { + let mut counts = [[0u32; 5]; 2]; // [hand][finger] + + for row in heatmap { + for (c, &count) in row.iter().enumerate() { + let (hand, finger) = col_to_hand_finger(c); + let hi = if hand == Hand::Left { 0 } else { 1 }; + let fi = match finger { + Finger::Pinky => 0, + Finger::Ring => 1, + Finger::Middle => 2, + Finger::Index => 3, + Finger::Thumb => 4, + }; + counts[hi][fi] += count; + } + } + + let total: u32 = counts[0].iter().sum::() + counts[1].iter().sum::(); + let mut result = Vec::with_capacity(10); + + // Left hand fingers + for fi in 0..5 { + let count = counts[0][fi]; + let pct = if total > 0 { count as f32 / total as f32 * 100.0 } else { 0.0 }; + let name = format!("{} L", FINGER_NAMES[fi]); + result.push(FingerLoad { name, hand: Hand::Left, count, pct }); + } + + // Right hand fingers + for fi in 0..5 { + let count = counts[1][fi]; + let pct = if total > 0 { count as f32 / total as f32 * 100.0 } else { 0.0 }; + let name = format!("{} R", FINGER_NAMES[fi]); + result.push(FingerLoad { name, hand: Hand::Right, count, pct }); + } + + result +} + +/// Compute row usage. +pub fn row_usage(heatmap: &[Vec]) -> Vec { + let mut row_counts = [0u32; ROWS]; + + for (r, row) in heatmap.iter().enumerate() { + if r >= ROWS { break; } + let row_sum: u32 = row.iter().sum(); + row_counts[r] = row_sum; + } + + let total: u32 = row_counts.iter().sum(); + let mut result = Vec::with_capacity(ROWS); + + for r in 0..ROWS { + let count = row_counts[r]; + let pct = if total > 0 { count as f32 / total as f32 * 100.0 } else { 0.0 }; + let name = ROW_NAMES[r].to_string(); + result.push(RowUsage { name, row: r, count, pct }); + } + + result +} + +/// Compute top N keys by press count. +pub fn top_keys(heatmap: &[Vec], keymap: &[Vec], n: usize) -> Vec { + let mut all_keys: Vec<(u32, usize, usize)> = Vec::new(); // (count, row, col) + + for (r, row) in heatmap.iter().enumerate() { + for (c, &count) in row.iter().enumerate() { + if count > 0 { + all_keys.push((count, r, c)); + } + } + } + + all_keys.sort_by(|a, b| b.0.cmp(&a.0)); + all_keys.truncate(n); + + let total: u32 = heatmap.iter().flat_map(|r| r.iter()).sum(); + + let mut result = Vec::with_capacity(n); + for (count, r, c) in all_keys { + let code = keymap.get(r).and_then(|row| row.get(c)).copied().unwrap_or(0); + let name = keycode::decode_keycode(code); + let (hand, finger) = col_to_hand_finger(c); + let hand_str = if hand == Hand::Left { "L" } else { "R" }; + let finger_str = match finger { + Finger::Pinky => "Pinky", + Finger::Ring => "Ring", + Finger::Middle => "Middle", + Finger::Index => "Index", + Finger::Thumb => "Thumb", + }; + let finger_label = format!("{} {}", finger_str, hand_str); + let pct = if total > 0 { count as f32 / total as f32 * 100.0 } else { 0.0 }; + result.push(TopKey { name, finger: finger_label, count, pct }); + } + + result +} + +/// Find keys that have never been pressed (count = 0, keycode != 0). +pub fn dead_keys(heatmap: &[Vec], keymap: &[Vec]) -> Vec { + let mut result = Vec::new(); + for (r, row) in heatmap.iter().enumerate() { + for (c, &count) in row.iter().enumerate() { + if count > 0 { continue; } + let code = keymap.get(r).and_then(|row| row.get(c)).copied().unwrap_or(0); + let is_mapped = code != 0; + if is_mapped { + let name = keycode::decode_keycode(code); + result.push(name); + } + } + } + result +} + +// ==================== Bigram analysis ==================== + +/// A parsed bigram entry. +pub struct BigramEntry { + pub from_row: u8, + pub from_col: u8, + pub to_row: u8, + pub to_col: u8, + pub count: u32, +} + +/// Bigram analysis results. +#[allow(dead_code)] +pub struct BigramAnalysis { + pub total: u32, + pub alt_hand: u32, + pub same_hand: u32, + pub sfb: u32, + pub alt_hand_pct: f32, + pub same_hand_pct: f32, + pub sfb_pct: f32, +} + +/// Parse bigram text lines from firmware. +/// Format: " R2C3 -> R2C4 : 150" +pub fn parse_bigram_lines(lines: &[String]) -> Vec { + let mut entries = Vec::new(); + + for line in lines { + let trimmed = line.trim(); + let has_arrow = trimmed.contains("->"); + if !has_arrow { continue; } + + let parts: Vec<&str> = trimmed.split("->").collect(); + if parts.len() != 2 { continue; } + + let left = parts[0].trim(); + let right_and_count = parts[1].trim(); + + let right_parts: Vec<&str> = right_and_count.split(':').collect(); + if right_parts.len() != 2 { continue; } + + let right = right_parts[0].trim(); + let count_str = right_parts[1].trim(); + + let from = parse_rc(left); + let to = parse_rc(right); + let count: u32 = count_str.parse().unwrap_or(0); + + if let (Some((fr, fc)), Some((tr, tc))) = (from, to) { + entries.push(BigramEntry { + from_row: fr, from_col: fc, + to_row: tr, to_col: tc, + count, + }); + } + } + + entries +} + +/// Parse "R2C3" into (row, col). +fn parse_rc(s: &str) -> Option<(u8, u8)> { + let s = s.trim(); + let r_pos = s.find('R')?; + let c_pos = s.find('C')?; + if c_pos <= r_pos { return None; } + + let row_str = &s[r_pos + 1..c_pos]; + let col_str = &s[c_pos + 1..]; + let row: u8 = row_str.parse().ok()?; + let col: u8 = col_str.parse().ok()?; + Some((row, col)) +} + +/// Analyze bigram entries for hand alternation and SFB. +pub fn analyze_bigrams(entries: &[BigramEntry]) -> BigramAnalysis { + let mut alt_hand: u32 = 0; + let mut same_hand: u32 = 0; + let mut sfb: u32 = 0; + let mut total: u32 = 0; + + for entry in entries { + let (hand_from, finger_from) = col_to_hand_finger(entry.from_col as usize); + let (hand_to, finger_to) = col_to_hand_finger(entry.to_col as usize); + + total += entry.count; + + if hand_from != hand_to { + alt_hand += entry.count; + } else { + same_hand += entry.count; + if finger_from == finger_to { + sfb += entry.count; + } + } + } + + let alt_hand_pct = if total > 0 { alt_hand as f32 / total as f32 * 100.0 } else { 0.0 }; + let same_hand_pct = if total > 0 { same_hand as f32 / total as f32 * 100.0 } else { 0.0 }; + let sfb_pct = if total > 0 { sfb as f32 / total as f32 * 100.0 } else { 0.0 }; + + BigramAnalysis { + total, alt_hand, same_hand, sfb, + alt_hand_pct, same_hand_pct, sfb_pct, + } +} diff --git a/original-src/ui_background.rs b/original-src/ui_background.rs new file mode 100644 index 0000000..1c92af9 --- /dev/null +++ b/original-src/ui_background.rs @@ -0,0 +1,263 @@ +use super::BgResult; + +/// Map a query tag to its binary command ID. +#[cfg(target_arch = "wasm32")] +use crate::binary_protocol as bp; +#[cfg(target_arch = "wasm32")] +fn tag_to_binary_cmd(tag: &str) -> Option { + match tag { + "td" => Some(bp::cmd::TD_LIST), + "combo" => Some(bp::cmd::COMBO_LIST), + "leader" => Some(bp::cmd::LEADER_LIST), + "ko" => Some(bp::cmd::KO_LIST), + "bt" => Some(bp::cmd::BT_QUERY), + "tama" => Some(bp::cmd::TAMA_QUERY), + "wpm" => Some(bp::cmd::WPM_QUERY), + "macros" => Some(bp::cmd::LIST_MACROS), + "keystats" => Some(bp::cmd::KEYSTATS_BIN), + "features" => Some(bp::cmd::FEATURES), + _ => None, + } +} + +impl super::KaSeApp { + // ---- Background helpers ---- + + #[cfg(not(target_arch = "wasm32"))] + pub(super) fn bg_query(&mut self, tag: &str, cmd: &str) { + self.busy = true; + let serial = self.serial.clone(); + let tx = self.bg_tx.clone(); + let tag = tag.to_string(); + let cmd = cmd.to_string(); + + std::thread::spawn(move || { + let mut ser = serial.lock().unwrap_or_else(|p| p.into_inner()); + let lines = ser.query_command(&cmd).unwrap_or_default(); + let _ = tx.send(BgResult::TextLines(tag, lines)); + }); + } + + #[cfg(target_arch = "wasm32")] + pub(super) fn bg_query(&mut self, tag: &str, cmd: &str) { + if self.web_busy.get() { return; } + self.busy = true; + self.web_busy.set(true); + let serial = self.serial.clone(); + let tx = self.bg_tx.clone(); + let web_busy = self.web_busy.clone(); + let tag = tag.to_string(); + let cmd = cmd.to_string(); + + wasm_bindgen_futures::spawn_local(async move { + let v2 = serial.borrow().v2; + let handles = serial.borrow().io_handles(); + if let Ok((reader, writer)) = handles { + if let Some(cmd_id) = v2.then(|| tag_to_binary_cmd(&tag)).flatten() { + match crate::serial::send_binary(&reader, &writer, cmd_id, &[]).await { + Ok(resp) => { + let _ = tx.send(BgResult::BinaryPayload(tag, resp.payload)); + } + Err(_) => {} + } + } else { + let result = crate::serial::query_command(&reader, &writer, &cmd).await; + match result { + Ok(lines) => { + let _ = tx.send(BgResult::TextLines(tag, lines)); + } + Err(e) => { + if e == "timeout_refresh" { + serial.borrow_mut().refresh_reader(); + } + } + } + } + } + web_busy.set(false); + }); + } + + /// Run multiple queries sequentially (avoids mutex contention). + #[cfg(not(target_arch = "wasm32"))] + pub(super) fn bg_query_batch(&mut self, queries: &[(&str, &str)]) { + self.busy = true; + let serial = self.serial.clone(); + let tx = self.bg_tx.clone(); + + let owned_queries: Vec<(String, String)> = queries + .iter() + .map(|(t, c)| (t.to_string(), c.to_string())) + .collect(); + + std::thread::spawn(move || { + let mut ser = serial.lock().unwrap_or_else(|p| p.into_inner()); + + for (tag, cmd) in owned_queries { + let lines = ser.query_command(&cmd).unwrap_or_default(); + let _ = tx.send(BgResult::TextLines(tag, lines)); + } + }); + } + + #[cfg(target_arch = "wasm32")] + pub(super) fn bg_query_batch(&mut self, queries: &[(&str, &str)]) { + if self.web_busy.get() { return; } + self.busy = true; + self.web_busy.set(true); + let serial = self.serial.clone(); + let tx = self.bg_tx.clone(); + let web_busy = self.web_busy.clone(); + + let owned: Vec<(String, String)> = queries + .iter() + .map(|(t, c)| (t.to_string(), c.to_string())) + .collect(); + + wasm_bindgen_futures::spawn_local(async move { + let v2 = serial.borrow().v2; + + for (tag, text_cmd) in &owned { + let handles = serial.borrow().io_handles(); + let (reader, writer) = match handles { + Ok(h) => h, + Err(_) => break, + }; + + if let Some(cmd_id) = v2.then(|| tag_to_binary_cmd(tag)).flatten() { + match crate::serial::send_binary(&reader, &writer, cmd_id, &[]).await { + Ok(resp) => { + let _ = tx.send(BgResult::BinaryPayload(tag.clone(), resp.payload)); + } + Err(_) => {} + } + } else { + match crate::serial::query_command(&reader, &writer, text_cmd).await { + Ok(lines) => { + let _ = tx.send(BgResult::TextLines(tag.clone(), lines)); + } + Err(e) => { + if e == "timeout_refresh" { + serial.borrow_mut().refresh_reader(); + } + } + } + } + } + web_busy.set(false); + }); + } + + #[cfg(not(target_arch = "wasm32"))] + pub(super) fn bg_send(&self, cmd: &str) { + let serial = self.serial.clone(); + let cmd = cmd.to_string(); + + std::thread::spawn(move || { + let mut ser = serial.lock().unwrap_or_else(|p| p.into_inner()); + let _ = ser.send_command(&cmd); + }); + } + + #[cfg(target_arch = "wasm32")] + pub(super) fn bg_send(&self, cmd: &str) { + let serial = self.serial.clone(); + let cmd = cmd.to_string(); + + wasm_bindgen_futures::spawn_local(async move { + let handles = serial.borrow().io_handles(); + if let Ok((_reader, writer)) = handles { + let _ = crate::serial::send_command(&writer, &cmd).await; + } + }); + } + + // ---- Data helpers ---- + + #[cfg(not(target_arch = "wasm32"))] + pub(super) fn load_keymap(&mut self) { + self.busy = true; + let serial = self.serial.clone(); + let tx = self.bg_tx.clone(); + let layer = self.current_layer as u8; + + std::thread::spawn(move || { + let mut ser = serial.lock().unwrap_or_else(|p| p.into_inner()); + + match ser.get_keymap(layer) { + Ok(km) => { let _ = tx.send(BgResult::Keymap(km)); } + Err(e) => { let _ = tx.send(BgResult::Error(e)); } + } + }); + } + + #[cfg(target_arch = "wasm32")] + pub(super) fn load_keymap(&mut self) { + if self.web_busy.get() { return; } + self.busy = true; + self.web_busy.set(true); + let serial = self.serial.clone(); + let tx = self.bg_tx.clone(); + let web_busy = self.web_busy.clone(); + let layer = self.current_layer as u8; + + wasm_bindgen_futures::spawn_local(async move { + let ser = serial.borrow(); + let v2 = ser.v2; + let handles = ser.io_handles(); + drop(ser); + match handles { + Ok((reader, writer)) => { + match crate::serial::get_keymap(&reader, &writer, layer, v2).await { + Ok(km) => { let _ = tx.send(BgResult::Keymap(km)); } + Err(e) => { let _ = tx.send(BgResult::Error(e)); } + } + } + Err(e) => { let _ = tx.send(BgResult::Error(e)); } + } + web_busy.set(false); + }); + } + + #[cfg(not(target_arch = "wasm32"))] + pub(super) fn load_layer_names(&self) { + let serial = self.serial.clone(); + let tx = self.bg_tx.clone(); + + std::thread::spawn(move || { + let mut ser = serial.lock().unwrap_or_else(|p| p.into_inner()); + + match ser.get_layer_names() { + Ok(names) if !names.is_empty() => { + let _ = tx.send(BgResult::LayerNames(names)); + } + _ => {} + } + }); + } + + #[cfg(target_arch = "wasm32")] + pub(super) fn load_layer_names(&self) { + if self.web_busy.get() { return; } + self.web_busy.set(true); + let serial = self.serial.clone(); + let tx = self.bg_tx.clone(); + let web_busy = self.web_busy.clone(); + + wasm_bindgen_futures::spawn_local(async move { + let ser = serial.borrow(); + let v2 = ser.v2; + let handles = ser.io_handles(); + drop(ser); + if let Ok((reader, writer)) = handles { + match crate::serial::get_layer_names(&reader, &writer, v2).await { + Ok(names) if !names.is_empty() => { + let _ = tx.send(BgResult::LayerNames(names)); + } + _ => {} + } + } + web_busy.set(false); + }); + } +} diff --git a/original-src/ui_connection.rs b/original-src/ui_connection.rs new file mode 100644 index 0000000..a8376ba --- /dev/null +++ b/original-src/ui_connection.rs @@ -0,0 +1,139 @@ +use super::BgResult; + +#[cfg(target_arch = "wasm32")] +use eframe::egui; + +impl super::KaSeApp { + // ---- Connection helpers ---- + + #[cfg(not(target_arch = "wasm32"))] + pub(super) fn is_connected(&self) -> bool { + // Utilise le cache — PAS de serial.lock() ici ! + // Sinon le thread UI bloque quand un thread background + // (OTA, query batch) tient le lock. + self.connected_cache + } + + #[cfg(target_arch = "wasm32")] + pub(super) fn is_connected(&self) -> bool { + let ser = self.serial.borrow(); + ser.connected + } + + #[cfg(not(target_arch = "wasm32"))] + pub(super) fn connect(&mut self) { + let serial = self.serial.clone(); + let tx = self.bg_tx.clone(); + self.busy = true; + self.status_msg = "Scanning ports...".into(); + + std::thread::spawn(move || { + let mut ser = match serial.lock() { + Ok(g) => g, + Err(poisoned) => poisoned.into_inner(), + }; + + match ser.auto_connect() { + Ok(port_name) => { + let fw = ser.get_firmware_version().unwrap_or_default(); + let names = ser.get_layer_names().unwrap_or_default(); + let km = ser.get_keymap(0).unwrap_or_default(); + let _ = tx.send(BgResult::Connected(port_name, fw, names, km)); + + // Try to fetch physical layout from firmware + let layout = ser.get_layout_json() + .ok() + .and_then(|json| crate::layout::parse_json(&json).ok()); + if let Some(keys) = layout { + let _ = tx.send(BgResult::LayoutJson(keys)); + } + } + Err(e) => { + let _ = tx.send(BgResult::ConnectError(e)); + } + } + }); + } + + #[cfg(target_arch = "wasm32")] + pub(super) fn connect_web(&mut self, ctx: &egui::Context) { + self.busy = true; + self.web_busy.set(true); + self.status_msg = "Selecting port...".into(); + let serial = self.serial.clone(); + let tx = self.bg_tx.clone(); + let web_busy = self.web_busy.clone(); + let ctx = ctx.clone(); + + wasm_bindgen_futures::spawn_local(async move { + // Step 1: async port selection (no borrow held) + let conn = match crate::serial::request_port().await { + Ok(c) => c, + Err(e) => { + let _ = tx.send(BgResult::ConnectError(e)); + web_busy.set(false); + ctx.request_repaint(); + return; + } + }; + + // Step 2: extract handles before moving conn + let reader = conn.reader.clone(); + let writer = conn.writer.clone(); + let v2 = conn.v2; + + // Step 3: store connection + serial.borrow_mut().apply_connection(conn); + + // Step 4: async queries (no borrow held) + let fw = crate::serial::get_firmware_version(&reader, &writer, v2) + .await + .unwrap_or_default(); + let names = crate::serial::get_layer_names(&reader, &writer, v2) + .await + .unwrap_or_default(); + let km = crate::serial::get_keymap(&reader, &writer, 0, v2) + .await + .unwrap_or_default(); + + let _ = tx.send(BgResult::Connected("WebSerial".into(), fw, names, km)); + + // Try to fetch physical layout from firmware + let layout_json = crate::serial::get_layout_json(&reader, &writer, v2).await; + if let Ok(json) = layout_json { + if let Ok(keys) = crate::layout::parse_json(&json) { + let _ = tx.send(BgResult::LayoutJson(keys)); + } + } + + web_busy.set(false); + ctx.request_repaint(); + }); + } + + #[cfg(not(target_arch = "wasm32"))] + pub(super) fn disconnect(&mut self) { + let mut guard = match self.serial.lock() { + Ok(g) => g, + Err(poisoned) => poisoned.into_inner(), + }; + guard.disconnect(); + drop(guard); + + self.connected_cache = false; + self.firmware_version.clear(); + self.keymap.clear(); + self.layer_names = vec!["Layer 0".into()]; + self.status_msg = "Disconnected".into(); + } + + #[cfg(target_arch = "wasm32")] + pub(super) fn disconnect(&mut self) { + self.serial.borrow_mut().disconnect(); + + self.firmware_version.clear(); + self.keymap.clear(); + self.layer_names = vec!["Layer 0".into()]; + self.status_msg = "Disconnected".into(); + } +} diff --git a/original-src/ui_helpers.rs b/original-src/ui_helpers.rs new file mode 100644 index 0000000..b697e21 --- /dev/null +++ b/original-src/ui_helpers.rs @@ -0,0 +1,218 @@ +use super::{BgResult, Instant}; + +impl super::KaSeApp { + #[allow(dead_code)] + pub(super) fn notify(&mut self, msg: &str) { + let timestamp = Instant::now(); + let entry = (msg.to_string(), timestamp); + self.notifications.push(entry); + } + + pub(super) fn get_heatmap_intensity(&self, row: usize, col: usize) -> f32 { + if !self.heatmap_on || self.heatmap_max == 0 { + return 0.0; + } + + let row_data = self.heatmap_data.get(row); + let cell_option = row_data.and_then(|r| r.get(col)); + let count = cell_option.copied().unwrap_or(0); + + let count_float = count as f32; + let max_float = self.heatmap_max as f32; + let intensity = count_float / max_float; + intensity + } + + #[cfg(not(target_arch = "wasm32"))] + pub(super) fn load_heatmap(&mut self) { + self.busy = true; + let serial = self.serial.clone(); + let tx = self.bg_tx.clone(); + + std::thread::spawn(move || { + let mut ser = serial.lock().unwrap_or_else(|p| p.into_inner()); + let lines = ser.query_command("KEYSTATS?").unwrap_or_default(); + let (data, max) = crate::parsers::parse_heatmap_lines(&lines); + let _ = tx.send(BgResult::HeatmapData(data, max)); + }); + } + + #[cfg(target_arch = "wasm32")] + pub(super) fn load_heatmap(&mut self) { + if self.web_busy.get() { return; } + self.busy = true; + self.web_busy.set(true); + let serial = self.serial.clone(); + let tx = self.bg_tx.clone(); + let web_busy = self.web_busy.clone(); + + wasm_bindgen_futures::spawn_local(async move { + let handles = serial.borrow().io_handles(); + if let Ok((reader, writer)) = handles { + let lines = crate::serial::query_command(&reader, &writer, "KEYSTATS?") + .await + .unwrap_or_default(); + let (data, max) = crate::parsers::parse_heatmap_lines(&lines); + let _ = tx.send(BgResult::HeatmapData(data, max)); + } + web_busy.set(false); + }); + } + + + /// Apply a key selection from the key selector. + /// row == 951: new KO result key + /// row == 950: new KO trigger key + /// row 900-949: KO edit (900 + idx*10 + 0=trig, +1=result) + /// row >= 800: new leader result + /// row >= 700: new leader sequence key (append) + /// row >= 600: leader result edit (row-600 = leader index) + /// row >= 500: leader sequence key edit (row = 500 + idx*10 + seq_pos) + /// row >= 400: new combo result mode + /// row >= 300: combo result edit (row-300 = combo index) + /// row >= 200: macro step mode (add key as step) + /// row >= 100: TD mode (row-100 = td index, col = action slot) + /// row < 100: keymap mode + pub(super) fn apply_key_selection(&mut self, row: usize, col: usize, code: u16) { + if row == 951 { + // New KO result key + self.ko_new_res_key = code as u8; + self.ko_new_res_set = true; + self.status_msg = format!("KO result = {}", crate::keycode::hid_key_name(code as u8)); + return; + } + + if row == 950 { + // New KO trigger key + self.ko_new_trig_key = code as u8; + self.ko_new_trig_set = true; + self.status_msg = format!("KO trigger = {}", crate::keycode::hid_key_name(code as u8)); + return; + } + + if row >= 900 { + // KO edit: row = 900 + idx*10 + field (0=trig, 1=result) + let offset = row - 900; + let ko_idx = offset / 10; + let field = offset % 10; + let idx_valid = ko_idx < self.ko_data.len(); + if idx_valid { + if field == 0 { + self.ko_data[ko_idx][0] = code as u8; + self.status_msg = format!("KO #{} trigger = 0x{:02X}", ko_idx, code); + } else { + self.ko_data[ko_idx][2] = code as u8; + self.status_msg = format!("KO #{} result = 0x{:02X}", ko_idx, code); + } + } + return; + } + + if row >= 800 { + // New leader result + self.leader_new_result = code as u8; + self.leader_new_result_set = true; + self.status_msg = format!("Leader result = {}", crate::keycode::hid_key_name(code as u8)); + return; + } + + if row >= 700 { + // New leader sequence key (append) + let seq_not_full = self.leader_new_seq.len() < 4; + if seq_not_full { + self.leader_new_seq.push(code as u8); + let key_name = crate::keycode::hid_key_name(code as u8); + self.status_msg = format!("Leader seq + {}", key_name); + } + return; + } + + if row >= 600 { + // Leader result edit (existing) + let leader_idx = row - 600; + let idx_valid = leader_idx < self.leader_data.len(); + if idx_valid { + self.leader_data[leader_idx].result = code as u8; + self.status_msg = format!("Leader #{} result = 0x{:02X}", leader_idx, code); + } + return; + } + + if row >= 500 { + // Leader sequence key edit (existing) + // row = 500 + leader_idx*10 + seq_pos + let offset = row - 500; + let leader_idx = offset / 10; + let seq_pos = offset % 10; + let idx_valid = leader_idx < self.leader_data.len(); + if idx_valid { + let seq_valid = seq_pos < self.leader_data[leader_idx].sequence.len(); + if seq_valid { + self.leader_data[leader_idx].sequence[seq_pos] = code as u8; + self.status_msg = format!("Leader #{} key {} = 0x{:02X}", leader_idx, seq_pos, code); + } + } + return; + } + + if row >= 400 { + // New combo result mode + self.combo_new_result = code; + self.status_msg = format!("New combo result = 0x{:04X}", code); + return; + } + + if row >= 300 { + // Combo result edit mode + let combo_idx = row - 300; + let idx_valid = combo_idx < self.combo_data.len(); + if idx_valid { + self.combo_data[combo_idx].result = code; + self.status_msg = format!("Combo #{} result = 0x{:04X}", combo_idx, code); + } + return; + } + + if row >= 200 { + // Macro step mode + self.apply_macro_step(code); + return; + } + + if row >= 100 { + // TD mode + let td_idx = row - 100; + let idx_valid = td_idx < self.td_data.len(); + let col_valid = col < 4; + + if idx_valid && col_valid { + self.td_data[td_idx][col] = code; + self.status_msg = format!("TD {} action {} = 0x{:04X}", td_idx, col, code); + } + } else { + // Keymap mode - validate bounds BEFORE sending + let row_valid = row < self.keymap.len(); + let col_valid = row_valid && col < self.keymap[row].len(); + + if col_valid { + let layer = self.current_layer as u8; + let row_byte = row as u8; + let col_byte = col as u8; + let cmd = crate::protocol::cmd_set_key(layer, row_byte, col_byte, code); + self.bg_send(&cmd); + + self.keymap[row][col] = code; + self.status_msg = format!("[{},{}] = 0x{:04X}", row, col, code); + } else { + self.status_msg = format!("Invalid key position [{},{}]", row, col); + } + } + } + + pub(super) fn get_key(&self, row: usize, col: usize) -> u16 { + let row_data = self.keymap.get(row); + let cell_option = row_data.and_then(|r| r.get(col)); + let value = cell_option.copied(); + value.unwrap_or(0) + } +} diff --git a/original-src/ui_mod.rs b/original-src/ui_mod.rs new file mode 100644 index 0000000..ea634fa --- /dev/null +++ b/original-src/ui_mod.rs @@ -0,0 +1,277 @@ +mod background; +mod connection; +mod geometry; +mod helpers; +mod key_selector; +mod render; +mod tab_advanced; +mod tab_keymap; +mod tab_macros; +mod tab_settings; +mod tab_stats; +mod update; + +use std::sync::mpsc; +use crate::serial::{self, SharedSerial}; +use crate::layout::{self, KeycapPos}; +use crate::layout_remap::KeyboardLayout; + +// Instant doesn't exist in WASM - use a wrapper +#[cfg(not(target_arch = "wasm32"))] +type Instant = std::time::Instant; + +#[cfg(target_arch = "wasm32")] +#[derive(Clone, Copy)] +struct Instant(f64); + +#[cfg(target_arch = "wasm32")] +impl Instant { + fn now() -> Self { + let window = web_sys::window().unwrap(); + let performance = window.performance().unwrap(); + Instant(performance.now()) + } + fn elapsed(&self) -> std::time::Duration { + let now = Self::now(); + let ms = now.0 - self.0; + std::time::Duration::from_millis(ms as u64) + } +} + +/// Results from background serial operations. +pub(super) enum BgResult { + Connected(String, String, Vec, Vec>), + ConnectError(String), + Keymap(Vec>), + LayerNames(Vec), + TextLines(String, Vec), + #[allow(dead_code)] // constructed only in WASM builds + BinaryPayload(String, Vec), // tag, raw KR payload + LayoutJson(Vec), // physical key positions from firmware + OtaProgress(f32, String), // progress 0-1, status message + HeatmapData(Vec>, u32), // counts, max + Error(String), +} + +#[derive(PartialEq)] +pub(super) enum Tab { + Keymap, + Advanced, + Macros, + Stats, + Settings, +} + +pub struct KaSeApp { + pub(super) serial: SharedSerial, + pub(super) bg_tx: mpsc::Sender, + pub(super) bg_rx: mpsc::Receiver, + pub(super) busy: bool, + #[cfg(target_arch = "wasm32")] + pub(super) web_busy: std::rc::Rc>, + pub(super) tab: Tab, + + // Connection + /// Cached connection status — avoids serial.lock() in views. + /// Updated in poll_bg() when Connected/ConnectError is received. + pub(super) connected_cache: bool, + pub(super) firmware_version: String, + + // Keymap + pub(super) current_layer: usize, + pub(super) layer_names: Vec, + pub(super) keymap: Vec>, // rows x cols + pub(super) key_layout: Vec, + pub(super) editing_key: Option<(usize, usize)>, // (row, col) being edited + + // Keymap editing + pub(super) layer_rename: String, + + // Advanced + pub(super) td_lines: Vec, + pub(super) td_data: Vec<[u16; 4]>, // parsed tap dance slots + pub(super) combo_lines: Vec, + pub(super) combo_data: Vec, + /// None = pas de picking en cours. Some((combo_idx, slot)) = on attend un clic clavier. + /// combo_idx: usize::MAX = nouveau combo. slot: 0 = key1, 1 = key2. + pub(super) combo_picking: Option<(usize, u8)>, + pub(super) combo_new_r1: u8, + pub(super) combo_new_c1: u8, + pub(super) combo_new_r2: u8, + pub(super) combo_new_c2: u8, + pub(super) combo_new_result: u16, + pub(super) combo_new_key1_set: bool, + pub(super) combo_new_key2_set: bool, + pub(super) leader_lines: Vec, + pub(super) leader_data: Vec, + // Leader editing: new sequence being built + pub(super) leader_new_seq: Vec, // HID keycodes for the sequence + pub(super) leader_new_result: u8, + pub(super) leader_new_mod: u8, + pub(super) leader_new_result_set: bool, + pub(super) ko_lines: Vec, + pub(super) ko_data: Vec<[u8; 4]>, // parsed: [trigger, trig_mod, result, res_mod] + pub(super) ko_new_trig_key: u8, + pub(super) ko_new_trig_mod: u8, + pub(super) ko_new_res_key: u8, + pub(super) ko_new_res_mod: u8, + pub(super) ko_new_trig_set: bool, + pub(super) ko_new_res_set: bool, + pub(super) bt_lines: Vec, + pub(super) tama_lines: Vec, + pub(super) wpm_text: String, + pub(super) autoshift_status: String, + pub(super) tri_l1: String, + pub(super) tri_l2: String, + pub(super) tri_l3: String, + + // Macros + pub(super) macro_lines: Vec, + pub(super) macro_data: Vec, + pub(super) macro_slot: String, + pub(super) macro_name: String, + pub(super) macro_steps: String, + + // Stats / Heatmap + pub(super) keystats_lines: Vec, + pub(super) bigrams_lines: Vec, + pub(super) heatmap_data: Vec>, // rows x cols press counts + pub(super) heatmap_max: u32, + pub(super) heatmap_on: bool, + pub(super) heatmap_selected: Option<(usize, usize)>, // selected key for bigram view + pub(super) stats_dirty: bool, + + // Key selector + pub(super) key_search: String, + pub(super) mt_mod: u8, + pub(super) mt_key: u8, + pub(super) lt_layer: u8, + pub(super) lt_key: u8, + pub(super) hex_input: String, + + // Settings + pub(super) keyboard_layout: crate::layout_remap::KeyboardLayout, + + // OTA + pub(super) ota_path: String, + pub(super) ota_status: String, + pub(super) ota_firmware_data: Vec, + pub(super) ota_progress: f32, + pub(super) ota_releases: Vec<(String, String)>, // (tag_name, asset_url) + pub(super) ota_selected_release: usize, + + // Prog port flasher (native only) + #[cfg(not(target_arch = "wasm32"))] + pub(super) prog_port: String, + #[cfg(not(target_arch = "wasm32"))] + pub(super) prog_path: String, + #[cfg(not(target_arch = "wasm32"))] + pub(super) prog_ports_list: Vec, + + // Notifications + pub(super) notifications: Vec<(String, Instant)>, + + // Status + pub(super) status_msg: String, + pub(super) last_reconnect_poll: Instant, + pub(super) last_port_check: Instant, + pub(super) last_wpm_poll: Instant, + pub(super) last_stats_refresh: Instant, +} + +impl KaSeApp { + pub fn new(_cc: &eframe::CreationContext<'_>) -> Self { + let (bg_tx, bg_rx) = mpsc::channel(); + + let saved_settings = crate::settings::load(); + let keyboard_layout = KeyboardLayout::from_name(&saved_settings.keyboard_layout); + + Self { + serial: serial::new_shared(), + bg_tx, + bg_rx, + busy: false, + connected_cache: false, + #[cfg(target_arch = "wasm32")] + web_busy: std::rc::Rc::new(std::cell::Cell::new(false)), + tab: Tab::Keymap, + firmware_version: String::new(), + current_layer: 0, + layer_names: vec!["Layer 0".into()], + keymap: Vec::new(), + key_layout: layout::default_layout(), + editing_key: None, + layer_rename: String::new(), + td_lines: Vec::new(), + td_data: Vec::new(), + combo_lines: Vec::new(), + combo_data: Vec::new(), + combo_picking: None, + combo_new_r1: 0, + combo_new_c1: 0, + combo_new_r2: 0, + combo_new_c2: 0, + combo_new_result: 0, + combo_new_key1_set: false, + combo_new_key2_set: false, + leader_new_seq: Vec::new(), + leader_new_result: 0, + leader_new_mod: 0, + leader_new_result_set: false, + leader_lines: Vec::new(), + leader_data: Vec::new(), + ko_lines: Vec::new(), + ko_data: Vec::new(), + ko_new_trig_key: 0, + ko_new_trig_mod: 0, + ko_new_res_key: 0, + ko_new_res_mod: 0, + ko_new_trig_set: false, + ko_new_res_set: false, + bt_lines: Vec::new(), + macro_lines: Vec::new(), + macro_data: Vec::new(), + tama_lines: Vec::new(), + wpm_text: String::new(), + autoshift_status: String::new(), + tri_l1: "1".into(), + tri_l2: "2".into(), + tri_l3: "3".into(), + macro_slot: "0".into(), + macro_name: String::new(), + macro_steps: String::new(), + keystats_lines: Vec::new(), + bigrams_lines: Vec::new(), + heatmap_data: Vec::new(), + heatmap_max: 0, + heatmap_on: false, + heatmap_selected: None, + stats_dirty: true, + key_search: String::new(), + mt_mod: 0x02, // HID Left Shift + mt_key: 0x04, // HID 'A' + lt_layer: 1, + lt_key: 0x2C, // HID Space + hex_input: String::new(), + keyboard_layout, + #[cfg(not(target_arch = "wasm32"))] + prog_port: String::new(), + #[cfg(not(target_arch = "wasm32"))] + prog_path: String::new(), + #[cfg(not(target_arch = "wasm32"))] + prog_ports_list: Vec::new(), + notifications: Vec::new(), + ota_path: String::new(), + ota_status: String::new(), + ota_firmware_data: Vec::new(), + ota_progress: 0.0, + ota_releases: Vec::new(), + ota_selected_release: 0, + status_msg: "Searching KeSp...".into(), + last_reconnect_poll: Instant::now(), + last_port_check: Instant::now(), + last_wpm_poll: Instant::now(), + last_stats_refresh: Instant::now(), + } + } +} diff --git a/src/logic/binary_protocol.rs b/src/logic/binary_protocol.rs new file mode 100644 index 0000000..fa34b85 --- /dev/null +++ b/src/logic/binary_protocol.rs @@ -0,0 +1,259 @@ +/// KaSe Binary CDC Protocol v2 +/// Frame: KS(2) + cmd(1) + len(2 LE) + payload(N) + crc8(1) +/// Response: KR(2) + cmd(1) + status(1) + len(2 LE) + payload(N) + crc8(1) + +#[allow(dead_code)] +pub mod cmd { + // System + pub const VERSION: u8 = 0x01; + pub const FEATURES: u8 = 0x02; + pub const DFU: u8 = 0x03; + pub const PING: u8 = 0x04; + + // Keymap + pub const SETLAYER: u8 = 0x10; + pub const SETKEY: u8 = 0x11; + pub const KEYMAP_CURRENT: u8 = 0x12; + pub const KEYMAP_GET: u8 = 0x13; + pub const LAYER_INDEX: u8 = 0x14; + pub const LAYER_NAME: u8 = 0x15; + + // Layout + pub const SET_LAYOUT_NAME: u8 = 0x20; + pub const LIST_LAYOUTS: u8 = 0x21; + pub const GET_LAYOUT_JSON: u8 = 0x22; + + // Macros + pub const LIST_MACROS: u8 = 0x30; + pub const MACRO_ADD: u8 = 0x31; + pub const MACRO_ADD_SEQ: u8 = 0x32; + pub const MACRO_DELETE: u8 = 0x33; + + // Statistics + pub const KEYSTATS_BIN: u8 = 0x40; + pub const KEYSTATS_RESET: u8 = 0x42; + pub const BIGRAMS_BIN: u8 = 0x43; + pub const BIGRAMS_RESET: u8 = 0x45; + + // Tap Dance + pub const TD_SET: u8 = 0x50; + pub const TD_LIST: u8 = 0x51; + pub const TD_DELETE: u8 = 0x52; + + // Combos + pub const COMBO_SET: u8 = 0x60; + pub const COMBO_LIST: u8 = 0x61; + pub const COMBO_DELETE: u8 = 0x62; + + // Leader + pub const LEADER_SET: u8 = 0x70; + pub const LEADER_LIST: u8 = 0x71; + pub const LEADER_DELETE: u8 = 0x72; + + // Bluetooth + pub const BT_QUERY: u8 = 0x80; + pub const BT_SWITCH: u8 = 0x81; + pub const BT_PAIR: u8 = 0x82; + pub const BT_DISCONNECT: u8 = 0x83; + pub const BT_NEXT: u8 = 0x84; + pub const BT_PREV: u8 = 0x85; + + // Features + pub const AUTOSHIFT_TOGGLE: u8 = 0x90; + pub const KO_SET: u8 = 0x91; + pub const KO_LIST: u8 = 0x92; + pub const KO_DELETE: u8 = 0x93; + pub const WPM_QUERY: u8 = 0x94; + pub const TRILAYER_SET: u8 = 0x94; + + // Tamagotchi + pub const TAMA_QUERY: u8 = 0xA0; + pub const TAMA_ENABLE: u8 = 0xA1; + pub const TAMA_DISABLE: u8 = 0xA2; + pub const TAMA_FEED: u8 = 0xA3; + pub const TAMA_PLAY: u8 = 0xA4; + pub const TAMA_SLEEP: u8 = 0xA5; + pub const TAMA_MEDICINE: u8 = 0xA6; + pub const TAMA_SAVE: u8 = 0xA7; + + // OTA + pub const OTA_START: u8 = 0xF0; + pub const OTA_DATA: u8 = 0xF1; + pub const OTA_ABORT: u8 = 0xF2; +} + +#[allow(dead_code)] +pub mod status { + pub const OK: u8 = 0x00; + pub const ERR_UNKNOWN: u8 = 0x01; + pub const ERR_CRC: u8 = 0x02; + pub const ERR_INVALID: u8 = 0x03; + pub const ERR_RANGE: u8 = 0x04; + pub const ERR_BUSY: u8 = 0x05; + pub const ERR_OVERFLOW: u8 = 0x06; +} + +/// CRC-8/MAXIM (polynomial 0x31, init 0x00) +pub fn crc8(data: &[u8]) -> u8 { + let mut crc: u8 = 0x00; + for &b in data { + crc ^= b; + for _ in 0..8 { + crc = if crc & 0x80 != 0 { + (crc << 1) ^ 0x31 + } else { + crc << 1 + }; + } + } + crc +} + +/// Build a KS request frame. +pub fn ks_frame(cmd_id: u8, payload: &[u8]) -> Vec { + let len = payload.len() as u16; + let mut frame = Vec::with_capacity(6 + payload.len()); + frame.push(0x4B); // 'K' + frame.push(0x53); // 'S' + frame.push(cmd_id); + frame.push((len & 0xFF) as u8); + frame.push((len >> 8) as u8); + frame.extend_from_slice(payload); + frame.push(crc8(payload)); + frame +} + +/// Build MACRO_ADD_SEQ payload: [slot][name_len][name...][step_count][{kc,mod}...] +pub fn macro_add_seq_payload(slot: u8, name: &str, steps_hex: &str) -> Vec { + let name_bytes = name.as_bytes(); + let name_len = name_bytes.len().min(255) as u8; + + // Parse hex steps "06:01,FF:0A,19:01" into (kc, mod) pairs + let mut step_pairs: Vec<(u8, u8)> = Vec::new(); + if !steps_hex.is_empty() { + for part in steps_hex.split(',') { + let trimmed = part.trim(); + let kv: Vec<&str> = trimmed.split(':').collect(); + let has_two = kv.len() == 2; + if !has_two { + continue; + } + let kc = u8::from_str_radix(kv[0].trim(), 16).unwrap_or(0); + let md = u8::from_str_radix(kv[1].trim(), 16).unwrap_or(0); + step_pairs.push((kc, md)); + } + } + let step_count = step_pairs.len().min(255) as u8; + + let mut payload = Vec::new(); + payload.push(slot); + payload.push(name_len); + payload.extend_from_slice(&name_bytes[..name_len as usize]); + payload.push(step_count); + for (kc, md) in &step_pairs { + payload.push(*kc); + payload.push(*md); + } + payload +} + +/// Build MACRO_DELETE payload: [slot] +pub fn macro_delete_payload(slot: u8) -> Vec { + vec![slot] +} + +/// Build COMBO_SET payload: [index][r1][c1][r2][c2][result] +pub fn combo_set_payload(index: u8, r1: u8, c1: u8, r2: u8, c2: u8, result: u8) -> Vec { + vec![index, r1, c1, r2, c2, result] +} + +/// Build TD_SET payload: [index][a1][a2][a3][a4] +pub fn td_set_payload(index: u8, actions: &[u16; 4]) -> Vec { + vec![index, actions[0] as u8, actions[1] as u8, actions[2] as u8, actions[3] as u8] +} + +/// Build KO_SET payload: [index][trigger_key][trigger_mod][result_key][result_mod] +pub fn ko_set_payload(index: u8, trig_key: u8, trig_mod: u8, res_key: u8, res_mod: u8) -> Vec { + vec![index, trig_key, trig_mod, res_key, res_mod] +} + +/// Build LEADER_SET payload: [index][seq_len][seq...][result][result_mod] +pub fn leader_set_payload(index: u8, sequence: &[u8], result: u8, result_mod: u8) -> Vec { + let seq_len = sequence.len().min(4) as u8; + let mut payload = Vec::with_capacity(4 + sequence.len()); + payload.push(index); + payload.push(seq_len); + payload.extend_from_slice(&sequence[..seq_len as usize]); + payload.push(result); + payload.push(result_mod); + payload +} + +/// Parsed KR response. +#[derive(Debug)] +pub struct KrResponse { + #[allow(dead_code)] + pub cmd: u8, + pub status: u8, + pub payload: Vec, +} + +impl KrResponse { + pub fn is_ok(&self) -> bool { + self.status == status::OK + } + + pub fn status_name(&self) -> &str { + match self.status { + status::OK => "OK", + status::ERR_UNKNOWN => "ERR_UNKNOWN", + status::ERR_CRC => "ERR_CRC", + status::ERR_INVALID => "ERR_INVALID", + status::ERR_RANGE => "ERR_RANGE", + status::ERR_BUSY => "ERR_BUSY", + status::ERR_OVERFLOW => "ERR_OVERFLOW", + _ => "UNKNOWN", + } + } +} + +/// Parse a KR response from raw bytes. Returns (response, bytes_consumed). +pub fn parse_kr(data: &[u8]) -> Result<(KrResponse, usize), String> { + // Find KR magic + let pos = data + .windows(2) + .position(|w| w[0] == 0x4B && w[1] == 0x52) + .ok_or("No KR header found")?; + + if data.len() < pos + 7 { + return Err("Response too short".into()); + } + + let cmd = data[pos + 2]; + let status = data[pos + 3]; + let plen = data[pos + 4] as u16 | ((data[pos + 5] as u16) << 8); + let payload_start = pos + 6; + let payload_end = payload_start + plen as usize; + + if data.len() < payload_end + 1 { + return Err(format!( + "Incomplete response: need {} bytes, got {}", + payload_end + 1, + data.len() + )); + } + + let payload = data[payload_start..payload_end].to_vec(); + let expected_crc = data[payload_end]; + let actual_crc = crc8(&payload); + + if expected_crc != actual_crc { + return Err(format!( + "CRC mismatch: expected 0x{:02X}, got 0x{:02X}", + expected_crc, actual_crc + )); + } + + let consumed = payload_end + 1 - pos; + Ok((KrResponse { cmd, status, payload }, consumed)) +} diff --git a/src/logic/flasher.rs b/src/logic/flasher.rs new file mode 100644 index 0000000..930ab7b --- /dev/null +++ b/src/logic/flasher.rs @@ -0,0 +1,523 @@ +/// ESP32 ROM bootloader flasher via serial (CH340/CP2102 programming port). +/// Implements minimal SLIP-framed bootloader protocol for firmware flashing +/// without requiring esptool. + +#[cfg(not(target_arch = "wasm32"))] +use serialport::SerialPort; +#[cfg(not(target_arch = "wasm32"))] +use std::sync::mpsc; +#[cfg(not(target_arch = "wasm32"))] +use std::time::{Duration, Instant}; + +/// Progress message sent back to the UI during flashing. +/// Replaces the old `ui::BgResult::OtaProgress` variant. +#[cfg(not(target_arch = "wasm32"))] +pub enum FlashProgress { + OtaProgress(f32, String), +} + +// ==================== SLIP framing ==================== + +const SLIP_END: u8 = 0xC0; +const SLIP_ESC: u8 = 0xDB; +const SLIP_ESC_END: u8 = 0xDC; +const SLIP_ESC_ESC: u8 = 0xDD; + +#[cfg(not(target_arch = "wasm32"))] +fn slip_encode(data: &[u8]) -> Vec { + let mut frame = Vec::with_capacity(data.len() + 10); + frame.push(SLIP_END); + for &byte in data { + match byte { + SLIP_END => { + frame.push(SLIP_ESC); + frame.push(SLIP_ESC_END); + } + SLIP_ESC => { + frame.push(SLIP_ESC); + frame.push(SLIP_ESC_ESC); + } + _ => frame.push(byte), + } + } + frame.push(SLIP_END); + frame +} + +#[cfg(not(target_arch = "wasm32"))] +fn slip_decode(frame: &[u8]) -> Vec { + let mut data = Vec::with_capacity(frame.len()); + let mut escaped = false; + for &byte in frame { + if escaped { + match byte { + SLIP_ESC_END => data.push(SLIP_END), + SLIP_ESC_ESC => data.push(SLIP_ESC), + _ => data.push(byte), + } + escaped = false; + } else if byte == SLIP_ESC { + escaped = true; + } else if byte != SLIP_END { + data.push(byte); + } + } + data +} + +// ==================== Bootloader commands ==================== + +const CMD_SYNC: u8 = 0x08; +const CMD_CHANGE_BAUDRATE: u8 = 0x0F; +const CMD_SPI_ATTACH: u8 = 0x0D; +const CMD_FLASH_BEGIN: u8 = 0x02; +const CMD_FLASH_DATA: u8 = 0x03; +const CMD_FLASH_END: u8 = 0x04; + +const FLASH_BLOCK_SIZE: u32 = 1024; +/// Flash erase granularity — the ROM erases in minimum 4 KB units. +/// erase_size in FLASH_BEGIN must be aligned to this boundary. +const FLASH_SECTOR_SIZE: u32 = 0x1000; +const INITIAL_BAUD: u32 = 115200; +const FLASH_BAUD: u32 = 460800; + +#[cfg(not(target_arch = "wasm32"))] +fn xor_checksum(data: &[u8]) -> u32 { + let mut chk: u8 = 0xEF; + for &b in data { + chk ^= b; + } + chk as u32 +} + +/// Build a bootloader command packet (before SLIP encoding). +/// Format: [0x00][cmd][size:u16 LE][checksum:u32 LE][data...] +#[cfg(not(target_arch = "wasm32"))] +fn build_command(cmd: u8, data: &[u8], checksum: u32) -> Vec { + let size = data.len() as u16; + let mut pkt = Vec::with_capacity(8 + data.len()); + pkt.push(0x00); // direction: command + pkt.push(cmd); + pkt.push((size & 0xFF) as u8); + pkt.push((size >> 8) as u8); + pkt.push((checksum & 0xFF) as u8); + pkt.push(((checksum >> 8) & 0xFF) as u8); + pkt.push(((checksum >> 16) & 0xFF) as u8); + pkt.push(((checksum >> 24) & 0xFF) as u8); + pkt.extend_from_slice(data); + pkt +} + +/// Extract complete SLIP frames from a byte buffer. +/// Returns (frames, remaining_bytes_not_consumed). +#[cfg(not(target_arch = "wasm32"))] +fn extract_slip_frames(raw: &[u8]) -> Vec> { + let mut frames = Vec::new(); + let mut in_frame = false; + let mut current = Vec::new(); + + for &byte in raw { + if byte == SLIP_END { + if in_frame && !current.is_empty() { + // End of frame + frames.push(current.clone()); + current.clear(); + in_frame = false; + } else { + // Start of frame (or consecutive 0xC0) + in_frame = true; + current.clear(); + } + } else if in_frame { + current.push(byte); + } + // If !in_frame and byte != SLIP_END, it's garbage — skip + } + frames +} + +/// Send a command and receive a valid response. +/// Handles boot log garbage and multiple SYNC responses. +#[cfg(not(target_arch = "wasm32"))] +fn send_command( + port: &mut Box, + cmd: u8, + data: &[u8], + checksum: u32, + timeout_ms: u64, +) -> Result, String> { + let pkt = build_command(cmd, data, checksum); + let frame = slip_encode(&pkt); + + port.write_all(&frame) + .map_err(|e| format!("Write error: {}", e))?; + port.flush() + .map_err(|e| format!("Flush error: {}", e))?; + + // Read bytes and extract SLIP frames, looking for a valid response + let mut raw = Vec::new(); + let mut buf = [0u8; 512]; + let start = Instant::now(); + let timeout = Duration::from_millis(timeout_ms); + + loop { + let elapsed = start.elapsed(); + if elapsed > timeout { + let got = if raw.is_empty() { + "nothing".to_string() + } else { + format!("{} raw bytes, no valid response", raw.len()) + }; + return Err(format!("Response timeout (got {})", got)); + } + + let read_result = port.read(&mut buf); + match read_result { + Ok(n) if n > 0 => { + raw.extend_from_slice(&buf[..n]); + } + _ => { + std::thread::sleep(Duration::from_millis(1)); + if raw.is_empty() { + continue; + } + } + } + + // Try to find a valid response in accumulated data + let frames = extract_slip_frames(&raw); + for slip_data in &frames { + let decoded = slip_decode(slip_data); + + if decoded.len() < 8 { + continue; + } + + let direction = decoded[0]; + let resp_cmd = decoded[1]; + + if direction != 0x01 || resp_cmd != cmd { + continue; + } + + // ROM bootloader status is at offset 8 (right after 8-byte header) + // Format: [dir][cmd][size:u16][value:u32][status][error][pad][pad] + if decoded.len() >= 10 { + let status = decoded[8]; + let error = decoded[9]; + if status != 0 { + return Err(format!("Bootloader error: cmd=0x{:02X} status={}, error={} (0x{:02X})", + cmd, status, error, error)); + } + } + + return Ok(decoded); + } + } +} + +// ==================== Bootloader entry ==================== + +/// Toggle DTR/RTS to reset ESP32 into bootloader mode. +/// Standard auto-reset circuit: DTR→EN, RTS→GPIO0. +#[cfg(not(target_arch = "wasm32"))] +fn enter_bootloader(port: &mut Box) -> Result<(), String> { + // Hold GPIO0 low (RTS=true) while pulsing EN (DTR) + port.write_data_terminal_ready(false) + .map_err(|e| format!("DTR error: {}", e))?; + port.write_request_to_send(true) + .map_err(|e| format!("RTS error: {}", e))?; + std::thread::sleep(Duration::from_millis(100)); + + // Release EN (DTR=true) while keeping GPIO0 low + port.write_data_terminal_ready(true) + .map_err(|e| format!("DTR error: {}", e))?; + port.write_request_to_send(false) + .map_err(|e| format!("RTS error: {}", e))?; + std::thread::sleep(Duration::from_millis(50)); + + // Release all + port.write_data_terminal_ready(false) + .map_err(|e| format!("DTR error: {}", e))?; + + // Wait for ROM to boot and print its banner before we drain it. + // esptool uses DEFAULT_RESET_DELAY = 500 ms here. + // 200 ms is too short — the ROM isn't ready to accept SYNC yet, + // causing the first SYNC attempts to fail or receive garbage. + std::thread::sleep(Duration::from_millis(500)); + + // Drain any boot message + let _ = port.clear(serialport::ClearBuffer::All); + + Ok(()) +} + +// ==================== High-level commands ==================== + +#[cfg(not(target_arch = "wasm32"))] +fn sync(port: &mut Box) -> Result<(), String> { + // SYNC payload: [0x07, 0x07, 0x12, 0x20] + 32 x 0x55 + let mut payload = vec![0x07, 0x07, 0x12, 0x20]; + payload.extend_from_slice(&[0x55; 32]); + + for attempt in 0..10 { + let result = send_command(port, CMD_SYNC, &payload, 0, 500); + match result { + Ok(_) => return Ok(()), + Err(_) if attempt < 9 => { + // Drain any pending data before retry + let _ = port.clear(serialport::ClearBuffer::Input); + std::thread::sleep(Duration::from_millis(50)); + } + Err(e) => return Err(format!("SYNC failed after 10 attempts: {}", e)), + } + } + Err("SYNC failed".into()) +} + +/// Tell the bootloader to switch to a faster baud rate, then reconnect. +#[cfg(not(target_arch = "wasm32"))] +fn change_baudrate(port: &mut Box, new_baud: u32) -> Result<(), String> { + // Payload: [new_baud:u32 LE][old_baud:u32 LE] (old_baud=0 means "current") + let mut payload = Vec::with_capacity(8); + payload.extend_from_slice(&new_baud.to_le_bytes()); + payload.extend_from_slice(&0u32.to_le_bytes()); + + send_command(port, CMD_CHANGE_BAUDRATE, &payload, 0, 3000)?; + + // Switch host side to new baud + port.set_baud_rate(new_baud) + .map_err(|e| format!("Set baud error: {}", e))?; + + // Small delay for baud switch to take effect + std::thread::sleep(Duration::from_millis(50)); + let _ = port.clear(serialport::ClearBuffer::All); + + Ok(()) +} + +#[cfg(not(target_arch = "wasm32"))] +fn spi_attach(port: &mut Box) -> Result<(), String> { + let payload = [0u8; 8]; + send_command(port, CMD_SPI_ATTACH, &payload, 0, 3000)?; + Ok(()) +} + +#[cfg(not(target_arch = "wasm32"))] +fn flash_begin( + port: &mut Box, + offset: u32, + total_size: u32, + block_size: u32, +) -> Result<(), String> { + let num_blocks = (total_size + block_size - 1) / block_size; + + // erase_size must be rounded up to flash sector boundary (4 KB). + // Passing total_size directly causes the ROM to compute the wrong + // sector count — the last sector is never erased, writing into + // 0xFF-filled space and producing "invalid segment length 0xffffffff". + // This matches esptool's get_erase_size(offset, size) logic. + let erase_size = (total_size + FLASH_SECTOR_SIZE - 1) & !(FLASH_SECTOR_SIZE - 1); + + let mut payload = Vec::with_capacity(20); + // erase_size (sector-aligned, not raw file size) + payload.extend_from_slice(&erase_size.to_le_bytes()); + // num_blocks + payload.extend_from_slice(&num_blocks.to_le_bytes()); + // block_size + payload.extend_from_slice(&block_size.to_le_bytes()); + // offset + payload.extend_from_slice(&offset.to_le_bytes()); + // encrypted (ESP32-S3 requires this 5th field — 0 = not encrypted) + payload.extend_from_slice(&0u32.to_le_bytes()); + + // FLASH_BEGIN can take a while (flash erase) — long timeout + send_command(port, CMD_FLASH_BEGIN, &payload, 0, 30_000)?; + Ok(()) +} + +#[cfg(not(target_arch = "wasm32"))] +fn flash_data( + port: &mut Box, + seq: u32, + data: &[u8], +) -> Result<(), String> { + let data_len = data.len() as u32; + + let mut payload = Vec::with_capacity(16 + data.len()); + // data length + payload.extend_from_slice(&data_len.to_le_bytes()); + // sequence number + payload.extend_from_slice(&seq.to_le_bytes()); + // reserved (2 x u32) + payload.extend_from_slice(&0u32.to_le_bytes()); + payload.extend_from_slice(&0u32.to_le_bytes()); + // data + payload.extend_from_slice(data); + + let checksum = xor_checksum(data); + send_command(port, CMD_FLASH_DATA, &payload, checksum, 10_000)?; + Ok(()) +} + +#[cfg(not(target_arch = "wasm32"))] +fn flash_end(port: &mut Box, reboot: bool) -> Result<(), String> { + let flag: u32 = if reboot { 0 } else { 1 }; + let payload = flag.to_le_bytes(); + // FLASH_END might not get a response if device reboots + let _ = send_command(port, CMD_FLASH_END, &payload, 0, 2000); + + if reboot { + // Hard reset: toggle RTS to pulse EN pin (like esptool --after hard_reset) + std::thread::sleep(Duration::from_millis(100)); + port.write_request_to_send(true) + .map_err(|e| format!("RTS error: {}", e))?; + std::thread::sleep(Duration::from_millis(100)); + port.write_request_to_send(false) + .map_err(|e| format!("RTS error: {}", e))?; + } + + Ok(()) +} + +// ==================== Main entry point ==================== + +/// Flash firmware to ESP32 via programming port (CH340/CP2102). +/// Sends progress updates via the channel as (progress_0_to_1, status_message). +#[cfg(not(target_arch = "wasm32"))] +pub fn flash_firmware( + port_name: &str, + firmware: &[u8], + offset: u32, + tx: &mpsc::Sender, +) -> Result<(), String> { + let send_progress = |progress: f32, msg: String| { + let _ = tx.send(FlashProgress::OtaProgress(progress, msg)); + }; + + send_progress(0.0, "Opening port...".into()); + + let builder = serialport::new(port_name, INITIAL_BAUD); + let builder_timeout = builder.timeout(Duration::from_millis(500)); + let mut port = builder_timeout.open() + .map_err(|e| format!("Cannot open {}: {}", port_name, e))?; + + // Step 1: Enter bootloader + send_progress(0.0, "Resetting into bootloader...".into()); + enter_bootloader(&mut port)?; + + // Step 2: Sync at 115200 + send_progress(0.0, "Syncing with bootloader...".into()); + sync(&mut port)?; + send_progress(0.02, "Bootloader sync OK".into()); + + // Step 3: Switch to 460800 baud for faster flashing + send_progress(0.03, format!("Switching to {} baud...", FLASH_BAUD)); + change_baudrate(&mut port, FLASH_BAUD)?; + send_progress(0.04, format!("Baud: {}", FLASH_BAUD)); + + // Step 4: SPI attach + send_progress(0.05, "Attaching SPI flash...".into()); + spi_attach(&mut port)?; + + // Step 5: Flash begin (this erases the flash — can take several seconds) + let total_size = firmware.len() as u32; + let num_blocks = (total_size + FLASH_BLOCK_SIZE - 1) / FLASH_BLOCK_SIZE; + send_progress(0.05, format!("Erasing flash ({} KB)...", total_size / 1024)); + flash_begin(&mut port, offset, total_size, FLASH_BLOCK_SIZE)?; + send_progress(0.10, "Flash erased, writing...".into()); + + // Step 6: Flash data blocks + for (i, chunk) in firmware.chunks(FLASH_BLOCK_SIZE as usize).enumerate() { + // Pad last block to block_size + let mut block = chunk.to_vec(); + let pad_needed = FLASH_BLOCK_SIZE as usize - block.len(); + if pad_needed > 0 { + block.extend(std::iter::repeat(0xFF).take(pad_needed)); + } + + flash_data(&mut port, i as u32, &block)?; + + let blocks_done = (i + 1) as f32; + let total_blocks = num_blocks as f32; + let progress = 0.10 + 0.85 * (blocks_done / total_blocks); + let msg = format!("Writing block {}/{} ({} KB / {} KB)", + i + 1, num_blocks, + ((i + 1) as u32 * FLASH_BLOCK_SIZE).min(total_size) / 1024, + total_size / 1024); + send_progress(progress, msg); + } + + // Step 7: Flash end + reboot + send_progress(0.97, "Finalizing...".into()); + flash_end(&mut port, true)?; + + send_progress(1.0, format!("Flash OK — {} KB written at 0x{:X}", total_size / 1024, offset)); + Ok(()) +} + +// ==================== Tests ==================== + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn slip_encode_no_special() { + let data = vec![0x01, 0x02, 0x03]; + let encoded = slip_encode(&data); + assert_eq!(encoded, vec![0xC0, 0x01, 0x02, 0x03, 0xC0]); + } + + #[test] + fn slip_encode_with_end_byte() { + let data = vec![0x01, 0xC0, 0x03]; + let encoded = slip_encode(&data); + assert_eq!(encoded, vec![0xC0, 0x01, 0xDB, 0xDC, 0x03, 0xC0]); + } + + #[test] + fn slip_encode_with_esc_byte() { + let data = vec![0x01, 0xDB, 0x03]; + let encoded = slip_encode(&data); + assert_eq!(encoded, vec![0xC0, 0x01, 0xDB, 0xDD, 0x03, 0xC0]); + } + + #[test] + fn slip_roundtrip() { + let original = vec![0xC0, 0xDB, 0x00, 0xFF, 0xC0]; + let encoded = slip_encode(&original); + let decoded = slip_decode(&encoded); + assert_eq!(decoded, original); + } + + #[test] + fn xor_checksum_basic() { + let data = vec![0x01, 0x02, 0x03]; + let chk = xor_checksum(&data); + let expected = 0xEF ^ 0x01 ^ 0x02 ^ 0x03; + assert_eq!(chk, expected as u32); + } + + #[test] + fn xor_checksum_empty() { + let chk = xor_checksum(&[]); + assert_eq!(chk, 0xEF); + } + + #[test] + fn build_command_format() { + let data = vec![0xAA, 0xBB]; + let pkt = build_command(0x08, &data, 0x12345678); + assert_eq!(pkt[0], 0x00); // direction + assert_eq!(pkt[1], 0x08); // command + assert_eq!(pkt[2], 0x02); // size low + assert_eq!(pkt[3], 0x00); // size high + assert_eq!(pkt[4], 0x78); // checksum byte 0 + assert_eq!(pkt[5], 0x56); // checksum byte 1 + assert_eq!(pkt[6], 0x34); // checksum byte 2 + assert_eq!(pkt[7], 0x12); // checksum byte 3 + assert_eq!(pkt[8], 0xAA); // data + assert_eq!(pkt[9], 0xBB); + } +} diff --git a/src/logic/keycode.rs b/src/logic/keycode.rs new file mode 100644 index 0000000..6709ee3 --- /dev/null +++ b/src/logic/keycode.rs @@ -0,0 +1,390 @@ +/// Decode a raw 16-bit keycode into a human-readable string. +/// +/// Covers all KaSe firmware keycode ranges: HID basic keys, layer switches, +/// macros, Bluetooth, one-shot, mod-tap, layer-tap, tap-dance, and more. +pub fn decode_keycode(raw: u16) -> String { + // --- HID basic keycodes 0x00..=0xE7 --- + if raw <= 0x00E7 { + return hid_key_name(raw as u8); + } + + // --- MO (Momentary Layer): 0x0100..=0x0A00, low byte == 0 --- + if raw >= 0x0100 && raw <= 0x0A00 && (raw & 0xFF) == 0 { + let layer = (raw >> 8) - 1; + return format!("MO {layer}"); + } + + // --- TO (Toggle Layer): 0x0B00..=0x1400, low byte == 0 --- + if raw >= 0x0B00 && raw <= 0x1400 && (raw & 0xFF) == 0 { + let layer = (raw >> 8) - 0x0B; + return format!("TO {layer}"); + } + + // --- MACRO: 0x1500..=0x2800, low byte == 0 --- + if raw >= 0x1500 && raw <= 0x2800 && (raw & 0xFF) == 0 { + let idx = (raw >> 8) - 0x14; + return format!("M{idx}"); + } + + // --- BT keycodes --- + match raw { + 0x2900 => return "BT Next".into(), + 0x2A00 => return "BT Prev".into(), + 0x2B00 => return "BT Pair".into(), + 0x2C00 => return "BT Disc".into(), + 0x2E00 => return "USB/BT".into(), + 0x2F00 => return "BT On/Off".into(), + _ => {} + } + + // --- OSM (One-Shot Mod): 0x3000..=0x30FF --- + if raw >= 0x3000 && raw <= 0x30FF { + let mods = (raw & 0xFF) as u8; + return format!("OSM {}", mod_name(mods)); + } + + // --- OSL (One-Shot Layer): 0x3100..=0x310F --- + if raw >= 0x3100 && raw <= 0x310F { + let layer = raw & 0x0F; + return format!("OSL {layer}"); + } + + // --- Fixed special codes --- + match raw { + 0x3200 => return "Caps Word".into(), + 0x3300 => return "Repeat".into(), + 0x3400 => return "Leader".into(), + 0x3500 => return "Feed".into(), + 0x3600 => return "Play".into(), + 0x3700 => return "Sleep".into(), + 0x3800 => return "Meds".into(), + 0x3900 => return "GEsc".into(), + 0x3A00 => return "Layer Lock".into(), + 0x3C00 => return "AS Toggle".into(), + _ => {} + } + + // --- KO (Key Override) slots: 0x3D00..=0x3DFF --- + if raw >= 0x3D00 && raw <= 0x3DFF { + let slot = raw & 0xFF; + return format!("KO {slot}"); + } + + // --- LT (Layer-Tap): 0x4000..=0x4FFF --- + // layout: 0x4LKK where L = layer (0..F), KK = HID keycode + if raw >= 0x4000 && raw <= 0x4FFF { + let layer = (raw >> 8) & 0x0F; + let kc = (raw & 0xFF) as u8; + return format!("LT {} {}", layer, hid_key_name(kc)); + } + + // --- MT (Mod-Tap): 0x5000..=0x5FFF --- + // layout: 0x5MKK where M = mod nibble (4 bits), KK = HID keycode + if raw >= 0x5000 && raw <= 0x5FFF { + let mods = ((raw >> 8) & 0x0F) as u8; + let kc = (raw & 0xFF) as u8; + return format!("MT {} {}", mod_name(mods), hid_key_name(kc)); + } + + // --- TD (Tap Dance): 0x6000..=0x6FFF --- + if raw >= 0x6000 && raw <= 0x6FFF { + let index = (raw >> 8) & 0x0F; + return format!("TD {index}"); + } + + // --- Unknown --- + format!("0x{raw:04X}") +} + +/// Decode a modifier bitmask into a human-readable string. +/// +/// Bits: 0x01=Ctrl, 0x02=Shift, 0x04=Alt, 0x08=GUI, +/// 0x10=RCtrl, 0x20=RShift, 0x40=RAlt, 0x80=RGUI. +/// Multiple modifiers are joined with "+". +pub fn mod_name(mod_mask: u8) -> String { + let mut parts = Vec::new(); + if mod_mask & 0x01 != 0 { parts.push("Ctrl"); } + if mod_mask & 0x02 != 0 { parts.push("Shift"); } + if mod_mask & 0x04 != 0 { parts.push("Alt"); } + if mod_mask & 0x08 != 0 { parts.push("GUI"); } + if mod_mask & 0x10 != 0 { parts.push("RCtrl"); } + if mod_mask & 0x20 != 0 { parts.push("RShift"); } + if mod_mask & 0x40 != 0 { parts.push("RAlt"); } + if mod_mask & 0x80 != 0 { parts.push("RGUI"); } + if parts.is_empty() { + format!("0x{mod_mask:02X}") + } else { + parts.join("+") + } +} + +/// Map a single HID usage code (0x00..=0xE7) to a short readable name. +pub fn hid_key_name(code: u8) -> String { + match code { + // No key / transparent + 0x00 => "None", + // 0x01 = ErrorRollOver, 0x02 = POSTFail, 0x03 = ErrorUndefined (not user-facing) + 0x01 => "ErrRollOver", + 0x02 => "POSTFail", + 0x03 => "ErrUndef", + + // Letters + 0x04 => "A", + 0x05 => "B", + 0x06 => "C", + 0x07 => "D", + 0x08 => "E", + 0x09 => "F", + 0x0A => "G", + 0x0B => "H", + 0x0C => "I", + 0x0D => "J", + 0x0E => "K", + 0x0F => "L", + 0x10 => "M", + 0x11 => "N", + 0x12 => "O", + 0x13 => "P", + 0x14 => "Q", + 0x15 => "R", + 0x16 => "S", + 0x17 => "T", + 0x18 => "U", + 0x19 => "V", + 0x1A => "W", + 0x1B => "X", + 0x1C => "Y", + 0x1D => "Z", + + // Number row + 0x1E => "1", + 0x1F => "2", + 0x20 => "3", + 0x21 => "4", + 0x22 => "5", + 0x23 => "6", + 0x24 => "7", + 0x25 => "8", + 0x26 => "9", + 0x27 => "0", + + // Common control keys + 0x28 => "Enter", + 0x29 => "Esc", + 0x2A => "Backspace", + 0x2B => "Tab", + 0x2C => "Space", + + // Punctuation / symbols + 0x2D => "-", + 0x2E => "=", + 0x2F => "[", + 0x30 => "]", + 0x31 => "\\", + 0x32 => "Europe1", + 0x33 => ";", + 0x34 => "'", + 0x35 => "`", + 0x36 => ",", + 0x37 => ".", + 0x38 => "/", + + // Caps Lock + 0x39 => "Caps Lock", + + // Function keys + 0x3A => "F1", + 0x3B => "F2", + 0x3C => "F3", + 0x3D => "F4", + 0x3E => "F5", + 0x3F => "F6", + 0x40 => "F7", + 0x41 => "F8", + 0x42 => "F9", + 0x43 => "F10", + 0x44 => "F11", + 0x45 => "F12", + + // Navigation / editing cluster + 0x46 => "PrtSc", + 0x47 => "ScrLk", + 0x48 => "Pause", + 0x49 => "Ins", + 0x4A => "Home", + 0x4B => "PgUp", + 0x4C => "Del", + 0x4D => "End", + 0x4E => "PgDn", + + // Arrow keys + 0x4F => "Right", + 0x50 => "Left", + 0x51 => "Down", + 0x52 => "Up", + + // Keypad + 0x53 => "NumLk", + 0x54 => "Num /", + 0x55 => "Num *", + 0x56 => "Num -", + 0x57 => "Num +", + 0x58 => "Num Enter", + 0x59 => "Num 1", + 0x5A => "Num 2", + 0x5B => "Num 3", + 0x5C => "Num 4", + 0x5D => "Num 5", + 0x5E => "Num 6", + 0x5F => "Num 7", + 0x60 => "Num 8", + 0x61 => "Num 9", + 0x62 => "Num 0", + 0x63 => "Num .", + 0x64 => "Europe2", + 0x65 => "Menu", + 0x66 => "Power", + 0x67 => "Num =", + + // F13-F24 + 0x68 => "F13", + 0x69 => "F14", + 0x6A => "F15", + 0x6B => "F16", + 0x6C => "F17", + 0x6D => "F18", + 0x6E => "F19", + 0x6F => "F20", + 0x70 => "F21", + 0x71 => "F22", + 0x72 => "F23", + 0x73 => "F24", + + // Misc system keys + 0x74 => "Execute", + 0x75 => "Help", + 0x76 => "Menu2", + 0x77 => "Select", + 0x78 => "Stop", + 0x79 => "Again", + 0x7A => "Undo", + 0x7B => "Cut", + 0x7C => "Copy", + 0x7D => "Paste", + 0x7E => "Find", + 0x7F => "Mute", + 0x80 => "Vol Up", + 0x81 => "Vol Down", + + // Locking keys + 0x82 => "Lock Caps", + 0x83 => "Lock Num", + 0x84 => "Lock Scroll", + + // Keypad extras + 0x85 => "Num ,", + 0x86 => "Num =2", + + // International / Kanji + 0x87 => "Kanji1", + 0x88 => "Kanji2", + 0x89 => "Kanji3", + 0x8A => "Kanji4", + 0x8B => "Kanji5", + 0x8C => "Kanji6", + 0x8D => "Kanji7", + 0x8E => "Kanji8", + 0x8F => "Kanji9", + + // Language keys + 0x90 => "Lang1", + 0x91 => "Lang2", + 0x92 => "Lang3", + 0x93 => "Lang4", + 0x94 => "Lang5", + 0x95 => "Lang6", + 0x96 => "Lang7", + 0x97 => "Lang8", + 0x98 => "Lang9", + + // Rare system keys + 0x99 => "Alt Erase", + 0x9A => "SysReq", + 0x9B => "Cancel", + 0x9C => "Clear", + 0x9D => "Prior", + 0x9E => "Return", + 0x9F => "Separator", + 0xA0 => "Out", + 0xA1 => "Oper", + 0xA2 => "Clear Again", + 0xA3 => "CrSel", + 0xA4 => "ExSel", + + // 0xA5..=0xAF reserved / not defined in standard HID tables + + // Extended keypad + 0xB0 => "Num 00", + 0xB1 => "Num 000", + 0xB2 => "Thousands Sep", + 0xB3 => "Decimal Sep", + 0xB4 => "Currency", + 0xB5 => "Currency Sub", + 0xB6 => "Num (", + 0xB7 => "Num )", + 0xB8 => "Num {", + 0xB9 => "Num }", + 0xBA => "Num Tab", + 0xBB => "Num Bksp", + 0xBC => "Num A", + 0xBD => "Num B", + 0xBE => "Num C", + 0xBF => "Num D", + 0xC0 => "Num E", + 0xC1 => "Num F", + 0xC2 => "Num XOR", + 0xC3 => "Num ^", + 0xC4 => "Num %", + 0xC5 => "Num <", + 0xC6 => "Num >", + 0xC7 => "Num &", + 0xC8 => "Num &&", + 0xC9 => "Num |", + 0xCA => "Num ||", + 0xCB => "Num :", + 0xCC => "Num #", + 0xCD => "Num Space", + 0xCE => "Num @", + 0xCF => "Num !", + 0xD0 => "Num M Store", + 0xD1 => "Num M Recall", + 0xD2 => "Num M Clear", + 0xD3 => "Num M+", + 0xD4 => "Num M-", + 0xD5 => "Num M*", + 0xD6 => "Num M/", + 0xD7 => "Num +/-", + 0xD8 => "Num Clear", + 0xD9 => "Num ClrEntry", + 0xDA => "Num Binary", + 0xDB => "Num Octal", + 0xDC => "Num Decimal", + 0xDD => "Num Hex", + + // 0xDE..=0xDF reserved + + // Modifier keys + 0xE0 => "LCtrl", + 0xE1 => "LShift", + 0xE2 => "LAlt", + 0xE3 => "LGUI", + 0xE4 => "RCtrl", + 0xE5 => "RShift", + 0xE6 => "RAlt", + 0xE7 => "RGUI", + + // Anything else in 0x00..=0xFF not covered above + _ => return format!("0x{code:02X}"), + } + .into() +} diff --git a/src/logic/layout.rs b/src/logic/layout.rs new file mode 100644 index 0000000..3a093b1 --- /dev/null +++ b/src/logic/layout.rs @@ -0,0 +1,286 @@ +use serde_json::Value; + +/// A keycap with computed absolute position. +#[derive(Clone, Debug, PartialEq)] +pub struct KeycapPos { + pub row: usize, + pub col: usize, + pub x: f32, + pub y: f32, + pub w: f32, + pub h: f32, + pub angle: f32, // degrees +} + +const KEY_SIZE: f32 = 50.0; +const KEY_GAP: f32 = 4.0; + +/// Parse a layout JSON string into absolute key positions. +pub fn parse_json(json: &str) -> Result, String> { + let val: Value = serde_json::from_str(json) + .map_err(|e| format!("Invalid layout JSON: {}", e))?; + let mut keys = Vec::new(); + walk(&val, 0.0, 0.0, 0.0, &mut keys); + if keys.is_empty() { + return Err("No keys found in layout".into()); + } + Ok(keys) +} + +/// Default layout embedded at compile time. +pub fn default_layout() -> Vec { + let json = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/default.json")); + parse_json(json).unwrap_or_default() +} + +fn walk(node: &Value, ox: f32, oy: f32, parent_angle: f32, out: &mut Vec) { + let obj = match node.as_object() { + Some(o) => o, + None => return, + }; + + for (key, val) in obj { + let key_str = key.as_str(); + match key_str { + "Group" => walk_group(val, ox, oy, parent_angle, out), + "Line" => walk_line(val, ox, oy, parent_angle, out), + "Keycap" => walk_keycap(val, ox, oy, parent_angle, out), + _ => {} + } + } +} + +fn parse_margin(val: &Value) -> (f32, f32, f32, f32) { + let as_str = val.as_str(); + if let Some(s) = as_str { + let split = s.split(','); + let parts: Vec = split + .filter_map(|p| { + let trimmed = p.trim(); + let parsed = trimmed.parse().ok(); + parsed + }) + .collect(); + let has_four_parts = parts.len() == 4; + if has_four_parts { + return (parts[0], parts[1], parts[2], parts[3]); + } + } + (0.0, 0.0, 0.0, 0.0) +} + +fn parse_angle(val: &Value) -> f32 { + let rotate_transform = val.get("RotateTransform"); + let angle_val = rotate_transform + .and_then(|rt| rt.get("Angle")); + let angle_f64 = angle_val + .and_then(|a| a.as_f64()); + let angle = angle_f64.unwrap_or(0.0) as f32; + angle +} + +fn walk_group(val: &Value, ox: f32, oy: f32, parent_angle: f32, out: &mut Vec) { + let obj = match val.as_object() { + Some(o) => o, + None => return, + }; + + let margin_val = obj.get("Margin"); + let (ml, mt, _, _) = margin_val + .map(parse_margin) + .unwrap_or_default(); + let transform_val = obj.get("RenderTransform"); + let angle = transform_val + .map(parse_angle) + .unwrap_or(0.0); + + let gx = ox + ml; + let gy = oy + mt; + + let children_val = obj.get("Children"); + let children_array = children_val + .and_then(|c| c.as_array()); + if let Some(children) = children_array { + let combined_angle = parent_angle + angle; + for child in children { + walk(child, gx, gy, combined_angle, out); + } + } +} + +fn walk_line(val: &Value, ox: f32, oy: f32, parent_angle: f32, out: &mut Vec) { + let obj = match val.as_object() { + Some(o) => o, + None => return, + }; + + let margin_val = obj.get("Margin"); + let (ml, mt, _, _) = margin_val + .map(parse_margin) + .unwrap_or_default(); + let transform_val = obj.get("RenderTransform"); + let angle = transform_val + .map(parse_angle) + .unwrap_or(0.0); + let total_angle = parent_angle + angle; + + let orientation_val = obj.get("Orientation"); + let orientation_str = orientation_val + .and_then(|o| o.as_str()) + .unwrap_or("Vertical"); + let horiz = orientation_str == "Horizontal"; + + let lx = ox + ml; + let ly = oy + mt; + + let rad = total_angle.to_radians(); + let cos_a = rad.cos(); + let sin_a = rad.sin(); + + let mut cursor = 0.0f32; + + let children_val = obj.get("Children"); + let children_array = children_val + .and_then(|c| c.as_array()); + if let Some(children) = children_array { + for child in children { + let (cx, cy) = if horiz { + let x = lx + cursor * cos_a; + let y = ly + cursor * sin_a; + (x, y) + } else { + let x = lx - cursor * sin_a; + let y = ly + cursor * cos_a; + (x, y) + }; + + let child_size = measure(child, horiz); + walk(child, cx, cy, total_angle, out); + cursor += child_size; + } + } +} + +/// Measure a child's extent along the parent's main axis. +fn measure(node: &Value, horiz: bool) -> f32 { + let obj = match node.as_object() { + Some(o) => o, + None => return 0.0, + }; + + for (key, val) in obj { + let key_str = key.as_str(); + match key_str { + "Keycap" => { + let width_val = val.get("Width"); + let width_f64 = width_val + .and_then(|v| v.as_f64()); + let w = width_f64.unwrap_or(KEY_SIZE as f64) as f32; + let extent = if horiz { + w + KEY_GAP + } else { + KEY_SIZE + KEY_GAP + }; + return extent; + } + "Line" => { + let sub = match val.as_object() { + Some(o) => o, + None => return 0.0, + }; + let sub_orientation = sub.get("Orientation"); + let sub_orient_str = sub_orientation + .and_then(|o| o.as_str()) + .unwrap_or("Vertical"); + let sub_horiz = sub_orient_str == "Horizontal"; + + let sub_children_val = sub.get("Children"); + let sub_children_array = sub_children_val + .and_then(|c| c.as_array()); + let children = sub_children_array + .map(|a| a.as_slice()) + .unwrap_or(&[]); + + let same_direction = sub_horiz == horiz; + let content: f32 = if same_direction { + // Same direction: sum + children + .iter() + .map(|c| measure(c, sub_horiz)) + .sum() + } else { + // Cross direction: max + children + .iter() + .map(|c| measure(c, horiz)) + .fold(0.0f32, f32::max) + }; + + return content; + } + "Group" => { + let sub = match val.as_object() { + Some(o) => o, + None => return 0.0, + }; + let sub_children_val = sub.get("Children"); + let sub_children_array = sub_children_val + .and_then(|c| c.as_array()); + let children = sub_children_array + .map(|a| a.as_slice()) + .unwrap_or(&[]); + let max_extent = children + .iter() + .map(|c| measure(c, horiz)) + .fold(0.0f32, f32::max); + return max_extent; + } + _ => {} + } + } + 0.0 +} + +fn walk_keycap(val: &Value, ox: f32, oy: f32, parent_angle: f32, out: &mut Vec) { + let obj = match val.as_object() { + Some(o) => o, + None => return, + }; + + let col_val = obj.get("Column"); + let col_u64 = col_val + .and_then(|v| v.as_u64()); + let col = col_u64.unwrap_or(0) as usize; + + let row_val = obj.get("Row"); + let row_u64 = row_val + .and_then(|v| v.as_u64()); + let row = row_u64.unwrap_or(0) as usize; + + let width_val = obj.get("Width"); + let width_f64 = width_val + .and_then(|v| v.as_f64()); + let w = width_f64.unwrap_or(KEY_SIZE as f64) as f32; + + let margin_val = obj.get("Margin"); + let (ml, mt, _, _) = margin_val + .map(parse_margin) + .unwrap_or_default(); + + let transform_val = obj.get("RenderTransform"); + let angle = transform_val + .map(parse_angle) + .unwrap_or(0.0); + + let total_angle = parent_angle + angle; + + out.push(KeycapPos { + row, + col, + x: ox + ml, + y: oy + mt, + w, + h: KEY_SIZE, + angle: total_angle, + }); +} diff --git a/src/logic/layout_remap.rs b/src/logic/layout_remap.rs new file mode 100644 index 0000000..023f97d --- /dev/null +++ b/src/logic/layout_remap.rs @@ -0,0 +1,339 @@ +/// Remaps HID key names to their visual representation based on the user's +/// keyboard layout (language). Ported from the C# `KeyConverter.LayoutOverrides`. + +// Display implementation for the keyboard layout picker in settings. +impl std::fmt::Display for KeyboardLayout { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.name()) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum KeyboardLayout { + Qwerty, + Azerty, + Qwertz, + Dvorak, + Colemak, + Bepo, + QwertyEs, + QwertyPt, + QwertyIt, + QwertyNordic, + QwertyBr, + QwertyTr, + QwertyUk, +} + +impl KeyboardLayout { + /// All known layout variants. + pub fn all() -> &'static [KeyboardLayout] { + &[ + KeyboardLayout::Qwerty, + KeyboardLayout::Azerty, + KeyboardLayout::Qwertz, + KeyboardLayout::Dvorak, + KeyboardLayout::Colemak, + KeyboardLayout::Bepo, + KeyboardLayout::QwertyEs, + KeyboardLayout::QwertyPt, + KeyboardLayout::QwertyIt, + KeyboardLayout::QwertyNordic, + KeyboardLayout::QwertyBr, + KeyboardLayout::QwertyTr, + KeyboardLayout::QwertyUk, + ] + } + + /// Human-readable display name. + pub fn name(&self) -> &str { + match self { + KeyboardLayout::Qwerty => "QWERTY", + KeyboardLayout::Azerty => "AZERTY", + KeyboardLayout::Qwertz => "QWERTZ", + KeyboardLayout::Dvorak => "DVORAK", + KeyboardLayout::Colemak => "COLEMAK", + KeyboardLayout::Bepo => "BEPO", + KeyboardLayout::QwertyEs => "QWERTY_ES", + KeyboardLayout::QwertyPt => "QWERTY_PT", + KeyboardLayout::QwertyIt => "QWERTY_IT", + KeyboardLayout::QwertyNordic => "QWERTY_NORDIC", + KeyboardLayout::QwertyBr => "QWERTY_BR", + KeyboardLayout::QwertyTr => "QWERTY_TR", + KeyboardLayout::QwertyUk => "QWERTY_UK", + } + } + + /// Parse a layout name (case-insensitive). Falls back to `Qwerty`. + pub fn from_name(s: &str) -> Self { + match s.to_ascii_uppercase().as_str() { + "QWERTY" => KeyboardLayout::Qwerty, + "AZERTY" => KeyboardLayout::Azerty, + "QWERTZ" => KeyboardLayout::Qwertz, + "DVORAK" => KeyboardLayout::Dvorak, + "COLEMAK" => KeyboardLayout::Colemak, + "BEPO" | "BÉPO" => KeyboardLayout::Bepo, + "QWERTY_ES" => KeyboardLayout::QwertyEs, + "QWERTY_PT" => KeyboardLayout::QwertyPt, + "QWERTY_IT" => KeyboardLayout::QwertyIt, + "QWERTY_NORDIC" => KeyboardLayout::QwertyNordic, + "QWERTY_BR" => KeyboardLayout::QwertyBr, + "QWERTY_TR" => KeyboardLayout::QwertyTr, + "QWERTY_UK" => KeyboardLayout::QwertyUk, + _ => KeyboardLayout::Qwerty, + } + } +} + +/// Given a `layout` and an HID key name (e.g. `"A"`, `"COMMA"`, `"SEMICOLON"`), +/// returns the visual label for that key on the given layout, or `None` when no +/// override exists (meaning the default / QWERTY label applies). +/// +/// The lookup is **case-insensitive** on `hid_name`. +pub fn remap_key_label(layout: &KeyboardLayout, hid_name: &str) -> Option<&'static str> { + // Normalise to uppercase for matching. + let key = hid_name.to_ascii_uppercase(); + let key = key.as_str(); + + match layout { + // QWERTY has no overrides — it *is* the reference layout. + KeyboardLayout::Qwerty => None, + + KeyboardLayout::Azerty => match key { + "COMMA" | "COMM" | "COMA" => Some(";"), + "SEMICOLON" | "SCOLON" | "SCLN" => Some(","), + "PERIOD" | "DOT" => Some(":"), + "SLASH" | "SLSH" | "/" => Some("!"), + "M" => Some(","), + "W" => Some("Z"), + "Z" => Some("W"), + "Q" => Some("A"), + "A" => Some("Q"), + "," => Some(";"), + "." => Some(":"), + ";" => Some("M"), + "-" | "MINUS" | "MIN" => Some(")"), + "BRACKET_LEFT" | "LBRCKT" | "LBRC" | "[" => Some("^"), + "BRACKET_RIGHT" | "RBRCKT" | "RBRC" | "]" => Some("$"), + "BACKSLASH" | "BSLSH" | "\\" => Some("<"), + "APOSTROPHE" | "APO" | "QUOT" | "'" => Some("\u{00f9}"), // ù + "1" => Some("& 1"), + "2" => Some("\u{00e9} 2 ~"), // é 2 ~ + "3" => Some("\" 3 #"), + "4" => Some("' 4 }"), + "5" => Some("( 5 ["), + "6" => Some("- 6 |"), + "7" => Some("\u{00e8} 7 `"), // è 7 ` + "8" => Some("_ 8 \\"), + "9" => Some("\u{00e7} 9 ^"), // ç 9 ^ + "0" => Some("\u{00e0} 0 @"), // à 0 @ + _ => None, + }, + + KeyboardLayout::Qwertz => match key { + "Y" => Some("Z"), + "Z" => Some("Y"), + "MINUS" | "MIN" => Some("\u{00df}"), // ß + "EQUAL" | "EQL" => Some("'"), + "BRACKET_LEFT" | "LBRCKT" | "LBRC" => Some("\u{00fc}"), // ü + "BRACKET_RIGHT" | "RBRCKT" | "RBRC" => Some("+"), + "SEMICOLON" | "SCOLON" | "SCLN" => Some("\u{00f6}"), // ö + "APOSTROPHE" | "APO" | "QUOT" => Some("\u{00e4}"), // ä + "GRAVE" | "GRV" => Some("^"), + "SLASH" | "SLSH" => Some("-"), + "BACKSLASH" | "BSLSH" => Some("#"), + _ => None, + }, + + KeyboardLayout::Dvorak => match key { + "Q" => Some("'"), + "W" => Some(","), + "E" => Some("."), + "R" => Some("P"), + "T" => Some("Y"), + "Y" => Some("F"), + "U" => Some("G"), + "I" => Some("C"), + "O" => Some("R"), + "P" => Some("L"), + "A" => Some("A"), + "S" => Some("O"), + "D" => Some("E"), + "F" => Some("U"), + "G" => Some("I"), + "H" => Some("D"), + "J" => Some("H"), + "K" => Some("T"), + "L" => Some("N"), + "SEMICOLON" | "SCOLON" | "SCLN" => Some("S"), + "APOSTROPHE" | "APO" | "QUOT" => Some("-"), + "Z" => Some(";"), + "X" => Some("Q"), + "C" => Some("J"), + "V" => Some("K"), + "B" => Some("X"), + "N" => Some("B"), + "M" => Some("M"), + "COMMA" | "COMM" | "COMA" => Some("W"), + "PERIOD" | "DOT" => Some("V"), + "SLASH" | "SLSH" => Some("Z"), + "MINUS" | "MIN" => Some("["), + "EQUAL" | "EQL" => Some("]"), + "BRACKET_LEFT" | "LBRCKT" | "LBRC" => Some("/"), + "BRACKET_RIGHT" | "RBRCKT" | "RBRC" => Some("="), + _ => None, + }, + + KeyboardLayout::Colemak => match key { + "E" => Some("F"), + "R" => Some("P"), + "T" => Some("G"), + "Y" => Some("J"), + "U" => Some("L"), + "I" => Some("U"), + "O" => Some("Y"), + "P" => Some(";"), + "S" => Some("R"), + "D" => Some("S"), + "F" => Some("T"), + "G" => Some("D"), + "J" => Some("N"), + "K" => Some("E"), + "L" => Some("I"), + "SEMICOLON" | "SCOLON" | "SCLN" => Some("O"), + "N" => Some("K"), + _ => None, + }, + + KeyboardLayout::Bepo => match key { + "Q" => Some("B"), + "W" => Some("E"), + "E" => Some("P"), + "R" => Some("O"), + "T" => Some("E"), + "Y" => Some("^"), + "U" => Some("V"), + "I" => Some("D"), + "O" => Some("L"), + "P" => Some("J"), + "A" => Some("A"), + "S" => Some("U"), + "D" => Some("I"), + "F" => Some("E"), + "G" => Some(","), + "H" => Some("C"), + "J" => Some("T"), + "K" => Some("S"), + "L" => Some("R"), + "SEMICOLON" | "SCOLON" | "SCLN" => Some("N"), + "Z" => Some("A"), + "X" => Some("Y"), + "C" => Some("X"), + "V" => Some("."), + "B" => Some("K"), + "N" => Some("'"), + "M" => Some("Q"), + "COMMA" | "COMM" | "COMA" => Some("G"), + "PERIOD" | "DOT" => Some("H"), + "SLASH" | "SLSH" => Some("F"), + "1" => Some("\""), + "2" => Some("<"), + "3" => Some(">"), + "4" => Some("("), + "5" => Some(")"), + "6" => Some("@"), + "7" => Some("+"), + "8" => Some("-"), + "9" => Some("/"), + "0" => Some("*"), + _ => None, + }, + + KeyboardLayout::QwertyEs => match key { + "MINUS" | "MIN" => Some("'"), + "EQUAL" | "EQL" => Some("\u{00a1}"), // ¡ + "BRACKET_LEFT" | "LBRCKT" | "LBRC" => Some("`"), + "BRACKET_RIGHT" | "RBRCKT" | "RBRC" => Some("+"), + "SEMICOLON" | "SCOLON" | "SCLN" => Some("\u{00f1}"), // ñ + "APOSTROPHE" | "APO" | "QUOT" => Some("'"), + "GRAVE" | "GRV" => Some("\u{00ba}"), // º + "SLASH" | "SLSH" => Some("-"), + "BACKSLASH" | "BSLSH" => Some("\u{00e7}"), // ç + _ => None, + }, + + KeyboardLayout::QwertyPt => match key { + "MINUS" | "MIN" => Some("'"), + "EQUAL" | "EQL" => Some("\u{00ab}"), // « + "BRACKET_LEFT" | "LBRCKT" | "LBRC" => Some("+"), + "BRACKET_RIGHT" | "RBRCKT" | "RBRC" => Some("'"), + "SEMICOLON" | "SCOLON" | "SCLN" => Some("\u{00e7}"), // ç + "APOSTROPHE" | "APO" | "QUOT" => Some("\u{00ba}"), // º + "GRAVE" | "GRV" => Some("\\"), + "SLASH" | "SLSH" => Some("-"), + "BACKSLASH" | "BSLSH" => Some("~"), + _ => None, + }, + + KeyboardLayout::QwertyIt => match key { + "MINUS" | "MIN" => Some("'"), + "EQUAL" | "EQL" => Some("\u{00ec}"), // ì + "BRACKET_LEFT" | "LBRCKT" | "LBRC" => Some("\u{00e8}"), // è + "BRACKET_RIGHT" | "RBRCKT" | "RBRC" => Some("+"), + "SEMICOLON" | "SCOLON" | "SCLN" => Some("\u{00f2}"), // ò + "APOSTROPHE" | "APO" | "QUOT" => Some("\u{00e0}"), // à + "GRAVE" | "GRV" => Some("\\"), + "SLASH" | "SLSH" => Some("-"), + "BACKSLASH" | "BSLSH" => Some("\u{00f9}"), // ù + _ => None, + }, + + KeyboardLayout::QwertyNordic => match key { + "MINUS" | "MIN" => Some("+"), + "EQUAL" | "EQL" => Some("'"), + "BRACKET_LEFT" | "LBRCKT" | "LBRC" => Some("\u{00e5}"), // å + "BRACKET_RIGHT" | "RBRCKT" | "RBRC" => Some("\u{00a8}"), // ¨ + "SEMICOLON" | "SCOLON" | "SCLN" => Some("\u{00f6}"), // ö + "APOSTROPHE" | "APO" | "QUOT" => Some("\u{00e4}"), // ä + "GRAVE" | "GRV" => Some("\u{00a7}"), // § + "SLASH" | "SLSH" => Some("-"), + "BACKSLASH" | "BSLSH" => Some("'"), + _ => None, + }, + + KeyboardLayout::QwertyBr => match key { + "MINUS" | "MIN" => Some("-"), + "EQUAL" | "EQL" => Some("="), + "BRACKET_LEFT" | "LBRCKT" | "LBRC" => Some("'"), + "BRACKET_RIGHT" | "RBRCKT" | "RBRC" => Some("["), + "SEMICOLON" | "SCOLON" | "SCLN" => Some("\u{00e7}"), // ç + "APOSTROPHE" | "APO" | "QUOT" => Some("~"), + "GRAVE" | "GRV" => Some("'"), + "SLASH" | "SLSH" => Some(";"), + "BACKSLASH" | "BSLSH" => Some("]"), + _ => None, + }, + + KeyboardLayout::QwertyTr => match key { + "MINUS" | "MIN" => Some("*"), + "EQUAL" | "EQL" => Some("-"), + "BRACKET_LEFT" | "LBRCKT" | "LBRC" => Some("\u{011f}"), // ğ + "BRACKET_RIGHT" | "RBRCKT" | "RBRC" => Some("\u{00fc}"), // ü + "SEMICOLON" | "SCOLON" | "SCLN" => Some("\u{015f}"), // ş + "APOSTROPHE" | "APO" | "QUOT" => Some("i"), + "GRAVE" | "GRV" => Some("\""), + "SLASH" | "SLSH" => Some("."), + "BACKSLASH" | "BSLSH" => Some(","), + _ => None, + }, + + KeyboardLayout::QwertyUk => match key { + "GRAVE" | "GRV" => Some("`"), + "MINUS" | "MIN" => Some("-"), + "EQUAL" | "EQL" => Some("="), + "BACKSLASH" | "BSLSH" => Some("#"), + "APOSTROPHE" | "APO" | "QUOT" => Some("'"), + _ => None, + }, + } +} diff --git a/src/logic/mod.rs b/src/logic/mod.rs new file mode 100644 index 0000000..4ce54e4 --- /dev/null +++ b/src/logic/mod.rs @@ -0,0 +1,19 @@ +#[allow(dead_code)] +pub mod binary_protocol; +#[allow(dead_code)] +pub mod flasher; +#[allow(dead_code)] +pub mod keycode; +pub mod layout; +#[allow(dead_code)] +pub mod layout_remap; +#[allow(dead_code)] +pub mod parsers; +#[allow(dead_code)] +pub mod protocol; +#[allow(dead_code)] +pub mod serial; +#[allow(dead_code)] +pub mod settings; +#[allow(dead_code)] +pub mod stats_analyzer; diff --git a/src/logic/parsers.rs b/src/logic/parsers.rs new file mode 100644 index 0000000..6fed54a --- /dev/null +++ b/src/logic/parsers.rs @@ -0,0 +1,900 @@ +/// Parsing functions for firmware text and binary responses. +/// Separated for testability. + +/// Keyboard physical dimensions (must match firmware). +pub const ROWS: usize = 5; +pub const COLS: usize = 13; + +/// Parse "TD0: 04,05,06,29" lines into an array of 8 tap dance slots. +/// Each slot has 4 actions: [1-tap, 2-tap, 3-tap, hold]. +pub fn parse_td_lines(lines: &[String]) -> Vec<[u16; 4]> { + let mut result = vec![[0u16; 4]; 8]; + + for line in lines { + // Only process lines starting with "TD" + let starts_with_td = line.starts_with("TD"); + if !starts_with_td { + continue; + } + + // Find the colon separator: "TD0: ..." + let colon = match line.find(':') { + Some(i) => i, + None => continue, + }; + + // Extract the index between "TD" and ":" + let index_str = &line[2..colon]; + let idx: usize = match index_str.parse() { + Ok(i) if i < 8 => i, + _ => continue, + }; + + // Parse the comma-separated hex values after the colon + let after_colon = &line[colon + 1..]; + let trimmed_values = after_colon.trim(); + let split_parts = trimmed_values.split(','); + let vals: Vec = split_parts + .filter_map(|s| { + let trimmed_part = s.trim(); + u16::from_str_radix(trimmed_part, 16).ok() + }) + .collect(); + + // We need exactly 4 values + let has_four_values = vals.len() == 4; + if has_four_values { + result[idx] = [vals[0], vals[1], vals[2], vals[3]]; + } + } + + result +} + +/// Parse KO (Key Override) lines into arrays of [trigger, mod, result, res_mod]. +/// Format: "KO0: trigger=2A mod=02 -> result=4C resmod=00" +pub fn parse_ko_lines(lines: &[String]) -> Vec<[u8; 4]> { + let mut result = Vec::new(); + + for line in lines { + // Only process lines starting with "KO" + let starts_with_ko = line.starts_with("KO"); + if !starts_with_ko { + continue; + } + + // Helper: extract hex value after a keyword like "trigger=" + let parse_hex = |key: &str| -> u8 { + let key_position = line.find(key); + + let after_key = match key_position { + Some(i) => { + let rest = &line[i + key.len()..]; + let first_token = rest.split_whitespace().next(); + first_token + } + None => None, + }; + + let parsed_value = match after_key { + Some(s) => { + let without_prefix = s.trim_start_matches("0x"); + u8::from_str_radix(without_prefix, 16).ok() + } + None => None, + }; + + parsed_value.unwrap_or(0) + }; + + let trigger = parse_hex("trigger="); + let modifier = parse_hex("mod="); + let result_key = parse_hex("result="); + let result_mod = parse_hex("resmod="); + + result.push([trigger, modifier, result_key, result_mod]); + } + + result +} + +/// Parse heatmap lines (KEYSTATS? response). +/// Format: "R0: 100 50 30 20 10 5 0 15 25 35 45 55 65" +/// Returns (data[5][13], max_value). +pub fn parse_heatmap_lines(lines: &[String]) -> (Vec>, u32) { + let mut data: Vec> = vec![vec![0u32; COLS]; ROWS]; + let mut max = 0u32; + + for line in lines { + let trimmed = line.trim(); + + // Only process lines starting with "R" + if !trimmed.starts_with('R') { + continue; + } + + // Find the colon + let colon = match trimmed.find(':') { + Some(i) => i, + None => continue, + }; + + // Extract row number between "R" and ":" + let row_str = &trimmed[1..colon]; + let row: usize = match row_str.parse() { + Ok(r) if r < ROWS => r, + _ => continue, + }; + + // Parse space-separated values after the colon + let values_str = &trimmed[colon + 1..]; + for (col, token) in values_str.split_whitespace().enumerate() { + if col >= COLS { + break; + } + let count: u32 = token.parse().unwrap_or(0); + data[row][col] = count; + if count > max { + max = count; + } + } + } + + (data, max) +} + +/// Parsed combo: [index, row1, col1, row2, col2, result_keycode] +#[derive(Clone)] +pub struct ComboEntry { + pub index: u8, + pub r1: u8, + pub c1: u8, + pub r2: u8, + pub c2: u8, + pub result: u16, +} + +/// Parse "COMBO0: r3c3+r3c4=29" lines. +pub fn parse_combo_lines(lines: &[String]) -> Vec { + let mut result = Vec::new(); + + for line in lines { + let starts_with_combo = line.starts_with("COMBO"); + if !starts_with_combo { + continue; + } + + // Find colon: "COMBO0: ..." + let colon = match line.find(':') { + Some(i) => i, + None => continue, + }; + + // Index between "COMBO" and ":" + let index_str = &line[5..colon]; + let index: u8 = match index_str.parse() { + Ok(i) => i, + _ => continue, + }; + + // After colon: "r3c3+r3c4=29" + let rest = line[colon + 1..].trim(); + + // Split by "=" + let eq_parts: Vec<&str> = rest.split('=').collect(); + let has_two_parts = eq_parts.len() == 2; + if !has_two_parts { + continue; + } + + // Split left side by "+" + let key_parts: Vec<&str> = eq_parts[0].split('+').collect(); + let has_two_keys = key_parts.len() == 2; + if !has_two_keys { + continue; + } + + // Parse "r3c4" format + let pos1 = parse_rc(key_parts[0].trim()); + let pos2 = parse_rc(key_parts[1].trim()); + + let (r1, c1) = match pos1 { + Some(rc) => rc, + None => continue, + }; + let (r2, c2) = match pos2 { + Some(rc) => rc, + None => continue, + }; + + // Parse result keycode (hex) + let result_str = eq_parts[1].trim(); + let result_code: u16 = match u16::from_str_radix(result_str, 16) { + Ok(v) => v, + _ => continue, + }; + + result.push(ComboEntry { + index, + r1, + c1, + r2, + c2, + result: result_code, + }); + } + + result +} + +/// Parse "r3c4" into (row, col). +fn parse_rc(s: &str) -> Option<(u8, u8)> { + let lower = s.to_lowercase(); + + let r_pos = lower.find('r'); + let c_pos = lower.find('c'); + + let r_idx = match r_pos { + Some(i) => i, + None => return None, + }; + let c_idx = match c_pos { + Some(i) => i, + None => return None, + }; + + let row_str = &lower[r_idx + 1..c_idx]; + let col_str = &lower[c_idx + 1..]; + + let row: u8 = match row_str.parse() { + Ok(v) => v, + _ => return None, + }; + let col: u8 = match col_str.parse() { + Ok(v) => v, + _ => return None, + }; + + Some((row, col)) +} + +/// Parsed leader sequence entry. +#[derive(Clone)] +pub struct LeaderEntry { + pub index: u8, + pub sequence: Vec, // HID keycodes + pub result: u8, + pub result_mod: u8, +} + +/// Parse "LEADER0: 04,->29+00" lines. +pub fn parse_leader_lines(lines: &[String]) -> Vec { + let mut result = Vec::new(); + + for line in lines { + let starts_with_leader = line.starts_with("LEADER"); + if !starts_with_leader { + continue; + } + + let colon = match line.find(':') { + Some(i) => i, + None => continue, + }; + + let index_str = &line[6..colon]; + let index: u8 = match index_str.parse() { + Ok(i) => i, + _ => continue, + }; + + // After colon: "04,->29+00" + let rest = line[colon + 1..].trim(); + + // Split by "->" + let arrow_parts: Vec<&str> = rest.split("->").collect(); + let has_two_parts = arrow_parts.len() == 2; + if !has_two_parts { + continue; + } + + // Sequence: comma-separated hex keycodes (trailing comma OK) + let seq_str = arrow_parts[0].trim().trim_end_matches(','); + let sequence: Vec = seq_str + .split(',') + .filter_map(|s| { + let trimmed = s.trim(); + u8::from_str_radix(trimmed, 16).ok() + }) + .collect(); + + // Result: "29+00" = keycode + modifier + let result_parts: Vec<&str> = arrow_parts[1].trim().split('+').collect(); + let has_result = result_parts.len() == 2; + if !has_result { + continue; + } + + let result_key = match u8::from_str_radix(result_parts[0].trim(), 16) { + Ok(v) => v, + _ => continue, + }; + let result_mod = match u8::from_str_radix(result_parts[1].trim(), 16) { + Ok(v) => v, + _ => continue, + }; + + result.push(LeaderEntry { + index, + sequence, + result: result_key, + result_mod, + }); + } + + result +} + +/// A single macro step: keycode + modifier, or delay. +#[derive(Clone)] +pub struct MacroStep { + pub keycode: u8, + pub modifier: u8, +} + +impl MacroStep { + /// Returns true if this step is a delay (keycode 0xFF). + pub fn is_delay(&self) -> bool { + self.keycode == 0xFF + } + + /// Delay in milliseconds (modifier * 10). + pub fn delay_ms(&self) -> u32 { + self.modifier as u32 * 10 + } +} + +/// A parsed macro entry. +#[derive(Clone)] +pub struct MacroEntry { + pub slot: u8, + pub name: String, + pub steps: Vec, +} + +/// Parse MACROS? text response. +/// Lines can be like: +/// "MACRO 0: CopyPaste [06:01,FF:0A,19:01]" +/// "M0: name=CopyPaste steps=06:01,FF:0A,19:01" +/// or just raw text lines +pub fn parse_macro_lines(lines: &[String]) -> Vec { + let mut result = Vec::new(); + + for line in lines { + // Try format: "MACRO 0: name [steps]" or "M0: ..." + let trimmed = line.trim(); + + // Skip empty or header lines + let is_empty = trimmed.is_empty(); + if is_empty { + continue; + } + + // Try to find slot number + let has_macro_prefix = trimmed.starts_with("MACRO") || trimmed.starts_with("M"); + if !has_macro_prefix { + continue; + } + + let colon = match trimmed.find(':') { + Some(i) => i, + None => continue, + }; + + // Extract slot number from prefix + let prefix_end = trimmed[..colon].trim(); + let digits_start = prefix_end + .find(|c: char| c.is_ascii_digit()) + .unwrap_or(prefix_end.len()); + let slot_str = &prefix_end[digits_start..]; + let slot: u8 = match slot_str.trim().parse() { + Ok(s) => s, + _ => continue, + }; + + let after_colon = trimmed[colon + 1..].trim(); + + // Try to parse name and steps from brackets: "CopyPaste [06:01,FF:0A,19:01]" + let bracket_start = after_colon.find('['); + let bracket_end = after_colon.find(']'); + + let (name, steps_str) = match (bracket_start, bracket_end) { + (Some(bs), Some(be)) => { + let name_part = after_colon[..bs].trim().to_string(); + let steps_part = &after_colon[bs + 1..be]; + (name_part, steps_part.to_string()) + } + _ => { + // Try "name=X steps=Y" format + let name_eq = after_colon.find("name="); + let steps_eq = after_colon.find("steps="); + match (name_eq, steps_eq) { + (Some(ni), Some(si)) => { + let name_start = ni + 5; + let name_end = si; + let n = after_colon[name_start..name_end].trim().to_string(); + let s = after_colon[si + 6..].trim().to_string(); + (n, s) + } + _ => { + // Just use the whole thing as name, no steps + (after_colon.to_string(), String::new()) + } + } + } + }; + + // Parse steps: "06:01,FF:0A,19:01" + let mut steps = Vec::new(); + let has_steps = !steps_str.is_empty(); + if has_steps { + let step_parts = steps_str.split(','); + for part in step_parts { + let step_trimmed = part.trim(); + let kv: Vec<&str> = step_trimmed.split(':').collect(); + let has_two = kv.len() == 2; + if !has_two { + continue; + } + let key_byte = u8::from_str_radix(kv[0].trim(), 16).unwrap_or(0); + let mod_byte = u8::from_str_radix(kv[1].trim(), 16).unwrap_or(0); + steps.push(MacroStep { + keycode: key_byte, + modifier: mod_byte, + }); + } + } + + result.push(MacroEntry { + slot, + name, + steps, + }); + } + + result +} + +// --------------------------------------------------------------------------- +// Binary payload parsers (protocol v2) +// --------------------------------------------------------------------------- +// These functions parse the *payload* bytes extracted from a KR response frame. +// They produce the same data types as the text parsers above. + +/// Parse TD_LIST (0x51) binary payload. +/// Format: [count:u8] then count entries of [idx:u8][a1:u8][a2:u8][a3:u8][a4:u8]. +/// Returns Vec<[u16; 4]> with 8 slots (same shape as parse_td_lines). +pub fn parse_td_binary(payload: &[u8]) -> Vec<[u16; 4]> { + let mut result = vec![[0u16; 4]; 8]; + + // Need at least 1 byte for count + let has_count = !payload.is_empty(); + if !has_count { + return result; + } + + let count = payload[0] as usize; + let entry_size = 5; // idx(1) + actions(4) + let mut offset = 1; + + for _ in 0..count { + // Bounds check: need 5 bytes for this entry + let remaining = payload.len().saturating_sub(offset); + let enough_bytes = remaining >= entry_size; + if !enough_bytes { + break; + } + + let idx = payload[offset] as usize; + let action1 = payload[offset + 1] as u16; + let action2 = payload[offset + 2] as u16; + let action3 = payload[offset + 3] as u16; + let action4 = payload[offset + 4] as u16; + + let valid_index = idx < 8; + if valid_index { + result[idx] = [action1, action2, action3, action4]; + } + + offset += entry_size; + } + + result +} + +/// Parse COMBO_LIST (0x61) binary payload. +/// Format: [count:u8] then count entries of [idx:u8][r1:u8][c1:u8][r2:u8][c2:u8][result:u8]. +pub fn parse_combo_binary(payload: &[u8]) -> Vec { + let mut result = Vec::new(); + + let has_count = !payload.is_empty(); + if !has_count { + return result; + } + + let count = payload[0] as usize; + let entry_size = 6; // idx + r1 + c1 + r2 + c2 + result + let mut offset = 1; + + for _ in 0..count { + let remaining = payload.len().saturating_sub(offset); + let enough_bytes = remaining >= entry_size; + if !enough_bytes { + break; + } + + let index = payload[offset]; + let r1 = payload[offset + 1]; + let c1 = payload[offset + 2]; + let r2 = payload[offset + 3]; + let c2 = payload[offset + 4]; + let result_code = payload[offset + 5] as u16; + + result.push(ComboEntry { + index, + r1, + c1, + r2, + c2, + result: result_code, + }); + + offset += entry_size; + } + + result +} + +/// Parse LEADER_LIST (0x71) binary payload. +/// Format: [count:u8] then per entry: [idx:u8][seq_len:u8][seq: seq_len bytes][result:u8][result_mod:u8]. +pub fn parse_leader_binary(payload: &[u8]) -> Vec { + let mut result = Vec::new(); + + let has_count = !payload.is_empty(); + if !has_count { + return result; + } + + let count = payload[0] as usize; + let mut offset = 1; + + for _ in 0..count { + // Need at least idx(1) + seq_len(1) + let remaining = payload.len().saturating_sub(offset); + let enough_for_header = remaining >= 2; + if !enough_for_header { + break; + } + + let index = payload[offset]; + let seq_len = payload[offset + 1] as usize; + offset += 2; + + // Need seq_len bytes for sequence + 2 bytes for result+result_mod + let remaining_after_header = payload.len().saturating_sub(offset); + let enough_for_body = remaining_after_header >= seq_len + 2; + if !enough_for_body { + break; + } + + let sequence = payload[offset..offset + seq_len].to_vec(); + offset += seq_len; + + let result_key = payload[offset]; + let result_mod = payload[offset + 1]; + offset += 2; + + result.push(LeaderEntry { + index, + sequence, + result: result_key, + result_mod, + }); + } + + result +} + +/// Parse KO_LIST (0x92) binary payload. +/// Format: [count:u8] then count entries of [idx:u8][trigger_key:u8][trigger_mod:u8][result_key:u8][result_mod:u8]. +/// Returns Vec<[u8; 4]> = [trigger_key, trigger_mod, result_key, result_mod] (same as parse_ko_lines). +pub fn parse_ko_binary(payload: &[u8]) -> Vec<[u8; 4]> { + let mut result = Vec::new(); + + let has_count = !payload.is_empty(); + if !has_count { + return result; + } + + let count = payload[0] as usize; + let entry_size = 5; // idx + trigger_key + trigger_mod + result_key + result_mod + let mut offset = 1; + + for _ in 0..count { + let remaining = payload.len().saturating_sub(offset); + let enough_bytes = remaining >= entry_size; + if !enough_bytes { + break; + } + + // idx is payload[offset], but we skip it (not stored in output) + let trigger_key = payload[offset + 1]; + let trigger_mod = payload[offset + 2]; + let result_key = payload[offset + 3]; + let result_mod = payload[offset + 4]; + + result.push([trigger_key, trigger_mod, result_key, result_mod]); + + offset += entry_size; + } + + result +} + +/// Parse BT_QUERY (0x80) binary payload. +/// Format: [active_slot:u8][initialized:u8][connected:u8][pairing:u8] +/// then 3 slot entries: [slot_idx:u8][valid:u8][addr:6 bytes][name_len:u8][name: name_len bytes] +/// Returns Vec of text lines compatible with the UI (same shape as legacy text parsing). +pub fn parse_bt_binary(payload: &[u8]) -> Vec { + let mut lines = Vec::new(); + + // Need at least 4 bytes for the global state header + let enough_for_header = payload.len() >= 4; + if !enough_for_header { + return lines; + } + + let active_slot = payload[0]; + let initialized = payload[1]; + let connected = payload[2]; + let pairing = payload[3]; + + let status_line = format!( + "BT: slot={} init={} conn={} pairing={}", + active_slot, initialized, connected, pairing + ); + lines.push(status_line); + + let mut offset = 4; + let slot_count = 3; + + for _ in 0..slot_count { + // Each slot: slot_idx(1) + valid(1) + addr(6) + name_len(1) = 9 bytes minimum + let remaining = payload.len().saturating_sub(offset); + let enough_for_slot_header = remaining >= 9; + if !enough_for_slot_header { + break; + } + + let slot_idx = payload[offset]; + let valid = payload[offset + 1]; + let addr_bytes = &payload[offset + 2..offset + 8]; + let name_len = payload[offset + 8] as usize; + offset += 9; + + // Read the name string + let remaining_for_name = payload.len().saturating_sub(offset); + let enough_for_name = remaining_for_name >= name_len; + if !enough_for_name { + break; + } + + let name_bytes = &payload[offset..offset + name_len]; + let name = String::from_utf8_lossy(name_bytes).to_string(); + offset += name_len; + + // Format address as "XX:XX:XX:XX:XX:XX" + let addr_str = format!( + "{:02X}:{:02X}:{:02X}:{:02X}:{:02X}:{:02X}", + addr_bytes[0], addr_bytes[1], addr_bytes[2], + addr_bytes[3], addr_bytes[4], addr_bytes[5] + ); + + let slot_line = format!( + "BT slot {}: valid={} addr={} name={}", + slot_idx, valid, addr_str, name + ); + lines.push(slot_line); + } + + lines +} + +/// Parse TAMA_QUERY (0xA0) binary payload (22 bytes fixed). +/// Format: [enabled:u8][state:u8][hunger:u16 LE][happiness:u16 LE][energy:u16 LE] +/// [health:u16 LE][level:u16 LE][xp:u16 LE][total_keys:u32 LE][max_kpm:u32 LE] +/// Returns Vec with one summary line. +pub fn parse_tama_binary(payload: &[u8]) -> Vec { + let expected_size = 22; + let enough_bytes = payload.len() >= expected_size; + if !enough_bytes { + return vec!["TAMA: invalid payload".to_string()]; + } + + let enabled = payload[0]; + let _state = payload[1]; + + let hunger = u16::from_le_bytes([payload[2], payload[3]]); + let happiness = u16::from_le_bytes([payload[4], payload[5]]); + let energy = u16::from_le_bytes([payload[6], payload[7]]); + let health = u16::from_le_bytes([payload[8], payload[9]]); + let level = u16::from_le_bytes([payload[10], payload[11]]); + let _xp = u16::from_le_bytes([payload[12], payload[13]]); + let total_keys = u32::from_le_bytes([payload[14], payload[15], payload[16], payload[17]]); + let _max_kpm = u32::from_le_bytes([payload[18], payload[19], payload[20], payload[21]]); + + let line = format!( + "TAMA: Lv{} hunger={} happy={} energy={} health={} keys={} enabled={}", + level, hunger, happiness, energy, health, total_keys, enabled + ); + + vec![line] +} + +/// Parse WPM_QUERY (0x93) binary payload (2 bytes fixed). +/// Format: [wpm:u16 LE] +pub fn parse_wpm_binary(payload: &[u8]) -> String { + let enough_bytes = payload.len() >= 2; + if !enough_bytes { + return "WPM: 0".to_string(); + } + + let wpm = u16::from_le_bytes([payload[0], payload[1]]); + + format!("WPM: {}", wpm) +} + +/// Parse LIST_MACROS (0x30) binary payload. +/// Format: [count:u8] then per entry: +/// [idx:u8][keycode:u16 LE][name_len:u8][name: name_len bytes] +/// [keys_len:u8][keys: keys_len bytes][step_count:u8][{kc:u8,mod:u8}... step_count*2 bytes] +pub fn parse_macros_binary(payload: &[u8]) -> Vec { + let mut result = Vec::new(); + + let has_count = !payload.is_empty(); + if !has_count { + return result; + } + + let count = payload[0] as usize; + let mut offset = 1; + + for _ in 0..count { + // Need at least: idx(1) + keycode(2) + name_len(1) = 4 + let remaining = payload.len().saturating_sub(offset); + let enough_for_prefix = remaining >= 4; + if !enough_for_prefix { + break; + } + + let slot = payload[offset]; + // keycode is stored but not used in MacroEntry (it's the trigger keycode) + let _keycode = u16::from_le_bytes([payload[offset + 1], payload[offset + 2]]); + let name_len = payload[offset + 3] as usize; + offset += 4; + + // Read name + let remaining_for_name = payload.len().saturating_sub(offset); + let enough_for_name = remaining_for_name >= name_len; + if !enough_for_name { + break; + } + + let name_bytes = &payload[offset..offset + name_len]; + let name = String::from_utf8_lossy(name_bytes).to_string(); + offset += name_len; + + // Read keys_len + keys (raw key bytes, skipped for MacroEntry) + let remaining_for_keys_len = payload.len().saturating_sub(offset); + let enough_for_keys_len = remaining_for_keys_len >= 1; + if !enough_for_keys_len { + break; + } + + let keys_len = payload[offset] as usize; + offset += 1; + + let remaining_for_keys = payload.len().saturating_sub(offset); + let enough_for_keys = remaining_for_keys >= keys_len; + if !enough_for_keys { + break; + } + + // Skip the raw keys bytes + offset += keys_len; + + // Read step_count + steps + let remaining_for_step_count = payload.len().saturating_sub(offset); + let enough_for_step_count = remaining_for_step_count >= 1; + if !enough_for_step_count { + break; + } + + let step_count = payload[offset] as usize; + offset += 1; + + let steps_byte_size = step_count * 2; + let remaining_for_steps = payload.len().saturating_sub(offset); + let enough_for_steps = remaining_for_steps >= steps_byte_size; + if !enough_for_steps { + break; + } + + let mut steps = Vec::with_capacity(step_count); + for i in 0..step_count { + let step_offset = offset + i * 2; + let kc = payload[step_offset]; + let md = payload[step_offset + 1]; + steps.push(MacroStep { + keycode: kc, + modifier: md, + }); + } + offset += steps_byte_size; + + result.push(MacroEntry { + slot, + name, + steps, + }); + } + + result +} + +/// Parse KEYSTATS_BIN (0x40) binary payload. +/// Format: [rows:u8][cols:u8][counts: rows*cols * u32 LE] +/// Returns (heatmap_data, max_value) — same shape as parse_heatmap_lines. +pub fn parse_keystats_binary(payload: &[u8]) -> (Vec>, u32) { + // Need at least 2 bytes for rows and cols + let enough_for_header = payload.len() >= 2; + if !enough_for_header { + return (vec![], 0); + } + + let rows = payload[0] as usize; + let cols = payload[1] as usize; + let total_cells = rows * cols; + let data_byte_size = total_cells * 4; // each count is u32 LE + + let remaining = payload.len().saturating_sub(2); + let enough_for_data = remaining >= data_byte_size; + if !enough_for_data { + return (vec![], 0); + } + + let mut data: Vec> = vec![vec![0u32; cols]; rows]; + let mut max_value = 0u32; + let mut offset = 2; + + for row in 0..rows { + for col in 0..cols { + let count = u32::from_le_bytes([ + payload[offset], + payload[offset + 1], + payload[offset + 2], + payload[offset + 3], + ]); + data[row][col] = count; + + let is_new_max = count > max_value; + if is_new_max { + max_value = count; + } + + offset += 4; + } + } + + (data, max_value) +} diff --git a/src/logic/protocol.rs b/src/logic/protocol.rs new file mode 100644 index 0000000..03f9b63 --- /dev/null +++ b/src/logic/protocol.rs @@ -0,0 +1,65 @@ +#![allow(dead_code)] +/// CDC protocol command helpers for KaSe keyboard firmware. + +// Text-based query commands +pub const CMD_TAP_DANCE: &str = "TD?"; +pub const CMD_COMBOS: &str = "COMBO?"; +pub const CMD_LEADER: &str = "LEADER?"; +pub const CMD_KEY_OVERRIDE: &str = "KO?"; +pub const CMD_BT_STATUS: &str = "BT?"; +pub const CMD_WPM: &str = "WPM?"; +pub const CMD_TAMA: &str = "TAMA?"; +pub const CMD_MACROS_TEXT: &str = "MACROS?"; +pub const CMD_FEATURES: &str = "FEATURES?"; +pub const CMD_KEYSTATS: &str = "KEYSTATS?"; +pub const CMD_BIGRAMS: &str = "BIGRAMS?"; + +pub fn cmd_set_key(layer: u8, row: u8, col: u8, keycode: u16) -> String { + format!("SETKEY {},{},{},{:04X}", layer, row, col, keycode) +} + +pub fn cmd_set_layer_name(layer: u8, name: &str) -> String { + format!("LAYOUTNAME{}:{}", layer, name) +} + +pub fn cmd_bt_switch(slot: u8) -> String { + format!("BT SWITCH {}", slot) +} + +pub fn cmd_trilayer(l1: u8, l2: u8, l3: u8) -> String { + format!("TRILAYER {},{},{}", l1, l2, l3) +} + +pub fn cmd_macroseq(slot: u8, name: &str, steps: &str) -> String { + format!("MACROSEQ {};{};{}", slot, name, steps) +} + +pub fn cmd_macro_del(slot: u8) -> String { + format!("MACRODEL {}", slot) +} + +pub fn cmd_comboset(index: u8, r1: u8, c1: u8, r2: u8, c2: u8, result: u8) -> String { + format!("COMBOSET {};{},{},{},{},{:02X}", index, r1, c1, r2, c2, result) +} + +pub fn cmd_combodel(index: u8) -> String { + format!("COMBODEL {}", index) +} + +pub fn cmd_koset(index: u8, trig_key: u8, trig_mod: u8, res_key: u8, res_mod: u8) -> String { + format!("KOSET {};{:02X},{:02X},{:02X},{:02X}", index, trig_key, trig_mod, res_key, res_mod) +} + +pub fn cmd_kodel(index: u8) -> String { + format!("KODEL {}", index) +} + +pub fn cmd_leaderset(index: u8, sequence: &[u8], result: u8, result_mod: u8) -> String { + let seq_hex: Vec = sequence.iter().map(|k| format!("{:02X}", k)).collect(); + let seq_str = seq_hex.join(","); + format!("LEADERSET {};{};{:02X},{:02X}", index, seq_str, result, result_mod) +} + +pub fn cmd_leaderdel(index: u8) -> String { + format!("LEADERDEL {}", index) +} diff --git a/src/logic/serial/mod.rs b/src/logic/serial/mod.rs new file mode 100644 index 0000000..cfc2f59 --- /dev/null +++ b/src/logic/serial/mod.rs @@ -0,0 +1,15 @@ +/// Serial communication module. +/// Dispatches to native (serialport crate) or web (WebSerial API) +/// depending on the target architecture. + +#[cfg(not(target_arch = "wasm32"))] +mod native; + +#[cfg(target_arch = "wasm32")] +mod web; + +#[cfg(not(target_arch = "wasm32"))] +pub use native::*; + +#[cfg(target_arch = "wasm32")] +pub use web::*; diff --git a/src/logic/serial/native.rs b/src/logic/serial/native.rs new file mode 100644 index 0000000..efb2d86 --- /dev/null +++ b/src/logic/serial/native.rs @@ -0,0 +1,573 @@ +use serialport::SerialPort; +use std::io::{BufRead, BufReader, Read, Write}; +use std::sync::{Arc, Mutex}; +use std::time::{Duration, Instant}; + +use crate::logic::binary_protocol::{self as bp, KrResponse}; +use crate::logic::parsers::{ROWS, COLS}; + +const BAUD_RATE: u32 = 115200; +const CONNECT_TIMEOUT_MS: u64 = 300; +const QUERY_TIMEOUT_MS: u64 = 800; +const BINARY_READ_TIMEOUT_MS: u64 = 1500; +const LEGACY_BINARY_SETTLE_MS: u64 = 50; +const BINARY_SETTLE_MS: u64 = 30; +const JSON_TIMEOUT_SECS: u64 = 3; + +pub struct SerialManager { + port: Option>, + pub port_name: String, + pub connected: bool, + pub v2: bool, // true if firmware supports binary protocol v2 +} + +impl SerialManager { + pub fn new() -> Self { + Self { + port: None, + port_name: String::new(), + connected: false, + v2: false, + } + } + + #[allow(dead_code)] + pub fn list_ports() -> Vec { + let available = serialport::available_ports(); + let ports = available.unwrap_or_default(); + ports.into_iter().map(|p| p.port_name).collect() + } + + /// List only ports that look like ESP32 programming ports (CH340, CP210x, FTDI). + pub fn list_prog_ports() -> Vec { + const CH340_VID: u16 = 0x1A86; + const CP210X_VID: u16 = 0x10C4; + const FTDI_VID: u16 = 0x0403; + + let available = serialport::available_ports(); + let ports = available.unwrap_or_default(); + ports.into_iter() + .filter(|p| { + matches!(&p.port_type, serialport::SerialPortType::UsbPort(usb) + if usb.vid == CH340_VID || usb.vid == CP210X_VID || usb.vid == FTDI_VID) + }) + .map(|p| p.port_name) + .collect() + } + + pub fn connect(&mut self, port_name: &str) -> Result<(), String> { + let builder = serialport::new(port_name, BAUD_RATE); + let builder_with_timeout = builder.timeout(Duration::from_millis(CONNECT_TIMEOUT_MS)); + let open_result = builder_with_timeout.open(); + let port = open_result.map_err(|e| format!("Failed to open {}: {}", port_name, e))?; + + self.port = Some(port); + self.port_name = port_name.to_string(); + self.connected = true; + self.v2 = false; + + // Detect v2: try PING + if let Some(p) = self.port.as_mut() { + let _ = p.clear(serialport::ClearBuffer::All); + } + std::thread::sleep(Duration::from_millis(LEGACY_BINARY_SETTLE_MS)); + + let ping_result = self.send_binary(bp::cmd::PING, &[]); + if ping_result.is_ok() { + self.v2 = true; + } + + Ok(()) + } + + pub fn auto_connect(&mut self) -> Result { + let port_name = Self::find_kase_port()?; + self.connect(&port_name)?; + Ok(port_name) + } + + pub fn find_kase_port() -> Result { + const TARGET_VID: u16 = 0xCAFE; + const TARGET_PID: u16 = 0x4001; + + let available = serialport::available_ports(); + let ports = available.unwrap_or_default(); + if ports.is_empty() { + return Err("No serial ports found".into()); + } + + // First pass: check USB VID/PID and product name + for port in &ports { + let port_type = &port.port_type; + match port_type { + serialport::SerialPortType::UsbPort(usb) => { + let vid_matches = usb.vid == TARGET_VID; + let pid_matches = usb.pid == TARGET_PID; + if vid_matches && pid_matches { + return Ok(port.port_name.clone()); + } + + match &usb.product { + Some(product) => { + let is_kase = product.contains("KaSe"); + let is_kesp = product.contains("KeSp"); + if is_kase || is_kesp { + return Ok(port.port_name.clone()); + } + } + None => {} + } + } + _ => {} + } + } + + // Second pass (Linux only): check udevadm info + #[cfg(target_os = "linux")] + for port in &ports { + let udevadm_result = std::process::Command::new("udevadm") + .args(["info", "-n", &port.port_name]) + .output(); + + match udevadm_result { + Ok(output) => { + let stdout_bytes = &output.stdout; + let text = String::from_utf8_lossy(stdout_bytes); + let has_kase = text.contains("KaSe"); + let has_kesp = text.contains("KeSp"); + if has_kase || has_kesp { + return Ok(port.port_name.clone()); + } + } + Err(_) => {} + } + } + + let scanned_count = ports.len(); + Err(format!("No KaSe keyboard found ({} port(s) scanned)", scanned_count)) + } + + pub fn port_mut(&mut self) -> Option<&mut Box> { + self.port.as_mut() + } + + pub fn disconnect(&mut self) { + self.port = None; + self.port_name.clear(); + self.connected = false; + self.v2 = false; + } + + // ==================== LOW-LEVEL: ASCII LEGACY ==================== + + pub fn send_command(&mut self, cmd: &str) -> Result<(), String> { + let port = self.port.as_mut().ok_or("Not connected")?; + let data = format!("{}\r\n", cmd); + let bytes = data.as_bytes(); + + let write_result = port.write_all(bytes); + write_result.map_err(|e| format!("Write: {}", e))?; + + let flush_result = port.flush(); + flush_result.map_err(|e| format!("Flush: {}", e))?; + + Ok(()) + } + + pub fn query_command(&mut self, cmd: &str) -> Result, String> { + self.send_command(cmd)?; + + let port = self.port.as_mut().ok_or("Not connected")?; + let cloned_port = port.try_clone(); + let port_clone = cloned_port.map_err(|e| e.to_string())?; + let mut reader = BufReader::new(port_clone); + + let mut lines = Vec::new(); + let start = Instant::now(); + let max_wait = Duration::from_millis(QUERY_TIMEOUT_MS); + + loop { + let elapsed = start.elapsed(); + if elapsed > max_wait { + break; + } + + let mut line = String::new(); + let read_result = reader.read_line(&mut line); + + match read_result { + Ok(0) => break, + Ok(_) => { + let trimmed = line.trim().to_string(); + let is_terminal = trimmed == "OK" || trimmed == "ERROR"; + if is_terminal { + break; + } + let is_not_empty = !trimmed.is_empty(); + if is_not_empty { + lines.push(trimmed); + } + } + Err(_) => break, + } + } + + Ok(lines) + } + + fn read_raw(&mut self, timeout_ms: u64) -> Result, String> { + let port = self.port.as_mut().ok_or("Not connected")?; + let mut buf = vec![0u8; 4096]; + let mut result = Vec::new(); + let start = Instant::now(); + let timeout = Duration::from_millis(timeout_ms); + + while start.elapsed() < timeout { + let read_result = port.read(&mut buf); + + match read_result { + Ok(n) if n > 0 => { + let received_bytes = &buf[..n]; + result.extend_from_slice(received_bytes); + std::thread::sleep(Duration::from_millis(5)); + } + _ => { + let got_something = !result.is_empty(); + if got_something { + break; + } + std::thread::sleep(Duration::from_millis(5)); + } + } + } + + Ok(result) + } + + /// Legacy C> binary protocol + fn query_legacy_binary(&mut self, cmd: &str) -> Result<(u8, Vec), String> { + self.send_command(cmd)?; + std::thread::sleep(Duration::from_millis(LEGACY_BINARY_SETTLE_MS)); + let raw = self.read_raw(BINARY_READ_TIMEOUT_MS)?; + + // Look for the "C>" header in the raw bytes + let mut windows = raw.windows(2); + let header_search = windows.position(|w| w == b"C>"); + let pos = header_search.ok_or("No C> header found")?; + + let min_packet_size = pos + 5; + if raw.len() < min_packet_size { + return Err("Packet too short".into()); + } + + let cmd_type = raw[pos + 2]; + let low_byte = raw[pos + 3] as u16; + let high_byte = (raw[pos + 4] as u16) << 8; + let data_len = low_byte | high_byte; + + let data_start = pos + 5; + let data_end = data_start.checked_add(data_len as usize) + .ok_or("Data length overflow")?; + + if raw.len() < data_end { + return Err(format!("Incomplete: need {}, got {}", data_end, raw.len())); + } + + let payload = raw[data_start..data_end].to_vec(); + Ok((cmd_type, payload)) + } + + // ==================== LOW-LEVEL: BINARY V2 ==================== + + /// Send a KS frame, read KR response. + pub fn send_binary(&mut self, cmd_id: u8, payload: &[u8]) -> Result { + let frame = bp::ks_frame(cmd_id, payload); + let port = self.port.as_mut().ok_or("Not connected")?; + + let write_result = port.write_all(&frame); + write_result.map_err(|e| format!("Write: {}", e))?; + + let flush_result = port.flush(); + flush_result.map_err(|e| format!("Flush: {}", e))?; + + std::thread::sleep(Duration::from_millis(BINARY_SETTLE_MS)); + let raw = self.read_raw(BINARY_READ_TIMEOUT_MS)?; + + let (resp, _remaining) = bp::parse_kr(&raw)?; + let firmware_ok = resp.is_ok(); + if !firmware_ok { + let status = resp.status_name(); + return Err(format!("Firmware error: {}", status)); + } + + Ok(resp) + } + + // ==================== HIGH-LEVEL: AUTO V2/LEGACY ==================== + + pub fn get_firmware_version(&mut self) -> Option { + // Try v2 binary first + if self.v2 { + let binary_result = self.send_binary(bp::cmd::VERSION, &[]); + match binary_result { + Ok(resp) => { + let raw_bytes = &resp.payload; + let lossy_string = String::from_utf8_lossy(raw_bytes); + let version = lossy_string.to_string(); + return Some(version); + } + Err(_) => {} + } + } + + // Legacy fallback + let query_result = self.query_command("VERSION?"); + let lines = match query_result { + Ok(l) => l, + Err(_) => return None, + }; + let mut line_iter = lines.into_iter(); + let first_line = line_iter.next(); + first_line + } + + pub fn get_keymap(&mut self, layer: u8) -> Result>, String> { + // Try v2 binary first + if self.v2 { + let resp = self.send_binary(bp::cmd::KEYMAP_GET, &[layer])?; + let keymap = self.parse_keymap_payload(&resp.payload)?; + return Ok(keymap); + } + + // Legacy + let cmd = format!("KEYMAP{}", layer); + let (cmd_type, data) = self.query_legacy_binary(&cmd)?; + + if cmd_type != 1 { + return Err(format!("Unexpected cmd type: {}", cmd_type)); + } + if data.len() < 2 { + return Err("Data too short".into()); + } + + // skip 2-byte layer index in legacy + let data_without_header = &data[2..]; + self.parse_keymap_payload(data_without_header) + } + + fn parse_keymap_payload(&self, data: &[u8]) -> Result>, String> { + // v2: [layer:u8][keycodes...] -- skip first byte + // legacy: already stripped + let expected_with_layer_byte = 1 + ROWS * COLS * 2; + let has_layer_byte = data.len() >= expected_with_layer_byte; + let offset = if has_layer_byte { 1 } else { 0 }; + let kc_data = &data[offset..]; + + let needed_bytes = ROWS * COLS * 2; + if kc_data.len() < needed_bytes { + return Err(format!("Keymap data too short: {} bytes (need {})", kc_data.len(), needed_bytes)); + } + + let mut keymap = Vec::with_capacity(ROWS); + + for row_index in 0..ROWS { + let mut row = Vec::with_capacity(COLS); + + for col_index in 0..COLS { + let idx = (row_index * COLS + col_index) * 2; + let low_byte = kc_data[idx] as u16; + let high_byte = (kc_data[idx + 1] as u16) << 8; + let keycode = low_byte | high_byte; + row.push(keycode); + } + + keymap.push(row); + } + + Ok(keymap) + } + + pub fn get_layer_names(&mut self) -> Result, String> { + // Try v2 binary first + if self.v2 { + let resp = self.send_binary(bp::cmd::LIST_LAYOUTS, &[])?; + + let payload = &resp.payload; + if payload.is_empty() { + return Err("Empty response".into()); + } + + let count = payload[0] as usize; + let mut names = Vec::with_capacity(count); + let mut i = 1; + + for _ in 0..count { + let remaining = payload.len(); + let need_header = i + 2; + if need_header > remaining { + break; + } + + let _layer_index = payload[i]; + let name_len = payload[i + 1] as usize; + i += 2; + + let need_name = i + name_len; + if need_name > payload.len() { + break; + } + + let name_bytes = &payload[i..i + name_len]; + let name_lossy = String::from_utf8_lossy(name_bytes); + let name = name_lossy.to_string(); + names.push(name); + i += name_len; + } + + let found_names = !names.is_empty(); + if found_names { + return Ok(names); + } + } + + // Legacy fallback: try C> binary protocol + let legacy_result = self.query_legacy_binary("LAYOUTS?"); + match legacy_result { + Ok((cmd_type, data)) => { + let is_layout_type = cmd_type == 4; + let has_data = !data.is_empty(); + + if is_layout_type && has_data { + let text = String::from_utf8_lossy(&data); + let parts = text.split(';'); + let non_empty = parts.filter(|s| !s.is_empty()); + let trimmed_names = non_empty.map(|s| { + let long_enough = s.len() > 1; + if long_enough { + s[1..].to_string() + } else { + s.to_string() + } + }); + let names: Vec = trimmed_names.collect(); + + let found_names = !names.is_empty(); + if found_names { + return Ok(names); + } + } + } + Err(_) => {} + } + + // Last resort: raw text + self.send_command("LAYOUTS?")?; + std::thread::sleep(Duration::from_millis(LEGACY_BINARY_SETTLE_MS * 2)); + let raw = self.read_raw(BINARY_READ_TIMEOUT_MS)?; + + let text = String::from_utf8_lossy(&raw); + let split_by_delimiters = text.split(|c: char| c == ';' || c == '\n'); + + let cleaned = split_by_delimiters.map(|s| { + let step1 = s.trim(); + let step2 = step1.trim_matches(|c: char| c.is_control() || c == '"'); + step2 + }); + + let valid_names = cleaned.filter(|s| { + let is_not_empty = !s.is_empty(); + let is_short_enough = s.len() < 30; + let no_header_marker = !s.contains("C>"); + let not_ok = *s != "OK"; + is_not_empty && is_short_enough && no_header_marker && not_ok + }); + + let as_strings = valid_names.map(|s| s.to_string()); + let names: Vec = as_strings.collect(); + + let found_any = !names.is_empty(); + if found_any { + Ok(names) + } else { + Err("No layer names found".into()) + } + } + + pub fn get_layout_json(&mut self) -> Result { + // Try v2 binary first + if self.v2 { + let resp = self.send_binary(bp::cmd::GET_LAYOUT_JSON, &[])?; + let has_payload = !resp.payload.is_empty(); + if has_payload { + let raw_bytes = &resp.payload; + let lossy_string = String::from_utf8_lossy(raw_bytes); + let json = lossy_string.to_string(); + return Ok(json); + } + } + + // Legacy: text brace-counting + self.send_command("LAYOUT?")?; + std::thread::sleep(Duration::from_millis(LEGACY_BINARY_SETTLE_MS)); + + let port = self.port.as_mut().ok_or("Not connected")?; + let mut result = String::new(); + let mut buf = [0u8; 4096]; + let start = Instant::now(); + let max_wait = Duration::from_secs(JSON_TIMEOUT_SECS); + let mut brace_count: i32 = 0; + let mut started = false; + + while start.elapsed() < max_wait { + let read_result = port.read(&mut buf); + + match read_result { + Ok(n) if n > 0 => { + let received_bytes = &buf[..n]; + let chunk = String::from_utf8_lossy(received_bytes); + + for ch in chunk.chars() { + let is_open_brace = ch == '{'; + if is_open_brace { + started = true; + brace_count += 1; + } + + if started { + result.push(ch); + } + + let is_close_brace = ch == '}'; + if is_close_brace && started { + brace_count -= 1; + let json_complete = brace_count == 0; + if json_complete { + return Ok(result); + } + } + } + } + _ => { + std::thread::sleep(Duration::from_millis(10)); + } + } + } + + let got_nothing = result.is_empty(); + if got_nothing { + Err("No JSON".into()) + } else { + Err("Incomplete JSON".into()) + } + } +} + +/// Thread-safe wrapper +pub type SharedSerial = Arc>; + +pub fn new_shared() -> SharedSerial { + let manager = SerialManager::new(); + let mutex = Mutex::new(manager); + let shared = Arc::new(mutex); + shared +} diff --git a/src/logic/settings.rs b/src/logic/settings.rs new file mode 100644 index 0000000..531200f --- /dev/null +++ b/src/logic/settings.rs @@ -0,0 +1,118 @@ +/// Persistent settings for KaSe Controller. +/// +/// - **Native**: `kase_settings.json` next to the executable. +/// - **WASM**: `localStorage` under key `"kase_settings"`. +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Settings { + #[serde(default = "default_layout")] + pub keyboard_layout: String, +} + +fn default_layout() -> String { + "QWERTY".to_string() +} + +impl Default for Settings { + fn default() -> Self { + Self { + keyboard_layout: default_layout(), + } + } +} + +// ============================================================================= +// Native: JSON file next to executable +// ============================================================================= + +#[cfg(not(target_arch = "wasm32"))] +mod native_settings { + use super::Settings; + use std::path::PathBuf; + + /// Config file path: next to the executable. + fn settings_path() -> PathBuf { + let exe = std::env::current_exe().unwrap_or_default(); + let parent_dir = exe.parent().unwrap_or(std::path::Path::new(".")); + parent_dir.join("kase_settings.json") + } + + pub fn load() -> Settings { + let path = settings_path(); + let json_content = std::fs::read_to_string(&path).ok(); + let parsed = json_content.and_then(|s| serde_json::from_str(&s).ok()); + parsed.unwrap_or_default() + } + + pub fn save(settings: &Settings) { + let path = settings_path(); + let json_result = serde_json::to_string_pretty(settings); + if let Ok(json) = json_result { + let _ = std::fs::write(path, json); + } + } +} + +// ============================================================================= +// WASM: browser localStorage +// ============================================================================= + +#[cfg(target_arch = "wasm32")] +mod web_settings { + use super::Settings; + + const STORAGE_KEY: &str = "kase_settings"; + + /// Get localStorage. Returns None if not in a browser context. + fn get_storage() -> Option { + let window = web_sys::window()?; + window.local_storage().ok().flatten() + } + + pub fn load() -> Settings { + let storage = match get_storage() { + Some(s) => s, + None => return Settings::default(), + }; + + let json_option = storage.get_item(STORAGE_KEY).ok().flatten(); + let parsed = json_option.and_then(|s| serde_json::from_str(&s).ok()); + parsed.unwrap_or_default() + } + + pub fn save(settings: &Settings) { + let storage = match get_storage() { + Some(s) => s, + None => return, + }; + + let json_result = serde_json::to_string(settings); + if let Ok(json) = json_result { + let _ = storage.set_item(STORAGE_KEY, &json); + } + } +} + +// ============================================================================= +// Public interface +// ============================================================================= + +/// Load settings from persistent storage. +/// Returns `Settings::default()` if none found. +pub fn load() -> Settings { + #[cfg(not(target_arch = "wasm32"))] + return native_settings::load(); + + #[cfg(target_arch = "wasm32")] + return web_settings::load(); +} + +/// Save settings to persistent storage. Fails silently. +pub fn save(settings: &Settings) { + #[cfg(not(target_arch = "wasm32"))] + native_settings::save(settings); + + #[cfg(target_arch = "wasm32")] + web_settings::save(settings); +} diff --git a/src/logic/stats_analyzer.rs b/src/logic/stats_analyzer.rs new file mode 100644 index 0000000..a48cfae --- /dev/null +++ b/src/logic/stats_analyzer.rs @@ -0,0 +1,335 @@ +/// Stats analysis: hand balance, finger load, row usage, top keys, bigrams. +/// Transforms raw heatmap data into structured analysis for the stats tab. + +use super::keycode; +use super::parsers::ROWS; + +/// Which hand a column belongs to. +#[derive(Clone, Copy, PartialEq)] +pub enum Hand { + Left, + Right, +} + +/// Which finger a column belongs to. +#[derive(Clone, Copy, PartialEq)] +pub enum Finger { + Pinky, + Ring, + Middle, + Index, + Thumb, +} + +/// Row names for the 5 rows. +const ROW_NAMES: [&str; 5] = ["Number", "Upper", "Home", "Lower", "Thumb"]; + +/// Finger names (French). +const FINGER_NAMES: [&str; 5] = ["Pinky", "Ring", "Middle", "Index", "Thumb"]; + +/// Map column index → (Hand, Finger). +/// KaSe layout: cols 0-5 = left hand, cols 6 (gap), cols 7-12 = right hand. +fn col_to_hand_finger(col: usize) -> (Hand, Finger) { + match col { + 0 => (Hand::Left, Finger::Pinky), + 1 => (Hand::Left, Finger::Ring), + 2 => (Hand::Left, Finger::Middle), + 3 => (Hand::Left, Finger::Index), + 4 => (Hand::Left, Finger::Index), // inner column, still index + 5 => (Hand::Left, Finger::Thumb), + 6 => (Hand::Left, Finger::Thumb), // center / gap + 7 => (Hand::Right, Finger::Thumb), + 8 => (Hand::Right, Finger::Index), + 9 => (Hand::Right, Finger::Index), // inner column + 10 => (Hand::Right, Finger::Middle), + 11 => (Hand::Right, Finger::Ring), + 12 => (Hand::Right, Finger::Pinky), + _ => (Hand::Left, Finger::Pinky), + } +} + +/// Hand balance result. +#[allow(dead_code)] +pub struct HandBalance { + pub left_count: u32, + pub right_count: u32, + pub total: u32, + pub left_pct: f32, + pub right_pct: f32, +} + +/// Finger load for one finger. +#[allow(dead_code)] +pub struct FingerLoad { + pub name: String, + pub hand: Hand, + pub count: u32, + pub pct: f32, +} + +/// Row usage for one row. +#[allow(dead_code)] +pub struct RowUsage { + pub name: String, + pub row: usize, + pub count: u32, + pub pct: f32, +} + +/// A key in the top keys ranking. +#[allow(dead_code)] +pub struct TopKey { + pub name: String, + pub finger: String, + pub count: u32, + pub pct: f32, +} + +/// Compute hand balance from heatmap data. +pub fn hand_balance(heatmap: &[Vec]) -> HandBalance { + let mut left: u32 = 0; + let mut right: u32 = 0; + + for row in heatmap { + for (c, &count) in row.iter().enumerate() { + let (hand, _) = col_to_hand_finger(c); + match hand { + Hand::Left => left += count, + Hand::Right => right += count, + } + } + } + + let total = left + right; + let left_pct = if total > 0 { left as f32 / total as f32 * 100.0 } else { 0.0 }; + let right_pct = if total > 0 { right as f32 / total as f32 * 100.0 } else { 0.0 }; + + HandBalance { left_count: left, right_count: right, total, left_pct, right_pct } +} + +/// Compute finger load (10 fingers: 5 left + 5 right). +pub fn finger_load(heatmap: &[Vec]) -> Vec { + let mut counts = [[0u32; 5]; 2]; // [hand][finger] + + for row in heatmap { + for (c, &count) in row.iter().enumerate() { + let (hand, finger) = col_to_hand_finger(c); + let hi = if hand == Hand::Left { 0 } else { 1 }; + let fi = match finger { + Finger::Pinky => 0, + Finger::Ring => 1, + Finger::Middle => 2, + Finger::Index => 3, + Finger::Thumb => 4, + }; + counts[hi][fi] += count; + } + } + + let total: u32 = counts[0].iter().sum::() + counts[1].iter().sum::(); + let mut result = Vec::with_capacity(10); + + // Left hand fingers + for fi in 0..5 { + let count = counts[0][fi]; + let pct = if total > 0 { count as f32 / total as f32 * 100.0 } else { 0.0 }; + let name = format!("{} L", FINGER_NAMES[fi]); + result.push(FingerLoad { name, hand: Hand::Left, count, pct }); + } + + // Right hand fingers + for fi in 0..5 { + let count = counts[1][fi]; + let pct = if total > 0 { count as f32 / total as f32 * 100.0 } else { 0.0 }; + let name = format!("{} R", FINGER_NAMES[fi]); + result.push(FingerLoad { name, hand: Hand::Right, count, pct }); + } + + result +} + +/// Compute row usage. +pub fn row_usage(heatmap: &[Vec]) -> Vec { + let mut row_counts = [0u32; ROWS]; + + for (r, row) in heatmap.iter().enumerate() { + if r >= ROWS { break; } + let row_sum: u32 = row.iter().sum(); + row_counts[r] = row_sum; + } + + let total: u32 = row_counts.iter().sum(); + let mut result = Vec::with_capacity(ROWS); + + for r in 0..ROWS { + let count = row_counts[r]; + let pct = if total > 0 { count as f32 / total as f32 * 100.0 } else { 0.0 }; + let name = ROW_NAMES[r].to_string(); + result.push(RowUsage { name, row: r, count, pct }); + } + + result +} + +/// Compute top N keys by press count. +pub fn top_keys(heatmap: &[Vec], keymap: &[Vec], n: usize) -> Vec { + let mut all_keys: Vec<(u32, usize, usize)> = Vec::new(); // (count, row, col) + + for (r, row) in heatmap.iter().enumerate() { + for (c, &count) in row.iter().enumerate() { + if count > 0 { + all_keys.push((count, r, c)); + } + } + } + + all_keys.sort_by(|a, b| b.0.cmp(&a.0)); + all_keys.truncate(n); + + let total: u32 = heatmap.iter().flat_map(|r| r.iter()).sum(); + + let mut result = Vec::with_capacity(n); + for (count, r, c) in all_keys { + let code = keymap.get(r).and_then(|row| row.get(c)).copied().unwrap_or(0); + let name = keycode::decode_keycode(code); + let (hand, finger) = col_to_hand_finger(c); + let hand_str = if hand == Hand::Left { "L" } else { "R" }; + let finger_str = match finger { + Finger::Pinky => "Pinky", + Finger::Ring => "Ring", + Finger::Middle => "Middle", + Finger::Index => "Index", + Finger::Thumb => "Thumb", + }; + let finger_label = format!("{} {}", finger_str, hand_str); + let pct = if total > 0 { count as f32 / total as f32 * 100.0 } else { 0.0 }; + result.push(TopKey { name, finger: finger_label, count, pct }); + } + + result +} + +/// Find keys that have never been pressed (count = 0, keycode != 0). +pub fn dead_keys(heatmap: &[Vec], keymap: &[Vec]) -> Vec { + let mut result = Vec::new(); + for (r, row) in heatmap.iter().enumerate() { + for (c, &count) in row.iter().enumerate() { + if count > 0 { continue; } + let code = keymap.get(r).and_then(|row| row.get(c)).copied().unwrap_or(0); + let is_mapped = code != 0; + if is_mapped { + let name = keycode::decode_keycode(code); + result.push(name); + } + } + } + result +} + +// ==================== Bigram analysis ==================== + +/// A parsed bigram entry. +pub struct BigramEntry { + pub from_row: u8, + pub from_col: u8, + pub to_row: u8, + pub to_col: u8, + pub count: u32, +} + +/// Bigram analysis results. +#[allow(dead_code)] +pub struct BigramAnalysis { + pub total: u32, + pub alt_hand: u32, + pub same_hand: u32, + pub sfb: u32, + pub alt_hand_pct: f32, + pub same_hand_pct: f32, + pub sfb_pct: f32, +} + +/// Parse bigram text lines from firmware. +/// Format: " R2C3 -> R2C4 : 150" +pub fn parse_bigram_lines(lines: &[String]) -> Vec { + let mut entries = Vec::new(); + + for line in lines { + let trimmed = line.trim(); + let has_arrow = trimmed.contains("->"); + if !has_arrow { continue; } + + let parts: Vec<&str> = trimmed.split("->").collect(); + if parts.len() != 2 { continue; } + + let left = parts[0].trim(); + let right_and_count = parts[1].trim(); + + let right_parts: Vec<&str> = right_and_count.split(':').collect(); + if right_parts.len() != 2 { continue; } + + let right = right_parts[0].trim(); + let count_str = right_parts[1].trim(); + + let from = parse_rc(left); + let to = parse_rc(right); + let count: u32 = count_str.parse().unwrap_or(0); + + if let (Some((fr, fc)), Some((tr, tc))) = (from, to) { + entries.push(BigramEntry { + from_row: fr, from_col: fc, + to_row: tr, to_col: tc, + count, + }); + } + } + + entries +} + +/// Parse "R2C3" into (row, col). +fn parse_rc(s: &str) -> Option<(u8, u8)> { + let s = s.trim(); + let r_pos = s.find('R')?; + let c_pos = s.find('C')?; + if c_pos <= r_pos { return None; } + + let row_str = &s[r_pos + 1..c_pos]; + let col_str = &s[c_pos + 1..]; + let row: u8 = row_str.parse().ok()?; + let col: u8 = col_str.parse().ok()?; + Some((row, col)) +} + +/// Analyze bigram entries for hand alternation and SFB. +pub fn analyze_bigrams(entries: &[BigramEntry]) -> BigramAnalysis { + let mut alt_hand: u32 = 0; + let mut same_hand: u32 = 0; + let mut sfb: u32 = 0; + let mut total: u32 = 0; + + for entry in entries { + let (hand_from, finger_from) = col_to_hand_finger(entry.from_col as usize); + let (hand_to, finger_to) = col_to_hand_finger(entry.to_col as usize); + + total += entry.count; + + if hand_from != hand_to { + alt_hand += entry.count; + } else { + same_hand += entry.count; + if finger_from == finger_to { + sfb += entry.count; + } + } + } + + let alt_hand_pct = if total > 0 { alt_hand as f32 / total as f32 * 100.0 } else { 0.0 }; + let same_hand_pct = if total > 0 { same_hand as f32 / total as f32 * 100.0 } else { 0.0 }; + let sfb_pct = if total > 0 { sfb as f32 / total as f32 * 100.0 } else { 0.0 }; + + BigramAnalysis { + total, alt_hand, same_hand, sfb, + alt_hand_pct, same_hand_pct, sfb_pct, + } +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..411533b --- /dev/null +++ b/src/main.rs @@ -0,0 +1,1612 @@ +mod logic; + +slint::include_modules!(); + +use logic::keycode; +use logic::layout::KeycapPos; +use logic::serial::SerialManager; +use slint::{Model, ModelRc, SharedString, VecModel}; +use std::sync::mpsc; +use std::sync::{Arc, Mutex}; +use std::rc::Rc; + +// Messages from background serial thread to UI +enum BgMsg { + Connected(String, String, Vec, Vec>), // port, fw_version, layer_names, keymap + ConnectError(String), + Keymap(Vec>), + LayerNames(Vec), + Disconnected, + TextLines(String, Vec), // tag, lines + HeatmapData(Vec>, u32), // counts, max + BigramLines(Vec), + LayoutJson(Vec), // physical key positions from firmware + FlashProgress(f32, String), // progress 0-1, status message + FlashDone(Result<(), String>), +} + +fn build_keycap_model(keys: &[KeycapPos]) -> Rc> { + let keycaps: Vec = keys + .iter() + .enumerate() + .map(|(idx, kp)| KeycapData { + x: kp.x, + y: kp.y, + w: kp.w, + h: kp.h, + rotation: kp.angle, + rotation_cx: kp.w / 2.0, + rotation_cy: kp.h / 2.0, + label: SharedString::from(format!("{},{}", kp.col, kp.row)), + sublabel: SharedString::default(), + keycode: 0, + color: slint::Color::from_argb_u8(255, 0x44, 0x47, 0x5a), + heat: 0.0, + selected: false, + index: idx as i32, + }) + .collect(); + Rc::new(VecModel::from(keycaps)) +} + +fn build_layer_model(names: &[String]) -> Rc> { + let layers: Vec = names + .iter() + .enumerate() + .map(|(i, name)| LayerInfo { + index: i as i32, + name: SharedString::from(name.as_str()), + active: i == 0, + }) + .collect(); + Rc::new(VecModel::from(layers)) +} + +/// Update keycap labels from keymap data (row x col -> keycode -> label) +fn update_keycap_labels( + keycap_model: &impl Model, + keys: &[KeycapPos], + keymap: &[Vec], + layout: &logic::layout_remap::KeyboardLayout, +) { + for i in 0..keycap_model.row_count() { + if i >= keys.len() { break; } + let mut item = keycap_model.row_data(i).unwrap(); + let kp = &keys[i]; + let row = kp.row as usize; + let col = kp.col as usize; + + if row < keymap.len() && col < keymap[row].len() { + let code = keymap[row][col]; + let decoded = keycode::decode_keycode(code); + let remapped = logic::layout_remap::remap_key_label(layout, &decoded); + let label = remapped.unwrap_or(&decoded).to_string(); + item.keycode = code as i32; + item.label = SharedString::from(label); + item.sublabel = if decoded != format!("0x{:04X}", code) { + SharedString::default() + } else { + SharedString::from(format!("0x{:04X}", code)) + }; + } + keycap_model.set_row_data(i, item); + } +} + +/// Build the list of all selectable HID keycodes for the key selector +fn build_key_entries() -> Rc> { + let mut entries = Vec::new(); + + // Letters + for code in 0x04u16..=0x1D { + entries.push(KeyEntry { + name: SharedString::from(keycode::hid_key_name(code as u8)), + code: code as i32, + category: SharedString::from("Letter"), + }); + } + // Numbers + for code in 0x1Eu16..=0x27 { + entries.push(KeyEntry { + name: SharedString::from(keycode::hid_key_name(code as u8)), + code: code as i32, + category: SharedString::from("Number"), + }); + } + // Control keys + for code in [0x28u16, 0x29, 0x2A, 0x2B, 0x2C] { + entries.push(KeyEntry { + name: SharedString::from(keycode::hid_key_name(code as u8)), + code: code as i32, + category: SharedString::from("Control"), + }); + } + // Punctuation + for code in 0x2Du16..=0x38 { + entries.push(KeyEntry { + name: SharedString::from(keycode::hid_key_name(code as u8)), + code: code as i32, + category: SharedString::from("Symbol"), + }); + } + // F keys + for code in 0x3Au16..=0x45 { + entries.push(KeyEntry { + name: SharedString::from(keycode::hid_key_name(code as u8)), + code: code as i32, + category: SharedString::from("Function"), + }); + } + // Navigation + for code in [0x46u16, 0x47, 0x48, 0x49, 0x4A, 0x4B, 0x4C, 0x4D, 0x4E, 0x4F, 0x50, 0x51, 0x52] { + entries.push(KeyEntry { + name: SharedString::from(keycode::hid_key_name(code as u8)), + code: code as i32, + category: SharedString::from("Navigation"), + }); + } + // Modifiers + for code in 0xE0u16..=0xE7 { + entries.push(KeyEntry { + name: SharedString::from(keycode::hid_key_name(code as u8)), + code: code as i32, + category: SharedString::from("Modifier"), + }); + } + // Caps Lock + entries.push(KeyEntry { + name: SharedString::from("Caps Lock"), + code: 0x39, + category: SharedString::from("Control"), + }); + // Keypad + for code in 0x53u16..=0x63 { + entries.push(KeyEntry { + name: SharedString::from(keycode::hid_key_name(code as u8)), + code: code as i32, + category: SharedString::from("Keypad"), + }); + } + // F13-F24 + for code in 0x68u16..=0x73 { + entries.push(KeyEntry { + name: SharedString::from(keycode::hid_key_name(code as u8)), + code: code as i32, + category: SharedString::from("Function"), + }); + } + // Media + for code in [0x7Fu16, 0x80, 0x81] { + entries.push(KeyEntry { + name: SharedString::from(keycode::hid_key_name(code as u8)), + code: code as i32, + category: SharedString::from("Media"), + }); + } + // BT keys + for (code, name) in [ + (0x2900u16, "BT Next"), (0x2A00, "BT Prev"), (0x2B00, "BT Pair"), + (0x2C00, "BT Disc"), (0x2E00, "USB/BT"), (0x2F00, "BT On/Off"), + ] { + entries.push(KeyEntry { + name: SharedString::from(name), + code: code as i32, + category: SharedString::from("Bluetooth"), + }); + } + // Tap Dance 0..7 + for i in 0u16..=7 { + let code = (0x60 | i) << 8; + entries.push(KeyEntry { + name: SharedString::from(format!("TD {}", i)), + code: code as i32, + category: SharedString::from("Tap Dance"), + }); + } + // Macro 0..9 + for i in 0u16..=9 { + let code = (i + 0x15) << 8; + entries.push(KeyEntry { + name: SharedString::from(format!("M{}", i)), + code: code as i32, + category: SharedString::from("Macro"), + }); + } + // OSL 0..9 + for i in 0u16..=9 { + entries.push(KeyEntry { + name: SharedString::from(format!("OSL {}", i)), + code: 0x3100 + i as i32, + category: SharedString::from("Layer"), + }); + } + // Layer: MO 0..9 + for layer in 0u16..=9 { + let code = (layer + 1) << 8; + entries.push(KeyEntry { + name: SharedString::from(format!("MO {}", layer)), + code: code as i32, + category: SharedString::from("Layer"), + }); + } + // Layer: TO 0..9 + for layer in 0u16..=9 { + let code = (layer + 0x0B) << 8; + entries.push(KeyEntry { + name: SharedString::from(format!("TO {}", layer)), + code: code as i32, + category: SharedString::from("Layer"), + }); + } + // Special KaSe firmware keys + for (code, name) in [ + (0x3200u16, "Caps Word"), + (0x3300, "Repeat"), + (0x3400, "Leader"), + (0x3900, "GEsc"), + (0x3A00, "Layer Lock"), + (0x3C00, "AS Toggle"), + ] { + entries.push(KeyEntry { + name: SharedString::from(name), + code: code as i32, + category: SharedString::from("Special"), + }); + } + // None + entries.insert(0, KeyEntry { + name: SharedString::from("None"), + code: 0, + category: SharedString::from("Special"), + }); + + Rc::new(VecModel::from(entries)) +} + +fn populate_key_categories(window: &MainWindow, all_keys: &VecModel, search: &str) { + let search_lower = search.to_lowercase(); + let filter = |cat: &str| -> Vec { + (0..all_keys.row_count()) + .filter_map(|i| { + let e = all_keys.row_data(i).unwrap(); + let cat_match = e.category.as_str() == cat + || (cat == "Navigation" && (e.category.as_str() == "Control" || e.category.as_str() == "Navigation")) + || (cat == "Special" && (e.category.as_str() == "Special" || e.category.as_str() == "Bluetooth" || e.category.as_str() == "Media")) + || (cat == "TDMacro" && (e.category.as_str() == "Tap Dance" || e.category.as_str() == "Macro")); + let search_match = search_lower.is_empty() + || e.name.to_lowercase().contains(&search_lower) + || e.category.to_lowercase().contains(&search_lower); + if cat_match && search_match { Some(e) } else { None } + }) + .collect() + }; + let set = |model: Vec| ModelRc::from(Rc::new(VecModel::from(model))); + let ks = window.global::(); + ks.set_cat_letters(set(filter("Letter"))); + ks.set_cat_numbers(set(filter("Number"))); + ks.set_cat_modifiers(set(filter("Modifier"))); + ks.set_cat_nav(set(filter("Navigation"))); + ks.set_cat_function(set(filter("Function"))); + ks.set_cat_symbols(set(filter("Symbol"))); + ks.set_cat_layers(set(filter("Layer"))); + ks.set_cat_special(set(filter("Special"))); + ks.set_cat_td_macro(set(filter("TDMacro"))); +} + +/// Map ComboBox index [None,Ctrl,Shift,Alt,GUI,RCtrl,RShift,RAlt,RGUI] to HID mod byte +fn mod_idx_to_byte(idx: i32) -> u8 { + match idx { + 1 => 0x01, // Ctrl + 2 => 0x02, // Shift + 3 => 0x04, // Alt + 4 => 0x08, // GUI + 5 => 0x10, // RCtrl + 6 => 0x20, // RShift + 7 => 0x40, // RAlt + 8 => 0x80, // RGUI + _ => 0x00, // None + } +} + +fn main() { + let keys = logic::layout::default_layout(); + let keys_arc: Rc>> = Rc::new(std::cell::RefCell::new(keys.clone())); + + let window = MainWindow::new().unwrap(); + + // Set up initial keymap models + let keymap_bridge = window.global::(); + keymap_bridge.set_keycaps(ModelRc::from(build_keycap_model(&keys))); + keymap_bridge.set_layers(ModelRc::from(build_layer_model(&[ + "Layer 0".into(), "Layer 1".into(), "Layer 2".into(), "Layer 3".into(), + ]))); + // Compute initial content bounds + { + let mut max_x: f32 = 0.0; + let mut max_y: f32 = 0.0; + for kp in &keys { + let right = kp.x + kp.w; + let bottom = kp.y + kp.h; + if right > max_x { max_x = right; } + if bottom > max_y { max_y = bottom; } + } + keymap_bridge.set_content_width(max_x); + keymap_bridge.set_content_height(max_y); + } + + // Set up settings bridge + { + let layouts: Vec = logic::layout_remap::KeyboardLayout::all() + .iter() + .map(|l| SharedString::from(l.name())) + .collect(); + let layout_model = Rc::new(VecModel::from(layouts)); + window.global::().set_available_layouts(ModelRc::from(layout_model)); + } + + // Set up key selector + let all_keys = build_key_entries(); + window.global::().set_all_keys(ModelRc::from(all_keys.clone())); + populate_key_categories(&window, &all_keys, ""); + + // Serial manager shared between threads + let serial: Arc> = Arc::new(Mutex::new(SerialManager::new())); + let (bg_tx, bg_rx) = mpsc::channel::(); + + // Current state + let current_keymap: Rc>>> = Rc::new(std::cell::RefCell::new(Vec::new())); + let current_layer: Rc> = Rc::new(std::cell::Cell::new(0)); + let saved_settings = logic::settings::load(); + let keyboard_layout = Rc::new(std::cell::RefCell::new( + logic::layout_remap::KeyboardLayout::from_name(&saved_settings.keyboard_layout), + )); + + // Set initial layout index + { + let all_layouts = logic::layout_remap::KeyboardLayout::all(); + let current = *keyboard_layout.borrow(); + let idx = all_layouts.iter().position(|l| *l == current).unwrap_or(0); + window.global::().set_selected_layout_index(idx as i32); + } + + // Heatmap data (for stats) + let heatmap_data: Rc>>> = Rc::new(std::cell::RefCell::new(Vec::new())); + + // --- Auto-connect on startup --- + { + let serial = serial.clone(); + let tx = bg_tx.clone(); + window.global::().set_status_text("Scanning ports...".into()); + window.global::().set_connection(ConnectionState::Connecting); + + std::thread::spawn(move || { + let mut ser = serial.lock().unwrap_or_else(|e| e.into_inner()); + match ser.auto_connect() { + Ok(port_name) => { + let fw = ser.get_firmware_version().unwrap_or_default(); + let names = ser.get_layer_names().unwrap_or_default(); + let km = ser.get_keymap(0).unwrap_or_default(); + let _ = tx.send(BgMsg::Connected(port_name, fw, names, km)); + // Fetch physical layout from firmware + match ser.get_layout_json() { + Ok(json) => { + match logic::layout::parse_json(&json) { + Ok(keys) => { let _ = tx.send(BgMsg::LayoutJson(keys)); } + Err(e) => eprintln!("Layout parse error: {}", e), + } + } + Err(e) => eprintln!("get_layout_json error: {}", e), + } + } + Err(e) => { + let _ = tx.send(BgMsg::ConnectError(e)); + } + } + }); + } + + // --- Key selection callback --- + { + let window_weak = window.as_weak(); + keymap_bridge.on_select_key(move |key_index| { + let Some(w) = window_weak.upgrade() else { return }; + let keycaps = w.global::().get_keycaps(); + let idx = key_index as usize; + if idx >= keycaps.row_count() { return; } + for i in 0..keycaps.row_count() { + let mut item = keycaps.row_data(i).unwrap(); + let should_select = i == idx; + if item.selected != should_select { + item.selected = should_select; + keycaps.set_row_data(i, item); + } + } + let bridge = w.global::(); + bridge.set_selected_key_index(key_index); + let item = keycaps.row_data(idx).unwrap(); + bridge.set_selected_key_label(item.label.clone()); + }); + } + + // --- Layer switch callback --- + { + let serial = serial.clone(); + let tx = bg_tx.clone(); + let current_layer = current_layer.clone(); + let window_weak = window.as_weak(); + + keymap_bridge.on_switch_layer(move |layer_index| { + let idx = layer_index as usize; + current_layer.set(idx); + + // Update active flag on the CURRENT model (not a captured stale ref) + if let Some(w) = window_weak.upgrade() { + let layers = w.global::().get_layers(); + for i in 0..layers.row_count() { + let mut item = layers.row_data(i).unwrap(); + let should_be_active = item.index == layer_index; + if item.active != should_be_active { + item.active = should_be_active; + layers.set_row_data(i, item); + } + } + w.global::().set_status_text(SharedString::from(format!("Loading layer {}...", idx))); + } + let serial = serial.clone(); + let tx = tx.clone(); + std::thread::spawn(move || { + let mut ser = serial.lock().unwrap_or_else(|e| e.into_inner()); + match ser.get_keymap(idx as u8) { + Ok(km) => { let _ = tx.send(BgMsg::Keymap(km)); } + Err(e) => { let _ = tx.send(BgMsg::ConnectError(e)); } + } + }); + }); + } + + // --- Layer rename callback --- + { + let serial = serial.clone(); + let tx = bg_tx.clone(); + let window_weak = window.as_weak(); + + keymap_bridge.on_rename_layer(move |layer_idx, new_name| { + let cmd = logic::protocol::cmd_set_layer_name(layer_idx as u8, &new_name); + let serial = serial.clone(); + let tx = tx.clone(); + std::thread::spawn(move || { + let mut ser = serial.lock().unwrap_or_else(|e| e.into_inner()); + let _ = ser.send_command(&cmd); + if let Ok(names) = ser.get_layer_names() { + let _ = tx.send(BgMsg::LayerNames(names)); + } + }); + if let Some(w) = window_weak.upgrade() { + w.global::().set_status_text( + SharedString::from(format!("Renamed layer {} → {}", layer_idx, new_name)) + ); + } + }); + } + + // --- Connect/Disconnect callbacks --- + { + let serial_c = serial.clone(); + let tx_c = bg_tx.clone(); + let window_weak = window.as_weak(); + window.global::().on_connect(move || { + if let Some(w) = window_weak.upgrade() { + w.global::().set_status_text("Scanning ports...".into()); + w.global::().set_connection(ConnectionState::Connecting); + } + let serial = serial_c.clone(); + let tx = tx_c.clone(); + std::thread::spawn(move || { + let mut ser = serial.lock().unwrap_or_else(|e| e.into_inner()); + match ser.auto_connect() { + Ok(port_name) => { + let fw = ser.get_firmware_version().unwrap_or_default(); + let names = ser.get_layer_names().unwrap_or_default(); + let km = ser.get_keymap(0).unwrap_or_default(); + let _ = tx.send(BgMsg::Connected(port_name, fw, names, km)); + match ser.get_layout_json() { + Ok(json) => { + match logic::layout::parse_json(&json) { + Ok(keys) => { let _ = tx.send(BgMsg::LayoutJson(keys)); } + Err(e) => eprintln!("Layout parse error: {}", e), + } + } + Err(e) => eprintln!("get_layout_json error: {}", e), + } + } + Err(e) => { let _ = tx.send(BgMsg::ConnectError(e)); } + } + }); + }); + } + + { + let serial_d = serial.clone(); + let tx_d = bg_tx.clone(); + window.global::().on_disconnect(move || { + let mut ser = serial_d.lock().unwrap_or_else(|e| e.into_inner()); + ser.disconnect(); + let _ = tx_d.send(BgMsg::Disconnected); + }); + } + + window.global::().on_refresh_ports(|| {}); + + // --- Settings: change layout --- + { + let keyboard_layout = keyboard_layout.clone(); + let keys_arc = keys_arc.clone(); + let current_keymap = current_keymap.clone(); + let window_weak = window.as_weak(); + + window.global::().on_change_layout(move |idx| { + let all_layouts = logic::layout_remap::KeyboardLayout::all(); + let idx = idx as usize; + if idx >= all_layouts.len() { return; } + + let new_layout = all_layouts[idx]; + *keyboard_layout.borrow_mut() = new_layout; + + let settings = logic::settings::Settings { + keyboard_layout: new_layout.name().to_string(), + }; + logic::settings::save(&settings); + + let km = current_keymap.borrow(); + let keys = keys_arc.borrow(); + if let Some(w) = window_weak.upgrade() { + if !km.is_empty() { + let keycaps = w.global::().get_keycaps(); + update_keycap_labels(&keycaps, &keys, &km, &new_layout); + } + } + + if let Some(w) = window_weak.upgrade() { + w.global::().set_status_text( + SharedString::from(format!("Layout: {}", new_layout.name())) + ); + } + }); + } + + // --- Key selector: filter --- + { + let all_keys = all_keys.clone(); + let window_weak = window.as_weak(); + + window.global::().on_apply_filter(move |search| { + if let Some(w) = window_weak.upgrade() { + populate_key_categories(&w, &all_keys, &search); + } + }); + } + + // --- Key selector: shared apply logic --- + // Wraps keycode application in a closure shared by all key selector actions. + let apply_keycode = { + let serial = serial.clone(); + let keys_arc = keys_arc.clone(); + let current_keymap = current_keymap.clone(); + let current_layer = current_layer.clone(); + let keyboard_layout = keyboard_layout.clone(); + let window_weak = window.as_weak(); + + Rc::new(move |code: u16| { + let Some(w) = window_weak.upgrade() else { return }; + let key_idx = w.global::().get_selected_key_index(); + if key_idx < 0 { return; } + let key_idx = key_idx as usize; + let keys = keys_arc.borrow(); + if key_idx >= keys.len() { return; } + + let kp = &keys[key_idx]; + let row = kp.row as usize; + let col = kp.col as usize; + drop(keys); + let layer = current_layer.get() as u8; + + { + let mut km = current_keymap.borrow_mut(); + if row < km.len() && col < km[row].len() { + km[row][col] = code; + } + } + + // Clone out of RefCells to avoid holding borrows across bridge calls + let layout = *keyboard_layout.borrow(); + let km = current_keymap.borrow().clone(); + let keys = keys_arc.borrow().clone(); + let keycaps = w.global::().get_keycaps(); + update_keycap_labels(&keycaps, &keys, &km, &layout); + + let cmd = logic::protocol::cmd_set_key(layer, row as u8, col as u8, code); + let serial = serial.clone(); + std::thread::spawn(move || { + let mut ser = serial.lock().unwrap_or_else(|e| e.into_inner()); + let _ = ser.send_command(&cmd); + }); + + w.global::().set_status_text( + SharedString::from(format!("[{},{}] = 0x{:04X}", row, col, code)) + ); + }) + }; + + // Macro steps state (needed by dispatch_keycode) + let macro_steps: Rc>> = Rc::new(std::cell::RefCell::new(Vec::new())); + let refresh_macro_display = { + let macro_steps = macro_steps.clone(); + let window_weak = window.as_weak(); + Rc::new(move || { + let Some(w) = window_weak.upgrade() else { return }; + let steps = macro_steps.borrow(); + let display: Vec = steps.iter().map(|&(kc, _md)| { + if kc == 0xFF { + MacroStepDisplay { + label: SharedString::from(format!("T {}ms", _md as u32 * 10)), + is_delay: true, + } + } else { + MacroStepDisplay { + label: SharedString::from(keycode::hid_key_name(kc)), + is_delay: false, + } + } + }).collect(); + let text: Vec = steps.iter().map(|&(kc, md)| { + if kc == 0xFF { format!("T({})", md as u32 * 10) } + else { format!("D({:02X})", kc) } + }).collect(); + let mb = w.global::(); + mb.set_new_steps(ModelRc::from(Rc::new(VecModel::from(display)))); + mb.set_new_steps_text(SharedString::from(text.join(" "))); + }) + }; + + // Dispatch key selection based on target + let dispatch_keycode = { + let apply_keycode = apply_keycode.clone(); + let keys_arc = keys_arc.clone(); + let macro_steps = macro_steps.clone(); + let refresh_macro_display = refresh_macro_display.clone(); + let window_weak = window.as_weak(); + + Rc::new(move |code: u16| { + let Some(w) = window_weak.upgrade() else { return }; + let target = w.global::().get_selector_target(); + let name = SharedString::from(keycode::decode_keycode(code)); + + match target.as_str() { + "keymap" => { apply_keycode(code); } + "combo-result" => { + let adv = w.global::(); + adv.set_new_combo_result_code(code as i32); + adv.set_new_combo_result_name(name); + } + "ko-trigger" => { + let adv = w.global::(); + adv.set_new_ko_trigger_code(code as i32); + adv.set_new_ko_trigger_name(name); + } + "ko-result" => { + let adv = w.global::(); + adv.set_new_ko_result_code(code as i32); + adv.set_new_ko_result_name(name); + } + "leader-result" => { + let adv = w.global::(); + adv.set_new_leader_result_code(code as i32); + adv.set_new_leader_result_name(name); + } + "combo-key1" | "combo-key2" => { + // For combo key picking, the user clicked on the keyboard view. + // The selected key index tells us which physical key was clicked. + let key_idx = w.global::().get_selected_key_index(); + if key_idx >= 0 { + let keys = keys_arc.borrow(); + let idx = key_idx as usize; + if idx < keys.len() { + let kp = &keys[idx]; + let adv = w.global::(); + let label = SharedString::from(format!("R{}C{}", kp.row, kp.col)); + if target.as_str() == "combo-key1" { + adv.set_new_combo_r1(kp.row as i32); + adv.set_new_combo_c1(kp.col as i32); + adv.set_new_combo_key1_name(label); + } else { + adv.set_new_combo_r2(kp.row as i32); + adv.set_new_combo_c2(kp.col as i32); + adv.set_new_combo_key2_name(label); + } + } + } + } + "leader-seq" => { + let adv = w.global::(); + let count = adv.get_new_leader_seq_count(); + match count { + 0 => { adv.set_new_leader_seq0_code(code as i32); adv.set_new_leader_seq0_name(name); } + 1 => { adv.set_new_leader_seq1_code(code as i32); adv.set_new_leader_seq1_name(name); } + 2 => { adv.set_new_leader_seq2_code(code as i32); adv.set_new_leader_seq2_name(name); } + 3 => { adv.set_new_leader_seq3_code(code as i32); adv.set_new_leader_seq3_name(name); } + _ => {} + } + if count < 4 { adv.set_new_leader_seq_count(count + 1); } + } + "macro-step" => { + // Add key press (Down + Up) to macro steps + let mut steps = macro_steps.borrow_mut(); + steps.push((code as u8, 0x00)); // D(key) + drop(steps); + refresh_macro_display(); + } + _ => { apply_keycode(code); } + } + }) + }; + + // Key from list + { + let dispatch = dispatch_keycode.clone(); + window.global::().on_select_keycode(move |code| { + dispatch(code as u16); + }); + } + + // Hex input + { + let dispatch = dispatch_keycode.clone(); + window.global::().on_apply_hex(move |hex_str| { + if let Ok(code) = u16::from_str_radix(hex_str.trim(), 16) { + dispatch(code); + } + }); + } + + // Hex preview: decode keycode and show human-readable name + { + let window_weak = window.as_weak(); + window.global::().on_preview_hex(move |hex_str| { + let preview = u16::from_str_radix(hex_str.trim(), 16) + .map(|code| keycode::decode_keycode(code)) + .unwrap_or_default(); + if let Some(w) = window_weak.upgrade() { + w.global::().set_hex_preview(SharedString::from(preview)); + } + }); + } + + // MT builder: mod_combo_index maps to modifier nibble, key_combo_index maps to HID code + { + let dispatch = dispatch_keycode.clone(); + window.global::().on_apply_mt(move |mod_idx, key_idx| { + let mod_nibble: u16 = match mod_idx { + 0 => 0x01, // Ctrl + 1 => 0x02, // Shift + 2 => 0x04, // Alt + 3 => 0x08, // GUI + 4 => 0x10, // RCtrl + 5 => 0x20, // RShift + 6 => 0x40, // RAlt + 7 => 0x80, // RGUI + _ => 0x02, + }; + // ComboBox order: A-Z (0-25), 1-0 (26-35), Space(36), Enter(37), Esc(38), Tab(39), Bksp(40) + let hid: u16 = match key_idx { + 0..=25 => 0x04 + key_idx as u16, // A-Z + 26..=35 => 0x1E + (key_idx - 26) as u16, // 1-0 + 36 => 0x2C, // Space + 37 => 0x28, // Enter + 38 => 0x29, // Esc + 39 => 0x2B, // Tab + 40 => 0x2A, // Backspace + _ => 0x04, + }; + let code = 0x5000 | (mod_nibble << 8) | hid; + dispatch(code); + }); + } + + // LT builder: layer_combo_index = layer (0-9), key_combo_index maps to HID code + { + let dispatch = dispatch_keycode.clone(); + window.global::().on_apply_lt(move |layer_idx, key_idx| { + let layer = (layer_idx as u16) & 0x0F; + // ComboBox order: Space(0), Enter(1), Esc(2), Bksp(3), Tab(4), A-E(5-9) + let hid: u16 = match key_idx { + 0 => 0x2C, // Space + 1 => 0x28, // Enter + 2 => 0x29, // Esc + 3 => 0x2A, // Backspace + 4 => 0x2B, // Tab + 5..=9 => 0x04 + (key_idx - 5) as u16, // A-E + _ => 0x2C, + }; + let code = 0x4000 | (layer << 8) | hid; + dispatch(code); + }); + } + + // --- Stats: refresh --- + { + let serial = serial.clone(); + let tx = bg_tx.clone(); + + window.global::().on_refresh_stats(move || { + let serial = serial.clone(); + let tx = tx.clone(); + std::thread::spawn(move || { + let mut ser = serial.lock().unwrap_or_else(|e| e.into_inner()); + let lines = ser.query_command("KEYSTATS?").unwrap_or_default(); + let (data, max) = logic::parsers::parse_heatmap_lines(&lines); + let _ = tx.send(BgMsg::HeatmapData(data, max)); + // Also fetch bigrams + let bigram_lines = ser.query_command("BIGRAMS?").unwrap_or_default(); + let _ = tx.send(BgMsg::BigramLines(bigram_lines)); + }); + }); + } + + // --- Advanced: refresh --- + { + let serial = serial.clone(); + let tx = bg_tx.clone(); + + window.global::().on_refresh_advanced(move || { + let serial = serial.clone(); + let tx = tx.clone(); + std::thread::spawn(move || { + let mut ser = serial.lock().unwrap_or_else(|e| e.into_inner()); + for (tag, cmd) in [("td", "TD?"), ("combo", "COMBO?"), ("leader", "LEADER?"), ("ko", "KO?"), ("bt", "BT?"), ("tama", "TAMA?"), ("autoshift", "AUTOSHIFT?")] { + let lines = ser.query_command(cmd).unwrap_or_default(); + let _ = tx.send(BgMsg::TextLines(tag.into(), lines)); + } + }); + }); + } + + // --- Advanced: delete combo --- + { + let serial = serial.clone(); + let tx = bg_tx.clone(); + + window.global::().on_delete_combo(move |idx| { + let serial = serial.clone(); + let tx = tx.clone(); + let cmd = logic::protocol::cmd_combodel(idx as u8); + std::thread::spawn(move || { + let mut ser = serial.lock().unwrap_or_else(|e| e.into_inner()); + let _ = ser.send_command(&cmd); + let lines = ser.query_command("COMBO?").unwrap_or_default(); + let _ = tx.send(BgMsg::TextLines("combo".into(), lines)); + }); + }); + } + + // --- Advanced: delete leader --- + { + let serial = serial.clone(); + let tx = bg_tx.clone(); + + window.global::().on_delete_leader(move |idx| { + let serial = serial.clone(); + let tx = tx.clone(); + let cmd = logic::protocol::cmd_leaderdel(idx as u8); + std::thread::spawn(move || { + let mut ser = serial.lock().unwrap_or_else(|e| e.into_inner()); + let _ = ser.send_command(&cmd); + let lines = ser.query_command("LEADER?").unwrap_or_default(); + let _ = tx.send(BgMsg::TextLines("leader".into(), lines)); + }); + }); + } + + // --- Advanced: delete KO --- + { + let serial = serial.clone(); + let tx = bg_tx.clone(); + + window.global::().on_delete_ko(move |idx| { + let serial = serial.clone(); + let tx = tx.clone(); + let cmd = logic::protocol::cmd_kodel(idx as u8); + std::thread::spawn(move || { + let mut ser = serial.lock().unwrap_or_else(|e| e.into_inner()); + let _ = ser.send_command(&cmd); + let lines = ser.query_command("KO?").unwrap_or_default(); + let _ = tx.send(BgMsg::TextLines("ko".into(), lines)); + }); + }); + } + + // --- Advanced: set trilayer --- + { + let serial = serial.clone(); + let window_weak = window.as_weak(); + + window.global::().on_set_trilayer(move |l1, l2, l3| { + let l1 = l1 as u8; + let l2 = l2 as u8; + let l3 = l3 as u8; + let cmd = logic::protocol::cmd_trilayer(l1, l2, l3); + let serial = serial.clone(); + std::thread::spawn(move || { + let mut ser = serial.lock().unwrap_or_else(|e| e.into_inner()); + let _ = ser.send_command(&cmd); + }); + if let Some(w) = window_weak.upgrade() { + w.global::().set_status_text( + SharedString::from(format!("Tri-layer: {} + {} → {}", l1, l2, l3)) + ); + } + }); + } + + // --- Advanced: BT switch --- + { + let serial = serial.clone(); + + window.global::().on_bt_switch(move |slot| { + let cmd = logic::protocol::cmd_bt_switch(slot as u8); + let serial = serial.clone(); + std::thread::spawn(move || { + let mut ser = serial.lock().unwrap_or_else(|e| e.into_inner()); + let _ = ser.send_command(&cmd); + }); + }); + } + + // --- Advanced: TAMA action --- + { + let serial = serial.clone(); + let tx = bg_tx.clone(); + + window.global::().on_tama_action(move |action| { + let cmd = match action.as_str() { + "feed" => "TAMA FEED", + "play" => "TAMA PLAY", + "sleep" => "TAMA SLEEP", + "meds" => "TAMA MEDS", + "toggle" => "TAMA TOGGLE", + _ => return, + }; + let serial = serial.clone(); + let tx = tx.clone(); + let cmd = cmd.to_string(); + std::thread::spawn(move || { + let mut ser = serial.lock().unwrap_or_else(|e| e.into_inner()); + let _ = ser.send_command(&cmd); + let lines = ser.query_command("TAMA?").unwrap_or_default(); + let _ = tx.send(BgMsg::TextLines("tama".into(), lines)); + }); + }); + } + + // --- Advanced: toggle autoshift --- + { + let serial = serial.clone(); + let tx = bg_tx.clone(); + + window.global::().on_toggle_autoshift(move || { + let serial = serial.clone(); + let tx = tx.clone(); + std::thread::spawn(move || { + let mut ser = serial.lock().unwrap_or_else(|e| e.into_inner()); + let _ = ser.send_command("AUTOSHIFT TOGGLE"); + let lines = ser.query_command("AUTOSHIFT?").unwrap_or_default(); + let _ = tx.send(BgMsg::TextLines("autoshift".into(), lines)); + }); + }); + } + + // --- Advanced: create combo --- + { + let serial = serial.clone(); + let tx = bg_tx.clone(); + let window_weak = window.as_weak(); + + window.global::().on_create_combo(move || { + let Some(w) = window_weak.upgrade() else { return }; + let adv = w.global::(); + let r1 = adv.get_new_combo_r1() as u8; + let c1 = adv.get_new_combo_c1() as u8; + let r2 = adv.get_new_combo_r2() as u8; + let c2 = adv.get_new_combo_c2() as u8; + let result = adv.get_new_combo_result_code() as u8; + let cmd = logic::protocol::cmd_comboset(255, r1, c1, r2, c2, result); + let serial = serial.clone(); + let tx = tx.clone(); + std::thread::spawn(move || { + let mut ser = serial.lock().unwrap_or_else(|e| e.into_inner()); + let _ = ser.send_command(&cmd); + let lines = ser.query_command("COMBO?").unwrap_or_default(); + let _ = tx.send(BgMsg::TextLines("combo".into(), lines)); + }); + w.global::().set_status_text("Creating combo...".into()); + }); + } + + // --- Advanced: create KO --- + { + let serial = serial.clone(); + let tx = bg_tx.clone(); + let window_weak = window.as_weak(); + + window.global::().on_create_ko(move |trig_code, trig_mod_idx, result_code, res_mod_idx| { + let trig = trig_code as u8; + let trig_mod = mod_idx_to_byte(trig_mod_idx); + let result = result_code as u8; + let res_mod = mod_idx_to_byte(res_mod_idx); + let cmd = logic::protocol::cmd_koset(255, trig, trig_mod, result, res_mod); + let serial = serial.clone(); + let tx = tx.clone(); + std::thread::spawn(move || { + let mut ser = serial.lock().unwrap_or_else(|e| e.into_inner()); + let _ = ser.send_command(&cmd); + let lines = ser.query_command("KO?").unwrap_or_default(); + let _ = tx.send(BgMsg::TextLines("ko".into(), lines)); + }); + if let Some(w) = window_weak.upgrade() { + w.global::().set_status_text("Creating key override...".into()); + } + }); + } + + // --- Advanced: create leader --- + { + let serial = serial.clone(); + let tx = bg_tx.clone(); + let window_weak = window.as_weak(); + + window.global::().on_create_leader(move |result_code: i32, mod_idx: i32| { + let Some(w) = window_weak.upgrade() else { return }; + let adv = w.global::(); + let count = adv.get_new_leader_seq_count() as usize; + let mut sequence = Vec::new(); + if count > 0 { sequence.push(adv.get_new_leader_seq0_code() as u8); } + if count > 1 { sequence.push(adv.get_new_leader_seq1_code() as u8); } + if count > 2 { sequence.push(adv.get_new_leader_seq2_code() as u8); } + if count > 3 { sequence.push(adv.get_new_leader_seq3_code() as u8); } + let result = result_code as u8; + let result_mod = mod_idx_to_byte(mod_idx); + let cmd = logic::protocol::cmd_leaderset(255, &sequence, result, result_mod); + let serial = serial.clone(); + let tx = tx.clone(); + std::thread::spawn(move || { + let mut ser = serial.lock().unwrap_or_else(|e| e.into_inner()); + let _ = ser.send_command(&cmd); + let lines = ser.query_command("LEADER?").unwrap_or_default(); + let _ = tx.send(BgMsg::TextLines("leader".into(), lines)); + }); + if let Some(w) = window_weak.upgrade() { + w.global::().set_status_text("Creating leader key...".into()); + } + }); + } + + // --- Macros: refresh --- + { + let serial = serial.clone(); + let tx = bg_tx.clone(); + + window.global::().on_refresh_macros(move || { + let serial = serial.clone(); + let tx = tx.clone(); + std::thread::spawn(move || { + let mut ser = serial.lock().unwrap_or_else(|e| e.into_inner()); + let lines = ser.query_command("MACROS?").unwrap_or_default(); + let _ = tx.send(BgMsg::TextLines("macros".into(), lines)); + }); + }); + } + + // --- Macros: add delay step --- + { + let macro_steps = macro_steps.clone(); + let refresh = refresh_macro_display.clone(); + + window.global::().on_add_delay_step(move |ms| { + let units = (ms as u8) / 10; + macro_steps.borrow_mut().push((0xFF, units)); + refresh(); + }); + } + + // --- Macros: remove last step --- + { + let macro_steps = macro_steps.clone(); + let refresh = refresh_macro_display.clone(); + + window.global::().on_remove_last_step(move || { + macro_steps.borrow_mut().pop(); + refresh(); + }); + } + + // --- Macros: clear steps --- + { + let macro_steps = macro_steps.clone(); + let refresh = refresh_macro_display.clone(); + + window.global::().on_clear_steps(move || { + macro_steps.borrow_mut().clear(); + refresh(); + }); + } + + // --- Macros: save --- + { + let serial = serial.clone(); + let tx = bg_tx.clone(); + let macro_steps = macro_steps.clone(); + let window_weak = window.as_weak(); + + window.global::().on_save_macro(move || { + let Some(w) = window_weak.upgrade() else { return }; + let mb = w.global::(); + let slot_num = mb.get_new_slot_idx() as u8; + let name = mb.get_new_name().to_string(); + let steps = macro_steps.borrow(); + let steps_str: Vec = steps.iter().map(|&(kc, md)| { + if kc == 0xFF { format!("{:02X}:{:02X}", kc, md) } + else { format!("{:02X}:{:02X}", kc, md) } + }).collect(); + let steps_text = steps_str.join(","); + drop(steps); + let cmd = logic::protocol::cmd_macroseq(slot_num, &name, &steps_text); + let serial = serial.clone(); + let tx = tx.clone(); + std::thread::spawn(move || { + let mut ser = serial.lock().unwrap_or_else(|e| e.into_inner()); + let _ = ser.send_command(&cmd); + let lines = ser.query_command("MACROS?").unwrap_or_default(); + let _ = tx.send(BgMsg::TextLines("macros".into(), lines)); + }); + w.global::().set_status_text( + SharedString::from(format!("Saving macro #{}...", slot_num)) + ); + }); + } + + // --- Macros: delete --- + { + let serial = serial.clone(); + let tx = bg_tx.clone(); + + window.global::().on_delete_macro(move |slot| { + let cmd = logic::protocol::cmd_macro_del(slot as u8); + let serial = serial.clone(); + let tx = tx.clone(); + std::thread::spawn(move || { + let mut ser = serial.lock().unwrap_or_else(|e| e.into_inner()); + let _ = ser.send_command(&cmd); + let lines = ser.query_command("MACROS?").unwrap_or_default(); + let _ = tx.send(BgMsg::TextLines("macros".into(), lines)); + }); + }); + } + + // --- Flasher: refresh prog ports --- + { + let window_weak = window.as_weak(); + + window.global::().on_refresh_prog_ports(move || { + let ports = SerialManager::list_prog_ports(); + if let Some(w) = window_weak.upgrade() { + if let Some(first) = ports.first() { + w.global::().set_selected_prog_port(SharedString::from(first.as_str())); + } + let model: Vec = ports.iter().map(|p| SharedString::from(p.as_str())).collect(); + w.global::().set_prog_ports( + ModelRc::from(Rc::new(VecModel::from(model))) + ); + } + }); + } + + // --- Flasher: browse firmware --- + { + let window_weak = window.as_weak(); + + window.global::().on_browse_firmware(move || { + let window_weak = window_weak.clone(); + std::thread::spawn(move || { + let file = rfd::FileDialog::new() + .add_filter("Firmware", &["bin"]) + .pick_file(); + if let Some(path) = file { + let path_str = path.to_string_lossy().to_string(); + let _ = slint::invoke_from_event_loop(move || { + if let Some(w) = window_weak.upgrade() { + w.global::().set_firmware_path( + SharedString::from(path_str.as_str()) + ); + } + }); + } + }); + }); + } + + // --- Flasher: flash --- + { + let tx = bg_tx.clone(); + let window_weak = window.as_weak(); + + window.global::().on_flash(move || { + let Some(w) = window_weak.upgrade() else { return }; + let flasher = w.global::(); + let port = flasher.get_selected_prog_port().to_string(); + let path = flasher.get_firmware_path().to_string(); + let offset: u32 = match flasher.get_flash_offset_index() { + 0 => 0x20000, // factory + 1 => 0x220000, // ota_0 + _ => 0x20000, + }; + + if port.is_empty() || path.is_empty() { return; } + + // Read firmware file + let firmware = match std::fs::read(&path) { + Ok(data) => data, + Err(e) => { + let _ = tx.send(BgMsg::FlashDone(Err(format!("Cannot read {}: {}", path, e)))); + return; + } + }; + + flasher.set_flashing(true); + flasher.set_flash_progress(0.0); + flasher.set_flash_status(SharedString::from("Starting...")); + + let tx = tx.clone(); + std::thread::spawn(move || { + let (ftx, frx) = mpsc::channel(); + // Forward flash progress to main bg channel + let tx2 = tx.clone(); + let progress_thread = std::thread::spawn(move || { + while let Ok(logic::flasher::FlashProgress::OtaProgress(p, msg)) = frx.recv() { + let _ = tx2.send(BgMsg::FlashProgress(p, msg)); + } + }); + + let result = logic::flasher::flash_firmware(&port, &firmware, offset, &ftx); + drop(ftx); // close channel so progress_thread exits + let _ = progress_thread.join(); + let _ = tx.send(BgMsg::FlashDone(result.map_err(|e| e.to_string()))); + }); + }); + } + + // Init prog ports list + { + let ports = SerialManager::list_prog_ports(); + if let Some(first) = ports.first() { + window.global::().set_selected_prog_port(SharedString::from(first.as_str())); + } + let model: Vec = ports.iter().map(|p| SharedString::from(p.as_str())).collect(); + window.global::().set_prog_ports( + ModelRc::from(Rc::new(VecModel::from(model))) + ); + } + + // --- Poll background messages via timer --- + { + let window_weak = window.as_weak(); + let keys_arc = keys_arc.clone(); + let current_keymap = current_keymap.clone(); + let keyboard_layout = keyboard_layout.clone(); + let heatmap_data = heatmap_data.clone(); + + let timer = slint::Timer::default(); + timer.start( + slint::TimerMode::Repeated, + std::time::Duration::from_millis(50), + move || { + let Some(window) = window_weak.upgrade() else { return }; + + while let Ok(msg) = bg_rx.try_recv() { + match msg { + BgMsg::Connected(port, fw, names, km) => { + let app = window.global::(); + app.set_connection(ConnectionState::Connected); + app.set_firmware_version(SharedString::from(&fw)); + app.set_status_text(SharedString::from(format!("Connected to {}", port))); + + let new_layers = build_layer_model(&names); + window.global::().set_layers(ModelRc::from(new_layers)); + + *current_keymap.borrow_mut() = km.clone(); + let keycaps = window.global::().get_keycaps(); + let layout = keyboard_layout.borrow(); + let keys = keys_arc.borrow(); + update_keycap_labels(&keycaps, &keys, &km, &layout); + } + BgMsg::ConnectError(e) => { + let app = window.global::(); + app.set_connection(ConnectionState::Disconnected); + app.set_status_text(SharedString::from(format!("Error: {}", e))); + } + BgMsg::Keymap(km) => { + *current_keymap.borrow_mut() = km.clone(); + let keycaps = window.global::().get_keycaps(); + let layout = keyboard_layout.borrow(); + let keys = keys_arc.borrow(); + update_keycap_labels(&keycaps, &keys, &km, &layout); + window.global::().set_status_text("Keymap loaded".into()); + } + BgMsg::LayerNames(names) => { + let new_layers = build_layer_model(&names); + window.global::().set_layers(ModelRc::from(new_layers)); + } + BgMsg::Disconnected => { + let app = window.global::(); + app.set_connection(ConnectionState::Disconnected); + app.set_firmware_version(SharedString::default()); + app.set_status_text("Disconnected".into()); + } + BgMsg::LayoutJson(new_keys) => { + *keys_arc.borrow_mut() = new_keys.clone(); + let new_model = build_keycap_model(&new_keys); + let km = current_keymap.borrow(); + if !km.is_empty() { + let layout = keyboard_layout.borrow(); + update_keycap_labels(&new_model, &new_keys, &km, &layout); + } + // Compute content bounds for responsive scaling + let mut max_x: f32 = 0.0; + let mut max_y: f32 = 0.0; + for kp in &new_keys { + let right = kp.x + kp.w; + let bottom = kp.y + kp.h; + if right > max_x { max_x = right; } + if bottom > max_y { max_y = bottom; } + } + let bridge = window.global::(); + bridge.set_content_width(max_x); + bridge.set_content_height(max_y); + bridge.set_keycaps(ModelRc::from(new_model)); + window.global::().set_status_text( + SharedString::from(format!("Layout loaded ({} keys)", new_keys.len())) + ); + } + BgMsg::BigramLines(lines) => { + let entries = logic::stats_analyzer::parse_bigram_lines(&lines); + let analysis = logic::stats_analyzer::analyze_bigrams(&entries); + window.global::().set_bigrams(BigramData { + alt_hand_pct: analysis.alt_hand_pct, + same_hand_pct: analysis.same_hand_pct, + sfb_pct: analysis.sfb_pct, + total: analysis.total as i32, + }); + } + BgMsg::FlashProgress(progress, msg) => { + let flasher = window.global::(); + flasher.set_flash_progress(progress); + flasher.set_flash_status(SharedString::from(msg)); + } + BgMsg::FlashDone(result) => { + let flasher = window.global::(); + flasher.set_flashing(false); + match result { + Ok(()) => { + flasher.set_flash_progress(1.0); + flasher.set_flash_status(SharedString::from("Flash complete!")); + window.global::().set_status_text("Flash complete!".into()); + } + Err(e) => { + flasher.set_flash_status(SharedString::from(format!("Error: {}", e))); + window.global::().set_status_text( + SharedString::from(format!("Flash error: {}", e)) + ); + } + } + } + BgMsg::HeatmapData(data, max) => { + *heatmap_data.borrow_mut() = data.clone(); + + // Update heat intensity on keycaps + let keycaps = window.global::().get_keycaps(); + let keys = keys_arc.borrow(); + for i in 0..keycaps.row_count() { + if i >= keys.len() { break; } + let mut item = keycaps.row_data(i).unwrap(); + let kp = &keys[i]; + let row = kp.row as usize; + let col = kp.col as usize; + let count = data.get(row) + .and_then(|r| r.get(col)) + .copied() + .unwrap_or(0); + item.heat = if max > 0 { count as f32 / max as f32 } else { 0.0 }; + keycaps.set_row_data(i, item); + } + drop(keys); + + let km = current_keymap.borrow(); + let balance = logic::stats_analyzer::hand_balance(&data); + let fingers = logic::stats_analyzer::finger_load(&data); + let rows = logic::stats_analyzer::row_usage(&data); + let top = logic::stats_analyzer::top_keys(&data, &km, 10); + let dead = logic::stats_analyzer::dead_keys(&data, &km); + + let stats = window.global::(); + stats.set_hand_balance(HandBalanceData { + left_pct: balance.left_pct, + right_pct: balance.right_pct, + total: balance.total as i32, + }); + stats.set_total_presses(balance.total as i32); + + let finger_model: Vec = fingers.iter().map(|f| FingerLoadData { + name: SharedString::from(&f.name), + pct: f.pct, + count: f.count as i32, + }).collect(); + stats.set_finger_load(ModelRc::from(Rc::new(VecModel::from(finger_model)))); + + let row_model: Vec = rows.iter().map(|r| RowUsageData { + name: SharedString::from(&r.name), + pct: r.pct, + count: r.count as i32, + }).collect(); + stats.set_row_usage(ModelRc::from(Rc::new(VecModel::from(row_model)))); + + let top_model: Vec = top.iter().map(|t| TopKeyData { + name: SharedString::from(&t.name), + finger: SharedString::from(&t.finger), + count: t.count as i32, + pct: t.pct, + }).collect(); + stats.set_top_keys(ModelRc::from(Rc::new(VecModel::from(top_model)))); + + let dead_model: Vec = dead.iter().map(|d| SharedString::from(d.as_str())).collect(); + stats.set_dead_keys(ModelRc::from(Rc::new(VecModel::from(dead_model)))); + + window.global::().set_status_text( + SharedString::from(format!("Stats loaded ({} total presses, max {})", balance.total, max)) + ); + } + BgMsg::TextLines(tag, lines) => { + match tag.as_str() { + "td" => { + let td_data = logic::parsers::parse_td_lines(&lines); + let model: Vec = td_data.iter().enumerate() + .filter(|(_, actions)| actions.iter().any(|&a| a != 0)) + .map(|(i, actions)| TapDanceData { + index: i as i32, + actions: ModelRc::from(Rc::new(VecModel::from( + actions.iter().map(|&a| SharedString::from(keycode::decode_keycode(a))).collect::>() + ))), + }) + .collect(); + window.global::().set_tap_dances( + ModelRc::from(Rc::new(VecModel::from(model))) + ); + } + "combo" => { + let combo_data = logic::parsers::parse_combo_lines(&lines); + let model: Vec = combo_data.iter().map(|c| ComboData { + index: c.index as i32, + key1: SharedString::from(format!("R{}C{}", c.r1, c.c1)), + key2: SharedString::from(format!("R{}C{}", c.r2, c.c2)), + result: SharedString::from(keycode::decode_keycode(c.result)), + }).collect(); + window.global::().set_combos( + ModelRc::from(Rc::new(VecModel::from(model))) + ); + } + "leader" => { + let leader_data = logic::parsers::parse_leader_lines(&lines); + let model: Vec = leader_data.iter().map(|l| { + let seq: Vec = l.sequence.iter() + .map(|&k| keycode::hid_key_name(k)) + .collect(); + LeaderData { + index: l.index as i32, + sequence: SharedString::from(seq.join(" → ")), + result: SharedString::from(keycode::hid_key_name(l.result)), + } + }).collect(); + window.global::().set_leaders( + ModelRc::from(Rc::new(VecModel::from(model))) + ); + } + "ko" => { + let ko_data = logic::parsers::parse_ko_lines(&lines); + let model: Vec = ko_data.iter().enumerate().map(|(i, ko)| { + KeyOverrideData { + index: i as i32, + trigger: SharedString::from(keycode::hid_key_name(ko[0])), + result: SharedString::from(keycode::hid_key_name(ko[2])), + } + }).collect(); + window.global::().set_key_overrides( + ModelRc::from(Rc::new(VecModel::from(model))) + ); + } + "bt" => { + let bt_text = lines.join("\n"); + window.global::().set_bt_status(SharedString::from(bt_text)); + } + "macros" => { + let macro_data = logic::parsers::parse_macro_lines(&lines); + let model: Vec = macro_data.iter().map(|m| { + let steps_str: Vec = m.steps.iter().map(|s| { + if s.is_delay() { + format!("T({})", s.delay_ms()) + } else { + format!("D({:02X})", s.keycode) + } + }).collect(); + MacroData { + slot: m.slot as i32, + name: SharedString::from(&m.name), + steps: SharedString::from(steps_str.join(" ")), + } + }).collect(); + window.global::().set_macros( + ModelRc::from(Rc::new(VecModel::from(model))) + ); + } + "wpm" => { + if let Some(line) = lines.first() { + let wpm: u16 = line.split_whitespace() + .last() + .and_then(|s| s.parse().ok()) + .unwrap_or(0); + window.global::().set_wpm(wpm as i32); + } + } + "tama" => { + let text = lines.join("\n"); + window.global::().set_tama_status(SharedString::from(text)); + } + "autoshift" => { + let text = lines.join(" "); + window.global::().set_autoshift_status(SharedString::from(text)); + } + _ => {} + } + } + } + } + }, + ); + + // WPM polling timer (5s, non-blocking try_lock in background thread) + let wpm_timer = slint::Timer::default(); + { + let serial = serial.clone(); + let tx = bg_tx.clone(); + let window_weak2 = window.as_weak(); + + wpm_timer.start( + slint::TimerMode::Repeated, + std::time::Duration::from_secs(5), + move || { + let Some(w) = window_weak2.upgrade() else { return }; + if w.global::().get_connection() != ConnectionState::Connected { return; } + let serial = serial.clone(); + let tx = tx.clone(); + std::thread::spawn(move || { + let Ok(mut ser) = serial.try_lock() else { return }; + let lines = ser.query_command("WPM?").unwrap_or_default(); + drop(ser); + let _ = tx.send(BgMsg::TextLines("wpm".into(), lines)); + }); + }, + ); + } + + // Keep timers alive + let _keep_timer = timer; + let _keep_wpm = wpm_timer; + window.run().unwrap(); + } +} diff --git a/ui/components/connection_bar.slint b/ui/components/connection_bar.slint new file mode 100644 index 0000000..7dbb575 --- /dev/null +++ b/ui/components/connection_bar.slint @@ -0,0 +1,56 @@ +import { ComboBox, Button } from "std-widgets.slint"; +import { Theme } from "../theme.slint"; +import { AppState, ConnectionBridge, ConnectionState } from "../globals.slint"; + +export component ConnectionBar inherits Rectangle { + height: 48px; + background: Theme.bg-secondary; + + HorizontalLayout { + padding: 8px; + spacing: 12px; + alignment: start; + // Status LED (wrapped in a VerticalLayout to center it) + VerticalLayout { + alignment: center; + Rectangle { + width: 16px; + height: 16px; + border-radius: 8px; + background: AppState.connection == ConnectionState.connected ? Theme.connected : Theme.disconnected; + } + } + + // Port selector (placeholder - ComboBox needs string model) + Text { + text: ConnectionBridge.selected-port != "" ? ConnectionBridge.selected-port : "No port selected"; + color: Theme.fg-primary; + vertical-alignment: center; + } + + // Connect/Disconnect button + Button { + text: AppState.connection == ConnectionState.connected ? "Disconnect" : "Connect"; + enabled: AppState.connection == ConnectionState.connected || AppState.connection == ConnectionState.disconnected; + clicked => { + if AppState.connection == ConnectionState.connected { + ConnectionBridge.disconnect(); + } else { + ConnectionBridge.connect(); + } + } + } + + // Spacer + Rectangle { + horizontal-stretch: 1; + } + + // Firmware version + Text { + text: AppState.firmware-version; + color: Theme.fg-secondary; + vertical-alignment: center; + } + } +} diff --git a/ui/components/key_button.slint b/ui/components/key_button.slint new file mode 100644 index 0000000..1f65b98 --- /dev/null +++ b/ui/components/key_button.slint @@ -0,0 +1,57 @@ +import { Theme } from "../theme.slint"; +import { KeycapData, KeymapBridge } from "../globals.slint"; + +export component KeyButton inherits Rectangle { + in property data; + in property scale: 1.0; + callback clicked(int); + + // Heat color: interpolate blue(cold) -> yellow -> red(hot) + property heat-color: + data.heat < 0.5 + ? Colors.blue.mix(Colors.yellow, data.heat * 2) + : Colors.yellow.mix(Colors.red, (data.heat - 0.5) * 2); + + property base-color: + KeymapBridge.heatmap-enabled && data.heat > 0 ? root.heat-color : data.color; + + width: data.w; + height: data.h; + background: transparent; + + inner := Rectangle { + width: 100%; + height: 100%; + border-radius: 4px; + background: data.selected ? Theme.accent-purple : root.base-color; + border-width: 1px; + border-color: data.selected ? Theme.accent-cyan : Theme.bg-primary; + transform-rotation: data.rotation * 1deg; + transform-origin: { + x: self.width / 2, + y: self.height / 2, + }; + + Text { + text: data.label; + color: Theme.fg-primary; + font-size: max(7px, 11px * root.scale); + horizontal-alignment: center; + vertical-alignment: center; + } + + if data.sublabel != "" : Text { + y: parent.height - 14px * root.scale; + text: data.sublabel; + color: Theme.fg-secondary; + font-size: max(5px, 8px * root.scale); + horizontal-alignment: center; + width: 100%; + } + + TouchArea { + clicked => { root.clicked(data.index); } + mouse-cursor: pointer; + } + } +} diff --git a/ui/components/key_selector.slint b/ui/components/key_selector.slint new file mode 100644 index 0000000..f7eb169 --- /dev/null +++ b/ui/components/key_selector.slint @@ -0,0 +1,271 @@ +import { LineEdit, Button, ComboBox, ScrollView } from "std-widgets.slint"; +import { Theme } from "../theme.slint"; +import { KeySelectorBridge, KeymapBridge, KeyEntry, KeycapData } from "../globals.slint"; +import { KeyButton } from "key_button.slint"; + +component KeyTile inherits Rectangle { + in property label; + in property code; + callback picked(int); + + width: 52px; + height: 30px; + border-radius: 4px; + background: ta.has-hover ? Theme.accent-purple : Theme.button-bg; + + Text { + text: root.label; + color: Theme.fg-primary; + font-size: 11px; + horizontal-alignment: center; + vertical-alignment: center; + } + + ta := TouchArea { + clicked => { root.picked(root.code); } + mouse-cursor: pointer; + } +} + +component SectionLabel inherits Text { + color: Theme.accent-cyan; + font-size: 12px; + font-weight: 600; +} + +component KeySection inherits VerticalLayout { + in property title; + in property <[KeyEntry]> keys; + in property cols: 7; + callback picked(int); + + property tile-w: 55px; + property tile-h: 30px; + property gap: 3px; + + spacing: 3px; + + if keys.length > 0 : SectionLabel { text: root.title; } + if keys.length > 0 : Rectangle { + height: (Math.ceil(keys.length / root.cols) ) * (root.tile-h + root.gap); + + for key[idx] in root.keys : KeyTile { + x: mod(idx, root.cols) * (root.tile-w + root.gap); + y: floor(idx / root.cols) * (root.tile-h + root.gap); + width: root.tile-w; + label: key.name; + code: key.code; + picked(c) => { root.picked(c); } + } + } +} + +export component KeySelector inherits Rectangle { + visible: KeymapBridge.key-selector-open; + background: #000000aa; + + TouchArea { clicked => { KeymapBridge.key-selector-open = false; } } + + function pick(code: int) { + KeySelectorBridge.select-keycode(code); + KeymapBridge.key-selector-open = false; + } + + Rectangle { + x: (parent.width - self.width) / 2; + y: (parent.height - self.height) / 2; + width: min(480px, parent.width - 40px); + height: min(520px, parent.height - 40px); + background: Theme.bg-primary; + border-radius: 12px; + border-width: 1px; + border-color: Theme.accent-purple; + clip: true; + + TouchArea { } + + VerticalLayout { + padding: 14px; + spacing: 8px; + + // Header + HorizontalLayout { + + Text { text: "Select Key"; color: Theme.fg-primary; font-size: 16px; font-weight: 700; vertical-alignment: center; } + Rectangle { horizontal-stretch: 1; } + Rectangle { + width: 26px; height: 26px; border-radius: 4px; + background: close-ta.has-hover ? Theme.accent-red : Theme.button-bg; + Text { text: "X"; color: Theme.fg-primary; font-size: 13px; horizontal-alignment: center; vertical-alignment: center; } + close-ta := TouchArea { clicked => { KeymapBridge.key-selector-open = false; } mouse-cursor: pointer; } + } + } + + // Keyboard mode for combo key picking + property keyboard-mode: KeymapBridge.selector-target == "combo-key1" || KeymapBridge.selector-target == "combo-key2"; + + if keyboard-mode : Text { + text: "Click a key on the keyboard:"; + color: Theme.fg-secondary; + font-size: 12px; + } + + if keyboard-mode : Rectangle { + vertical-stretch: 1; + background: Theme.bg-surface; + border-radius: 8px; + clip: true; + + property kb-scale-x: self.width / KeymapBridge.content-width; + property kb-scale-y: self.height / KeymapBridge.content-height; + property kb-scale: min(kb-scale-x, kb-scale-y) * 0.95; + property kb-offset-x: (self.width - KeymapBridge.content-width * kb-scale) / 2; + property kb-offset-y: (self.height - KeymapBridge.content-height * kb-scale) / 2; + + for keycap[idx] in KeymapBridge.keycaps : KeyButton { + x: parent.kb-offset-x + keycap.x * parent.kb-scale; + y: parent.kb-offset-y + keycap.y * parent.kb-scale; + width: keycap.w * parent.kb-scale; + height: keycap.h * parent.kb-scale; + scale: parent.kb-scale; + data: keycap; + clicked(key-index) => { + KeymapBridge.select-key(key-index); + // The dispatch in main.rs handles combo-key1/combo-key2 + KeySelectorBridge.select-keycode(0); // trigger dispatch + KeymapBridge.key-selector-open = false; + } + } + } + + // Normal mode: search + key grid + if !keyboard-mode : LineEdit { + placeholder-text: "Search..."; + text <=> KeySelectorBridge.search-text; + edited(text) => { KeySelectorBridge.apply-filter(text); } + } + + if !keyboard-mode : ScrollView { + vertical-stretch: 1; + + VerticalLayout { + spacing: 6px; + padding-right: 8px; + + KeySection { title: "Letters"; keys: KeySelectorBridge.cat-letters; picked(c) => { root.pick(c); } } + KeySection { title: "Numbers"; keys: KeySelectorBridge.cat-numbers; picked(c) => { root.pick(c); } } + KeySection { title: "Modifiers"; keys: KeySelectorBridge.cat-modifiers; picked(c) => { root.pick(c); } } + KeySection { title: "Navigation"; keys: KeySelectorBridge.cat-nav; picked(c) => { root.pick(c); } } + KeySection { title: "Function Keys"; keys: KeySelectorBridge.cat-function; picked(c) => { root.pick(c); } } + KeySection { title: "Symbols"; keys: KeySelectorBridge.cat-symbols; picked(c) => { root.pick(c); } } + KeySection { title: "Layers"; keys: KeySelectorBridge.cat-layers; picked(c) => { root.pick(c); } } + KeySection { title: "Special / BT / Media"; keys: KeySelectorBridge.cat-special; picked(c) => { root.pick(c); } } + KeySection { title: "Tap Dance / Macros"; keys: KeySelectorBridge.cat-td-macro; picked(c) => { root.pick(c); } } + + // Mod-Tap builder + SectionLabel { text: "Mod-Tap"; } + HorizontalLayout { + spacing: 6px; + height: 32px; + Text { text: "Mod:"; color: Theme.fg-secondary; font-size: 11px; vertical-alignment: center; } + mt-mod := ComboBox { + width: 80px; + model: ["Ctrl", "Shift", "Alt", "GUI", "RCtrl", "RShift", "RAlt", "RGUI"]; + current-index: 1; + } + Text { text: "Key:"; color: Theme.fg-secondary; font-size: 11px; vertical-alignment: center; } + mt-key := ComboBox { + width: 70px; + model: ["A","B","C","D","E","F","G","H","I","J","K","L","M","N","O","P","Q","R","S","T","U","V","W","X","Y","Z","1","2","3","4","5","6","7","8","9","0","Space","Enter","Esc","Tab","Bksp"]; + current-index: 0; + } + Text { + text: "= MT " + mt-mod.current-value + " " + mt-key.current-value; + color: Theme.accent-green; + font-size: 11px; + vertical-alignment: center; + } + Button { + text: "Set"; + clicked => { + KeySelectorBridge.apply-mt(mt-mod.current-index, mt-key.current-index); + KeymapBridge.key-selector-open = false; + } + } + } + + // Layer-Tap builder + SectionLabel { text: "Layer-Tap"; } + HorizontalLayout { + spacing: 6px; + height: 32px; + Text { text: "Layer:"; color: Theme.fg-secondary; font-size: 11px; vertical-alignment: center; } + lt-layer := ComboBox { + width: 50px; + model: ["0","1","2","3","4","5","6","7","8","9"]; + current-index: 1; + } + Text { text: "Key:"; color: Theme.fg-secondary; font-size: 11px; vertical-alignment: center; } + lt-key := ComboBox { + width: 80px; + model: ["Space","Enter","Esc","Bksp","Tab","A","B","C","D","E"]; + current-index: 0; + } + Text { + text: "= LT " + lt-layer.current-value + " " + lt-key.current-value; + color: Theme.accent-green; + font-size: 11px; + vertical-alignment: center; + } + Button { + text: "Set"; + clicked => { + KeySelectorBridge.apply-lt(lt-layer.current-index, lt-key.current-index); + KeymapBridge.key-selector-open = false; + } + } + } + + // Hex input + SectionLabel { text: "Hex Code"; } + HorizontalLayout { + spacing: 6px; + height: 32px; + Text { text: "0x"; color: Theme.accent-orange; font-size: 12px; font-weight: 600; vertical-alignment: center; } + hex-edit := LineEdit { + width: 80px; + text <=> KeySelectorBridge.hex-input; + placeholder-text: "5204"; + edited(text) => { KeySelectorBridge.preview-hex(text); } + accepted(text) => { + KeySelectorBridge.apply-hex(text); + KeymapBridge.key-selector-open = false; + } + } + Text { + text: KeySelectorBridge.hex-preview != "" ? "= " + KeySelectorBridge.hex-preview : ""; + color: Theme.accent-green; + font-size: 11px; + vertical-alignment: center; + } + Button { + text: "Set"; + clicked => { + KeySelectorBridge.apply-hex(KeySelectorBridge.hex-input); + KeymapBridge.key-selector-open = false; + } + } + } + + // None + KeyTile { + width: 120px; + label: "None (transparent)"; + code: 0; + picked(c) => { root.pick(c); } + } + } + } + } + } +} diff --git a/ui/components/keyboard_view.slint b/ui/components/keyboard_view.slint new file mode 100644 index 0000000..17e22e7 --- /dev/null +++ b/ui/components/keyboard_view.slint @@ -0,0 +1,32 @@ +import { Theme } from "../theme.slint"; +import { KeymapBridge, KeycapData } from "../globals.slint"; +import { KeyButton } from "key_button.slint"; + +export component KeyboardView inherits Rectangle { + background: Theme.bg-surface; + border-radius: 8px; + min-height: 150px; + clip: true; + + // Scale to fit: min(available / content) so it always fits + property scale-x: self.width / KeymapBridge.content-width; + property scale-y: self.height / KeymapBridge.content-height; + property scale: min(root.scale-x, root.scale-y) * 0.95; + // Center offset + property offset-x: (self.width - KeymapBridge.content-width * root.scale) / 2; + property offset-y: (self.height - KeymapBridge.content-height * root.scale) / 2; + + for keycap[idx] in KeymapBridge.keycaps : KeyButton { + x: root.offset-x + keycap.x * root.scale; + y: root.offset-y + keycap.y * root.scale; + width: keycap.w * root.scale; + height: keycap.h * root.scale; + scale: root.scale; + data: keycap; + clicked(key-index) => { + KeymapBridge.select-key(key-index); + KeymapBridge.selector-target = "keymap"; + KeymapBridge.key-selector-open = true; + } + } +} diff --git a/ui/components/status_bar.slint b/ui/components/status_bar.slint new file mode 100644 index 0000000..fcaaeb1 --- /dev/null +++ b/ui/components/status_bar.slint @@ -0,0 +1,39 @@ +import { Theme } from "../theme.slint"; +import { AppState } from "../globals.slint"; + +export component StatusBar inherits Rectangle { + height: 32px; + background: Theme.bg-secondary; + + HorizontalLayout { + padding-left: 12px; + padding-right: 12px; + spacing: 16px; + + // Status text + Text { + text: AppState.status-text; + color: Theme.fg-secondary; + font-size: 12px; + vertical-alignment: center; + } + + Rectangle { horizontal-stretch: 1; } + + // WPM + Text { + text: AppState.wpm > 0 ? "WPM: " + AppState.wpm : ""; + color: Theme.accent-cyan; + font-size: 12px; + vertical-alignment: center; + } + + // Version + Text { + text: "v0.6.0"; + color: Theme.fg-secondary; + font-size: 11px; + vertical-alignment: center; + } + } +} diff --git a/ui/globals.slint b/ui/globals.slint new file mode 100644 index 0000000..6fbcbbd --- /dev/null +++ b/ui/globals.slint @@ -0,0 +1,274 @@ +// Shared data structures +export struct KeycapData { + x: length, + y: length, + w: length, + h: length, + rotation: float, + rotation-cx: length, + rotation-cy: length, + label: string, + sublabel: string, + keycode: int, + color: color, + heat: float, // 0.0 = cold, 1.0 = hottest + selected: bool, + index: int, +} + +export struct LayerInfo { + index: int, + name: string, + active: bool, +} + +export struct PortInfo { + name: string, + path: string, +} + +export enum ConnectionState { disconnected, connecting, connected } +export enum ActiveTab { keymap, advanced, macros, stats, settings } + +// Global singletons for Rust<->Slint bridge +export global AppState { + in-out property connection: ConnectionState.disconnected; + in-out property active-tab: ActiveTab.keymap; + in-out property status-text: "Disconnected"; + in-out property spinner-visible: false; + in-out property firmware-version: ""; + in-out property wpm: 0; +} + +export global ConnectionBridge { + in property <[PortInfo]> ports; + in-out property selected-port: ""; + callback connect(); + callback disconnect(); + callback refresh-ports(); +} + +export global KeymapBridge { + in property <[KeycapData]> keycaps; + in property <[LayerInfo]> layers; + in property content-width: 860px; + in property content-height: 360px; + in-out property selected-key-index: -1; + in-out property active-layer: 0; + in-out property selected-key-label: ""; + in-out property heatmap-enabled: false; + in-out property key-selector-open: false; + // Target for key selector: "keymap", "combo-result", "ko-trigger", "ko-result", "leader-result" + in-out property selector-target: "keymap"; + callback select-key(int); + callback switch-layer(int); + callback change-key(int, int); // key-index, new-keycode + callback rename-layer(int, string); // layer-index, new-name +} + +// ---- Settings ---- + +export global SettingsBridge { + in property <[string]> available-layouts; + in-out property selected-layout-index: 0; + callback change-layout(int); +} + +// ---- Stats ---- + +export struct HandBalanceData { + left-pct: float, + right-pct: float, + total: int, +} + +export struct FingerLoadData { + name: string, + pct: float, + count: int, +} + +export struct RowUsageData { + name: string, + pct: float, + count: int, +} + +export struct TopKeyData { + name: string, + finger: string, + count: int, + pct: float, +} + +export struct BigramData { + alt-hand-pct: float, + same-hand-pct: float, + sfb-pct: float, + total: int, +} + +export global StatsBridge { + in property hand-balance; + in property <[FingerLoadData]> finger-load; + in property <[RowUsageData]> row-usage; + in property <[TopKeyData]> top-keys; + in property <[string]> dead-keys; + in property total-presses; + in property bigrams; + callback refresh-stats(); +} + +// ---- Advanced ---- + +export struct TapDanceData { + index: int, + actions: [string], +} + +export struct ComboData { + index: int, + key1: string, + key2: string, + result: string, +} + +export struct LeaderData { + index: int, + sequence: string, + result: string, +} + +export struct KeyOverrideData { + index: int, + trigger: string, + result: string, +} + +export global AdvancedBridge { + in property <[TapDanceData]> tap-dances; + in property <[ComboData]> combos; + in property <[LeaderData]> leaders; + in property <[KeyOverrideData]> key-overrides; + in-out property bt-status: ""; + in-out property tri-l1-idx: 1; + in-out property tri-l2-idx: 2; + in-out property tri-l3-idx: 3; + // Combo creation: physical key positions + in-out property new-combo-r1: 0; + in-out property new-combo-c1: 0; + in-out property new-combo-key1-name: "Pick..."; + in-out property new-combo-r2: 0; + in-out property new-combo-c2: 0; + in-out property new-combo-key2-name: "Pick..."; + in-out property new-combo-result-code: 0; + in-out property new-combo-result-name: "Pick..."; + // KO creation: keycodes for trigger and result + in-out property new-ko-trigger-code: 0; + in-out property new-ko-trigger-name: "Pick..."; + in-out property new-ko-trig-mod-idx: 0; // 0=None,1=Ctrl,2=Shift,... + in-out property new-ko-result-code: 0; + in-out property new-ko-result-name: "Pick..."; + in-out property new-ko-res-mod-idx: 0; + // Leader creation + in-out property new-leader-seq0-code: 0; + in-out property new-leader-seq0-name: ""; + in-out property new-leader-seq1-code: 0; + in-out property new-leader-seq1-name: ""; + in-out property new-leader-seq2-code: 0; + in-out property new-leader-seq2-name: ""; + in-out property new-leader-seq3-code: 0; + in-out property new-leader-seq3-name: ""; + in-out property new-leader-seq-count: 0; + in-out property new-leader-result-code: 0; + in-out property new-leader-result-name: "Pick..."; + in-out property new-leader-mod-idx: 0; + callback refresh-advanced(); + callback delete-combo(int); + callback delete-leader(int); + callback delete-ko(int); + callback set-trilayer(int, int, int); + callback bt-switch(int); + callback create-combo(); // reads r1,c1,r2,c2,result from properties + callback create-ko(int, int, int, int); // trigger_code, trig_mod_idx, result_code, res_mod_idx + callback create-leader(int, int); // result_code, result_mod_idx (sequence comes from seq0-3 properties) + // TAMA + in property tama-status: ""; + callback tama-action(string); // "feed", "play", "sleep", "meds", "toggle" + // Autoshift + in property autoshift-status: ""; + callback toggle-autoshift(); +} + +// ---- Macros ---- + +export struct MacroData { + slot: int, + name: string, + steps: string, +} + +export struct MacroStepDisplay { + label: string, // e.g. "A", "T 100ms" + is-delay: bool, +} + +export global MacroBridge { + in property <[MacroData]> macros; + in-out property new-slot-idx: 0; + in-out property new-name: ""; + // Visual step builder + in property <[MacroStepDisplay]> new-steps; + in-out property new-steps-text: ""; // built text for display + callback refresh-macros(); + callback save-macro(); // reads slot, name, steps from properties + callback delete-macro(int); + callback add-delay-step(int); // delay in ms (50, 100, 200, 500) + callback remove-last-step(); + callback clear-steps(); +} + +// ---- OTA / Flasher ---- + +export global FlasherBridge { + in property <[string]> prog-ports; + in-out property selected-prog-port: ""; + in-out property firmware-path: ""; + in-out property flash-offset-index: 0; // 0=factory(0x20000), 1=ota_0(0x220000) + in property flash-progress: 0; + in property flash-status: ""; + in property flashing: false; + callback refresh-prog-ports(); + callback browse-firmware(); + callback flash(); +} + +// ---- Key Selector ---- + +export struct KeyEntry { + name: string, + code: int, + category: string, +} + +export global KeySelectorBridge { + in property <[KeyEntry]> all-keys; + in property <[KeyEntry]> cat-letters; + in property <[KeyEntry]> cat-numbers; + in property <[KeyEntry]> cat-modifiers; + in property <[KeyEntry]> cat-nav; + in property <[KeyEntry]> cat-function; + in property <[KeyEntry]> cat-symbols; + in property <[KeyEntry]> cat-layers; + in property <[KeyEntry]> cat-special; + in property <[KeyEntry]> cat-td-macro; + in-out property search-text: ""; + in-out property hex-input: ""; + in property hex-preview: ""; + callback apply-filter(string); + callback select-keycode(int); + callback apply-hex(string); + callback apply-mt(int, int); + callback apply-lt(int, int); + callback preview-hex(string); // updates hex-preview +} diff --git a/ui/main.slint b/ui/main.slint new file mode 100644 index 0000000..3081653 --- /dev/null +++ b/ui/main.slint @@ -0,0 +1,62 @@ +import { TabWidget } from "std-widgets.slint"; +import { Theme } from "theme.slint"; +import { AppState, ActiveTab } from "globals.slint"; +import { ConnectionBar } from "components/connection_bar.slint"; +import { StatusBar } from "components/status_bar.slint"; +import { KeySelector } from "components/key_selector.slint"; +import { TabKeymap } from "tabs/tab_keymap.slint"; +import { TabAdvanced } from "tabs/tab_advanced.slint"; +import { TabMacros } from "tabs/tab_macros.slint"; +import { TabStats } from "tabs/tab_stats.slint"; +import { TabSettings } from "tabs/tab_settings.slint"; +import { TabFlasher } from "tabs/tab_flasher.slint"; + +export { AppState, Theme } +export { ConnectionBridge, KeymapBridge, SettingsBridge, StatsBridge, AdvancedBridge, MacroBridge, FlasherBridge, KeySelectorBridge } from "globals.slint"; + +export component MainWindow inherits Window { + title: "KaSe Controller"; + preferred-width: 1000px; + preferred-height: 700px; + min-width: 600px; + min-height: 450px; + background: Theme.bg-primary; + + VerticalLayout { + ConnectionBar { } + + TabWidget { + vertical-stretch: 1; + + Tab { + title: "Keymap"; + TabKeymap { } + } + Tab { + title: "Advanced"; + TabAdvanced { } + } + Tab { + title: "Macros"; + TabMacros { } + } + Tab { + title: "Stats"; + TabStats { } + } + Tab { + title: "Settings"; + TabSettings { } + } + Tab { + title: "Flash"; + TabFlasher { } + } + } + + StatusBar { } + } + + // Modal overlay (above everything) + KeySelector { } +} diff --git a/ui/tabs/tab_advanced.slint b/ui/tabs/tab_advanced.slint new file mode 100644 index 0000000..add0d20 --- /dev/null +++ b/ui/tabs/tab_advanced.slint @@ -0,0 +1,347 @@ +import { Button, ComboBox, ScrollView } from "std-widgets.slint"; +import { Theme } from "../theme.slint"; +import { AdvancedBridge, AppState, ConnectionState, KeymapBridge } from "../globals.slint"; + +component SectionHeader inherits Text { + color: Theme.accent-cyan; + font-size: 14px; + font-weight: 600; +} + +component ItemRow inherits Rectangle { + in property prefix; + in property left; + in property right; + callback delete(); + background: Theme.bg-primary; + border-radius: 4px; + height: 30px; + + HorizontalLayout { + padding-left: 8px; + padding-right: 8px; + spacing: 6px; + Text { text: root.prefix; color: Theme.accent-purple; font-size: 11px; vertical-alignment: center; width: 35px; } + Text { text: root.left; color: Theme.fg-primary; font-size: 11px; vertical-alignment: center; horizontal-stretch: 1; } + Text { text: "->"; color: Theme.fg-secondary; font-size: 11px; vertical-alignment: center; } + Text { text: root.right; color: Theme.accent-green; font-size: 11px; vertical-alignment: center; } + Button { text: "Del"; clicked => { root.delete(); } } + } +} + +component PickButton inherits Rectangle { + in property label: "Pick..."; + in property target; + width: 100px; + height: 28px; + border-radius: 4px; + background: pick-ta.has-hover ? Theme.button-hover : Theme.button-bg; + Text { text: root.label; color: Theme.fg-primary; font-size: 11px; horizontal-alignment: center; vertical-alignment: center; } + pick-ta := TouchArea { + clicked => { KeymapBridge.selector-target = root.target; KeymapBridge.key-selector-open = true; } + mouse-cursor: pointer; + } +} + +component ModComboBox inherits ComboBox { + model: ["None", "Ctrl", "Shift", "Alt", "GUI", "RCtrl", "RShift", "RAlt", "RGUI"]; + width: 90px; +} + +export component TabAdvanced inherits Rectangle { + background: Theme.bg-primary; + + ScrollView { + VerticalLayout { + padding: 16px; + spacing: 12px; + + // Header + HorizontalLayout { + spacing: 8px; + Text { text: "Advanced Features"; color: Theme.fg-primary; font-size: 20px; font-weight: 700; vertical-alignment: center; } + Rectangle { horizontal-stretch: 1; } + Button { + text: "Refresh All"; + enabled: AppState.connection == ConnectionState.connected; + clicked => { AdvancedBridge.refresh-advanced(); } + } + } + + // Two columns + HorizontalLayout { + spacing: 12px; + + // ==================== Left column ==================== + VerticalLayout { + horizontal-stretch: 1; + spacing: 12px; + + // --- Tap Dance --- + Rectangle { + background: Theme.bg-secondary; + border-radius: 8px; + VerticalLayout { + padding: 12px; + spacing: 4px; + SectionHeader { text: "Tap Dance"; } + if AdvancedBridge.tap-dances.length == 0 : Text { text: "No tap dances"; color: Theme.fg-secondary; font-size: 11px; } + for td in AdvancedBridge.tap-dances : Rectangle { + background: Theme.bg-primary; + border-radius: 4px; + height: 30px; + HorizontalLayout { + padding-left: 8px; padding-right: 8px; spacing: 8px; + Text { text: "TD" + td.index; color: Theme.accent-purple; font-size: 11px; vertical-alignment: center; width: 35px; } + for action in td.actions : Text { text: action; color: Theme.fg-primary; font-size: 11px; vertical-alignment: center; } + } + } + } + } + + // --- Key Overrides --- + Rectangle { + background: Theme.bg-secondary; + border-radius: 8px; + VerticalLayout { + padding: 12px; + spacing: 4px; + SectionHeader { text: "Key Overrides"; } + if AdvancedBridge.key-overrides.length == 0 : Text { text: "No key overrides"; color: Theme.fg-secondary; font-size: 11px; } + for ko in AdvancedBridge.key-overrides : ItemRow { + prefix: "KO" + ko.index; + left: ko.trigger; + right: ko.result; + delete => { AdvancedBridge.delete-ko(ko.index); } + } + + // --- Add KO --- + Rectangle { + background: Theme.bg-primary; + border-radius: 4px; + VerticalLayout { + padding: 10px; + spacing: 8px; + Text { text: "Add Key Override"; color: Theme.fg-secondary; font-size: 12px; font-weight: 600; } + HorizontalLayout { + spacing: 8px; + alignment: start; + Text { text: "Trigger:"; color: Theme.fg-secondary; font-size: 11px; vertical-alignment: center; } + PickButton { label: AdvancedBridge.new-ko-trigger-name; target: "ko-trigger"; } + Text { text: "Mod:"; color: Theme.fg-secondary; font-size: 11px; vertical-alignment: center; } + ModComboBox { current-index <=> AdvancedBridge.new-ko-trig-mod-idx; } + } + HorizontalLayout { + spacing: 8px; + alignment: start; + Text { text: "Result:"; color: Theme.fg-secondary; font-size: 11px; vertical-alignment: center; } + PickButton { label: AdvancedBridge.new-ko-result-name; target: "ko-result"; } + Text { text: "Mod:"; color: Theme.fg-secondary; font-size: 11px; vertical-alignment: center; } + ModComboBox { current-index <=> AdvancedBridge.new-ko-res-mod-idx; } + } + Button { + text: "Add Key Override"; + clicked => { AdvancedBridge.create-ko(AdvancedBridge.new-ko-trigger-code, AdvancedBridge.new-ko-trig-mod-idx, AdvancedBridge.new-ko-result-code, AdvancedBridge.new-ko-res-mod-idx); } + } + } + } + } + } + + // --- Autoshift --- + Rectangle { + background: Theme.bg-secondary; + border-radius: 8px; + VerticalLayout { + padding: 12px; + spacing: 6px; + SectionHeader { text: "Autoshift"; } + HorizontalLayout { + spacing: 8px; + Text { text: AdvancedBridge.autoshift-status != "" ? AdvancedBridge.autoshift-status : "Unknown"; color: Theme.fg-primary; font-size: 11px; vertical-alignment: center; } + Button { text: "Toggle"; clicked => { AdvancedBridge.toggle-autoshift(); } } + } + } + } + } + + // ==================== Right column ==================== + VerticalLayout { + horizontal-stretch: 1; + spacing: 12px; + + // --- Combos --- + Rectangle { + background: Theme.bg-secondary; + border-radius: 8px; + VerticalLayout { + padding: 12px; + spacing: 4px; + SectionHeader { text: "Combos"; } + if AdvancedBridge.combos.length == 0 : Text { text: "No combos"; color: Theme.fg-secondary; font-size: 11px; } + for combo in AdvancedBridge.combos : ItemRow { + prefix: "#" + combo.index; + left: combo.key1 + " + " + combo.key2; + right: combo.result; + delete => { AdvancedBridge.delete-combo(combo.index); } + } + + // --- Add Combo --- + Rectangle { + background: Theme.bg-primary; + border-radius: 4px; + VerticalLayout { + padding: 10px; + spacing: 8px; + Text { text: "Add Combo"; color: Theme.fg-secondary; font-size: 12px; font-weight: 600; } + HorizontalLayout { + spacing: 8px; + alignment: start; + Text { text: "Key 1:"; color: Theme.fg-secondary; font-size: 11px; vertical-alignment: center; } + PickButton { label: AdvancedBridge.new-combo-key1-name; target: "combo-key1"; } + Text { text: "Key 2:"; color: Theme.fg-secondary; font-size: 11px; vertical-alignment: center; } + PickButton { label: AdvancedBridge.new-combo-key2-name; target: "combo-key2"; } + } + HorizontalLayout { + spacing: 8px; + alignment: start; + Text { text: "Result:"; color: Theme.fg-secondary; font-size: 11px; vertical-alignment: center; } + PickButton { label: AdvancedBridge.new-combo-result-name; target: "combo-result"; } + Button { + text: "Add Combo"; + clicked => { AdvancedBridge.create-combo(); } + } + } + } + } + } + } + + // --- Leader Keys --- + Rectangle { + background: Theme.bg-secondary; + border-radius: 8px; + VerticalLayout { + padding: 12px; + spacing: 4px; + SectionHeader { text: "Leader Keys"; } + if AdvancedBridge.leaders.length == 0 : Text { text: "No leader keys"; color: Theme.fg-secondary; font-size: 11px; } + for leader in AdvancedBridge.leaders : ItemRow { + prefix: "#" + leader.index; + left: leader.sequence; + right: leader.result; + delete => { AdvancedBridge.delete-leader(leader.index); } + } + + // --- Add Leader --- + Rectangle { + background: Theme.bg-primary; + border-radius: 4px; + VerticalLayout { + padding: 10px; + spacing: 8px; + Text { text: "Add Leader Key"; color: Theme.fg-secondary; font-size: 12px; font-weight: 600; } + + // Sequence: show picked keys + Add button + HorizontalLayout { + spacing: 6px; + alignment: start; + Text { text: "Sequence:"; color: Theme.fg-secondary; font-size: 11px; vertical-alignment: center; } + if AdvancedBridge.new-leader-seq0-name != "" : Rectangle { + width: 60px; height: 26px; border-radius: 3px; background: Theme.button-bg; + Text { text: AdvancedBridge.new-leader-seq0-name; color: Theme.fg-primary; font-size: 10px; horizontal-alignment: center; vertical-alignment: center; } + } + if AdvancedBridge.new-leader-seq1-name != "" : Rectangle { + width: 60px; height: 26px; border-radius: 3px; background: Theme.button-bg; + Text { text: AdvancedBridge.new-leader-seq1-name; color: Theme.fg-primary; font-size: 10px; horizontal-alignment: center; vertical-alignment: center; } + } + if AdvancedBridge.new-leader-seq2-name != "" : Rectangle { + width: 60px; height: 26px; border-radius: 3px; background: Theme.button-bg; + Text { text: AdvancedBridge.new-leader-seq2-name; color: Theme.fg-primary; font-size: 10px; horizontal-alignment: center; vertical-alignment: center; } + } + if AdvancedBridge.new-leader-seq3-name != "" : Rectangle { + width: 60px; height: 26px; border-radius: 3px; background: Theme.button-bg; + Text { text: AdvancedBridge.new-leader-seq3-name; color: Theme.fg-primary; font-size: 10px; horizontal-alignment: center; vertical-alignment: center; } + } + if AdvancedBridge.new-leader-seq-count < 4 : PickButton { + width: 40px; + label: "+"; + target: "leader-seq"; + } + } + + HorizontalLayout { + spacing: 8px; + alignment: start; + Text { text: "Result:"; color: Theme.fg-secondary; font-size: 11px; vertical-alignment: center; } + PickButton { label: AdvancedBridge.new-leader-result-name; target: "leader-result"; } + Text { text: "Mod:"; color: Theme.fg-secondary; font-size: 11px; vertical-alignment: center; } + ModComboBox { current-index <=> AdvancedBridge.new-leader-mod-idx; } + } + Button { + text: "Add Leader Key"; + clicked => { AdvancedBridge.create-leader(AdvancedBridge.new-leader-result-code, AdvancedBridge.new-leader-mod-idx); } + } + } + } + } + } + + // --- Tri-Layer --- + Rectangle { + background: Theme.bg-secondary; + border-radius: 8px; + VerticalLayout { + padding: 12px; + spacing: 8px; + SectionHeader { text: "Tri-Layer"; } + HorizontalLayout { + spacing: 8px; + alignment: start; + Text { text: "L1:"; color: Theme.fg-secondary; font-size: 12px; vertical-alignment: center; } + ComboBox { width: 50px; model: ["0","1","2","3","4","5","6","7","8","9"]; current-index <=> AdvancedBridge.tri-l1-idx; } + Text { text: "L2:"; color: Theme.fg-secondary; font-size: 12px; vertical-alignment: center; } + ComboBox { width: 50px; model: ["0","1","2","3","4","5","6","7","8","9"]; current-index <=> AdvancedBridge.tri-l2-idx; } + Text { text: "-> L3:"; color: Theme.fg-secondary; font-size: 12px; vertical-alignment: center; } + ComboBox { width: 50px; model: ["0","1","2","3","4","5","6","7","8","9"]; current-index <=> AdvancedBridge.tri-l3-idx; } + Button { text: "Set"; clicked => { AdvancedBridge.set-trilayer(AdvancedBridge.tri-l1-idx, AdvancedBridge.tri-l2-idx, AdvancedBridge.tri-l3-idx); } } + } + } + } + + // --- BT --- + if AdvancedBridge.bt-status != "" : Rectangle { + background: Theme.bg-secondary; + border-radius: 8px; + VerticalLayout { + padding: 12px; + spacing: 4px; + SectionHeader { text: "Bluetooth"; } + Text { text: AdvancedBridge.bt-status; color: Theme.fg-primary; font-size: 11px; } + } + } + + // --- TAMA --- + if AdvancedBridge.tama-status != "" : Rectangle { + background: Theme.bg-secondary; + border-radius: 8px; + VerticalLayout { + padding: 12px; + spacing: 6px; + SectionHeader { text: "Tamagotchi"; } + Text { text: AdvancedBridge.tama-status; color: Theme.fg-primary; font-size: 11px; wrap: word-wrap; } + HorizontalLayout { + spacing: 4px; + Button { text: "Feed"; clicked => { AdvancedBridge.tama-action("feed"); } } + Button { text: "Play"; clicked => { AdvancedBridge.tama-action("play"); } } + Button { text: "Sleep"; clicked => { AdvancedBridge.tama-action("sleep"); } } + Button { text: "Meds"; clicked => { AdvancedBridge.tama-action("meds"); } } + Button { text: "On/Off"; clicked => { AdvancedBridge.tama-action("toggle"); } } + } + } + } + } + } + } + } +} diff --git a/ui/tabs/tab_flasher.slint b/ui/tabs/tab_flasher.slint new file mode 100644 index 0000000..db74107 --- /dev/null +++ b/ui/tabs/tab_flasher.slint @@ -0,0 +1,215 @@ +import { Button, ComboBox, LineEdit } from "std-widgets.slint"; +import { Theme } from "../theme.slint"; +import { FlasherBridge } from "../globals.slint"; + +export component TabFlasher inherits Rectangle { + background: Theme.bg-primary; + + VerticalLayout { + padding: 20px; + spacing: 16px; + alignment: start; + + Text { + text: "Firmware Flasher"; + color: Theme.fg-primary; + font-size: 20px; + font-weight: 700; + } + + Text { + text: "Flash firmware via ESP32 programming port (CH340/CP2102)."; + color: Theme.fg-secondary; + font-size: 12px; + wrap: word-wrap; + } + + // Port selection + Rectangle { + background: Theme.bg-secondary; + border-radius: 8px; + + VerticalLayout { + padding: 16px; + spacing: 10px; + + Text { + text: "Programming Port (CH340/CP210x only)"; + color: Theme.accent-cyan; + font-size: 14px; + font-weight: 600; + } + + HorizontalLayout { + spacing: 8px; + + if FlasherBridge.prog-ports.length > 0 : ComboBox { + horizontal-stretch: 1; + model: FlasherBridge.prog-ports; + selected(value) => { + FlasherBridge.selected-prog-port = value; + } + } + + if FlasherBridge.prog-ports.length == 0 : LineEdit { + horizontal-stretch: 1; + text <=> FlasherBridge.selected-prog-port; + placeholder-text: "/dev/ttyUSB0"; + } + + Button { + text: "Refresh"; + clicked => { FlasherBridge.refresh-prog-ports(); } + } + } + + if FlasherBridge.prog-ports.length == 0 : Text { + text: "No CH340/CP210x port detected. Enter path manually or plug in the programming cable."; + color: Theme.accent-yellow; + font-size: 11px; + wrap: word-wrap; + } + } + } + + // Flash target partition + Rectangle { + background: Theme.bg-secondary; + border-radius: 8px; + + VerticalLayout { + padding: 16px; + spacing: 10px; + + Text { + text: "Target Partition"; + color: Theme.accent-cyan; + font-size: 14px; + font-weight: 600; + } + + ComboBox { + model: ["factory (0x20000)", "ota_0 (0x220000)"]; + current-index <=> FlasherBridge.flash-offset-index; + } + } + } + + // Firmware file + Rectangle { + background: Theme.bg-secondary; + border-radius: 8px; + + VerticalLayout { + padding: 16px; + spacing: 10px; + + Text { + text: "Firmware File"; + color: Theme.accent-cyan; + font-size: 14px; + font-weight: 600; + } + + HorizontalLayout { + spacing: 8px; + + Text { + horizontal-stretch: 1; + text: FlasherBridge.firmware-path != "" ? FlasherBridge.firmware-path : "No file selected"; + color: FlasherBridge.firmware-path != "" ? Theme.fg-primary : Theme.fg-secondary; + font-size: 12px; + vertical-alignment: center; + overflow: elide; + } + + Button { + text: "Browse..."; + clicked => { FlasherBridge.browse-firmware(); } + } + } + } + } + + // Flash button + progress + Rectangle { + background: Theme.bg-secondary; + border-radius: 8px; + + VerticalLayout { + padding: 16px; + spacing: 10px; + + HorizontalLayout { + spacing: 12px; + + Button { + text: FlasherBridge.flashing ? "Flashing..." : "Flash Firmware"; + enabled: !FlasherBridge.flashing + && FlasherBridge.firmware-path != "" + && FlasherBridge.selected-prog-port != ""; + clicked => { FlasherBridge.flash(); } + } + + Text { + text: FlasherBridge.flash-status; + color: Theme.fg-primary; + font-size: 12px; + vertical-alignment: center; + horizontal-stretch: 1; + } + } + + if FlasherBridge.flashing || FlasherBridge.flash-progress > 0 : Rectangle { + height: 20px; + background: Theme.bg-primary; + border-radius: 4px; + + Rectangle { + x: 0; + width: parent.width * clamp(FlasherBridge.flash-progress, 0, 1); + height: 100%; + background: FlasherBridge.flash-progress >= 1.0 ? Theme.accent-green : Theme.accent-purple; + border-radius: 4px; + } + + Text { + text: round(FlasherBridge.flash-progress * 100) + "%"; + color: Theme.fg-primary; + font-size: 11px; + horizontal-alignment: center; + vertical-alignment: center; + } + } + } + } + + // Warning + Rectangle { + background: #ff555520; + border-radius: 8px; + border-width: 1px; + border-color: Theme.accent-red; + + VerticalLayout { + padding: 12px; + spacing: 4px; + + Text { + text: "Warning"; + color: Theme.accent-red; + font-size: 13px; + font-weight: 600; + } + Text { + text: "Do not disconnect the keyboard during flashing. The device will reboot automatically when done."; + color: Theme.fg-secondary; + font-size: 11px; + wrap: word-wrap; + } + } + } + + Rectangle { vertical-stretch: 1; } + } +} diff --git a/ui/tabs/tab_keymap.slint b/ui/tabs/tab_keymap.slint new file mode 100644 index 0000000..c8576ed --- /dev/null +++ b/ui/tabs/tab_keymap.slint @@ -0,0 +1,140 @@ +import { Button, LineEdit } from "std-widgets.slint"; +import { Theme } from "../theme.slint"; +import { KeymapBridge, AppState, ConnectionState } from "../globals.slint"; +import { KeyboardView } from "../components/keyboard_view.slint"; + +export component TabKeymap inherits VerticalLayout { + in-out property renaming: false; + + padding: 8px; + spacing: 6px; + + // Main area: layers sidebar + keyboard + HorizontalLayout { + vertical-stretch: 1; + spacing: 6px; + + // Layer sidebar (vertical) + VerticalLayout { + width: 90px; + spacing: 4px; + alignment: start; + + Text { + text: "Layers"; + color: Theme.fg-secondary; + font-size: 11px; + horizontal-alignment: center; + } + + for layer[idx] in KeymapBridge.layers : Rectangle { + height: 30px; + border-radius: 4px; + background: layer.active ? Theme.accent-purple : Theme.button-bg; + + Text { + text: layer.name; + color: Theme.fg-primary; + font-size: 11px; + horizontal-alignment: center; + vertical-alignment: center; + } + + TouchArea { + clicked => { KeymapBridge.switch-layer(layer.index); } + mouse-cursor: pointer; + } + } + + // Rename + if root.renaming : VerticalLayout { + spacing: 4px; + rename-input := LineEdit { + placeholder-text: "New name"; + accepted(text) => { + KeymapBridge.rename-layer(KeymapBridge.active-layer, text); + root.renaming = false; + } + } + Button { + text: "Cancel"; + clicked => { root.renaming = false; } + } + } + + if !root.renaming && AppState.connection == ConnectionState.connected : Rectangle { + height: 24px; + border-radius: 4px; + background: rename-ta.has-hover ? Theme.button-hover : Theme.button-bg; + + Text { + text: "Rename"; + color: Theme.fg-secondary; + font-size: 10px; + horizontal-alignment: center; + vertical-alignment: center; + } + + rename-ta := TouchArea { + clicked => { root.renaming = true; } + mouse-cursor: pointer; + } + } + + Rectangle { vertical-stretch: 1; } + + // Heatmap toggle + Rectangle { + height: 30px; + border-radius: 4px; + background: KeymapBridge.heatmap-enabled ? Theme.accent-orange : Theme.button-bg; + + Text { + text: "Heatmap"; + color: Theme.fg-primary; + font-size: 11px; + horizontal-alignment: center; + vertical-alignment: center; + } + + TouchArea { + clicked => { KeymapBridge.heatmap-enabled = !KeymapBridge.heatmap-enabled; } + mouse-cursor: pointer; + } + } + } + + // Keyboard view + KeyboardView { + horizontal-stretch: 1; + } + } + + // Selected key info bar + if KeymapBridge.selected-key-index >= 0 : Rectangle { + height: 36px; + background: Theme.bg-secondary; + border-radius: 4px; + + HorizontalLayout { + padding: 8px; + spacing: 12px; + + Text { + text: "Selected: " + KeymapBridge.selected-key-label; + color: Theme.accent-cyan; + font-size: 12px; + vertical-alignment: center; + } + + Rectangle { horizontal-stretch: 1; } + + Button { + text: "Change Key..."; + clicked => { + KeymapBridge.key-selector-open = true; + } + } + } + } +} diff --git a/ui/tabs/tab_macros.slint b/ui/tabs/tab_macros.slint new file mode 100644 index 0000000..50901a4 --- /dev/null +++ b/ui/tabs/tab_macros.slint @@ -0,0 +1,162 @@ +import { Button, ComboBox, ScrollView, LineEdit } from "std-widgets.slint"; +import { Theme } from "../theme.slint"; +import { MacroBridge, AppState, ConnectionState, KeymapBridge } from "../globals.slint"; + +export component TabMacros inherits Rectangle { + background: Theme.bg-primary; + + VerticalLayout { + padding: 16px; + spacing: 12px; + + // Header + HorizontalLayout { + spacing: 8px; + Text { text: "Macros"; color: Theme.fg-primary; font-size: 20px; font-weight: 700; vertical-alignment: center; } + Rectangle { horizontal-stretch: 1; } + Button { + text: "Refresh"; + enabled: AppState.connection == ConnectionState.connected; + clicked => { MacroBridge.refresh-macros(); } + } + } + + // New macro builder + Rectangle { + background: Theme.bg-secondary; + border-radius: 8px; + + VerticalLayout { + padding: 12px; + spacing: 10px; + + Text { text: "Add / Edit Macro"; color: Theme.accent-cyan; font-size: 13px; font-weight: 600; } + + HorizontalLayout { + spacing: 8px; + alignment: start; + + Text { text: "Slot:"; color: Theme.fg-secondary; font-size: 12px; vertical-alignment: center; } + ComboBox { + width: 60px; + model: ["0","1","2","3","4","5","6","7","8","9"]; + current-index <=> MacroBridge.new-slot-idx; + } + Text { text: "Name:"; color: Theme.fg-secondary; font-size: 12px; vertical-alignment: center; } + LineEdit { + horizontal-stretch: 1; + text <=> MacroBridge.new-name; + placeholder-text: "macro name"; + } + } + + // Steps display + Text { text: "Steps:"; color: Theme.fg-secondary; font-size: 12px; } + + // Show added steps as tags + HorizontalLayout { + spacing: 4px; + height: 30px; + + for step in MacroBridge.new-steps : Rectangle { + width: self.preferred-width + 12px; + preferred-width: step-text.preferred-width; + height: 26px; + border-radius: 4px; + background: step.is-delay ? Theme.accent-orange : Theme.accent-purple; + + step-text := Text { + text: step.label; + color: Theme.fg-primary; + font-size: 10px; + horizontal-alignment: center; + vertical-alignment: center; + } + } + + if MacroBridge.new-steps.length == 0 : Text { + text: "(empty - add keys and delays below)"; + color: Theme.fg-secondary; + font-size: 11px; + vertical-alignment: center; + } + } + + // Action buttons + HorizontalLayout { + spacing: 6px; + alignment: start; + + Button { + text: "Add Key"; + clicked => { + KeymapBridge.selector-target = "macro-step"; + KeymapBridge.key-selector-open = true; + } + } + + Button { text: "T 50ms"; clicked => { MacroBridge.add-delay-step(50); } } + Button { text: "T 100ms"; clicked => { MacroBridge.add-delay-step(100); } } + Button { text: "T 200ms"; clicked => { MacroBridge.add-delay-step(200); } } + + Rectangle { horizontal-stretch: 1; } + + Button { text: "Undo"; clicked => { MacroBridge.remove-last-step(); } } + Button { text: "Clear"; clicked => { MacroBridge.clear-steps(); } } + } + + // Save + HorizontalLayout { + spacing: 8px; + alignment: start; + + Button { + text: "Save Macro"; + enabled: AppState.connection == ConnectionState.connected; + clicked => { MacroBridge.save-macro(); } + } + + if MacroBridge.new-steps-text != "" : Text { + text: MacroBridge.new-steps-text; + color: Theme.fg-secondary; + font-size: 10px; + vertical-alignment: center; + overflow: elide; + } + } + } + } + + // Macro list + ScrollView { + vertical-stretch: 1; + + VerticalLayout { + spacing: 4px; + + if MacroBridge.macros.length == 0 : Text { + text: "No macros configured"; + color: Theme.fg-secondary; + font-size: 11px; + } + + for macro in MacroBridge.macros : Rectangle { + background: Theme.bg-secondary; + border-radius: 4px; + height: 36px; + + HorizontalLayout { + padding-left: 8px; + padding-right: 8px; + spacing: 8px; + + Text { text: "#" + macro.slot; color: Theme.accent-purple; font-size: 12px; vertical-alignment: center; width: 30px; } + Text { text: macro.name; color: Theme.fg-primary; font-size: 12px; font-weight: 600; vertical-alignment: center; width: 100px; } + Text { text: macro.steps; color: Theme.fg-secondary; font-size: 10px; vertical-alignment: center; horizontal-stretch: 1; overflow: elide; } + Button { text: "Del"; clicked => { MacroBridge.delete-macro(macro.slot); } } + } + } + } + } + } +} diff --git a/ui/tabs/tab_settings.slint b/ui/tabs/tab_settings.slint new file mode 100644 index 0000000..4bbcbef --- /dev/null +++ b/ui/tabs/tab_settings.slint @@ -0,0 +1,97 @@ +import { ComboBox } from "std-widgets.slint"; +import { Theme } from "../theme.slint"; +import { SettingsBridge } from "../globals.slint"; + +export component TabSettings inherits Rectangle { + background: Theme.bg-primary; + + VerticalLayout { + padding: 20px; + spacing: 16px; + alignment: start; + + Text { + text: "Settings"; + color: Theme.fg-primary; + font-size: 20px; + font-weight: 700; + } + + // Keyboard layout section + Rectangle { + background: Theme.bg-secondary; + border-radius: 8px; + height: 80px; + + HorizontalLayout { + padding: 16px; + spacing: 12px; + alignment: start; + + VerticalLayout { + alignment: center; + Text { + text: "Keyboard Layout"; + color: Theme.fg-primary; + font-size: 14px; + } + Text { + text: "Controls how keycodes are displayed (label remapping)"; + color: Theme.fg-secondary; + font-size: 11px; + } + } + + Rectangle { horizontal-stretch: 1; } + + VerticalLayout { + alignment: center; + ComboBox { + width: 200px; + model: SettingsBridge.available-layouts; + current-index <=> SettingsBridge.selected-layout-index; + selected(value) => { + SettingsBridge.change-layout(self.current-index); + } + } + } + } + } + + // About section + Rectangle { + background: Theme.bg-secondary; + border-radius: 8px; + + VerticalLayout { + padding: 16px; + spacing: 8px; + + Text { + text: "About"; + color: Theme.fg-primary; + font-size: 14px; + font-weight: 600; + } + Text { + text: "KaSe Controller v0.6.0"; + color: Theme.fg-secondary; + font-size: 12px; + } + Text { + text: "Split keyboard configurator — Slint UI port"; + color: Theme.fg-secondary; + font-size: 12px; + } + Text { + text: "Made with Slint"; + color: Theme.accent-purple; + font-size: 11px; + } + } + } + + // Spacer + Rectangle { vertical-stretch: 1; } + } +} diff --git a/ui/tabs/tab_stats.slint b/ui/tabs/tab_stats.slint new file mode 100644 index 0000000..39e7bdd --- /dev/null +++ b/ui/tabs/tab_stats.slint @@ -0,0 +1,332 @@ +import { Button, ScrollView } from "std-widgets.slint"; +import { Theme } from "../theme.slint"; +import { StatsBridge, AppState, ConnectionState } from "../globals.slint"; + +component BarChart inherits Rectangle { + in property value; // 0-100 + in property label; + in property bar-color: Theme.accent-purple; + height: 28px; + + HorizontalLayout { + spacing: 8px; + + Text { + width: 80px; + text: root.label; + color: Theme.fg-secondary; + font-size: 11px; + vertical-alignment: center; + horizontal-alignment: right; + } + + Rectangle { + horizontal-stretch: 1; + background: Theme.bg-primary; + border-radius: 3px; + + Rectangle { + x: 0; + width: parent.width * clamp(root.value / 100, 0, 1); + height: 100%; + background: root.bar-color; + border-radius: 3px; + } + } + + Text { + width: 50px; + text: round(root.value * 10) / 10 + "%"; + color: Theme.fg-primary; + font-size: 11px; + vertical-alignment: center; + } + } +} + +export component TabStats inherits Rectangle { + background: Theme.bg-primary; + + ScrollView { + VerticalLayout { + padding: 16px; + spacing: 12px; + + // Header + HorizontalLayout { + spacing: 8px; + Text { + text: "Typing Statistics"; + color: Theme.fg-primary; + font-size: 20px; + font-weight: 700; + vertical-alignment: center; + } + Rectangle { horizontal-stretch: 1; } + Button { + text: "Refresh"; + enabled: AppState.connection == ConnectionState.connected; + clicked => { StatsBridge.refresh-stats(); } + } + } + + // Content in two columns + HorizontalLayout { + spacing: 12px; + vertical-stretch: 1; + + // Left column + VerticalLayout { + horizontal-stretch: 1; + spacing: 12px; + + // Hand balance + Rectangle { + background: Theme.bg-secondary; + border-radius: 8px; + vertical-stretch: 0; + + VerticalLayout { + padding: 12px; + spacing: 6px; + + Text { + text: "Hand Balance"; + color: Theme.accent-cyan; + font-size: 13px; + font-weight: 600; + } + + HorizontalLayout { + spacing: 4px; + // Left bar + Rectangle { + horizontal-stretch: StatsBridge.hand-balance.left-pct > 0 ? round(StatsBridge.hand-balance.left-pct) : 1; + height: 24px; + background: Theme.accent-purple; + border-radius: 3px; + Text { + text: "L " + round(StatsBridge.hand-balance.left-pct) + "%"; + color: Theme.fg-primary; + font-size: 11px; + horizontal-alignment: center; + vertical-alignment: center; + } + } + // Right bar + Rectangle { + horizontal-stretch: StatsBridge.hand-balance.right-pct > 0 ? round(StatsBridge.hand-balance.right-pct) : 1; + height: 24px; + background: Theme.accent-green; + border-radius: 3px; + Text { + text: "R " + round(StatsBridge.hand-balance.right-pct) + "%"; + color: Theme.bg-primary; + font-size: 11px; + horizontal-alignment: center; + vertical-alignment: center; + } + } + } + + Text { + text: "Total: " + StatsBridge.hand-balance.total + " presses"; + color: Theme.fg-secondary; + font-size: 11px; + } + } + } + + // Bigrams + if StatsBridge.bigrams.total > 0 : Rectangle { + background: Theme.bg-secondary; + border-radius: 8px; + vertical-stretch: 0; + + VerticalLayout { + padding: 12px; + spacing: 6px; + + Text { + text: "Bigram Analysis"; + color: Theme.accent-cyan; + font-size: 13px; + font-weight: 600; + } + + BarChart { + label: "Alt Hand"; + value: StatsBridge.bigrams.alt-hand-pct; + bar-color: Theme.accent-green; + } + BarChart { + label: "Same Hand"; + value: StatsBridge.bigrams.same-hand-pct; + bar-color: Theme.accent-orange; + } + BarChart { + label: "SFB"; + value: StatsBridge.bigrams.sfb-pct; + bar-color: Theme.accent-red; + } + } + } + + // Finger load + Rectangle { + background: Theme.bg-secondary; + border-radius: 8px; + vertical-stretch: 1; + + VerticalLayout { + padding: 12px; + spacing: 4px; + + Text { + text: "Finger Load"; + color: Theme.accent-cyan; + font-size: 13px; + font-weight: 600; + } + + for finger in StatsBridge.finger-load : BarChart { + label: finger.name; + value: finger.pct; + bar-color: Theme.accent-orange; + } + } + } + } + + // Right column + VerticalLayout { + horizontal-stretch: 1; + spacing: 12px; + + // Row usage + Rectangle { + background: Theme.bg-secondary; + border-radius: 8px; + vertical-stretch: 0; + + VerticalLayout { + padding: 12px; + spacing: 4px; + + Text { + text: "Row Usage"; + color: Theme.accent-cyan; + font-size: 13px; + font-weight: 600; + } + + for row in StatsBridge.row-usage : BarChart { + label: row.name; + value: row.pct; + bar-color: Theme.accent-green; + } + } + } + + // Top keys + Rectangle { + background: Theme.bg-secondary; + border-radius: 8px; + vertical-stretch: 1; + + VerticalLayout { + padding: 12px; + spacing: 4px; + + Text { + text: "Top Keys"; + color: Theme.accent-cyan; + font-size: 13px; + font-weight: 600; + } + + for key in StatsBridge.top-keys : HorizontalLayout { + height: 22px; + spacing: 8px; + + Text { + width: 80px; + text: key.name; + color: Theme.fg-primary; + font-size: 11px; + vertical-alignment: center; + } + Text { + width: 60px; + text: key.finger; + color: Theme.fg-secondary; + font-size: 10px; + vertical-alignment: center; + } + Rectangle { + horizontal-stretch: 1; + background: Theme.bg-primary; + border-radius: 3px; + Rectangle { + x: 0; + width: parent.width * clamp(key.pct / 20, 0, 1); + height: 100%; + background: Theme.accent-yellow; + border-radius: 3px; + } + } + Text { + width: 50px; + text: key.count; + color: Theme.fg-primary; + font-size: 11px; + vertical-alignment: center; + horizontal-alignment: right; + } + } + } + } + + // Dead keys + if StatsBridge.dead-keys.length > 0 : Rectangle { + background: Theme.bg-secondary; + border-radius: 8px; + vertical-stretch: 0; + + VerticalLayout { + padding: 12px; + spacing: 4px; + + Text { + text: "Dead Keys (never pressed)"; + color: Theme.accent-red; + font-size: 13px; + font-weight: 600; + } + + HorizontalLayout { + spacing: 4px; + + for dk in StatsBridge.dead-keys : Rectangle { + width: self.preferred-width + 12px; + preferred-width: dk-text.preferred-width; + height: 22px; + background: Theme.bg-primary; + border-radius: 3px; + + dk-text := Text { + text: dk; + color: Theme.accent-red; + font-size: 10px; + horizontal-alignment: center; + vertical-alignment: center; + } + } + } + } + } + } + } + } + } +} diff --git a/ui/theme.slint b/ui/theme.slint new file mode 100644 index 0000000..052c5ad --- /dev/null +++ b/ui/theme.slint @@ -0,0 +1,23 @@ +// Dracula theme colors (matching the egui version) +export global Theme { + // Backgrounds + out property bg-primary: #282a36; + out property bg-secondary: #44475a; + out property bg-surface: #1e1e2e; + // Text + out property fg-primary: #f8f8f2; + out property fg-secondary: #6272a4; + // Accents + out property accent-purple: #bd93f9; + out property accent-green: #50fa7b; + out property accent-cyan: #8be9fd; + out property accent-red: #ff5555; + out property accent-yellow: #f1fa8c; + out property accent-orange: #ffb86c; + out property accent-pink: #ff79c6; + // UI elements + out property connected: #50fa7b; + out property disconnected: #ff5555; + out property button-bg: #44475a; + out property button-hover: #6272a4; +}