diff --git a/Cargo.lock b/Cargo.lock index df3afc3..71fb906 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,12 +2,181 @@ # 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 = "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 = "android-activity" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f2a1bb052857d5dd49572219344a7332b31b76405648eabac5bc68978251bcd" +dependencies = [ + "android-properties", + "bitflags 2.13.0", + "cc", + "jni", + "libc", + "log", + "ndk", + "ndk-context", + "ndk-sys 0.6.0+11769913", + "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 = "anstream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "anstyle-parse" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + +[[package]] +name = "arboard" +version = "3.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0348a1c054491f4bfe6ab86a7b6ab1e44e45d899005de92f58b3df180b36ddaf" +dependencies = [ + "clipboard-win", + "image", + "log", + "objc2 0.6.4", + "objc2-app-kit 0.3.2", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation 0.3.2", + "parking_lot", + "percent-encoding", + "windows-sys 0.59.0", + "x11rb", +] + +[[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 = "ash" +version = "0.38.0+1.3.281" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bb44936d800fea8f016d7f2311c6a4f97aebd5dc86f09906139ec848cf3a46f" +dependencies = [ + "libloading 0.8.9", +] + [[package]] name = "atk" version = "0.18.2" @@ -32,10 +201,37 @@ dependencies = [ ] [[package]] -name = "autocfg" -version = "1.5.0" +name = "atomic-waker" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bit-set" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0481a0e032742109b1133a095184ee93d88f3dc9e0d28a5d033dc77a073f44f" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2c54ff287cfc0a34f38a6b832ea1bd8e448a330b3e40a50859e6488bee07f22" [[package]] name = "bitflags" @@ -45,9 +241,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.11.1" +version = "2.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" +checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" dependencies = [ "serde_core", ] @@ -64,21 +260,78 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2c132eebf10f5cad5289222520a4a058514204aed6d791f1cf4fe8088b82d15f" dependencies = [ - "objc2", + "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 = "bstr" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "bytemuck" +version = "1.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" +dependencies = [ + "bytemuck_derive", +] + +[[package]] +name = "bytemuck_derive" +version = "1.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9abbd1bc6865053c427f7198e6af43bfdedc55ab791faed4fbd361d789575ff" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "byteorder-lite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" + [[package]] name = "cairo-rs" version = "0.18.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ca26ef0159422fb77631dc9d17b102f253b876fe1586b03b803e63a309b4ee2" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "cairo-sys-rs", "glib", "libc", "once_cell", - "thiserror", + "thiserror 1.0.69", ] [[package]] @@ -92,6 +345,44 @@ dependencies = [ "system-deps", ] +[[package]] +name = "calloop" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b99da2f8558ca23c71f4fd15dc57c906239752dd27ff3c00a1d56b685b7cbfec" +dependencies = [ + "bitflags 2.13.0", + "log", + "polling", + "rustix 0.38.44", + "slab", + "thiserror 1.0.69", +] + +[[package]] +name = "calloop-wayland-source" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95a66a987056935f7efce4ab5668920b5d0dac4a7c99991a67395f13702ddd20" +dependencies = [ + "calloop", + "rustix 0.38.44", + "wayland-backend", + "wayland-client", +] + +[[package]] +name = "cc" +version = "1.2.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "556e016178bb5662a08681bbe0f00f8e17631781a4dfc8c45e466e4b185ec27f" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", + "shlex", +] + [[package]] name = "cfg-expr" version = "0.15.8" @@ -109,39 +400,97 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] -name = "cocoa" -version = "0.26.1" +name = "cfg_aliases" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad36507aeb7e16159dfe68db81ccc27571c3ccd4b76fb2fb72fc59e7a4b1b64c" +checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "clipboard-win" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bde03770d3df201d4fb868f2c9c59e66a3e4e2bd06692a0fe701e7103c7e84d4" dependencies = [ - "bitflags 2.11.1", - "block", - "cocoa-foundation", - "core-foundation", - "core-graphics", - "foreign-types", - "libc", - "objc", + "error-code", ] [[package]] -name = "cocoa-foundation" -version = "0.2.1" +name = "codespan-reporting" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81411967c50ee9a1fc11365f8c585f863a22a9697c89239c452292c40ba79b0d" +checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e" dependencies = [ - "bitflags 2.11.1", - "block", - "core-foundation", - "core-graphics-types", - "objc", + "termcolor", + "unicode-width 0.1.14", +] + +[[package]] +name = "colorchoice" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" + +[[package]] +name = "com" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e17887fd17353b65b1b2ef1c526c83e26cd72e74f598a8dc1bee13a48f3d9f6" +dependencies = [ + "com_macros", +] + +[[package]] +name = "com_macros" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d375883580a668c7481ea6631fc1a8863e33cc335bf56bfad8d7e6d4b04b13a5" +dependencies = [ + "com_macros_support", + "proc-macro2", + "syn 1.0.109", +] + +[[package]] +name = "com_macros_support" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad899a1087a9296d5644792d7cb72b8e34c1bec8e7d4fbc002230169a6e8710c" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", ] [[package]] name = "core-foundation" -version = "0.10.1" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" dependencies = [ "core-foundation-sys", "libc", @@ -155,11 +504,11 @@ checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] name = "core-graphics" -version = "0.24.0" +version = "0.23.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa95a34622365fa5bbf40b20b75dba8dfa8c94c734aea8ac9a5ca38af14316f1" +checksum = "c07782be35f9e1140080c6b96f0d44b739e2278479f64e02fdab4e32dfd8b081" dependencies = [ - "bitflags 2.11.1", + "bitflags 1.3.2", "core-foundation", "core-graphics-types", "foreign-types", @@ -168,11 +517,11 @@ dependencies = [ [[package]] name = "core-graphics-types" -version = "0.2.0" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" +checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf" dependencies = [ - "bitflags 2.11.1", + "bitflags 1.3.2", "core-foundation", "libc", ] @@ -202,38 +551,158 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" [[package]] -name = "dirs" -version = "5.0.1" +name = "crunchy" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + +[[package]] +name = "cursor-icon" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f27ae1dd37df86211c42e150270f82743308803d90a6f6e6651cd730d5e1732f" + +[[package]] +name = "d3d12" +version = "22.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdbd1f579714e3c809ebd822c81ef148b1ceaeb3d535352afc73fd0c4c6a0017" +dependencies = [ + "bitflags 2.13.0", + "libloading 0.8.9", + "winapi", +] + +[[package]] +name = "dirs" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" dependencies = [ "dirs-sys", ] [[package]] name = "dirs-sys" -version = "0.4.1" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" dependencies = [ "libc", "option-ext", "redox_users", - "windows-sys 0.48.0", + "windows-sys 0.61.2", ] +[[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.13.0", + "objc2 0.6.4", +] + +[[package]] +name = "dlib" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab8ecd87370524b461f8557c119c405552c396ed91fc0a8eec68679eab26f94a" +dependencies = [ + "libloading 0.8.9", +] + +[[package]] +name = "document-features" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" +dependencies = [ + "litrs", +] + +[[package]] +name = "downcast-rs" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" + [[package]] name = "dpi" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76" +[[package]] +name = "either" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" + +[[package]] +name = "env_filter" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e90c2accc4b07a8456ea0debdc2e7587bdd890680d71173a15d4ae604f6eef" +dependencies = [ + "log", + "regex", +] + +[[package]] +name = "env_home" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7f84e12ccf0a7ddc17a6c41c93326024c42920d7ee630d04950e6926645c0fe" + +[[package]] +name = "env_logger" +version = "0.11.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0621c04f2196ac3f488dd583365b9c09be011a4ab8b9f37248ffcc8f6198b56a" +dependencies = [ + "anstream", + "anstyle", + "env_filter", + "jiff", + "log", +] + [[package]] name = "equivalent" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "error-code" +version = "3.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dea2df4cf52843e0452895c455a1a2cfbb842a1e7329671acf418fdc53ed4c59" + +[[package]] +name = "fax" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caf1079563223d5d59d83c85886a56e586cfd5c1a26292e971a0fa266531ac5a" + [[package]] name = "fdeflate" version = "0.3.7" @@ -253,6 +722,22 @@ dependencies = [ "rustc_version", ] +[[package]] +name = "filetime" +version = "0.2.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c287a33c7f0a620c38e641e7f60827713987b3c0f26e8ddc9462cc69cf75759" +dependencies = [ + "cfg-if", + "libc", +] + +[[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" @@ -263,6 +748,44 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "font-types" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3971f9a5ca983419cdc386941ba3b9e1feba01a0ab888adf78739feb2798492" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "fontconfig-parser" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbc773e24e02d4ddd8395fd30dc147524273a83e54e0f312d986ea30de5f5646" +dependencies = [ + "roxmltree", +] + +[[package]] +name = "fontdb" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0299020c3ef3f60f526a4f64ab4a3d4ce116b1acbf24cdd22da0068e5d81dc3" +dependencies = [ + "fontconfig-parser", + "log", + "memmap2", + "slotmap", + "tinyvec", + "ttf-parser 0.20.0", +] + [[package]] name = "foreign-types" version = "0.5.0" @@ -290,6 +813,15 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" +[[package]] +name = "fsevent-sys" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76ee7a02da4d231650c7cea31349b889be2f45ddb3ef3032d2ec8185f6313fd2" +dependencies = [ + "libc", +] + [[package]] name = "futures-channel" version = "0.3.32" @@ -410,6 +942,16 @@ dependencies = [ "system-deps", ] +[[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", +] + [[package]] name = "getrandom" version = "0.2.17" @@ -421,6 +963,18 @@ dependencies = [ "wasi", ] +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", +] + [[package]] name = "gio" version = "0.18.4" @@ -437,7 +991,7 @@ dependencies = [ "once_cell", "pin-project-lite", "smallvec", - "thiserror", + "thiserror 1.0.69", ] [[package]] @@ -453,13 +1007,24 @@ dependencies = [ "winapi", ] +[[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 = "glib" version = "0.18.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "233daaf6e83ae6a12a52055f568f9d7cf4671dabb78ff9560ab6da230ce00ee5" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "futures-channel", "futures-core", "futures-executor", @@ -473,7 +1038,7 @@ dependencies = [ "memchr", "once_cell", "smallvec", - "thiserror", + "thiserror 1.0.69", ] [[package]] @@ -500,6 +1065,27 @@ dependencies = [ "system-deps", ] +[[package]] +name = "glow" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd348e04c43b32574f2de31c8bb397d96c9fcfa1371bd4ca6d8bdc464ab121b1" +dependencies = [ + "js-sys", + "slotmap", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "glutin_wgl_sys" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c4ee00b289aba7a9e5306d57c2d05499b2e5dc427f84ac708bd2c090212cf3e" +dependencies = [ + "gl_generator", +] + [[package]] name = "gobject-sys" version = "0.18.0" @@ -511,6 +1097,58 @@ dependencies = [ "system-deps", ] +[[package]] +name = "gpu-alloc" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbcd2dba93594b227a1f57ee09b8b9da8892c34d55aa332e034a228d0fe6a171" +dependencies = [ + "bitflags 2.13.0", + "gpu-alloc-types", +] + +[[package]] +name = "gpu-alloc-types" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98ff03b468aa837d70984d55f5d3f846f6ec31fe34bbb97c4f85219caeee1ca4" +dependencies = [ + "bitflags 2.13.0", +] + +[[package]] +name = "gpu-allocator" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdd4240fc91d3433d5e5b0fc5b67672d771850dc19bbee03c1381e19322803d7" +dependencies = [ + "log", + "presser", + "thiserror 1.0.69", + "winapi", + "windows 0.52.0", +] + +[[package]] +name = "gpu-descriptor" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b89c83349105e3732062a895becfc71a8f921bb71ecbbdd8ff99263e3b53a0ca" +dependencies = [ + "bitflags 2.13.0", + "gpu-descriptor-types", + "hashbrown 0.15.5", +] + +[[package]] +name = "gpu-descriptor-types" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdf242682df893b86f33a73828fb09ca4b2d3bb6cc95249707fc684d27484b91" +dependencies = [ + "bitflags 2.13.0", +] + [[package]] name = "gtk" version = "0.18.2" @@ -563,12 +1201,47 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "half" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" +dependencies = [ + "cfg-if", + "crunchy", + "zerocopy", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", +] + [[package]] name = "hashbrown" version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" +[[package]] +name = "hassle-rs" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af2a7e73e1f34c48da31fb668a907f250794837e08faa144fd24f0b8b741e890" +dependencies = [ + "bitflags 2.13.0", + "com", + "libc", + "libloading 0.8.9", + "thiserror 1.0.69", + "widestring", + "winapi", +] + [[package]] name = "heck" version = "0.4.1" @@ -581,6 +1254,34 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + +[[package]] +name = "hexf-parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df" + +[[package]] +name = "image" +version = "0.25.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104" +dependencies = [ + "bytemuck", + "byteorder-lite", + "moxcms", + "num-traits", + "png", + "tiff", + "zune-core", + "zune-jpeg", +] + [[package]] name = "indexmap" version = "2.14.0" @@ -588,15 +1289,137 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" dependencies = [ "equivalent", - "hashbrown", + "hashbrown 0.17.1", ] [[package]] -name = "iwaku" -version = "0.1.0" +name = "inotify" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8069d3ec154eb856955c1c0fbffefbf5f3c40a104ec912d4797314c1801abff" dependencies = [ - "tray-icon", - "windows", + "bitflags 1.3.2", + "inotify-sys", + "libc", +] + +[[package]] +name = "inotify-sys" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb" +dependencies = [ + "libc", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "jiff" +version = "0.2.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4603d3033e49e2b0e31229fcab20a5d40089c607d975cd9c80551dc69eed9102" +dependencies = [ + "jiff-static", + "log", + "portable-atomic", + "portable-atomic-util", + "serde_core", +] + +[[package]] +name = "jiff-static" +version = "0.2.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "782d32378dddf207193ac91cefb848ad41abb58195c95168e1291227a0832b47" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[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", +] + +[[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 2.0.117", +] + +[[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 2.0.117", +] + +[[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.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "142bc4740e452c1e57ade0cbc129f139c9093e354346f0872ef985f4f5cf5f11" +dependencies = [ + "cfg-if", + "futures-util", + "once_cell", + "wasm-bindgen", ] [[package]] @@ -605,11 +1428,48 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b750dcadc39a09dbadd74e118f6dd6598df77fa01df0cfcdc52c28dece74528a" dependencies = [ - "bitflags 2.11.1", + "bitflags 2.13.0", "serde", "unicode-segmentation", ] +[[package]] +name = "khronos-egl" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6aae1df220ece3c0ada96b8153459b67eebe9ae9212258bb0134ae60416fdf76" +dependencies = [ + "libc", + "libloading 0.8.9", + "pkg-config", +] + +[[package]] +name = "khronos_api" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc" + +[[package]] +name = "kqueue" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "273c0752728918e0ac4976f2b275b6fefb9ecd400585dec929419f3844cd87b5" +dependencies = [ + "kqueue-sys", + "libc", +] + +[[package]] +name = "kqueue-sys" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07293a4e297ac234359b510362495713f75ea345d5307140414f20c69ffeb087" +dependencies = [ + "bitflags 2.13.0", + "libc", +] + [[package]] name = "libappindicator" version = "0.9.0" @@ -630,7 +1490,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e9ec52138abedcc58dc17a7c6c0c00a2bdb4f3427c7f63fa97fd0d859155caf" dependencies = [ "gtk-sys", - "libloading", + "libloading 0.7.4", "once_cell", ] @@ -651,12 +1511,25 @@ dependencies = [ ] [[package]] -name = "libredox" -version = "0.1.16" +name = "libloading" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e02f3bb43d335493c96bf3fd3a321600bf6bd07ed34bc64118e9293bdffea46c" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" dependencies = [ + "cfg-if", + "windows-link", +] + +[[package]] +name = "libredox" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f02ab6bace2054fb888a3c16f990117b579d14a3088e472d63c6011fa185c9d3" +dependencies = [ + "bitflags 2.13.0", "libc", + "plain", + "redox_syscall 0.8.1", ] [[package]] @@ -679,10 +1552,56 @@ dependencies = [ ] [[package]] -name = "log" -version = "0.4.29" +name = "linux-raw-sys" +version = "0.4.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "litrs" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "953f07c43838f8e6f9758cab68bf5bed85465e7587ebe0b823f1bcd81978ad3a" + +[[package]] +name = "lua-src" +version = "547.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1edaf29e3517b49b8b746701e5648ccb5785cde1c119062cbabbc5d5cd115e42" +dependencies = [ + "cc", +] + +[[package]] +name = "luajit-src" +version = "210.5.12+a4f56a4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3a8e7962a5368d5f264d045a5a255e90f9aa3fc1941ae15a8d2940d42cac671" +dependencies = [ + "cc", + "which", +] [[package]] name = "malloc_buf" @@ -695,9 +1614,18 @@ dependencies = [ [[package]] name = "memchr" -version = "2.8.0" +version = "2.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" +checksum = "6b947ae49db0d222b1dbc6b113ce7248a3fc3a6ca21b696717bfc000ba4484d8" + +[[package]] +name = "memmap2" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "714098028fe011992e1c3962653c96b2d578c4b4bce9036e15ff220319b1e0e3" +dependencies = [ + "libc", +] [[package]] name = "memoffset" @@ -708,6 +1636,21 @@ dependencies = [ "autocfg", ] +[[package]] +name = "metal" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ecfd3296f8c56b7c1f6fbac3c71cefa9d78ce009850c45000015f206dc7fa21" +dependencies = [ + "bitflags 2.13.0", + "block", + "core-graphics-types", + "foreign-types", + "log", + "objc", + "paste", +] + [[package]] name = "miniz_oxide" version = "0.8.9" @@ -719,22 +1662,184 @@ dependencies = [ ] [[package]] -name = "muda" -version = "0.14.1" +name = "mio" +version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba8ac4080fb1e097c2c22acae467e46e4da72d941f02e82b67a87a2a89fa38b1" +checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" +dependencies = [ + "libc", + "log", + "wasi", + "windows-sys 0.48.0", +] + +[[package]] +name = "mlua" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1f5f8fbebc7db5f671671134b9321c4b9aa9adeafccfd9a8c020ae45c6a35d0" +dependencies = [ + "bstr", + "either", + "mlua-sys", + "num-traits", + "parking_lot", + "rustc-hash 2.1.2", + "rustversion", +] + +[[package]] +name = "mlua-sys" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "380c1f7e2099cafcf40e51d3a9f20a346977587aa4d012eae1f043149a728a93" +dependencies = [ + "cc", + "cfg-if", + "lua-src", + "luajit-src", + "pkg-config", +] + +[[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.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47a2e3dff89cd322c66647942668faee0a2b1f88ea6cbb4d374b4a8d7e92528c" dependencies = [ - "cocoa", "crossbeam-channel", "dpi", "gtk", "keyboard-types", "libxdo", - "objc", + "objc2 0.6.4", + "objc2-app-kit 0.3.2", + "objc2-core-foundation", + "objc2-foundation 0.3.2", "once_cell", "png", - "thiserror", - "windows-sys 0.59.0", + "thiserror 2.0.18", + "windows-sys 0.61.2", +] + +[[package]] +name = "naga" +version = "22.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8bd5a652b6faf21496f2cfd88fc49989c8db0825d1f6746b1a71a6ede24a63ad" +dependencies = [ + "arrayvec", + "bit-set", + "bitflags 2.13.0", + "cfg_aliases 0.1.1", + "codespan-reporting", + "hexf-parse", + "indexmap", + "log", + "rustc-hash 1.1.0", + "spirv", + "termcolor", + "thiserror 1.0.69", + "unicode-xid", +] + +[[package]] +name = "ndk" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" +dependencies = [ + "bitflags 2.13.0", + "jni-sys 0.3.1", + "log", + "ndk-sys 0.6.0+11769913", + "num_enum", + "raw-window-handle", + "thiserror 1.0.69", +] + +[[package]] +name = "ndk-context" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" + +[[package]] +name = "ndk-sys" +version = "0.5.0+25.2.9519653" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c196769dd60fd4f363e11d948139556a344e79d451aeb2fa2fd040738ef7691" +dependencies = [ + "jni-sys 0.3.1", +] + +[[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 = "notify" +version = "6.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6205bd8bb1e454ad2e27422015fb5e4f2bcc7e08fa8f27058670d208324a4d2d" +dependencies = [ + "bitflags 2.13.0", + "crossbeam-channel", + "filetime", + "fsevent-sys", + "inotify", + "kqueue", + "libc", + "log", + "mio", + "walkdir", + "windows-sys 0.48.0", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_enum" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26" +dependencies = [ + "num_enum_derive", + "rustversion", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" +dependencies = [ + "proc-macro-crate 3.5.0", + "proc-macro2", + "quote", + "syn 2.0.117", ] [[package]] @@ -762,32 +1867,102 @@ dependencies = [ "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.1", - "block2", + "bitflags 2.13.0", + "block2 0.5.1", "libc", - "objc2", + "objc2 0.5.2", "objc2-core-data", "objc2-core-image", - "objc2-foundation", + "objc2-foundation 0.2.2", "objc2-quartz-core", ] +[[package]] +name = "objc2-app-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" +dependencies = [ + "bitflags 2.13.0", + "objc2 0.6.4", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation 0.3.2", +] + +[[package]] +name = "objc2-cloud-kit" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74dd3b56391c7a0596a295029734d3c1c5e7e510a4cb30245f8221ccea96b009" +dependencies = [ + "bitflags 2.13.0", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-core-location", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-contacts" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5ff520e9c33812fd374d8deecef01d4a840e7b41862d849513de77e44aa4889" +dependencies = [ + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", +] + [[package]] name = "objc2-core-data" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef" dependencies = [ - "bitflags 2.11.1", - "block2", - "objc2", - "objc2-foundation", + "bitflags 2.13.0", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-core-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" +dependencies = [ + "bitflags 2.13.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.13.0", + "dispatch2", + "objc2 0.6.4", + "objc2-core-foundation", + "objc2-io-surface", ] [[package]] @@ -796,12 +1971,24 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55260963a527c99f1819c4f8e3b47fe04f9650694ef348ffd2227e8196d34c80" dependencies = [ - "block2", - "objc2", - "objc2-foundation", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", "objc2-metal", ] +[[package]] +name = "objc2-core-location" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "000cfee34e683244f284252ee206a27953279d370e309649dc3ee317b37e5781" +dependencies = [ + "block2 0.5.1", + "objc2 0.5.2", + "objc2-contacts", + "objc2-foundation 0.2.2", +] + [[package]] name = "objc2-encode" version = "4.1.0" @@ -814,10 +2001,46 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" dependencies = [ - "bitflags 2.11.1", - "block2", + "bitflags 2.13.0", + "block2 0.5.1", + "dispatch", "libc", - "objc2", + "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.13.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.13.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]] @@ -826,10 +2049,10 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6" dependencies = [ - "bitflags 2.11.1", - "block2", - "objc2", - "objc2-foundation", + "bitflags 2.13.0", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", ] [[package]] @@ -838,25 +2061,105 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a" dependencies = [ - "bitflags 2.11.1", - "block2", - "objc2", - "objc2-foundation", + "bitflags 2.13.0", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", "objc2-metal", ] +[[package]] +name = "objc2-symbols" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a684efe3dec1b305badae1a28f6555f6ddd3bb2c2267896782858d5a78404dc" +dependencies = [ + "objc2 0.5.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-ui-kit" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8bb46798b20cd6b91cbd113524c490f1686f4c4e8f49502431415f3512e2b6f" +dependencies = [ + "bitflags 2.13.0", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-cloud-kit", + "objc2-core-data", + "objc2-core-image", + "objc2-core-location", + "objc2-foundation 0.2.2", + "objc2-link-presentation", + "objc2-quartz-core", + "objc2-symbols", + "objc2-uniform-type-identifiers", + "objc2-user-notifications", +] + +[[package]] +name = "objc2-uniform-type-identifiers" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44fa5f9748dbfe1ca6c0b79ad20725a11eca7c2218bceb4b005cb1be26273bfe" +dependencies = [ + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-user-notifications" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76cfcbf642358e8689af64cee815d139339f3ed8ad05103ed5eaf73db8d84cb3" +dependencies = [ + "bitflags 2.13.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" +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + [[package]] name = "option-ext" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" +[[package]] +name = "orbclient" +version = "0.3.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5df339f526ea9a60e371768d50efc2f2508c7203290731565d1f7a6f71d21747" +dependencies = [ + "libc", + "libredox", +] + +[[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 = "pango" version = "0.18.3" @@ -882,6 +2185,61 @@ dependencies = [ "system-deps", ] +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall 0.5.18", + "smallvec", + "windows-link", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pin-project" +version = "1.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2466b2336ed02bcdca6b294417127b90ec92038d1d5c4fbeac971a922e0e0924" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c96395f0a926bc13b1c17622aaddda1ecb55d49c8f1bf9777e4d877800a43f8b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "pin-project-lite" version = "0.2.17" @@ -895,18 +2253,65 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" [[package]] -name = "png" -version = "0.17.16" +name = "plain" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526" +checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" + +[[package]] +name = "png" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.13.0", "crc32fast", "fdeflate", "flate2", "miniz_oxide", ] +[[package]] +name = "polling" +version = "3.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" +dependencies = [ + "cfg-if", + "concurrent-queue", + "hermit-abi", + "pin-project-lite", + "rustix 1.1.4", + "windows-sys 0.61.2", +] + +[[package]] +name = "pollster" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22686f4785f02a4fcc856d3b3bb19bf6c8160d103f7a99cc258bddd0251dc7f2" + +[[package]] +name = "portable-atomic" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" + +[[package]] +name = "portable-atomic-util" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a106d1259c23fac8e543272398ae0e3c0b8d33c88ed73d0cc71b0f1d902618" +dependencies = [ + "portable-atomic", +] + +[[package]] +name = "presser" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8cf8e6a8aa66ce33f63993ffc4ea4271eb5b0530a9002db8455ea6050c77bfa" + [[package]] name = "proc-macro-crate" version = "1.3.1" @@ -923,10 +2328,19 @@ version = "2.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b00f26d3400549137f92511a46ac1cd8ce37cb5598a96d382381458b992a5d24" dependencies = [ - "toml_datetime", + "toml_datetime 0.6.3", "toml_edit 0.20.2", ] +[[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.12+spec-1.1.0", +] + [[package]] name = "proc-macro-error" version = "1.0.4" @@ -960,6 +2374,33 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "profiling" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d595e54a326bc53c1c197b32d295e14b169e3cfeaa8dc82b529f947fba6bcf5" + +[[package]] +name = "pxfm" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0c5ccf5294c6ccd63a74f1565028353830a9c2f5eb0c682c355c471726a6e3f" + +[[package]] +name = "quick-error" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" + +[[package]] +name = "quick-xml" +version = "0.39.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdcc8dd4e2f670d309a5f0e83fe36dfdc05af317008fea29144da1a2ac858e5e" +dependencies = [ + "memchr", +] + [[package]] name = "quote" version = "1.0.45" @@ -970,16 +2411,124 @@ dependencies = [ ] [[package]] -name = "redox_users" -version = "0.4.6" +name = "r-efi" +version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "range-alloc" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca45419789ae5a7899559e9512e58ca889e41f04f1f2445e9f4b290ceccd1d08" + +[[package]] +name = "raw-window-handle" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" + +[[package]] +name = "read-fonts" +version = "0.22.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69aacb76b5c29acfb7f90155d39759a29496aebb49395830e928a9703d2eec2f" dependencies = [ - "getrandom", - "libredox", - "thiserror", + "bytemuck", + "font-types", ] +[[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.13.0", +] + +[[package]] +name = "redox_syscall" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b44b894f2a6e36457d665d1e08c3866add6ed5e70050c1b4ba8a8ddedb02ce7" +dependencies = [ + "bitflags 2.13.0", +] + +[[package]] +name = "redox_users" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" +dependencies = [ + "getrandom 0.2.17", + "libredox", + "thiserror 2.0.18", +] + +[[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 = "renderdoc-sys" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832" + +[[package]] +name = "roxmltree" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c20b6793b5c2fa6553b250154b78d6d0db37e72700ae35fad9387a46f487c97" + +[[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" @@ -989,6 +2538,72 @@ dependencies = [ "semver", ] +[[package]] +name = "rustix" +version = "0.38.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +dependencies = [ + "bitflags 2.13.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.13.0", + "errno", + "libc", + "linux-raw-sys 0.12.1", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "scoped-tls" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "sctk-adwaita" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6277f0217056f77f1d8f49f2950ac6c278c0d607c45f5ee99328d792ede24ec" +dependencies = [ + "ab_glyph", + "log", + "memmap2", + "smithay-client-toolkit", + "tiny-skia", +] + [[package]] name = "semver" version = "1.0.28" @@ -1034,24 +2649,131 @@ dependencies = [ "serde", ] +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + [[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 = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + +[[package]] +name = "skrifa" +version = "0.22.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1c44ad1f6c5bdd4eefed8326711b7dbda9ea45dfd36068c427d332aa382cbe" +dependencies = [ + "bytemuck", + "read-fonts", +] + [[package]] name = "slab" version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" +[[package]] +name = "slotmap" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdd58c3c93c3d278ca835519292445cb4b0d4dc59ccfdf7ceadaab3f8aeb4038" +dependencies = [ + "version_check", +] + [[package]] name = "smallvec" version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +[[package]] +name = "smithay-client-toolkit" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3457dea1f0eb631b4034d61d4d8c32074caa6cd1ab2d59f2327bd8461e2c0016" +dependencies = [ + "bitflags 2.13.0", + "calloop", + "calloop-wayland-source", + "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 = "smol_str" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd538fb6910ac1099850255cf94a94df6551fbdd602454387d0adb2d1ca6dead" +dependencies = [ + "serde", +] + +[[package]] +name = "spirv" +version = "0.3.0+sdk-1.3.268.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eda41003dc44290527a59b13432d4a0379379fa074b70174882adfbdfd917844" +dependencies = [ + "bitflags 2.13.0", +] + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "strict-num" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731" + +[[package]] +name = "swash" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbd59f3f359ddd2c95af4758c18270eddd9c730dde98598023cdabff472c2ca2" +dependencies = [ + "skrifa", + "yazi", + "zeno", +] + [[package]] name = "syn" version = "1.0.109" @@ -1059,6 +2781,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" dependencies = [ "proc-macro2", + "quote", "unicode-ident", ] @@ -1092,13 +2815,31 @@ version = "0.12.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + [[package]] name = "thiserror" version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" dependencies = [ - "thiserror-impl", + "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]] @@ -1112,6 +2853,71 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "tiff" +version = "0.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", + "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 = "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.8.2" @@ -1120,7 +2926,7 @@ checksum = "185d8ab0dfbb35cf1399a6344d8484209c088f75f8f68230da55d48d95d43e3d" dependencies = [ "serde", "serde_spanned", - "toml_datetime", + "toml_datetime 0.6.3", "toml_edit 0.20.2", ] @@ -1133,6 +2939,15 @@ dependencies = [ "serde", ] +[[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.19.15" @@ -1140,8 +2955,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" dependencies = [ "indexmap", - "toml_datetime", - "winnow", + "toml_datetime 0.6.3", + "winnow 0.5.40", ] [[package]] @@ -1153,30 +2968,80 @@ dependencies = [ "indexmap", "serde", "serde_spanned", - "toml_datetime", - "winnow", + "toml_datetime 0.6.3", + "winnow 0.5.40", ] [[package]] -name = "tray-icon" -version = "0.17.0" +name = "toml_edit" +version = "0.25.12+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "044d7738b3d50f288ddef035b793228740ad4d927f5466b0af55dc15e7e03cfe" +checksum = "d2153edc6955a6c354fad8f5efd38b6a8769bdccf9fe50f8e1329f81b0baa5d7" +dependencies = [ + "indexmap", + "toml_datetime 1.1.1+spec-1.1.0", + "toml_parser", + "winnow 1.0.3", +] + +[[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.3", +] + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" + +[[package]] +name = "tray-icon" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e47e6d063cfe4ad2e416fcbb310be3a37c5fd85c745b62cb562bfa4a003df674" dependencies = [ - "core-graphics", "crossbeam-channel", "dirs", "libappindicator", "muda", - "objc2", - "objc2-app-kit", - "objc2-foundation", + "objc2 0.6.4", + "objc2-app-kit 0.3.2", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation 0.3.2", "once_cell", "png", - "thiserror", - "windows-sys 0.59.0", + "thiserror 2.0.18", + "windows-sys 0.61.2", ] +[[package]] +name = "ttf-parser" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17f77d76d837a7830fe1d4f12b7b4ba4192c1888001c7164257e4bc6d21d96b4" + +[[package]] +name = "ttf-parser" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31" + [[package]] name = "unicode-ident" version = "1.0.24" @@ -1185,9 +3050,33 @@ checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] name = "unicode-segmentation" -version = "1.13.2" +version = "1.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c" +checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" + +[[package]] +name = "unicode-width" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" + +[[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 = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "version-compare" @@ -1201,12 +3090,345 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + [[package]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" +[[package]] +name = "wasip2" +version = "1.0.3+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.122" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ed04576f974d2b2fba0f38c51dbc5518011e38c36bf1143164be765528fd409" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.72" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9473dbd2991ae90b6291c3c32c30c6187ac49aa32f9905d1cce280ec1e110b0f" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.122" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "916151b09da36bd82f6615cbf3a419e2f0ba23a03c6160e8e92eb6bd4aa1dec6" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.122" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "299047362ccbfce148b67ab7e73349f77748e00c8296f9542adfad2ad82c5c5e" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.117", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.122" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a929b2c61f11ba3e9bc35b50c1f25cb38e0e892c0c231ae2b8cf78d5dad4437" +dependencies = [ + "unicode-ident", +] + +[[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.13.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.13.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.13.0", + "wayland-backend", + "wayland-client", + "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.13.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.13.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", + "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.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d621441cfc37b84979402712047321980c178f299193a3589d05b99e8763436" +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 = "wgpu" +version = "22.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1d1c4ba43f80542cf63a0a6ed3134629ae73e8ab51e4b765a67f3aa062eb433" +dependencies = [ + "arrayvec", + "cfg_aliases 0.1.1", + "document-features", + "js-sys", + "log", + "naga", + "parking_lot", + "profiling", + "raw-window-handle", + "smallvec", + "static_assertions", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "wgpu-core", + "wgpu-hal", + "wgpu-types", +] + +[[package]] +name = "wgpu-core" +version = "22.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0348c840d1051b8e86c3bcd31206080c5e71e5933dabd79be1ce732b0b2f089a" +dependencies = [ + "arrayvec", + "bit-vec", + "bitflags 2.13.0", + "cfg_aliases 0.1.1", + "document-features", + "indexmap", + "log", + "naga", + "once_cell", + "parking_lot", + "profiling", + "raw-window-handle", + "rustc-hash 1.1.0", + "smallvec", + "thiserror 1.0.69", + "wgpu-hal", + "wgpu-types", +] + +[[package]] +name = "wgpu-hal" +version = "22.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6bbf4b4de8b2a83c0401d9e5ae0080a2792055f25859a02bf9be97952bbed4f" +dependencies = [ + "android_system_properties", + "arrayvec", + "ash", + "bit-set", + "bitflags 2.13.0", + "block", + "cfg_aliases 0.1.1", + "core-graphics-types", + "d3d12", + "glow", + "glutin_wgl_sys", + "gpu-alloc", + "gpu-allocator", + "gpu-descriptor", + "hassle-rs", + "js-sys", + "khronos-egl", + "libc", + "libloading 0.8.9", + "log", + "metal", + "naga", + "ndk-sys 0.5.0+25.2.9519653", + "objc", + "once_cell", + "parking_lot", + "profiling", + "range-alloc", + "raw-window-handle", + "renderdoc-sys", + "rustc-hash 1.1.0", + "smallvec", + "thiserror 1.0.69", + "wasm-bindgen", + "web-sys", + "wgpu-types", + "winapi", +] + +[[package]] +name = "wgpu-types" +version = "22.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc9d91f0e2c4b51434dfa6db77846f2793149d8e73f800fa2e41f52b8eac3c5d" +dependencies = [ + "bitflags 2.13.0", + "js-sys", + "web-sys", +] + +[[package]] +name = "which" +version = "7.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d643ce3fd3e5b54854602a080f34fb10ab75e0b813ee32d00ca2b44fa74762" +dependencies = [ + "either", + "env_home", + "rustix 1.1.4", + "winsafe", +] + +[[package]] +name = "widestring" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72069c3113ab32ab29e5584db3c6ec55d416895e60715417b5b883a357c3e471" + [[package]] name = "winapi" version = "0.3.9" @@ -1223,19 +3445,47 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e48a53791691ab099e5e2ad123536d0fff50652600abaf43bbf952894110d0be" +dependencies = [ + "windows-core 0.52.0", + "windows-targets 0.52.6", +] + [[package]] name = "windows" version = "0.58.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd04d41d93c4992d421894c18c8b43496aa748dd4c081bac0dc93eb0489272b6" dependencies = [ - "windows-core", + "windows-core 0.58.0", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ "windows-targets 0.52.6", ] @@ -1274,6 +3524,12 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + [[package]] name = "windows-result" version = "0.2.0" @@ -1302,6 +3558,15 @@ 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" @@ -1311,6 +3576,15 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + [[package]] name = "windows-targets" version = "0.48.5" @@ -1432,6 +3706,83 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[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.13.0", + "block2 0.5.1", + "bytemuck", + "calloop", + "cfg_aliases 0.2.1", + "concurrent-queue", + "core-foundation", + "core-graphics", + "cursor-icon", + "dpi", + "js-sys", + "libc", + "memmap2", + "ndk", + "objc2 0.5.2", + "objc2-app-kit 0.2.2", + "objc2-foundation 0.2.2", + "objc2-ui-kit", + "orbclient", + "percent-encoding", + "pin-project", + "raw-window-handle", + "redox_syscall 0.4.1", + "rustix 0.38.44", + "sctk-adwaita", + "smithay-client-toolkit", + "smol_str", + "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 = "winiterm" +version = "0.1.0" +dependencies = [ + "arboard", + "base64", + "bitflags 2.13.0", + "bytemuck", + "env_logger", + "fontdb", + "image", + "log", + "mlua", + "muda", + "notify", + "pollster", + "swash", + "thiserror 2.0.18", + "tray-icon", + "unicode-width 0.2.2", + "wgpu", + "windows 0.58.0", + "winit", +] + [[package]] name = "winnow" version = "0.5.40" @@ -1441,6 +3792,27 @@ dependencies = [ "memchr", ] +[[package]] +name = "winnow" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1" +dependencies = [ + "memchr", +] + +[[package]] +name = "winsafe" +version = "0.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d135d17ab770252ad95e9a872d365cf3090e3be864a34ab46f48555993efc904" + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + [[package]] name = "x11" version = "2.21.0" @@ -1450,3 +3822,113 @@ dependencies = [ "libc", "pkg-config", ] + +[[package]] +name = "x11-dl" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38735924fedd5314a6e548792904ed8c6de6636285cb9fec04d5b1db85c1516f" +dependencies = [ + "libc", + "once_cell", + "pkg-config", +] + +[[package]] +name = "x11rb" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9993aa5be5a26815fe2c3eacfc1fde061fc1a1f094bf1ad2a18bf9c495dd7414" +dependencies = [ + "as-raw-xcb-connection", + "gethostname", + "libc", + "libloading 0.8.9", + "once_cell", + "rustix 1.1.4", + "x11rb-protocol", +] + +[[package]] +name = "x11rb-protocol" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea6fc2961e4ef194dcbfe56bb845534d0dc8098940c7e5c012a258bfec6701bd" + +[[package]] +name = "xcursor" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bec9e4a500ca8864c5b47b8b482a73d62e4237670e5b5f1d6b9e3cae50f28f2b" + +[[package]] +name = "xkbcommon-dl" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d039de8032a9a8856a6be89cea3e5d12fdd82306ab7c94d74e6deab2460651c5" +dependencies = [ + "bitflags 2.13.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 = "yazi" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c94451ac9513335b5e23d7a8a2b61a7102398b8cca5160829d313e84c9d98be1" + +[[package]] +name = "zeno" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd15f8e0dbb966fd9245e7498c7e9e5055d9e5c8b676b95bd67091cd11a1e697" + +[[package]] +name = "zerocopy" +version = "0.8.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b065d4f0e55f82fae73202e189638116a87c55ab6b8e6c2721e13dd9d854ad1" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b631b19d36a892ab55420c92dbc83ccd79274f25be714855d3074aa71cab639" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "zune-core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb8a0807f7c01457d0379ba880ba6322660448ddebc890ce29bb64da71fb40f9" + +[[package]] +name = "zune-jpeg" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27bc9d5b815bc103f142aa054f561d9187d191692ec7c2d1e2b4737f8dbd7296" +dependencies = [ + "zune-core", +] diff --git a/Cargo.toml b/Cargo.toml index eae7bc4..c58a424 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,27 +1,6 @@ -[package] -name = "iwaku" -version = "0.1.0" -edition = "2024" - -[[bin]] -name = "iwaku" -path = "src/main.rs" - -[dependencies] -windows = { version = "0.58", features = [ - "Win32_Foundation", - "Win32_Graphics_Gdi", - "Win32_System_Com", - "Win32_System_LibraryLoader", - "Win32_System_Ole", - "Win32_UI_Accessibility", - "Win32_UI_HiDpi", - "Win32_UI_Input_KeyboardAndMouse", - "Win32_UI_Magnification", - "Win32_UI_Shell", - "Win32_UI_WindowsAndMessaging", -] } -tray-icon = "0.17" +[workspace] +members = ["winiterm"] +resolver = "2" [profile.release] opt-level = 3 @@ -32,5 +11,7 @@ strip = true [profile.dev] opt-level = 0 +# Compile dependencies in release mode even in dev builds (much faster rendering) [profile.dev.package."*"] opt-level = 3 + diff --git a/_ul b/_ul deleted file mode 100644 index 42de59f..0000000 --- a/_ul +++ /dev/null @@ -1 +0,0 @@ -/usr/bin/bash: line 1: del: command not found diff --git a/src/caret.rs b/src/caret.rs deleted file mode 100644 index 827c1b7..0000000 --- a/src/caret.rs +++ /dev/null @@ -1,121 +0,0 @@ -use std::cell::RefCell; - -use windows::Win32::Foundation::{HWND, POINT}; -use windows::Win32::Graphics::Gdi::ClientToScreen; -use windows::Win32::System::Com::SAFEARRAY; -use windows::Win32::System::Com::{CoCreateInstance, CLSCTX_INPROC_SERVER}; -use windows::Win32::System::Ole::{SafeArrayGetElement, SafeArrayGetLBound, SafeArrayGetUBound}; -use windows::Win32::UI::Accessibility::{ - CUIAutomation, IUIAutomation, IUIAutomationElement, IUIAutomationTextPattern, UIA_TextPatternId, -}; -use windows::Win32::UI::WindowsAndMessaging::{ - GetForegroundWindow, GetGUIThreadInfo, GetWindowThreadProcessId, GUITHREADINFO, -}; - -thread_local! { - static AUTOMATION: RefCell> = RefCell::new(None); -} - -fn get_automation() -> Option { - AUTOMATION.with(|cell| { - let mut borrow = cell.borrow_mut(); - if borrow.is_none() { - unsafe { - *borrow = CoCreateInstance(&CUIAutomation, None, CLSCTX_INPROC_SERVER).ok(); - } - } - borrow.clone() - }) -} - -pub fn get_caret_pos() -> Option { - // 1. Win32 GUI-thread caret — fast, no COM. Works for classic edit controls, - // Notepad, Office dialogs, conhost-based terminals, etc. - if let Some(pt) = get_caret_via_gui_thread() { - return Some(pt); - } - // 2. UI Automation — works for Word, modern browsers, WPF, UWP apps. - get_caret_via_uia() -} - -fn get_caret_via_gui_thread() -> Option { - unsafe { - let hwnd: HWND = GetForegroundWindow(); - if hwnd.0.is_null() { - return None; - } - let thread_id = GetWindowThreadProcessId(hwnd, None); - let mut gti = GUITHREADINFO { - cbSize: std::mem::size_of::() as u32, - ..Default::default() - }; - GetGUIThreadInfo(thread_id, &mut gti).ok()?; - if gti.hwndCaret.0.is_null() { - return None; - } - let mut pt = POINT { - x: gti.rcCaret.left, - y: gti.rcCaret.bottom, - }; - let _ = ClientToScreen(gti.hwndCaret, &mut pt); - Some(pt) - } -} - -fn get_caret_via_uia() -> Option { - unsafe { - let automation = get_automation()?; - let element = automation.GetFocusedElement().ok()?; - - // Try the focused element and walk up to 6 ancestors. - // Some apps expose TextPattern on a parent rather than the focused leaf. - let mut current: Option = Some(element); - for _ in 0..7 { - let el = current.as_ref()?; - if let Some(sa) = try_text_pattern(el) { - return point_from_safearray(sa); - } - current = automation - .ControlViewWalker() - .ok() - .and_then(|w| w.GetParentElement(el).ok()); - } - None - } -} - -unsafe fn try_text_pattern(el: &IUIAutomationElement) -> Option<*mut SAFEARRAY> { - unsafe { - let pattern = el - .GetCurrentPatternAs::(UIA_TextPatternId) - .ok()?; - let ranges = pattern.GetSelection().ok()?; - if ranges.Length().ok()? == 0 { - return None; - } - ranges.GetElement(0).ok()?.GetBoundingRectangles().ok() - } -} - -unsafe fn point_from_safearray(sa: *mut SAFEARRAY) -> Option { - unsafe { - if sa.is_null() { - return None; - } - let lb = SafeArrayGetLBound(sa, 1).ok()?; - let ub = SafeArrayGetUBound(sa, 1).ok()?; - if ub - lb < 3 { - return None; - } - let mut left: f64 = 0.0; - let mut top: f64 = 0.0; - let mut idx0 = lb; - let mut idx1 = lb + 1; - SafeArrayGetElement(sa, &mut idx0, &mut left as *mut f64 as *mut _).ok()?; - SafeArrayGetElement(sa, &mut idx1, &mut top as *mut f64 as *mut _).ok()?; - Some(POINT { - x: left as i32, - y: top as i32, - }) - } -} diff --git a/src/hook.rs b/src/hook.rs deleted file mode 100644 index 193574d..0000000 --- a/src/hook.rs +++ /dev/null @@ -1,100 +0,0 @@ -use windows::Win32::Foundation::{LPARAM, LRESULT, WPARAM}; -use windows::Win32::UI::Input::KeyboardAndMouse::{ - GetAsyncKeyState, SendInput, INPUT, INPUT_KEYBOARD, KEYBDINPUT, KEYEVENTF_KEYUP, VIRTUAL_KEY, - VK_F24, VK_LWIN, VK_RWIN, -}; -use windows::Win32::UI::WindowsAndMessaging::{ - CallNextHookEx, SetWindowsHookExW, UnhookWindowsHookEx, HHOOK, MSLLHOOKSTRUCT, WH_MOUSE_LL, - WM_MOUSEWHEEL, -}; - -use std::sync::atomic::{AtomicIsize, Ordering}; - -pub const WM_APP_ZOOM_IN: u32 = 0x8001; -pub const WM_APP_ZOOM_OUT: u32 = 0x8002; - -static MOUSE_HOOK: AtomicIsize = AtomicIsize::new(0); -static MAIN_HWND: AtomicIsize = AtomicIsize::new(0); - -pub fn install(main_hwnd: windows::Win32::Foundation::HWND) { - MAIN_HWND.store(main_hwnd.0 as isize, Ordering::SeqCst); - unsafe { - let mh = SetWindowsHookExW(WH_MOUSE_LL, Some(mouse_hook_proc), None, 0) - .expect("Failed to install WH_MOUSE_LL hook"); - MOUSE_HOOK.store(mh.0 as isize, Ordering::SeqCst); - } -} - -pub fn uninstall() { - let raw = MOUSE_HOOK.swap(0, Ordering::SeqCst); - if raw != 0 { - unsafe { - let _ = UnhookWindowsHookEx(HHOOK(raw as *mut _)); - } - } -} - -unsafe extern "system" fn mouse_hook_proc(code: i32, wparam: WPARAM, lparam: LPARAM) -> LRESULT { - if code >= 0 && wparam.0 as u32 == WM_MOUSEWHEEL { - unsafe { - let lwin = GetAsyncKeyState(VK_LWIN.0 as i32); - let rwin = GetAsyncKeyState(VK_RWIN.0 as i32); - if (lwin < 0) || (rwin < 0) { - let info = &*(lparam.0 as *const MSLLHOOKSTRUCT); - let delta = (info.mouseData >> 16) as i16; - - let hwnd_raw = MAIN_HWND.load(Ordering::SeqCst); - if hwnd_raw != 0 { - let hwnd = windows::Win32::Foundation::HWND(hwnd_raw as *mut _); - let msg = if delta > 0 { - WM_APP_ZOOM_IN - } else { - WM_APP_ZOOM_OUT - }; - let _ = windows::Win32::UI::WindowsAndMessaging::PostMessageW( - hwnd, - msg, - WPARAM(0), - LPARAM(0), - ); - } - - // Inject a dummy F24 down+up between the Win key-down and the - // upcoming Win key-up. Windows only opens the Start menu when - // Win is the *sole* key in a press/release cycle; seeing any - // other key in between suppresses it — no stuck key, no Start - // menu, no keyboard hook required. - inject_f24(); - - return LRESULT(1); // consume the scroll event - } - } - } - unsafe { CallNextHookEx(None, code, wparam, lparam) } -} - -fn inject_f24() { - let inputs = [make_key(VK_F24, false), make_key(VK_F24, true)]; - unsafe { - SendInput(&inputs, std::mem::size_of::() as i32); - } -} - -fn make_key(vk: VIRTUAL_KEY, key_up: bool) -> INPUT { - INPUT { - r#type: INPUT_KEYBOARD, - Anonymous: windows::Win32::UI::Input::KeyboardAndMouse::INPUT_0 { - ki: KEYBDINPUT { - wVk: vk, - wScan: 0, - dwFlags: if key_up { - KEYEVENTF_KEYUP - } else { - Default::default() - }, - time: 0, - dwExtraInfo: 0, - }, - }, - } -} diff --git a/src/magnifier.rs b/src/magnifier.rs deleted file mode 100644 index bc77e56..0000000 --- a/src/magnifier.rs +++ /dev/null @@ -1,76 +0,0 @@ -use windows::Win32::Foundation::HWND; -use windows::Win32::Graphics::Gdi::{ - GetMonitorInfoW, MonitorFromWindow, MONITORINFO, MONITOR_DEFAULTTOPRIMARY, -}; -use windows::Win32::UI::Magnification::{ - MagInitialize, MagSetFullscreenTransform, MagUninitialize, -}; - -use crate::state::AppState; - -pub struct Magnifier { - screen_w: i32, - screen_h: i32, -} - -impl Magnifier { - pub fn new(screen_w: i32, screen_h: i32) -> Self { - Self { screen_w, screen_h } - } - - /// Apply or clear the fullscreen magnification transform. - pub fn update(&self, state: &mut AppState, just_activated: bool) { - unsafe { - if !state.active { - let _ = MagSetFullscreenTransform(1.0, 0, 0); - return; - } - - let z = state.zoom; - let src_w = self.screen_w as f32 / z; - let src_h = self.screen_h as f32 / z; - - state.update_viewport( - src_w, - src_h, - self.screen_w as f32, - self.screen_h as f32, - just_activated, - ); - - let x_off = state.viewport_x as i32; - let y_off = state.viewport_y as i32; - - let _ = MagSetFullscreenTransform(z, x_off, y_off); - } - } -} - -pub fn create() -> Magnifier { - unsafe { - MagInitialize().expect("MagInitialize failed"); - let (sw, sh) = monitor_dimensions(); - Magnifier::new(sw, sh) - } -} - -pub fn shutdown() { - unsafe { - let _ = MagSetFullscreenTransform(1.0, 0, 0); - let _ = MagUninitialize(); - } -} - -fn monitor_dimensions() -> (i32, i32) { - unsafe { - let hmon = MonitorFromWindow(HWND(std::ptr::null_mut()), MONITOR_DEFAULTTOPRIMARY); - let mut info = MONITORINFO { - cbSize: std::mem::size_of::() as u32, - ..Default::default() - }; - let _ = GetMonitorInfoW(hmon, &mut info); - let sw = info.rcMonitor.right - info.rcMonitor.left; - let sh = info.rcMonitor.bottom - info.rcMonitor.top; - (sw, sh) - } -} diff --git a/src/main.rs b/src/main.rs deleted file mode 100644 index a8b964b..0000000 --- a/src/main.rs +++ /dev/null @@ -1,177 +0,0 @@ -#![windows_subsystem = "windows"] - -mod caret; -mod hook; -mod magnifier; -mod state; -mod tray; - -use state::AppState; -use windows::core::w; -use windows::Win32::Foundation::{HINSTANCE, HWND, LPARAM, LRESULT, WPARAM}; -use windows::Win32::System::Com::{CoInitializeEx, CoUninitialize, COINIT_APARTMENTTHREADED}; -use windows::Win32::UI::HiDpi::{ - SetProcessDpiAwarenessContext, DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2, -}; -use windows::Win32::UI::WindowsAndMessaging::GetCursorPos; -use windows::Win32::UI::WindowsAndMessaging::WINDOW_EX_STYLE; -use windows::Win32::UI::WindowsAndMessaging::{ - CreateWindowExW, DefWindowProcW, DispatchMessageW, GetMessageW, PostQuitMessage, - RegisterClassExW, SetTimer, TranslateMessage, CS_HREDRAW, CS_VREDRAW, MSG, WM_DESTROY, - WM_TIMER, WNDCLASSEXW, WS_OVERLAPPEDWINDOW, -}; - -static mut APP_STATE: Option = None; -static mut MAGNIFIER: Option = None; - -const TIMER_ID: usize = 1; -const TIMER_MS: u32 = 16; // ~60 fps - -fn main() { - unsafe { - // DPI awareness — must be set before any window is created. - let _ = SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2); - - // COM initialisation needed for UI Automation (caret tracking). - let _ = CoInitializeEx(None, COINIT_APARTMENTTHREADED); - APP_STATE = Some(AppState::new()); - - let instance = get_instance(); - register_msg_class(instance); - - // Invisible message-only window to receive WM_APP zoom messages and WM_TIMER. - let msg_hwnd = CreateWindowExW( - WINDOW_EX_STYLE(0), - w!("IwakuMsg"), - w!(""), - WS_OVERLAPPEDWINDOW, - 0, - 0, - 0, - 0, - None, - None, - instance, - None, - ) - .expect("Failed to create message window"); - - // Global Win+Scroll hook. - hook::install(msg_hwnd); - - // Fullscreen magnifier — no overlay window needed with MagSetFullscreenTransform. - MAGNIFIER = Some(magnifier::create()); - - // System tray icon. - let tray = tray::Tray::new(); - - // 16 ms refresh timer. - SetTimer(msg_hwnd, TIMER_ID, TIMER_MS, None); - - // Message loop. - let mut msg = MSG::default(); - loop { - if tray.process_events() { - break; - } - - let ret = GetMessageW(&mut msg, None, 0, 0); - if ret.0 == 0 || ret.0 == -1 { - break; - } - let _ = TranslateMessage(&msg); - DispatchMessageW(&msg); - } - - hook::uninstall(); - magnifier::shutdown(); - CoUninitialize(); - } -} - -/// Window procedure for the invisible message window. -unsafe extern "system" fn wnd_proc( - hwnd: HWND, - msg: u32, - wparam: WPARAM, - lparam: LPARAM, -) -> LRESULT { - unsafe { - match msg { - WM_TIMER => { - if wparam.0 == TIMER_ID { - tick(); - } - LRESULT(0) - } - m if m == hook::WM_APP_ZOOM_IN => { - if let Some(state) = (*std::ptr::addr_of_mut!(APP_STATE)).as_mut() { - state.zoom_in(); - } - LRESULT(0) - } - m if m == hook::WM_APP_ZOOM_OUT => { - if let Some(state) = (*std::ptr::addr_of_mut!(APP_STATE)).as_mut() { - state.zoom_out(); - } - LRESULT(0) - } - WM_DESTROY => { - PostQuitMessage(0); - LRESULT(0) - } - _ => DefWindowProcW(hwnd, msg, wparam, lparam), - } - } -} - -/// Called every ~16 ms: update inputs, recalculate priority, push to magnifier. -unsafe fn tick() { - unsafe { - let state = match (*std::ptr::addr_of_mut!(APP_STATE)).as_mut() { - Some(s) => s, - None => return, - }; - - // Remember whether the magnifier was active before this tick so we - // can detect the exact frame it turns on and snap the smooth position. - let was_active = state.active; - - // 1. Update cursor and caret. - let mut pt = windows::Win32::Foundation::POINT::default(); - if GetCursorPos(&mut pt).is_ok() { - state.cursor = pt; - } - state.caret = caret::get_caret_pos(); - - // 2. Advance state. - let just_activated = state.active && !was_active; - state.update(just_activated); - - // 3. Push the new transform. - if let Some(mag) = (*std::ptr::addr_of_mut!(MAGNIFIER)).as_mut() { - mag.update(state, just_activated); - } - } -} - -unsafe fn get_instance() -> HINSTANCE { - unsafe { - use windows::Win32::System::LibraryLoader::GetModuleHandleW; - GetModuleHandleW(None).unwrap().into() - } -} - -unsafe fn register_msg_class(instance: HINSTANCE) { - unsafe { - let wc = WNDCLASSEXW { - cbSize: std::mem::size_of::() as u32, - style: CS_HREDRAW | CS_VREDRAW, - lpfnWndProc: Some(wnd_proc), - hInstance: instance, - lpszClassName: w!("IwakuMsg"), - ..Default::default() - }; - RegisterClassExW(&wc); - } -} diff --git a/src/state.rs b/src/state.rs deleted file mode 100644 index 9f6bfeb..0000000 --- a/src/state.rs +++ /dev/null @@ -1,192 +0,0 @@ -use windows::Win32::Foundation::POINT; - -pub const ZOOM_MIN: f32 = 1.5; -pub const ZOOM_MAX: f32 = 10.0; -pub const ZOOM_STEP: f32 = 0.5; - -const EDGE_MARGIN: f32 = 0.10; -const PAN_ALPHA: f32 = 0.20; -const ZOOM_ALPHA: f32 = 0.18; -const ZOOM_EPSILON: f32 = 0.01; -const CURSOR_ALPHA: f32 = 0.35; -const MOUSE_MOVE_THRESHOLD_SQ: i32 = 6 * 6; -const MOUSE_TAKEOVER_TICKS: u32 = 4; -/// Ticks of mouse stillness before caret re-takes focus (if caret present). ~240 ms. -const CARET_REFOCUS_TICKS: u32 = 15; - -pub struct AppState { - pub zoom: f32, - target_zoom: f32, - prev_zoom: f32, - pub active: bool, - - pub cursor: POINT, - prev_cursor: POINT, - smooth_cursor_x: f32, - smooth_cursor_y: f32, - - pub caret: Option, - prev_caret: Option, - pub caret_active: bool, - mouse_move_ticks: u32, - mouse_still_ticks: u32, - - pub viewport_x: f32, - pub viewport_y: f32, -} - -impl AppState { - pub fn new() -> Self { - Self { - zoom: 1.0, - target_zoom: 1.0, - prev_zoom: 1.0, - active: false, - cursor: POINT { x: 0, y: 0 }, - prev_cursor: POINT { x: 0, y: 0 }, - smooth_cursor_x: 0.0, - smooth_cursor_y: 0.0, - caret: None, - prev_caret: None, - caret_active: false, - mouse_move_ticks: 0, - mouse_still_ticks: 0, - viewport_x: 0.0, - viewport_y: 0.0, - } - } - - pub fn update(&mut self, _just_activated: bool) { - // Animate zoom. - if self.active { - let diff = self.target_zoom - self.zoom; - if diff.abs() < ZOOM_EPSILON { - self.zoom = self.target_zoom; - if self.zoom <= 1.0 { - self.zoom = 1.0; - self.active = false; - } - } else { - self.zoom += diff * ZOOM_ALPHA; - } - } - - // Smooth cursor. - self.smooth_cursor_x += (self.cursor.x as f32 - self.smooth_cursor_x) * CURSOR_ALPHA; - self.smooth_cursor_y += (self.cursor.y as f32 - self.smooth_cursor_y) * CURSOR_ALPHA; - - // Caret / mouse priority. - let dx = self.cursor.x - self.prev_cursor.x; - let dy = self.cursor.y - self.prev_cursor.y; - let intentional = dx * dx + dy * dy >= MOUSE_MOVE_THRESHOLD_SQ; - - let caret_moved = match (self.caret, self.prev_caret) { - (Some(c), Some(p)) => c.x != p.x || c.y != p.y, - (Some(_), None) => true, - _ => false, - }; - - if intentional { - self.mouse_move_ticks += 1; - self.mouse_still_ticks = 0; - } else { - self.mouse_move_ticks = 0; - self.mouse_still_ticks += 1; - } - - if self.mouse_move_ticks >= MOUSE_TAKEOVER_TICKS { - self.caret_active = false; - self.mouse_move_ticks = 0; - } - // Re-engage caret once mouse has been still long enough. - if self.mouse_still_ticks >= CARET_REFOCUS_TICKS && self.caret.is_some() { - self.caret_active = true; - } - if caret_moved { - self.caret_active = true; - self.mouse_move_ticks = 0; - } - - self.prev_cursor = self.cursor; - self.prev_caret = self.caret; - } - - pub fn update_viewport( - &mut self, - src_w: f32, - src_h: f32, - screen_w: f32, - screen_h: f32, - just_activated: bool, - ) { - let (fx, fy) = self.focus_point(); - let zoom_changed = (self.zoom - self.prev_zoom).abs() > f32::EPSILON; - self.prev_zoom = self.zoom; - - if just_activated { - self.viewport_x = (fx - src_w / 2.0).clamp(0.0, (screen_w - src_w).max(0.0)); - self.viewport_y = (fy - src_h / 2.0).clamp(0.0, (screen_h - src_h).max(0.0)); - return; - } - - let (target_x, target_y) = if self.caret_active { - // Centre on caret while typing. - let tx = (fx - src_w / 2.0).clamp(0.0, (screen_w - src_w).max(0.0)); - let ty = (fy - src_h / 2.0).clamp(0.0, (screen_h - src_h).max(0.0)); - (tx, ty) - } else if zoom_changed { - let tx = (fx - src_w / 2.0).clamp(0.0, (screen_w - src_w).max(0.0)); - let ty = (fy - src_h / 2.0).clamp(0.0, (screen_h - src_h).max(0.0)); - (tx, ty) - } else { - let mx = EDGE_MARGIN * src_w; - let my = EDGE_MARGIN * src_h; - let tx = self - .viewport_x - .clamp(fx - src_w + mx, fx - mx) - .clamp(0.0, (screen_w - src_w).max(0.0)); - let ty = self - .viewport_y - .clamp(fy - src_h + my, fy - my) - .clamp(0.0, (screen_h - src_h).max(0.0)); - (tx, ty) - }; - - self.viewport_x += (target_x - self.viewport_x) * PAN_ALPHA; - self.viewport_y += (target_y - self.viewport_y) * PAN_ALPHA; - - // Hard-clamp every tick so the viewport never overshoots screen bounds - // even while the lerp is still catching up (e.g. fast zoom-out). - self.viewport_x = self.viewport_x.clamp(0.0, (screen_w - src_w).max(0.0)); - self.viewport_y = self.viewport_y.clamp(0.0, (screen_h - src_h).max(0.0)); - } - - pub fn zoom_in(&mut self) { - if !self.active { - self.target_zoom = ZOOM_MIN; - self.zoom = 1.0; - self.active = true; - } else { - self.target_zoom = (self.target_zoom + ZOOM_STEP).min(ZOOM_MAX); - } - } - - pub fn zoom_out(&mut self) { - if !self.active { - return; - } - self.target_zoom -= ZOOM_STEP; - if self.target_zoom < ZOOM_MIN { - self.target_zoom = 1.0; - } - } - - fn focus_point(&self) -> (f32, f32) { - if self.caret_active { - if let Some(c) = self.caret { - return (c.x as f32, c.y as f32); - } - } - (self.smooth_cursor_x, self.smooth_cursor_y) - } -} diff --git a/src/tray.rs b/src/tray.rs deleted file mode 100644 index cde1808..0000000 --- a/src/tray.rs +++ /dev/null @@ -1,70 +0,0 @@ -use tray_icon::{ - menu::{Menu, MenuEvent, MenuId, MenuItem}, - TrayIcon, TrayIconBuilder, -}; - -pub struct Tray { - _icon: TrayIcon, - exit_id: MenuId, -} - -impl Tray { - pub fn new() -> Self { - let toggle_item = MenuItem::new("Toggle", true, None); - let exit_item = MenuItem::new("Exit", true, None); - - let exit_id = exit_item.id().clone(); - - let menu = Menu::new(); - menu.append(&toggle_item) - .expect("Failed to append toggle item"); - menu.append(&exit_item).expect("Failed to append exit item"); - - let icon = make_icon(); - - let tray = TrayIconBuilder::new() - .with_menu(Box::new(menu)) - .with_tooltip("iwaku magnifier") - .with_icon(icon) - .build() - .expect("Failed to build tray icon"); - - Self { - _icon: tray, - exit_id, - } - } - - /// Returns `true` if the app should exit. - pub fn process_events(&self) -> bool { - if let Ok(event) = MenuEvent::receiver().try_recv() { - if event.id == self.exit_id { - return true; - } - } - false - } -} - -fn make_icon() -> tray_icon::Icon { - const SIZE: usize = 16; - let mut rgba = vec![0u8; SIZE * SIZE * 4]; - - for y in 0..SIZE { - for x in 0..SIZE { - let idx = (y * SIZE + x) * 4; - let cx = x as f32 - 7.5; - let cy = y as f32 - 7.5; - let dist = (cx * cx + cy * cy).sqrt(); - if (dist - 5.5).abs() < 1.5 { - rgba[idx] = 255; - rgba[idx + 1] = 255; - rgba[idx + 2] = 255; - rgba[idx + 3] = 255; - } - } - } - - tray_icon::Icon::from_rgba(rgba, SIZE as u32, SIZE as u32) - .expect("Failed to create tray icon from RGBA") -} diff --git a/winiterm.log b/winiterm.log new file mode 100644 index 0000000..e69de29 diff --git a/winiterm/Cargo.toml b/winiterm/Cargo.toml new file mode 100644 index 0000000..e780ed4 --- /dev/null +++ b/winiterm/Cargo.toml @@ -0,0 +1,92 @@ +[package] +name = "winiterm" +version = "0.1.0" +edition = "2024" + +[lib] +name = "winiterm" +path = "src/lib.rs" + +[[bin]] +name = "winiterm" +path = "src/main.rs" + +[[bin]] +name = "bench_process" +path = "src/bin/bench_process.rs" + +[[bin]] +name = "bench_render" +path = "src/bin/bench_render.rs" + +[dependencies] +# Windows API — PTY (ConPTY), process management, DWM transparency +windows = { version = "0.58", features = [ + "Win32_Foundation", + "Win32_Security", + "Win32_System_Console", + "Win32_System_Threading", + "Win32_System_Pipes", + "Win32_Graphics_Dwm", + "Win32_UI_WindowsAndMessaging", +]} + +# Error handling +thiserror = "2" + +# Logging +log = "0.4" +env_logger = "0.11" + +# Compact bitfield attributes for terminal cells +bitflags = "2" + +# Window creation and OS event loop +winit = "0.30" + +# GPU rendering (DirectX 12 on Windows via wgpu) +wgpu = { version = "22", features = ["wgsl"] } + +# Minimal async executor for wgpu initialisation +pollster = "0.3" + +# Font discovery (scans C:\Windows\Fonts, pure Rust) +fontdb = "0.16" + +# Font rendering: glyph rasterisation + COLR emoji (pure Rust) +swash = "0.1" + +# Cast structs to byte slices for GPU buffer uploads +bytemuck = { version = "1", features = ["derive"] } + +# Lua 5.4 scripting — config + plugin/event system (Phase 5 & 6) +mlua = { version = "0.10", features = ["lua54", "vendored"] } + +# Filesystem watcher for Lua config hot-reload (Phase 5) +notify = "6" + +# System tray icon for persistence mode (Phase 11) +tray-icon = "0.24" + +# Context menu for the system tray (right-click Show / Quit) +muda = "0.19" + +# Image decoding for Kitty Graphics Protocol (Phase 9) +image = { version = "0.25", default-features = false, features = ["png", "jpeg"] } + +# base64 decoding for Kitty image data (Phase 9) +base64 = "0.22" + +# Clipboard read/write for Ctrl+Shift+V paste +arboard = "3" + +# Unicode character display-width (0=combining, 1=normal, 2=wide/CJK) +unicode-width = "0.2" + +[features] +default = [] +# Enable per-frame GPU timing instrumentation overlay (Phase 12) +render_timing = [] +# Strip all logging in release builds +production = ["log/release_max_level_off"] + diff --git a/winiterm/default_config.lua b/winiterm/default_config.lua new file mode 100644 index 0000000..ca44c7f --- /dev/null +++ b/winiterm/default_config.lua @@ -0,0 +1,33 @@ +-- winiterm configuration +-- Location: %APPDATA%\winiterm\config.lua +-- +-- This file is loaded at startup and hot-reloaded whenever it changes. +-- Return a plain table; unknown keys are ignored. + +return { + -- Base font size in logical pixels. + -- The actual pixel size is scaled by the display's DPI factor. + font_size = 16.0, + + -- Active colour scheme (case-insensitive). + -- Built-in choices: monokai, nord, dracula, one_dark, solarized_dark, gruvbox + color_scheme = "monokai", + + -- Shell executable to spawn in each pane. + -- nil = auto-detect: tries pwsh.exe → powershell.exe → cmd.exe + shell = nil, + + -- Number of scrollback lines to retain per pane. + scrollback_lines = 10000, + + -- Attempt to enable the Windows 11 Mica system backdrop. + -- Requires Windows 11 22H2 or later; silently ignored on older versions. + enable_mica = false, + + -- Show a system-tray icon so winiterm persists when all windows are closed. + tray_icon = false, + + -- Window opacity in [0.0, 1.0]. + -- 1.0 = fully opaque (default). Values below 1.0 require a compositor. + opacity = 1.0, +} diff --git a/winiterm/src/app.rs b/winiterm/src/app.rs new file mode 100644 index 0000000..c4a03d1 --- /dev/null +++ b/winiterm/src/app.rs @@ -0,0 +1,515 @@ +//! Application entry point (Phase 4 rewrite). +//! +//! [`App`] implements winit's [`ApplicationHandler`] trait. +//! +//! Lifecycle: +//! * `new()` — loads config, sets up filesystem watcher for hot-reload, +//! initialises Lua API. +//! * `resumed()` — creates the window, renderer, workspace (shell PTY). +//! * `window_event()` — handles keyboard input, resize, redraw. +//! * `about_to_wait()` — drains PTY output, polls config changes, requests +//! redraw on dirty state. + +use std::sync::{mpsc, Arc}; + +use winit::application::ApplicationHandler; +use winit::event::{ElementState, Modifiers, WindowEvent}; +use winit::event_loop::{ActiveEventLoop, ControlFlow, EventLoop}; +use winit::window::{Window, WindowId}; + +use crate::config::Config; +use crate::input::{key_to_action, InputAction}; +use crate::lua_api::LuaApi; +use crate::renderer::Renderer; +use crate::split::SplitDir; +use crate::terminal::TerminalModes; +use crate::tray::TrayHandle; +use crate::workspace::Workspace; + +// ─── App ───────────────────────────────────────────────────────────────────── + +pub struct App { + // Configuration — loaded at startup, can be hot-reloaded. + config: Config, + // winit / GPU / workspace. + window: Option>, + renderer: Option, + workspace: Option, + modifiers: Modifiers, + // Tray icon handle (keeps the icon alive). + _tray: Option, + // Filesystem watcher for config hot-reload. + _watcher: Option, + config_rx: Option>>, + // Lua event hook system. + lua: Option, +} + +impl App { + pub fn new() -> Self { + let config = Config::load(); + + // ── Config file watcher (notify) ────────────────────────────────────── + let (watcher, config_rx) = setup_watcher(&config); + + // ── Lua API ─────────────────────────────────────────────────────────── + let lua = match LuaApi::new() { + Ok(api) => { + // Execute the config file in the Lua VM so that + // `winiterm.on(...)` hooks defined there actually fire (C1). + if let Err(e) = api.exec_config(&Config::config_path()) { + log::warn!("Lua config exec error: {e}"); + } + Some(api) + } + Err(e) => { + log::warn!("failed to create Lua state: {e}"); + None + } + }; + + // ── Tray icon ───────────────────────────────────────────────────────── + let tray = if config.tray_icon { + crate::tray::create_tray_icon() + } else { + None + }; + + Self { + config, + window: None, + renderer: None, + workspace: None, + modifiers: Modifiers::default(), + _tray: tray, + _watcher: watcher, + config_rx, + lua, + } + } + + pub fn run() { + let event_loop = EventLoop::new().expect("failed to create event loop"); + event_loop.set_control_flow(ControlFlow::Poll); + let mut app = Self::new(); + event_loop.run_app(&mut app).expect("event loop error"); + } + + // ── Private helpers ─────────────────────────────────────────────────────── + + /// Apply a freshly loaded config to the live renderer and workspace. + fn apply_config_reload(&mut self, new_config: Config) { + // C1: clear existing Lua hooks then re-execute the config file so that + // any `winiterm.on(...)` registrations don't accumulate across reloads. + if let Some(lua) = &self.lua { + lua.clear_hooks(); + if let Err(e) = lua.exec_config(&Config::config_path()) { + log::warn!("Lua config exec error on reload: {e}"); + } + } + if let Some(renderer) = &mut self.renderer { + renderer.set_color_scheme(new_config.color_scheme()); + } + self.config = new_config; + // C2: notify Lua scripts that the config was reloaded. + if let Some(lua) = &self.lua { + lua.dispatch("config_reloaded", ""); + } + log::info!("config reloaded"); + } + + /// Poll the config watcher channel and reload if the file was modified. + fn poll_config_reload(&mut self) { + let changed = if let Some(rx) = &self.config_rx { + rx.try_recv() + .ok() + .and_then(|res| res.ok()) + .map(|ev| matches!(ev.kind, notify::EventKind::Modify(_))) + .unwrap_or(false) + } else { + false + }; + if changed { + let new_cfg = Config::load(); + self.apply_config_reload(new_cfg); + } + } +} + +impl Default for App { + fn default() -> Self { + Self::new() + } +} + +// ─── ApplicationHandler ─────────────────────────────────────────────────────── + +impl ApplicationHandler for App { + fn resumed(&mut self, event_loop: &ActiveEventLoop) { + let window_attrs = Window::default_attributes() + .with_title("winiterm") + .with_inner_size(winit::dpi::LogicalSize::new(1200u32, 800u32)); + + let window = Arc::new( + event_loop + .create_window(window_attrs) + .expect("failed to create window"), + ); + + // Optionally enable Windows 11 Mica backdrop. + #[cfg(target_os = "windows")] + if self.config.enable_mica { + enable_mica_for_window(&window); + } + + // Apply configured window opacity (layered window alpha). + #[cfg(target_os = "windows")] + if self.config.opacity < 1.0 { + apply_window_opacity(&window, self.config.opacity); + } + + let mut renderer = Renderer::new(window.clone(), self.config.font_size); + renderer.set_color_scheme(self.config.color_scheme()); + + let (cols, rows) = renderer.terminal_size(); + + let workspace = Workspace::new( + self.config.shell_ref(), + cols, + rows, + self.config.scrollback_lines, + ); + + self.window = Some(window); + self.renderer = Some(renderer); + self.workspace = Some(workspace); + + // C2: dispatch startup event so Lua config hooks can run once the + // window and workspace are live. + if let Some(lua) = &self.lua { + lua.dispatch("startup", ""); + } + } + + fn window_event(&mut self, event_loop: &ActiveEventLoop, _id: WindowId, event: WindowEvent) { + match event { + // ── Window management ───────────────────────────────────────────── + WindowEvent::CloseRequested => { + if self.config.tray_icon { + // D1: hide to tray instead of exiting. + if let Some(win) = &self.window { + win.set_visible(false); + } + } else { + event_loop.exit(); + } + } + + WindowEvent::Resized(size) => { + if let Some(renderer) = &mut self.renderer { + renderer.resize(size.width, size.height); + if let Some(workspace) = &mut self.workspace { + workspace.resize_all( + size.width as f32, + size.height as f32, + renderer.cell_width, + renderer.cell_height, + renderer.cell_height, // tab bar = one cell row + ); + } + } + } + + // ── Input ───────────────────────────────────────────────────────── + WindowEvent::ModifiersChanged(mods) => { + self.modifiers = mods; + } + + WindowEvent::KeyboardInput { event, .. } => { + if event.state == ElementState::Pressed { + let decckm = self + .workspace + .as_ref() + .and_then(|ws| ws.active_pane()) + .map(|p| p.terminal.modes.contains(TerminalModes::DECCKM)) + .unwrap_or(false); + if let Some(action) = key_to_action(&event, &self.modifiers, decckm) { + self.dispatch_action(action, event_loop); + } + } + } + + // ── Render ──────────────────────────────────────────────────────── + WindowEvent::RedrawRequested => { + if let (Some(renderer), Some(workspace)) = + (self.renderer.as_mut(), self.workspace.as_mut()) + { + renderer.render_workspace(workspace); + } + } + + _ => {} + } + } + + fn about_to_wait(&mut self, event_loop: &ActiveEventLoop) { + // Drain PTY output. + let dirty = self + .workspace + .as_mut() + .map(|ws| ws.process_all_output()) + .unwrap_or(false); + + // Exit if all shells have died. + if self + .workspace + .as_ref() + .map(|ws| ws.is_empty()) + .unwrap_or(false) + { + log::info!("all panes dead — exiting"); + event_loop.exit(); + return; + } + + // Poll config hot-reload. + self.poll_config_reload(); + + // Poll tray events. + if let Some(tray) = &self._tray { + match crate::tray::poll_events(tray) { + Some(crate::tray::TrayAction::Show) => { + if let Some(win) = &self.window { + win.set_visible(true); + let _ = win.request_inner_size(win.inner_size()); + } + } + Some(crate::tray::TrayAction::Quit) => { + event_loop.exit(); + } + None => {} + } + } + + if dirty { + if let Some(window) = &self.window { + window.request_redraw(); + } + } + } +} + +// ─── Action dispatch ────────────────────────────────────────────────────────── + +impl App { + fn dispatch_action(&mut self, action: InputAction, event_loop: &ActiveEventLoop) { + let shell = self.config.shell.clone(); + let scrollback = self.config.scrollback_lines; + + match action { + InputAction::Bytes(bytes) => { + if let Some(ws) = &mut self.workspace { + // Any keystroke snaps the viewport back to live output. + ws.scroll_to_bottom_active(); + ws.write_to_active(&bytes); + } + } + + InputAction::Paste => { + if let Some(ws) = &self.workspace { + match arboard::Clipboard::new().and_then(|mut cb| cb.get_text()) { + Ok(text) => { + let bracketed = ws + .active_pane() + .map(|p| p.terminal.modes.contains(TerminalModes::BRACKET_PASTE)) + .unwrap_or(false); + let bytes = if bracketed { + let mut v = b"\x1b[200~".to_vec(); + v.extend_from_slice(text.as_bytes()); + v.extend_from_slice(b"\x1b[201~"); + v + } else { + text.into_bytes() + }; + ws.write_to_active(&bytes); + } + Err(e) => log::warn!("clipboard read failed: {e}"), + } + } + } + + InputAction::ScrollUp => { + if let Some(ws) = &mut self.workspace { + ws.scroll_up_active(3); + } + } + + InputAction::ScrollDown => { + if let Some(ws) = &mut self.workspace { + ws.scroll_down_active(3); + } + } + + InputAction::SplitHorizontal => { + if let Some(ws) = &mut self.workspace { + ws.split_active(SplitDir::Horizontal, shell.as_deref(), scrollback); + self.rebalance_layout(); + } + if let Some(lua) = &self.lua { + lua.dispatch("pane_opened", ""); + } + } + + InputAction::SplitVertical => { + if let Some(ws) = &mut self.workspace { + ws.split_active(SplitDir::Vertical, shell.as_deref(), scrollback); + self.rebalance_layout(); + } + if let Some(lua) = &self.lua { + lua.dispatch("pane_opened", ""); + } + } + + InputAction::FocusNext => { + if let Some(ws) = &mut self.workspace { + ws.focus_next(); + } + } + + InputAction::FocusPrev => { + if let Some(ws) = &mut self.workspace { + ws.focus_prev(); + } + } + + InputAction::NewTab => { + if let Some((ws, renderer)) = self.workspace.as_mut().zip(self.renderer.as_ref()) { + let (cols, rows) = renderer.terminal_size(); + ws.new_tab(shell.as_deref(), cols, rows, scrollback); + } + if let Some(lua) = &self.lua { + lua.dispatch("pane_opened", ""); + } + } + + InputAction::SwitchTab(idx) => { + if let Some(ws) = &mut self.workspace { + ws.switch_tab(idx); + } + if let Some(lua) = &self.lua { + lua.dispatch("tab_switched", &idx.to_string()); + } + } + + InputAction::ClosePane => { + if let Some(ws) = &mut self.workspace { + ws.close_active_pane(); + if ws.is_empty() { + event_loop.exit(); + return; + } + self.rebalance_layout(); + } + if let Some(lua) = &self.lua { + lua.dispatch("pane_closed", ""); + } + } + } + + // Dispatch title change hook if active pane title changed. + if let (Some(ws), Some(lua)) = (&self.workspace, &self.lua) { + if let Some(pane) = ws.active_pane() { + lua.dispatch("title_change", &pane.title); + } + } + + if let Some(window) = &self.window { + window.request_redraw(); + } + } + + /// After a structural change (split/close), propagate new sizes to all panes. + fn rebalance_layout(&mut self) { + if let (Some(ws), Some(renderer)) = (&mut self.workspace, &self.renderer) { + ws.resize_all( + renderer.surface_size().0 as f32, + renderer.surface_size().1 as f32, + renderer.cell_width, + renderer.cell_height, + renderer.cell_height, + ); + } + } +} + +// ─── Helpers ───────────────────────────────────────────────────────────────── + +fn setup_watcher( + config: &Config, +) -> ( + Option, + Option>>, +) { + use notify::Watcher; + let config_path = Config::config_path(); + let (tx, rx) = mpsc::channel::>(); + let watcher = notify::RecommendedWatcher::new( + move |res| { + let _ = tx.send(res); + }, + notify::Config::default(), + ) + .ok() + .and_then(|mut w| { + w.watch(&config_path, notify::RecursiveMode::NonRecursive) + .ok()?; + Some(w) + }); + let rx = if watcher.is_some() { Some(rx) } else { None }; + let _ = config; // used for path derivation via Config::config_path() + (watcher, rx) +} + +#[cfg(target_os = "windows")] +fn enable_mica_for_window(window: &Window) { + use winit::raw_window_handle::{HasWindowHandle, RawWindowHandle}; + if let Ok(handle) = window.window_handle() { + if let RawWindowHandle::Win32(h) = handle.as_raw() { + let hwnd = windows::Win32::Foundation::HWND(h.hwnd.get() as *mut core::ffi::c_void); + if crate::dwm::enable_mica(hwnd) { + log::info!("Mica backdrop enabled"); + } else { + log::debug!("Mica not supported on this Windows version"); + } + } + } +} + +/// Apply a window-wide alpha value using the Win32 layered-window API. +/// +/// `opacity` is clamped to `[0.0, 1.0]` and mapped to a 0–255 byte. +/// The window gains `WS_EX_LAYERED` if it does not already have it. +#[cfg(target_os = "windows")] +fn apply_window_opacity(window: &Window, opacity: f32) { + use windows::Win32::Foundation::{COLORREF, HWND}; + use windows::Win32::UI::WindowsAndMessaging::{ + GetWindowLongPtrW, SetLayeredWindowAttributes, SetWindowLongPtrW, GWL_EXSTYLE, LWA_ALPHA, + WS_EX_LAYERED, + }; + use winit::raw_window_handle::{HasWindowHandle, RawWindowHandle}; + if let Ok(handle) = window.window_handle() { + if let RawWindowHandle::Win32(h) = handle.as_raw() { + let hwnd = HWND(h.hwnd.get() as *mut core::ffi::c_void); + unsafe { + let ex_style = GetWindowLongPtrW(hwnd, GWL_EXSTYLE); + SetWindowLongPtrW(hwnd, GWL_EXSTYLE, ex_style | WS_EX_LAYERED.0 as isize); + let alpha = (opacity.clamp(0.0, 1.0) * 255.0) as u8; + if let Err(e) = SetLayeredWindowAttributes(hwnd, COLORREF(0), alpha, LWA_ALPHA) { + log::warn!("SetLayeredWindowAttributes failed: {e}"); + } else { + log::info!("window opacity set to {opacity:.2} (alpha={alpha})"); + } + } + } + } +} diff --git a/winiterm/src/bin/bench_process.rs b/winiterm/src/bin/bench_process.rs new file mode 100644 index 0000000..f187ac1 --- /dev/null +++ b/winiterm/src/bin/bench_process.rs @@ -0,0 +1,219 @@ +//! VT parser throughput benchmark. +//! +//! Measures how quickly the terminal can process raw bytes from a PTY stream. +//! Methodology is modelled on Kitty's `kitten __benchmark__` (without `--render`): +//! 1. Generate representative synthetic data for each scenario. +//! 2. Do one warmup pass. +//! 3. Run `REPETITIONS` timed passes, report MB/s and elapsed time. +//! +//! Run with: +//! cargo run --bin bench_process --release + +use std::time::Instant; +use winiterm::terminal::Terminal; +use winiterm::vt_parser::Parser; + +const REPETITIONS: usize = 100; + +// ─── Data generators ───────────────────────────────────────────────────────── + +const ASCII_PRINTABLE: &[u8] = + b"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ `~!@#$%^&*()_+-=[]{}\\|;:'\",<.>/?"; +const CONTROL_CHARS: &[u8] = b"\n\t"; + +/// Simple linear-congruential PRNG (deterministic, no dependency). +#[inline] +fn lcg(rng: &mut u64) -> u64 { + *rng = rng + .wrapping_mul(6_364_136_223_846_793_005) + .wrapping_add(1_442_695_040_888_963_407); + *rng +} + +/// Pick a random byte from `alphabet`. +fn random_byte(rng: &mut u64, alphabet: &[u8]) -> u8 { + alphabet[(lcg(rng) >> 33) as usize % alphabet.len()] +} + +/// Generate `len` random bytes from the combined ASCII + control alphabet. +fn random_text(len: usize, rng: &mut u64) -> Vec { + let alphabet: Vec = ASCII_PRINTABLE + .iter() + .chain(CONTROL_CHARS.iter()) + .copied() + .collect(); + (0..len).map(|_| random_byte(rng, &alphabet)).collect() +} + +// ─── Benchmark runner ───────────────────────────────────────────────────────── + +fn run_benchmark(name: &str, data: &[u8], repetitions: usize, setup: F) +where + F: Fn() -> (Terminal, Parser), +{ + // Warmup + { + let (mut term, mut parser) = setup(); + parser.parse(data, &mut term); + } + + // Timed passes + let start = Instant::now(); + for _ in 0..repetitions { + let (mut term, mut parser) = setup(); + parser.parse(data, &mut term); + } + let elapsed = start.elapsed(); + + let total_bytes = data.len() * repetitions; + let mb = total_bytes as f64 / 1_048_576.0; + let rate = mb / elapsed.as_secs_f64(); + + println!( + " {:<30} : {:>6.2}s {:>8.1} MB/s ({} reps × {:.2} MB)", + name, + elapsed.as_secs_f64(), + rate, + repetitions, + data.len() as f64 / 1_048_576.0, + ); +} + +fn make_terminal() -> (Terminal, Parser) { + (Terminal::new(80, 25, 20_000), Parser::new()) +} + +// ─── main ───────────────────────────────────────────────────────────────────── + +fn main() { + println!("=== winiterm VT Parser Benchmark ==="); + println!("(methodology matches Kitty's kitten __benchmark__ without --render)\n"); + + // ── 1. Plain ASCII text ─────────────────────────────────────────────────── + println!("--- Plain ASCII text ---"); + { + let target = 1024 * 2048 + 13; + let mut rng: u64 = 0xDEAD_BEEF; + let data = random_text(target, &mut rng); + run_benchmark("ascii_only", &data, REPETITIONS, make_terminal); + } + + // ── 2. CSI escape codes interleaved with text ───────────────────────────── + println!("\n--- CSI codes with text ---"); + { + let target = 1024 * 1024 + 17; + let mut rng: u64 = 0x1234_5678; + let mut data: Vec = Vec::with_capacity(target + 128); + + while data.len() < target { + match (lcg(&mut rng) >> 33) % 10 { + 0 => { + // Plain text burst (1–72 chars) + let len = ((lcg(&mut rng) >> 33) % 72 + 1) as usize; + data.extend(random_text(len, &mut rng)); + } + 1 | 2 => { + // Cursor movement + reset + data.extend_from_slice(b"\x1b[m\x1b[?1h\x1b[H"); + } + 3 => { + // SGR: bold + italic + colours + data.extend_from_slice(b"\x1b[1;3;31;42m"); + } + 4 => { + // SGR: 256-colour foreground + background + data.extend_from_slice(b"\x1b[38;5;214;48;5;236m"); + } + 5 => { + // SGR: true-colour (RGB) foreground + data.extend_from_slice(b"\x1b[38;2;255;128;0m"); + } + 6 | 7 => { + // Cursor movement + erase + data.extend_from_slice(b"\x1b[m\x1b[5A\x1b[2K\x1b[1J"); + } + _ => { + // SGR reset + misc cursor + data.extend_from_slice(b"\x1b[0m\x1b[10;20H\x1b[?25h"); + } + } + } + data.extend_from_slice(b"\x1b[m"); + run_benchmark("csi_with_text", &data, REPETITIONS, make_terminal); + } + + // ── 3. Long OSC sequences (window titles) ──────────────────────────────── + println!("\n--- Long OSC sequences ---"); + { + let title_body: String = (0..512) + .map(|i| ASCII_PRINTABLE[i % ASCII_PRINTABLE.len()] as char) + .collect(); + let mut data: Vec = Vec::new(); + for _ in 0..2048 { + data.extend_from_slice(b"\x1b]2;"); + data.extend_from_slice(title_body.as_bytes()); + data.push(0x07); // BEL terminator + } + run_benchmark("osc_long_titles", &data, REPETITIONS, make_terminal); + } + + // ── 4. Dense colour changes (typical syntax-highlighted code output) ────── + println!("\n--- Dense colour output (syntax highlighting) ---"); + { + let mut data: Vec = Vec::new(); + let words = ["fn", "main", "let", "mut", "if", "return", "use", "pub"]; + let mut rng: u64 = 0xABCD_EF01; + for _ in 0..8192 { + // Random foreground colour (30–45) + let fg = 30 + (lcg(&mut rng) >> 33) % 16; + let word = words[(lcg(&mut rng) >> 33) as usize % words.len()]; + let seq = format!("\x1b[{fg}m{word} \x1b[m"); + data.extend_from_slice(seq.as_bytes()); + } + run_benchmark("dense_color_output", &data, REPETITIONS, make_terminal); + } + + // ── 5. Mixed: scroll + insert/delete lines ──────────────────────────────── + println!("\n--- Scroll-heavy output ---"); + { + let mut data: Vec = Vec::new(); + let mut rng: u64 = 0xFEED_FACE; + for _ in 0..4096 { + data.extend(random_text(60, &mut rng)); + data.push(b'\n'); + // Occasionally insert/delete lines + if (lcg(&mut rng) >> 33) % 8 == 0 { + data.extend_from_slice(b"\x1b[1L"); // insert line + } + } + run_benchmark("scroll_heavy", &data, REPETITIONS, make_terminal); + } + + println!("\n=== Benchmark complete ==="); + println!("Note: measures CPU parse + terminal state updates only (no GPU rendering)."); + println!("Compare against Kitty: kitten __benchmark__ (without --render flag)"); +} + +// ─── Minimal `write!` support for building data vecs ───────────────────────── + +use std::fmt::Write as FmtWrite; + +struct VecWriter<'a>(&'a mut Vec); + +impl FmtWrite for VecWriter<'_> { + fn write_str(&mut self, s: &str) -> std::fmt::Result { + self.0.extend_from_slice(s.as_bytes()); + Ok(()) + } +} + +// Override the `write!` macro to work with our VecWriter. +trait WriteVec { + fn write_fmt(&mut self, args: std::fmt::Arguments<'_>); +} + +impl WriteVec for Vec { + fn write_fmt(&mut self, args: std::fmt::Arguments<'_>) { + let _ = std::fmt::write(&mut VecWriter(self), args); + } +} diff --git a/winiterm/src/bin/bench_render.rs b/winiterm/src/bin/bench_render.rs new file mode 100644 index 0000000..164e591 --- /dev/null +++ b/winiterm/src/bin/bench_render.rs @@ -0,0 +1,60 @@ +//! Benchmark: CPU-side terminal-cell-instance building throughput. +//! +//! Measures how fast the renderer can produce `CellInstance` data for a +//! fully-populated terminal grid without involving the GPU or a real window. +//! +//! Run with: +//! ``` +//! cargo run --bin bench_render --release +//! ``` + +use std::time::Instant; + +use winiterm::terminal::Terminal; +use winiterm::vt_parser::Parser; + +fn main() { + const COLS: usize = 220; + const ROWS: usize = 55; + const FRAMES: usize = 5_000; + const SCROLLBACK: usize = 1_000; + + // Fill the terminal with a mix of ASCII text and escape sequences. + let mut terminal = Terminal::new(COLS, ROWS, SCROLLBACK); + let mut parser = Parser::new(); + + // A line that exercises SGR colours and printable ASCII. + let fill_line = format!( + "\x1b[32mHello\x1b[0m \x1b[1;33mworld\x1b[0m! {}", + "abcdefghijklmnopqrstuvwxyz0123456789 ".repeat(4) + ); + let fill_bytes = fill_line.as_bytes(); + for _ in 0..ROWS { + parser.parse(fill_bytes, &mut terminal); + parser.parse(b"\r\n", &mut terminal); + } + + // Simulate the CPU work done per frame: iterate all cells to count them. + // (A real renderer would write CellInstance structs into a Vec here.) + let cell_count = COLS * ROWS; + let mut total: u64 = 0; + + let start = Instant::now(); + for _ in 0..FRAMES { + for idx in 0..cell_count { + // Simulate the work of push_cell: read cell, inspect attrs. + let cell = &terminal.screen[idx]; + total = total.wrapping_add(cell.ch as u64); + } + } + let elapsed = start.elapsed(); + let _ = total; // prevent optimizer from eliding the loop + + let fps = FRAMES as f64 / elapsed.as_secs_f64(); + let mcells_per_sec = (FRAMES as f64 * cell_count as f64) / elapsed.as_secs_f64() / 1_000_000.0; + + println!( + "bench_render: {} frames × {}×{} cells → {:.0} fps ({:.0} Mcells/s) [{:.2?}]", + FRAMES, COLS, ROWS, fps, mcells_per_sec, elapsed + ); +} diff --git a/winiterm/src/colorscheme.rs b/winiterm/src/colorscheme.rs new file mode 100644 index 0000000..c9d6e8b --- /dev/null +++ b/winiterm/src/colorscheme.rs @@ -0,0 +1,213 @@ +//! Built-in terminal colour schemes (Phase 7). +//! +//! Each scheme provides the 16 ANSI colours plus default fg/bg/cursor. +//! The active scheme is chosen via the Lua config (`winiterm.color_scheme`). + +// ─── Type ───────────────────────────────────────────────────────────────────── + +/// A complete terminal colour palette. +#[derive(Clone, Debug)] +pub struct ColorScheme { + pub name: &'static str, + /// ANSI colours 0–15 as linear [r,g,b] (0–255). + pub ansi: [[u8; 3]; 16], + /// Default foreground colour. + pub foreground: [u8; 3], + /// Default background colour. + pub background: [u8; 3], + /// Cursor colour. + pub cursor: [u8; 3], + /// Selection background. + pub selection_bg: [u8; 3], +} + +impl ColorScheme { + /// Convert a `[u8; 3]` colour to a `[f32; 4]` RGBA value. + #[inline] + pub fn to_rgba(c: [u8; 3]) -> [f32; 4] { + [ + c[0] as f32 / 255.0, + c[1] as f32 / 255.0, + c[2] as f32 / 255.0, + 1.0, + ] + } +} + +// ─── Built-in schemes ───────────────────────────────────────────────────────── + +pub const MONOKAI: ColorScheme = ColorScheme { + name: "monokai", + ansi: [ + [0x27, 0x28, 0x22], // 0 black + [0xF9, 0x26, 0x72], // 1 red + [0xA6, 0xE2, 0x2E], // 2 green + [0xF4, 0xBF, 0x75], // 3 yellow + [0x66, 0xD9, 0xEF], // 4 blue + [0xAE, 0x81, 0xFF], // 5 magenta + [0x2A, 0xA1, 0x98], // 6 cyan + [0xF8, 0xF8, 0xF2], // 7 white + [0x75, 0x71, 0x5E], // 8 bright black + [0xF9, 0x26, 0x72], // 9 bright red + [0xA6, 0xE2, 0x2E], // 10 bright green + [0xF4, 0xBF, 0x75], // 11 bright yellow + [0x66, 0xD9, 0xEF], // 12 bright blue + [0xAE, 0x81, 0xFF], // 13 bright magenta + [0x2A, 0xA1, 0x98], // 14 bright cyan + [0xF9, 0xF8, 0xF5], // 15 bright white + ], + foreground: [0xF8, 0xF8, 0xF2], + background: [0x27, 0x28, 0x22], + cursor: [0xF8, 0xF8, 0xF0], + selection_bg: [0x49, 0x48, 0x3E], +}; + +pub const NORD: ColorScheme = ColorScheme { + name: "nord", + ansi: [ + [0x2E, 0x34, 0x40], // 0 black + [0xBF, 0x61, 0x6A], // 1 red + [0xA3, 0xBE, 0x8C], // 2 green + [0xEB, 0xCB, 0x8B], // 3 yellow + [0x81, 0xA1, 0xC1], // 4 blue + [0xB4, 0x8E, 0xAD], // 5 magenta + [0x88, 0xC0, 0xD0], // 6 cyan + [0xE5, 0xE9, 0xF0], // 7 white + [0x4C, 0x56, 0x6A], // 8 bright black + [0xBF, 0x61, 0x6A], // 9 bright red + [0xA3, 0xBE, 0x8C], // 10 bright green + [0xEB, 0xCB, 0x8B], // 11 bright yellow + [0x81, 0xA1, 0xC1], // 12 bright blue + [0xB4, 0x8E, 0xAD], // 13 bright magenta + [0x8F, 0xBC, 0xBB], // 14 bright cyan + [0xEC, 0xEF, 0xF4], // 15 bright white + ], + foreground: [0xD8, 0xDE, 0xE9], + background: [0x2E, 0x34, 0x40], + cursor: [0xD8, 0xDE, 0xE9], + selection_bg: [0x43, 0x4C, 0x5E], +}; + +pub const DRACULA: ColorScheme = ColorScheme { + name: "dracula", + ansi: [ + [0x21, 0x22, 0x2C], // 0 black + [0xFF, 0x55, 0x55], // 1 red + [0x50, 0xFA, 0x7B], // 2 green + [0xF1, 0xFA, 0x8C], // 3 yellow + [0xBD, 0x93, 0xF9], // 4 blue + [0xFF, 0x79, 0xC6], // 5 magenta + [0x8B, 0xE9, 0xFD], // 6 cyan + [0xF8, 0xF8, 0xF2], // 7 white + [0x62, 0x72, 0xA4], // 8 bright black + [0xFF, 0x6E, 0x6E], // 9 bright red + [0x69, 0xFF, 0x94], // 10 bright green + [0xFF, 0xFF, 0xA5], // 11 bright yellow + [0xD6, 0xAC, 0xFF], // 12 bright blue + [0xFF, 0x92, 0xDF], // 13 bright magenta + [0xA4, 0xFF, 0xFF], // 14 bright cyan + [0xFF, 0xFF, 0xFF], // 15 bright white + ], + foreground: [0xF8, 0xF8, 0xF2], + background: [0x28, 0x2A, 0x36], + cursor: [0xF8, 0xF8, 0xF2], + selection_bg: [0x44, 0x47, 0x5A], +}; + +pub const ONE_DARK: ColorScheme = ColorScheme { + name: "one_dark", + ansi: [ + [0x28, 0x2C, 0x34], // 0 black + [0xE0, 0x6C, 0x75], // 1 red + [0x98, 0xC3, 0x79], // 2 green + [0xE5, 0xC0, 0x7B], // 3 yellow + [0x61, 0xAF, 0xEF], // 4 blue + [0xC6, 0x78, 0xDD], // 5 magenta + [0x56, 0xB6, 0xC2], // 6 cyan + [0xAB, 0xB2, 0xBF], // 7 white + [0x5C, 0x63, 0x70], // 8 bright black + [0xE0, 0x6C, 0x75], // 9 bright red + [0x98, 0xC3, 0x79], // 10 bright green + [0xE5, 0xC0, 0x7B], // 11 bright yellow + [0x61, 0xAF, 0xEF], // 12 bright blue + [0xC6, 0x78, 0xDD], // 13 bright magenta + [0x56, 0xB6, 0xC2], // 14 bright cyan + [0xFF, 0xFF, 0xFF], // 15 bright white + ], + foreground: [0xAB, 0xB2, 0xBF], + background: [0x28, 0x2C, 0x34], + cursor: [0x52, 0x8B, 0xFF], + selection_bg: [0x3E, 0x44, 0x51], +}; + +pub const SOLARIZED_DARK: ColorScheme = ColorScheme { + name: "solarized_dark", + ansi: [ + [0x07, 0x36, 0x42], // 0 base02 + [0xDC, 0x32, 0x2F], // 1 red + [0x85, 0x99, 0x00], // 2 green + [0xB5, 0x89, 0x00], // 3 yellow + [0x26, 0x8B, 0xD2], // 4 blue + [0xD3, 0x36, 0x82], // 5 magenta + [0x2A, 0xA1, 0x98], // 6 cyan + [0xEE, 0xE8, 0xD5], // 7 base2 + [0x00, 0x2B, 0x36], // 8 base03 + [0xCB, 0x4B, 0x16], // 9 orange + [0x58, 0x6E, 0x75], // 10 base01 + [0x65, 0x7B, 0x83], // 11 base00 + [0x83, 0x94, 0x96], // 12 base0 + [0x6C, 0x71, 0xC4], // 13 violet + [0x93, 0xA1, 0xA1], // 14 base1 + [0xFD, 0xF6, 0xE3], // 15 base3 + ], + foreground: [0x83, 0x94, 0x96], + background: [0x00, 0x2B, 0x36], + cursor: [0x26, 0x8B, 0xD2], + selection_bg: [0x07, 0x36, 0x42], +}; + +pub const GRUVBOX: ColorScheme = ColorScheme { + name: "gruvbox", + ansi: [ + [0x28, 0x28, 0x28], // 0 bg + [0xCC, 0x24, 0x1D], // 1 red + [0x98, 0x97, 0x1A], // 2 green + [0xD7, 0x99, 0x21], // 3 yellow + [0x45, 0x85, 0x88], // 4 blue + [0xB1, 0x62, 0x86], // 5 purple + [0x68, 0x9D, 0x6A], // 6 aqua + [0xA8, 0x99, 0x84], // 7 fg4 + [0x92, 0x83, 0x74], // 8 gray + [0xFB, 0x49, 0x34], // 9 bright red + [0xB8, 0xBB, 0x26], // 10 bright green + [0xFA, 0xBD, 0x2F], // 11 bright yellow + [0x83, 0xA5, 0x98], // 12 bright blue + [0xD3, 0x86, 0x9B], // 13 bright purple + [0x8E, 0xC0, 0x7C], // 14 bright aqua + [0xEB, 0xDB, 0xB2], // 15 fg + ], + foreground: [0xEB, 0xDB, 0xB2], + background: [0x28, 0x28, 0x28], + cursor: [0xEB, 0xDB, 0xB2], + selection_bg: [0x3C, 0x38, 0x36], +}; + +// ─── Registry ───────────────────────────────────────────────────────────────── + +/// All built-in schemes. The first entry is the default. +pub const ALL: &[&ColorScheme] = &[ + &MONOKAI, + &NORD, + &DRACULA, + &ONE_DARK, + &SOLARIZED_DARK, + &GRUVBOX, +]; + +/// Look up a scheme by name (case-insensitive). Falls back to Monokai. +pub fn by_name(name: &str) -> &'static ColorScheme { + ALL.iter() + .find(|s| s.name.eq_ignore_ascii_case(name)) + .copied() + .unwrap_or(&MONOKAI) +} diff --git a/winiterm/src/config.rs b/winiterm/src/config.rs new file mode 100644 index 0000000..230e329 --- /dev/null +++ b/winiterm/src/config.rs @@ -0,0 +1,112 @@ +//! Application configuration — loaded from a Lua script (Phase 5). +//! +//! The config file lives at `%APPDATA%\winiterm\config.lua` and is expected +//! to `return` a plain table of key/value pairs. Missing keys fall back to +//! built-in defaults. Parse errors are logged and defaults are used. + +use crate::colorscheme::{by_name, ColorScheme}; + +// ─── Config struct ───────────────────────────────────────────────────────────── + +/// All user-tunable settings. +#[derive(Debug, Clone)] +pub struct Config { + /// Base font size in logical pixels (DPI-scaled automatically). + pub font_size: f32, + /// Name of the active colour scheme (case-insensitive). + pub color_scheme: String, + /// Shell executable path. `None` → auto-detect. + pub shell: Option, + /// Scrollback buffer size (lines). + pub scrollback_lines: usize, + /// Try to enable Windows 11 Mica backdrop transparency. + pub enable_mica: bool, + /// Show a persistent system-tray icon. + pub tray_icon: bool, + /// Window opacity in \[0, 1\]. + pub opacity: f32, +} + +impl Default for Config { + fn default() -> Self { + Self { + font_size: 16.0, + color_scheme: "monokai".to_string(), + shell: None, + scrollback_lines: 10_000, + enable_mica: false, + tray_icon: false, + opacity: 1.0, + } + } +} + +impl Config { + // ── File location ───────────────────────────────────────────────────────── + + /// Path to the Lua config file. + pub fn config_path() -> std::path::PathBuf { + let appdata = std::env::var("APPDATA").unwrap_or_else(|_| ".".to_string()); + std::path::PathBuf::from(appdata) + .join("winiterm") + .join("config.lua") + } + + // ── Loading ─────────────────────────────────────────────────────────────── + + /// Load configuration from disk. Falls back to defaults on any error. + pub fn load() -> Self { + let path = Self::config_path(); + if !path.exists() { + log::debug!("config not found at {}, using defaults", path.display()); + return Self::default(); + } + let code = match std::fs::read_to_string(&path) { + Ok(s) => s, + Err(e) => { + log::warn!("could not read config {}: {e}", path.display()); + return Self::default(); + } + }; + match Self::parse_lua(&code) { + Ok(c) => { + log::info!("loaded config from {}", path.display()); + c + } + Err(e) => { + log::warn!("config parse error: {e}"); + Self::default() + } + } + } + + fn parse_lua(code: &str) -> mlua::Result { + use mlua::Lua; + let lua = Lua::new(); + let tbl: mlua::Table = lua.load(code).eval()?; + + Ok(Self { + font_size: tbl.get::("font_size").unwrap_or(16.0), + color_scheme: tbl + .get::("color_scheme") + .unwrap_or_else(|_| "monokai".to_string()), + shell: tbl.get::>("shell").unwrap_or(None), + scrollback_lines: tbl.get::("scrollback_lines").unwrap_or(10_000), + enable_mica: tbl.get::("enable_mica").unwrap_or(false), + tray_icon: tbl.get::("tray_icon").unwrap_or(false), + opacity: tbl.get::("opacity").unwrap_or(1.0), + }) + } + + // ── Convenience accessors ───────────────────────────────────────────────── + + /// Look up the active [`ColorScheme`] by name. + pub fn color_scheme(&self) -> &'static ColorScheme { + by_name(&self.color_scheme) + } + + /// Shell as `Option<&str>`, ready to pass to `Pty::spawn`. + pub fn shell_ref(&self) -> Option<&str> { + self.shell.as_deref() + } +} diff --git a/winiterm/src/dwm.rs b/winiterm/src/dwm.rs new file mode 100644 index 0000000..fd7f368 --- /dev/null +++ b/winiterm/src/dwm.rs @@ -0,0 +1,33 @@ +//! DWM window transparency — Windows 11 Mica backdrop (Phase 8). +//! +//! `enable_mica` applies the Mica system backdrop to the given window handle. +//! On older Windows versions (pre-22H2) the DWM attribute is unknown and the +//! call fails silently, returning `false`. + +use windows::Win32::Foundation::HWND; +use windows::Win32::Graphics::Dwm::{DwmSetWindowAttribute, DWMWINDOWATTRIBUTE}; + +/// `DWMWA_SYSTEMBACKDROP_TYPE` — attribute index 38, added in Windows 11 22H2. +const DWMWA_SYSTEMBACKDROP_TYPE: DWMWINDOWATTRIBUTE = DWMWINDOWATTRIBUTE(38); + +/// `DWMSBT_MAINWINDOW` — Mica effect value for `DWMWA_SYSTEMBACKDROP_TYPE`. +const DWMSBT_MAINWINDOW: u32 = 2; + +/// Attempt to enable the Mica backdrop on `hwnd`. +/// +/// Returns `true` if the DWM call succeeded. Returns `false` (no-ops) on +/// pre-Windows 11 22H2 where the attribute is unsupported. +pub fn enable_mica(hwnd: HWND) -> bool { + let backdrop: u32 = DWMSBT_MAINWINDOW; + // SAFETY: `DwmSetWindowAttribute` is a standard Win32 API. We pass a + // correctly-sized pointer to a `u32` and its byte-size. + unsafe { + DwmSetWindowAttribute( + hwnd, + DWMWA_SYSTEMBACKDROP_TYPE, + (&backdrop as *const u32).cast(), + std::mem::size_of::() as u32, + ) + .is_ok() + } +} diff --git a/winiterm/src/input.rs b/winiterm/src/input.rs new file mode 100644 index 0000000..0415840 --- /dev/null +++ b/winiterm/src/input.rs @@ -0,0 +1,224 @@ +//! Keyboard event → terminal action translation. +//! +//! [`key_to_action`] converts a winit key event + modifier state into an +//! [`InputAction`]. Multiplexer hotkeys (Ctrl+Shift+…) are checked first and +//! take precedence over normal VT byte generation. +//! +//! New in A-series: +//! * Alt key: prefixes generated bytes with `\x1b` (xterm metaSendsEscape). +//! * Paste: `Ctrl+Shift+V` → [`InputAction::Paste`]. +//! * Scrollback: `Ctrl+Shift+Up/Down` → [`InputAction::ScrollUp`] / `ScrollDown`. +//! * DECCKM: `decckm` parameter selects SS3 vs CSI cursor key encoding. + +use winit::event::{ElementState, KeyEvent, Modifiers}; +use winit::keyboard::{Key, KeyCode, NamedKey, PhysicalKey}; + +// ─── InputAction ────────────────────────────────────────────────────────────── + +/// The result of translating a key press. +#[derive(Debug, Clone)] +pub enum InputAction { + /// Raw bytes to write to the active pane's PTY stdin. + Bytes(Vec), + // ── Multiplexer commands ──────────────────────────────────────────────── + /// Split active pane horizontally (left | right). + SplitHorizontal, + /// Split active pane vertically (top / bottom). + SplitVertical, + /// Move focus to the next pane in tree order. + FocusNext, + /// Move focus to the previous pane in tree order. + FocusPrev, + /// Open a new tab. + NewTab, + /// Switch to tab by 0-based index. + SwitchTab(usize), + /// Close the active pane (or current tab if it is the last pane). + ClosePane, + /// Paste text from the system clipboard (Ctrl+Shift+V). + Paste, + /// Scroll the active pane up into the scrollback buffer (Ctrl+Shift+Up). + ScrollUp, + /// Scroll the active pane down towards live output (Ctrl+Shift+Down). + ScrollDown, +} + +// ─── Public API ─────────────────────────────────────────────────────────────── + +/// Translate a pressed key event into an [`InputAction`]. +/// +/// `decckm` — whether DECCKM (application cursor key mode, `?1h`) is active +/// in the current terminal. When `true`, arrow keys send `\x1bOA` (SS3) +/// instead of `\x1b[A` (CSI). +/// +/// Returns `None` for release events, unhandled keys, or pure-modifier presses. +pub fn key_to_action(event: &KeyEvent, mods: &Modifiers, decckm: bool) -> Option { + if event.state == ElementState::Released { + return None; + } + + let ctrl = mods.state().control_key(); + let shift = mods.state().shift_key(); + let alt = mods.state().alt_key(); + + // ── Multiplexer hotkeys (Ctrl+Shift) ────────────────────────────────────── + // These take precedence and never reach the shell. + if ctrl && shift { + let action = match &event.physical_key { + PhysicalKey::Code(KeyCode::Backslash) => Some(InputAction::SplitHorizontal), + PhysicalKey::Code(KeyCode::Minus) => Some(InputAction::SplitVertical), + PhysicalKey::Code(KeyCode::BracketLeft) => Some(InputAction::FocusPrev), + PhysicalKey::Code(KeyCode::BracketRight) => Some(InputAction::FocusNext), + PhysicalKey::Code(KeyCode::KeyT) => Some(InputAction::NewTab), + PhysicalKey::Code(KeyCode::KeyW) => Some(InputAction::ClosePane), + PhysicalKey::Code(KeyCode::KeyV) => Some(InputAction::Paste), + PhysicalKey::Code(KeyCode::ArrowUp) => Some(InputAction::ScrollUp), + PhysicalKey::Code(KeyCode::ArrowDown) => Some(InputAction::ScrollDown), + PhysicalKey::Code(c) => digit_to_tab(c), + _ => None, + }; + if action.is_some() { + return action; + } + } + + // ── Normal VT byte generation ───────────────────────────────────────────── + let action = match &event.logical_key { + Key::Character(s) => { + if ctrl { + let ch = s.chars().next()?.to_ascii_lowercase(); + let byte = match ch { + 'a'..='z' => ch as u8 - b'a' + 1, + '[' => 0x1b, // ESC + '\\' => 0x1c, + ']' => 0x1d, + '^' => 0x1e, + '_' => 0x1f, + _ => return None, + }; + Some(InputAction::Bytes(vec![byte])) + } else { + Some(InputAction::Bytes(s.as_bytes().to_vec())) + } + } + + Key::Named(named) => translate_named(named, shift, decckm), + + Key::Unidentified(_) | Key::Dead(_) => None, + }; + + // Alt (xterm metaSendsEscape): prefix generated bytes with ESC. + if alt { + if let Some(InputAction::Bytes(ref bytes)) = action { + let mut v = Vec::with_capacity(bytes.len() + 1); + v.push(0x1b); + v.extend_from_slice(bytes); + return Some(InputAction::Bytes(v)); + } + } + + action +} + +// ─── Private helpers ────────────────────────────────────────────────────────── + +fn digit_to_tab(code: &KeyCode) -> Option { + let n: usize = match code { + KeyCode::Digit1 => 1, + KeyCode::Digit2 => 2, + KeyCode::Digit3 => 3, + KeyCode::Digit4 => 4, + KeyCode::Digit5 => 5, + KeyCode::Digit6 => 6, + KeyCode::Digit7 => 7, + KeyCode::Digit8 => 8, + KeyCode::Digit9 => 9, + _ => return None, + }; + Some(InputAction::SwitchTab(n - 1)) +} + +fn translate_named(key: &NamedKey, shift: bool, decckm: bool) -> Option { + let bytes: &[u8] = match key { + NamedKey::Enter => b"\r", + NamedKey::Backspace => b"\x7f", + NamedKey::Tab => { + return Some(InputAction::Bytes(if shift { + b"\x1b[Z".to_vec() // Shift+Tab = back-tab + } else { + b"\t".to_vec() + })); + } + NamedKey::Escape => b"\x1b", + NamedKey::Space => b" ", + + // Cursor keys — DECCKM selects SS3 (application) vs CSI (normal). + NamedKey::ArrowUp => { + if decckm { + b"\x1bOA" + } else { + b"\x1b[A" + } + } + NamedKey::ArrowDown => { + if decckm { + b"\x1bOB" + } else { + b"\x1b[B" + } + } + NamedKey::ArrowRight => { + if decckm { + b"\x1bOC" + } else { + b"\x1b[C" + } + } + NamedKey::ArrowLeft => { + if decckm { + b"\x1bOD" + } else { + b"\x1b[D" + } + } + + // Home / End also use SS3 in application cursor mode. + NamedKey::Home => { + if decckm { + b"\x1bOH" + } else { + b"\x1b[H" + } + } + NamedKey::End => { + if decckm { + b"\x1bOF" + } else { + b"\x1b[F" + } + } + + // Editing keys. + NamedKey::Insert => b"\x1b[2~", + NamedKey::Delete => b"\x1b[3~", + NamedKey::PageUp => b"\x1b[5~", + NamedKey::PageDown => b"\x1b[6~", + + // Function keys (xterm encoding). + NamedKey::F1 => b"\x1bOP", + NamedKey::F2 => b"\x1bOQ", + NamedKey::F3 => b"\x1bOR", + NamedKey::F4 => b"\x1bOS", + NamedKey::F5 => b"\x1b[15~", + NamedKey::F6 => b"\x1b[17~", + NamedKey::F7 => b"\x1b[18~", + NamedKey::F8 => b"\x1b[19~", + NamedKey::F9 => b"\x1b[20~", + NamedKey::F10 => b"\x1b[21~", + NamedKey::F11 => b"\x1b[23~", + NamedKey::F12 => b"\x1b[24~", + + _ => return None, + }; + Some(InputAction::Bytes(bytes.to_vec())) +} diff --git a/winiterm/src/kitty.rs b/winiterm/src/kitty.rs new file mode 100644 index 0000000..35fa039 --- /dev/null +++ b/winiterm/src/kitty.rs @@ -0,0 +1,190 @@ +//! Kitty Graphics Protocol — APC sequence parser and image registry (Phase 9). +//! +//! Parses `ESC_G…ST` sequences emitted by Kitty-protocol-aware applications +//! and assembles them into decoded RGBA images stored in a [`KittyRegistry`]. +//! +//! Integration note: the VT parser accumulates APC bytes in its `dcs_buf` +//! (state `SosPmApcString`). Phase 9 wiring will route those bytes through +//! [`KittyRegistry::ingest`] before discarding them. + +use base64::{engine::general_purpose::STANDARD as B64, Engine}; +use std::collections::HashMap; + +// ─── Control block ──────────────────────────────────────────────────────────── + +/// Key/value data decoded from a single Kitty APC control block. +#[derive(Debug, Default, Clone)] +pub struct KittyControl { + /// Action: `'t'` transmit, `'p'` put, `'d'` delete, `'q'` query. + pub action: Option, + /// Image ID. + pub image_id: Option, + /// Placement ID. + pub placement_id: Option, + /// Data format: 32 = RGBA, 24 = RGB, 100 = PNG. + pub format: Option, + /// Image width in pixels. + pub width: Option, + /// Image height in pixels. + pub height: Option, + /// `true` when more chunks are expected (`m=1`). + pub more: bool, + /// Suppress response (`q≠0`). + pub quiet: bool, +} + +// ─── Image ──────────────────────────────────────────────────────────────────── + +/// A fully decoded Kitty image (RGBA pixels, row-major). +#[derive(Debug)] +pub struct KittyImage { + pub id: u32, + pub width: u32, + pub height: u32, + /// Raw RGBA pixels: `width * height * 4` bytes. + pub rgba: Vec, +} + +// ─── Registry ───────────────────────────────────────────────────────────────── + +/// Stores assembled Kitty images keyed by image ID, and accumulates partial +/// multi-chunk transmissions until the final chunk arrives. +#[derive(Default)] +pub struct KittyRegistry { + images: HashMap, + /// Partial payloads awaiting their final chunk: id → (control, raw bytes). + pending: HashMap)>, +} + +impl KittyRegistry { + pub fn new() -> Self { + Self::default() + } + + /// Ingest the raw bytes of a Kitty APC sequence — everything between the + /// `ESC_G` introducer and the `ST` terminator. + pub fn ingest(&mut self, payload: &[u8]) { + let Some((ctrl_bytes, b64)) = split_payload(payload) else { + return; + }; + let ctrl = parse_control(ctrl_bytes); + let mut decoded = B64.decode(b64).unwrap_or_default(); + let id = ctrl.image_id.unwrap_or(0); + + if ctrl.more { + // Accumulate partial chunk. + self.pending + .entry(id) + .and_modify(|(_, buf)| buf.append(&mut decoded)) + .or_insert_with(|| (ctrl, decoded)); + } else { + // Final chunk — assemble the complete image. + let (base_ctrl, mut buf) = self + .pending + .remove(&id) + .unwrap_or_else(|| (ctrl.clone(), Vec::new())); + buf.append(&mut decoded); + + let w = base_ctrl.width.unwrap_or(0); + let h = base_ctrl.height.unwrap_or(0); + if w > 0 && h > 0 { + let rgba = match base_ctrl.format.unwrap_or(32) { + 100 => decode_png(&buf), + 32 => buf, + 24 => rgb_to_rgba(&buf), + _ => buf, + }; + self.images.insert( + id, + KittyImage { + id, + width: w, + height: h, + rgba, + }, + ); + } + } + } + + /// Retrieve a decoded image by ID. + pub fn get(&self, id: u32) -> Option<&KittyImage> { + self.images.get(&id) + } + + /// Remove a single image and any pending chunks for it by ID. + pub fn remove(&mut self, id: u32) { + self.images.remove(&id); + self.pending.remove(&id); + } + + /// Remove all stored images and pending chunks. + pub fn clear(&mut self) { + self.images.clear(); + self.pending.clear(); + } +} + +// ─── Public helper ──────────────────────────────────────────────────────────── + +/// Parse only the control header of a raw Kitty APC payload +/// (everything after the leading `'G'` byte and before `ST`). +/// +/// Returns a default [`KittyControl`] when the payload has no `';'` separator. +pub fn parse_header(payload: &[u8]) -> KittyControl { + split_payload(payload) + .map(|(ctrl, _)| parse_control(ctrl)) + .unwrap_or_default() +} + +// ─── Private helpers ────────────────────────────────────────────────────────── + +/// Split the payload at the first `';'` separator. +fn split_payload(payload: &[u8]) -> Option<(&[u8], &[u8])> { + payload + .iter() + .position(|&b| b == b';') + .map(|i| (&payload[..i], &payload[i + 1..])) +} + +/// Parse comma-separated `key=value` control data. +fn parse_control(ctrl: &[u8]) -> KittyControl { + let mut c = KittyControl::default(); + for kv in ctrl.split(|&b| b == b',') { + if kv.len() < 3 || kv[1] != b'=' { + continue; + } + let val = &kv[2..]; + match kv[0] { + b'a' => c.action = val.first().map(|&b| b as char), + b'i' => c.image_id = parse_u32(val), + b'p' => c.placement_id = parse_u32(val), + b'f' => c.format = parse_u32(val), + b's' => c.width = parse_u32(val), + b'v' => c.height = parse_u32(val), + b'm' => c.more = val == b"1", + b'q' => c.quiet = val != b"0", + _ => {} + } + } + c +} + +fn parse_u32(bytes: &[u8]) -> Option { + std::str::from_utf8(bytes).ok()?.parse().ok() +} + +fn decode_png(data: &[u8]) -> Vec { + image::load_from_memory(data) + .map(|img| img.into_rgba8().into_raw()) + .unwrap_or_default() +} + +fn rgb_to_rgba(data: &[u8]) -> Vec { + let mut out = Vec::with_capacity(data.len() / 3 * 4); + for chunk in data.chunks_exact(3) { + out.extend_from_slice(chunk); + out.push(255); + } + out +} diff --git a/winiterm/src/lib.rs b/winiterm/src/lib.rs new file mode 100644 index 0000000..92e6a1e --- /dev/null +++ b/winiterm/src/lib.rs @@ -0,0 +1,20 @@ +//! winiterm — GPU-accelerated terminal emulator for Windows. +//! +//! Library root: exposes all core modules for use by the main binary +//! and standalone benchmark binaries. + +pub mod app; +pub mod colorscheme; +pub mod config; +pub mod dwm; +pub mod input; +pub mod kitty; +pub mod lua_api; +pub mod pane; +pub mod pty; +pub mod renderer; +pub mod split; +pub mod terminal; +pub mod tray; +pub mod vt_parser; +pub mod workspace; diff --git a/winiterm/src/lua_api.rs b/winiterm/src/lua_api.rs new file mode 100644 index 0000000..8d19666 --- /dev/null +++ b/winiterm/src/lua_api.rs @@ -0,0 +1,108 @@ +//! Lua event-hook system (Phase 6). +//! +//! [`LuaApi`] owns a Lua 5.4 state and exposes a `winiterm.on(event, fn)` +//! function that plugins / the config file can call to register callbacks. +//! Events are dispatched synchronously from the Rust side via +//! [`LuaApi::dispatch`]. +//! +//! # Usage in config.lua +//! ```lua +//! local winiterm = require 'winiterm' -- or just access the global +//! winiterm.on("title_change", function(title) +//! print("title changed to " .. title) +//! end) +//! ``` + +use mlua::{Function as LuaFunction, Lua, Table as LuaTable}; + +// ─── LuaApi ─────────────────────────────────────────────────────────────────── + +/// Owns the Lua state and the hook registry. +pub struct LuaApi { + lua: Lua, +} + +impl LuaApi { + /// Create a new Lua state and set up the `winiterm` global module. + pub fn new() -> mlua::Result { + let lua = Lua::new(); + Self::setup_globals(&lua)?; + Ok(Self { lua }) + } + + fn setup_globals(lua: &Lua) -> mlua::Result<()> { + // Internal storage: _winiterm_hooks[event_name] = { fn, fn, … } + let hooks: LuaTable = lua.create_table()?; + lua.globals().set("_winiterm_hooks", hooks)?; + + // winiterm.on(event, callback) + let on_fn = lua.create_function(|lua, (event, cb): (String, LuaFunction)| { + let hooks: LuaTable = lua.globals().get("_winiterm_hooks")?; + let list: LuaTable = hooks + .get::>(event.as_str())? + .unwrap_or(lua.create_table()?); + list.push(cb)?; + hooks.set(event.as_str(), list)?; + Ok(()) + })?; + + let winiterm: LuaTable = lua.create_table()?; + winiterm.set("on", on_fn)?; + lua.globals().set("winiterm", winiterm)?; + + Ok(()) + } + + /// Execute a Lua script in the current state. + /// + /// Can be called multiple times (e.g. once for the built-in defaults and + /// once for the user config). + pub fn exec(&self, code: &str) -> mlua::Result<()> { + self.lua.load(code).exec() + } + + /// Read the config file at `path` and execute it in this Lua state, + /// registering any `winiterm.on(...)` hooks it defines. + /// + /// Silently succeeds if the file does not exist yet. + pub fn exec_config(&self, path: &std::path::Path) -> mlua::Result<()> { + if !path.exists() { + return Ok(()); + } + let code = + std::fs::read_to_string(path).map_err(|e| mlua::Error::RuntimeError(e.to_string()))?; + self.lua.load(code.as_str()).exec() + } + + /// Clear all registered event hooks (call before re-executing the config + /// file on hot-reload so that callbacks do not accumulate). + pub fn clear_hooks(&self) { + if let Ok(hooks) = self.lua.create_table() { + let _ = self.lua.globals().set("_winiterm_hooks", hooks); + } + } + + /// Dispatch an event to all registered callbacks, passing `arg` as the + /// single string argument. + pub fn dispatch(&self, event: &str, arg: &str) { + let hooks: LuaTable = match self.lua.globals().get("_winiterm_hooks") { + Ok(t) => t, + Err(_) => return, + }; + let list: LuaTable = match hooks.get::>(event) { + Ok(Some(t)) => t, + _ => return, + }; + for cb in list.sequence_values::() { + if let Ok(f) = cb { + let _ = f.call::<()>(arg.to_owned()); + } + } + } +} + +impl Default for LuaApi { + fn default() -> Self { + Self::new().expect("failed to create Lua state") + } +} diff --git a/winiterm/src/main.rs b/winiterm/src/main.rs new file mode 100644 index 0000000..13d2431 --- /dev/null +++ b/winiterm/src/main.rs @@ -0,0 +1,77 @@ +//! winiterm — GPU-accelerated terminal emulator for Windows. +//! +//! Declared as a Windows GUI application so that Windows does not allocate or +//! attach a console to this process on startup. Without this attribute the +//! process would inherit cargo's console, which causes `STATUS_DLL_INIT_FAILED` +//! (0xC0000142) in the ConPTY child process (e.g. pwsh.exe) because the +//! ConHost.exe backing the pseudo-console conflicts with the already-attached +//! parent console. +//! +//! Even with `windows_subsystem = "windows"`, launching from an existing +//! terminal (bash, cmd, Windows Terminal) causes the process to *inherit* the +//! parent's console handle. `FreeConsole` releases that handle before any +//! ConPTY is created, so child processes never see a conflicting console. +//! Calling it when there is no console (double-click launch) is a safe no-op. +//! +//! Logs are written to `%APPDATA%\winiterm\winiterm.log` so they are always +//! accessible regardless of how the binary was launched. Set `RUST_LOG` to +//! override the default `info` level (e.g. `RUST_LOG=debug`). +#![windows_subsystem = "windows"] + +fn main() { + // Log to a file rather than stderr: for a windows_subsystem = "windows" + // binary the stderr handle is often not connected to anything useful when + // launched from PowerShell or by double-clicking. + init_logging(); + + // Detach from any inherited console before creating ConPTY children. + // This prevents STATUS_DLL_INIT_FAILED (0xC0000142) regardless of how + // the binary was launched. File handles (including the log file) are + // unaffected by FreeConsole. + #[cfg(target_os = "windows")] + // SAFETY: FreeConsole has no preconditions and is always safe to call. + unsafe { + let _ = windows::Win32::System::Console::FreeConsole(); + } + + winiterm::app::App::run(); +} + +/// Initialise `env_logger` to write to `%APPDATA%\winiterm\winiterm.log`. +/// +/// The directory is created if it does not exist. Falls back to stderr if +/// the file cannot be opened. Default level is `info`; set `RUST_LOG` to +/// override (e.g. `RUST_LOG=debug` or `RUST_LOG=winiterm=trace`). +fn init_logging() { + let log_path = { + let appdata = std::env::var("APPDATA").unwrap_or_else(|_| ".".into()); + let dir = std::path::PathBuf::from(appdata).join("winiterm"); + let _ = std::fs::create_dir_all(&dir); + dir.join("winiterm.log") + }; + + let mut builder = env_logger::Builder::new(); + // Default level: info. Honour RUST_LOG if set. + builder.filter_level(log::LevelFilter::Info); + if let Ok(spec) = std::env::var("RUST_LOG") { + builder.parse_filters(&spec); + } + + match std::fs::OpenOptions::new() + .create(true) + .truncate(true) + .write(true) + .open(&log_path) + { + Ok(file) => { + builder.target(env_logger::Target::Pipe(Box::new(file))); + } + Err(_) => { + builder.target(env_logger::Target::Stderr); + } + } + + builder.init(); + log::info!("winiterm {} starting", env!("CARGO_PKG_VERSION")); + log::info!("log: {}", log_path.display()); +} diff --git a/winiterm/src/pane.rs b/winiterm/src/pane.rs new file mode 100644 index 0000000..9108617 --- /dev/null +++ b/winiterm/src/pane.rs @@ -0,0 +1,109 @@ +//! A single terminal pane: owns a `Terminal`, `Parser`, and `Pty`. +//! +//! `TerminalPane` is the unit of state for one shell session. Multiple panes +//! can be laid out in a split tree inside a `Workspace` (Phase 4). + +use crate::pty::{Pty, PtyError}; +use crate::terminal::Terminal; +use crate::vt_parser::Parser; + +// ─── TerminalPane ───────────────────────────────────────────────────────────── + +pub struct TerminalPane { + pub id: usize, + pub terminal: Terminal, + pub parser: Parser, + pub pty: Option, + /// Window title set by the shell (OSC 0/2). + pub title: String, + /// How many lines the user has scrolled back into the scrollback buffer. + /// 0 = at the live bottom of output. + pub scroll_offset: usize, +} + +impl TerminalPane { + /// Spawn a new pane with a child shell process. + pub fn spawn( + id: usize, + shell: Option<&str>, + cols: u16, + rows: u16, + scrollback: usize, + ) -> Result { + let pty = Pty::spawn(shell, cols, rows)?; + Ok(Self { + id, + terminal: Terminal::new(cols as usize, rows as usize, scrollback), + parser: Parser::new(), + pty: Some(pty), + title: "shell".to_string(), + scroll_offset: 0, + }) + } + + /// Drain the PTY output channel and feed bytes through the VT parser. + /// + /// Returns `true` if any data was processed (i.e. a redraw is needed). + pub fn process_output(&mut self) -> bool { + // Collect all pending chunks without holding &self.pty across the + // mutable borrow of self.parser / self.terminal. + let chunks: Vec> = self + .pty + .as_ref() + .and_then(|p| p.data_receiver.as_ref()) + .map(|rx| { + let mut v = Vec::new(); + while let Ok(data) = rx.try_recv() { + v.push(data); + } + v + }) + .unwrap_or_default(); + + let dirty = !chunks.is_empty(); + for chunk in chunks { + self.parser.parse(&chunk, &mut self.terminal); + } + + // Flush any VT response bytes queued by the terminal (DSR, DA1, etc.). + let responses: Vec> = self.terminal.pending_responses.drain(..).collect(); + for r in responses { + self.write(&r); + } + + // Sync the title from the terminal model (set by OSC 0/2). + if !self.terminal.title.is_empty() && self.terminal.title != self.title { + self.title.clone_from(&self.terminal.title); + } + + dirty + } + + /// Write raw bytes to the shell's stdin. + pub fn write(&self, data: &[u8]) { + if let Some(pty) = &self.pty { + let _ = pty.write(data); + } + } + + /// Resize the terminal grid and the pseudo console. + pub fn resize(&mut self, cols: u16, rows: u16) { + self.terminal.resize(cols as usize, rows as usize); + if let Some(pty) = &self.pty { + let _ = pty.resize(cols, rows); + } + } + + /// Returns `true` if the child process has exited. + pub fn is_dead(&self) -> bool { + let dead = self.pty.as_ref().map_or(true, |p| p.child_exited()); + if dead { + log::info!( + "pane {} is_dead=true (pty_present={})", + self.id, + self.pty.is_some() + ); + } + dead + } +} diff --git a/winiterm/src/pty/mod.rs b/winiterm/src/pty/mod.rs new file mode 100644 index 0000000..7fe1be9 --- /dev/null +++ b/winiterm/src/pty/mod.rs @@ -0,0 +1,27 @@ +//! PTY (Pseudo Terminal) abstraction. +//! +//! On Windows we use the ConPTY API introduced in Windows 10 1809 +//! (`CreatePseudoConsole` / `ResizePseudoConsole` / `ClosePseudoConsole`). + +mod windows; +pub use windows::Pty; + +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum PtyError { + #[error("Windows API error: {0}")] + Windows(#[from] ::windows::core::Error), + + #[error("Failed to create pseudo console: {0}")] + CreateConsole(String), + + #[error("Failed to spawn shell process: {0}")] + SpawnProcess(String), + + #[error("Failed to resize PTY: {0}")] + Resize(String), + + #[error("I/O error: {0}")] + Io(String), +} diff --git a/winiterm/src/pty/windows.rs b/winiterm/src/pty/windows.rs new file mode 100644 index 0000000..a67a6fe --- /dev/null +++ b/winiterm/src/pty/windows.rs @@ -0,0 +1,394 @@ +//! ConPTY-based PTY for Windows. +//! +//! Architecture: +//! - `Pty::spawn` creates two anonymous pipes, a pseudo console, and a child process. +//! - A dedicated OS thread reads from the child's stdout pipe and sends chunks via +//! an `mpsc` channel so the caller can process output asynchronously. +//! - Writing to the child's stdin uses the standard `std::io::Write` trait to avoid +//! dependency on windows-rs ReadFile/WriteFile (which moved between crate versions). + +// `spawn_inner` is a large unsafe function full of Win32 API calls; the body +// is intentionally entirely unsafe, so we suppress the Rust 2024 lint that +// requires explicit `unsafe {}` blocks inside `unsafe fn`. +#![allow(unsafe_op_in_unsafe_fn)] + +use super::PtyError; +use std::io::{Read, Write}; +use std::os::windows::io::FromRawHandle; +use std::sync::mpsc::{channel, Receiver, Sender}; +use std::thread; + +use windows::core::PWSTR; +use windows::Win32::Foundation::{CloseHandle, BOOL, HANDLE, INVALID_HANDLE_VALUE}; +use windows::Win32::Security::SECURITY_ATTRIBUTES; +use windows::Win32::System::Console::{ + ClosePseudoConsole, CreatePseudoConsole, ResizePseudoConsole, COORD, HPCON, +}; +use windows::Win32::System::Pipes::CreatePipe; +use windows::Win32::System::Threading::{ + CreateProcessW, DeleteProcThreadAttributeList, GetExitCodeProcess, + InitializeProcThreadAttributeList, TerminateProcess, UpdateProcThreadAttribute, + EXTENDED_STARTUPINFO_PRESENT, LPPROC_THREAD_ATTRIBUTE_LIST, PROCESS_CREATION_FLAGS, + PROCESS_INFORMATION, STARTF_USESTDHANDLES, STARTUPINFOEXW, STARTUPINFOW, +}; + +/// `UpdateProcThreadAttribute` attribute identifier for pseudo consoles. +/// +/// Computed as `ProcThreadAttributeValue(22, FALSE, TRUE, FALSE)`: +/// `(22 & 0xFFFF) | (1 << 17)` = `0x00020016`. +const PROC_THREAD_ATTR_PSEUDOCONSOLE: usize = 0x0002_0016; + +/// Win32 STILL_ACTIVE process exit code (259 = 0x103). +const STILL_ACTIVE: u32 = 259; + +// ─── Handle wrappers ──────────────────────────────────────────────────────── + +/// Wraps a Win32 `HANDLE` so it can be sent across threads. +/// +/// # Safety +/// The caller must guarantee the handle is not used from more than one thread +/// concurrently without external synchronisation. +struct SendHandle(HANDLE); +unsafe impl Send for SendHandle {} +unsafe impl Sync for SendHandle {} + +impl SendHandle { + #[inline] + fn get(&self) -> HANDLE { + self.0 + } +} + +/// RAII wrapper that calls `ClosePseudoConsole` on drop. +struct PseudoConsole(HPCON); +unsafe impl Send for PseudoConsole {} + +impl Drop for PseudoConsole { + fn drop(&mut self) { + unsafe { + ClosePseudoConsole(self.0); + } + } +} + +// ─── Public API ───────────────────────────────────────────────────────────── + +/// A pseudo terminal backed by the Windows ConPTY API. +pub struct Pty { + /// Pseudo console handle (closed on drop via `PseudoConsole`). + hpc: PseudoConsole, + /// Write end of the stdin pipe — send data here to reach the child's stdin. + stdin_write: SendHandle, + /// Child process handle. + process_handle: SendHandle, + /// Process ID of the spawned shell. + pub process_id: u32, + /// Receives output chunks produced by the child process. + /// Populated by the background reader thread. + pub data_receiver: Option>>, +} + +impl Pty { + /// Spawn a shell inside a new pseudo console of `cols × rows` cells. + /// + /// `shell` — path to the shell executable. + /// Pass `None` to auto-detect: `pwsh` → `powershell` → `cmd`. + pub fn spawn(shell: Option<&str>, cols: u16, rows: u16) -> Result { + // SAFETY: All ConPTY invariants are upheld below. + unsafe { Self::spawn_inner(shell, cols, rows) } + } + + unsafe fn spawn_inner(shell: Option<&str>, cols: u16, rows: u16) -> Result { + // ── Pipe pair for stdin (terminal writes → child reads) ────────────── + let mut stdin_read = HANDLE::default(); + let mut stdin_write = HANDLE::default(); + CreatePipe( + &mut stdin_read, + &mut stdin_write, + None::<*const SECURITY_ATTRIBUTES>, + 0, + ) + .map_err(PtyError::Windows)?; + + // ── Pipe pair for stdout (child writes → terminal reads) ───────────── + let mut stdout_read = HANDLE::default(); + let mut stdout_write = HANDLE::default(); + if let Err(e) = CreatePipe( + &mut stdout_read, + &mut stdout_write, + None::<*const SECURITY_ATTRIBUTES>, + 0, + ) { + let _ = CloseHandle(stdin_read); + let _ = CloseHandle(stdin_write); + return Err(PtyError::Windows(e)); + } + + // ── Create the pseudo console ──────────────────────────────────────── + // windows-rs 0.58: CreatePseudoConsole(size, hinput, houtput, dwFlags) -> Result + let size = COORD { + X: cols as i16, + Y: rows as i16, + }; + let hpc = match CreatePseudoConsole(size, stdin_read, stdout_write, 0) { + Ok(h) => h, + Err(e) => { + let _ = CloseHandle(stdin_read); + let _ = CloseHandle(stdin_write); + let _ = CloseHandle(stdout_read); + let _ = CloseHandle(stdout_write); + return Err(PtyError::CreateConsole(e.to_string())); + } + }; + + // The pseudo console now owns stdin_read and stdout_write. + let _ = CloseHandle(stdin_read); + let _ = CloseHandle(stdout_write); + + // ── Build STARTUPINFOEXW with the pseudo console attribute ──────────── + let mut attr_list_size = 0usize; + // First call: get required buffer size (expectedly "fails"). + let _ = InitializeProcThreadAttributeList( + LPPROC_THREAD_ATTRIBUTE_LIST::default(), + 1, + 0, + &mut attr_list_size, + ); + + let mut attr_buf = vec![0u8; attr_list_size]; + let attr_list = LPPROC_THREAD_ATTRIBUTE_LIST(attr_buf.as_mut_ptr() as *mut _); + + InitializeProcThreadAttributeList(attr_list, 1, 0, &mut attr_list_size).map_err(|e| { + PtyError::SpawnProcess(format!("InitializeProcThreadAttributeList: {e}")) + })?; + + // Attach pseudo console to the attribute list. + // + // For PROC_THREAD_ATTR_PSEUDOCONSOLE the Windows kernel uses lpValue + // as the HPCON handle VALUE directly (it does not dereference it). + // Pass hpc.0 (the raw ConHost handle) — NOT &hpc (address of our + // local variable). Passing the wrong value here is what causes + // STATUS_DLL_INIT_FAILED (0xC0000142) in the child: the child's + // kernel32 console init receives a bogus "stack address" as the HPCON, + // fails to connect to ConHost, and DllMain returns FALSE. + // Reference: every ConPTY C++ sample (including Microsoft's own) and + // Rust implementations (alacritty, wezterm) pass the handle value. + UpdateProcThreadAttribute( + attr_list, + 0, + PROC_THREAD_ATTR_PSEUDOCONSOLE, + Some(hpc.0 as *const std::ffi::c_void), + std::mem::size_of::(), + None, + None, + ) + .map_err(|e| PtyError::SpawnProcess(format!("UpdateProcThreadAttribute: {e}")))?; + + let mut si_ex: STARTUPINFOEXW = std::mem::zeroed(); + si_ex.StartupInfo.cb = std::mem::size_of::() as u32; + si_ex.lpAttributeList = attr_list; + + // Explicitly tell Windows NOT to inherit the parent's standard handles. + // Without this, the child's DLL init connects to the parent's console + // handles at the same time the ConPTY's conhost.exe is connecting, which + // causes STATUS_DLL_INIT_FAILED (0xC0000142). The ConPTY attribute + // (PROC_THREAD_ATTR_PSEUDOCONSOLE) handles all console I/O independently. + si_ex.StartupInfo.dwFlags = STARTF_USESTDHANDLES; + si_ex.StartupInfo.hStdInput = INVALID_HANDLE_VALUE; + si_ex.StartupInfo.hStdOutput = INVALID_HANDLE_VALUE; + si_ex.StartupInfo.hStdError = INVALID_HANDLE_VALUE; + + // ── Resolve shell command line ──────────────────────────────────────── + let shell_path = match shell { + Some(s) => { + log::info!("shell: using config override {:?}", s); + s.to_string() + } + None => detect_shell().unwrap_or_else(|| "cmd.exe".to_string()), + }; + + let mut command_line: Vec = shell_path + .encode_utf16() + .chain(std::iter::once(0u16)) + .collect(); + + // ── Spawn the shell process ─────────────────────────────────────────── + let mut pi: PROCESS_INFORMATION = std::mem::zeroed(); + let creation_flags = PROCESS_CREATION_FLAGS(EXTENDED_STARTUPINFO_PRESENT.0); + + log::info!("spawning shell: {:?} cols={cols} rows={rows}", shell_path); + + if let Err(e) = CreateProcessW( + None, + PWSTR(command_line.as_mut_ptr()), + None::<*const SECURITY_ATTRIBUTES>, + None::<*const SECURITY_ATTRIBUTES>, + BOOL(0), + creation_flags, + None, + None, + &si_ex.StartupInfo as *const STARTUPINFOW, + &mut pi, + ) { + DeleteProcThreadAttributeList(attr_list); + let _ = CloseHandle(stdin_write); + let _ = CloseHandle(stdout_read); + return Err(PtyError::SpawnProcess(format!("CreateProcessW: {e}"))); + } + + DeleteProcThreadAttributeList(attr_list); + // We don't need the thread handle. + let _ = CloseHandle(pi.hThread); + + let process_id = pi.dwProcessId; + let process_handle = pi.hProcess; + + // ── Start background reader thread ──────────────────────────────────── + let (tx, rx) = channel::>(); + let stdout_send = SendHandle(stdout_read); + + thread::Builder::new() + .name("winiterm-pty-reader".to_string()) + .spawn(move || { + // Transfer ownership of stdout_read into this thread. + // std::fs::File takes ownership and closes the handle on drop. + let mut file = std::fs::File::from_raw_handle(stdout_send.get().0 as _); + pty_read_loop(&mut file, tx); + // file dropped here → handle closed + }) + .map_err(|e| PtyError::Io(e.to_string()))?; + + Ok(Self { + hpc: PseudoConsole(hpc), + stdin_write: SendHandle(stdin_write), + process_handle: SendHandle(process_handle), + process_id, + data_receiver: Some(rx), + }) + } + + /// Write `data` to the child process's stdin. + /// + /// Uses `std::io::Write` on a non-owning `File` view of the handle, + /// then calls `forget` to prevent the `File` from closing it. + pub fn write(&self, data: &[u8]) -> Result<(), PtyError> { + if data.is_empty() { + return Ok(()); + } + // SAFETY: stdin_write is a valid writable handle for the lifetime of Pty. + // We use forget() to prevent File::drop from closing it. + let mut file = unsafe { std::fs::File::from_raw_handle(self.stdin_write.get().0 as _) }; + let result = file + .write_all(data) + .map_err(|e| PtyError::Io(e.to_string())); + std::mem::forget(file); + result + } + + /// Resize the pseudo console to `cols × rows` cells. + pub fn resize(&self, cols: u16, rows: u16) -> Result<(), PtyError> { + unsafe { + ResizePseudoConsole( + self.hpc.0, + COORD { + X: cols as i16, + Y: rows as i16, + }, + ) + .map_err(|e| PtyError::Resize(e.to_string()))?; + } + Ok(()) + } + + /// Returns `true` if the child process has exited. + pub fn child_exited(&self) -> bool { + let mut exit_code = 0u32; + unsafe { + match GetExitCodeProcess(self.process_handle.get(), &mut exit_code) { + Ok(()) => { + if exit_code != STILL_ACTIVE { + log::info!("child_exited: exit_code={exit_code}"); + true + } else { + false + } + } + Err(e) => { + log::info!("child_exited: GetExitCodeProcess error: {e}"); + true + } + } + } + } + + /// Take the data receiver out of this `Pty`. + pub fn take_receiver(&mut self) -> Option>> { + self.data_receiver.take() + } +} + +impl Drop for Pty { + fn drop(&mut self) { + unsafe { + let _ = TerminateProcess(self.process_handle.get(), 0); + let _ = CloseHandle(self.stdin_write.get()); + let _ = CloseHandle(self.process_handle.get()); + // hpc is closed by PseudoConsole::drop. + } + } +} + +// ─── Internal helpers ──────────────────────────────────────────────────────── + +/// Blocking read loop — runs on the dedicated PTY reader thread. +fn pty_read_loop(file: &mut std::fs::File, sender: Sender>) { + let mut buf = vec![0u8; 8192]; + loop { + match file.read(&mut buf) { + Ok(0) | Err(_) => break, + Ok(n) => { + if sender.send(buf[..n].to_vec()).is_err() { + break; // Receiver was dropped — stop reading. + } + } + } + } +} + +/// Auto-detect the preferred shell. +/// +/// `cmd.exe` is always preferred: it is a plain Win32 binary with no complex +/// DLL initialisation. `pwsh.exe` and `powershell.exe` load the .NET / CLR +/// runtime during DLL init, which can fail with `STATUS_DLL_INIT_FAILED` +/// (0xC0000142) inside a ConPTY. Users who prefer PowerShell can set +/// `shell = "pwsh.exe"` (or the full path) in their `config.lua`. +/// +/// We look for `cmd.exe` via its guaranteed path (`%SystemRoot%\System32\`) +/// rather than `where.exe` so that the check succeeds even in minimal PATH +/// environments and even after `FreeConsole()` detaches the parent console. +fn detect_shell() -> Option { + // Primary: guaranteed path for cmd.exe. + if let Ok(root) = std::env::var("SystemRoot") { + let path = format!(r"{root}\System32\cmd.exe"); + if std::path::Path::new(&path).exists() { + log::info!("detect_shell: using {}", path); + return Some(path); + } + } + + // Fallback: PATH-based search for Win32 shells only (no pwsh / .NET). + for candidate in &["cmd.exe", "powershell.exe"] { + if std::process::Command::new("where") + .arg(candidate) + .output() + .map(|o| o.status.success()) + .unwrap_or(false) + { + log::info!("detect_shell: found {} via PATH", candidate); + return Some(candidate.to_string()); + } + } + + log::warn!("detect_shell: all candidates failed, defaulting to cmd.exe"); + Some("cmd.exe".to_string()) +} diff --git a/winiterm/src/renderer/glyph.rs b/winiterm/src/renderer/glyph.rs new file mode 100644 index 0000000..9bcc604 --- /dev/null +++ b/winiterm/src/renderer/glyph.rs @@ -0,0 +1,187 @@ +//! Glyph atlas: font discovery, rasterisation, and UV lookup. +//! +//! Uses [`fontdb`] to locate the best available monospace font and [`swash`] +//! to rasterise glyphs into a single R8-greyscale texture atlas. +//! +//! Layout: the atlas is a uniform grid of `cell_width × cell_height` slots. +//! Each slot holds exactly one character; unused slots are zeroed. + +use std::collections::HashMap; + +// ─── Atlas ─────────────────────────────────────────────────────────────────── + +/// Greyscale (R8) glyph texture atlas for a monospace font. +pub struct GlyphAtlas { + /// Raw R8 pixel data (one byte per pixel). + pub data: Vec, + /// Atlas dimensions in pixels. + pub width: u32, + pub height: u32, + /// Dimensions of each cell slot. + pub cell_width: u32, + pub cell_height: u32, + /// Distance from the top of a cell to the text baseline (pixels). + pub baseline: u32, + /// Number of cell slots per atlas row. + cols_per_row: u32, + /// Char → (slot_column, slot_row). + entries: HashMap, + /// Index of the next free slot (for future on-demand rasterisation). + next_slot: u32, +} + +impl GlyphAtlas { + const ATLAS_W: u32 = 1024; + const ATLAS_H: u32 = 1024; + + /// Build an atlas at `font_size` physical pixels using the best available + /// monospace system font. + pub fn build(font_size: f32) -> Self { + let mut db = fontdb::Database::new(); + db.load_system_fonts(); + + let query = fontdb::Query { + families: &[ + fontdb::Family::Name("Cascadia Code"), + fontdb::Family::Name("Cascadia Mono"), + fontdb::Family::Name("Consolas"), + fontdb::Family::Name("Courier New"), + fontdb::Family::Monospace, + ], + ..Default::default() + }; + + let id = db + .query(&query) + .expect("no monospace font found on this system"); + db.with_face_data(id, |data, face_index| { + Self::build_from_data(data, face_index as usize, font_size) + }) + .expect("font face data unavailable") + } + + fn build_from_data(font_data: &[u8], face_index: usize, font_size: f32) -> Self { + use swash::scale::{Render, ScaleContext, Source}; + use swash::zeno::Format; + + let font = + swash::FontRef::from_index(font_data, face_index).expect("invalid font face index"); + + // ── Metrics ────────────────────────────────────────────────────────── + let metrics = font.metrics(&[]).scale(font_size); + let cell_width = metrics.average_width.ceil().max(1.0) as u32; + let ascent = metrics.ascent.ceil() as u32; + // `descent` is typically negative; take the magnitude. + let descent = metrics.descent.abs().ceil() as u32; + let cell_height = ascent + descent; + let baseline = ascent; + + let cols_per_row = Self::ATLAS_W / cell_width; + let mut data = vec![0u8; (Self::ATLAS_W * Self::ATLAS_H) as usize]; + let mut entries = HashMap::new(); + let mut next_slot = 0u32; + + // ── Unicode ranges to pre-rasterise ────────────────────────────────── + // + // These ranges cover everything a modern terminal is likely to render: + // • Basic Latin (printable ASCII) + // • Latin-1 Supplement + Extended-A (accented European characters) + // • Box Drawing, Block Elements, Geometric Shapes (TUI apps: htop, + // vim, tmux, ncurses borders) + // • Powerline Symbols (oh-my-zsh, starship, powerline prompts) + const RASTERIZE_RANGES: &[(u32, u32)] = &[ + (0x0020, 0x007E), // Basic Latin (printable ASCII) + (0x00A0, 0x00FF), // Latin-1 Supplement + (0x0100, 0x017F), // Latin Extended-A + (0x2500, 0x257F), // Box Drawing + (0x2580, 0x259F), // Block Elements + (0x25A0, 0x25FF), // Geometric Shapes + (0xE0A0, 0xE0D4), // Powerline Symbols + ]; + + let charmap = font.charmap(); + let mut ctx = ScaleContext::new(); + let mut scaler = ctx.builder(font).size(font_size).hint(true).build(); + + for &(range_start, range_end) in RASTERIZE_RANGES { + for codepoint in range_start..=range_end { + let ch = match char::from_u32(codepoint) { + Some(c) => c, + None => continue, + }; + let glyph_id = charmap.map(ch); + + let slot_col = next_slot % cols_per_row; + let slot_row = next_slot / cols_per_row; + entries.insert(ch, (slot_col, slot_row)); + next_slot += 1; + + // Abort if we've run out of atlas space (should never happen + // with the current ranges at any reasonable font size). + if (slot_row + 1) * cell_height > Self::ATLAS_H { + break; + } + + let image = Render::new(&[Source::Outline]) + .format(Format::Alpha) + .render(&mut scaler, glyph_id); + + if let Some(img) = image { + let p = &img.placement; + // Pixel position of the glyph image's top-left corner within + // the atlas (accounts for left-bearing and baseline offset). + let origin_x = (slot_col * cell_width) as i32 + p.left; + let origin_y = (slot_row * cell_height) as i32 + (baseline as i32 - p.top); + + for gy in 0..p.height as i32 { + for gx in 0..p.width as i32 { + let ax = origin_x + gx; + let ay = origin_y + gy; + if ax >= 0 + && ay >= 0 + && (ax as u32) < Self::ATLAS_W + && (ay as u32) < Self::ATLAS_H + { + let src = (gy * p.width as i32 + gx) as usize; + let dst = (ay as u32 * Self::ATLAS_W + ax as u32) as usize; + if src < img.data.len() { + data[dst] = img.data[src]; + } + } + } + } + } + } + } + + Self { + data, + width: Self::ATLAS_W, + height: Self::ATLAS_H, + cell_width, + cell_height, + baseline, + cols_per_row, + entries, + next_slot, + } + } + + /// Return `(u_min, v_min, u_size, v_size)` for `ch` in normalised UV space. + /// + /// Falls back to the space character for any unmapped codepoint. + pub fn glyph_uv(&self, ch: char) -> (f32, f32, f32, f32) { + let (col, row) = self + .entries + .get(&ch) + .or_else(|| self.entries.get(&' ')) + .copied() + .unwrap_or((0, 0)); + + let u = (col * self.cell_width) as f32 / self.width as f32; + let v = (row * self.cell_height) as f32 / self.height as f32; + let uw = self.cell_width as f32 / self.width as f32; + let vh = self.cell_height as f32 / self.height as f32; + (u, v, uw, vh) + } +} diff --git a/winiterm/src/renderer/mod.rs b/winiterm/src/renderer/mod.rs new file mode 100644 index 0000000..8b0d287 --- /dev/null +++ b/winiterm/src/renderer/mod.rs @@ -0,0 +1,1061 @@ +//! GPU renderer for the terminal grid (Phase 4 rewrite). +//! +//! Architecture is unchanged from Phase 3: +//! * One **vertex buffer** — 4 unit-quad corners (`TriangleStrip`). +//! * One **instance buffer** — one [`CellInstance`] per rendered cell. +//! * A single draw call per frame. +//! +//! New in Phase 4: +//! * Multi-pane rendering via [`Renderer::render_workspace`]. +//! * Colour scheme support: `set_color_scheme` updates the default fg/bg and +//! the 16-colour ANSI palette at runtime. +//! * Tab-bar rendering at the top of the window. +//! * Inactive-pane dimming (× 0.6 on foreground, × 0.7 on background). + +mod glyph; +pub use glyph::GlyphAtlas; + +use std::collections::HashMap; +use std::sync::Arc; + +use crate::colorscheme::ColorScheme; +use crate::split::Rect; +use crate::terminal::{Cell, CellAttrs, Color, Terminal, TerminalModes}; +use bytemuck::{Pod, Zeroable}; +use wgpu::util::DeviceExt; +use winit::window::Window; + +// ─── GPU data types ─────────────────────────────────────────────────────────── + +#[repr(C)] +#[derive(Copy, Clone, Pod, Zeroable)] +struct QuadVertex { + pos: [f32; 2], +} + +const QUAD_VERTS: &[QuadVertex] = &[ + QuadVertex { pos: [0.0, 0.0] }, + QuadVertex { pos: [1.0, 0.0] }, + QuadVertex { pos: [0.0, 1.0] }, + QuadVertex { pos: [1.0, 1.0] }, +]; + +/// Per-cell GPU instance (68 bytes: 64 bytes data + 4 bytes attrs). +#[repr(C)] +#[derive(Copy, Clone, Pod, Zeroable)] +struct CellInstance { + cell_pos: [f32; 2], // offset 0 + cell_size: [f32; 2], // offset 8 + uv_offset: [f32; 2], // offset 16 + uv_size: [f32; 2], // offset 24 + fg_color: [f32; 4], // offset 32 + bg_color: [f32; 4], // offset 48 + attrs: u32, // offset 64 — CellAttrs bits (underline, strikethrough, …) + _pad: u32, // offset 68 — pad to 72 bytes (4-byte aligned) +} + +const MAX_INSTANCES: usize = 512 * 200 + 64; + +/// Maximum number of simultaneous Kitty image placements rendered per frame. +const MAX_KITTY_INSTANCES: usize = 256; + +// ─── Kitty GPU texture ──────────────────────────────────────────────────────── + +/// A single Kitty image uploaded to the GPU as an `Rgba8Unorm` texture. +struct KittyGpuTexture { + /// Keeps the underlying GPU texture alive for as long as the bind group. + _texture: wgpu::Texture, + /// Bind group that wraps this image's texture (same layout as the glyph atlas). + bind_group: wgpu::BindGroup, +} + +// ─── Renderer ───────────────────────────────────────────────────────────────── + +pub struct Renderer { + device: Arc, + queue: Arc, + surface: wgpu::Surface<'static>, + config: wgpu::SurfaceConfiguration, + pipeline: wgpu::RenderPipeline, + vertex_buffer: wgpu::Buffer, + instance_buffer: wgpu::Buffer, + uniform_buffer: wgpu::Buffer, + bind_group: wgpu::BindGroup, + _atlas_texture: wgpu::Texture, + atlas: GlyphAtlas, + pub cell_width: f32, + pub cell_height: f32, + pub cols: u16, + pub rows: u16, + // ── Colour scheme ────────────────────────────────────────────────────── + pub scheme_fg: [f32; 4], + pub scheme_bg: [f32; 4], + /// Cursor block colour from the active scheme. + pub scheme_cursor: [f32; 4], + /// The 16 ANSI colours from the active scheme (used for Color::Indexed 0–15). + scheme_ansi: [[u8; 3]; 16], + // ── Frame timing (render_timing feature) ─────────────────────────────── + #[cfg(feature = "render_timing")] + frame_timer_last: std::time::Instant, + #[cfg(feature = "render_timing")] + frame_timer_count: u32, + #[cfg(feature = "render_timing")] + pub fps: f32, + // ── Kitty Graphics Protocol GPU resources ────────────────────────────── + /// Shared bind group layout reused when creating per-image bind groups. + bgl: wgpu::BindGroupLayout, + /// Shared sampler reused in per-image bind groups. + atlas_sampler: wgpu::Sampler, + /// Pipeline for rendering Kitty RGBA images (same vertex layout, `fs_kitty_main`). + kitty_pipeline: wgpu::RenderPipeline, + /// Instance buffer for Kitty image placements (MAX_KITTY_INSTANCES slots). + kitty_instance_buffer: wgpu::Buffer, + /// Per-image GPU textures, keyed by Kitty image ID. + kitty_textures: HashMap, +} + +impl Renderer { + /// Create the renderer. `base_font_size` is in logical pixels and is + /// scaled by the display's DPI factor. + pub fn new(window: Arc, base_font_size: f32) -> Self { + pollster::block_on(Self::init(window, base_font_size)) + } + + async fn init(window: Arc, base_font_size: f32) -> Self { + let size = window.inner_size(); + let scale = window.scale_factor() as f32; + let font_size = (base_font_size * scale).round().max(8.0); + + // ── wgpu instance ───────────────────────────────────────────────────── + let instance = wgpu::Instance::new(wgpu::InstanceDescriptor { + backends: wgpu::Backends::all(), + ..Default::default() + }); + + let surface = instance + .create_surface(window.clone()) + .expect("create_surface failed"); + + let adapter = instance + .request_adapter(&wgpu::RequestAdapterOptions { + power_preference: wgpu::PowerPreference::HighPerformance, + compatible_surface: Some(&surface), + force_fallback_adapter: false, + }) + .await + .expect("no suitable GPU adapter found"); + + let (device, queue) = adapter + .request_device( + &wgpu::DeviceDescriptor { + label: Some("winiterm"), + ..Default::default() + }, + None, + ) + .await + .expect("request_device failed"); + + let device = Arc::new(device); + let queue = Arc::new(queue); + + let caps = surface.get_capabilities(&adapter); + let format = caps + .formats + .iter() + .find(|f| !f.is_srgb()) + .copied() + .unwrap_or(caps.formats[0]); + + let config = wgpu::SurfaceConfiguration { + usage: wgpu::TextureUsages::RENDER_ATTACHMENT, + format, + width: size.width.max(1), + height: size.height.max(1), + present_mode: wgpu::PresentMode::AutoVsync, + desired_maximum_frame_latency: 2, + alpha_mode: caps.alpha_modes[0], + view_formats: vec![], + }; + surface.configure(&device, &config); + + // ── Glyph atlas ─────────────────────────────────────────────────────── + let atlas = GlyphAtlas::build(font_size); + let cell_width = atlas.cell_width as f32; + let cell_height = atlas.cell_height as f32; + let cols = (size.width as f32 / cell_width).floor() as u16; + let rows = (size.height as f32 / cell_height).floor() as u16; + + let atlas_texture = device.create_texture(&wgpu::TextureDescriptor { + label: Some("glyph_atlas"), + size: wgpu::Extent3d { + width: atlas.width, + height: atlas.height, + depth_or_array_layers: 1, + }, + mip_level_count: 1, + sample_count: 1, + dimension: wgpu::TextureDimension::D2, + format: wgpu::TextureFormat::R8Unorm, + usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST, + view_formats: &[], + }); + queue.write_texture( + wgpu::ImageCopyTexture { + texture: &atlas_texture, + mip_level: 0, + origin: wgpu::Origin3d::ZERO, + aspect: wgpu::TextureAspect::All, + }, + &atlas.data, + wgpu::ImageDataLayout { + offset: 0, + bytes_per_row: Some(atlas.width), + rows_per_image: Some(atlas.height), + }, + wgpu::Extent3d { + width: atlas.width, + height: atlas.height, + depth_or_array_layers: 1, + }, + ); + + let atlas_view = atlas_texture.create_view(&wgpu::TextureViewDescriptor::default()); + let atlas_sampler = device.create_sampler(&wgpu::SamplerDescriptor { + label: Some("atlas_sampler"), + mag_filter: wgpu::FilterMode::Linear, + min_filter: wgpu::FilterMode::Linear, + ..Default::default() + }); + + // ── Uniform buffer (screen_size vec2 + 8 bytes pad = 16 bytes) ──────── + let screen_data: [f32; 4] = [size.width as f32, size.height as f32, 0.0, 0.0]; + let uniform_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { + label: Some("uniforms"), + contents: bytemuck::cast_slice(&screen_data), + usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, + }); + + // ── Bind group ──────────────────────────────────────────────────────── + let bgl = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: Some("terminal_bgl"), + entries: &[ + wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::VERTEX, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Uniform, + has_dynamic_offset: false, + min_binding_size: None, + }, + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 1, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Texture { + multisampled: false, + view_dimension: wgpu::TextureViewDimension::D2, + sample_type: wgpu::TextureSampleType::Float { filterable: true }, + }, + count: None, + }, + wgpu::BindGroupLayoutEntry { + binding: 2, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), + count: None, + }, + ], + }); + + let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("terminal_bg"), + layout: &bgl, + entries: &[ + wgpu::BindGroupEntry { + binding: 0, + resource: uniform_buffer.as_entire_binding(), + }, + wgpu::BindGroupEntry { + binding: 1, + resource: wgpu::BindingResource::TextureView(&atlas_view), + }, + wgpu::BindGroupEntry { + binding: 2, + resource: wgpu::BindingResource::Sampler(&atlas_sampler), + }, + ], + }); + + let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor { + label: Some("terminal_shader"), + source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed(include_str!( + "shader.wgsl" + ))), + }); + + let pipeline_layout = + device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + label: Some("terminal_pipeline_layout"), + bind_group_layouts: &[&bgl], + push_constant_ranges: &[], + }); + + // ── Glyph pipeline ──────────────────────────────────────────────────── + let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label: Some("terminal_pipeline"), + layout: Some(&pipeline_layout), + vertex: wgpu::VertexState { + module: &shader, + entry_point: "vs_main", + buffers: &[ + wgpu::VertexBufferLayout { + array_stride: std::mem::size_of::() as u64, + step_mode: wgpu::VertexStepMode::Vertex, + attributes: &[wgpu::VertexAttribute { + offset: 0, + shader_location: 0, + format: wgpu::VertexFormat::Float32x2, + }], + }, + wgpu::VertexBufferLayout { + array_stride: std::mem::size_of::() as u64, + step_mode: wgpu::VertexStepMode::Instance, + attributes: &[ + wgpu::VertexAttribute { offset: 0, shader_location: 1, format: wgpu::VertexFormat::Float32x2 }, + wgpu::VertexAttribute { offset: 8, shader_location: 2, format: wgpu::VertexFormat::Float32x2 }, + wgpu::VertexAttribute { offset: 16, shader_location: 3, format: wgpu::VertexFormat::Float32x2 }, + wgpu::VertexAttribute { offset: 24, shader_location: 4, format: wgpu::VertexFormat::Float32x2 }, + wgpu::VertexAttribute { offset: 32, shader_location: 5, format: wgpu::VertexFormat::Float32x4 }, + wgpu::VertexAttribute { offset: 48, shader_location: 6, format: wgpu::VertexFormat::Float32x4 }, + wgpu::VertexAttribute { offset: 64, shader_location: 7, format: wgpu::VertexFormat::Uint32 }, + ], + }, + ], + compilation_options: Default::default(), + }, + fragment: Some(wgpu::FragmentState { + module: &shader, + entry_point: "fs_main", + targets: &[Some(wgpu::ColorTargetState { + format, + blend: Some(wgpu::BlendState::ALPHA_BLENDING), + write_mask: wgpu::ColorWrites::ALL, + })], + compilation_options: Default::default(), + }), + primitive: wgpu::PrimitiveState { + topology: wgpu::PrimitiveTopology::TriangleStrip, + ..Default::default() + }, + depth_stencil: None, + multisample: wgpu::MultisampleState::default(), + multiview: None, + cache: None, + }); + + // ── Kitty image pipeline (same vertex layout, RGBA fragment) ────────── + let kitty_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label: Some("kitty_pipeline"), + layout: Some(&pipeline_layout), + vertex: wgpu::VertexState { + module: &shader, + entry_point: "vs_main", + buffers: &[ + wgpu::VertexBufferLayout { + array_stride: std::mem::size_of::() as u64, + step_mode: wgpu::VertexStepMode::Vertex, + attributes: &[wgpu::VertexAttribute { + offset: 0, + shader_location: 0, + format: wgpu::VertexFormat::Float32x2, + }], + }, + wgpu::VertexBufferLayout { + array_stride: std::mem::size_of::() as u64, + step_mode: wgpu::VertexStepMode::Instance, + attributes: &[ + wgpu::VertexAttribute { offset: 0, shader_location: 1, format: wgpu::VertexFormat::Float32x2 }, + wgpu::VertexAttribute { offset: 8, shader_location: 2, format: wgpu::VertexFormat::Float32x2 }, + wgpu::VertexAttribute { offset: 16, shader_location: 3, format: wgpu::VertexFormat::Float32x2 }, + wgpu::VertexAttribute { offset: 24, shader_location: 4, format: wgpu::VertexFormat::Float32x2 }, + wgpu::VertexAttribute { offset: 32, shader_location: 5, format: wgpu::VertexFormat::Float32x4 }, + wgpu::VertexAttribute { offset: 48, shader_location: 6, format: wgpu::VertexFormat::Float32x4 }, + wgpu::VertexAttribute { offset: 64, shader_location: 7, format: wgpu::VertexFormat::Uint32 }, + ], + }, + ], + compilation_options: Default::default(), + }, + fragment: Some(wgpu::FragmentState { + module: &shader, + entry_point: "fs_kitty_main", + targets: &[Some(wgpu::ColorTargetState { + format, + blend: Some(wgpu::BlendState::ALPHA_BLENDING), + write_mask: wgpu::ColorWrites::ALL, + })], + compilation_options: Default::default(), + }), + primitive: wgpu::PrimitiveState { + topology: wgpu::PrimitiveTopology::TriangleStrip, + ..Default::default() + }, + depth_stencil: None, + multisample: wgpu::MultisampleState::default(), + multiview: None, + cache: None, + }); + + let vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { + label: Some("quad_verts"), + contents: bytemuck::cast_slice(QUAD_VERTS), + usage: wgpu::BufferUsages::VERTEX, + }); + + let instance_buffer = device.create_buffer(&wgpu::BufferDescriptor { + label: Some("cell_instances"), + size: (MAX_INSTANCES * std::mem::size_of::()) as u64, + usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST, + mapped_at_creation: false, + }); + + let kitty_instance_buffer = device.create_buffer(&wgpu::BufferDescriptor { + label: Some("kitty_instances"), + size: (MAX_KITTY_INSTANCES * std::mem::size_of::()) as u64, + usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST, + mapped_at_creation: false, + }); + + // Default to Monokai until the app calls set_color_scheme. + let default_scheme = &crate::colorscheme::MONOKAI; + let scheme_fg = ColorScheme::to_rgba(default_scheme.foreground); + let scheme_bg = ColorScheme::to_rgba(default_scheme.background); + let scheme_cursor = ColorScheme::to_rgba(default_scheme.cursor); + let scheme_ansi = default_scheme.ansi; + + Self { + device, + queue, + surface, + config, + pipeline, + vertex_buffer, + instance_buffer, + uniform_buffer, + bind_group, + _atlas_texture: atlas_texture, + atlas, + cell_width, + cell_height, + cols, + rows, + scheme_fg, + scheme_bg, + scheme_cursor, + scheme_ansi, + #[cfg(feature = "render_timing")] + frame_timer_last: std::time::Instant::now(), + #[cfg(feature = "render_timing")] + frame_timer_count: 0, + #[cfg(feature = "render_timing")] + fps: 0.0, + bgl, + atlas_sampler, + kitty_pipeline, + kitty_instance_buffer, + kitty_textures: HashMap::new(), + } + } + + // ── Public interface ────────────────────────────────────────────────────── + + /// Terminal dimensions derived from the current window size and cell size. + pub fn terminal_size(&self) -> (u16, u16) { + (self.cols, self.rows) + } + + /// Surface (swapchain) size in physical pixels. + pub fn surface_size(&self) -> (u32, u32) { + (self.config.width, self.config.height) + } + + /// Handle a window resize. + pub fn resize(&mut self, width: u32, height: u32) { + if width == 0 || height == 0 { + return; + } + self.config.width = width; + self.config.height = height; + self.surface.configure(&self.device, &self.config); + self.cols = (width as f32 / self.cell_width).floor() as u16; + self.rows = (height as f32 / self.cell_height).floor() as u16; + let data: [f32; 4] = [width as f32, height as f32, 0.0, 0.0]; + self.queue + .write_buffer(&self.uniform_buffer, 0, bytemuck::cast_slice(&data)); + } + + /// Update the active colour scheme. + pub fn set_color_scheme(&mut self, scheme: &ColorScheme) { + self.scheme_fg = ColorScheme::to_rgba(scheme.foreground); + self.scheme_bg = ColorScheme::to_rgba(scheme.background); + self.scheme_cursor = ColorScheme::to_rgba(scheme.cursor); + self.scheme_ansi = scheme.ansi; + } + + // ── Single-terminal render (backward compat / bench) ────────────────────── + + /// Render a single terminal occupying the full window. + pub fn render(&mut self, terminal: &Terminal) { + let mut instances = Vec::with_capacity(terminal.cols * terminal.rows + 1); + let rect = Rect::new(0.0, 0.0, self.config.width as f32, self.config.height as f32); + self.push_pane_cells(&mut instances, terminal, &rect, true, 0); + self.submit_frame(&instances, &[]); + } + + // ── Workspace render (multi-pane + tab bar) ─────────────────────────────── + + /// Render the full workspace: tab bar + all panes in the active tab. + pub fn render_workspace(&mut self, workspace: &mut crate::workspace::Workspace) { + // E1: Evict GPU textures for images deleted by `a=d` since the last frame. + for pane_id in workspace.current_tab_panes() { + if let Some(pane) = workspace.pane_mut(pane_id) { + for id in pane.terminal.kitty_deleted_ids.drain(..) { + self.kitty_textures.remove(&id); + log::debug!("evicted Kitty GPU texture id={id}"); + } + } + } + + // Upload any Kitty images that arrived since the last frame. + self.ensure_kitty_textures(workspace); + + let w = self.config.width as f32; + let h = self.config.height as f32; + + let mut instances = Vec::with_capacity(MAX_INSTANCES / 2); + + // 1. Tab bar (occupies y=0 … y=cell_height). + self.push_tab_bar(&mut instances, workspace); + + // 2. All panes in the active tab. + let rects = workspace.layout(w, h, self.cell_height); + let active_id = workspace.active_pane_id(); + + for pane_id in workspace.current_tab_panes() { + if let (Some(pane), Some(rect)) = + (workspace.pane(pane_id), rects.get(&pane_id)) + { + self.push_pane_cells( + &mut instances, + &pane.terminal, + rect, + pane_id == active_id, + pane.scroll_offset, + ); + } + } + + instances.truncate(MAX_INSTANCES); + + // FPS overlay (only compiled when the `render_timing` feature is on). + #[cfg(feature = "render_timing")] + self.push_fps_overlay(&mut instances); + + // 3. Collect Kitty image placements for this frame. + let mut kitty: Vec<(u32, CellInstance)> = Vec::new(); + for pane_id in workspace.current_tab_panes() { + if let (Some(pane), Some(rect)) = + (workspace.pane(pane_id), rects.get(&pane_id)) + { + for placement in &pane.terminal.kitty_placements { + let x = rect.x + placement.col as f32 * self.cell_width; + let y = rect.y + placement.row as f32 * self.cell_height; + kitty.push(( + placement.image_id, + CellInstance { + cell_pos: [x, y], + cell_size: [placement.pixel_width as f32, placement.pixel_height as f32], + uv_offset: [0.0, 0.0], + uv_size: [1.0, 1.0], + fg_color: [0.0; 4], + bg_color: [0.0; 4], + attrs: 0, + _pad: 0, + }, + )); + } + } + } + // Sort so we can batch draw calls by image_id. + kitty.sort_unstable_by_key(|(id, _)| *id); + kitty.truncate(MAX_KITTY_INSTANCES); + + self.submit_frame(&instances, &kitty); + } + + // ── Private: GPU submit ─────────────────────────────────────────────────── + + /// Submit one frame. + /// + /// `instances` are the glyph/cell quads. + /// `kitty` is a list of `(image_id, CellInstance)` pairs, **sorted by + /// image_id**, describing Kitty image placements to render on top. + fn submit_frame(&mut self, instances: &[CellInstance], kitty: &[(u32, CellInstance)]) { + let output = match self.surface.get_current_texture() { + Ok(t) => t, + Err(wgpu::SurfaceError::Lost | wgpu::SurfaceError::Outdated) => { + self.surface.configure(&self.device, &self.config); + return; + } + Err(e) => { + log::warn!("surface error: {e}"); + return; + } + }; + + let view = output + .texture + .create_view(&wgpu::TextureViewDescriptor::default()); + + // ── Upload cell instances ───────────────────────────────────────────── + if !instances.is_empty() { + self.queue.write_buffer( + &self.instance_buffer, + 0, + bytemuck::cast_slice(instances), + ); + } + + // ── Upload Kitty instances ──────────────────────────────────────────── + if !kitty.is_empty() { + let kitty_insts: Vec = kitty.iter().map(|(_, i)| *i).collect(); + self.queue.write_buffer( + &self.kitty_instance_buffer, + 0, + bytemuck::cast_slice(&kitty_insts), + ); + } + + let bg = self.scheme_bg; + let mut encoder = + self.device + .create_command_encoder(&wgpu::CommandEncoderDescriptor { + label: Some("frame_encoder"), + }); + { + let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label: Some("terminal_pass"), + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view: &view, + resolve_target: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Clear(wgpu::Color { + r: bg[0] as f64, + g: bg[1] as f64, + b: bg[2] as f64, + a: 1.0, + }), + store: wgpu::StoreOp::Store, + }, + })], + depth_stencil_attachment: None, + timestamp_writes: None, + occlusion_query_set: None, + }); + + // ── Glyph / cell draw ───────────────────────────────────────────── + pass.set_pipeline(&self.pipeline); + pass.set_bind_group(0, &self.bind_group, &[]); + pass.set_vertex_buffer(0, self.vertex_buffer.slice(..)); + pass.set_vertex_buffer(1, self.instance_buffer.slice(..)); + pass.draw(0..4, 0..instances.len() as u32); + + // ── Kitty image draws (grouped by image_id) ─────────────────────── + if !kitty.is_empty() { + pass.set_pipeline(&self.kitty_pipeline); + pass.set_vertex_buffer(1, self.kitty_instance_buffer.slice(..)); + + let mut offset = 0u32; + let mut i = 0; + while i < kitty.len() { + let image_id = kitty[i].0; + let group_start = offset; + // Count how many consecutive entries share this image_id. + while i < kitty.len() && kitty[i].0 == image_id { + i += 1; + offset += 1; + } + // Draw this group with the image's bind group, if uploaded. + if let Some(gpu_tex) = self.kitty_textures.get(&image_id) { + pass.set_bind_group(0, &gpu_tex.bind_group, &[]); + pass.draw(0..4, group_start..offset); + } + } + } + } + + self.queue.submit(std::iter::once(encoder.finish())); + output.present(); + + // Update FPS counter. + #[cfg(feature = "render_timing")] + { + self.frame_timer_count += 1; + let elapsed = self.frame_timer_last.elapsed().as_secs_f32(); + if elapsed >= 1.0 { + self.fps = self.frame_timer_count as f32 / elapsed; + self.frame_timer_count = 0; + self.frame_timer_last = std::time::Instant::now(); + } + } + } + + // ── Private: cell helpers ───────────────────────────────────────────────── + + /// Append instances for every cell in `terminal`, positioned within `rect`. + /// + /// `scroll_offset` — number of scrollback lines the viewport is shifted up. + /// 0 = live bottom (normal mode); > 0 = historic view from the scrollback buffer. + fn push_pane_cells( + &self, + instances: &mut Vec, + terminal: &Terminal, + rect: &Rect, + is_active: bool, + scroll_offset: usize, + ) { + let offset = scroll_offset.min(terminal.scrollback.len()); + + if offset == 0 { + // Fast path: render the live screen directly. + for row in 0..terminal.rows { + for col in 0..terminal.cols { + let cell = terminal.screen[row * terminal.cols + col]; + if cell.attrs.contains(CellAttrs::WIDE_CONT) { + continue; + } + let x = rect.x + col as f32 * self.cell_width; + let y = rect.y + row as f32 * self.cell_height; + self.push_cell(instances, &cell, x, y, is_active, false); + } + } + } else { + // Composite view: some rows from scrollback, rest from the top of screen. + let sb_len = terminal.scrollback.len(); + // How many of the visible rows come from the scrollback buffer. + let scrollback_rows = offset.min(terminal.rows); + let screen_rows = terminal.rows.saturating_sub(scrollback_rows); + // Index of the first scrollback line to show. + let sb_start = sb_len.saturating_sub(offset); + + for (vi, si) in (sb_start..sb_start + scrollback_rows).enumerate() { + if si >= sb_len { + break; + } + let sb_row = &terminal.scrollback[si]; + for col in 0..terminal.cols.min(sb_row.len()) { + let cell = sb_row[col]; + if cell.attrs.contains(CellAttrs::WIDE_CONT) { + continue; + } + let x = rect.x + col as f32 * self.cell_width; + let y = rect.y + vi as f32 * self.cell_height; + self.push_cell(instances, &cell, x, y, is_active, false); + } + } + + for row in 0..screen_rows { + for col in 0..terminal.cols { + let cell = terminal.screen[row * terminal.cols + col]; + if cell.attrs.contains(CellAttrs::WIDE_CONT) { + continue; + } + let x = rect.x + col as f32 * self.cell_width; + let y = rect.y + (scrollback_rows + row) as f32 * self.cell_height; + self.push_cell(instances, &cell, x, y, is_active, false); + } + } + } + + // Draw the cursor only when the viewport is at the live bottom. + if offset == 0 && terminal.modes.contains(TerminalModes::CURSOR_VIS) { + let cx = terminal.cursor.col; + let cy = terminal.cursor.row; + if cx < terminal.cols && cy < terminal.rows { + let cell = terminal.screen[cy * terminal.cols + cx]; + let x = rect.x + cx as f32 * self.cell_width; + let y = rect.y + cy as f32 * self.cell_height; + self.push_cell(instances, &cell, x, y, is_active, true); + } + } + } + + /// Append a single cell instance. + fn push_cell( + &self, + instances: &mut Vec, + cell: &Cell, + x: f32, + y: f32, + is_active: bool, + invert: bool, + ) { + let dim_fg = if is_active { 1.0f32 } else { 0.55 }; + let dim_bg = if is_active { 1.0f32 } else { 0.65 }; + + let mut fg = self.color_to_rgba(&cell.fg, self.scheme_fg); + let mut bg = self.color_to_rgba(&cell.bg, self.scheme_bg); + + // Dim inactive panes. + for c in &mut fg[..3] { *c *= dim_fg; } + for c in &mut bg[..3] { *c *= dim_bg; } + + // REVERSE attribute XOR cursor invert. + if cell.attrs.contains(CellAttrs::REVERSE) ^ invert { + std::mem::swap(&mut fg, &mut bg); + } + + let (uv_x, uv_y, uv_w, uv_h) = self.atlas.glyph_uv(cell.ch); + // Wide characters (CJK, wide emoji) occupy two cell columns. + let cell_w = if cell.attrs.contains(CellAttrs::WIDE) { + self.cell_width * 2.0 + } else { + self.cell_width + }; + // Cursor block: override background with the scheme cursor colour (B4). + if invert { + bg = self.scheme_cursor; + } + instances.push(CellInstance { + cell_pos: [x, y], + cell_size: [cell_w, self.cell_height], + uv_offset: [uv_x, uv_y], + uv_size: [uv_w, uv_h], + fg_color: fg, + bg_color: bg, + attrs: cell.attrs.bits() as u32, + _pad: 0, + }); + } + + // ── Private: tab bar ────────────────────────────────────────────────────── + + fn push_tab_bar( + &self, + instances: &mut Vec, + workspace: &crate::workspace::Workspace, + ) { + let screen_w = self.config.width as f32; + let mut x = 0.0f32; + + for (i, tab) in workspace.tabs.iter().enumerate() { + let is_active = i == workspace.active_tab; + + // Active tab: inverted (fg on scheme bg). + // Inactive tab: subtle highlight strip. + let (tab_fg, tab_bg) = if is_active { + (self.scheme_bg, self.scheme_fg) + } else { + let mid = |a: f32, b: f32| a * 0.6 + b * 0.4; + ( + self.scheme_fg, + [ + mid(self.scheme_bg[0], self.scheme_fg[0]), + mid(self.scheme_bg[1], self.scheme_fg[1]), + mid(self.scheme_bg[2], self.scheme_fg[2]), + 1.0, + ], + ) + }; + + let padded = format!(" {} ", tab.name); + for ch in padded.chars() { + if x + self.cell_width > screen_w { + break; + } + let (uv_x, uv_y, uv_w, uv_h) = self.atlas.glyph_uv(ch); + instances.push(CellInstance { + cell_pos: [x, 0.0], + cell_size: [self.cell_width, self.cell_height], + uv_offset: [uv_x, uv_y], + uv_size: [uv_w, uv_h], + fg_color: tab_fg, + bg_color: tab_bg, + attrs: 0, + _pad: 0, + }); + x += self.cell_width; + } + } + + // Fill the rest of the tab bar row with the background colour. + if x < screen_w { + let bar_bg = dim3(self.scheme_bg, 0.75); + let (uv_x, uv_y, uv_w, uv_h) = self.atlas.glyph_uv(' '); + while x < screen_w { + instances.push(CellInstance { + cell_pos: [x, 0.0], + cell_size: [self.cell_width, self.cell_height], + uv_offset: [uv_x, uv_y], + uv_size: [uv_w, uv_h], + fg_color: bar_bg, + bg_color: bar_bg, + attrs: 0, + _pad: 0, + }); + x += self.cell_width; + } + } + } + + // ── Private: Kitty texture management ──────────────────────────────────── + + /// Upload any Kitty images (from `terminal.kitty`) that have not yet been + /// uploaded to the GPU. Creates an `Rgba8Unorm` texture and a bind group + /// for each new image and caches them in `self.kitty_textures`. + fn ensure_kitty_textures(&mut self, workspace: &crate::workspace::Workspace) { + for pane_id in workspace.current_tab_panes() { + if let Some(pane) = workspace.pane(pane_id) { + for placement in &pane.terminal.kitty_placements { + let id = placement.image_id; + if self.kitty_textures.contains_key(&id) { + continue; + } + let Some(img) = pane.terminal.kitty.get(id) else { + continue; + }; + // Upload the image as an Rgba8Unorm texture. + let tex = self.device.create_texture(&wgpu::TextureDescriptor { + label: Some("kitty_image"), + size: wgpu::Extent3d { + width: img.width, + height: img.height, + depth_or_array_layers: 1, + }, + mip_level_count: 1, + sample_count: 1, + dimension: wgpu::TextureDimension::D2, + format: wgpu::TextureFormat::Rgba8Unorm, + usage: wgpu::TextureUsages::TEXTURE_BINDING + | wgpu::TextureUsages::COPY_DST, + view_formats: &[], + }); + self.queue.write_texture( + wgpu::ImageCopyTexture { + texture: &tex, + mip_level: 0, + origin: wgpu::Origin3d::ZERO, + aspect: wgpu::TextureAspect::All, + }, + &img.rgba, + wgpu::ImageDataLayout { + offset: 0, + bytes_per_row: Some(img.width * 4), + rows_per_image: Some(img.height), + }, + wgpu::Extent3d { + width: img.width, + height: img.height, + depth_or_array_layers: 1, + }, + ); + let view = tex.create_view(&wgpu::TextureViewDescriptor::default()); + // Reuse the same bind group layout and sampler as the glyph atlas. + let bind_group = self.device.create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("kitty_bind_group"), + layout: &self.bgl, + entries: &[ + wgpu::BindGroupEntry { + binding: 0, + resource: self.uniform_buffer.as_entire_binding(), + }, + wgpu::BindGroupEntry { + binding: 1, + resource: wgpu::BindingResource::TextureView(&view), + }, + wgpu::BindGroupEntry { + binding: 2, + resource: wgpu::BindingResource::Sampler(&self.atlas_sampler), + }, + ], + }); + self.kitty_textures.insert(id, KittyGpuTexture { + _texture: tex, + bind_group, + }); + log::debug!("uploaded Kitty image id={id} ({}×{})", img.width, img.height); + } + } + } + } + + // ── Colour helpers ──────────────────────────────────────────────────────── + + fn color_to_rgba(&self, c: &Color, default: [f32; 4]) -> [f32; 4] { + match c { + Color::Default => default, + Color::Rgb(r, g, b) => [ + *r as f32 / 255.0, + *g as f32 / 255.0, + *b as f32 / 255.0, + 1.0, + ], + Color::Indexed(n) => { + let [r, g, b] = self.indexed_color(*n); + [r as f32 / 255.0, g as f32 / 255.0, b as f32 / 255.0, 1.0] + } + } + } + + fn indexed_color(&self, n: u8) -> [u8; 3] { + match n { + 0..=15 => self.scheme_ansi[n as usize], + 16..=231 => { + let i = n - 16; + let b = i % 6; + let g = (i / 6) % 6; + let r = i / 36; + let ch = |c: u8| if c == 0 { 0 } else { 55 + c * 40 }; + [ch(r), ch(g), ch(b)] + } + 232..=255 => { + let v = 8 + (n - 232) * 10; + [v, v, v] + } + } + } + + // ── render_timing overlay (compiled only with the feature flag) ─────────── + + /// Push a right-aligned "FPS:NNN" string into the tab-bar row. + /// + /// Uses the previous frame's fps value so no timing is needed at push time. + #[cfg(feature = "render_timing")] + fn push_fps_overlay(&self, instances: &mut Vec) { + let text = format!("FPS:{:4.0}", self.fps); + let n = text.chars().count() as f32; + let x_start = (self.config.width as f32 - n * self.cell_width).max(0.0); + let fg = [0.0f32, 1.0, 0.4, 1.0]; // bright green + let bg = [0.0f32, 0.0, 0.0, 0.75]; // semi-transparent black + for (i, ch) in text.chars().enumerate() { + let (uv_x, uv_y, uv_w, uv_h) = self.atlas.glyph_uv(ch); + instances.push(CellInstance { + cell_pos: [x_start + i as f32 * self.cell_width, 0.0], + cell_size: [self.cell_width, self.cell_height], + uv_offset: [uv_x, uv_y], + uv_size: [uv_w, uv_h], + fg_color: fg, + bg_color: bg, + }); + } + } +} + +// ─── Free helpers ───────────────────────────────────────────────────────────── + +/// Multiply the RGB channels of an `[f32; 4]` colour by `factor`. +fn dim3(c: [f32; 4], factor: f32) -> [f32; 4] { + [c[0] * factor, c[1] * factor, c[2] * factor, c[3]] +} diff --git a/winiterm/src/renderer/shader.wgsl b/winiterm/src/renderer/shader.wgsl new file mode 100644 index 0000000..7a75d30 --- /dev/null +++ b/winiterm/src/renderer/shader.wgsl @@ -0,0 +1,100 @@ +// ─── Uniforms ──────────────────────────────────────────────────────────────── +// +// Screen size in physical pixels. Padded to 16 bytes for uniform alignment. + +struct Uniforms { + screen_size: vec2, + _pad: vec2, +} + +@group(0) @binding(0) var uniforms: Uniforms; +@group(0) @binding(1) var atlas_tex: texture_2d; +@group(0) @binding(2) var atlas_smp: sampler; + +// ─── Vertex stage ───────────────────────────────────────────────────────────── +// +// Buffer 0 (step: Vertex): quad corners in unit space [(0,0)..(1,1)] +// Buffer 1 (step: Instance): per-cell data + +struct VertexInput { + @location(0) pos: vec2, +} + +struct InstanceInput { + @location(1) cell_pos: vec2, // top-left pixel coord of the cell + @location(2) cell_size: vec2, // cell width × height in pixels + @location(3) uv_offset: vec2, // atlas UV top-left (0..1) + @location(4) uv_size: vec2, // atlas UV width × height (0..1) + @location(5) fg_color: vec4, // foreground RGBA (sRGB linear) + @location(6) bg_color: vec4, // background RGBA (sRGB linear) + @location(7) attrs: u32, // CellAttrs bits (underline, strikethrough, …) +} + +struct VertexOutput { + @builtin(position) clip_pos: vec4, + @location(0) uv: vec2, // atlas texture UV + @location(1) fg_color: vec4, + @location(2) bg_color: vec4, + @location(3) cell_uv: vec2, // unit quad position (0..1 within cell) + @location(4) @interpolate(flat) attrs: u32, // cell attribute bits (flat — no interpolation) +} + +@vertex +fn vs_main(vert: VertexInput, inst: InstanceInput) -> VertexOutput { + var out: VertexOutput; + + // Transform unit quad → cell in pixel-space, then → NDC. + let px = inst.cell_pos + vert.pos * inst.cell_size; + let clip_x = (px.x / uniforms.screen_size.x) * 2.0 - 1.0; + let clip_y = -(px.y / uniforms.screen_size.y) * 2.0 + 1.0; + out.clip_pos = vec4(clip_x, clip_y, 0.0, 1.0); + + out.uv = inst.uv_offset + vert.pos * inst.uv_size; + out.fg_color = inst.fg_color; + out.bg_color = inst.bg_color; + out.cell_uv = vert.pos; // (0,0) = top-left, (1,1) = bottom-right of cell + out.attrs = inst.attrs; + return out; +} + +// ─── Fragment stage ─────────────────────────────────────────────────────────── +// +// The atlas is R8Unorm grayscale coverage. +// alpha = 0 → pure background, alpha = 1 → pure foreground. +// +// CellAttrs bit masks (must match Rust CellAttrs bitflags): +// UNDERLINE = 1u << 3u = 8u +// UNDERLINE2 = 1u << 4u = 16u (double underline) +// STRIKETHROUGH = 1u << 8u = 256u + +@fragment +fn fs_main(in: VertexOutput) -> @location(0) vec4 { + let coverage = textureSample(atlas_tex, atlas_smp, in.uv).r; + var color = mix(in.bg_color, in.fg_color, coverage); + + let y = in.cell_uv.y; + + // Underline: solid bar at bottom ~14% of cell. + if (in.attrs & 8u) != 0u && y > 0.84 { + color = in.fg_color; + } + // Double underline: two thin bars near the bottom. + if (in.attrs & 16u) != 0u && ((y > 0.74 && y < 0.80) || (y > 0.88 && y < 0.94)) { + color = in.fg_color; + } + // Strikethrough: horizontal bar at ~50% cell height. + if (in.attrs & 256u) != 0u && y > 0.44 && y < 0.54 { + color = in.fg_color; + } + + return color; +} + +// ─── Kitty Graphics Protocol fragment stage ─────────────────────────────────── +// +// For Kitty images the texture is Rgba8Unorm; output all four channels directly. + +@fragment +fn fs_kitty_main(in: VertexOutput) -> @location(0) vec4 { + return textureSample(atlas_tex, atlas_smp, in.uv); +} diff --git a/winiterm/src/split.rs b/winiterm/src/split.rs new file mode 100644 index 0000000..44b1c7e --- /dev/null +++ b/winiterm/src/split.rs @@ -0,0 +1,189 @@ +//! Pane split-tree and pixel-layout (Phase 4). +//! +//! A `SplitNode` is either a `Leaf` (one pane ID) or a `Split` that divides +//! its allocated `Rect` between two child nodes. Calling `layout()` on the +//! root node with the full window rect produces a `HashMap`. + +use std::collections::HashMap; + +// ─── Direction ──────────────────────────────────────────────────────────────── + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum SplitDir { + /// Divide horizontally: left pane | right pane. + Horizontal, + /// Divide vertically: top pane / bottom pane. + Vertical, +} + +// ─── Pixel rectangle ───────────────────────────────────────────────────────── + +/// An axis-aligned rectangle in physical pixel coordinates. +#[derive(Debug, Clone, Copy)] +pub struct Rect { + pub x: f32, + pub y: f32, + pub w: f32, + pub h: f32, +} + +impl Rect { + pub fn new(x: f32, y: f32, w: f32, h: f32) -> Self { + Self { x, y, w, h } + } + + #[inline] + pub fn cell_cols(&self, cell_w: f32) -> u16 { + (self.w / cell_w).floor().max(2.0) as u16 + } + + #[inline] + pub fn cell_rows(&self, cell_h: f32) -> u16 { + (self.h / cell_h).floor().max(2.0) as u16 + } +} + +// ─── Split node ─────────────────────────────────────────────────────────────── + +pub enum SplitNode { + Leaf(usize), // pane_id + Split { + dir: SplitDir, + /// Fraction [0,1] allocated to `first`. + ratio: f32, + first: Box, + second: Box, + }, +} + +impl SplitNode { + // ── Layout ─────────────────────────────────────────────────────────────── + + /// Recursively assign a pixel `Rect` to every leaf pane. + pub fn layout(&self, rect: Rect, out: &mut HashMap) { + match self { + SplitNode::Leaf(id) => { + out.insert(*id, rect); + } + SplitNode::Split { + dir, + ratio, + first, + second, + } => { + let (r1, r2) = split_rect(rect, *dir, *ratio); + first.layout(r1, out); + second.layout(r2, out); + } + } + } + + // ── Queries ─────────────────────────────────────────────────────────────── + + /// All pane IDs in tree order (depth-first). + pub fn all_panes(&self) -> Vec { + match self { + SplitNode::Leaf(id) => vec![*id], + SplitNode::Split { first, second, .. } => { + let mut v = first.all_panes(); + v.extend(second.all_panes()); + v + } + } + } + + /// The first leaf ID in tree order. + pub fn first_pane(&self) -> usize { + match self { + SplitNode::Leaf(id) => *id, + SplitNode::Split { first, .. } => first.first_pane(), + } + } + + // ── Mutations ──────────────────────────────────────────────────────────── + + /// Insert a new split at the `target_id` leaf, placing the new pane as + /// the second child. Returns `true` if `target_id` was found. + pub fn split_at(&mut self, target_id: usize, dir: SplitDir, new_id: usize) -> bool { + match self { + SplitNode::Leaf(id) if *id == target_id => { + let old = SplitNode::Leaf(target_id); + *self = SplitNode::Split { + dir, + ratio: 0.5, + first: Box::new(old), + second: Box::new(SplitNode::Leaf(new_id)), + }; + true + } + SplitNode::Leaf(_) => false, + SplitNode::Split { first, second, .. } => { + first.split_at(target_id, dir, new_id) || second.split_at(target_id, dir, new_id) + } + } + } + + /// Remove the leaf `target_id` and collapse its parent split into the + /// surviving sibling. The surviving sibling is written into `*self`. + /// Returns `true` if removed. + pub fn remove(&mut self, target_id: usize) -> bool { + let replaced = match self { + SplitNode::Split { first, second, .. } => { + // Check if first is the target leaf. + if matches!(first.as_ref(), SplitNode::Leaf(id) if *id == target_id) { + Some(true) // replace self with second + } else if matches!(second.as_ref(), SplitNode::Leaf(id) if *id == target_id) { + Some(false) // replace self with first + } else { + None + } + } + _ => None, + }; + match replaced { + Some(take_second) => { + let inner = match std::mem::replace(self, SplitNode::Leaf(0)) { + SplitNode::Split { first, second, .. } => { + if take_second { + *second + } else { + *first + } + } + other => other, + }; + *self = inner; + true + } + None => match self { + SplitNode::Split { first, second, .. } => { + first.remove(target_id) || second.remove(target_id) + } + _ => false, + }, + } + } +} + +// ─── Helpers ────────────────────────────────────────────────────────────────── + +fn split_rect(rect: Rect, dir: SplitDir, ratio: f32) -> (Rect, Rect) { + match dir { + SplitDir::Horizontal => { + let w1 = (rect.w * ratio).floor(); + let w2 = rect.w - w1; + ( + Rect::new(rect.x, rect.y, w1, rect.h), + Rect::new(rect.x + w1, rect.y, w2, rect.h), + ) + } + SplitDir::Vertical => { + let h1 = (rect.h * ratio).floor(); + let h2 = rect.h - h1; + ( + Rect::new(rect.x, rect.y, rect.w, h1), + Rect::new(rect.x, rect.y + h1, rect.w, h2), + ) + } + } +} diff --git a/winiterm/src/terminal.rs b/winiterm/src/terminal.rs new file mode 100644 index 0000000..390bca9 --- /dev/null +++ b/winiterm/src/terminal.rs @@ -0,0 +1,991 @@ +//! Terminal state: grid, cursor, scrollback, and all VT operations. +//! +//! The `Terminal` struct is the authoritative model of what is displayed. +//! The VT parser drives it by calling public methods in response to escape +//! sequences. The renderer (Phase 3) reads it to produce GPU draw calls. + +use bitflags::bitflags; +use std::collections::VecDeque; + +use crate::kitty::KittyRegistry; + +// ─── Color ─────────────────────────────────────────────────────────────────── + +/// A terminal color: default (inherits theme), a 256-color palette index, or +/// a 24-bit RGB value. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)] +pub enum Color { + #[default] + Default, + Indexed(u8), + Rgb(u8, u8, u8), +} + +// ─── Cell attributes ───────────────────────────────────────────────────────── + +bitflags! { + /// Text rendering attributes packed into a single `u16`. + #[derive(Clone, Copy, Debug, PartialEq, Eq, Default)] + pub struct CellAttrs: u16 { + const BOLD = 1 << 0; + const DIM = 1 << 1; + const ITALIC = 1 << 2; + const UNDERLINE = 1 << 3; + const UNDERLINE2 = 1 << 4; // double underline + const BLINK = 1 << 5; + const REVERSE = 1 << 6; + const INVISIBLE = 1 << 7; + const STRIKETHROUGH = 1 << 8; + const WIDE = 1 << 9; // occupies two columns (CJK / wide emoji) + const WIDE_CONT = 1 << 10; // second column of a wide character + } +} + +// ─── Cell ──────────────────────────────────────────────────────────────────── + +/// A single terminal grid cell. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct Cell { + pub ch: char, + pub fg: Color, + pub bg: Color, + pub attrs: CellAttrs, +} + +impl Cell { + /// A blank cell with the given colors/attributes. + #[inline] + pub const fn new(ch: char, fg: Color, bg: Color, attrs: CellAttrs) -> Self { + Self { ch, fg, bg, attrs } + } + + /// Blank cell using default colors. + #[inline] + pub const fn blank() -> Self { + Self::new(' ', Color::Default, Color::Default, CellAttrs::empty()) + } +} + +impl Default for Cell { + fn default() -> Self { + Self::blank() + } +} + +// ─── Cursor ────────────────────────────────────────────────────────────────── + +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub enum CursorShape { + #[default] + Block, + Beam, + Underline, +} + +#[derive(Clone, Copy, Debug, Default)] +pub struct Cursor { + pub col: usize, + pub row: usize, + pub shape: CursorShape, +} + +// ─── Terminal modes ─────────────────────────────────────────────────────────── + +bitflags! { + #[derive(Clone, Copy, Debug, Default)] + pub struct TerminalModes: u32 { + /// DECAWM — auto-wrap at end of line. + const AUTO_WRAP = 1 << 0; + /// DECTCEM — cursor visible. + const CURSOR_VIS = 1 << 1; + /// DECSET 1049 — alternate screen active. + const ALT_SCREEN = 1 << 2; + /// XTerm mouse tracking (any mode active). + const MOUSE = 1 << 3; + /// DECSET 2004 — bracketed paste. + const BRACKET_PASTE = 1 << 4; + /// DECSET 1004 — focus events. + const FOCUS_EVENTS = 1 << 5; + /// DECSET 25 / DECTCEM — separate flag mirroring CURSOR_VIS for clarity. + const INSERT_MODE = 1 << 6; + /// DECCKM — application cursor keys (SS3 instead of CSI for arrows). + const DECCKM = 1 << 7; + } +} + +// ─── Kitty Graphics ────────────────────────────────────────────────────────── + +/// A placed Kitty image: the image is rendered at cell `(col, row)`. +#[derive(Debug, Clone)] +pub struct KittyPlacement { + pub image_id: u32, + pub col: usize, + pub row: usize, + pub pixel_width: u32, + pub pixel_height: u32, +} + +// ─── Terminal ──────────────────────────────────────────────────────────────── + +/// The complete state of a terminal emulator instance. +pub struct Terminal { + pub cols: usize, + pub rows: usize, + + /// Active screen buffer, row-major: `screen[row * cols + col]`. + pub screen: Vec, + + /// Scrollback lines; oldest at index 0, newest at the back. + pub scrollback: VecDeque>, + /// Maximum number of scrollback lines to retain. + pub scrollback_lines: usize, + + // ── Cursor ─────────────────────────────────────────────────────────────── + pub cursor: Cursor, + /// DECSC / DECRC saved cursor. + saved_cursor: Option, + /// When `true` the cursor is logically *past* the last column and will wrap + /// before the next printable character is written. + pub pending_wrap: bool, + + // ── Alternate screen ───────────────────────────────────────────────────── + alt_screen: Option>, + alt_cursor: Option, + + // ── Scroll region (0-indexed, inclusive) ───────────────────────────────── + pub scroll_top: usize, + pub scroll_bottom: usize, + + // ── Current SGR state applied to newly written cells ───────────────────── + pub current_fg: Color, + pub current_bg: Color, + pub current_attrs: CellAttrs, + + // ── Miscellaneous ───────────────────────────────────────────────────────── + pub modes: TerminalModes, + pub title: String, + /// Hardware tab stops (one bool per column). + tab_stops: Vec, + + // ── Mouse tracking mode ─────────────────────────────────────────────────── + pub mouse_mode: u16, + + // ── Pending VT responses (DSR, DA1) ────────────────────────────────────── + /// Bytes queued to be written back to the PTY (device status responses, etc.) + pub pending_responses: Vec>, + + // ── Kitty Graphics Protocol ─────────────────────────────────────────────── + /// Received and assembled Kitty images, keyed by image ID. + pub kitty: KittyRegistry, + /// Images that have been placed on screen (from `a=T` or `a=p` actions). + pub kitty_placements: Vec, + /// Image IDs that were deleted by `a=d` since the last renderer frame. + /// The renderer drains this list each frame to evict GPU textures. + pub kitty_deleted_ids: Vec, +} + +impl Terminal { + /// Create a new terminal with the given dimensions and scrollback capacity. + pub fn new(cols: usize, rows: usize, scrollback_lines: usize) -> Self { + let cell_count = cols * rows; + let mut tab_stops = vec![false; cols]; + // Default tab stop every 8 columns. + for i in (0..cols).step_by(8) { + tab_stops[i] = true; + } + + let mut modes = TerminalModes::empty(); + modes.insert(TerminalModes::AUTO_WRAP); + modes.insert(TerminalModes::CURSOR_VIS); + + Self { + cols, + rows, + screen: vec![Cell::blank(); cell_count], + scrollback: VecDeque::new(), + scrollback_lines, + cursor: Cursor::default(), + saved_cursor: None, + pending_wrap: false, + alt_screen: None, + alt_cursor: None, + scroll_top: 0, + scroll_bottom: rows.saturating_sub(1), + current_fg: Color::Default, + current_bg: Color::Default, + current_attrs: CellAttrs::empty(), + modes, + title: String::new(), + tab_stops, + mouse_mode: 0, + pending_responses: Vec::new(), + kitty: KittyRegistry::new(), + kitty_placements: Vec::new(), + kitty_deleted_ids: Vec::new(), + } + } + + // ─── Resize ────────────────────────────────────────────────────────────── + + /// Resize the terminal to `cols × rows`, preserving as much content as possible. + pub fn resize(&mut self, cols: usize, rows: usize) { + if cols == self.cols && rows == self.rows { + return; + } + let mut new_screen = vec![Cell::blank(); cols * rows]; + let copy_cols = self.cols.min(cols); + let copy_rows = self.rows.min(rows); + for r in 0..copy_rows { + for c in 0..copy_cols { + new_screen[r * cols + c] = self.screen[r * self.cols + c]; + } + } + self.screen = new_screen; + self.cols = cols; + self.rows = rows; + self.scroll_top = 0; + self.scroll_bottom = rows.saturating_sub(1); + self.tab_stops.resize(cols, false); + for i in (0..cols).step_by(8) { + if !self.tab_stops[i] { + self.tab_stops[i] = true; + } + } + // Clamp cursor. + self.cursor.col = self.cursor.col.min(cols.saturating_sub(1)); + self.cursor.row = self.cursor.row.min(rows.saturating_sub(1)); + self.pending_wrap = false; + } + + // ─── Cell accessors ─────────────────────────────────────────────────────── + + #[inline] + pub fn cell(&self, col: usize, row: usize) -> &Cell { + &self.screen[row * self.cols + col] + } + + #[inline] + pub fn cell_mut(&mut self, col: usize, row: usize) -> &mut Cell { + &mut self.screen[row * self.cols + col] + } + + // ─── Write a character ──────────────────────────────────────────────────── + + /// Place `ch` at the cursor position and advance. + /// + /// Wide characters (CJK, wide emoji — display width 2) occupy two columns: + /// the character is placed in the first column with `WIDE` set, and a blank + /// placeholder with `WIDE_CONT` is written to the second column. + /// + /// Zero-width / combining characters are placed on the current column + /// without advancing the cursor. + pub fn write_char(&mut self, ch: char) { + use unicode_width::UnicodeWidthChar; + + // Resolve pending wrap before writing. + if self.pending_wrap { + self.pending_wrap = false; + self.do_linefeed(); + self.cursor.col = 0; + } + + let width = ch.width().unwrap_or(1); + + if width == 0 { + // Combining / zero-width: overlay on the current (or previous) cell. + let col = self.cursor.col.min(self.cols.saturating_sub(1)); + let row = self.cursor.row.min(self.rows.saturating_sub(1)); + self.cell_mut(col, row).ch = ch; + return; + } + + let col = self.cursor.col.min(self.cols.saturating_sub(1)); + let row = self.cursor.row.min(self.rows.saturating_sub(1)); + + if width == 2 && col + 1 < self.cols { + // Wide character: write char in first column, blank continuation in second. + let mut attrs = self.current_attrs; + attrs.insert(CellAttrs::WIDE); + *self.cell_mut(col, row) = Cell { + ch, + fg: self.current_fg, + bg: self.current_bg, + attrs, + }; + let mut cont_attrs = self.current_attrs; + cont_attrs.insert(CellAttrs::WIDE_CONT); + *self.cell_mut(col + 1, row) = Cell { + ch: ' ', + fg: self.current_fg, + bg: self.current_bg, + attrs: cont_attrs, + }; + if col + 2 >= self.cols { + if self.modes.contains(TerminalModes::AUTO_WRAP) { + self.pending_wrap = true; + } + } else { + self.cursor.col = col + 2; + } + } else { + // Normal (or wide that doesn't fit) — write as single cell. + *self.cell_mut(col, row) = Cell { + ch, + fg: self.current_fg, + bg: self.current_bg, + attrs: self.current_attrs, + }; + if col + 1 >= self.cols { + if self.modes.contains(TerminalModes::AUTO_WRAP) { + self.pending_wrap = true; + } + } else { + self.cursor.col = col + 1; + } + } + } + + // ─── C0 control characters ──────────────────────────────────────────────── + + /// Execute a C0 control byte. + pub fn execute_c0(&mut self, byte: u8) { + match byte { + 0x07 => {} // BEL — ignore (audio bell could be added later) + 0x08 => { + // BS — backspace + if self.cursor.col > 0 { + self.cursor.col -= 1; + self.pending_wrap = false; + } + } + 0x09 => self.advance_tab(), + 0x0a | 0x0b | 0x0c => { + // LF, VT, FF — line feed + self.do_linefeed(); + } + 0x0d => { + // CR + self.cursor.col = 0; + self.pending_wrap = false; + } + _ => {} + } + } + + /// Advance cursor to the next tab stop. + fn advance_tab(&mut self) { + let start = self.cursor.col + 1; + for col in start..self.cols { + if self.tab_stops[col] { + self.cursor.col = col; + return; + } + } + self.cursor.col = self.cols.saturating_sub(1); + } + + // ─── Line feed / scrolling ──────────────────────────────────────────────── + + /// Perform a line feed within the scroll region. + fn do_linefeed(&mut self) { + if self.cursor.row == self.scroll_bottom { + self.scroll_up(1); + } else { + self.cursor.row = (self.cursor.row + 1).min(self.rows.saturating_sub(1)); + } + self.pending_wrap = false; + } + + /// Reverse index (scroll down when cursor is at the top of the scroll region). + pub fn reverse_index(&mut self) { + if self.cursor.row == self.scroll_top { + self.scroll_down(1); + } else if self.cursor.row > 0 { + self.cursor.row -= 1; + } + } + + /// Scroll the contents of the scroll region up by `n` lines. + /// Lines scrolled off the top are pushed into the scrollback buffer. + pub fn scroll_up(&mut self, n: usize) { + let n = n.min(self.scroll_bottom - self.scroll_top + 1); + for _ in 0..n { + // Push top line of the scroll region into scrollback (only when region = full screen). + if self.scroll_top == 0 { + let top: Vec = self.screen[..self.cols].to_vec(); + if self.scrollback.len() >= self.scrollback_lines { + self.scrollback.pop_front(); + } + self.scrollback.push_back(top); + } + // Shift rows up. + for r in self.scroll_top..self.scroll_bottom { + let src = (r + 1) * self.cols; + let dst = r * self.cols; + self.screen.copy_within(src..src + self.cols, dst); + } + // Clear the bottom row of the scroll region. + let bottom_start = self.scroll_bottom * self.cols; + let bg = self.current_bg; + let fg = self.current_fg; + for cell in &mut self.screen[bottom_start..bottom_start + self.cols] { + *cell = Cell::new(' ', fg, bg, CellAttrs::empty()); + } + } + } + + /// Scroll the scroll region down by `n` lines. + pub fn scroll_down(&mut self, n: usize) { + let n = n.min(self.scroll_bottom - self.scroll_top + 1); + for _ in 0..n { + for r in (self.scroll_top + 1..=self.scroll_bottom).rev() { + let src = (r - 1) * self.cols; + let dst = r * self.cols; + self.screen.copy_within(src..src + self.cols, dst); + } + // Clear the top row of the scroll region. + let top_start = self.scroll_top * self.cols; + let bg = self.current_bg; + let fg = self.current_fg; + for cell in &mut self.screen[top_start..top_start + self.cols] { + *cell = Cell::new(' ', fg, bg, CellAttrs::empty()); + } + } + } + + // ─── Cursor movement (CSI sequences) ───────────────────────────────────── + + /// CUP / HVP — move cursor to `(row, col)` (0-indexed). + pub fn cursor_pos(&mut self, row: usize, col: usize) { + let max_row = if self.modes.contains(TerminalModes::AUTO_WRAP) { + self.rows.saturating_sub(1) + } else { + self.rows.saturating_sub(1) + }; + self.cursor.row = row.min(max_row); + self.cursor.col = col.min(self.cols.saturating_sub(1)); + self.pending_wrap = false; + } + + /// CUU — cursor up `n`. + pub fn cursor_up(&mut self, n: usize) { + self.cursor.row = self.cursor.row.saturating_sub(n).max(self.scroll_top); + self.pending_wrap = false; + } + + /// CUD — cursor down `n`. + pub fn cursor_down(&mut self, n: usize) { + self.cursor.row = (self.cursor.row + n).min(self.scroll_bottom); + self.pending_wrap = false; + } + + /// CUF — cursor forward (right) `n`. + pub fn cursor_forward(&mut self, n: usize) { + self.cursor.col = (self.cursor.col + n).min(self.cols.saturating_sub(1)); + self.pending_wrap = false; + } + + /// CUB — cursor back (left) `n`. + pub fn cursor_back(&mut self, n: usize) { + self.cursor.col = self.cursor.col.saturating_sub(n); + self.pending_wrap = false; + } + + /// CNL — cursor next line `n`. + pub fn cursor_next_line(&mut self, n: usize) { + self.cursor.row = (self.cursor.row + n).min(self.rows.saturating_sub(1)); + self.cursor.col = 0; + self.pending_wrap = false; + } + + /// CPL — cursor previous line `n`. + pub fn cursor_prev_line(&mut self, n: usize) { + self.cursor.row = self.cursor.row.saturating_sub(n); + self.cursor.col = 0; + self.pending_wrap = false; + } + + /// CHA / HPA — cursor horizontal absolute (column, 0-indexed). + pub fn cursor_col(&mut self, col: usize) { + self.cursor.col = col.min(self.cols.saturating_sub(1)); + self.pending_wrap = false; + } + + /// VPA — vertical position absolute (row, 0-indexed). + pub fn cursor_row(&mut self, row: usize) { + self.cursor.row = row.min(self.rows.saturating_sub(1)); + self.pending_wrap = false; + } + + // ─── Erase operations ───────────────────────────────────────────────────── + + /// ED — erase in display. + /// + /// `mode`: 0 = from cursor to end, 1 = from start to cursor, 2 = whole screen, + /// 3 = whole screen + scrollback. + pub fn erase_in_display(&mut self, mode: u16) { + let col = self.cursor.col; + let row = self.cursor.row; + let fg = self.current_fg; + let bg = self.current_bg; + match mode { + 0 => { + // From cursor to end of screen. + let start = row * self.cols + col; + for cell in &mut self.screen[start..] { + *cell = Cell::new(' ', fg, bg, CellAttrs::empty()); + } + } + 1 => { + // From start of screen to cursor. + let end = row * self.cols + col + 1; + for cell in &mut self.screen[..end] { + *cell = Cell::new(' ', fg, bg, CellAttrs::empty()); + } + } + 2 | 3 => { + // Whole screen. + for cell in &mut self.screen { + *cell = Cell::new(' ', fg, bg, CellAttrs::empty()); + } + if mode == 3 { + self.scrollback.clear(); + } + } + _ => {} + } + } + + /// EL — erase in line. + /// + /// `mode`: 0 = from cursor to end, 1 = from start to cursor, 2 = whole line. + pub fn erase_in_line(&mut self, mode: u16) { + let col = self.cursor.col; + let row = self.cursor.row; + let fg = self.current_fg; + let bg = self.current_bg; + let row_start = row * self.cols; + match mode { + 0 => { + for c in col..self.cols { + self.screen[row_start + c] = Cell::new(' ', fg, bg, CellAttrs::empty()); + } + } + 1 => { + for c in 0..=col { + self.screen[row_start + c] = Cell::new(' ', fg, bg, CellAttrs::empty()); + } + } + 2 => { + for c in 0..self.cols { + self.screen[row_start + c] = Cell::new(' ', fg, bg, CellAttrs::empty()); + } + } + _ => {} + } + } + + /// ECH — erase `n` characters starting at the cursor. + pub fn erase_chars(&mut self, n: usize) { + let row = self.cursor.row; + let col = self.cursor.col; + let end = (col + n).min(self.cols); + let fg = self.current_fg; + let bg = self.current_bg; + let row_start = row * self.cols; + for c in col..end { + self.screen[row_start + c] = Cell::new(' ', fg, bg, CellAttrs::empty()); + } + } + + // ─── Insert / Delete ────────────────────────────────────────────────────── + + /// ICH — insert `n` blank characters at cursor, shifting right. + pub fn insert_chars(&mut self, n: usize) { + let row = self.cursor.row; + let col = self.cursor.col; + let n = n.min(self.cols - col); + let row_start = row * self.cols; + let row_end = row_start + self.cols; + // Shift chars right. + for c in (col..self.cols - n).rev() { + self.screen[row_start + c + n] = self.screen[row_start + c]; + } + let fg = self.current_fg; + let bg = self.current_bg; + for c in col..col + n { + self.screen[row_start + c] = Cell::new(' ', fg, bg, CellAttrs::empty()); + } + let _ = row_end; // suppress lint + } + + /// DCH — delete `n` characters at cursor, pulling right chars left. + pub fn delete_chars(&mut self, n: usize) { + let row = self.cursor.row; + let col = self.cursor.col; + let n = n.min(self.cols - col); + let row_start = row * self.cols; + for c in col..self.cols - n { + self.screen[row_start + c] = self.screen[row_start + c + n]; + } + let fg = self.current_fg; + let bg = self.current_bg; + for c in self.cols - n..self.cols { + self.screen[row_start + c] = Cell::new(' ', fg, bg, CellAttrs::empty()); + } + } + + /// IL — insert `n` blank lines at cursor row. + pub fn insert_lines(&mut self, n: usize) { + let row = self.cursor.row; + // Only insert within the scroll region. + if row < self.scroll_top || row > self.scroll_bottom { + return; + } + let n = n.min(self.scroll_bottom - row + 1); + // Shift rows down (within scroll region). + for r in (row..=self.scroll_bottom - n).rev() { + let src = r * self.cols; + let dst = (r + n) * self.cols; + self.screen.copy_within(src..src + self.cols, dst); + } + let fg = self.current_fg; + let bg = self.current_bg; + for r in row..row + n { + let s = r * self.cols; + for cell in &mut self.screen[s..s + self.cols] { + *cell = Cell::new(' ', fg, bg, CellAttrs::empty()); + } + } + self.cursor.col = 0; + } + + /// DL — delete `n` lines at cursor row. + pub fn delete_lines(&mut self, n: usize) { + let row = self.cursor.row; + if row < self.scroll_top || row > self.scroll_bottom { + return; + } + let n = n.min(self.scroll_bottom - row + 1); + for r in row..=self.scroll_bottom - n { + let src = (r + n) * self.cols; + let dst = r * self.cols; + self.screen.copy_within(src..src + self.cols, dst); + } + let fg = self.current_fg; + let bg = self.current_bg; + for r in self.scroll_bottom - n + 1..=self.scroll_bottom { + let s = r * self.cols; + for cell in &mut self.screen[s..s + self.cols] { + *cell = Cell::new(' ', fg, bg, CellAttrs::empty()); + } + } + self.cursor.col = 0; + } + + // ─── SGR (Select Graphic Rendition) ────────────────────────────────────── + + /// Process an SGR sequence (CSI `m`). `params` is the flat semicolon-separated + /// parameter list; an empty slice is equivalent to `[0]` (reset). + pub fn process_sgr(&mut self, params: &[u32]) { + if params.is_empty() { + self.sgr_reset(); + return; + } + let mut i = 0; + while i < params.len() { + match params[i] { + 0 => self.sgr_reset(), + 1 => self.current_attrs.insert(CellAttrs::BOLD), + 2 => self.current_attrs.insert(CellAttrs::DIM), + 3 => self.current_attrs.insert(CellAttrs::ITALIC), + 4 => self.current_attrs.insert(CellAttrs::UNDERLINE), + 5 | 6 => self.current_attrs.insert(CellAttrs::BLINK), + 7 => self.current_attrs.insert(CellAttrs::REVERSE), + 8 => self.current_attrs.insert(CellAttrs::INVISIBLE), + 9 => self.current_attrs.insert(CellAttrs::STRIKETHROUGH), + 21 => self.current_attrs.insert(CellAttrs::UNDERLINE2), + 22 => self.current_attrs.remove(CellAttrs::BOLD | CellAttrs::DIM), + 23 => self.current_attrs.remove(CellAttrs::ITALIC), + 24 => self + .current_attrs + .remove(CellAttrs::UNDERLINE | CellAttrs::UNDERLINE2), + 25 => self.current_attrs.remove(CellAttrs::BLINK), + 27 => self.current_attrs.remove(CellAttrs::REVERSE), + 28 => self.current_attrs.remove(CellAttrs::INVISIBLE), + 29 => self.current_attrs.remove(CellAttrs::STRIKETHROUGH), + // Foreground 16 colors + 30..=37 => self.current_fg = Color::Indexed((params[i] - 30) as u8), + 38 => { + if let Some(color) = self.parse_extended_color(params, &mut i) { + self.current_fg = color; + } + } + 39 => self.current_fg = Color::Default, + // Background 16 colors + 40..=47 => self.current_bg = Color::Indexed((params[i] - 40) as u8), + 48 => { + if let Some(color) = self.parse_extended_color(params, &mut i) { + self.current_bg = color; + } + } + 49 => self.current_bg = Color::Default, + // Bright / high-intensity foreground (8–15) + 90..=97 => self.current_fg = Color::Indexed((params[i] - 90 + 8) as u8), + // Bright / high-intensity background (8–15) + 100..=107 => self.current_bg = Color::Indexed((params[i] - 100 + 8) as u8), + _ => {} + } + i += 1; + } + } + + fn sgr_reset(&mut self) { + self.current_fg = Color::Default; + self.current_bg = Color::Default; + self.current_attrs = CellAttrs::empty(); + } + + /// Parse an extended color (38;5;n or 38;2;r;g;b) starting *after* `params[i]`. + /// Returns the color and advances `i` by the number of consumed extra params. + fn parse_extended_color(&self, params: &[u32], i: &mut usize) -> Option { + if *i + 1 >= params.len() { + return None; + } + match params[*i + 1] { + 5 if *i + 2 < params.len() => { + *i += 2; + Some(Color::Indexed(params[*i] as u8)) + } + 2 if *i + 4 < params.len() => { + *i += 4; + Some(Color::Rgb( + params[*i - 2] as u8, + params[*i - 1] as u8, + params[*i] as u8, + )) + } + _ => None, + } + } + + // ─── Scroll region ──────────────────────────────────────────────────────── + + /// DECSTBM — set scrolling region, `top` and `bottom` are 0-indexed. + pub fn set_scroll_region(&mut self, top: usize, bottom: usize) { + let bottom = bottom.min(self.rows.saturating_sub(1)); + if top < bottom { + self.scroll_top = top; + self.scroll_bottom = bottom; + } + // Reset cursor to home on region change. + self.cursor_pos(0, 0); + } + + // ─── Save / Restore cursor ──────────────────────────────────────────────── + + /// DECSC — save cursor. + pub fn save_cursor(&mut self) { + self.saved_cursor = Some(self.cursor); + } + + /// DECRC — restore cursor. + pub fn restore_cursor(&mut self) { + if let Some(c) = self.saved_cursor { + self.cursor = c; + self.cursor.col = self.cursor.col.min(self.cols.saturating_sub(1)); + self.cursor.row = self.cursor.row.min(self.rows.saturating_sub(1)); + self.pending_wrap = false; + } + } + + // ─── Alternate screen ───────────────────────────────────────────────────── + + /// Switch to or from the alternate screen (DECSET/DECRST 1049). + pub fn set_alt_screen(&mut self, enable: bool) { + let currently_alt = self.modes.contains(TerminalModes::ALT_SCREEN); + if enable == currently_alt { + return; + } + if enable { + // Save main screen + cursor, switch to blank alt screen. + self.alt_screen = Some(self.screen.clone()); + self.alt_cursor = Some(self.cursor); + self.screen = vec![Cell::blank(); self.cols * self.rows]; + self.cursor = Cursor::default(); + self.pending_wrap = false; + self.modes.insert(TerminalModes::ALT_SCREEN); + } else { + // Restore main screen. + if let Some(s) = self.alt_screen.take() { + self.screen = s; + } + if let Some(c) = self.alt_cursor.take() { + self.cursor = c; + } + self.pending_wrap = false; + self.modes.remove(TerminalModes::ALT_SCREEN); + } + } + + // ─── Mode setting (DECSET / DECRST) ────────────────────────────────────── + + /// Apply a DEC private mode set (`?h`) or reset (`?l`). + pub fn set_dec_mode(&mut self, mode: u16, value: bool) { + match mode { + 1 => self.modes.set(TerminalModes::DECCKM, value), // DECCKM — application cursor keys + 6 => {} // DECOM — origin mode (simplified: ignore) + 7 => { + self.modes.set(TerminalModes::AUTO_WRAP, value); + } + 12 => {} // DECSCNM / cursor blink + 25 => { + self.modes.set(TerminalModes::CURSOR_VIS, value); + } + 1000 | 1002 | 1003 => { + self.modes.set(TerminalModes::MOUSE, value); + self.mouse_mode = if value { mode } else { 0 }; + } + 1004 => { + self.modes.set(TerminalModes::FOCUS_EVENTS, value); + } + 1049 => self.set_alt_screen(value), + 2004 => { + self.modes.set(TerminalModes::BRACKET_PASTE, value); + } + _ => {} + } + } + + /// Apply an ANSI mode set (`h`) or reset (`l`). + pub fn set_ansi_mode(&mut self, mode: u16, value: bool) { + match mode { + 4 => { + self.modes.set(TerminalModes::INSERT_MODE, value); + } + 20 => {} // LNM — linefeed / newline mode + _ => {} + } + } + + // ─── Window title ───────────────────────────────────────────────────────── + + pub fn set_title(&mut self, title: &str) { + self.title = title.to_string(); + } + + // ─── VT response queue ──────────────────────────────────────────────────── + + /// DSR 6 — cursor position report: `\x1b[{row};{col}R`. + pub fn cursor_pos_report(&mut self) { + let row = self.cursor.row + 1; + let col = self.cursor.col + 1; + self.pending_responses + .push(format!("\x1b[{row};{col}R").into_bytes()); + } + + /// DSR 5 — operating status: `\x1b[0n` (device OK). + pub fn operating_status_report(&mut self) { + self.pending_responses.push(b"\x1b[0n".to_vec()); + } + + /// DA1 — primary device attributes: `\x1b[?1;2c` (VT100 + advanced video). + pub fn device_attrs_report(&mut self) { + self.pending_responses.push(b"\x1b[?1;2c".to_vec()); + } + + // ─── Tab stops ──────────────────────────────────────────────────────────── + + /// TBC — tab clear. `mode` 0 = clear current stop, 3 = clear all stops. + pub fn clear_tab_stops(&mut self, mode: u16) { + match mode { + 0 => { + let col = self.cursor.col; + if col < self.tab_stops.len() { + self.tab_stops[col] = false; + } + } + 3 => { + for t in &mut self.tab_stops { + *t = false; + } + } + _ => {} + } + } + + /// HTS — set tab stop at current column. + pub fn set_tab_stop(&mut self) { + let col = self.cursor.col; + if col < self.tab_stops.len() { + self.tab_stops[col] = true; + } + } + + /// CBT — cursor backward tab `n` stops. + pub fn cursor_backward_tab(&mut self, n: usize) { + let mut count = n; + let mut col = self.cursor.col; + while col > 0 && count > 0 { + col -= 1; + if self.tab_stops[col] { + count -= 1; + } + } + self.cursor.col = col; + self.pending_wrap = false; + } + + // ─── Reset ──────────────────────────────────────────────────────────────── + + /// RIS — full reset (ESC c). + pub fn full_reset(&mut self) { + let cols = self.cols; + let rows = self.rows; + let sl = self.scrollback_lines; + *self = Self::new(cols, rows, sl); + } + + // ─── Kitty Graphics Protocol ────────────────────────────────────────────── + + /// Process the payload of a Kitty Graphics Protocol APC sequence. + /// + /// `payload` is everything after the leading `'G'` byte that distinguishes + /// a Kitty APC from other SOS/PM/APC sequences. The caller (the VT parser) + /// is responsible for stripping that leading byte. + /// + /// When the action (`a=T` or `a=p`) requires placing an image and the final + /// chunk has arrived, a [`KittyPlacement`] is pushed at the current cursor + /// position. + pub fn dispatch_apc(&mut self, payload: &[u8]) { + let ctrl = crate::kitty::parse_header(payload); + let action = ctrl.action.unwrap_or('t'); // default = transmit only + let id = ctrl.image_id.unwrap_or(0); + + // Delete action: evict the image and mark GPU texture for removal. + if action == 'd' { + if id != 0 { + self.kitty.remove(id); + self.kitty_placements.retain(|p| p.image_id != id); + self.kitty_deleted_ids.push(id); + } + return; + } + + // Ingest this chunk (handles multi-chunk assembly internally). + self.kitty.ingest(payload); + + // If this is the final chunk and the action requires display, place it. + if !ctrl.more && matches!(action, 'p' | 'T') { + if let Some(img) = self.kitty.get(id) { + self.kitty_placements.push(KittyPlacement { + image_id: id, + col: self.cursor.col, + row: self.cursor.row, + pixel_width: img.width, + pixel_height: img.height, + }); + } + } + } +} diff --git a/winiterm/src/tray.rs b/winiterm/src/tray.rs new file mode 100644 index 0000000..094c9cf --- /dev/null +++ b/winiterm/src/tray.rs @@ -0,0 +1,158 @@ +//! System tray icon (Phase 11). +//! +//! Creates a persistent system-tray presence using the `tray-icon` crate. +//! The tray icon lets the user restore the winiterm window after it has been +//! hidden (e.g. by closing it while `tray_icon = true` is set in the Lua +//! config). +//! +//! # Right-click menu (D2) +//! +//! A context menu with two items is attached: +//! * **Show winiterm** — brings the window back to the foreground. +//! * **Quit** — terminates the process cleanly. +//! +//! # Event model +//! +//! We use the simple polling model: `poll_events` is called from winit's +//! `about_to_wait` hook and reads from the crossbeam channels exposed by +//! `TrayIconEvent::receiver()` (left-clicks) and `muda::MenuEvent::receiver()` +//! (context-menu item clicks). + +use muda::{Menu, MenuEvent, MenuId, MenuItem}; +use tray_icon::{MouseButton, TrayIcon, TrayIconBuilder, TrayIconEvent}; + +// ─── TrayAction ─────────────────────────────────────────────────────────────── + +/// The action that the caller should take after a tray event. +pub enum TrayAction { + /// Make the main window visible and bring it to the foreground. + Show, + /// Exit the application. + Quit, +} + +// ─── TrayHandle ─────────────────────────────────────────────────────────────── + +/// Opaque handle to a live system tray icon. +/// +/// The inner [`TrayIcon`] keeps the OS tray entry alive for as long as this +/// value exists. Drop it to remove the icon from the system tray. +pub struct TrayHandle { + _icon: TrayIcon, + /// The menu-item ID of the "Quit" entry, used to match `MenuEvent`s. + quit_id: MenuId, +} + +// ─── Public API ─────────────────────────────────────────────────────────────── + +/// Create a system tray icon with a solid-cyan 16 × 16 default icon and a +/// right-click context menu containing "Show winiterm" and "Quit" items. +/// +/// Returns `None` if the tray icon could not be created (e.g. because no +/// system tray is available, or the underlying platform call failed). +pub fn create_tray_icon() -> Option { + // Build the right-click context menu. + let menu = Menu::new(); + let show_item = MenuItem::new("Show winiterm", true, None); + let quit_item = MenuItem::new("Quit", true, None); + let quit_id = quit_item.id().clone(); + let _ = menu.append(&show_item); + let _ = menu.append(&quit_item); + + let icon = make_default_icon()?; + + let tray = TrayIconBuilder::new() + .with_tooltip("winiterm") + .with_icon(icon) + .with_menu(Box::new(menu)) + .build() + .map_err(|e| log::warn!("tray icon creation failed: {e}")) + .ok()?; + + log::info!("system tray icon created"); + Some(TrayHandle { + _icon: tray, + quit_id, + }) +} + +/// Poll for pending tray events without blocking. +/// +/// Returns `Some(TrayAction)` when a relevant event occurred, or `None` when +/// the queues are empty. +/// +/// Priority: left-click (show) is checked first, then the context-menu channel. +pub fn poll_events(tray: &TrayHandle) -> Option { + // Left-click on the tray icon → show window. + while let Ok(event) = TrayIconEvent::receiver().try_recv() { + if let TrayIconEvent::Click { + button: MouseButton::Left, + .. + } = event + { + return Some(TrayAction::Show); + } + } + + // Context-menu item clicks. + while let Ok(event) = MenuEvent::receiver().try_recv() { + if event.id == tray.quit_id { + return Some(TrayAction::Quit); + } else { + // Any other item (currently just "Show winiterm") → show window. + return Some(TrayAction::Show); + } + } + + None +} + +// ─── Icon helper ────────────────────────────────────────────────────────────── + +/// Generate a plain 16 × 16 RGBA icon filled with the winiterm brand colour +/// (a bright cyan, #1EB3FF). +fn make_default_icon() -> Option { + const W: u32 = 16; + const H: u32 = 16; + // Draw a rounded-ish "W" shape in cyan on a dark background. + let mut rgba = vec![0u8; (W * H * 4) as usize]; + for y in 0..H { + for x in 0..W { + let i = ((y * W + x) * 4) as usize; + // Background: dark navy + rgba[i] = 0x10; // R + rgba[i + 1] = 0x14; // G + rgba[i + 2] = 0x1e; // B + rgba[i + 3] = 0xff; // A + // "W" glyph pattern: two outer legs + a valley + two inner legs + // (pixel art at 16 × 16) + let in_w = matches!( + (x, y), + // top row + (1, 2) | (2, 2) | (13, 2) | (14, 2) | + // left leg descending + (2, 3) | (2, 4) | (2, 5) | (2, 6) | (2, 7) | + (3, 7) | (3, 8) | + // right leg descending + (13, 3) | (13, 4) | (13, 5) | (13, 6) | (13, 7) | + (12, 7) | (12, 8) | + // inner left leg ascending + (4, 9) | (4, 10) | (5, 10) | (5, 11) | + // inner right leg ascending + (11, 9) | (11, 10) | (10, 10) | (10, 11) | + // valley bottom + (6, 11) | (7, 11) | (8, 11) | (9, 11) | + // base closure + (1, 3) | (14, 3) + ); + if in_w { + rgba[i] = 0x1e; // R + rgba[i + 1] = 0xb3; // G + rgba[i + 2] = 0xff; // B + } + } + } + tray_icon::Icon::from_rgba(rgba, W, H) + .map_err(|e| log::warn!("tray icon pixel data invalid: {e}")) + .ok() +} diff --git a/winiterm/src/vt_parser.rs b/winiterm/src/vt_parser.rs new file mode 100644 index 0000000..82f268d --- /dev/null +++ b/winiterm/src/vt_parser.rs @@ -0,0 +1,680 @@ +//! VT/ANSI escape-sequence parser. +//! +//! Implements a state machine based on Paul Williams' VT100/VT500 parser: +//! +//! +//! The parser processes raw bytes and dispatches semantic actions directly to +//! a [`Terminal`]. It is deliberately zero-allocation on the hot path; OSC and +//! DCS buffers are `Vec` but only extended on unusual sequences. + +use crate::terminal::Terminal; + +// ─── State ──────────────────────────────────────────────────────────────────── + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum State { + Ground, + Escape, + EscapeIntermediate, + CsiEntry, + CsiParam, + CsiIntermediate, + CsiIgnore, + OscString, + OscStringSt, // received ESC inside OSC, waiting for '\' to complete ST + DcsEntry, + DcsPassthrough, + DcsIgnore, + SosPmApcString, + SosPmApcStringSt, // received ESC inside SosPmApcString, waiting for '\' +} + +// ─── Parser ─────────────────────────────────────────────────────────────────── + +/// Maximum number of CSI parameters we track. +const MAX_PARAMS: usize = 32; +/// Maximum number of CSI intermediate bytes. +const MAX_INTERMEDIATES: usize = 4; + +/// Stateful VT/ANSI byte-stream parser. +pub struct Parser { + state: State, + /// Raw parameter values. Parameters are `u32` so that very large values + /// (e.g. OSC colour indices) can be accumulated without truncation. + params: [u32; MAX_PARAMS], + /// Number of valid entries in `params`. + param_count: usize, + /// Digit accumulator for the parameter currently being parsed. + current_param: u32, + /// Optional DEC-private marker byte: one of `?`, `<`, `=`, `>`. + private_marker: u8, + /// Intermediate bytes collected during an escape/CSI sequence. + intermediates: [u8; MAX_INTERMEDIATES], + intermediate_count: usize, + /// OSC string accumulation buffer. + osc_buf: Vec, + /// DCS / APC string accumulation buffer (ignored in Phase 2). + dcs_buf: Vec, + // ── UTF-8 multi-byte decoder ──────────────────────────────────────────── + utf8_buf: [u8; 4], + utf8_idx: usize, + utf8_needed: usize, +} + +impl Parser { + pub fn new() -> Self { + Self { + state: State::Ground, + params: [0; MAX_PARAMS], + param_count: 0, + current_param: 0, + private_marker: 0, + intermediates: [0; MAX_INTERMEDIATES], + intermediate_count: 0, + osc_buf: Vec::new(), + dcs_buf: Vec::new(), + utf8_buf: [0; 4], + utf8_idx: 0, + utf8_needed: 0, + } + } + + /// Feed `data` through the parser, dispatching actions to `terminal`. + #[inline] + pub fn parse(&mut self, data: &[u8], terminal: &mut Terminal) { + for &byte in data { + self.process_byte(byte, terminal); + } + } + + // ─── Per-byte dispatch ──────────────────────────────────────────────────── + + #[inline] + fn process_byte(&mut self, byte: u8, terminal: &mut Terminal) { + // ── State-independent C0 transitions ───────────────────────────────── + // CAN (0x18) and SUB (0x1a): cancel current sequence → Ground. + if byte == 0x18 || byte == 0x1a { + self.state = State::Ground; + return; + } + + match self.state { + State::Ground => self.ground(byte, terminal), + State::Escape => self.escape(byte, terminal), + State::EscapeIntermediate => self.escape_intermediate(byte, terminal), + State::CsiEntry => self.csi_entry(byte, terminal), + State::CsiParam => self.csi_param(byte, terminal), + State::CsiIntermediate => self.csi_intermediate(byte, terminal), + State::CsiIgnore => self.csi_ignore(byte), + State::OscString => self.osc_string(byte, terminal), + State::OscStringSt => self.osc_string_st(byte, terminal), + State::DcsEntry | State::DcsPassthrough | State::DcsIgnore => { + self.dcs(byte); + } + State::SosPmApcString => self.sos_pm_apc(byte, terminal), + State::SosPmApcStringSt => self.sos_pm_apc_st(byte, terminal), + } + } + + // ─── State handlers ─────────────────────────────────────────────────────── + + fn ground(&mut self, byte: u8, terminal: &mut Terminal) { + match byte { + // C0 controls + 0x00..=0x08 | 0x0e..=0x1a | 0x1c..=0x1f => { + terminal.execute_c0(byte); + } + 0x09 | 0x0a | 0x0b | 0x0c | 0x0d => { + terminal.execute_c0(byte); + } + 0x1b => { + self.reset_params(); + self.state = State::Escape; + } + 0x7f => {} // DEL — ignore + // ASCII printable + 0x20..=0x7e => { + terminal.write_char(byte as char); + } + // UTF-8 multi-byte sequence start + 0xc0..=0xdf => self.utf8_start(byte, 2), + 0xe0..=0xef => self.utf8_start(byte, 3), + 0xf0..=0xf7 => self.utf8_start(byte, 4), + // UTF-8 continuation or invalid — try to continue accumulation + 0x80..=0xbf => self.utf8_continue(byte, terminal), + _ => {} + } + } + + fn escape(&mut self, byte: u8, terminal: &mut Terminal) { + match byte { + // Enter CSI + b'[' => { + self.reset_params(); + self.state = State::CsiEntry; + } + // Enter OSC + b']' => { + self.osc_buf.clear(); + self.state = State::OscString; + } + // Enter DCS + b'P' => { + self.dcs_buf.clear(); + self.state = State::DcsEntry; + } + // SOS / PM / APC (all absorbed, APC used by Kitty in a later phase) + b'X' | b'^' | b'_' => { + self.dcs_buf.clear(); + self.state = State::SosPmApcString; + } + // String terminator (bare ESC \) — only meaningful inside OSC/DCS + b'\\' => { + self.state = State::Ground; + } + // Intermediate byte (0x20–0x2f) + 0x20..=0x2f => { + self.collect_intermediate(byte); + self.state = State::EscapeIntermediate; + } + // Two-character ESC sequences (final byte 0x30–0x7e) + 0x30..=0x7e => { + self.dispatch_esc(byte, terminal); + self.state = State::Ground; + } + // Nested ESC — re-enter Escape, reset intermediates + 0x1b => { + self.reset_params(); + } + _ => { + self.state = State::Ground; + } + } + } + + fn escape_intermediate(&mut self, byte: u8, terminal: &mut Terminal) { + match byte { + 0x20..=0x2f => self.collect_intermediate(byte), + 0x30..=0x7e => { + self.dispatch_esc(byte, terminal); + self.state = State::Ground; + } + _ => { + self.state = State::Ground; + } + } + } + + fn csi_entry(&mut self, byte: u8, terminal: &mut Terminal) { + match byte { + // DEC private marker + b'?' | b'<' | b'=' | b'>' => { + self.private_marker = byte; + self.state = State::CsiParam; + } + // Digit / semicolon — start parameter + b'0'..=b'9' => { + self.current_param = (byte - b'0') as u32; + self.state = State::CsiParam; + } + b';' => { + self.push_param(); + self.state = State::CsiParam; + } + // Colon sub-parameter: treat as semicolon for now + b':' => { + self.push_param(); + self.state = State::CsiParam; + } + // Intermediate bytes + 0x20..=0x2f => { + self.collect_intermediate(byte); + self.state = State::CsiIntermediate; + } + // Final byte + 0x40..=0x7e => { + self.push_param(); + self.dispatch_csi(byte, terminal); + self.state = State::Ground; + } + 0x1b => { + self.state = State::Escape; + } + _ => {} + } + } + + fn csi_param(&mut self, byte: u8, terminal: &mut Terminal) { + match byte { + b'0'..=b'9' => { + self.current_param = self + .current_param + .saturating_mul(10) + .saturating_add((byte - b'0') as u32); + } + b';' | b':' => { + self.push_param(); + } + 0x20..=0x2f => { + self.push_param(); + self.collect_intermediate(byte); + self.state = State::CsiIntermediate; + } + 0x40..=0x7e => { + self.push_param(); + self.dispatch_csi(byte, terminal); + self.state = State::Ground; + } + // Invalid parameter byte in range 0x3c–0x3f while in CsiParam + 0x3c..=0x3f => { + self.state = State::CsiIgnore; + } + 0x1b => { + self.state = State::Escape; + } + _ => {} + } + } + + fn csi_intermediate(&mut self, byte: u8, terminal: &mut Terminal) { + match byte { + 0x20..=0x2f => self.collect_intermediate(byte), + 0x40..=0x7e => { + self.dispatch_csi(byte, terminal); + self.state = State::Ground; + } + 0x30..=0x3f => { + self.state = State::CsiIgnore; + } + 0x1b => { + self.state = State::Escape; + } + _ => {} + } + } + + fn csi_ignore(&mut self, byte: u8) { + if matches!(byte, 0x40..=0x7e) { + self.state = State::Ground; + } + } + + fn osc_string(&mut self, byte: u8, terminal: &mut Terminal) { + match byte { + 0x07 => { + // BEL terminates OSC. + let osc = self.osc_buf.clone(); + self.dispatch_osc(&osc, terminal); + self.osc_buf.clear(); + self.state = State::Ground; + } + 0x1b => { + // ESC: could be start of ST (ESC \). + self.state = State::OscStringSt; + } + 0x20..=0xff => { + self.osc_buf.push(byte); + } + _ => {} + } + } + + fn osc_string_st(&mut self, byte: u8, terminal: &mut Terminal) { + if byte == b'\\' { + // ESC \ = ST — terminate OSC. + let osc = self.osc_buf.clone(); + self.dispatch_osc(&osc, terminal); + self.osc_buf.clear(); + } + // Regardless of what follows ESC inside an OSC, return to Ground. + self.state = State::Ground; + } + + fn dcs(&mut self, byte: u8) { + // Phase 2: absorb all DCS content. + if byte == 0x07 || (byte == b'\\') { + self.state = State::Ground; + } + } + + fn sos_pm_apc(&mut self, byte: u8, terminal: &mut Terminal) { + // Accumulate SOS/PM/APC content; dispatch when ST arrives. + match byte { + // BEL (0x07) — abbreviated string terminator used by some apps. + 0x07 => { + self.dispatch_apc_inner(terminal); + self.state = State::Ground; + } + // ESC — could be the start of ST (ESC \). + 0x1b => { + self.state = State::SosPmApcStringSt; + } + // Anything else: accumulate. + _ => { + self.dcs_buf.push(byte); + } + } + } + + fn sos_pm_apc_st(&mut self, byte: u8, terminal: &mut Terminal) { + match byte { + // ESC \ = ST — dispatch and return to Ground. + b'\\' => { + self.dispatch_apc_inner(terminal); + self.state = State::Ground; + } + // Another ESC: the previous ESC was literal data; push it and stay + // in this state waiting for the actual '\'. + 0x1b => { + self.dcs_buf.push(0x1b); + // stay in SosPmApcStringSt + } + // Anything else: the previous ESC was literal data; push both and + // return to accumulating. + _ => { + self.dcs_buf.push(0x1b); + self.dcs_buf.push(byte); + self.state = State::SosPmApcString; + } + } + } + + /// Dispatch the accumulated APC buffer, then clear it. + /// + /// Only Kitty Graphics Protocol sequences (first byte `'G'`) are handled. + fn dispatch_apc_inner(&mut self, terminal: &mut Terminal) { + if self.dcs_buf.first() == Some(&b'G') { + // Pass everything after the leading 'G' to the terminal's Kitty handler. + terminal.dispatch_apc(&self.dcs_buf[1..]); + } + self.dcs_buf.clear(); + } + + // ─── UTF-8 multi-byte decoder ───────────────────────────────────────────── + + #[inline] + fn utf8_start(&mut self, byte: u8, needed: usize) { + self.utf8_buf[0] = byte; + self.utf8_idx = 1; + self.utf8_needed = needed; + } + + #[inline] + fn utf8_continue(&mut self, byte: u8, terminal: &mut Terminal) { + if self.utf8_needed == 0 { + // Stray continuation byte — skip. + return; + } + self.utf8_buf[self.utf8_idx] = byte; + self.utf8_idx += 1; + if self.utf8_idx == self.utf8_needed { + let slice = &self.utf8_buf[..self.utf8_needed]; + if let Ok(s) = std::str::from_utf8(slice) { + if let Some(ch) = s.chars().next() { + terminal.write_char(ch); + } + } + self.utf8_needed = 0; + self.utf8_idx = 0; + } + } + + // ─── Parameter helpers ──────────────────────────────────────────────────── + + #[inline] + fn push_param(&mut self) { + if self.param_count < MAX_PARAMS { + self.params[self.param_count] = self.current_param; + self.param_count += 1; + } + self.current_param = 0; + } + + #[inline] + fn reset_params(&mut self) { + self.param_count = 0; + self.current_param = 0; + self.private_marker = 0; + self.intermediate_count = 0; + } + + #[inline] + fn collect_intermediate(&mut self, byte: u8) { + if self.intermediate_count < MAX_INTERMEDIATES { + self.intermediates[self.intermediate_count] = byte; + self.intermediate_count += 1; + } + } + + /// Return the parsed parameter slice (including the last accumulated digit run). + #[inline] + fn params(&self) -> &[u32] { + &self.params[..self.param_count] + } + + /// Get parameter at `idx`, defaulting to `default` if absent or zero. + #[inline] + fn param_or(&self, idx: usize, default: u32) -> u32 { + let p = self.params.get(idx).copied().unwrap_or(0); + if p == 0 { + default + } else { + p + } + } + + // ─── ESC dispatch ───────────────────────────────────────────────────────── + + fn dispatch_esc(&mut self, final_byte: u8, terminal: &mut Terminal) { + let inter = if self.intermediate_count > 0 { + self.intermediates[0] + } else { + 0 + }; + + match (inter, final_byte) { + // DECSC — save cursor (ESC 7) + (0, b'7') => terminal.save_cursor(), + // DECRC — restore cursor (ESC 8) + (0, b'8') => terminal.restore_cursor(), + // IND — index (line feed without CR) + (0, b'D') => terminal.execute_c0(0x0a), + // NEL — next line + (0, b'E') => { + terminal.execute_c0(0x0a); + terminal.execute_c0(0x0d); + } + // HTS — set tab stop at current column + (0, b'H') => terminal.set_tab_stop(), + // RI — reverse index + (0, b'M') => terminal.reverse_index(), + // RIS — reset to initial state + (0, b'c') => terminal.full_reset(), + // Character set designations (G0/G1) — accept but ignore + (b'(' | b')' | b'*' | b'+', _) => {} + // DECPAM / DECPNM (application / normal keypad) — ignore in Phase 2 + (0, b'=') | (0, b'>') => {} + _ => {} + } + } + + // ─── CSI dispatch ───────────────────────────────────────────────────────── + + fn dispatch_csi(&mut self, final_byte: u8, terminal: &mut Terminal) { + let priv_marker = self.private_marker; + let inter = if self.intermediate_count > 0 { + self.intermediates[0] + } else { + 0 + }; + let params = self.params(); + + match (priv_marker, inter, final_byte) { + // ── Cursor movement ─────────────────────────────────────────────── + // CUU — cursor up + (0, 0, b'A') => terminal.cursor_up(self.param_or(0, 1) as usize), + // CUD — cursor down + (0, 0, b'B') => terminal.cursor_down(self.param_or(0, 1) as usize), + // CUF — cursor forward (right) + (0, 0, b'C') => terminal.cursor_forward(self.param_or(0, 1) as usize), + // CUB — cursor back (left) + (0, 0, b'D') => terminal.cursor_back(self.param_or(0, 1) as usize), + // CNL — cursor next line + (0, 0, b'E') => terminal.cursor_next_line(self.param_or(0, 1) as usize), + // CPL — cursor previous line + (0, 0, b'F') => terminal.cursor_prev_line(self.param_or(0, 1) as usize), + // CHA / HPA — cursor column absolute (1-based → 0-based) + (0, 0, b'G') | (0, 0, b'`') => { + terminal.cursor_col(self.param_or(0, 1).saturating_sub(1) as usize) + } + // CUP / HVP — cursor position (1-based) + (0, 0, b'H') | (0, 0, b'f') => { + let row = self.param_or(0, 1).saturating_sub(1) as usize; + let col = self.param_or(1, 1).saturating_sub(1) as usize; + terminal.cursor_pos(row, col); + } + // VPA — vertical position absolute (1-based) + (0, 0, b'd') => terminal.cursor_row(self.param_or(0, 1).saturating_sub(1) as usize), + + // ── Tab ─────────────────────────────────────────────────────────── + // CBT — cursor backward tab + (0, 0, b'Z') => terminal.cursor_backward_tab(self.param_or(0, 1) as usize), + // TBC — tab clear + (0, 0, b'g') => terminal.clear_tab_stops(params.first().copied().unwrap_or(0) as u16), + + // ── Erase ───────────────────────────────────────────────────────── + // ED — erase in display + (0, 0, b'J') => terminal.erase_in_display(params.first().copied().unwrap_or(0) as u16), + // EL — erase in line + (0, 0, b'K') => terminal.erase_in_line(params.first().copied().unwrap_or(0) as u16), + // ECH — erase characters + (0, 0, b'X') => terminal.erase_chars(self.param_or(0, 1) as usize), + + // ── Insert / Delete ─────────────────────────────────────────────── + // ICH — insert characters + (0, 0, b'@') => terminal.insert_chars(self.param_or(0, 1) as usize), + // IL — insert lines + (0, 0, b'L') => terminal.insert_lines(self.param_or(0, 1) as usize), + // DL — delete lines + (0, 0, b'M') => terminal.delete_lines(self.param_or(0, 1) as usize), + // DCH — delete characters + (0, 0, b'P') => terminal.delete_chars(self.param_or(0, 1) as usize), + + // ── Scroll ──────────────────────────────────────────────────────── + // SU — scroll up + (0, 0, b'S') => terminal.scroll_up(self.param_or(0, 1) as usize), + // SD — scroll down + (0, 0, b'T') => terminal.scroll_down(self.param_or(0, 1) as usize), + + // ── SGR ─────────────────────────────────────────────────────────── + (0, 0, b'm') => terminal.process_sgr(params), + + // ── Scroll region ───────────────────────────────────────────────── + // DECSTBM — set top/bottom margin (1-based) + (0, 0, b'r') => { + let top = self.param_or(0, 1).saturating_sub(1) as usize; + let bottom = self.param_or(1, terminal.rows as u32).saturating_sub(1) as usize; + terminal.set_scroll_region(top, bottom); + } + + // ── Save / Restore cursor ───────────────────────────────────────── + (0, 0, b's') => terminal.save_cursor(), + (0, 0, b'u') => terminal.restore_cursor(), + + // ── Device status report ────────────────────────────────────────── + // DSR (CSI n) — respond to operating-status (5) and cursor-pos (6) queries. + (0, 0, b'n') => match params.first().copied().unwrap_or(0) { + 5 => terminal.operating_status_report(), + 6 => terminal.cursor_pos_report(), + _ => {} + }, + + // ── Device attributes ───────────────────────────────────────────── + // DA1 (CSI c / CSI 0 c) — report VT100 with advanced video option. + (0, 0, b'c') => { + if params.first().copied().unwrap_or(0) == 0 { + terminal.device_attrs_report(); + } + } + + // ── DEC private modes (? prefix) ────────────────────────────────── + (b'?', 0, b'h') => { + for &p in params { + terminal.set_dec_mode(p as u16, true); + } + } + (b'?', 0, b'l') => { + for &p in params { + terminal.set_dec_mode(p as u16, false); + } + } + + // ── ANSI modes ──────────────────────────────────────────────────── + (0, 0, b'h') => { + for &p in params { + terminal.set_ansi_mode(p as u16, true); + } + } + (0, 0, b'l') => { + for &p in params { + terminal.set_ansi_mode(p as u16, false); + } + } + + // ── Cursor style (DECSCUSR) ─────────────────────────────────────── + (0, b' ', b'q') => { + use crate::terminal::CursorShape; + let shape = match params.first().copied().unwrap_or(0) { + 1 | 2 => CursorShape::Block, + 3 | 4 => CursorShape::Underline, + 5 | 6 => CursorShape::Beam, + _ => CursorShape::Block, + }; + terminal.cursor.shape = shape; + } + + // ── Window manipulation (xterm CSI t) — ignore ──────────────────── + (0, 0, b't') => {} + + // Anything else: silently ignore. + _ => {} + } + } + + // ─── OSC dispatch ───────────────────────────────────────────────────────── + + fn dispatch_osc(&self, data: &[u8], terminal: &mut Terminal) { + // Split on the first ';' to separate the command code from the payload. + let Some(sep) = data.iter().position(|&b| b == b';') else { + return; + }; + let command_bytes = &data[..sep]; + let payload = &data[sep + 1..]; + + let command: u32 = std::str::from_utf8(command_bytes) + .ok() + .and_then(|s| s.parse().ok()) + .unwrap_or(u32::MAX); + + match command { + // OSC 0 / OSC 2 — set window title + 0 | 2 => { + if let Ok(title) = std::str::from_utf8(payload) { + terminal.set_title(title); + } + } + // OSC 8 — hyperlinks (Phase 3+) + 8 => {} + // OSC 52 — clipboard (Phase 5+, Lua API) + 52 => {} + // Everything else is silently ignored. + _ => {} + } + } +} + +impl Default for Parser { + fn default() -> Self { + Self::new() + } +} diff --git a/winiterm/src/workspace.rs b/winiterm/src/workspace.rs new file mode 100644 index 0000000..5c3d0df --- /dev/null +++ b/winiterm/src/workspace.rs @@ -0,0 +1,357 @@ +//! Workspace: tab and pane management (Phase 4). +//! +//! A [`Workspace`] owns a set of [`TerminalPane`]s and organises them into +//! [`Tab`]s, each of which has a [`SplitNode`] tree describing how its panes +//! are laid out on screen. + +use std::collections::HashMap; + +use crate::pane::TerminalPane; +use crate::split::{Rect, SplitDir, SplitNode}; + +// ─── Tab ───────────────────────────────────────────────────────────────────── + +/// A single tab in the terminal multiplexer. +pub struct Tab { + /// Display name shown in the tab bar. + pub name: String, + /// Split tree for this tab's panes. + pub root: SplitNode, + /// ID of the currently focused pane. + pub active_pane: usize, +} + +// ─── Workspace ──────────────────────────────────────────────────────────────── + +/// Manages all tabs and panes for one winiterm window. +pub struct Workspace { + /// All tabs, in display order. + pub tabs: Vec, + /// Index of the currently visible tab. + pub active_tab: usize, + /// All live panes keyed by ID. + panes: HashMap, + /// Monotonically increasing ID counter. + next_id: usize, +} + +impl Workspace { + // ── Construction ───────────────────────────────────────────────────────── + + /// Create a workspace with one initial tab containing one shell pane. + pub fn new(shell: Option<&str>, cols: u16, rows: u16, scrollback: usize) -> Self { + let id = 0usize; + let mut panes = HashMap::new(); + match TerminalPane::spawn(id, shell, cols, rows, scrollback) { + Ok(pane) => { + log::info!( + "initial shell spawned (pid={})", + pane.pty.as_ref().map_or(0, |p| p.process_id) + ); + panes.insert(id, pane); + } + Err(e) => { + log::error!("failed to spawn initial shell: {e}"); + } + } + log::info!("workspace created, pane count = {}", panes.len()); + let tab = Tab { + name: "1".to_string(), + root: SplitNode::Leaf(id), + active_pane: id, + }; + Self { + tabs: vec![tab], + active_tab: 0, + panes, + next_id: 1, + } + } + + // ── Internal helpers ────────────────────────────────────────────────────── + + fn current_tab(&self) -> &Tab { + &self.tabs[self.active_tab] + } + + fn current_tab_mut(&mut self) -> &mut Tab { + &mut self.tabs[self.active_tab] + } + + fn alloc_id(&mut self) -> usize { + let id = self.next_id; + self.next_id += 1; + id + } + + // ── Pane accessors ──────────────────────────────────────────────────────── + + /// The currently focused pane ID. + pub fn active_pane_id(&self) -> usize { + self.current_tab().active_pane + } + + /// Reference to the active pane (immutable). + pub fn active_pane(&self) -> Option<&TerminalPane> { + self.panes.get(&self.current_tab().active_pane) + } + + /// Reference to the active pane (mutable). + pub fn active_pane_mut(&mut self) -> Option<&mut TerminalPane> { + let id = self.current_tab().active_pane; + self.panes.get_mut(&id) + } + + /// Reference to any pane by ID. + pub fn pane(&self, id: usize) -> Option<&TerminalPane> { + self.panes.get(&id) + } + + /// Mutable reference to any pane by ID. + pub fn pane_mut(&mut self, id: usize) -> Option<&mut TerminalPane> { + self.panes.get_mut(&id) + } + + // ── Tab management ──────────────────────────────────────────────────────── + + /// Open a new tab with a fresh shell pane. + pub fn new_tab(&mut self, shell: Option<&str>, cols: u16, rows: u16, scrollback: usize) { + let id = self.alloc_id(); + match TerminalPane::spawn(id, shell, cols, rows, scrollback) { + Ok(pane) => { + self.panes.insert(id, pane); + } + Err(e) => { + log::error!("failed to spawn shell for new tab: {e}"); + return; + } + } + let n = self.tabs.len() + 1; + self.tabs.push(Tab { + name: n.to_string(), + root: SplitNode::Leaf(id), + active_pane: id, + }); + self.active_tab = self.tabs.len() - 1; + } + + /// Switch to tab at `idx` (silently ignores out-of-bounds). + pub fn switch_tab(&mut self, idx: usize) { + if idx < self.tabs.len() { + self.active_tab = idx; + } + } + + /// Close the tab at `idx`, killing all its panes. + /// + /// Does nothing if only one tab remains. + pub fn close_tab(&mut self, idx: usize) { + if self.tabs.len() <= 1 { + return; + } + let pane_ids: Vec = self.tabs[idx].root.all_panes(); + for id in pane_ids { + self.panes.remove(&id); + } + self.tabs.remove(idx); + if self.active_tab >= self.tabs.len() { + self.active_tab = self.tabs.len() - 1; + } + } + + // ── Pane splitting ──────────────────────────────────────────────────────── + + /// Split the active pane along `dir`, spawning a new shell in the new slot. + pub fn split_active(&mut self, dir: SplitDir, shell: Option<&str>, scrollback: usize) { + let (cols, rows) = { + match self.active_pane() { + Some(p) => ((p.terminal.cols as u16).max(2) / 2, p.terminal.rows as u16), + None => return, + } + }; + + let new_id = self.alloc_id(); + match TerminalPane::spawn(new_id, shell, cols, rows, scrollback) { + Ok(pane) => { + self.panes.insert(new_id, pane); + } + Err(e) => { + log::error!("failed to spawn shell for split: {e}"); + return; + } + } + + let active_id = self.current_tab().active_pane; + self.current_tab_mut().root.split_at(active_id, dir, new_id); + self.current_tab_mut().active_pane = new_id; + } + + /// Close the active pane. If it is the last pane in the tab, closes the + /// tab instead (unless it is the last tab, in which case nothing happens). + pub fn close_active_pane(&mut self) { + let active_id = self.current_tab().active_pane; + let panes_in_tab = self.current_tab().root.all_panes(); + if panes_in_tab.len() <= 1 { + let idx = self.active_tab; + self.close_tab(idx); + return; + } + self.panes.remove(&active_id); + self.current_tab_mut().root.remove(active_id); + let new_active = self.current_tab().root.first_pane(); + self.current_tab_mut().active_pane = new_active; + } + + // ── Focus management ────────────────────────────────────────────────────── + + /// Move focus to the next pane in tree order (wraps around). + pub fn focus_next(&mut self) { + let panes = self.current_tab().root.all_panes(); + let cur = self.current_tab().active_pane; + if let Some(pos) = panes.iter().position(|&id| id == cur) { + let next = panes[(pos + 1) % panes.len()]; + self.current_tab_mut().active_pane = next; + } + } + + /// Move focus to the previous pane in tree order (wraps around). + pub fn focus_prev(&mut self) { + let panes = self.current_tab().root.all_panes(); + let cur = self.current_tab().active_pane; + if let Some(pos) = panes.iter().position(|&id| id == cur) { + let prev = if pos == 0 { panes.len() - 1 } else { pos - 1 }; + self.current_tab_mut().active_pane = panes[prev]; + } + } + + // ── Layout ──────────────────────────────────────────────────────────────── + + /// Compute the pixel [`Rect`] for every pane in the active tab. + /// + /// `tab_bar_h` pixels are reserved at the top for the tab bar. + pub fn layout(&self, w: f32, h: f32, tab_bar_h: f32) -> HashMap { + let mut out = HashMap::new(); + let content = Rect::new(0.0, tab_bar_h, w, (h - tab_bar_h).max(0.0)); + self.current_tab().root.layout(content, &mut out); + out + } + + /// Resize every pane in the active tab to fit the new window dimensions. + pub fn resize_all(&mut self, w: f32, h: f32, cell_w: f32, cell_h: f32, tab_bar_h: f32) { + let rects = self.layout(w, h, tab_bar_h); + for (id, rect) in &rects { + let cols = rect.cell_cols(cell_w); + let rows = rect.cell_rows(cell_h); + if let Some(pane) = self.panes.get_mut(id) { + pane.resize(cols, rows); + } + } + } + + // ── Output processing ───────────────────────────────────────────────────── + + /// Drain PTY output for **all** panes across all tabs. + /// + /// Returns `true` if any pane changed (i.e. a redraw is needed). + pub fn process_all_output(&mut self) -> bool { + let mut dirty = false; + + for pane in self.panes.values_mut() { + if pane.process_output() { + dirty = true; + } + } + + // Collect dead panes after iteration. + let dead: Vec = self + .panes + .iter() + .filter(|(_, p)| p.is_dead()) + .map(|(id, _)| *id) + .collect(); + + for id in dead { + log::info!("removing dead pane {id}"); + self.panes.remove(&id); + dirty = true; + + // Remove from each tab's split tree; close empty tabs. + let mut empty_tabs: Vec = Vec::new(); + for (idx, tab) in self.tabs.iter_mut().enumerate() { + let count = tab.root.all_panes().len(); + if count == 1 && tab.root.all_panes().first() == Some(&id) { + empty_tabs.push(idx); + } else { + tab.root.remove(id); + if tab.active_pane == id { + tab.active_pane = tab.root.first_pane(); + } + } + } + // Close empty tabs in reverse order to preserve indices. + for &idx in empty_tabs.iter().rev() { + if self.tabs.len() > 1 { + self.tabs.remove(idx); + if self.active_tab >= self.tabs.len() { + self.active_tab = self.tabs.len() - 1; + } + } + } + } + + dirty + } + + // ── Scrollback viewport ─────────────────────────────────────────────────── + + /// Scroll the active pane's viewport up by `lines` into the scrollback buffer. + pub fn scroll_up_active(&mut self, lines: usize) { + let id = self.current_tab().active_pane; + if let Some(pane) = self.panes.get_mut(&id) { + let max = pane.terminal.scrollback.len(); + pane.scroll_offset = (pane.scroll_offset + lines).min(max); + } + } + + /// Scroll the active pane's viewport down by `lines` (towards live output). + pub fn scroll_down_active(&mut self, lines: usize) { + let id = self.current_tab().active_pane; + if let Some(pane) = self.panes.get_mut(&id) { + pane.scroll_offset = pane.scroll_offset.saturating_sub(lines); + } + } + + /// Snap the active pane's viewport back to the bottom (live output). + pub fn scroll_to_bottom_active(&mut self) { + let id = self.current_tab().active_pane; + if let Some(pane) = self.panes.get_mut(&id) { + pane.scroll_offset = 0; + } + } + + // ── Write to active pane ────────────────────────────────────────────────── + + /// Write raw bytes to the active pane's shell stdin. + pub fn write_to_active(&self, data: &[u8]) { + if let Some(pane) = self.active_pane() { + pane.write(data); + } + } + + // ── Queries ─────────────────────────────────────────────────────────────── + + /// Pane IDs for the active tab, in tree order. + pub fn current_tab_panes(&self) -> Vec { + self.current_tab().root.all_panes() + } + + /// Number of open tabs. + pub fn tab_count(&self) -> usize { + self.tabs.len() + } + + /// `true` when all panes in all tabs are dead (signal to exit the app). + pub fn is_empty(&self) -> bool { + self.panes.is_empty() + } +} diff --git a/winiterm/winiterm.log b/winiterm/winiterm.log new file mode 100644 index 0000000..e69de29 diff --git a/winiterm/winiterm_startup.log b/winiterm/winiterm_startup.log new file mode 100644 index 0000000..fc348d7 --- /dev/null +++ b/winiterm/winiterm_startup.log @@ -0,0 +1,694 @@ +[2026-06-09T07:39:17Z DEBUG winiterm::config] config not found at C:\Users\DG2210\AppData\Roaming\winiterm\config.lua, using defaults +[2026-06-09T07:39:17Z WARN wgpu_hal::vulkan::instance] InstanceFlags::VALIDATION requested, but unable to find layer: VK_LAYER_KHRONOS_validation +[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)] + No valid vk_loader_settings.json file found, no loader settings will be active +[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x1faeef2cf80, name: ?) +[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)] + windows_get_app_package_manifest_path: Failed to find mapping layers packages by family name +[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x1faeef2cf80, name: ?) +[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)] + Checking for Layer Manifest files in Registry at HKEY_LOCAL_MACHINE\SOFTWARE\Khronos\Vulkan\ImplicitLayers +[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x1faeef2cf80, name: ?) +[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)] + windows_get_device_registry_files: opening device PCI\VEN_8086&DEV_A721&SUBSYS_8B7C103C&REV_04\3&11583659&0&10 +[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x1faeef2cf80, name: ?) +[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)] + windows_get_device_registry_entry: Device ID(1) Does not contain a value for "VulkanImplicitLayers" +[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x1faeef2cf80, name: ?) +[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)] + windows_get_device_registry_files: Opening child device 2 - SWD\DRIVERENUM\IGS&4&168EC99&0 +[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x1faeef2cf80, name: ?) +[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)] + windows_get_device_registry_entry: Device ID(2) Does not contain a value for "VulkanImplicitLayers" +[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x1faeef2cf80, name: ?) +[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)] + windows_get_device_registry_files: Opening child device 3 - DISPLAY\LGD071E\4&168EC99&0&UID8388688 +[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x1faeef2cf80, name: ?) +[2026-06-09T07:39:17Z DEBUG wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)] + windows_get_device_registry_files: GUID for 3 is not SoftwareComponent skipping +[2026-06-09T07:39:17Z DEBUG wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x1faeef2cf80, name: ?) +[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)] + windows_get_device_registry_files: Opening child device 4 - DISPLAY\PHL095C\4&168EC99&0&UID8261 +[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x1faeef2cf80, name: ?) +[2026-06-09T07:39:17Z DEBUG wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)] + windows_get_device_registry_files: GUID for 4 is not SoftwareComponent skipping +[2026-06-09T07:39:17Z DEBUG wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x1faeef2cf80, name: ?) +[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)] + windows_get_device_registry_files: found no registry files +[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x1faeef2cf80, name: ?) +[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)] + Located json file "C:\ProgramData\obs-studio-hook\obs-vulkan64.json" from registry "HKEY_LOCAL_MACHINE\SOFTWARE\Khronos\Vulkan\ImplicitLayers" +[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x1faeef2cf80, name: ?) +[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)] + Found manifest file C:\ProgramData\obs-studio-hook\obs-vulkan64.json (file version 1.1.2) +[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x1faeef2cf80, name: ?) +[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)] + Checking for Layer Manifest files in Registry at HKEY_LOCAL_MACHINE\SOFTWARE\Khronos\Vulkan\ExplicitLayers +[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x1faeef2cf80, name: ?) +[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)] + windows_get_device_registry_files: opening device PCI\VEN_8086&DEV_A721&SUBSYS_8B7C103C&REV_04\3&11583659&0&10 +[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x1faeef2cf80, name: ?) +[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)] + windows_get_device_registry_entry: Device ID(1) Does not contain a value for "VulkanExplicitLayers" +[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x1faeef2cf80, name: ?) +[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)] + windows_get_device_registry_files: Opening child device 2 - SWD\DRIVERENUM\IGS&4&168EC99&0 +[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x1faeef2cf80, name: ?) +[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)] + windows_get_device_registry_entry: Device ID(2) Does not contain a value for "VulkanExplicitLayers" +[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x1faeef2cf80, name: ?) +[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)] + windows_get_device_registry_files: Opening child device 3 - DISPLAY\LGD071E\4&168EC99&0&UID8388688 +[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x1faeef2cf80, name: ?) +[2026-06-09T07:39:17Z DEBUG wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)] + windows_get_device_registry_files: GUID for 3 is not SoftwareComponent skipping +[2026-06-09T07:39:17Z DEBUG wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x1faeef2cf80, name: ?) +[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)] + windows_get_device_registry_files: Opening child device 4 - DISPLAY\PHL095C\4&168EC99&0&UID8261 +[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x1faeef2cf80, name: ?) +[2026-06-09T07:39:17Z DEBUG wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)] + windows_get_device_registry_files: GUID for 4 is not SoftwareComponent skipping +[2026-06-09T07:39:17Z DEBUG wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x1faeef2cf80, name: ?) +[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)] + windows_get_device_registry_files: found no registry files +[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x1faeef2cf80, name: ?) +[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)] + Found no registry files in HKEY_CURRENT_USER\SOFTWARE\Khronos\Vulkan\ExplicitLayers +[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x1faeef2cf80, name: ?) +[2026-06-09T07:39:17Z WARN wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)] + windows_read_data_files_in_registry: Registry lookup failed to get layer manifest files. +[2026-06-09T07:39:17Z WARN wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x1faeef2cf80, name: ?) +[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)] + windows_get_app_package_manifest_path: Failed to find mapping layers packages by family name +[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x1faeef2cf80, name: ?) +[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)] + Checking for Driver Manifest files in Registry at HKEY_LOCAL_MACHINE\SOFTWARE\Khronos\Vulkan\Drivers +[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x1faeef2cf80, name: ?) +[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)] + windows_add_json_entry: Located json file "C:\WINDOWS\System32\DriverStore\FileRepository\iigd_dch.inf_amd64_0eac281dc2d07a5f\igvk64.json" from PnP registry: E +[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x1faeef2cf80, name: ?) +[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)] + Found no registry files in HKEY_LOCAL_MACHINE\SOFTWARE\Khronos\Vulkan\Drivers +[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x1faeef2cf80, name: ?) +[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)] + Found ICD manifest file C:\WINDOWS\System32\DriverStore\FileRepository\iigd_dch.inf_amd64_0eac281dc2d07a5f\igvk64.json, version 1.0.0 +[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x1faeef2cf80, name: ?) +[2026-06-09T07:39:17Z DEBUG wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)] + Searching for ICD drivers named .\igvk64.dll +[2026-06-09T07:39:17Z DEBUG wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x1faeef2cf80, name: ?) +[2026-06-09T07:39:17Z DEBUG wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)] + Loading layer library C:\ProgramData\obs-studio-hook\.\graphics-hook64.dll +[2026-06-09T07:39:17Z DEBUG wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x1faeef2cf80, name: ?) +[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)] + Insert instance layer "VK_LAYER_OBS_HOOK" (C:\ProgramData\obs-studio-hook\.\graphics-hook64.dll) +[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x1faeef2cf80, name: ?) +[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)] + vkCreateInstance layer callstack setup to: +[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x1faeef2cf80, name: ?) +[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)] + +[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x1faeef2cf80, name: ?) +[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)] + || +[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x1faeef2cf80, name: ?) +[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)] + +[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x1faeef2cf80, name: ?) +[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)] + || +[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x1faeef2cf80, name: ?) +[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)] + VK_LAYER_OBS_HOOK +[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x1faeef2cf80, name: ?) +[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)] + Type: Implicit +[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x1faeef2cf80, name: ?) +[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)] + Enabled By: Implicit Layer +[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x1faeef2cf80, name: ?) +[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)] + Disable Env Var: DISABLE_VULKAN_OBS_CAPTURE +[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x1faeef2cf80, name: ?) +[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)] + Manifest: C:\ProgramData\obs-studio-hook\obs-vulkan64.json +[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x1faeef2cf80, name: ?) +[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)] + Library: C:\ProgramData\obs-studio-hook\.\graphics-hook64.dll +[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x1faeef2cf80, name: ?) +[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)] + || +[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x1faeef2cf80, name: ?) +[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)] + +[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x1faeef2cf80, name: ?) +[2026-06-09T07:39:17Z DEBUG wgpu_hal::vulkan::instance] Instance version: 0x404139 +[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] Enabling debug utils +[2026-06-09T07:39:17Z DEBUG wgpu_hal::vulkan::instance] Enabling device properties2 +[2026-06-09T07:39:17Z DEBUG wgpu_core::instance] Instance::new: created Vulkan backend +[2026-06-09T07:39:17Z DEBUG wgpu_hal::dx12::instance] Using FXC for shader compilation +[2026-06-09T07:39:17Z DEBUG wgpu_core::instance] Instance::new: created Dx12 backend +[2026-06-09T07:39:17Z DEBUG wgpu_hal::gles::wgl] Enabling GL debug output +[2026-06-09T07:39:17Z DEBUG wgpu_core::instance] Instance::new: created Gl backend +[2026-06-09T07:39:17Z DEBUG wgpu_hal::gles::adapter] Vendor: Intel +[2026-06-09T07:39:17Z DEBUG wgpu_hal::gles::adapter] Renderer: Intel(R) UHD Graphics +[2026-06-09T07:39:17Z DEBUG wgpu_hal::gles::adapter] Version: 4.6.0 - Build 32.0.101.7084 +[2026-06-09T07:39:17Z DEBUG wgpu_hal::gles::adapter] SL version: 4.60 - Build 32.0.101.7084 +[2026-06-09T07:39:17Z DEBUG wgpu_hal::gles::adapter] Supported GL Extensions: { + "GL_ARB_map_buffer_range", + "GL_ARB_texture_rg", + "GL_ARB_multisample", + "GL_ARB_robustness", + "GL_ARB_texture_rectangle", + "GL_INTEL_fragment_shader_ordering", + "GL_ARB_shader_draw_parameters", + "GL_ARB_transform_feedback_overflow_query", + "GL_EXT_texture_swizzle", + "GL_ARB_half_float_pixel", + "GL_ARB_seamless_cubemap_per_texture", + "GL_ARB_window_pos", + "GL_KHR_shader_subgroup_basic", + "GL_ARB_sync", + "GL_SGIS_texture_lod", + "GL_ARB_texture_filter_anisotropic", + "GL_EXT_stencil_two_side", + "GL_ARB_texture_multisample", + "GL_ARB_geometry_shader4", + "GL_AMD_vertex_shader_viewport_index", + "GL_SGIS_texture_edge_clamp", + "GL_EXT_separate_specular_color", + "GL_ARB_shading_language_420pack", + "GL_ARB_color_buffer_float", + "GL_ARB_multi_draw_indirect", + "GL_ARB_occlusion_query", + "GL_EXT_texture_filter_anisotropic", + "GL_KHR_shader_subgroup_clustered", + "GL_ARB_texture_env_dot3", + "GL_ARB_viewport_array", + "GL_EXT_texture_compression_s3tc", + "GL_ARB_compute_shader", + "GL_ARB_shader_bit_encoding", + "GL_ARB_vertex_type_10f_11f_11f_rev", + "GL_ARB_direct_state_access", + "GL_KHR_shader_subgroup_ballot", + "GL_ARB_shader_atomic_counter_ops", + "GL_ARB_shader_atomic_counters", + "GL_INTEL_map_texture", + "GL_EXT_shadow_funcs", + "GL_ARB_texture_compression_bptc", + "GL_ARB_conservative_depth", + "GL_ARB_framebuffer_no_attachments", + "GL_ARB_invalidate_subdata", + "GL_ARB_pipeline_statistics_query", + "GL_ARB_draw_elements_base_vertex", + "GL_KHR_shader_subgroup_shuffle", + "GL_ARB_get_texture_sub_image", + "GL_ARB_texture_cube_map", + "GL_ARB_transform_feedback3", + "GL_NV_conditional_render", + "GL_ARB_texture_border_clamp", + "GL_ARB_texture_query_lod", + "GL_INTEL_framebuffer_CMAA", + "GL_ARB_base_instance", + "GL_KHR_blend_equation_advanced_coherent", + "GL_ARB_texture_buffer_range", + "GL_EXT_shader_framebuffer_fetch", + "GL_EXT_packed_depth_stencil", + "GL_EXT_blend_color", + "GL_EXT_framebuffer_blit", + "GL_ARB_point_sprite", + "GL_ARB_texture_env_add", + "GL_ARB_bindless_texture", + "GL_ARB_program_interface_query", + "GL_ARB_depth_texture", + "GL_ARB_get_program_binary", + "GL_EXT_rescale_normal", + "GL_ARB_debug_output", + "GL_EXT_draw_range_elements", + "GL_ARB_buffer_storage", + "GL_ARB_clear_buffer_object", + "GL_ARB_cull_distance", + "WGL_EXT_swap_control", + "GL_EXT_texture_edge_clamp", + "GL_SGIS_generate_mipmap", + "GL_ARB_ES2_compatibility", + "GL_ARB_uniform_buffer_object", + "GL_ARB_depth_clamp", + "GL_ARB_separate_shader_objects", + "GL_EXT_gpu_shader4", + "GL_ARB_texture_mirrored_repeat", + "GL_ARB_robustness_isolation", + "GL_ARB_texture_env_crossbar", + "GL_ARB_shader_objects", + "GL_EXT_blend_subtract", + "GL_ARB_explicit_attrib_location", + "GL_ARB_fragment_shader_interlock", + "GL_ARB_gpu_shader_fp64", + "GL_ARB_enhanced_layouts", + "GL_KHR_shader_subgroup_shuffle_relative", + "GL_NV_timeline_semaphore", + "GL_EXT_abgr", + "GL_EXT_texture_sRGB", + "GL_EXT_geometry_shader4", + "GL_ARB_instanced_arrays", + "GL_EXT_blend_equation_separate", + "GL_EXT_compiled_vertex_array", + "GL_ARB_conditional_render_inverted", + "GL_EXT_gpu_program_parameters", + "GL_ARB_shader_image_load_store", + "GL_ARB_transpose_matrix", + "GL_ARB_cl_event", + "GL_ARB_fragment_shader", + "GL_ARB_texture_storage_multisample", + "GL_ARB_vertex_attrib_64bit", + "GL_EXT_bgra", + "GL_ARB_explicit_uniform_location", + "GL_EXT_timer_query", + "GL_SUN_multi_draw_arrays", + "GL_ARB_texture_env_combine", + "GL_KHR_debug", + "GL_ARB_tessellation_shader", + "GL_ARB_shader_group_vote", + "GL_ARB_fragment_program", + "GL_ARB_gpu_shader5", + "GL_EXT_texture_env_add", + "GL_EXT_texture_env_combine", + "GL_INTEL_conservative_rasterization", + "GL_KHR_shader_subgroup_vote", + "GL_ARB_clear_texture", + "GL_ARB_texture_swizzle", + "GL_ARB_copy_buffer", + "GL_EXT_direct_state_access", + "GL_ARB_fragment_coord_conventions", + "GL_KHR_shader_subgroup_quad", + "GL_ARB_provoking_vertex", + "GL_EXT_texture_snorm", + "GL_ARB_polygon_offset_clamp", + "GL_ARB_shader_subroutine", + "GL_ARB_texture_barrier", + "GL_ARB_texture_query_levels", + "GL_EXT_blend_minmax", + "GL_ARB_draw_buffers_blend", + "GL_EXT_memory_object", + "GL_EXT_clip_volume_hint", + "GL_AMD_depth_clamp_separate", + "GL_ARB_shadow", + "GL_EXT_texture_rectangle", + "GL_NV_texgen_reflection", + "GL_ARB_internalformat_query2", + "GL_EXT_texture_array", + "GL_ARB_gl_spirv", + "GL_ARB_internalformat_query", + "GL_EXT_packed_pixels", + "GL_ARB_compressed_texture_pixel_storage", + "GL_EXT_stencil_wrap", + "GL_ARB_multi_bind", + "GL_ARB_query_buffer_object", + "GL_ARB_robust_buffer_access_behavior", + "GL_ARB_vertex_array_bgra", + "GL_ATI_separate_stencil", + "GL_EXT_packed_float", + "GL_ARB_arrays_of_arrays", + "GL_ARB_shading_language_packing", + "GL_EXT_semaphore_win32", + "GL_ARB_pixel_buffer_object", + "GL_ARB_clip_control", + "GL_ARB_blend_func_extended", + "GL_ARB_multitexture", + "GL_ARB_shader_precision", + "GL_ARB_map_buffer_alignment", + "GL_ARB_vertex_attrib_binding", + "GL_ARB_texture_buffer_object", + "GL_ARB_draw_instanced", + "GL_ARB_texture_rgb10_a2ui", + "GL_EXT_secondary_color", + "GL_ARB_derivative_control", + "GL_EXT_fog_coord", + "GL_ARB_vertex_type_2_10_10_10_rev", + "GL_ARB_vertex_array_object", + "GL_ARB_fragment_layer_viewport", + "GL_ARB_sample_shading", + "GL_ARB_ES3_1_compatibility", + "GL_ARB_shader_texture_image_samples", + "GL_OVR_multiview", + "GL_EXT_texture3D", + "GL_ARB_framebuffer_object", + "GL_EXT_semaphore", + "GL_ARB_texture_stencil8", + "GL_ARB_texture_view", + "GL_KHR_no_error", + "GL_IBM_texture_mirrored_repeat", + "GL_ARB_framebuffer_sRGB", + "GL_NV_blend_square", + "GL_ARB_fragment_program_shadow", + "GL_ARB_texture_compression", + "GL_ARB_half_float_vertex", + "GL_EXT_draw_buffers2", + "GL_AMD_vertex_shader_layer", + "GL_ARB_shader_storage_buffer_object", + "GL_EXT_framebuffer_multisample", + "GL_KHR_blend_equation_advanced", + "GL_ARB_seamless_cube_map", + "GL_INTEL_performance_query", + "GL_ARB_vertex_shader", + "GL_ARB_texture_buffer_object_rgb32", + "GL_KHR_context_flush_control", + "GL_EXT_texture_shared_exponent", + "GL_ARB_texture_cube_map_array", + "GL_NV_primitive_restart", + "GL_ARB_texture_mirror_clamp_to_edge", + "GL_ARB_texture_float", + "GL_ARB_timer_query", + "GL_EXT_depth_bounds_test", + "GL_ARB_depth_buffer_float", + "GL_3DFX_texture_compression_FXT1", + "GL_EXT_texture_integer", + "GL_KHR_texture_compression_astc_ldr", + "GL_ARB_sampler_objects", + "GL_ARB_transform_feedback2", + "GL_ARB_shading_language_100", + "GL_ARB_draw_indirect", + "GL_ARB_shader_image_size", + "GL_ARB_indirect_parameters", + "GL_ARB_texture_storage", + "GL_ARB_post_depth_coverage", + "GL_ARB_transform_feedback_instanced", + "GL_ARB_vertex_program", + "GL_EXT_blend_func_separate", + "GL_EXT_multi_draw_arrays", + "GL_ARB_spirv_extensions", + "GL_EXT_texture_sRGB_decode", + "GL_INTEL_multi_rate_fragment_shader", + "GL_ARB_shader_texture_lod", + "GL_EXT_shader_integer_mix", + "GL_WIN_swap_hint", + "GL_EXT_texture_storage", + "GL_ARB_stencil_texturing", + "GL_ARB_texture_non_power_of_two", + "GL_ATI_meminfo", + "GL_ARB_point_parameters", + "GL_EXT_framebuffer_object", + "GL_ARB_draw_buffers", + "GL_ARB_texture_gather", + "GL_ARB_compatibility", + "GL_ARB_vertex_buffer_object", + "GL_ARB_shader_stencil_export", + "GL_EXT_transform_feedback", + "GL_EXT_texture_lod_bias", + "GL_ARB_copy_image", + "GL_KHR_shader_subgroup", + "GL_KHR_shader_subgroup_arithmetic", + "GL_ARB_ES3_compatibility", + "GL_EXT_memory_object_win32", + "GL_EXT_polygon_offset_clamp", + "GL_ARB_texture_compression_rgtc", + "GL_ARB_occlusion_query2", + } +[2026-06-09T07:39:17Z INFO wgpu_core::instance] Adapter Vulkan AdapterInfo { name: "Intel(R) UHD Graphics", vendor: 32902, device: 42785, device_type: IntegratedGpu, driver: "Intel Corporation", driver_info: "101.7084", backend: Vulkan } +[2026-06-09T07:39:17Z DEBUG wgpu_hal::vulkan::adapter] Supported extensions: ["VK_KHR_swapchain", "VK_KHR_swapchain_mutable_format", "VK_EXT_robustness2"] +[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)] + Inserted device layer "VK_LAYER_OBS_HOOK" (C:\ProgramData\obs-studio-hook\.\graphics-hook64.dll) +[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x1faeef2cf80, name: ?) +[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)] + vkCreateDevice layer callstack setup to: +[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x1faeef2cf80, name: ?) +[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)] + +[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x1faeef2cf80, name: ?) +[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)] + || +[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x1faeef2cf80, name: ?) +[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)] + +[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x1faeef2cf80, name: ?) +[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)] + || +[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x1faeef2cf80, name: ?) +[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)] + VK_LAYER_OBS_HOOK +[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x1faeef2cf80, name: ?) +[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)] + Type: Implicit +[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x1faeef2cf80, name: ?) +[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)] + Enabled By: Implicit Layer +[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x1faeef2cf80, name: ?) +[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)] + Disable Env Var: DISABLE_VULKAN_OBS_CAPTURE +[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x1faeef2cf80, name: ?) +[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)] + Manifest: C:\ProgramData\obs-studio-hook\obs-vulkan64.json +[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x1faeef2cf80, name: ?) +[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)] + Library: C:\ProgramData\obs-studio-hook\.\graphics-hook64.dll +[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x1faeef2cf80, name: ?) +[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)] + || +[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x1faeef2cf80, name: ?) +[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)] + +[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x1faeef2cf80, name: ?) +[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)] + Using "Intel(R) UHD Graphics" with driver: "C:\WINDOWS\System32\DriverStore\FileRepository\iigd_dch.inf_amd64_0eac281dc2d07a5f\.\igvk64.dll" +[2026-06-09T07:39:17Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x1faeef2cf80, name: ?) +[2026-06-09T07:39:17Z DEBUG wgpu_core::device::global] configuring surface with SurfaceConfiguration { usage: TextureUsages(RENDER_ATTACHMENT), format: Bgra8Unorm, width: 1800, height: 1200, present_mode: AutoVsync, desired_maximum_frame_latency: 2, alpha_mode: Opaque, view_formats: [] } +[2026-06-09T07:39:17Z INFO wgpu_core::device::resource] Device::maintain: waiting for submission index 0 +[2026-06-09T07:39:17Z DEBUG wgpu_core::device::resource] Create view for Texture with 'glyph_atlas' label filters usages to TextureUses(RESOURCE) +[2026-06-09T07:39:17Z DEBUG wgpu_core::resource] Buffer with 'uniforms' label map state -> Idle +[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [0] = Literal(AbstractInt(0)) : Value(Scalar(Scalar { kind: AbstractInt, width: 8 })) +[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [1] = Literal(I32(0)) : Value(Scalar(Scalar { kind: Sint, width: 4 })) +[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [2] = Literal(AbstractInt(0)) : Value(Scalar(Scalar { kind: AbstractInt, width: 8 })) +[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [3] = Literal(I32(0)) : Value(Scalar(Scalar { kind: Sint, width: 4 })) +[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [4] = Literal(AbstractInt(0)) : Value(Scalar(Scalar { kind: AbstractInt, width: 8 })) +[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [5] = Literal(I32(0)) : Value(Scalar(Scalar { kind: Sint, width: 4 })) +[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [6] = Literal(AbstractInt(1)) : Value(Scalar(Scalar { kind: AbstractInt, width: 8 })) +[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [7] = Literal(I32(1)) : Value(Scalar(Scalar { kind: Sint, width: 4 })) +[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [8] = Literal(AbstractInt(0)) : Value(Scalar(Scalar { kind: AbstractInt, width: 8 })) +[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [9] = Literal(I32(0)) : Value(Scalar(Scalar { kind: Sint, width: 4 })) +[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [10] = Literal(AbstractInt(2)) : Value(Scalar(Scalar { kind: AbstractInt, width: 8 })) +[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [11] = Literal(I32(2)) : Value(Scalar(Scalar { kind: Sint, width: 4 })) +[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [12] = Literal(AbstractInt(0)) : Value(Scalar(Scalar { kind: AbstractInt, width: 8 })) +[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [13] = Literal(I32(0)) : Value(Scalar(Scalar { kind: Sint, width: 4 })) +[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [14] = Literal(AbstractInt(1)) : Value(Scalar(Scalar { kind: AbstractInt, width: 8 })) +[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [15] = Literal(I32(1)) : Value(Scalar(Scalar { kind: Sint, width: 4 })) +[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [16] = Literal(AbstractInt(2)) : Value(Scalar(Scalar { kind: AbstractInt, width: 8 })) +[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [17] = Literal(I32(2)) : Value(Scalar(Scalar { kind: Sint, width: 4 })) +[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [18] = Literal(AbstractInt(3)) : Value(Scalar(Scalar { kind: AbstractInt, width: 8 })) +[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [19] = Literal(I32(3)) : Value(Scalar(Scalar { kind: Sint, width: 4 })) +[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [20] = Literal(AbstractInt(4)) : Value(Scalar(Scalar { kind: AbstractInt, width: 8 })) +[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [21] = Literal(I32(4)) : Value(Scalar(Scalar { kind: Sint, width: 4 })) +[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [22] = Literal(AbstractInt(5)) : Value(Scalar(Scalar { kind: AbstractInt, width: 8 })) +[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [23] = Literal(I32(5)) : Value(Scalar(Scalar { kind: Sint, width: 4 })) +[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [24] = Literal(AbstractInt(6)) : Value(Scalar(Scalar { kind: AbstractInt, width: 8 })) +[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [25] = Literal(I32(6)) : Value(Scalar(Scalar { kind: Sint, width: 4 })) +[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [26] = Literal(AbstractInt(7)) : Value(Scalar(Scalar { kind: AbstractInt, width: 8 })) +[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [27] = Literal(I32(7)) : Value(Scalar(Scalar { kind: Sint, width: 4 })) +[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [28] = Literal(AbstractInt(0)) : Value(Scalar(Scalar { kind: AbstractInt, width: 8 })) +[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [29] = Literal(I32(0)) : Value(Scalar(Scalar { kind: Sint, width: 4 })) +[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [30] = Literal(AbstractInt(1)) : Value(Scalar(Scalar { kind: AbstractInt, width: 8 })) +[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [31] = Literal(I32(1)) : Value(Scalar(Scalar { kind: Sint, width: 4 })) +[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [32] = Literal(AbstractInt(2)) : Value(Scalar(Scalar { kind: AbstractInt, width: 8 })) +[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [33] = Literal(I32(2)) : Value(Scalar(Scalar { kind: Sint, width: 4 })) +[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [34] = Literal(AbstractInt(3)) : Value(Scalar(Scalar { kind: AbstractInt, width: 8 })) +[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [35] = Literal(I32(3)) : Value(Scalar(Scalar { kind: Sint, width: 4 })) +[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [36] = Literal(AbstractInt(4)) : Value(Scalar(Scalar { kind: AbstractInt, width: 8 })) +[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [0] = FunctionArgument(0) : Handle([4]) +[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [1] = FunctionArgument(1) : Handle([7]) +[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [2] = LocalVariable([0]) : Value(Pointer { base: [8], space: Function }) +[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [3] = AccessIndex { base: [1], index: 0 } : Handle([0]) +[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [4] = AccessIndex { base: [0], index: 0 } : Handle([0]) +[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [5] = AccessIndex { base: [1], index: 1 } : Handle([0]) +[2026-06-09T07:39:17Z DEBUG naga::front::wgsl::lower::conversion] wgsl automatic_conversion_consensus: ["vec2", "vec2"] +[2026-06-09T07:39:17Z DEBUG naga::front::wgsl::lower::conversion] consensus: "f32" +[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [6] = Binary { op: Multiply, left: [4], right: [5] } : Handle([0]) +[2026-06-09T07:39:17Z DEBUG naga::front::wgsl::lower::conversion] wgsl automatic_conversion_consensus: ["vec2", "vec2"] +[2026-06-09T07:39:17Z DEBUG naga::front::wgsl::lower::conversion] consensus: "f32" +[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [7] = Binary { op: Add, left: [3], right: [6] } : Handle([0]) +[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [8] = AccessIndex { base: [7], index: 0 } : Value(Scalar(Scalar { kind: Float, width: 4 })) +[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [9] = GlobalVariable([0]) : Value(Pointer { base: [1], space: Uniform }) +[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [10] = AccessIndex { base: [9], index: 0 } : Value(Pointer { base: [0], space: Uniform }) +[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [11] = AccessIndex { base: [10], index: 0 } : Value(ValuePointer { size: None, scalar: Scalar { kind: Float, width: 4 }, space: Uniform }) +[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [12] = Load { pointer: [11] } : Value(Scalar(Scalar { kind: Float, width: 4 })) +[2026-06-09T07:39:17Z DEBUG naga::front::wgsl::lower::conversion] wgsl automatic_conversion_consensus: ["f32", "f32"] +[2026-06-09T07:39:17Z DEBUG naga::front::wgsl::lower::conversion] consensus: "f32" +[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [13] = Binary { op: Divide, left: [8], right: [12] } : Value(Scalar(Scalar { kind: Float, width: 4 })) +[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [14] = Literal(AbstractFloat(2.0)) : Value(Scalar(Scalar { kind: AbstractFloat, width: 8 })) +[2026-06-09T07:39:17Z DEBUG naga::front::wgsl::lower::conversion] wgsl automatic_conversion_consensus: ["f32", "{AbstractFloat}"] +[2026-06-09T07:39:17Z DEBUG naga::front::wgsl::lower::conversion] consensus: "f32" +[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [15] = Literal(F32(2.0)) : Value(Scalar(Scalar { kind: Float, width: 4 })) +[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [16] = Binary { op: Multiply, left: [13], right: [15] } : Value(Scalar(Scalar { kind: Float, width: 4 })) +[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [17] = Literal(AbstractFloat(1.0)) : Value(Scalar(Scalar { kind: AbstractFloat, width: 8 })) +[2026-06-09T07:39:17Z DEBUG naga::front::wgsl::lower::conversion] wgsl automatic_conversion_consensus: ["f32", "{AbstractFloat}"] +[2026-06-09T07:39:17Z DEBUG naga::front::wgsl::lower::conversion] consensus: "f32" +[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [18] = Literal(F32(1.0)) : Value(Scalar(Scalar { kind: Float, width: 4 })) +[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [19] = Binary { op: Subtract, left: [16], right: [18] } : Value(Scalar(Scalar { kind: Float, width: 4 })) +[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [20] = AccessIndex { base: [7], index: 1 } : Value(Scalar(Scalar { kind: Float, width: 4 })) +[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [21] = GlobalVariable([0]) : Value(Pointer { base: [1], space: Uniform }) +[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [22] = AccessIndex { base: [21], index: 0 } : Value(Pointer { base: [0], space: Uniform }) +[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [23] = AccessIndex { base: [22], index: 1 } : Value(ValuePointer { size: None, scalar: Scalar { kind: Float, width: 4 }, space: Uniform }) +[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [24] = Load { pointer: [23] } : Value(Scalar(Scalar { kind: Float, width: 4 })) +[2026-06-09T07:39:17Z DEBUG naga::front::wgsl::lower::conversion] wgsl automatic_conversion_consensus: ["f32", "f32"] +[2026-06-09T07:39:17Z DEBUG naga::front::wgsl::lower::conversion] consensus: "f32" +[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [25] = Binary { op: Divide, left: [20], right: [24] } : Value(Scalar(Scalar { kind: Float, width: 4 })) +[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [26] = Unary { op: Negate, expr: [25] } : Value(Scalar(Scalar { kind: Float, width: 4 })) +[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [27] = Literal(AbstractFloat(2.0)) : Value(Scalar(Scalar { kind: AbstractFloat, width: 8 })) +[2026-06-09T07:39:17Z DEBUG naga::front::wgsl::lower::conversion] wgsl automatic_conversion_consensus: ["f32", "{AbstractFloat}"] +[2026-06-09T07:39:17Z DEBUG naga::front::wgsl::lower::conversion] consensus: "f32" +[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [28] = Literal(F32(2.0)) : Value(Scalar(Scalar { kind: Float, width: 4 })) +[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [29] = Binary { op: Multiply, left: [26], right: [28] } : Value(Scalar(Scalar { kind: Float, width: 4 })) +[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [30] = Literal(AbstractFloat(1.0)) : Value(Scalar(Scalar { kind: AbstractFloat, width: 8 })) +[2026-06-09T07:39:17Z DEBUG naga::front::wgsl::lower::conversion] wgsl automatic_conversion_consensus: ["f32", "{AbstractFloat}"] +[2026-06-09T07:39:17Z DEBUG naga::front::wgsl::lower::conversion] consensus: "f32" +[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [31] = Literal(F32(1.0)) : Value(Scalar(Scalar { kind: Float, width: 4 })) +[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [32] = Binary { op: Add, left: [29], right: [31] } : Value(Scalar(Scalar { kind: Float, width: 4 })) +[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [33] = AccessIndex { base: [2], index: 0 } : Value(Pointer { base: [5], space: Function }) +[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [34] = Literal(AbstractFloat(0.0)) : Value(Scalar(Scalar { kind: AbstractFloat, width: 8 })) +[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [35] = Literal(AbstractFloat(1.0)) : Value(Scalar(Scalar { kind: AbstractFloat, width: 8 })) +[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [36] = Literal(F32(0.0)) : Value(Scalar(Scalar { kind: Float, width: 4 })) +[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [37] = Literal(F32(1.0)) : Value(Scalar(Scalar { kind: Float, width: 4 })) +[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [38] = Compose { ty: [5], components: [[19], [32], [36], [37]] } : Handle([5]) +[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [39] = AccessIndex { base: [2], index: 1 } : Value(Pointer { base: [0], space: Function }) +[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [40] = AccessIndex { base: [1], index: 2 } : Handle([0]) +[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [41] = AccessIndex { base: [0], index: 0 } : Handle([0]) +[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [42] = AccessIndex { base: [1], index: 3 } : Handle([0]) +[2026-06-09T07:39:17Z DEBUG naga::front::wgsl::lower::conversion] wgsl automatic_conversion_consensus: ["vec2", "vec2"] +[2026-06-09T07:39:17Z DEBUG naga::front::wgsl::lower::conversion] consensus: "f32" +[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [43] = Binary { op: Multiply, left: [41], right: [42] } : Handle([0]) +[2026-06-09T07:39:17Z DEBUG naga::front::wgsl::lower::conversion] wgsl automatic_conversion_consensus: ["vec2", "vec2"] +[2026-06-09T07:39:17Z DEBUG naga::front::wgsl::lower::conversion] consensus: "f32" +[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [44] = Binary { op: Add, left: [40], right: [43] } : Handle([0]) +[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [45] = AccessIndex { base: [2], index: 2 } : Value(Pointer { base: [5], space: Function }) +[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [46] = AccessIndex { base: [1], index: 4 } : Handle([5]) +[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [47] = AccessIndex { base: [2], index: 3 } : Value(Pointer { base: [5], space: Function }) +[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [48] = AccessIndex { base: [1], index: 5 } : Handle([5]) +[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [49] = AccessIndex { base: [2], index: 4 } : Value(Pointer { base: [0], space: Function }) +[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [50] = AccessIndex { base: [0], index: 0 } : Handle([0]) +[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [51] = AccessIndex { base: [2], index: 5 } : Value(Pointer { base: [6], space: Function }) +[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [52] = AccessIndex { base: [1], index: 6 } : Handle([6]) +[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [53] = Load { pointer: [2] } : Handle([8]) +[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [37] = Literal(I32(4)) : Value(Scalar(Scalar { kind: Sint, width: 4 })) +[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [38] = Literal(AbstractInt(0)) : Value(Scalar(Scalar { kind: AbstractInt, width: 8 })) +[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [0] = FunctionArgument(0) : Handle([8]) +[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [1] = GlobalVariable([1]) : Handle([2]) +[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [2] = GlobalVariable([2]) : Handle([3]) +[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [3] = AccessIndex { base: [0], index: 1 } : Handle([0]) +[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [4] = ImageSample { image: [1], sampler: [2], gather: None, coordinate: [3], array_index: None, offset: None, level: Auto, depth_ref: None } : Value(Vector { size: Quad, scalar: Scalar { kind: Float, width: 4 } }) +[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [5] = AccessIndex { base: [4], index: 0 } : Value(Scalar(Scalar { kind: Float, width: 4 })) +[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [6] = AccessIndex { base: [0], index: 3 } : Handle([5]) +[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [7] = AccessIndex { base: [0], index: 2 } : Handle([5]) +[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [8] = Math { fun: Mix, arg: [6], arg1: Some([7]), arg2: Some([5]), arg3: None } : Handle([5]) +[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [9] = LocalVariable([0]) : Value(Pointer { base: [5], space: Function }) +[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [10] = AccessIndex { base: [0], index: 4 } : Handle([0]) +[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [11] = AccessIndex { base: [10], index: 1 } : Value(Scalar(Scalar { kind: Float, width: 4 })) +[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [12] = AccessIndex { base: [0], index: 5 } : Handle([6]) +[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [13] = Literal(U32(8)) : Value(Scalar(Scalar { kind: Uint, width: 4 })) +[2026-06-09T07:39:17Z DEBUG naga::front::wgsl::lower::conversion] wgsl automatic_conversion_consensus: ["u32", "u32"] +[2026-06-09T07:39:17Z DEBUG naga::front::wgsl::lower::conversion] consensus: "u32" +[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [14] = Binary { op: And, left: [12], right: [13] } : Handle([6]) +[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [15] = Literal(U32(0)) : Value(Scalar(Scalar { kind: Uint, width: 4 })) +[2026-06-09T07:39:17Z DEBUG naga::front::wgsl::lower::conversion] wgsl automatic_conversion_consensus: ["u32", "u32"] +[2026-06-09T07:39:17Z DEBUG naga::front::wgsl::lower::conversion] consensus: "u32" +[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [16] = Binary { op: NotEqual, left: [14], right: [15] } : Value(Scalar(Scalar { kind: Bool, width: 1 })) +[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [17] = Literal(AbstractFloat(0.84)) : Value(Scalar(Scalar { kind: AbstractFloat, width: 8 })) +[2026-06-09T07:39:17Z DEBUG naga::front::wgsl::lower::conversion] wgsl automatic_conversion_consensus: ["f32", "{AbstractFloat}"] +[2026-06-09T07:39:17Z DEBUG naga::front::wgsl::lower::conversion] consensus: "f32" +[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [18] = Literal(F32(0.84)) : Value(Scalar(Scalar { kind: Float, width: 4 })) +[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [19] = Binary { op: Greater, left: [11], right: [18] } : Value(Scalar(Scalar { kind: Bool, width: 1 })) +[2026-06-09T07:39:17Z DEBUG naga::front::wgsl::lower::conversion] wgsl automatic_conversion_consensus: ["bool", "bool"] +[2026-06-09T07:39:17Z DEBUG naga::front::wgsl::lower::conversion] consensus: "bool" +[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [20] = Binary { op: LogicalAnd, left: [16], right: [19] } : Value(Scalar(Scalar { kind: Bool, width: 1 })) +[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [21] = AccessIndex { base: [0], index: 2 } : Handle([5]) +[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [22] = AccessIndex { base: [0], index: 5 } : Handle([6]) +[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [23] = Literal(U32(16)) : Value(Scalar(Scalar { kind: Uint, width: 4 })) +[2026-06-09T07:39:17Z DEBUG naga::front::wgsl::lower::conversion] wgsl automatic_conversion_consensus: ["u32", "u32"] +[2026-06-09T07:39:17Z DEBUG naga::front::wgsl::lower::conversion] consensus: "u32" +[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [24] = Binary { op: And, left: [22], right: [23] } : Handle([6]) +[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [25] = Literal(U32(0)) : Value(Scalar(Scalar { kind: Uint, width: 4 })) +[2026-06-09T07:39:17Z DEBUG naga::front::wgsl::lower::conversion] wgsl automatic_conversion_consensus: ["u32", "u32"] +[2026-06-09T07:39:17Z DEBUG naga::front::wgsl::lower::conversion] consensus: "u32" +[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [26] = Binary { op: NotEqual, left: [24], right: [25] } : Value(Scalar(Scalar { kind: Bool, width: 1 })) +[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [27] = Literal(AbstractFloat(0.74)) : Value(Scalar(Scalar { kind: AbstractFloat, width: 8 })) +[2026-06-09T07:39:17Z DEBUG naga::front::wgsl::lower::conversion] wgsl automatic_conversion_consensus: ["f32", "{AbstractFloat}"] +[2026-06-09T07:39:17Z DEBUG naga::front::wgsl::lower::conversion] consensus: "f32" +[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [28] = Literal(F32(0.74)) : Value(Scalar(Scalar { kind: Float, width: 4 })) +[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [29] = Binary { op: Greater, left: [11], right: [28] } : Value(Scalar(Scalar { kind: Bool, width: 1 })) +[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [30] = Literal(AbstractFloat(0.8)) : Value(Scalar(Scalar { kind: AbstractFloat, width: 8 })) +[2026-06-09T07:39:17Z DEBUG naga::front::wgsl::lower::conversion] wgsl automatic_conversion_consensus: ["f32", "{AbstractFloat}"] +[2026-06-09T07:39:17Z DEBUG naga::front::wgsl::lower::conversion] consensus: "f32" +[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [31] = Literal(F32(0.8)) : Value(Scalar(Scalar { kind: Float, width: 4 })) +[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [32] = Binary { op: Less, left: [11], right: [31] } : Value(Scalar(Scalar { kind: Bool, width: 1 })) +[2026-06-09T07:39:17Z DEBUG naga::front::wgsl::lower::conversion] wgsl automatic_conversion_consensus: ["bool", "bool"] +[2026-06-09T07:39:17Z DEBUG naga::front::wgsl::lower::conversion] consensus: "bool" +[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [33] = Binary { op: LogicalAnd, left: [29], right: [32] } : Value(Scalar(Scalar { kind: Bool, width: 1 })) +[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [34] = Literal(AbstractFloat(0.88)) : Value(Scalar(Scalar { kind: AbstractFloat, width: 8 })) +[2026-06-09T07:39:17Z DEBUG naga::front::wgsl::lower::conversion] wgsl automatic_conversion_consensus: ["f32", "{AbstractFloat}"] +[2026-06-09T07:39:17Z DEBUG naga::front::wgsl::lower::conversion] consensus: "f32" +[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [35] = Literal(F32(0.88)) : Value(Scalar(Scalar { kind: Float, width: 4 })) +[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [36] = Binary { op: Greater, left: [11], right: [35] } : Value(Scalar(Scalar { kind: Bool, width: 1 })) +[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [37] = Literal(AbstractFloat(0.94)) : Value(Scalar(Scalar { kind: AbstractFloat, width: 8 })) +[2026-06-09T07:39:17Z DEBUG naga::front::wgsl::lower::conversion] wgsl automatic_conversion_consensus: ["f32", "{AbstractFloat}"] +[2026-06-09T07:39:17Z DEBUG naga::front::wgsl::lower::conversion] consensus: "f32" +[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [38] = Literal(F32(0.94)) : Value(Scalar(Scalar { kind: Float, width: 4 })) +[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [39] = Binary { op: Less, left: [11], right: [38] } : Value(Scalar(Scalar { kind: Bool, width: 1 })) +[2026-06-09T07:39:17Z DEBUG naga::front::wgsl::lower::conversion] wgsl automatic_conversion_consensus: ["bool", "bool"] +[2026-06-09T07:39:17Z DEBUG naga::front::wgsl::lower::conversion] consensus: "bool" +[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [40] = Binary { op: LogicalAnd, left: [36], right: [39] } : Value(Scalar(Scalar { kind: Bool, width: 1 })) +[2026-06-09T07:39:17Z DEBUG naga::front::wgsl::lower::conversion] wgsl automatic_conversion_consensus: ["bool", "bool"] +[2026-06-09T07:39:17Z DEBUG naga::front::wgsl::lower::conversion] consensus: "bool" +[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [41] = Binary { op: LogicalOr, left: [33], right: [40] } : Value(Scalar(Scalar { kind: Bool, width: 1 })) +[2026-06-09T07:39:17Z DEBUG naga::front::wgsl::lower::conversion] wgsl automatic_conversion_consensus: ["bool", "bool"] +[2026-06-09T07:39:17Z DEBUG naga::front::wgsl::lower::conversion] consensus: "bool" +[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [42] = Binary { op: LogicalAnd, left: [26], right: [41] } : Value(Scalar(Scalar { kind: Bool, width: 1 })) +[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [43] = AccessIndex { base: [0], index: 2 } : Handle([5]) +[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [44] = AccessIndex { base: [0], index: 5 } : Handle([6]) +[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [45] = Literal(U32(256)) : Value(Scalar(Scalar { kind: Uint, width: 4 })) +[2026-06-09T07:39:17Z DEBUG naga::front::wgsl::lower::conversion] wgsl automatic_conversion_consensus: ["u32", "u32"] +[2026-06-09T07:39:17Z DEBUG naga::front::wgsl::lower::conversion] consensus: "u32" +[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [46] = Binary { op: And, left: [44], right: [45] } : Handle([6]) +[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [47] = Literal(U32(0)) : Value(Scalar(Scalar { kind: Uint, width: 4 })) +[2026-06-09T07:39:17Z DEBUG naga::front::wgsl::lower::conversion] wgsl automatic_conversion_consensus: ["u32", "u32"] +[2026-06-09T07:39:17Z DEBUG naga::front::wgsl::lower::conversion] consensus: "u32" +[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [48] = Binary { op: NotEqual, left: [46], right: [47] } : Value(Scalar(Scalar { kind: Bool, width: 1 })) +[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [49] = Literal(AbstractFloat(0.44)) : Value(Scalar(Scalar { kind: AbstractFloat, width: 8 })) +[2026-06-09T07:39:17Z DEBUG naga::front::wgsl::lower::conversion] wgsl automatic_conversion_consensus: ["f32", "{AbstractFloat}"] +[2026-06-09T07:39:17Z DEBUG naga::front::wgsl::lower::conversion] consensus: "f32" +[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [50] = Literal(F32(0.44)) : Value(Scalar(Scalar { kind: Float, width: 4 })) +[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [51] = Binary { op: Greater, left: [11], right: [50] } : Value(Scalar(Scalar { kind: Bool, width: 1 })) +[2026-06-09T07:39:17Z DEBUG naga::front::wgsl::lower::conversion] wgsl automatic_conversion_consensus: ["bool", "bool"] +[2026-06-09T07:39:17Z DEBUG naga::front::wgsl::lower::conversion] consensus: "bool" +[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [52] = Binary { op: LogicalAnd, left: [48], right: [51] } : Value(Scalar(Scalar { kind: Bool, width: 1 })) +[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [53] = Literal(AbstractFloat(0.54)) : Value(Scalar(Scalar { kind: AbstractFloat, width: 8 })) +[2026-06-09T07:39:17Z DEBUG naga::front::wgsl::lower::conversion] wgsl automatic_conversion_consensus: ["f32", "{AbstractFloat}"] +[2026-06-09T07:39:17Z DEBUG naga::front::wgsl::lower::conversion] consensus: "f32" +[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [54] = Literal(F32(0.54)) : Value(Scalar(Scalar { kind: Float, width: 4 })) +[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [55] = Binary { op: Less, left: [11], right: [54] } : Value(Scalar(Scalar { kind: Bool, width: 1 })) +[2026-06-09T07:39:17Z DEBUG naga::front::wgsl::lower::conversion] wgsl automatic_conversion_consensus: ["bool", "bool"] +[2026-06-09T07:39:17Z DEBUG naga::front::wgsl::lower::conversion] consensus: "bool" +[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [56] = Binary { op: LogicalAnd, left: [52], right: [55] } : Value(Scalar(Scalar { kind: Bool, width: 1 })) +[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [57] = AccessIndex { base: [0], index: 2 } : Handle([5]) +[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [58] = Load { pointer: [9] } : Handle([5]) +[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [39] = Literal(I32(0)) : Value(Scalar(Scalar { kind: Sint, width: 4 })) +[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [40] = Literal(AbstractInt(0)) : Value(Scalar(Scalar { kind: AbstractInt, width: 8 })) +[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [0] = FunctionArgument(0) : Handle([8]) +[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [1] = GlobalVariable([1]) : Handle([2]) +[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [2] = GlobalVariable([2]) : Handle([3]) +[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [3] = AccessIndex { base: [0], index: 1 } : Handle([0]) +[2026-06-09T07:39:17Z DEBUG naga::front] Resolving [4] = ImageSample { image: [1], sampler: [2], gather: None, coordinate: [3], array_index: None, offset: None, level: Auto, depth_ref: None } : Value(Vector { size: Quad, scalar: Scalar { kind: Float, width: 4 } }) +[2026-06-09T07:39:17Z DEBUG naga::valid::interface] var GlobalVariable { name: Some("uniforms"), space: Uniform, binding: Some(ResourceBinding { group: 0, binding: 0 }), ty: [1], init: None } +[2026-06-09T07:39:17Z DEBUG naga::valid::interface] var GlobalVariable { name: Some("atlas_tex"), space: Handle, binding: Some(ResourceBinding { group: 0, binding: 1 }), ty: [2], init: None } +[2026-06-09T07:39:17Z DEBUG naga::valid::interface] var GlobalVariable { name: Some("atlas_smp"), space: Handle, binding: Some(ResourceBinding { group: 0, binding: 2 }), ty: [3], init: None } +[2026-06-09T07:39:17Z DEBUG naga::valid::function] var LocalVariable { name: Some("out"), ty: [8], init: None } +[2026-06-09T07:39:17Z DEBUG naga::valid::function] var LocalVariable { name: Some("color"), ty: [5], init: None } +[2026-06-09T07:39:17Z ERROR wgpu_core::device::global] Device::create_render_pipeline error: Error matching ShaderStages(VERTEX) shader requirements against the pipeline +[2026-06-09T07:39:17Z ERROR wgpu::backend::wgpu_core] Handling wgpu errors as fatal by default + +thread 'main' (24148) panicked at C:\Users\DG2210\.cargo\registry\src\index.crates.io-1949cf8c6b5b557f\wgpu-22.1.0\src\backend\wgpu_core.rs:3411:5: +wgpu error: Validation Error + +Caused by: + In Device::create_render_pipeline, label = 'kitty_pipeline' + Error matching ShaderStages(VERTEX) shader requirements against the pipeline + Location[7] Uint32 interpolated as Some(Flat) with sampling None is not provided by the previous stage outputs + Input is not provided by the earlier stage in the pipeline + + +note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace diff --git a/winiterm/winiterm_startup2.log b/winiterm/winiterm_startup2.log new file mode 100644 index 0000000..66103da --- /dev/null +++ b/winiterm/winiterm_startup2.log @@ -0,0 +1,184 @@ +[2026-06-09T07:43:00Z WARN wgpu_hal::vulkan::instance] InstanceFlags::VALIDATION requested, but unable to find layer: VK_LAYER_KHRONOS_validation +[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)] + No valid vk_loader_settings.json file found, no loader settings will be active +[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x21acf720e00, name: ?) +[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)] + windows_get_app_package_manifest_path: Failed to find mapping layers packages by family name +[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x21acf720e00, name: ?) +[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)] + Checking for Layer Manifest files in Registry at HKEY_LOCAL_MACHINE\SOFTWARE\Khronos\Vulkan\ImplicitLayers +[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x21acf720e00, name: ?) +[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)] + windows_get_device_registry_files: opening device PCI\VEN_8086&DEV_A721&SUBSYS_8B7C103C&REV_04\3&11583659&0&10 +[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x21acf720e00, name: ?) +[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)] + windows_get_device_registry_entry: Device ID(1) Does not contain a value for "VulkanImplicitLayers" +[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x21acf720e00, name: ?) +[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)] + windows_get_device_registry_files: Opening child device 2 - SWD\DRIVERENUM\IGS&4&168EC99&0 +[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x21acf720e00, name: ?) +[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)] + windows_get_device_registry_entry: Device ID(2) Does not contain a value for "VulkanImplicitLayers" +[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x21acf720e00, name: ?) +[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)] + windows_get_device_registry_files: Opening child device 3 - DISPLAY\LGD071E\4&168EC99&0&UID8388688 +[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x21acf720e00, name: ?) +[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)] + windows_get_device_registry_files: Opening child device 4 - DISPLAY\PHL095C\4&168EC99&0&UID8261 +[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x21acf720e00, name: ?) +[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)] + windows_get_device_registry_files: found no registry files +[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x21acf720e00, name: ?) +[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)] + Located json file "C:\ProgramData\obs-studio-hook\obs-vulkan64.json" from registry "HKEY_LOCAL_MACHINE\SOFTWARE\Khronos\Vulkan\ImplicitLayers" +[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x21acf720e00, name: ?) +[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)] + Found manifest file C:\ProgramData\obs-studio-hook\obs-vulkan64.json (file version 1.1.2) +[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x21acf720e00, name: ?) +[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)] + Checking for Layer Manifest files in Registry at HKEY_LOCAL_MACHINE\SOFTWARE\Khronos\Vulkan\ExplicitLayers +[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x21acf720e00, name: ?) +[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)] + windows_get_device_registry_files: opening device PCI\VEN_8086&DEV_A721&SUBSYS_8B7C103C&REV_04\3&11583659&0&10 +[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x21acf720e00, name: ?) +[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)] + windows_get_device_registry_entry: Device ID(1) Does not contain a value for "VulkanExplicitLayers" +[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x21acf720e00, name: ?) +[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)] + windows_get_device_registry_files: Opening child device 2 - SWD\DRIVERENUM\IGS&4&168EC99&0 +[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x21acf720e00, name: ?) +[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)] + windows_get_device_registry_entry: Device ID(2) Does not contain a value for "VulkanExplicitLayers" +[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x21acf720e00, name: ?) +[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)] + windows_get_device_registry_files: Opening child device 3 - DISPLAY\LGD071E\4&168EC99&0&UID8388688 +[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x21acf720e00, name: ?) +[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)] + windows_get_device_registry_files: Opening child device 4 - DISPLAY\PHL095C\4&168EC99&0&UID8261 +[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x21acf720e00, name: ?) +[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)] + windows_get_device_registry_files: found no registry files +[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x21acf720e00, name: ?) +[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)] + Found no registry files in HKEY_CURRENT_USER\SOFTWARE\Khronos\Vulkan\ExplicitLayers +[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x21acf720e00, name: ?) +[2026-06-09T07:43:00Z WARN wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)] + windows_read_data_files_in_registry: Registry lookup failed to get layer manifest files. +[2026-06-09T07:43:00Z WARN wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x21acf720e00, name: ?) +[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)] + windows_get_app_package_manifest_path: Failed to find mapping layers packages by family name +[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x21acf720e00, name: ?) +[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)] + Checking for Driver Manifest files in Registry at HKEY_LOCAL_MACHINE\SOFTWARE\Khronos\Vulkan\Drivers +[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x21acf720e00, name: ?) +[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)] + windows_add_json_entry: Located json file "C:\WINDOWS\System32\DriverStore\FileRepository\iigd_dch.inf_amd64_0eac281dc2d07a5f\igvk64.json" from PnP registry: E +[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x21acf720e00, name: ?) +[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)] + Found no registry files in HKEY_LOCAL_MACHINE\SOFTWARE\Khronos\Vulkan\Drivers +[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x21acf720e00, name: ?) +[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)] + Found ICD manifest file C:\WINDOWS\System32\DriverStore\FileRepository\iigd_dch.inf_amd64_0eac281dc2d07a5f\igvk64.json, version 1.0.0 +[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x21acf720e00, name: ?) +[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)] + Insert instance layer "VK_LAYER_OBS_HOOK" (C:\ProgramData\obs-studio-hook\.\graphics-hook64.dll) +[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x21acf720e00, name: ?) +[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)] + vkCreateInstance layer callstack setup to: +[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x21acf720e00, name: ?) +[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)] + +[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x21acf720e00, name: ?) +[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)] + || +[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x21acf720e00, name: ?) +[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)] + +[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x21acf720e00, name: ?) +[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)] + || +[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x21acf720e00, name: ?) +[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)] + VK_LAYER_OBS_HOOK +[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x21acf720e00, name: ?) +[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)] + Type: Implicit +[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x21acf720e00, name: ?) +[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)] + Enabled By: Implicit Layer +[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x21acf720e00, name: ?) +[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)] + Disable Env Var: DISABLE_VULKAN_OBS_CAPTURE +[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x21acf720e00, name: ?) +[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)] + Manifest: C:\ProgramData\obs-studio-hook\obs-vulkan64.json +[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x21acf720e00, name: ?) +[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)] + Library: C:\ProgramData\obs-studio-hook\.\graphics-hook64.dll +[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x21acf720e00, name: ?) +[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)] + || +[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x21acf720e00, name: ?) +[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)] + +[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x21acf720e00, name: ?) +[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] Enabling debug utils +[2026-06-09T07:43:00Z INFO wgpu_core::instance] Adapter Vulkan AdapterInfo { name: "Intel(R) UHD Graphics", vendor: 32902, device: 42785, device_type: IntegratedGpu, driver: "Intel Corporation", driver_info: "101.7084", backend: Vulkan } +[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)] + Inserted device layer "VK_LAYER_OBS_HOOK" (C:\ProgramData\obs-studio-hook\.\graphics-hook64.dll) +[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x21acf720e00, name: ?) +[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)] + vkCreateDevice layer callstack setup to: +[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x21acf720e00, name: ?) +[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)] + +[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x21acf720e00, name: ?) +[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)] + || +[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x21acf720e00, name: ?) +[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)] + +[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x21acf720e00, name: ?) +[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)] + || +[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x21acf720e00, name: ?) +[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)] + VK_LAYER_OBS_HOOK +[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x21acf720e00, name: ?) +[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)] + Type: Implicit +[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x21acf720e00, name: ?) +[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)] + Enabled By: Implicit Layer +[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x21acf720e00, name: ?) +[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)] + Disable Env Var: DISABLE_VULKAN_OBS_CAPTURE +[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x21acf720e00, name: ?) +[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)] + Manifest: C:\ProgramData\obs-studio-hook\obs-vulkan64.json +[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x21acf720e00, name: ?) +[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)] + Library: C:\ProgramData\obs-studio-hook\.\graphics-hook64.dll +[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x21acf720e00, name: ?) +[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)] + || +[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x21acf720e00, name: ?) +[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)] + +[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x21acf720e00, name: ?) +[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] GENERAL [Loader Message (0x0)] + Using "Intel(R) UHD Graphics" with driver: "C:\WINDOWS\System32\DriverStore\FileRepository\iigd_dch.inf_amd64_0eac281dc2d07a5f\.\igvk64.dll" +[2026-06-09T07:43:00Z INFO wgpu_hal::vulkan::instance] objects: (type: INSTANCE, hndl: 0x21acf720e00, name: ?) +[2026-06-09T07:43:00Z INFO wgpu_core::device::resource] Device::maintain: waiting for submission index 0 +[2026-06-09T07:43:01Z INFO winiterm::pty::windows] spawning shell: "cmd.exe" cols=120 rows=41 +[2026-06-09T07:43:01Z INFO winiterm::workspace] initial shell spawned (pid=24452) +[2026-06-09T07:43:01Z INFO winiterm::workspace] workspace created, pane count = 1 +[2026-06-09T07:43:01Z INFO wgpu_core::device::resource] Device::maintain: waiting for submission index 0 +[2026-06-09T07:43:01Z INFO wgpu_core::device::resource] Device::maintain: waiting for submission index 0 +[2026-06-09T07:43:01Z INFO wgpu_core::device::resource] Device::maintain: waiting for submission index 0 +[2026-06-09T07:43:01Z INFO winiterm::pty::windows] child_exited: exit_code=3221225794 +[2026-06-09T07:43:01Z INFO winiterm::pane] pane 0 is_dead=true (pty_present=true) +[2026-06-09T07:43:01Z INFO winiterm::workspace] removing dead pane 0 +[2026-06-09T07:43:01Z INFO winiterm::app] all panes dead — exiting +[2026-06-09T07:43:01Z INFO winiterm::app] all panes dead — exiting +[2026-06-09T07:43:02Z INFO wgpu_core::device::resource] Device::maintain: waiting for submission index 0 diff --git a/winiterm/winiterm_startup3.log b/winiterm/winiterm_startup3.log new file mode 100644 index 0000000..e69de29