commit 902a2e4c413cfbc1503554c570c80ca00c235a5e Author: Gregor Lohaus Date: Mon Sep 7 21:45:50 2026 +0200 init diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..8d5d619 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,26 @@ +name: Rust +on: [push, pull_request] + +jobs: + test: + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-14] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + with: + components: rustfmt, clippy + - uses: mlugg/setup-zig@v2 + with: + version: 0.15.2 + - uses: Swatinem/rust-cache@v2 + - name: Linux development libraries + if: runner.os == 'Linux' + run: sudo apt-get update && sudo apt-get install -y pkg-config libx11-dev libxkbcommon-dev libwayland-dev libegl1-mesa-dev + - run: cargo fmt --check + - run: cargo clippy --locked --all-targets -- -D warnings + - run: cargo test --locked -- --test-threads=1 + - run: cargo build --locked diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..42181f9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/target/ +/.tools/ diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..c931818 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,3548 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "ab_glyph" +version = "0.2.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01c0457472c38ea5bd1c3b5ada5e368271cb550be7a4ca4a0b4634e9913f6cc2" +dependencies = [ + "ab_glyph_rasterizer", + "owned_ttf_parser", +] + +[[package]] +name = "ab_glyph_rasterizer" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "366ffbaa4442f4684d91e2cd7c5ea7c4ed8add41959a31447066e279e432b618" + +[[package]] +name = "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 = "android-activity" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f2a1bb052857d5dd49572219344a7332b31b76405648eabac5bc68978251bcd" +dependencies = [ + "android-properties", + "bitflags 2.13.1", + "cc", + "jni", + "libc", + "log", + "ndk", + "ndk-context", + "ndk-sys", + "num_enum", + "thiserror 2.0.20", +] + +[[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 = "anyhow" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" + +[[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.60.2", + "x11rb", +] + +[[package]] +name = "arrayvec" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56" + +[[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", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +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.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" + +[[package]] +name = "block" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" + +[[package]] +name = "block2" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c132eebf10f5cad5289222520a4a058514204aed6d791f1cf4fe8088b82d15f" +dependencies = [ + "objc2 0.5.2", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "bytemuck" +version = "1.25.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95832e849adfb21180ccb6826a99da14e5d266ae5c2e668e1602cf234f153797" +dependencies = [ + "bytemuck_derive", +] + +[[package]] +name = "bytemuck_derive" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0e56a716f1e132ff6bf4bdac1c944a3fcdc1cae65f70a4a2a1ac3b401d2d1f" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.5", +] + +[[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.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" + +[[package]] +name = "calloop" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b99da2f8558ca23c71f4fd15dc57c906239752dd27ff3c00a1d56b685b7cbfec" +dependencies = [ + "bitflags 2.13.1", + "log", + "polling", + "rustix 0.38.44", + "slab", + "thiserror 1.0.69", +] + +[[package]] +name = "calloop" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4dbf9978365bac10f54d1d4b04f7ce4427e51f71d61f2fe15e3fed5166474df7" +dependencies = [ + "bitflags 2.13.1", + "polling", + "rustix 1.1.4", + "slab", + "tracing", +] + +[[package]] +name = "calloop-wayland-source" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95a66a987056935f7efce4ab5668920b5d0dac4a7c99991a67395f13702ddd20" +dependencies = [ + "calloop 0.13.0", + "rustix 0.38.44", + "wayland-backend", + "wayland-client", +] + +[[package]] +name = "calloop-wayland-source" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "138efcf0940a02ebf0cc8d1eff41a1682a46b431630f4c52450d6265876021fa" +dependencies = [ + "calloop 0.14.4", + "rustix 1.1.4", + "wayland-backend", + "wayland-client", +] + +[[package]] +name = "cc" +version = "1.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "005ec2760ca554fae18df7a11195552ec576cd665632a881bc011d5bb2fd4d80" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cfg_aliases" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e" + +[[package]] +name = "cfg_aliases" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527" + +[[package]] +name = "cgl" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ced0551234e87afee12411d535648dd89d2e7f34c78b753395567aff3d447ff" +dependencies = [ + "libc", +] + +[[package]] +name = "clipboard-win" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bde03770d3df201d4fb868f2c9c59e66a3e4e2bd06692a0fe701e7103c7e84d4" +dependencies = [ + "error-code", +] + +[[package]] +name = "codespan-reporting" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe6d2e5af09e8c8ad56c969f2157a3d4238cebc7c55f0a517728c38f7b200f81" +dependencies = [ + "unicode-width", +] + +[[package]] +name = "combine" +version = "4.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfc320937d09e6de266b31b9afb480f197d7a861be86be7cb2ea7e5d1bfffc5e" +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.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "core-graphics" +version = "0.23.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c07782be35f9e1140080c6b96f0d44b739e2278479f64e02fdab4e32dfd8b081" +dependencies = [ + "bitflags 1.3.2", + "core-foundation 0.9.4", + "core-graphics-types 0.1.3", + "foreign-types", + "libc", +] + +[[package]] +name = "core-graphics-types" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf" +dependencies = [ + "bitflags 1.3.2", + "core-foundation 0.9.4", + "libc", +] + +[[package]] +name = "core-graphics-types" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" +dependencies = [ + "bitflags 2.13.1", + "core-foundation 0.10.1", + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8498c871161e1742aaa9d52551b2d6ebdd4c3d45a3be423e3728f33b955be550" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a31eee39dddec8330830986fcd7625edb5a24ec90ea038215273bbc3adb08ac6" + +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + +[[package]] +name = "cursor-icon" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f27ae1dd37df86211c42e150270f82743308803d90a6f6e6651cd730d5e1732f" + +[[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.1", + "objc2 0.6.4", +] + +[[package]] +name = "displaydoc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.5", +] + +[[package]] +name = "dlib" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab8ecd87370524b461f8557c119c405552c396ed91fc0a8eec68679eab26f94a" +dependencies = [ + "libloading", +] + +[[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 = "ecolor" +version = "0.33.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71ddb8ac7643d1dba1bb02110e804406dd459a838efcb14011ced10556711a8e" +dependencies = [ + "bytemuck", + "emath", +] + +[[package]] +name = "eframe" +version = "0.33.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "457481173e6db5ca9fa2be93a58df8f4c7be639587aeb4853b526c6cf87db4e6" +dependencies = [ + "ahash", + "bytemuck", + "document-features", + "egui", + "egui-wgpu", + "egui-winit", + "egui_glow", + "glutin", + "glutin-winit", + "image", + "js-sys", + "log", + "objc2 0.5.2", + "objc2-app-kit 0.2.2", + "objc2-foundation 0.2.2", + "parking_lot", + "percent-encoding", + "pollster", + "profiling", + "raw-window-handle", + "static_assertions", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "web-time", + "wgpu", + "windows-sys 0.61.2", + "winit", +] + +[[package]] +name = "egui" +version = "0.33.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a9b567d356674e9a5121ed3fedfb0a7c31e059fe71f6972b691bcd0bfc284e3" +dependencies = [ + "ahash", + "bitflags 2.13.1", + "emath", + "epaint", + "log", + "nohash-hasher", + "profiling", + "smallvec", + "unicode-segmentation", +] + +[[package]] +name = "egui-wgpu" +version = "0.33.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e4d209971c84b2352a06174abdba701af1e552ce56b144d96f2bd50a3c91236" +dependencies = [ + "ahash", + "bytemuck", + "document-features", + "egui", + "epaint", + "log", + "profiling", + "thiserror 2.0.20", + "type-map", + "web-time", + "wgpu", + "winit", +] + +[[package]] +name = "egui-winit" +version = "0.33.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec6687e5bb551702f4ad10ac428bab12acf9d53047ebb1082d4a0ed8c6251a29" +dependencies = [ + "arboard", + "bytemuck", + "egui", + "log", + "objc2 0.5.2", + "objc2-foundation 0.2.2", + "objc2-ui-kit", + "profiling", + "raw-window-handle", + "smithay-clipboard", + "web-time", + "webbrowser", + "winit", +] + +[[package]] +name = "egui_glow" +version = "0.33.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6420863ea1d90e750f75075231a260030ad8a9f30a7cef82cdc966492dc4c4eb" +dependencies = [ + "bytemuck", + "egui", + "glow", + "log", + "memoffset", + "profiling", + "wasm-bindgen", + "web-sys", + "winit", +] + +[[package]] +name = "emath" +version = "0.33.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "491bdf728bf25ddd9ad60d4cf1c48588fa82c013a2440b91aa7fc43e34a07c32" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "epaint" +version = "0.33.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "009d0dd3c2163823a0abdb899451ecbc78798dec545ee91b43aff1fa790bab62" +dependencies = [ + "ab_glyph", + "ahash", + "bytemuck", + "ecolor", + "emath", + "epaint_default_fonts", + "log", + "nohash-hasher", + "parking_lot", + "profiling", +] + +[[package]] +name = "epaint_default_fonts" +version = "0.33.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c4fbe202b6578d3d56428fa185cdf114a05e49da05f477b3c7f0fbb221f1862" + +[[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.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5343afd4a8365a643ac588dab4cf234a190c7f6c88c9f6dd6ffe00837661b7" + +[[package]] +name = "fax" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caf1079563223d5d59d83c85886a56e586cfd5c1a26292e971a0fa266531ac5a" + +[[package]] +name = "fdeflate" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "filedescriptor" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e40758ed24c9b2eeb76c35fb0aebc66c626084edd827e07e1552279814c6682d" +dependencies = [ + "libc", + "thiserror 1.0.69", + "winapi", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e0f1c7c3a72c66fd80abe965175f7523475c0489a87d3ff9d6e8c87d87a9d2d" + +[[package]] +name = "flate2" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e634e2e0ebac1ee034020da1ca582e17ffe4e0f5e985823721e168928136dcb" +dependencies = [ + "crc32fast", + "miniz_oxide 0.9.1", + "zlib-rs", +] + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "foreign-types" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" +dependencies = [ + "foreign-types-macros", + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-macros" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea5190182e6915eb873ddbc16e23b711b6eb1f9c00a0d0a3a91b5f6228475225" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.5", +] + +[[package]] +name = "foreign-types-shared" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures-core" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" + +[[package]] +name = "futures-task" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" + +[[package]] +name = "futures-util" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "slab", +] + +[[package]] +name = "gethostname" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bd49230192a3797a9a4d6abe9b3eed6f7fa4c8a8a4947977c6f80025f92cbd8" +dependencies = [ + "rustix 1.1.4", + "windows-link", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi 5.3.0", + "wasip2", +] + +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", +] + +[[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 = "glow" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5e5ea60d70410161c8bf5da3fdfeaa1c72ed2c15f8bbb9d19fe3a4fad085f08" +dependencies = [ + "js-sys", + "slotmap", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "glutin" +version = "0.32.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12124de845cacfebedff80e877bb37b5b75c34c5a4c89e47e1cdd67fb6041325" +dependencies = [ + "bitflags 2.13.1", + "cfg_aliases 0.2.2", + "cgl", + "dispatch2", + "glutin_egl_sys", + "glutin_glx_sys", + "glutin_wgl_sys", + "libloading", + "objc2 0.6.4", + "objc2-app-kit 0.3.2", + "objc2-core-foundation", + "objc2-foundation 0.3.2", + "once_cell", + "raw-window-handle", + "wayland-sys", + "windows-sys 0.52.0", + "x11-dl", +] + +[[package]] +name = "glutin-winit" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85edca7075f8fc728f28cb8fbb111a96c3b89e930574369e3e9c27eb75d3788f" +dependencies = [ + "cfg_aliases 0.2.2", + "glutin", + "raw-window-handle", + "winit", +] + +[[package]] +name = "glutin_egl_sys" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c4680ba6195f424febdc3ba46e7a42a0e58743f2edb115297b86d7f8ecc02d2" +dependencies = [ + "gl_generator", + "windows-sys 0.52.0", +] + +[[package]] +name = "glutin_glx_sys" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7bb2938045a88b612499fbcba375a77198e01306f52272e692f8c1f3751185" +dependencies = [ + "gl_generator", + "x11-dl", +] + +[[package]] +name = "glutin_wgl_sys" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c4ee00b289aba7a9e5306d57c2d05499b2e5dc427f84ac708bd2c090212cf3e" +dependencies = [ + "gl_generator", +] + +[[package]] +name = "gpu-alloc" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45cf04b2726f02df5508c6de726acdc90cdf97ac771a9a0ffd8ba10a6e696bf9" +dependencies = [ + "bitflags 2.13.1", + "gpu-alloc-types", +] + +[[package]] +name = "gpu-alloc-types" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2bbed164dd10ed526c2e4fe3e721ca4a71c61730e5aafac6844b417b3227058" +dependencies = [ + "bitflags 2.13.1", +] + +[[package]] +name = "gpu-descriptor" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b89c83349105e3732062a895becfc71a8f921bb71ecbbdd8ff99263e3b53a0ca" +dependencies = [ + "bitflags 2.13.1", + "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.1", +] + +[[package]] +name = "half" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" +dependencies = [ + "cfg-if", + "crunchy", + "num-traits", + "zerocopy", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash 0.1.5", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "foldhash 0.2.0", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "hermit-abi" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e17592d60ebacc7d5e169f4663c5f84f9161cc90328abcfe8456f41e4dfcb284" + +[[package]] +name = "hexf-parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df" + +[[package]] +name = "icu_collections" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa68d21081c4a05d5a901a1c62add574c77048b6a1c67be3b50ce0b60d4ca513" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d56e28588da92eee5c3201a6eff33fabdd49b62269c8938d4ff050ce4d900deb" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12f9cf5f235641ed274641dd81c3f28d870e276763d0797aeeab72317b1c646f" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1563da1ed3e0b3bf3d74c9b85917ac9c56464d2f57242270c09c9e752f8021a0" + +[[package]] +name = "icu_properties" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e7ca276ad3145661a65914e6daf131ca5120cd3dcee8f8f3214b8875184a148" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e590f038c1464a96894fd6d10127e90a8be4509f56ff7ecef851b15cee0b7caa" + +[[package]] +name = "icu_provider" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d27bbb9d3abbefac45d55f647c9de1d44aafcd1186eb91879afef17c396c3e73" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "image" +version = "0.25.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104" +dependencies = [ + "bytemuck", + "byteorder-lite", + "moxcms", + "num-traits", + "png", + "tiff", +] + +[[package]] +name = "indexmap" +version = "2.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", +] + +[[package]] +name = "int-enum" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e366a1634cccc76b4cfd3e7580de9b605e4d93f1edac48d786c1f867c0def495" +dependencies = [ + "proc-macro2", + "proc-macro2-diagnostics", + "quote", + "syn 2.0.119", +] + +[[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.20", + "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.119", +] + +[[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.119", +] + +[[package]] +name = "jobserver" +version = "0.1.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3" +dependencies = [ + "getrandom 0.4.3", + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce57d20d1ea864ce2ac172ab472d409214f4fd359f0b2a2775abdf522e2af99e" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "khronos-egl" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6aae1df220ece3c0ada96b8153459b67eebe9ae9212258bb0134ae60416fdf76" +dependencies = [ + "libc", + "libloading", + "pkg-config", +] + +[[package]] +name = "khronos_api" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc" + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.189" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" + +[[package]] +name = "libghostty-vt" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6219ed1c364b3ef5815fb0fd2acc0662f792adc706dabf59b7ca89280ee6d066" +dependencies = [ + "bitflags 2.13.1", + "int-enum", + "libghostty-vt-sys", + "png", +] + +[[package]] +name = "libghostty-vt-sys" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "865fed12a8b2bba3507b3bccd0bef439e06ef1100e652fe6b71d132c41ee8db0" + +[[package]] +name = "libloading" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" +dependencies = [ + "cfg-if", + "windows-link", +] + +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[package]] +name = "libredox" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d8f1ea3f21fd3405dcaf6c9b5c1630af9afc422d9073ea39c5f6d6c772e08ed" +dependencies = [ + "bitflags 2.13.1", + "libc", + "plain", + "redox_syscall 0.9.4", +] + +[[package]] +name = "linux-raw-sys" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "litemap" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d9d19d1d6efa0109d2f65ff4c85cddd50bd572e5a00127ab10987290bcefae" + +[[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.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" + +[[package]] +name = "malloc_buf" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" +dependencies = [ + "libc", +] + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "memmap2" +version = "0.9.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1219ed1b7f229ee7104d281dd01d6802fe28bb6e95d292942c4daacdeb798c0" +dependencies = [ + "libc", +] + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "metal" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00c15a6f673ff72ddcc22394663290f870fb224c1bfce55734a75c414150e605" +dependencies = [ + "bitflags 2.13.1", + "block", + "core-graphics-types 0.2.0", + "foreign-types", + "log", + "objc", + "paste", +] + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "miniz_oxide" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b63fbc4a50860e98e7b2aa7804ded1db5cbc3aff9193adaff57a6931bf7c4b4c" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "moxcms" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb85c154ba489f01b25c0d36ae69a87e4a1c73a72631fc6c0eb6dde34a73e44b" +dependencies = [ + "num-traits", + "pxfm", +] + +[[package]] +name = "naga" +version = "27.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "066cf25f0e8b11ee0df221219010f213ad429855f57c494f995590c861a9a7d8" +dependencies = [ + "arrayvec", + "bit-set", + "bitflags 2.13.1", + "cfg-if", + "cfg_aliases 0.2.2", + "codespan-reporting", + "half", + "hashbrown 0.16.1", + "hexf-parse", + "indexmap", + "libm", + "log", + "num-traits", + "once_cell", + "rustc-hash 1.1.0", + "spirv", + "thiserror 2.0.20", + "unicode-ident", +] + +[[package]] +name = "ndk" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" +dependencies = [ + "bitflags 2.13.1", + "jni-sys 0.3.1", + "log", + "ndk-sys", + "num_enum", + "raw-window-handle", + "thiserror 1.0.69", +] + +[[package]] +name = "ndk-context" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" + +[[package]] +name = "ndk-sys" +version = "0.6.0+11769913" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873" +dependencies = [ + "jni-sys 0.3.1", +] + +[[package]] +name = "nix" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab2156c4fce2f8df6c499cc1c763e4394b7482525bf2a9701c9d79d215f519e4" +dependencies = [ + "bitflags 2.13.1", + "cfg-if", + "cfg_aliases 0.1.1", + "libc", +] + +[[package]] +name = "nohash-hasher" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451" + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", + "libm", +] + +[[package]] +name = "num_enum" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26" +dependencies = [ + "num_enum_derive", + "rustversion", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "objc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" +dependencies = [ + "malloc_buf", +] + +[[package]] +name = "objc-sys" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb91bdd390c7ce1a8607f35f3ca7151b65afc0ff5ff3b34fa350f7d7c7e4310" + +[[package]] +name = "objc2" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46a785d4eeff09c14c487497c162e92766fbb3e4059a71840cecc03d9a50b804" +dependencies = [ + "objc-sys", + "objc2-encode", +] + +[[package]] +name = "objc2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" +dependencies = [ + "objc2-encode", +] + +[[package]] +name = "objc2-app-kit" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4e89ad9e3d7d297152b17d39ed92cd50ca8063a89a9fa569046d41568891eff" +dependencies = [ + "bitflags 2.13.1", + "block2", + "libc", + "objc2 0.5.2", + "objc2-core-data", + "objc2-core-image", + "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.1", + "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.1", + "block2", + "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", + "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.13.1", + "block2", + "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.1", + "dispatch2", + "objc2 0.6.4", +] + +[[package]] +name = "objc2-core-graphics" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" +dependencies = [ + "bitflags 2.13.1", + "dispatch2", + "objc2 0.6.4", + "objc2-core-foundation", + "objc2-io-surface", +] + +[[package]] +name = "objc2-core-image" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55260963a527c99f1819c4f8e3b47fe04f9650694ef348ffd2227e8196d34c80" +dependencies = [ + "block2", + "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", + "objc2 0.5.2", + "objc2-contacts", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + +[[package]] +name = "objc2-foundation" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" +dependencies = [ + "bitflags 2.13.1", + "block2", + "dispatch", + "libc", + "objc2 0.5.2", +] + +[[package]] +name = "objc2-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" +dependencies = [ + "bitflags 2.13.1", + "objc2 0.6.4", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-io-surface" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" +dependencies = [ + "bitflags 2.13.1", + "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", + "objc2 0.5.2", + "objc2-app-kit 0.2.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-metal" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6" +dependencies = [ + "bitflags 2.13.1", + "block2", + "objc2 0.5.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-quartz-core" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a" +dependencies = [ + "bitflags 2.13.1", + "block2", + "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.1", + "block2", + "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", + "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.1", + "block2", + "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 = "orbclient" +version = "0.3.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5df339f526ea9a60e371768d50efc2f2508c7203290731565d1f7a6f71d21747" +dependencies = [ + "libc", + "libredox", +] + +[[package]] +name = "ordered-float" +version = "5.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f4779c6901a562440c3786d08192c6fbda7c1c2060edd10006b05ee35d10f2d" +dependencies = [ + "num-traits", +] + +[[package]] +name = "owned_ttf_parser" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36820e9051aca1014ddc75770aab4d68bc1e9e632f0f5627c4086bc216fb583b" +dependencies = [ + "ttf-parser", +] + +[[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.119", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pkg-config" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548" + +[[package]] +name = "plain" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" + +[[package]] +name = "png" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" +dependencies = [ + "bitflags 2.13.1", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide 0.8.9", +] + +[[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.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f3a9f18d041e6d0e102a0a46750538147e5e8992d3b4873aaafee2520b00ce3" + +[[package]] +name = "portable-atomic" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85" + +[[package]] +name = "portable-atomic-util" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10ab3eb7f3becc3a1cbc4f2c6f20267996cfc1a6467a873763411b136a122715" +dependencies = [ + "portable-atomic", +] + +[[package]] +name = "portable-pty" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4a596a2b3d2752d94f51fac2d4a96737b8705dddd311a32b9af47211f08671e" +dependencies = [ + "anyhow", + "bitflags 1.3.2", + "downcast-rs", + "filedescriptor", + "lazy_static", + "libc", + "log", + "nix", + "serial2", + "shared_library", + "shell-words", + "winapi", + "winreg", +] + +[[package]] +name = "potential_utf" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d83eb9bc6d8e5cf568e7a1101d60ee05e81ed50ea106026f3d18deeb046d7661" +dependencies = [ + "zerovec", +] + +[[package]] +name = "proc-macro-crate" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +dependencies = [ + "toml_edit", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "proc-macro2-diagnostics" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", + "version_check", +] + +[[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.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d55d956fa96f5ec02be2e13af0e20391a5aa83d6a074e3ad368959d0fab299ea" + +[[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.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e660451e55124f798a69a5af3f49ccfbefbd41910eefd25caf2393e1f3473ec1" +dependencies = [ + "memchr", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "raw-window-handle" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" + +[[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.1", +] + +[[package]] +name = "redox_syscall" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "737970939a87c6fa31e7acad13307bccbb017a073b695b6089a2c484f929e20e" +dependencies = [ + "bitflags 2.13.1", +] + +[[package]] +name = "renderdoc-sys" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832" + +[[package]] +name = "rust-term" +version = "0.1.0" +dependencies = [ + "anyhow", + "base64", + "eframe", + "egui", + "libghostty-vt", + "png", + "portable-pty", + "serde", + "toml", + "wgpu", +] + +[[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.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "0.38.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +dependencies = [ + "bitflags 2.13.1", + "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.1", + "errno", + "libc", + "linux-raw-sys 0.12.1", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[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 = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.5", +] + +[[package]] +name = "serde_spanned" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serial2" +version = "0.2.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b16809bc35793b19ce4e0c53924bc0dce3937f15487997cfdaed936004180730" +dependencies = [ + "cfg-if", + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "shared_library" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a9e7e0f2bfae24d8a5b5a66c5b257a83c7412304311512a0c054cd5e619da11" +dependencies = [ + "lazy_static", + "libc", +] + +[[package]] +name = "shell-words" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc6fe69c597f9c37bfeeeeeb33da3530379845f10be461a66d16d03eca2ded77" + +[[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.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea" + +[[package]] +name = "simd_cesu8" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11031e251abf8611c80f460e19dbdeb54a66db918e49c65a7065b46ac7aec520" +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 = "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.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9be42f50aa861c555654aa3a37f52f4b1074bacf4e48fe0ef7fa584e80f1f0f" + +[[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.1", + "calloop 0.13.0", + "calloop-wayland-source 0.3.0", + "cursor-icon", + "libc", + "log", + "memmap2", + "rustix 0.38.44", + "thiserror 1.0.69", + "wayland-backend", + "wayland-client", + "wayland-csd-frame", + "wayland-cursor", + "wayland-protocols", + "wayland-protocols-wlr", + "wayland-scanner", + "xkeysym", +] + +[[package]] +name = "smithay-client-toolkit" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0512da38f5e2b31201a93524adb8d3136276fa4fe4aafab4e1f727a82b534cc0" +dependencies = [ + "bitflags 2.13.1", + "calloop 0.14.4", + "calloop-wayland-source 0.4.1", + "cursor-icon", + "libc", + "log", + "memmap2", + "rustix 1.1.4", + "thiserror 2.0.20", + "wayland-backend", + "wayland-client", + "wayland-csd-frame", + "wayland-cursor", + "wayland-protocols", + "wayland-protocols-experimental", + "wayland-protocols-misc", + "wayland-protocols-wlr", + "wayland-scanner", + "xkeysym", +] + +[[package]] +name = "smithay-clipboard" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71704c03f739f7745053bde45fa203a46c58d25bc5c4efba1d9a60e9dba81226" +dependencies = [ + "libc", + "smithay-client-toolkit 0.20.0", + "wayland-backend", +] + +[[package]] +name = "smol_str" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd538fb6910ac1099850255cf94a94df6551fbdd602454387d0adb2d1ca6dead" +dependencies = [ + "serde", +] + +[[package]] +name = "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.1", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12df2e0110f65b775f769bb17ef989067a1d931b2eb822bd4346631eeada89f9" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f" +dependencies = [ + "thiserror-impl 2.0.20", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.5", +] + +[[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 = "tinystr" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1e27c91459209c2986af3dcf603a5a74a4368754ce37414f59acc971167f643" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "toml" +version = "0.9.12+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863" +dependencies = [ + "indexmap", + "serde_core", + "serde_spanned", + "toml_datetime 0.7.5+spec-1.1.0", + "toml_parser", + "toml_writer", + "winnow 0.7.15", +] + +[[package]] +name = "toml_datetime" +version = "0.7.5+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.25.13+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6975367e4d2ef766d86af01ffad14b622fecc8d4357a998fbc4deb6e9bacaf9b" +dependencies = [ + "indexmap", + "toml_datetime 1.1.1+spec-1.1.0", + "toml_parser", + "winnow 1.0.4", +] + +[[package]] +name = "toml_parser" +version = "1.1.3+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56" +dependencies = [ + "winnow 1.0.4", +] + +[[package]] +name = "toml_writer" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d56353a2a665ad0f41a421187180aab746c8c325620617ad883a99a1cbe66d2" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "log", + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" + +[[package]] +name = "ttf-parser" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31" + +[[package]] +name = "type-map" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb30dbbd9036155e74adad6812e9898d03ec374946234fbcebd5dfc7b9187b90" +dependencies = [ + "rustc-hash 2.1.3", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-segmentation" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" + +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "version_check" +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 = "wasip2" +version = "1.0.4+wasi-0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.128" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aecb87a33d3b0c5e3b7aa46336eaf486cffafbd281b195e4c8b80d50df2351bf" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.78" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ef4c5d3d2cdf5c54f4231181768f5510842e350db025faf1f7163b1030ed928" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.128" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a690d511e3c1a8b3a55e33511e3c2c00c78415cd23650f32b808627f5696b9ed" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.128" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "411e4887f0071ef2d2164a9d5fdf2d20efbef78fccd3a78b0c10a1dc5295e48a" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 3.0.5", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.128" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81941cd78d0c92026c33e5e01312845a4cb1e9af3407f9134b100dd03144103e" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wayland-backend" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38a91b4eaddff87b1cd1074985e3713da4af2c49742d1b356b2c01670a67a078" +dependencies = [ + "cc", + "downcast-rs", + "rustix 1.1.4", + "scoped-tls", + "smallvec", + "wayland-sys", +] + +[[package]] +name = "wayland-client" +version = "0.31.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c36a0f861ad76d0901f2800b46321410d9f73f2ea88aac0650d86c32688073" +dependencies = [ + "bitflags 2.13.1", + "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.1", + "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.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23d0c813de3daa2ed6520af85a3bd49b0e722a3078506899aa9686fea58dc4b6" +dependencies = [ + "bitflags 2.13.1", + "wayland-backend", + "wayland-client", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-experimental" +version = "20250721.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40a1f863128dcaaec790d7b4b396cc9b9a7a079e878e18c47e6c2d2c5a8dcbb1" +dependencies = [ + "bitflags 2.13.1", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-misc" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e9567599ef23e09b8dad6e429e5738d4509dfc46b3b21f32841a304d16b29c8" +dependencies = [ + "bitflags 2.13.1", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-plasma" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b6d8cf1eb2c1c31ed1f5643c88a6e53538129d4af80030c8cabd1f9fa884d91" +dependencies = [ + "bitflags 2.13.1", + "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.1", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-scanner", +] + +[[package]] +name = "wayland-scanner" +version = "0.31.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "338e30461b3a2b67d70eb30a6d89f8e0c93a833e07d2ae89085cd070c4a00ac0" +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.105" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fbddc4a036f00ec4f18c83445bd3115cb306a91da554919a099d9222fe4a7f8" +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 = "webbrowser" +version = "1.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62c35be770821a214dbc362fc26908c853e776c0004294d0b10b8a6bad582f94" +dependencies = [ + "jni", + "log", + "ndk-context", + "objc2 0.6.4", + "objc2-app-kit 0.3.2", + "objc2-foundation 0.3.2", + "url", + "web-sys", +] + +[[package]] +name = "weezl" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88" + +[[package]] +name = "wgpu" +version = "27.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfe68bac7cde125de7a731c3400723cadaaf1703795ad3f4805f187459cd7a77" +dependencies = [ + "arrayvec", + "bitflags 2.13.1", + "cfg-if", + "cfg_aliases 0.2.2", + "document-features", + "hashbrown 0.16.1", + "log", + "portable-atomic", + "profiling", + "raw-window-handle", + "smallvec", + "static_assertions", + "wgpu-core", + "wgpu-hal", + "wgpu-types", +] + +[[package]] +name = "wgpu-core" +version = "27.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27a75de515543b1897b26119f93731b385a19aea165a1ec5f0e3acecc229cae7" +dependencies = [ + "arrayvec", + "bit-set", + "bit-vec", + "bitflags 2.13.1", + "bytemuck", + "cfg_aliases 0.2.2", + "document-features", + "hashbrown 0.16.1", + "indexmap", + "log", + "naga", + "once_cell", + "parking_lot", + "portable-atomic", + "profiling", + "raw-window-handle", + "rustc-hash 1.1.0", + "smallvec", + "thiserror 2.0.20", + "wgpu-core-deps-apple", + "wgpu-core-deps-emscripten", + "wgpu-core-deps-windows-linux-android", + "wgpu-hal", + "wgpu-types", +] + +[[package]] +name = "wgpu-core-deps-apple" +version = "27.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0772ae958e9be0c729561d5e3fd9a19679bcdfb945b8b1a1969d9bfe8056d233" +dependencies = [ + "wgpu-hal", +] + +[[package]] +name = "wgpu-core-deps-emscripten" +version = "27.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b06ac3444a95b0813ecfd81ddb2774b66220b264b3e2031152a4a29fda4da6b5" +dependencies = [ + "wgpu-hal", +] + +[[package]] +name = "wgpu-core-deps-windows-linux-android" +version = "27.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71197027d61a71748e4120f05a9242b2ad142e3c01f8c1b47707945a879a03c3" +dependencies = [ + "wgpu-hal", +] + +[[package]] +name = "wgpu-hal" +version = "27.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b21cb61c57ee198bc4aff71aeadff4cbb80b927beb912506af9c780d64313ce" +dependencies = [ + "android_system_properties", + "arrayvec", + "ash", + "bitflags 2.13.1", + "block", + "bytemuck", + "cfg-if", + "cfg_aliases 0.2.2", + "core-graphics-types 0.2.0", + "glow", + "glutin_wgl_sys", + "gpu-alloc", + "gpu-descriptor", + "hashbrown 0.16.1", + "js-sys", + "khronos-egl", + "libc", + "libloading", + "log", + "metal", + "naga", + "ndk-sys", + "objc", + "ordered-float", + "parking_lot", + "portable-atomic", + "portable-atomic-util", + "profiling", + "raw-window-handle", + "renderdoc-sys", + "smallvec", + "thiserror 2.0.20", + "wasm-bindgen", + "web-sys", + "wgpu-types", + "windows", +] + +[[package]] +name = "wgpu-types" +version = "27.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afdcf84c395990db737f2dd91628706cb31e86d72e53482320d368e52b5da5eb" +dependencies = [ + "bitflags 2.13.1", + "bytemuck", + "js-sys", + "log", + "thiserror 2.0.20", + "web-sys", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd04d41d93c4992d421894c18c8b43496aa748dd4c081bac0dc93eb0489272b6" +dependencies = [ + "windows-core", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-core" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ba6d44ec8c2591c134257ce647b7ea6b20335bf6379a27dac5f1641fcf59f99" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-result", + "windows-strings", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-implement" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "windows-interface" +version = "0.58.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-strings" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" +dependencies = [ + "windows-result", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "winit" +version = "0.30.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6755fa58a9f8350bd1e472d4c3fcc25f824ec358933bba33306d0b63df5978d" +dependencies = [ + "ahash", + "android-activity", + "atomic-waker", + "bitflags 2.13.1", + "block2", + "bytemuck", + "calloop 0.13.0", + "cfg_aliases 0.2.2", + "concurrent-queue", + "core-foundation 0.9.4", + "core-graphics", + "cursor-icon", + "dpi", + "js-sys", + "libc", + "memmap2", + "ndk", + "objc2 0.5.2", + "objc2-app-kit 0.2.2", + "objc2-foundation 0.2.2", + "objc2-ui-kit", + "orbclient", + "percent-encoding", + "pin-project", + "raw-window-handle", + "redox_syscall 0.4.1", + "rustix 0.38.44", + "smithay-client-toolkit 0.19.2", + "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 = "winnow" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" + +[[package]] +name = "winnow" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" +dependencies = [ + "memchr", +] + +[[package]] +name = "winreg" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d" +dependencies = [ + "winapi", +] + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "writeable" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc" + +[[package]] +name = "x11-dl" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38735924fedd5314a6e548792904ed8c6de6636285cb9fec04d5b1db85c1516f" +dependencies = [ + "libc", + "once_cell", + "pkg-config", +] + +[[package]] +name = "x11rb" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9993aa5be5a26815fe2c3eacfc1fde061fc1a1f094bf1ad2a18bf9c495dd7414" +dependencies = [ + "as-raw-xcb-connection", + "gethostname", + "libc", + "libloading", + "once_cell", + "rustix 1.1.4", + "x11rb-protocol", +] + +[[package]] +name = "x11rb-protocol" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea6fc2961e4ef194dcbfe56bb845534d0dc8098940c7e5c012a258bfec6701bd" + +[[package]] +name = "xcursor" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "163b33ed8786455e2fa5d72f554057ce3f3182425434f756cd39c99839d88e23" + +[[package]] +name = "xkbcommon-dl" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d039de8032a9a8856a6be89cea3e5d12fdd82306ab7c94d74e6deab2460651c5" +dependencies = [ + "bitflags 2.13.1", + "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.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e450f9b2ed1dff33c94c12589a87338689467b9c4f5d8a5710bd09a847d2c8a7" + +[[package]] +name = "yoke" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "556764e583adb45a9f8d413c2a147fa7e8d821e48e12b14fd560b607998b75eb" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2ab42fc20575779bd240faa45f94a74256f755c0fa9e89f0ede20d91d0cdfc1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", + "synstructure", +] + +[[package]] +name = "zerotrie" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ea269c3bd32f0a32c321907a2ae912ba6f4649bb0fc764a15627e99a7095a3f" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0464e17806c1d976d5cba29399c7f08e516e279e2ba493f63123b5fca67dd8" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34df6fc39dbd26ddc9c10e6a2984476e13acce22e64e4487636ef494369225da" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.5", +] + +[[package]] +name = "zlib-rs" +version = "0.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34b31d188d9d685a4f9c7b46d6e36631b07058d2cfe190267adce54dc230bf12" + +[[package]] +name = "zune-core" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d56377fd46368984a170bc5aac5567e52ca5da874caa60bea39fcbca78fb658b" + +[[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 new file mode 100644 index 0000000..adfd192 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "rust-term" +version = "0.1.0" +edition = "2024" +rust-version = "1.92" +description = "A Ghostty-powered terminal with tiled and floating panes" +license = "MIT" + +[dependencies] +anyhow = "1" +eframe = { version = "0.33", default-features = false, features = ["default_fonts", "wgpu", "wayland", "x11"] } +egui = "0.33" +libghostty-vt = { version = "=0.2.1", features = ["png"] } +png = "0.18" +portable-pty = "0.9" +serde = { version = "1", features = ["derive"] } +toml = "0.9" +wgpu = { version = "27", default-features = false, features = ["std", "vulkan", "gles", "metal", "wgsl"] } + +[dev-dependencies] +base64 = "0.22" + +[profile.release] +lto = "thin" diff --git a/config.example.toml b/config.example.toml new file mode 100644 index 0000000..03eb931 --- /dev/null +++ b/config.example.toml @@ -0,0 +1,40 @@ +# ~/.config/rust-term/config.toml +# Omit shell to use $SHELL. Arguments are separate array entries. +# shell = ["/bin/bash", "--login"] +# working_directory = "/home/you" +font_path = "/usr/local/share/fonts/MononokiNerdFont-Regular.ttf" +font_size = 16 +scrollback = 10000 +window_width = 1100 +window_height = 720 +background = "#17191c" +foreground = "#e2e4e8" +accent = "#58c4ad" + +[kitty] +image_storage_mb = 64 +allow_file = true +allow_temp_file = true +allow_shared_memory = true + +[multiplexer] +max_panes = 32 +floating_width = 0.65 +floating_height = 0.65 + +# Providing [keys] replaces the entire default map. +[keys] +"Alt+F" = "toggle_floats" +"Alt+M" = "new_pane" +"Alt+H" = "focus_left" +"Alt+J" = "focus_down" +"Alt+K" = "focus_up" +"Alt+L" = "focus_right" +"Alt+A" = "new_tab" +"Alt+Shift+H" = "previous_tab" +"Alt+Shift+L" = "next_tab" +"Alt+X" = "close_pane" +"Ctrl+Shift+Z" = "zoom_pane" +"Ctrl+Shift+R" = "reload_config" +"Ctrl+Shift+C" = "copy" +"Ctrl+Shift+V" = "paste" diff --git a/src/app.rs b/src/app.rs new file mode 100644 index 0000000..7d24407 --- /dev/null +++ b/src/app.rs @@ -0,0 +1,606 @@ +use crate::{ + config::{self, Action, Config}, + input, + layout::{Axis, Direction, Layout, PaneId, Tab}, + pane::{self, Pane}, + render::color, +}; +use anyhow::{Result, ensure}; +use egui::{Color32, Rect, Stroke, vec2}; +use std::{ + collections::{BTreeMap, HashSet}, + path::PathBuf, +}; + +pub struct App { + config: Config, + config_path: PathBuf, + panes: BTreeMap, + tabs: Vec, + current: usize, + next_id: u64, + error: Option, + suppressed: HashSet, +} + +pub fn fonts(config: &Config) -> Result { + let mut fonts = egui::FontDefinitions::default(); + if let Some(path) = &config.font_path { + let data = std::fs::read(path)?; + fonts + .font_data + .insert("terminal".into(), egui::FontData::from_owned(data).into()); + fonts + .families + .get_mut(&egui::FontFamily::Monospace) + .unwrap() + .insert(0, "terminal".into()); + } + Ok(fonts) +} + +impl App { + pub fn new( + cc: &eframe::CreationContext<'_>, + config: Config, + config_path: PathBuf, + ) -> Result { + cc.egui_ctx.set_visuals(egui::Visuals::dark()); + cc.egui_ctx.set_fonts(fonts(&config)?); + let pane = Pane::new(&config, &cc.egui_ctx)?; + Ok(Self { + config, + config_path, + panes: BTreeMap::from([(1, pane)]), + tabs: vec![Tab::new(1, 1)], + current: 0, + next_id: 2, + error: None, + suppressed: HashSet::new(), + }) + } + + fn spawn(&mut self, ctx: &egui::Context) -> Result { + ensure!( + self.panes.len() < self.config.multiplexer.max_panes, + "Pane limit reached" + ); + let pane = Pane::new(&self.config, ctx)?; + let id = self.next_id; + self.next_id += 1; + self.panes.insert(id, pane); + Ok(id) + } + + fn action(&mut self, action: Action, ctx: &egui::Context) -> Result<()> { + let active = self.tabs[self.current].active; + match action { + Action::NewPane => { + let action = if self.tabs[self.current].floats_visible { + Action::NewFloating + } else { + let rect = self.panes[&active].last_rect; + if rect.is_positive() && rect.height() > rect.width() { + Action::SplitDown + } else { + Action::SplitRight + } + }; + self.action(action, ctx)?; + } + Action::SplitRight | Action::SplitDown => { + let id = self.spawn(ctx)?; + let tab = &mut self.tabs[self.current]; + let axis = if action == Action::SplitRight { + Axis::Horizontal + } else { + Axis::Vertical + }; + if let Some(root) = &mut tab.root { + root.split( + if tab.floats.contains(&active) { + root.first() + } else { + active + }, + id, + axis, + ); + } else { + tab.root = Some(Layout::Pane(id)); + } + tab.active = id; + tab.zoomed = false; + } + Action::NewFloating => { + let id = self.spawn(ctx)?; + let tab = &mut self.tabs[self.current]; + tab.floats.push(id); + tab.active = id; + tab.floats_visible = true; + tab.zoomed = false; + } + Action::ToggleFloating => { + let tab = &mut self.tabs[self.current]; + let was_floating = tab.floats.contains(&active); + tab.detach(active); + if was_floating { + if let Some(root) = &mut tab.root { + root.split(root.first(), active, Axis::Horizontal); + } else { + tab.root = Some(Layout::Pane(active)); + } + } else { + tab.floats.push(active); + tab.floats_visible = true; + } + } + Action::ToggleFloats => { + if self.tabs[self.current].floats.is_empty() { + self.action(Action::NewFloating, ctx)?; + } else { + self.tabs[self.current].toggle_floats(); + } + } + Action::FocusLeft | Action::FocusDown | Action::FocusUp | Action::FocusRight => { + let direction = match action { + Action::FocusLeft => Direction::Left, + Action::FocusDown => Direction::Down, + Action::FocusUp => Direction::Up, + _ => Direction::Right, + }; + let rects = self + .panes + .iter() + .map(|(id, pane)| (*id, pane.last_rect)) + .collect::>(); + self.tabs[self.current].focus_direction(direction, &rects); + } + Action::NextPane | Action::PreviousPane => { + self.tabs[self.current].cycle(action == Action::PreviousPane) + } + Action::ClosePane => self.close(active, ctx), + Action::ZoomPane => { + let tab = &mut self.tabs[self.current]; + if tab.is_visible(tab.active) { + tab.zoomed = !tab.zoomed; + } + } + Action::NewTab => { + let id = self.spawn(ctx)?; + self.tabs.push(Tab::new(id, id)); + self.current = self.tabs.len() - 1; + } + Action::NextTab => self.current = (self.current + 1) % self.tabs.len(), + Action::PreviousTab => { + self.current = (self.current + self.tabs.len() - 1) % self.tabs.len() + } + Action::ReloadConfig => { + let config = Config::load(&self.config_path)?; + let fonts = fonts(&config)?; + for pane in self.panes.values_mut() { + pane::apply_config(&mut pane.terminal, &config)?; + } + ctx.set_fonts(fonts); + self.config = config; + } + Action::Copy => { + if let Some(text) = self.panes[&active].copy()? { + ctx.copy_text(text); + } + } + Action::Paste => ctx.send_viewport_cmd(egui::ViewportCommand::RequestPaste), + } + Ok(()) + } + + fn close(&mut self, id: PaneId, ctx: &egui::Context) { + self.panes.remove(&id); + for tab in &mut self.tabs { + if !tab.ids().contains(&id) { + continue; + } + tab.detach(id); + if tab.active == id + && let Some(next) = tab.ids().first() + { + tab.active = *next; + } + if tab.root.is_none() { + tab.floats_visible = true; + } + } + self.tabs.retain(|t| !t.ids().is_empty()); + self.current = self.current.min(self.tabs.len().saturating_sub(1)); + if self.tabs.is_empty() { + ctx.send_viewport_cmd(egui::ViewportCommand::Close); + } + } + + fn keyboard(&mut self, ctx: &egui::Context) -> Result<()> { + let events = ctx.input(|i| i.events.clone()); + let modifiers = ctx.input(|i| i.modifiers); + let mut terminal_events = vec![]; + let mut suppress_text = false; + for event in events { + if let egui::Event::Key { + key, + pressed, + repeat, + modifiers, + .. + } = &event + { + suppress_text = false; + if !pressed && self.suppressed.remove(key) { + continue; + } + let action = self.config.keys.iter().find_map(|(s, a)| { + let shortcut = config::shortcut(s).ok()?; + let m = shortcut.modifiers; + (shortcut.logical_key == *key + && m.ctrl == modifiers.ctrl + && m.shift == modifiers.shift + && m.alt == modifiers.alt + && m.mac_cmd == modifiers.mac_cmd) + .then_some(*a) + }); + if *pressed && let Some(action) = action { + self.flush_keys(&mut terminal_events)?; + self.suppressed.insert(*key); + suppress_text = true; + if !repeat { + self.action(action, ctx)?; + } + if self.tabs.is_empty() { + return Ok(()); + } + continue; + } + } + if suppress_text && matches!(event, egui::Event::Text(_)) { + continue; + } + let active = self.tabs[self.current].active; + if !self.tabs[self.current].is_visible(active) { + continue; + } + match event { + egui::Event::Paste(text) => { + self.flush_keys(&mut terminal_events)?; + self.panes.get_mut(&active).unwrap().paste(&text)?; + } + egui::Event::Copy => { + self.flush_keys(&mut terminal_events)?; + if modifiers.shift || modifiers.mac_cmd { + self.action(Action::Copy, ctx)?; + } else { + self.panes.get_mut(&active).unwrap().send(vec![3])?; + } + } + egui::Event::Cut => { + self.flush_keys(&mut terminal_events)?; + self.panes.get_mut(&active).unwrap().send(vec![24])?; + } + other => terminal_events.push(other), + } + } + self.flush_keys(&mut terminal_events) + } + + fn flush_keys(&mut self, events: &mut Vec) -> Result<()> { + let active = self.tabs[self.current].active; + if !self.tabs[self.current].is_visible(active) { + events.clear(); + return Ok(()); + } + let pane = self.panes.get_mut(&active).unwrap(); + let encoded = input::encode(&pane.terminal, events)?; + pane.send(encoded)?; + events.clear(); + Ok(()) + } +} + +impl eframe::App for App { + fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { + if self.tabs.is_empty() { + return; + } + for pane in self.panes.values_mut() { + pane.drain(ctx); + } + if let Err(e) = self.keyboard(ctx) { + self.error = Some(e.to_string()); + } + if self.tabs.is_empty() { + return; + } + let mut action = None; + egui::TopBottomPanel::top("tabs").show(ctx, |ui| { + ui.horizontal_wrapped(|ui| { + for (index, tab) in self.tabs.iter().enumerate() { + let title = self.panes[&tab.active].terminal.title().unwrap_or(""); + let label = if title.is_empty() { + format!("{}: shell", index + 1) + } else { + format!( + "{}: {}", + index + 1, + title.chars().take(28).collect::() + ) + }; + if ui.selectable_label(self.current == index, label).clicked() { + self.current = index; + } + } + if ui.button("+").on_hover_text("New tab").clicked() { + action = Some(Action::NewTab); + } + ui.menu_button("Pane", |ui| { + for (label, a) in [ + ("New pane", Action::NewPane), + ("Split right", Action::SplitRight), + ("Split down", Action::SplitDown), + ("New floating pane", Action::NewFloating), + ("Float / dock", Action::ToggleFloating), + ("Show / hide floating panes", Action::ToggleFloats), + ("Zoom / restore", Action::ZoomPane), + ("Next pane", Action::NextPane), + ("Close pane", Action::ClosePane), + ] { + if ui.button(label).clicked() { + action = Some(a); + ui.close(); + } + } + }); + ui.menu_button("Settings", |ui| { + if ui.button("Reload config").clicked() { + action = Some(Action::ReloadConfig); + ui.close(); + } + }); + }); + }); + if let Some(error) = self.error.clone() { + egui::TopBottomPanel::bottom("error").show(ctx, |ui| { + ui.horizontal_wrapped(|ui| { + ui.colored_label(Color32::LIGHT_RED, error); + if ui.button("Dismiss").clicked() { + self.error = None; + } + }); + }); + } + let bg = color(config::rgb(&self.config.background).unwrap()); + let tab = &mut self.tabs[self.current]; + let mut tiled = vec![]; + let mut workspace = Rect::NOTHING; + egui::CentralPanel::default() + .frame(egui::Frame::NONE.fill(bg)) + .show(ctx, |ui| { + workspace = ui.max_rect(); + if tab.zoomed { + tiled.push((tab.active, workspace)); + } else if let Some(root) = &mut tab.root { + root.arrange(ui, workspace, &mut tiled, 1); + } + for (id, rect) in &tiled { + let pane = self.panes.get_mut(id).unwrap(); + let active = tab.active == *id; + if !tab.zoomed { + pane.last_rect = *rect; + } + if show_pane(ui, *id, pane, *rect, &self.config, active) { + tab.active = *id; + } + } + }); + if tab.floats_visible && !tab.zoomed { + for id in tab.floats.clone() { + let pane = self.panes.get_mut(&id).unwrap(); + let window_id = egui::Id::new(("float", tab.id, id)); + let active = tab.active == id; + let result = egui::Window::new(pane_title(id, pane)) + .id(window_id) + .title_bar(false) + .constrain_to(workspace) + .default_pos( + workspace.min + + vec2(40.0 + (id % 5) as f32 * 22.0, 40.0 + (id % 5) as f32 * 22.0), + ) + .default_size( + workspace.size() + * vec2( + self.config.multiplexer.floating_width, + self.config.multiplexer.floating_height, + ), + ) + .min_size(vec2(160.0, 100.0)) + .frame(egui::Frame::NONE.fill(bg)) + .show(ctx, |ui| { + let rect = ui.available_rect_before_wrap(); + ui.allocate_rect(rect, egui::Sense::hover()); + pane.last_rect = rect; + if show_pane(ui, id, pane, rect, &self.config, active) { + tab.active = id; + } + }); + if let Some(result) = result + && result.response.contains_pointer() + && ctx.input(|i| i.pointer.any_pressed()) + { + tab.active = id; + } + if active { + ctx.move_to_top(egui::LayerId::new(egui::Order::Middle, window_id)); + } + } + } + let active = tab.active; + for (id, pane) in &mut self.panes { + if let Err(e) = + pane.focus(*id == active && tab.is_visible(*id) && ctx.input(|i| i.focused)) + { + pane.error = Some(e.to_string()); + } + } + if !self.tabs.is_empty() + && let Some(action) = action + && let Err(e) = self.action(action, ctx) + { + self.error = Some(e.to_string()); + } + } +} + +fn show_pane( + ui: &mut egui::Ui, + id: PaneId, + pane: &mut Pane, + rect: Rect, + config: &Config, + active: bool, +) -> bool { + let accent = color(config::rgb(&config.accent).unwrap()); + let title_rect = Rect::from_min_size(rect.min, vec2(rect.width(), 25.0_f32.min(rect.height()))); + ui.painter().rect_filled( + title_rect, + 0, + Color32::from_gray(if active { 43 } else { 31 }), + ); + ui.painter() + .with_clip_rect(title_rect.shrink(3.0).intersect(ui.clip_rect())) + .text( + title_rect.min + vec2(7.0, 4.0), + egui::Align2::LEFT_TOP, + pane_title(id, pane), + egui::FontId::proportional(13.0), + if active { accent } else { Color32::GRAY }, + ); + // Click-only leaves dragging to the floating window's existing move handler. + let header = ui.interact( + title_rect, + ui.id().with(("title", id)), + egui::Sense::click(), + ); + let mut focused = + header.clicked() || (header.contains_pointer() && ui.input(|i| i.pointer.any_pressed())); + let content = Rect::from_min_max(egui::pos2(rect.left(), title_rect.bottom()), rect.max); + // The pane rectangle is already allocated; a layout scope would grow its parent. + let mut content_ui = ui.new_child(egui::UiBuilder::new().id_salt(id).max_rect(content)); + content_ui.set_clip_rect(content.intersect(ui.clip_rect())); + match pane.show(&mut content_ui, content, config, active) { + Ok(clicked) => focused |= clicked, + Err(e) => pane.error = Some(e.to_string()), + } + ui.painter().rect_stroke( + rect, + 0, + Stroke::new( + 1.0, + if active { + accent + } else { + Color32::from_gray(54) + }, + ), + egui::StrokeKind::Inside, + ); + focused +} + +fn pane_title(id: PaneId, pane: &Pane) -> String { + let title = pane.terminal.title().unwrap_or(""); + format!( + "{id} {}{}", + if title.is_empty() { "shell" } else { title }, + if pane.dead { " [exited]" } else { "" } + ) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn test_app(ctx: &egui::Context) -> App { + let config = Config { + shell: vec!["/bin/sh".into()], + ..Default::default() + }; + let pane = Pane::new(&config, ctx).unwrap(); + App { + config, + config_path: PathBuf::new(), + panes: BTreeMap::from([(1, pane)]), + tabs: vec![Tab::new(1, 1)], + current: 0, + next_id: 2, + error: None, + suppressed: HashSet::new(), + } + } + + #[test] + fn floating_pane_size_stays_constant_across_frames() { + let ctx = egui::Context::default(); + let mut app = test_app(&ctx); + let mut sizes = vec![]; + for frame in 0..20 { + let input = egui::RawInput { + screen_rect: Some(Rect::from_min_size(egui::Pos2::ZERO, vec2(1000.0, 800.0))), + time: Some(frame as f64 / 60.0), + ..Default::default() + }; + let _ = ctx.run(input, |ctx| { + let window = egui::Window::new("float") + .title_bar(false) + .default_size(vec2(400.0, 240.0)) + .frame(egui::Frame::NONE) + .show(ctx, |ui| { + let rect = ui.available_rect_before_wrap(); + ui.allocate_rect(rect, egui::Sense::hover()); + show_pane( + ui, + 1, + app.panes.get_mut(&1).unwrap(), + rect, + &app.config, + true, + ); + }) + .unwrap(); + if frame >= 2 { + sizes.push(window.response.rect.size()); + } + }); + } + assert!( + sizes.iter().all(|size| *size == sizes[0]), + "floating pane grew: {sizes:?}" + ); + } + + #[test] + fn toggle_creates_first_float_then_reuses_it_and_close_removes_focus() { + let ctx = egui::Context::default(); + let mut app = test_app(&ctx); + app.action(Action::ToggleFloats, &ctx).unwrap(); + assert_eq!(app.tabs[0].floats, vec![2]); + assert_eq!(app.tabs[0].active, 2); + assert!(app.tabs[0].floats_visible); + app.action(Action::ToggleFloats, &ctx).unwrap(); + assert!(!app.tabs[0].floats_visible); + assert_eq!(app.tabs[0].active, 1); + app.action(Action::ToggleFloats, &ctx).unwrap(); + assert_eq!(app.panes.len(), 2); + assert_eq!(app.tabs[0].active, 2); + app.action(Action::ClosePane, &ctx).unwrap(); + assert_eq!(app.tabs[0].active, 1); + assert!(!app.panes.contains_key(&2)); + app.action(Action::ToggleFloats, &ctx).unwrap(); + assert_eq!(app.tabs[0].floats, vec![3]); + assert!(app.tabs[0].floats_visible); + } +} diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..ca1b61f --- /dev/null +++ b/src/config.rs @@ -0,0 +1,267 @@ +use anyhow::{Context, Result, bail, ensure}; +use serde::Deserialize; +use std::{ + collections::BTreeMap, + path::{Path, PathBuf}, +}; + +pub const EXAMPLE: &str = include_str!("../config.example.toml"); + +#[derive(Clone, Debug, Deserialize)] +#[serde(default, deny_unknown_fields)] +pub struct Config { + pub shell: Vec, + pub working_directory: Option, + pub font_size: f32, + pub font_path: Option, + pub scrollback: usize, + pub window_width: f32, + pub window_height: f32, + pub background: String, + pub foreground: String, + pub accent: String, + pub kitty: Kitty, + pub multiplexer: Multiplexer, + pub keys: BTreeMap, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(default, deny_unknown_fields)] +pub struct Kitty { + pub image_storage_mb: u64, + pub allow_file: bool, + pub allow_temp_file: bool, + pub allow_shared_memory: bool, +} + +#[derive(Clone, Debug, Deserialize)] +#[serde(default, deny_unknown_fields)] +pub struct Multiplexer { + pub max_panes: usize, + pub floating_width: f32, + pub floating_height: f32, +} + +#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] +pub enum Action { + SplitRight, + SplitDown, + NewFloating, + NewPane, + ToggleFloating, + ToggleFloats, + NextPane, + PreviousPane, + FocusLeft, + FocusDown, + FocusUp, + FocusRight, + ClosePane, + ZoomPane, + NewTab, + NextTab, + PreviousTab, + ReloadConfig, + Copy, + Paste, +} + +impl Default for Kitty { + fn default() -> Self { + Self { + image_storage_mb: 64, + allow_file: true, + allow_temp_file: true, + allow_shared_memory: true, + } + } +} +impl Default for Multiplexer { + fn default() -> Self { + Self { + max_panes: 32, + floating_width: 0.65, + floating_height: 0.65, + } + } +} +impl Default for Config { + fn default() -> Self { + Self { + shell: vec![std::env::var("SHELL").unwrap_or_else(|_| "/bin/sh".into())], + working_directory: None, + font_size: 16.0, + font_path: None, + scrollback: 10000, + window_width: 1100.0, + window_height: 720.0, + background: "#17191c".into(), + foreground: "#e2e4e8".into(), + accent: "#58c4ad".into(), + kitty: Kitty::default(), + multiplexer: Multiplexer::default(), + keys: [ + ("Alt+F", Action::ToggleFloats), + ("Alt+M", Action::NewPane), + ("Alt+H", Action::FocusLeft), + ("Alt+J", Action::FocusDown), + ("Alt+K", Action::FocusUp), + ("Alt+L", Action::FocusRight), + ("Alt+A", Action::NewTab), + ("Alt+Shift+H", Action::PreviousTab), + ("Alt+Shift+L", Action::NextTab), + ("Alt+X", Action::ClosePane), + ("Ctrl+Shift+Z", Action::ZoomPane), + ("Ctrl+Shift+R", Action::ReloadConfig), + ("Ctrl+Shift+C", Action::Copy), + ("Ctrl+Shift+V", Action::Paste), + ] + .into_iter() + .map(|(k, v)| (k.into(), v)) + .collect(), + } + } +} + +impl Config { + pub fn path() -> Result { + if let Some(dir) = std::env::var_os("XDG_CONFIG_HOME").filter(|p| !p.is_empty()) { + return Ok(PathBuf::from(dir).join("rust-term/config.toml")); + } + Ok( + PathBuf::from(std::env::var_os("HOME").context("HOME is not set")?) + .join(".config/rust-term/config.toml"), + ) + } + + pub fn load(path: &Path) -> Result { + match std::fs::read_to_string(path) { + Ok(text) => { + Self::parse(&text).with_context(|| format!("Invalid config: {}", path.display())) + } + Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(Self::default()), + Err(e) => Err(e).with_context(|| format!("Cannot read {}", path.display())), + } + } + + pub fn parse(text: &str) -> Result { + let config: Self = toml::from_str(text)?; + ensure!( + !config.shell.is_empty() && !config.shell[0].is_empty(), + "shell must contain an executable" + ); + ensure!( + (8.0..=48.0).contains(&config.font_size), + "font_size must be 8..48" + ); + ensure!( + (320.0..=16384.0).contains(&config.window_width) + && (200.0..=16384.0).contains(&config.window_height), + "invalid window dimensions" + ); + ensure!( + config.scrollback <= 1_000_000, + "scrollback must be <= 1000000" + ); + ensure!( + (1..=1024).contains(&config.kitty.image_storage_mb), + "image_storage_mb must be 1..1024" + ); + ensure!( + (1..=128).contains(&config.multiplexer.max_panes), + "max_panes must be 1..128" + ); + for fraction in [ + config.multiplexer.floating_width, + config.multiplexer.floating_height, + ] { + ensure!( + (0.2..=0.95).contains(&fraction), + "floating dimensions must be fractions in 0.2..0.95" + ); + } + for color in [&config.background, &config.foreground, &config.accent] { + rgb(color)?; + } + for key in config.keys.keys() { + shortcut(key)?; + } + Ok(config) + } +} + +pub fn rgb(text: &str) -> Result { + ensure!( + text.len() == 7 + && text.starts_with('#') + && text[1..].bytes().all(|c| c.is_ascii_hexdigit()), + "color must be #RRGGBB: {text}" + ); + let value = u32::from_str_radix(&text[1..], 16)?; + Ok(libghostty_vt::style::RgbColor { + r: (value >> 16) as u8, + g: (value >> 8) as u8, + b: value as u8, + }) +} + +pub fn shortcut(text: &str) -> Result { + let mut mods = egui::Modifiers::NONE; + let parts: Vec<_> = text.split('+').collect(); + for part in &parts[..parts.len() - 1] { + match *part { + "Ctrl" => mods.ctrl = true, + "Shift" => mods.shift = true, + "Alt" => mods.alt = true, + "Super" => { + mods.mac_cmd = true; + mods.command = true; + } + _ => bail!("unknown modifier in {text}"), + } + } + let key = egui::Key::from_name(parts[parts.len() - 1]) + .with_context(|| format!("unknown key in {text}"))?; + Ok(egui::KeyboardShortcut::new(mods, key)) +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn validates_example_and_rejects_bad_values() { + let example = Config::parse(EXAMPLE).unwrap(); + assert_eq!(example.keys, Config::default().keys); + for text in [ + "font_size = nan", + "shell = []", + "background = '#xx0000'", + "font_sze = 16", + "[keys]\n'Hyper+Q' = 'close_pane'", + "[multiplexer]\nfloating_width = 2", + ] { + assert!(Config::parse(text).is_err(), "accepted {text}"); + } + } + + #[test] + fn default_shortcuts_match_pane_and_tab_actions() { + let config = Config::default(); + for (binding, action) in [ + ("Alt+F", Action::ToggleFloats), + ("Alt+M", Action::NewPane), + ("Alt+H", Action::FocusLeft), + ("Alt+J", Action::FocusDown), + ("Alt+K", Action::FocusUp), + ("Alt+L", Action::FocusRight), + ("Alt+A", Action::NewTab), + ("Alt+Shift+H", Action::PreviousTab), + ("Alt+Shift+L", Action::NextTab), + ("Alt+X", Action::ClosePane), + ] { + assert_eq!(config.keys.get(binding), Some(&action)); + assert!(shortcut(binding).unwrap().modifiers.alt); + } + } +} diff --git a/src/input.rs b/src/input.rs new file mode 100644 index 0000000..5d8b53d --- /dev/null +++ b/src/input.rs @@ -0,0 +1,180 @@ +use anyhow::Result; +use libghostty_vt::{Terminal, key}; + +pub fn mods(m: egui::Modifiers) -> key::Mods { + let mut out = key::Mods::empty(); + if m.ctrl { + out |= key::Mods::CTRL; + } + if m.shift { + out |= key::Mods::SHIFT; + } + if m.alt { + out |= key::Mods::ALT; + } + if m.mac_cmd { + out |= key::Mods::SUPER; + } + out +} + +pub fn encode(terminal: &Terminal<'_, '_>, events: &[egui::Event]) -> Result> { + let mut encoder = key::Encoder::new()?; + encoder.set_options_from_terminal(terminal); + let mut event = key::Event::new()?; + let mut out = vec![]; + let mut used_text = std::collections::HashSet::new(); + for (index, e) in events.iter().enumerate() { + match e { + egui::Event::Key { + key: k, + physical_key, + pressed, + repeat, + modifiers, + } => { + let (logical, ucp) = map(*k); + let (physical, _) = map(physical_key.unwrap_or(*k)); + let mut text = None; + if *pressed && printable(*k) { + // winit emits text immediately after its key event. Stop at the next key + // so a burst of distinct keys cannot consume each other's text. + for (i, next) in events.iter().enumerate().skip(index + 1) { + if matches!(next, egui::Event::Key { .. }) { + break; + } + if let egui::Event::Text(value) = next { + text = Some(value.as_str()); + used_text.insert(i); + break; + } + } + } + event + .set_action(if !pressed { + key::Action::Release + } else if *repeat { + key::Action::Repeat + } else { + key::Action::Press + }) + .set_key(if physical == key::Key::Unidentified { + logical + } else { + physical + }) + .set_mods(mods(*modifiers)) + .set_consumed_mods(if text.is_some() { + key::Mods::SHIFT + } else { + key::Mods::empty() + }) + .set_unshifted_codepoint(ucp) + .set_utf8(text); + encoder.encode_to_vec(&event, &mut out)?; + } + egui::Event::Text(text) if !used_text.contains(&index) => { + event + .set_action(key::Action::Press) + .set_key(key::Key::Unidentified) + .set_mods(key::Mods::empty()) + .set_consumed_mods(key::Mods::empty()) + .set_unshifted_codepoint('\0') + .set_utf8(Some(text.as_str())); + encoder.encode_to_vec(&event, &mut out)?; + } + egui::Event::Ime(egui::ImeEvent::Commit(text)) => { + out.extend_from_slice(text.as_bytes()) + } + _ => {} + } + } + Ok(out) +} + +fn printable(k: egui::Key) -> bool { + let (_, c) = map(k); + c != '\0' +} + +fn map(k: egui::Key) -> (key::Key, char) { + use egui::Key as E; + use key::Key as G; + match k { + E::A => (G::A, 'a'), + E::B => (G::B, 'b'), + E::C => (G::C, 'c'), + E::D => (G::D, 'd'), + E::E => (G::E, 'e'), + E::F => (G::F, 'f'), + E::G => (G::G, 'g'), + E::H => (G::H, 'h'), + E::I => (G::I, 'i'), + E::J => (G::J, 'j'), + E::K => (G::K, 'k'), + E::L => (G::L, 'l'), + E::M => (G::M, 'm'), + E::N => (G::N, 'n'), + E::O => (G::O, 'o'), + E::P => (G::P, 'p'), + E::Q => (G::Q, 'q'), + E::R => (G::R, 'r'), + E::S => (G::S, 's'), + E::T => (G::T, 't'), + E::U => (G::U, 'u'), + E::V => (G::V, 'v'), + E::W => (G::W, 'w'), + E::X => (G::X, 'x'), + E::Y => (G::Y, 'y'), + E::Z => (G::Z, 'z'), + E::Num0 => (G::Digit0, '0'), + E::Num1 => (G::Digit1, '1'), + E::Num2 => (G::Digit2, '2'), + E::Num3 => (G::Digit3, '3'), + E::Num4 => (G::Digit4, '4'), + E::Num5 => (G::Digit5, '5'), + E::Num6 => (G::Digit6, '6'), + E::Num7 => (G::Digit7, '7'), + E::Num8 => (G::Digit8, '8'), + E::Num9 => (G::Digit9, '9'), + E::Space => (G::Space, ' '), + E::Minus => (G::Minus, '-'), + E::Equals => (G::Equal, '='), + E::Comma => (G::Comma, ','), + E::Period => (G::Period, '.'), + E::Slash => (G::Slash, '/'), + E::Backslash => (G::Backslash, '\\'), + E::OpenBracket => (G::BracketLeft, '['), + E::CloseBracket => (G::BracketRight, ']'), + E::Backtick => (G::Backquote, '`'), + E::Semicolon => (G::Semicolon, ';'), + E::Quote => (G::Quote, '\''), + E::ArrowDown => (G::ArrowDown, '\0'), + E::ArrowUp => (G::ArrowUp, '\0'), + E::ArrowLeft => (G::ArrowLeft, '\0'), + E::ArrowRight => (G::ArrowRight, '\0'), + E::Enter => (G::Enter, '\0'), + E::Escape => (G::Escape, '\0'), + E::Tab => (G::Tab, '\0'), + E::Backspace => (G::Backspace, '\0'), + E::Delete => (G::Delete, '\0'), + E::Insert => (G::Insert, '\0'), + E::Home => (G::Home, '\0'), + E::End => (G::End, '\0'), + E::PageUp => (G::PageUp, '\0'), + E::PageDown => (G::PageDown, '\0'), + E::F1 => (G::F1, '\0'), + E::F2 => (G::F2, '\0'), + E::F3 => (G::F3, '\0'), + E::F4 => (G::F4, '\0'), + E::F5 => (G::F5, '\0'), + E::F6 => (G::F6, '\0'), + E::F7 => (G::F7, '\0'), + E::F8 => (G::F8, '\0'), + E::F9 => (G::F9, '\0'), + E::F10 => (G::F10, '\0'), + E::F11 => (G::F11, '\0'), + E::F12 => (G::F12, '\0'), + _ => (G::Unidentified, '\0'), + } +} diff --git a/src/layout.rs b/src/layout.rs new file mode 100644 index 0000000..81547f1 --- /dev/null +++ b/src/layout.rs @@ -0,0 +1,365 @@ +use egui::Rect; + +pub type PaneId = u64; + +#[derive(Clone, Copy, Debug)] +pub enum Axis { + Horizontal, + Vertical, +} + +#[derive(Clone, Copy)] +pub enum Direction { + Left, + Down, + Up, + Right, +} + +#[derive(Debug)] +pub enum Layout { + Pane(PaneId), + Split { + axis: Axis, + ratio: f32, + first: Box, + second: Box, + }, +} + +impl Layout { + pub fn split(&mut self, target: PaneId, new: PaneId, axis: Axis) -> bool { + match self { + Self::Pane(id) if *id == target => { + *self = Self::Split { + axis, + ratio: 0.5, + first: Box::new(Self::Pane(target)), + second: Box::new(Self::Pane(new)), + }; + true + } + Self::Split { first, second, .. } => { + first.split(target, new, axis) || second.split(target, new, axis) + } + _ => false, + } + } + + pub fn remove(self, target: PaneId) -> Option { + match self { + Self::Pane(id) => (id != target).then_some(Self::Pane(id)), + Self::Split { + axis, + ratio, + first, + second, + } => match (first.remove(target), second.remove(target)) { + (Some(first), Some(second)) => Some(Self::Split { + axis, + ratio, + first: Box::new(first), + second: Box::new(second), + }), + (first, second) => first.or(second), + }, + } + } + + pub fn ids(&self, out: &mut Vec) { + match self { + Self::Pane(id) => out.push(*id), + Self::Split { first, second, .. } => { + first.ids(out); + second.ids(out); + } + } + } + + pub fn first(&self) -> PaneId { + match self { + Self::Pane(id) => *id, + Self::Split { first, .. } => first.first(), + } + } + + pub fn arrange( + &mut self, + ui: &mut egui::Ui, + rect: Rect, + out: &mut Vec<(PaneId, Rect)>, + path: u64, + ) { + match self { + Self::Pane(id) => out.push((*id, rect)), + Self::Split { + axis, + ratio, + first, + second, + } => { + let horizontal = matches!(axis, Axis::Horizontal); + let span = if horizontal { + rect.width() + } else { + rect.height() + }; + let start = if horizontal { rect.left() } else { rect.top() }; + let mut a = rect; + let mut b = rect; + let mid = start + span * *ratio; + if horizontal { + a.max.x = mid; + b.min.x = mid; + } else { + a.max.y = mid; + b.min.y = mid; + } + let divider = if horizontal { + Rect::from_min_max( + egui::pos2(mid - 3.0, rect.top()), + egui::pos2(mid + 3.0, rect.bottom()), + ) + } else { + Rect::from_min_max( + egui::pos2(rect.left(), mid - 3.0), + egui::pos2(rect.right(), mid + 3.0), + ) + }; + let response = + ui.interact(divider, ui.id().with(("split", path)), egui::Sense::drag()); + response.clone().on_hover_cursor(if horizontal { + egui::CursorIcon::ResizeHorizontal + } else { + egui::CursorIcon::ResizeVertical + }); + if response.dragged() + && let Some(pos) = response.interact_pointer_pos() + { + *ratio = ((if horizontal { pos.x } else { pos.y } - start) / span.max(1.0)) + .clamp(0.1, 0.9); + } + first.arrange(ui, a, out, path * 2); + second.arrange(ui, b, out, path * 2 + 1); + } + } + } +} + +pub struct Tab { + pub id: u64, + pub root: Option, + pub floats: Vec, + pub active: PaneId, + pub floats_visible: bool, + pub zoomed: bool, +} + +impl Tab { + pub fn new(id: u64, pane: PaneId) -> Self { + Self { + id, + root: Some(Layout::Pane(pane)), + floats: vec![], + active: pane, + floats_visible: false, + zoomed: false, + } + } + pub fn ids(&self) -> Vec { + let mut ids = vec![]; + if let Some(root) = &self.root { + root.ids(&mut ids); + } + ids.extend_from_slice(&self.floats); + ids + } + pub fn detach(&mut self, id: PaneId) { + self.root = self.root.take().and_then(|r| r.remove(id)); + self.floats.retain(|p| *p != id); + self.zoomed = false; + } + pub fn is_visible(&self, id: PaneId) -> bool { + self.floats_visible || !self.floats.contains(&id) + } + + pub fn toggle_floats(&mut self) { + self.floats_visible = !self.floats_visible; + self.zoomed = false; + if self.floats_visible { + if let Some(id) = self.floats.last() { + self.active = *id; + } + } else if self.floats.contains(&self.active) + && let Some(root) = &self.root + { + self.active = root.first(); + } + } + + pub fn focus_direction(&mut self, direction: Direction, rects: &[(PaneId, Rect)]) { + let Some((_, source)) = rects + .iter() + .find(|(id, _)| *id == self.active && self.is_visible(*id)) + else { + return; + }; + if !source.is_positive() || !source.is_finite() { + return; + } + let horizontal = matches!(direction, Direction::Left | Direction::Right); + let sign = if matches!(direction, Direction::Left | Direction::Up) { + -1.0 + } else { + 1.0 + }; + let ids = self.ids(); + let next = rects + .iter() + .filter_map(|(id, rect)| { + if *id == self.active + || !ids.contains(id) + || !self.is_visible(*id) + || !rect.is_positive() + || !rect.is_finite() + { + return None; + } + let delta = rect.center() - source.center(); + let forward = sign * if horizontal { delta.x } else { delta.y }; + if forward <= 0.5 { + return None; + } + let overlap = if horizontal { + source.bottom().min(rect.bottom()) - source.top().max(rect.top()) + } else { + source.right().min(rect.right()) - source.left().max(rect.left()) + }; + // Prefer aligned neighbors, then the nearest center, without wrapping. + Some((*id, overlap <= 0.0, delta.length_sq())) + }) + .min_by(|a, b| { + a.1.cmp(&b.1) + .then_with(|| a.2.total_cmp(&b.2)) + .then_with(|| a.0.cmp(&b.0)) + }); + if let Some((id, _, _)) = next { + self.active = id; + } + } + pub fn cycle(&mut self, backwards: bool) { + let ids: Vec<_> = self + .ids() + .into_iter() + .filter(|id| self.floats_visible || !self.floats.contains(id)) + .collect(); + if ids.is_empty() { + return; + } + let pos = ids.iter().position(|p| *p == self.active).unwrap_or(0); + self.active = ids[(pos + if backwards { ids.len() - 1 } else { 1 }) % ids.len()]; + } +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn removal_collapses_splits_without_losing_siblings() { + let mut root = Layout::Pane(1); + assert!(root.split(1, 2, Axis::Horizontal)); + assert!(root.split(2, 3, Axis::Vertical)); + assert!(!root.split(99, 4, Axis::Vertical)); + let root = root.remove(2).unwrap(); + let mut ids = vec![]; + root.ids(&mut ids); + assert_eq!(ids, vec![1, 3]); + let root = root.remove(1).unwrap(); + assert_eq!(root.first(), 3); + assert!(root.remove(3).is_none()); + } + #[test] + fn focus_skips_hidden_floats() { + let mut tab = Tab::new(1, 1); + tab.floats.push(2); + tab.floats_visible = true; + tab.cycle(false); + assert_eq!(tab.active, 2); + tab.floats_visible = false; + tab.cycle(false); + assert_eq!(tab.active, 1); + } + + #[test] + fn floating_visibility_controls_focus_even_without_tiles() { + let mut tab = Tab::new(1, 1); + assert!(!tab.floats_visible); + tab.toggle_floats(); + assert!(tab.floats_visible); + tab.floats.push(2); + tab.active = 2; + tab.toggle_floats(); + assert_eq!(tab.active, 1); + assert!(!tab.is_visible(2)); + tab.toggle_floats(); + assert_eq!(tab.active, 2); + tab.root = None; + tab.toggle_floats(); + assert!(!tab.is_visible(2)); + tab.toggle_floats(); + assert!(tab.is_visible(2)); + } + + #[test] + fn directional_focus_uses_geometry_and_ignores_hidden_or_foreign_panes() { + let mut tab = Tab::new(1, 1); + let root = tab.root.as_mut().unwrap(); + root.split(1, 2, Axis::Horizontal); + root.split(1, 3, Axis::Vertical); + root.split(2, 4, Axis::Vertical); + tab.floats.push(5); + let rect = |x, y| Rect::from_min_size(egui::pos2(x, y), egui::vec2(100.0, 100.0)); + let panes = [ + (1, rect(0.0, 0.0)), + (2, rect(100.0, 0.0)), + (3, rect(0.0, 100.0)), + (4, rect(100.0, 100.0)), + (5, rect(20.0, 0.0)), + (99, rect(10.0, 0.0)), + ]; + tab.focus_direction(Direction::Right, &panes); + assert_eq!(tab.active, 2); + tab.focus_direction(Direction::Down, &panes); + assert_eq!(tab.active, 4); + tab.focus_direction(Direction::Left, &panes); + assert_eq!(tab.active, 3); + tab.focus_direction(Direction::Up, &panes); + assert_eq!(tab.active, 1); + tab.focus_direction(Direction::Left, &panes); + assert_eq!(tab.active, 1); + tab.floats_visible = true; + tab.focus_direction(Direction::Right, &panes); + assert_eq!(tab.active, 5); + } + + #[test] + fn split_rectangles_touch_without_gaps() { + let ctx = egui::Context::default(); + let mut layout = Layout::Pane(1); + layout.split(1, 2, Axis::Horizontal); + layout.split(2, 3, Axis::Vertical); + let mut rects = vec![]; + let workspace = Rect::from_min_size(egui::pos2(0.0, 0.0), egui::vec2(800.0, 600.0)); + let _ = ctx.run(egui::RawInput::default(), |ctx| { + egui::CentralPanel::default() + .show(ctx, |ui| layout.arrange(ui, workspace, &mut rects, 1)); + }); + assert_eq!(rects.len(), 3); + assert_eq!(rects[0].1.right(), rects[1].1.left()); + assert_eq!(rects[1].1.bottom(), rects[2].1.top()); + assert_eq!( + rects.iter().map(|(_, rect)| rect.area()).sum::(), + workspace.area() + ); + } +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..8d091ac --- /dev/null +++ b/src/main.rs @@ -0,0 +1,85 @@ +mod app; +mod config; +mod input; +mod layout; +mod pane; +mod pty; +mod render; +#[cfg(test)] +mod tests; + +use anyhow::{Context, Result, bail}; + +fn main() -> Result<()> { + let mut path = config::Config::path()?; + let mut check = false; + let mut init = false; + let mut args = std::env::args().skip(1); + while let Some(arg) = args.next() { + match arg.as_str() { + "--config" => path = args.next().context("--config needs a path")?.into(), + "--check-config" => check = true, + "--init-config" => init = true, + "--version" => { + println!("rust-term {}", env!("CARGO_PKG_VERSION")); + return Ok(()); + } + "--help" | "-h" => { + println!( + "rust-term [--config PATH] [--check-config | --init-config]\n\nDefault config: {}", + path.display() + ); + return Ok(()); + } + _ => bail!("Unknown argument: {arg}"), + } + } + if init { + use std::io::Write; + if let Some(parent) = path.parent().filter(|p| !p.as_os_str().is_empty()) { + std::fs::create_dir_all(parent)?; + } + let mut file = std::fs::OpenOptions::new() + .write(true) + .create_new(true) + .open(&path) + .with_context(|| { + format!( + "Cannot create {} (existing files are never overwritten)", + path.display() + ) + })?; + file.write_all(config::EXAMPLE.as_bytes())?; + println!("Created {}", path.display()); + return Ok(()); + } + let config = config::Config::load(&path)?; + if check { + println!( + "Configuration valid: {}{}", + path.display(), + if path.exists() { + "" + } else { + " (built-in defaults)" + } + ); + return Ok(()); + } + libghostty_vt::kitty::graphics::set_png_decoder(Some(Box::new(render::PngDecoder)))?; + let options = eframe::NativeOptions { + viewport: egui::ViewportBuilder::default() + .with_title("rust-term") + .with_inner_size([config.window_width, config.window_height]) + .with_min_inner_size([320.0, 200.0]) + .with_app_id("rust-term"), + renderer: eframe::Renderer::Wgpu, + ..Default::default() + }; + eframe::run_native( + "rust-term", + options, + Box::new(move |cc| Ok(Box::new(app::App::new(cc, config, path)?))), + ) + .map_err(|e| anyhow::anyhow!("Cannot start rust-term: {e}")) +} diff --git a/src/pane.rs b/src/pane.rs new file mode 100644 index 0000000..0acfa4f --- /dev/null +++ b/src/pane.rs @@ -0,0 +1,364 @@ +use crate::{ + config::{Config, rgb}, + input, + pty::Pty, + render::Renderer, +}; +use anyhow::{Result, ensure}; +use egui::{FontId, Rect, Sense, vec2}; +use libghostty_vt::{ + Terminal, TerminalOptions, mouse, + selection::{FormatOptions, Selection}, + terminal::{self, Mode, Point, PointCoordinate, ScrollViewport}, +}; +use std::{cell::RefCell, rc::Rc, sync::mpsc::TryRecvError}; + +pub struct Pane { + pub terminal: Terminal<'static, 'static>, + pub renderer: Renderer, + pub pty: Pty, + pub dead: bool, + pub error: Option, + pub last_rect: Rect, + size: (u16, u16, u32, u32), + report_size: Rc>, + callback_error: Rc>>, + mouse_encoder: mouse::Encoder<'static>, + mouse_event: mouse::Event<'static>, + selection_start: Option, + focused: bool, +} + +pub fn terminal(config: &Config) -> Result> { + let mut term = Terminal::new(TerminalOptions { + cols: 80, + rows: 24, + max_scrollback: config.scrollback, + })?; + term.resize(80, 24, 8, 16)?; + apply_config(&mut term, config)?; + term.on_xtversion(|_| Some("rust-term 0.1.0"))?; + term.on_device_attributes(|_| { + Some(terminal::DeviceAttributes { + primary: terminal::PrimaryDeviceAttributes::new( + terminal::ConformanceLevel::VT220, + &[terminal::DeviceAttributeFeature::ANSI_COLOR], + ), + secondary: terminal::SecondaryDeviceAttributes { + device_type: terminal::DeviceType::VT220, + firmware_version: 1, + rom_cartridge: 0, + }, + tertiary: Default::default(), + }) + })?; + term.on_color_scheme(|_| Some(terminal::ColorScheme::Dark))?; + Ok(term) +} + +pub fn apply_config(term: &mut Terminal<'_, '_>, config: &Config) -> Result<()> { + term.set_default_fg_color(Some(rgb(&config.foreground)?))? + .set_default_bg_color(Some(rgb(&config.background)?))? + .set_default_cursor_color(Some(rgb(&config.accent)?))? + .set_kitty_image_storage_limit(config.kitty.image_storage_mb * 1024 * 1024)? + .set_kitty_image_from_file_allowed(config.kitty.allow_file)? + .set_kitty_image_from_temp_file_allowed(config.kitty.allow_temp_file)? + .set_kitty_image_from_shared_mem_allowed(config.kitty.allow_shared_memory)?; + Ok(()) +} + +impl Pane { + pub fn new(config: &Config, ctx: &egui::Context) -> Result { + let mut terminal = terminal(config)?; + let renderer = Renderer::new()?; + let mouse_encoder = mouse::Encoder::new()?; + let mouse_event = mouse::Event::new()?; + let pty = Pty::spawn(config, ctx)?; + let input = pty.input.clone(); + let callback_error = Rc::new(RefCell::new(None)); + let errors = callback_error.clone(); + terminal.on_pty_write(move |_, data| { + if let Err(e) = input.try_send(data.to_vec()) { + *errors.borrow_mut() = Some(format!("PTY response queue: {e}")); + } + })?; + let report_size = Rc::new(RefCell::new(terminal::SizeReportSize { + rows: 24, + columns: 80, + cell_width: 8, + cell_height: 16, + })); + let size = report_size.clone(); + terminal.on_size(move |_| Some(*size.borrow()))?; + Ok(Self { + terminal, + renderer, + pty, + dead: false, + error: None, + last_rect: Rect::NOTHING, + size: (80, 24, 8, 16), + report_size, + callback_error, + mouse_encoder, + mouse_event, + selection_start: None, + focused: false, + }) + } + + pub fn drain(&mut self, ctx: &egui::Context) { + // Bound work per frame as well as queued bytes, so one busy PTY cannot starve others. + for _ in 0..64 { + match self.pty.output.try_recv() { + Ok(data) => self.terminal.vt_write(&data), + Err(TryRecvError::Empty | TryRecvError::Disconnected) => break, + } + } + if self.pty.exited.try_recv().is_ok() { + self.dead = true; + } + if let Ok(e) = self.pty.errors.try_recv() { + self.error = Some(e); + } + if let Some(e) = self.callback_error.borrow_mut().take() { + self.error = Some(e); + } + if !self.dead { + ctx.request_repaint_after(std::time::Duration::from_millis(100)); + } + } + + pub fn send(&mut self, bytes: Vec) -> Result<()> { + if bytes.is_empty() { + return Ok(()); + } + ensure!(!self.dead, "shell has exited"); + self.pty + .input + .try_send(bytes) + .map_err(|e| anyhow::anyhow!("PTY input queue: {e}"))?; + self.terminal.scroll_viewport(ScrollViewport::Bottom); + Ok(()) + } + + pub fn paste(&mut self, text: &str) -> Result<()> { + ensure!(text.len() <= 4 * 1024 * 1024, "paste exceeds 4 MiB"); + // ESC can terminate bracketed paste early. Strip it and NUL from clipboard input. + let text = text + .replace(['\x1b', '\0'], "") + .replace("\r\n", "\n") + .replace('\n', "\r"); + let data = if self.terminal.mode(Mode::BRACKETED_PASTE)? { + format!("\x1b[200~{text}\x1b[201~").into_bytes() + } else { + text.into_bytes() + }; + self.send(data) + } + + pub fn copy(&self) -> Result> { + Ok(self + .terminal + .format_selection_alloc(None, FormatOptions::new().with_trim(true).with_unwrap(true))? + .map(|b| String::from_utf8_lossy(&b).into_owned())) + } + + pub fn focus(&mut self, focused: bool) -> Result<()> { + if focused != self.focused { + self.focused = focused; + if self + .terminal + .mode(Mode::new(1004, terminal::ModeKind::Dec))? + { + self.send(if focused { b"\x1b[I" } else { b"\x1b[O" }.to_vec())?; + } + } + Ok(()) + } + + pub fn show( + &mut self, + ui: &mut egui::Ui, + rect: Rect, + config: &Config, + active: bool, + ) -> Result { + let font = FontId::monospace(config.font_size); + let cw = ui + .painter() + .layout_no_wrap("M".into(), font.clone(), egui::Color32::WHITE) + .size() + .x + .ceil() + .max(1.0); + let ch = (config.font_size * 1.3).ceil(); + let cell = vec2(cw, ch); + let grid = rect.shrink(6.0); + if grid.width() < cw || grid.height() < ch { + return Ok(false); + } + let size = ( + (grid.width() / cw).floor().clamp(1.0, 1000.0) as u16, + (grid.height() / ch).floor().clamp(1.0, 1000.0) as u16, + cw as u32, + ch as u32, + ); + if size != self.size { + self.pty.resize(size.0, size.1, size.2, size.3)?; + self.terminal.resize(size.0, size.1, size.2, size.3)?; + self.size = size; + *self.report_size.borrow_mut() = terminal::SizeReportSize { + columns: size.0, + rows: size.1, + cell_width: size.2, + cell_height: size.3, + }; + } + let response = ui.interact(grid, ui.id().with("terminal"), Sense::click_and_drag()); + self.pointer(ui, &response, grid, cell)?; + self.renderer + .paint(&self.terminal, ui.painter(), grid, cell, &font, active)?; + if let Some(error) = &self.error { + ui.painter().text( + grid.left_bottom() - vec2(0.0, ch), + egui::Align2::LEFT_TOP, + error, + FontId::proportional(12.0), + egui::Color32::LIGHT_RED, + ); + } + Ok(response.clicked() + || response.drag_started() + || (response.hovered() && ui.input(|i| i.pointer.any_pressed()))) + } + + fn pointer( + &mut self, + ui: &egui::Ui, + response: &egui::Response, + grid: Rect, + cell: egui::Vec2, + ) -> Result<()> { + let modifiers = ui.input(|i| i.modifiers); + let tracking = self.terminal.is_mouse_tracking()? && !modifiers.shift; + let pos = response + .interact_pointer_pos() + .or_else(|| ui.input(|i| i.pointer.hover_pos())); + let Some(pos) = pos else { + return Ok(()); + }; + let local = pos - grid.min; + if tracking && response.hovered() { + let events = ui.input(|i| i.events.clone()); + self.mouse_encoder + .set_options_from_terminal(&self.terminal) + .set_size(mouse::EncoderSize { + screen_width: self.size.0 as u32 * self.size.2, + screen_height: self.size.1 as u32 * self.size.3, + cell_width: self.size.2, + cell_height: self.size.3, + padding_top: 0, + padding_bottom: 0, + padding_left: 0, + padding_right: 0, + }) + .set_any_button_pressed(ui.input(|i| i.pointer.any_down())) + .set_track_last_cell(true); + self.mouse_event + .set_position(mouse::Position { + x: local.x, + y: local.y, + }) + .set_mods(input::mods(modifiers)); + let mut data = vec![]; + for event in events { + match event { + egui::Event::PointerButton { + button, pressed, .. + } => { + let button = match button { + egui::PointerButton::Primary => mouse::Button::Left, + egui::PointerButton::Secondary => mouse::Button::Right, + egui::PointerButton::Middle => mouse::Button::Middle, + _ => continue, + }; + self.mouse_event + .set_action(if pressed { + mouse::Action::Press + } else { + mouse::Action::Release + }) + .set_button(Some(button)); + self.mouse_encoder + .encode_to_vec(&self.mouse_event, &mut data)?; + } + egui::Event::PointerMoved(_) => { + let button = ui.input(|i| { + if i.pointer.primary_down() { + Some(mouse::Button::Left) + } else if i.pointer.secondary_down() { + Some(mouse::Button::Right) + } else { + None + } + }); + self.mouse_event + .set_action(mouse::Action::Motion) + .set_button(button); + self.mouse_encoder + .encode_to_vec(&self.mouse_event, &mut data)?; + } + egui::Event::MouseWheel { delta, .. } => { + self.mouse_event + .set_action(mouse::Action::Press) + .set_button(Some(if delta.y > 0.0 { + mouse::Button::Four + } else { + mouse::Button::Five + })); + for _ in 0..3 { + self.mouse_encoder + .encode_to_vec(&self.mouse_event, &mut data)?; + } + } + _ => {} + } + } + self.send(data)?; + } else if !tracking { + let point = PointCoordinate { + x: (local.x / cell.x) + .floor() + .clamp(0.0, (self.size.0 - 1) as f32) as u16, + y: (local.y / cell.y) + .floor() + .clamp(0.0, (self.size.1 - 1) as f32) as u32, + }; + if response.drag_started() { + self.selection_start = Some(point); + } + if response.clicked() { + self.terminal.set_selection(None)?; + self.selection_start = None; + } + if response.dragged() + && let Some(start) = self.selection_start + { + let a = self.terminal.grid_ref(Point::Viewport(start))?; + let b = self.terminal.grid_ref(Point::Viewport(point))?; + self.terminal + .set_selection(Some(&Selection::new(a, b, modifiers.alt)))?; + } + if response.hovered() { + let scroll = ui.input(|i| i.raw_scroll_delta.y); + if scroll.abs() >= 1.0 { + self.terminal.scroll_viewport(ScrollViewport::Delta( + -(scroll / cell.y).round() as isize, + )); + } + } + } + Ok(()) + } +} diff --git a/src/pty.rs b/src/pty.rs new file mode 100644 index 0000000..4d35f66 --- /dev/null +++ b/src/pty.rs @@ -0,0 +1,131 @@ +use crate::config::Config; +use anyhow::{Context, Result}; +use portable_pty::{CommandBuilder, MasterPty, PtySize, native_pty_system}; +use std::{ + io::{Read, Write}, + sync::mpsc::{self, Receiver, Sender, SyncSender}, + thread, +}; + +pub struct Pty { + pub output: Receiver>, + pub input: SyncSender>, + pub errors: Receiver, + pub exited: Receiver<()>, + master: Box, + shutdown: Sender<()>, + reaper: Option>, +} + +impl Pty { + pub fn spawn(config: &Config, ctx: &egui::Context) -> Result { + let pair = native_pty_system().openpty(PtySize { + rows: 24, + cols: 80, + pixel_width: 640, + pixel_height: 384, + })?; + let mut cmd = CommandBuilder::new(&config.shell[0]); + cmd.args(&config.shell[1..]); + cmd.env("TERM", "xterm-256color"); + cmd.env("COLORTERM", "truecolor"); + cmd.env("TERM_PROGRAM", "rust-term"); + cmd.env("TERM_PROGRAM_VERSION", env!("CARGO_PKG_VERSION")); + if let Some(dir) = &config.working_directory { + cmd.cwd(dir); + } + let mut reader = pair.master.try_clone_reader()?; + let mut writer = pair.master.take_writer()?; + let mut child = pair + .slave + .spawn_command(cmd) + .context("Cannot spawn shell")?; + drop(pair.slave); + let (output_tx, output) = mpsc::sync_channel(64); + let (input, input_rx) = mpsc::sync_channel::>(64); + let (error_tx, errors) = mpsc::channel(); + let (exit_tx, exited) = mpsc::channel(); + let (shutdown, shutdown_rx) = mpsc::channel(); + let read_ctx = ctx.clone(); + let write_ctx = ctx.clone(); + let exit_ctx = ctx.clone(); + let read_errors = error_tx.clone(); + thread::spawn(move || { + let mut buf = [0; 16384]; + loop { + match reader.read(&mut buf) { + Ok(0) => break, + Ok(n) => { + if output_tx.send(buf[..n].to_vec()).is_err() { + break; + } + read_ctx.request_repaint(); + } + Err(e) if e.kind() == std::io::ErrorKind::Interrupted => continue, + // Linux PTYs report EIO when the slave closes. + Err(e) if e.raw_os_error() == Some(5) => break, + Err(e) => { + let _ = read_errors.send(format!("PTY read: {e}")); + read_ctx.request_repaint(); + break; + } + } + } + }); + thread::spawn(move || { + while let Ok(bytes) = input_rx.recv() { + if let Err(e) = writer.write_all(&bytes).and_then(|_| writer.flush()) { + let _ = error_tx.send(format!("PTY write: {e}")); + write_ctx.request_repaint(); + break; + } + } + }); + let reaper = thread::spawn(move || { + loop { + if matches!(child.try_wait(), Ok(Some(_))) { + break; + } + if !matches!( + shutdown_rx.recv_timeout(std::time::Duration::from_millis(25)), + Err(mpsc::RecvTimeoutError::Timeout) + ) { + // The worker owns the unreaped child, so shutdown cannot signal a reused PID. + let _ = child.kill(); + let _ = child.wait(); + break; + } + } + let _ = exit_tx.send(()); + exit_ctx.request_repaint(); + }); + Ok(Self { + output, + input, + errors, + exited, + master: pair.master, + shutdown, + reaper: Some(reaper), + }) + } + + pub fn resize(&self, cols: u16, rows: u16, cw: u32, ch: u32) -> Result<()> { + self.master.resize(PtySize { + cols, + rows, + pixel_width: (cols as u32 * cw).min(u16::MAX as u32) as u16, + pixel_height: (rows as u32 * ch).min(u16::MAX as u32) as u16, + })?; + Ok(()) + } +} + +impl Drop for Pty { + fn drop(&mut self) { + let _ = self.shutdown.send(()); + if let Some(reaper) = self.reaper.take() { + let _ = reaper.join(); + } + } +} diff --git a/src/render.rs b/src/render.rs new file mode 100644 index 0000000..8aab511 --- /dev/null +++ b/src/render.rs @@ -0,0 +1,303 @@ +use anyhow::{Result, ensure}; +use egui::{ + Color32, ColorImage, FontId, Painter, Rect, Stroke, TextureHandle, TextureOptions, pos2, vec2, +}; +use libghostty_vt::{ + RenderState, Terminal, + alloc::{Allocator, Bytes}, + kitty::graphics::{DecodePng, DecodedImage, ImageFormat, Layer, PlacementIterator}, + render::{CellIterator, CursorVisualStyle, Dirty, RowIterator}, + style::{RgbColor, Underline}, +}; +use std::{ + collections::{HashMap, HashSet}, + io::Cursor, +}; + +pub fn color(c: RgbColor) -> Color32 { + Color32::from_rgb(c.r, c.g, c.b) +} + +pub struct PngDecoder; +impl DecodePng for PngDecoder { + fn decode_png<'a>( + &mut self, + alloc: &'a Allocator<'_>, + bytes: &[u8], + ) -> Option> { + let mut decoder = png::Decoder::new(Cursor::new(bytes)); + decoder.set_transformations(png::Transformations::EXPAND | png::Transformations::STRIP_16); + decoder.set_limits(png::Limits { + bytes: 128 * 1024 * 1024, + }); + let mut reader = decoder.read_info().ok()?; + let size = reader.output_buffer_size()?; + if size > 128 * 1024 * 1024 { + return None; + } + let mut pixels = vec![0; size]; + let info = reader.next_frame(&mut pixels).ok()?; + let count = (info.width as usize).checked_mul(info.height as usize)?; + let len = count.checked_mul(4)?; + if len > 128 * 1024 * 1024 { + return None; + } + let mut data = Bytes::new_with_alloc(alloc, len).ok()?; + let input = &pixels[..info.buffer_size()]; + match info.color_type { + png::ColorType::Rgba => data.copy_from_slice(input), + png::ColorType::Rgb => { + for (out, src) in data.chunks_exact_mut(4).zip(input.chunks_exact(3)) { + out.copy_from_slice(&[src[0], src[1], src[2], 255]); + } + } + png::ColorType::Grayscale => { + for (out, value) in data.chunks_exact_mut(4).zip(input) { + out.copy_from_slice(&[*value, *value, *value, 255]); + } + } + png::ColorType::GrayscaleAlpha => { + for (out, src) in data.chunks_exact_mut(4).zip(input.chunks_exact(2)) { + out.copy_from_slice(&[src[0], src[0], src[0], src[1]]); + } + } + _ => return None, + } + Some(DecodedImage { + width: info.width, + height: info.height, + data, + }) + } +} + +pub struct Renderer { + state: RenderState<'static>, + rows: RowIterator<'static>, + cells: CellIterator<'static>, + placements: PlacementIterator<'static>, + textures: HashMap, +} + +impl Renderer { + pub fn new() -> Result { + Ok(Self { + state: RenderState::new()?, + rows: RowIterator::new()?, + cells: CellIterator::new()?, + placements: PlacementIterator::new()?, + textures: HashMap::new(), + }) + } + + pub fn paint( + &mut self, + term: &Terminal<'static, '_>, + painter: &Painter, + rect: Rect, + cell: egui::Vec2, + font: &FontId, + active: bool, + ) -> Result<()> { + let painter = painter.with_clip_rect(rect.intersect(painter.clip_rect())); + let snapshot = self.state.update(term)?; + let colors = snapshot.colors()?; + painter.rect_filled(rect, 0, color(colors.background)); + let mut backgrounds = vec![]; + let mut foregrounds = vec![]; + let mut rows = self.rows.update(&snapshot)?; + let mut text = String::new(); + let mut y = 0; + while let Some(row) = rows.next() { + let mut cells = self.cells.update(row)?; + let mut x = 0; + while let Some(c) = cells.next() { + let pos = rect.min + vec2(x as f32 * cell.x, y as f32 * cell.y); + let cell_rect = Rect::from_min_size(pos, cell); + let style = c.style()?; + let mut fg = color(c.fg_color()?.unwrap_or(colors.foreground)); + let explicit_bg = c.bg_color()?; + let mut bg = color(explicit_bg.unwrap_or(colors.background)); + let selected = c.is_selected()?; + if style.inverse ^ selected { + std::mem::swap(&mut fg, &mut bg); + } + if explicit_bg.is_some() || style.inverse || selected { + backgrounds.push(egui::Shape::rect_filled(cell_rect, 0, bg)); + } + if style.faint { + fg = fg.gamma_multiply(0.6); + } + if !style.invisible && c.graphemes_len()? > 0 { + c.graphemes_utf8(&mut text)?; + // Placeholder cells are reserved by the Kitty protocol, not printable text. + if !text.starts_with('\u{10eeee}') { + let galley = painter.layout_no_wrap(text.clone(), font.clone(), fg); + foregrounds.push(egui::Shape::galley(pos, galley.clone(), fg)); + if style.bold { + foregrounds.push(egui::Shape::galley( + pos + vec2(0.45, 0.0), + galley, + fg, + )); + } + } + } + for (enabled, height) in [ + (style.underline != Underline::None, cell.y - 2.0), + (style.strikethrough, cell.y * 0.55), + (style.overline, 1.0), + ] { + if enabled { + foregrounds.push(egui::Shape::line_segment( + [pos + vec2(0.0, height), pos + vec2(cell.x, height)], + Stroke::new(1.0, fg), + )); + } + } + x += 1; + } + row.set_dirty(false)?; + y += 1; + } + // Separate passes preserve Kitty's three z-order bands exactly. + let mut seen = HashSet::new(); + for layer in [Layer::BelowBg, Layer::BelowText, Layer::AboveText] { + if layer == Layer::BelowText { + painter.extend(std::mem::take(&mut backgrounds)); + } + if layer == Layer::AboveText { + painter.extend(std::mem::take(&mut foregrounds)); + } + paint_images( + term, + &painter, + rect, + cell, + layer, + &mut self.placements, + &mut self.textures, + &mut seen, + )?; + } + self.textures.retain(|id, _| seen.contains(id)); + if snapshot.cursor_visible()? + && let Some(cursor) = snapshot.cursor_viewport()? + { + let mut cursor_rect = Rect::from_min_size( + rect.min + vec2(cursor.x as f32 * cell.x, cursor.y as f32 * cell.y), + cell, + ); + let c = color(colors.cursor.unwrap_or(colors.foreground)); + match snapshot.cursor_visual_style()? { + CursorVisualStyle::Bar if active => { + cursor_rect.max.x = cursor_rect.min.x + 2.0; + painter.rect_filled(cursor_rect, 0, c); + } + CursorVisualStyle::Underline if active => { + cursor_rect.min.y = cursor_rect.max.y - 2.0; + painter.rect_filled(cursor_rect, 0, c); + } + _ => { + painter.rect_stroke( + cursor_rect, + 0, + Stroke::new(if active { 1.5 } else { 0.75 }, c), + egui::StrokeKind::Inside, + ); + } + } + } + snapshot.set_dirty(Dirty::Clean)?; + Ok(()) + } +} + +#[allow(clippy::too_many_arguments)] +fn paint_images( + term: &Terminal<'_, '_>, + painter: &Painter, + rect: Rect, + cell: egui::Vec2, + layer: Layer, + iterator: &mut PlacementIterator<'_>, + textures: &mut HashMap, + seen: &mut HashSet, +) -> Result<()> { + let graphics = term.kitty_graphics()?; + let mut placements = iterator.update(&graphics)?; + placements.set_layer(layer)?; + let mut images = vec![]; + while let Some(p) = placements.next() { + let id = p.image_id()?; + let Some(image) = graphics.image(id) else { + continue; + }; + let info = p.placement_render_info(&image, term)?; + if !info.viewport_visible || info.pixel_width == 0 || info.pixel_height == 0 { + continue; + } + let size = [image.width()? as usize, image.height()? as usize]; + if size.contains(&0) { + continue; + } + ensure!( + size[0] <= painter.ctx().input(|i| i.max_texture_side) + && size[1] <= painter.ctx().input(|i| i.max_texture_side), + "Kitty image exceeds GPU texture size limit" + ); + seen.insert(id); + let generation = image.generation()?; + if textures.get(&id).is_none_or(|(g, _)| *g != generation) { + let data = image.data()?; + let pixels = match image.format()? { + ImageFormat::Rgba => ColorImage::from_rgba_unmultiplied(size, data), + ImageFormat::Rgb => ColorImage::from_rgb(size, data), + _ => continue, + }; + textures.insert( + id, + ( + generation, + painter.ctx().load_texture( + format!("kitty-{id}"), + pixels, + TextureOptions::LINEAR, + ), + ), + ); + } + let scale = vec2(1.0, 1.0); + let dest = Rect::from_min_size( + rect.min + + vec2( + info.viewport_col as f32 * cell.x, + info.viewport_row as f32 * cell.y, + ) + + vec2( + p.x_offset()? as f32 * scale.x, + p.y_offset()? as f32 * scale.y, + ), + vec2( + info.pixel_width as f32 * scale.x, + info.pixel_height as f32 * scale.y, + ), + ); + let uv = Rect::from_min_max( + pos2( + info.source_x as f32 / size[0] as f32, + info.source_y as f32 / size[1] as f32, + ), + pos2( + (info.source_x + info.source_width) as f32 / size[0] as f32, + (info.source_y + info.source_height) as f32 / size[1] as f32, + ), + ); + images.push((p.z()?, id, p.placement_id()?, dest, uv)); + } + images.sort_by_key(|(z, id, pid, _, _)| (*z, *id, *pid)); + for (_, id, _, dest, uv) in images { + painter.image(textures[&id].1.id(), dest, uv, Color32::WHITE); + } + Ok(()) +} diff --git a/src/tests.rs b/src/tests.rs new file mode 100644 index 0000000..dfb354d --- /dev/null +++ b/src/tests.rs @@ -0,0 +1,207 @@ +use crate::{config::Config, input, pane, render}; +use base64::Engine; +use egui::{Event, Key, Modifiers}; +use libghostty_vt::{ + fmt, + kitty::graphics::{self, ImageFormat, PlacementIterator}, +}; +use std::{ + cell::RefCell, + rc::Rc, + time::{Duration, Instant}, +}; + +fn plain(term: &libghostty_vt::Terminal<'_, '_>) -> String { + let mut formatter = + fmt::Formatter::new(term, fmt::FormatterOptions::new().with_trim(true)).unwrap(); + let bytes = formatter.format_alloc(None).unwrap(); + String::from_utf8_lossy(&bytes).into_owned() +} + +fn key(key: Key, modifiers: Modifiers) -> Event { + Event::Key { + key, + physical_key: Some(key), + pressed: true, + repeat: false, + modifiers, + } +} + +#[test] +fn terminal_preserves_unicode_colors_and_application_input_modes() { + let mut term = pane::terminal(&Config::default()).unwrap(); + term.vt_write("hello \x1b[31mred\x1b[0m \u{754c}e\u{301}\r\n".as_bytes()); + assert!(plain(&term).contains("hello red \u{754c}e\u{301}")); + let events = [ + key(Key::A, Modifiers::NONE), + Event::Text("a".into()), + key(Key::B, Modifiers::NONE), + Event::Text("b".into()), + ]; + assert_eq!(input::encode(&term, &events).unwrap(), b"ab"); + assert_eq!( + input::encode(&term, &[key(Key::C, Modifiers::CTRL)]).unwrap(), + b"\x03" + ); + assert_eq!( + input::encode(&term, &[key(Key::ArrowUp, Modifiers::NONE)]).unwrap(), + b"\x1b[A" + ); + term.vt_write(b"\x1b[?1h"); + assert_eq!( + input::encode(&term, &[key(Key::ArrowUp, Modifiers::NONE)]).unwrap(), + b"\x1bOA" + ); + term.resize(12, 6, 9, 20).unwrap(); + assert!(plain(&term).contains("hello red")); +} + +#[test] +fn queries_return_to_the_pty_and_alternate_screen_restores() { + let mut term = pane::terminal(&Config::default()).unwrap(); + let response = Rc::new(RefCell::new(vec![])); + let writer = response.clone(); + term.on_pty_write(move |_, bytes| writer.borrow_mut().extend_from_slice(bytes)) + .unwrap(); + term.vt_write(b"hello\x1b[6n"); + assert_eq!(*response.borrow(), b"\x1b[1;6R"); + term.vt_write(b"\x1b[?1049hother screen\x1b[?1049l"); + assert!(plain(&term).contains("hello")); + assert!(!plain(&term).contains("other screen")); +} + +#[test] +fn kitty_images_decode_render_reuse_and_delete() { + graphics::set_png_decoder(Some(Box::new(render::PngDecoder))).unwrap(); + let mut term = pane::terminal(&Config::default()).unwrap(); + let replies = Rc::new(RefCell::new(vec![])); + let output = replies.clone(); + term.on_pty_write(move |_, data| output.borrow_mut().extend_from_slice(data)) + .unwrap(); + // Split delivery across VT writes, including the control string terminator. + term.vt_write(b"\x1b_Ga=T,f=24,s=2,v=1,i=7,c=4,r=2;//8A"); + term.vt_write(b"AP8A\x1b\\"); + assert!(String::from_utf8_lossy(&replies.borrow()).contains("OK")); + { + let graphics = term.kitty_graphics().unwrap(); + let image = graphics.image(7).expect("RGB image stored"); + assert_eq!(image.format().unwrap(), ImageFormat::Rgb); + assert_eq!(image.data().unwrap(), [255, 255, 0, 0, 255, 0]); + let mut it = PlacementIterator::new().unwrap(); + let mut ps = it.update(&graphics).unwrap(); + let info = ps + .next() + .unwrap() + .placement_render_info(&image, &term) + .unwrap(); + assert!(info.viewport_visible); + assert_eq!((info.pixel_width, info.pixel_height), (32, 32)); + } + let mut png = vec![]; + { + let mut encoder = png::Encoder::new(&mut png, 2, 1); + encoder.set_color(png::ColorType::Rgb); + encoder.set_depth(png::BitDepth::Eight); + encoder + .write_header() + .unwrap() + .write_image_data(&[255, 0, 0, 0, 255, 0]) + .unwrap(); + } + let payload = base64::engine::general_purpose::STANDARD.encode(png); + let split = payload.len() / 8 * 4; + term.vt_write( + format!( + "\x1b_Ga=T,f=100,i=8,c=2,r=1,m=1;{}\x1b\\", + &payload[..split] + ) + .as_bytes(), + ); + term.vt_write(format!("\x1b_Gm=0;{}\x1b\\", &payload[split..]).as_bytes()); + { + let graphics = term.kitty_graphics().unwrap(); + let image = graphics.image(8).expect("PNG image stored"); + assert_eq!(image.format().unwrap(), ImageFormat::Rgba); + assert_eq!(image.data().unwrap(), [255, 0, 0, 255, 0, 255, 0, 255]); + } + let ctx = egui::Context::default(); + let mut renderer = render::Renderer::new().unwrap(); + let rect = egui::Rect::from_min_size(egui::pos2(50.0, 60.0), egui::vec2(400.0, 240.0)); + let mut paint = || { + ctx.run(egui::RawInput::default(), |ctx| { + let painter = ctx + .layer_painter(egui::LayerId::background()) + .with_clip_rect(rect); + renderer + .paint( + &term, + &painter, + rect, + egui::vec2(8.0, 16.0), + &egui::FontId::monospace(12.0), + true, + ) + .unwrap(); + }) + }; + let first = paint(); + assert_eq!( + first + .textures_delta + .set + .iter() + .filter(|(id, _)| matches!(id,egui::TextureId::Managed(n) if *n>0)) + .count(), + 2 + ); + assert!(first.shapes.iter().all(|s| rect.contains_rect(s.clip_rect))); + let second = paint(); + assert!( + second.textures_delta.set.is_empty(), + "unchanged images must reuse GPU textures" + ); + term.vt_write(b"\x1b_Ga=d,d=I,i=7\x1b\\"); + assert!(term.kitty_graphics().unwrap().image(7).is_none()); + assert!(term.kitty_graphics().unwrap().image(8).is_some()); +} + +#[test] +fn pty_executes_input_and_bracketed_paste_is_sanitized() { + let config = Config { + shell: vec!["/bin/sh".into()], + ..Default::default() + }; + let ctx = egui::Context::default(); + let mut pane = pane::Pane::new(&config, &ctx).unwrap(); + pane.send(b"printf '\\033[31mpty-%s\\033[0m\\n' 'success'\r".to_vec()) + .unwrap(); + let deadline = Instant::now() + Duration::from_secs(5); + while Instant::now() < deadline { + pane.drain(&ctx); + if plain(&pane.terminal).contains("pty-success") { + break; + } + std::thread::sleep(Duration::from_millis(10)); + } + assert!(plain(&pane.terminal).contains("pty-success")); + pane.pty.resize(91, 31, 9, 20).unwrap(); + pane.send(b"stty size\r".to_vec()).unwrap(); + let deadline = Instant::now() + Duration::from_secs(5); + while Instant::now() < deadline { + pane.drain(&ctx); + if plain(&pane.terminal).contains("31 91") { + break; + } + std::thread::sleep(Duration::from_millis(10)); + } + assert!(plain(&pane.terminal).contains("31 91")); + pane.send(b"exit\r".to_vec()).unwrap(); + let deadline = Instant::now() + Duration::from_secs(5); + while !pane.dead && Instant::now() < deadline { + pane.drain(&ctx); + std::thread::sleep(Duration::from_millis(10)); + } + assert!(pane.dead); + assert!(pane.send(b"after exit".to_vec()).is_err()); +}