diff --git a/Cargo.lock b/Cargo.lock index 60aca16..d7f5bc2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8,6 +8,41 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common 0.1.7", + "generic-array", +] + +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "aes-gcm" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" +dependencies = [ + "aead", + "aes", + "cipher", + "ctr", + "ghash", + "subtle", +] + [[package]] name = "ahash" version = "0.8.12" @@ -116,6 +151,23 @@ version = "1.0.101" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f0e0fee31ef5ed1ba1316088939cea399010ed7731dba877ed44aeb407a75ea" +[[package]] +name = "arboard" +version = "3.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0348a1c054491f4bfe6ab86a7b6ab1e44e45d899005de92f58b3df180b36ddaf" +dependencies = [ + "clipboard-win", + "log", + "objc2", + "objc2-app-kit", + "objc2-foundation", + "parking_lot", + "percent-encoding", + "windows-sys 0.60.2", + "x11rb", +] + [[package]] name = "argon2" version = "0.5.3" @@ -152,6 +204,28 @@ dependencies = [ "tokio", ] +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "async_io_stream" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d7b9decdf35d8908a7e3ef02f64c5e9b1695e230154c0e8de3969142d9b94c" +dependencies = [ + "futures", + "pharos", + "rustc_version", +] + [[package]] name = "atk" version = "0.18.2" @@ -193,12 +267,33 @@ dependencies = [ "bytemuck", ] +[[package]] +name = "atomic-polyfill" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cf2bce30dfe09ef0bfaef228b9d414faaf7e563035494d7fe092dba54b300f4" +dependencies = [ + "critical-section", +] + [[package]] name = "atomic-waker" version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" +[[package]] +name = "attohttpc" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16e2cdb6d5ed835199484bb92bb8b3edd526effe995c61732580439c1a67e2e9" +dependencies = [ + "base64 0.22.1", + "http", + "log", + "url", +] + [[package]] name = "autocfg" version = "1.5.0" @@ -295,6 +390,23 @@ dependencies = [ "syn 2.0.114", ] +[[package]] +name = "backon" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cffb0e931875b666fc4fcb20fee52e9bbd1ef836fd9e9e04ec21555f9f85f7ef" +dependencies = [ + "fastrand", + "gloo-timers", + "tokio", +] + +[[package]] +name = "base32" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "022dfe9eb35f19ebbcb51e0b40a5ab759f46ad60cadf7297e0bd085afb50e076" + [[package]] name = "base64" version = "0.21.7" @@ -364,7 +476,7 @@ version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" dependencies = [ - "digest", + "digest 0.10.7", ] [[package]] @@ -390,6 +502,15 @@ dependencies = [ "generic-array", ] +[[package]] +name = "block-buffer" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96eb4cdd6cf1b31d671e9efe75c5d1ec614776856cefbe109ca373554a6d514f" +dependencies = [ + "hybrid-array", +] + [[package]] name = "block2" version = "0.6.2" @@ -623,6 +744,16 @@ dependencies = [ "half", ] +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common 0.1.7", + "inout", +] + [[package]] name = "clap" version = "4.5.57" @@ -663,6 +794,24 @@ version = "0.7.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3e64b0cc0439b12df2fa678eae89a1c56a529fd067a9115f7827f1fffd22b32" +[[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 = "cobs" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa961b519f0b462e3a3b4a34b64d119eeaca1d59af726fe450bbba07a9fc0a1" +dependencies = [ + "thiserror 2.0.18", +] + [[package]] name = "colorchoice" version = "1.0.4" @@ -728,6 +877,12 @@ version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" +[[package]] +name = "const-oid" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" + [[package]] name = "constant_time_eq" version = "0.4.2" @@ -759,6 +914,16 @@ dependencies = [ "version_check", ] +[[package]] +name = "cordyceps" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "688d7fbb8092b8de775ef2536f36c8c31f2bc4006ece2e8d8ad2d17d00ce0a2a" +dependencies = [ + "loom", + "tracing", +] + [[package]] name = "core-foundation" version = "0.10.1" @@ -832,6 +997,12 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "critical-section" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" + [[package]] name = "crossbeam-channel" version = "0.5.15" @@ -918,6 +1089,15 @@ dependencies = [ "typenum", ] +[[package]] +name = "crypto-common" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77727bb15fa921304124b128af125e7e3b968275d1b108b379190264f4423710" +dependencies = [ + "hybrid-array", +] + [[package]] name = "csscolorparser" version = "0.6.2" @@ -978,14 +1158,76 @@ dependencies = [ "syn 2.0.114", ] +[[package]] +name = "ctr" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" +dependencies = [ + "cipher", +] + +[[package]] +name = "curve25519-dalek" +version = "5.0.0-pre.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f9200d1d13637f15a6acb71e758f64624048d85b31a5fdbfd8eca1e2687d0b7" +dependencies = [ + "cfg-if", + "cpufeatures", + "curve25519-dalek-derive", + "digest 0.11.0-rc.10", + "fiat-crypto", + "rand_core 0.9.5", + "rustc_version", + "serde", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "darling" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" +dependencies = [ + "darling_core 0.20.11", + "darling_macro 0.20.11", +] + [[package]] name = "darling" version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" dependencies = [ - "darling_core", - "darling_macro", + "darling_core 0.23.0", + "darling_macro 0.23.0", +] + +[[package]] +name = "darling_core" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.114", ] [[package]] @@ -1001,13 +1243,24 @@ dependencies = [ "syn 2.0.114", ] +[[package]] +name = "darling_macro" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" +dependencies = [ + "darling_core 0.20.11", + "quote", + "syn 2.0.114", +] + [[package]] name = "darling_macro" version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" dependencies = [ - "darling_core", + "darling_core 0.23.0", "quote", "syn 2.0.114", ] @@ -1030,8 +1283,19 @@ version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" dependencies = [ - "const-oid", - "pem-rfc7468", + "const-oid 0.9.6", + "pem-rfc7468 0.7.0", + "zeroize", +] + +[[package]] +name = "der" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71fd89660b2dc699704064e59e9dba0147b903e85319429e131620d022be411b" +dependencies = [ + "const-oid 0.10.2", + "pem-rfc7468 1.0.0", "zeroize", ] @@ -1045,6 +1309,37 @@ dependencies = [ "serde_core", ] +[[package]] +name = "derive_builder" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "507dfb09ea8b7fa618fcf76e953f4f5e192547945816d5358edffe39f6f94947" +dependencies = [ + "derive_builder_macro", +] + +[[package]] +name = "derive_builder_core" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8" +dependencies = [ + "darling 0.20.11", + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "derive_builder_macro" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c" +dependencies = [ + "derive_builder_core", + "syn 2.0.114", +] + [[package]] name = "derive_more" version = "0.99.20" @@ -1078,20 +1373,38 @@ dependencies = [ "quote", "rustc_version", "syn 2.0.114", + "unicode-xid", ] +[[package]] +name = "diatomic-waker" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab03c107fafeb3ee9f5925686dbb7a73bc76e3932abb0d2b365cb64b169cf04c" + [[package]] name = "digest" version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ - "block-buffer", - "const-oid", - "crypto-common", + "block-buffer 0.10.4", + "const-oid 0.9.6", + "crypto-common 0.1.7", "subtle", ] +[[package]] +name = "digest" +version = "0.11.0-rc.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afa94b64bfc6549e6e4b5a3216f22593224174083da7a90db47e951c4fb31725" +dependencies = [ + "block-buffer 0.11.0", + "const-oid 0.10.2", + "crypto-common 0.2.1", +] + [[package]] name = "dirs" version = "5.0.1" @@ -1157,6 +1470,17 @@ dependencies = [ "syn 2.0.114", ] +[[package]] +name = "dlopen2" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09b4f5f101177ff01b8ec4ecc81eead416a8aa42819a2869311b3420fa114ffa" +dependencies = [ + "libc", + "once_cell", + "winapi", +] + [[package]] name = "dlopen2" version = "0.8.2" @@ -1252,6 +1576,33 @@ version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" +[[package]] +name = "ed25519" +version = "3.0.0-rc.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6e914c7c52decb085cea910552e24c63ac019e3ab8bf001ff736da9a9d9d890" +dependencies = [ + "pkcs8 0.11.0-rc.11", + "serde", + "signature 3.0.0-rc.10", +] + +[[package]] +name = "ed25519-dalek" +version = "3.0.0-pre.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad207ed88a133091f83224265eac21109930db09bedcad05d5252f2af2de20a1" +dependencies = [ + "curve25519-dalek", + "ed25519", + "rand_core 0.9.5", + "serde", + "sha2 0.11.0-rc.2", + "signature 3.0.0-rc.10", + "subtle", + "zeroize", +] + [[package]] name = "either" version = "1.15.0" @@ -1281,6 +1632,18 @@ version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7" +[[package]] +name = "embedded-io" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef1a6892d9eef45c8fa6b9e0086428a2cca8491aca8f787c534a3d6d0bcb3ced" + +[[package]] +name = "embedded-io" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d" + [[package]] name = "encode_unicode" version = "1.0.0" @@ -1297,12 +1660,35 @@ dependencies = [ ] [[package]] -name = "equivalent" -version = "1.0.2" +name = "enum-as-inner" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" - -[[package]] +checksum = "a1e6a265c649f3f5979b601d26f1d05ada116434c87741c9493cb56218f76cbc" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "enum-assoc" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ed8956bd5c1f0415200516e78ff07ec9e16415ade83c056c230d7b7ea0d55b7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] name = "erased-serde" version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1323,6 +1709,12 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "error-code" +version = "3.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dea2df4cf52843e0452895c455a1a2cfbb842a1e7329671acf418fdc53ed4c59" + [[package]] name = "etcetera" version = "0.8.0" @@ -1359,6 +1751,18 @@ dependencies = [ "regex", ] +[[package]] +name = "fastbloom" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7f34442dbe69c60fe8eaf58a8cafff81a1f278816d8ab4db255b3bef4ac3c4" +dependencies = [ + "getrandom 0.3.4", + "libm", + "rand 0.9.2", + "siphasher 1.0.2", +] + [[package]] name = "fastrand" version = "2.3.0" @@ -1374,6 +1778,12 @@ dependencies = [ "simd-adler32", ] +[[package]] +name = "fiat-crypto" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64cd1e32ddd350061ae6edb1b082d7c54915b5c672c389143b9a63403a109f24" + [[package]] name = "field-offset" version = "0.3.6" @@ -1456,7 +1866,7 @@ checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095" dependencies = [ "futures-core", "futures-sink", - "spin", + "spin 0.9.8", ] [[package]] @@ -1547,6 +1957,19 @@ dependencies = [ "futures-util", ] +[[package]] +name = "futures-buffered" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4421cb78ee172b6b06080093479d3c50f058e7c81b7d577bbb8d118d551d4cd5" +dependencies = [ + "cordyceps", + "diatomic-waker", + "futures-core", + "pin-project-lite", + "spin 0.10.0", +] + [[package]] name = "futures-channel" version = "0.3.31" @@ -1591,6 +2014,19 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" +[[package]] +name = "futures-lite" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "parking", + "pin-project-lite", +] + [[package]] name = "futures-macro" version = "0.3.31" @@ -1740,6 +2176,21 @@ dependencies = [ "x11", ] +[[package]] +name = "generator" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52f04ae4152da20c76fe800fa48659201d5cf627c5149ca0b707b69d7eef6cf9" +dependencies = [ + "cc", + "cfg-if", + "libc", + "log", + "rustversion", + "windows-link 0.2.1", + "windows-result 0.4.1", +] + [[package]] name = "generic-array" version = "0.14.7" @@ -1750,6 +2201,16 @@ dependencies = [ "version_check", ] +[[package]] +name = "gethostname" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bd49230192a3797a9a4d6abe9b3eed6f7fa4c8a8a4947977c6f80025f92cbd8" +dependencies = [ + "rustix", + "windows-link 0.2.1", +] + [[package]] name = "getrandom" version = "0.1.16" @@ -1795,10 +2256,22 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "139ef39800118c7683f2fd3c98c1b23c09ae076556b435f8e9064ae108aaeeec" dependencies = [ "cfg-if", + "js-sys", "libc", "r-efi", "wasip2", "wasip3", + "wasm-bindgen", +] + +[[package]] +name = "ghash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1" +dependencies = [ + "opaque-debug", + "polyval", ] [[package]] @@ -1899,6 +2372,18 @@ dependencies = [ "regex-syntax", ] +[[package]] +name = "gloo-timers" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbb143cf96099802033e0d4f4963b19fd2e0b728bcf076cd9cf7f6634f092994" +dependencies = [ + "futures-channel", + "futures-core", + "js-sys", + "wasm-bindgen", +] + [[package]] name = "gobject-sys" version = "0.18.0" @@ -1992,6 +2477,15 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "hash32" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0c35f58762feb77d74ebe43bdbc3210f09be9fe6742234d573bacc26ed92b67" +dependencies = [ + "byteorder", +] + [[package]] name = "hashbrown" version = "0.12.3" @@ -2061,6 +2555,20 @@ dependencies = [ "http", ] +[[package]] +name = "heapless" +version = "0.7.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdc6457c0eb62c71aac4bc17216026d8410337c4126773b9c5daba343f17964f" +dependencies = [ + "atomic-polyfill", + "hash32", + "rustc_version", + "serde", + "spin 0.9.8", + "stable_deref_trait", +] + [[package]] name = "heck" version = "0.4.1" @@ -2082,6 +2590,59 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "hickory-proto" +version = "0.25.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8a6fe56c0038198998a6f217ca4e7ef3a5e51f46163bd6dd60b5c71ca6c6502" +dependencies = [ + "async-trait", + "bytes", + "cfg-if", + "data-encoding", + "enum-as-inner", + "futures-channel", + "futures-io", + "futures-util", + "h2", + "http", + "idna", + "ipnet", + "once_cell", + "rand 0.9.2", + "ring", + "rustls", + "thiserror 2.0.18", + "tinyvec", + "tokio", + "tokio-rustls", + "tracing", + "url", +] + +[[package]] +name = "hickory-resolver" +version = "0.25.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc62a9a99b0bfb44d2ab95a7208ac952d31060efc16241c87eaf36406fecf87a" +dependencies = [ + "cfg-if", + "futures-util", + "hickory-proto", + "ipconfig", + "moka", + "once_cell", + "parking_lot", + "rand 0.9.2", + "resolv-conf", + "rustls", + "smallvec", + "thiserror 2.0.18", + "tokio", + "tokio-rustls", + "tracing", +] + [[package]] name = "hkdf" version = "0.12.4" @@ -2097,7 +2658,7 @@ version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" dependencies = [ - "digest", + "digest 0.10.7", ] [[package]] @@ -2195,6 +2756,15 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" +[[package]] +name = "hybrid-array" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3944cf8cf766b40e2a1a333ee5e9b563f854d5fa49d6a8ca2764e97c6eddb214" +dependencies = [ + "typenum", +] + [[package]] name = "hyper" version = "1.8.1" @@ -2385,6 +2955,12 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" +[[package]] +name = "identity-hash" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfdd7caa900436d8f13b2346fe10257e0c05c1f1f9e351f4f5d57c03bd5f45da" + [[package]] name = "idna" version = "1.1.0" @@ -2406,6 +2982,27 @@ dependencies = [ "icu_properties", ] +[[package]] +name = "igd-next" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "516893339c97f6011282d5825ac94fc1c7aad5cad26bdc2d0cee068c0bf97f97" +dependencies = [ + "async-trait", + "attohttpc", + "bytes", + "futures", + "http", + "http-body-util", + "hyper", + "hyper-util", + "log", + "rand 0.9.2", + "tokio", + "url", + "xmltree", +] + [[package]] name = "ignore" version = "0.4.25" @@ -2499,6 +3096,15 @@ dependencies = [ "libc", ] +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "generic-array", +] + [[package]] name = "insta" version = "1.46.3" @@ -2517,7 +3123,7 @@ version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "357b7205c6cd18dd2c86ed312d1e70add149aea98e7ef72b9fdf0270e555c11d" dependencies = [ - "darling", + "darling 0.23.0", "indoc", "proc-macro2", "quote", @@ -2542,6 +3148,19 @@ dependencies = [ "libc", ] +[[package]] +name = "ipconfig" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d40460c0ce33d6ce4b0630ad68ff63d6661961c48b6dba35e5a4d81cfb48222" +dependencies = [ + "socket2", + "widestring", + "windows-registry", + "windows-result 0.4.1", + "windows-sys 0.61.2", +] + [[package]] name = "ipnet" version = "2.11.0" @@ -2558,6 +3177,168 @@ dependencies = [ "serde", ] +[[package]] +name = "iroh" +version = "0.97.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "feb56e7e4b0ec7fba7efa6a236b016a52b5d927d50244aceb9e20566159b1a32" +dependencies = [ + "backon", + "bytes", + "cfg_aliases", + "data-encoding", + "derive_more 2.1.1", + "ed25519-dalek", + "futures-util", + "getrandom 0.3.4", + "hickory-resolver", + "http", + "ipnet", + "iroh-base", + "iroh-metrics", + "iroh-relay", + "n0-error", + "n0-future", + "n0-watcher", + "netwatch", + "noq", + "noq-proto", + "noq-udp", + "papaya", + "pin-project", + "pkarr", + "pkcs8 0.11.0-rc.11", + "portable-atomic", + "portmapper", + "rand 0.9.2", + "reqwest 0.12.28", + "rustc-hash", + "rustls", + "rustls-pki-types", + "rustls-webpki", + "serde", + "smallvec", + "strum 0.28.0", + "sync_wrapper", + "time", + "tokio", + "tokio-stream", + "tokio-util", + "tracing", + "url", + "wasm-bindgen-futures", + "webpki-roots 1.0.6", +] + +[[package]] +name = "iroh-base" +version = "0.97.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55a354e3396b62c14717ee807dfee9a7f43f6dad47e4ac0fd1d49f1ffad14ef0" +dependencies = [ + "curve25519-dalek", + "data-encoding", + "derive_more 2.1.1", + "digest 0.11.0-rc.10", + "ed25519-dalek", + "n0-error", + "rand_core 0.9.5", + "serde", + "sha2 0.11.0-rc.2", + "url", + "zeroize", + "zeroize_derive", +] + +[[package]] +name = "iroh-metrics" +version = "0.38.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "761b45ba046134b11eb3e432fa501616b45c4bf3a30c21717578bc07aa6461dd" +dependencies = [ + "iroh-metrics-derive", + "itoa", + "n0-error", + "portable-atomic", + "postcard", + "ryu", + "serde", + "tracing", +] + +[[package]] +name = "iroh-metrics-derive" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cab063c2bfd6c3d5a33a913d4fdb5252f140db29ec67c704f20f3da7e8f92dbf" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "iroh-relay" +version = "0.97.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d786b260cadfe82ae0b6a9e372e8c78949096a06c857d1c3521355cefced0f55" +dependencies = [ + "blake3", + "bytes", + "cfg_aliases", + "data-encoding", + "derive_more 2.1.1", + "getrandom 0.3.4", + "hickory-resolver", + "http", + "http-body-util", + "hyper", + "hyper-util", + "iroh-base", + "iroh-metrics", + "lru", + "n0-error", + "n0-future", + "noq", + "noq-proto", + "num_enum", + "pin-project", + "pkarr", + "postcard", + "rand 0.9.2", + "reqwest 0.12.28", + "rustls", + "rustls-pki-types", + "serde", + "serde_bytes", + "strum 0.28.0", + "tokio", + "tokio-rustls", + "tokio-util", + "tokio-websockets", + "tracing", + "url", + "vergen-gitcl", + "webpki-roots 1.0.6", + "ws_stream_wasm", + "z32", +] + +[[package]] +name = "iroh-tickets" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab64bac4bb573b9cfd2142bd2876ed65ca792efbc4398361a4ee51a0f9afbed6" +dependencies = [ + "data-encoding", + "derive_more 2.1.1", + "iroh-base", + "n0-error", + "postcard", + "serde", +] + [[package]] name = "is-docker" version = "0.2.0" @@ -2741,7 +3522,7 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" dependencies = [ - "spin", + "spin 0.9.8", ] [[package]] @@ -2860,6 +3641,19 @@ version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +[[package]] +name = "loom" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "419e0dc8046cb947daa77eb95ae174acfbddb7673b4151f56d1eed8e93fbfaca" +dependencies = [ + "cfg-if", + "generator", + "scoped-tls", + "tracing", + "tracing-subscriber", +] + [[package]] name = "lru" version = "0.16.3" @@ -2881,6 +3675,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" +[[package]] +name = "mac-addr" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3d25b0e0b648a86960ac23b7ad4abb9717601dec6f66c165f5b037f3f03065f" + [[package]] name = "mac_address" version = "1.1.8" @@ -2979,7 +3779,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" dependencies = [ "cfg-if", - "digest", + "digest 0.10.7", ] [[package]] @@ -3028,6 +3828,26 @@ dependencies = [ "unicase", ] +[[package]] +name = "mindmeld" +version = "0.1.0" +dependencies = [ + "anyhow", + "arboard", + "clap", + "iroh", + "iroh-tickets", + "pty_manager", + "ratatui", + "serde", + "tokio", + "toml 1.1.2+spec-1.1.0", + "tracing", + "tracing-subscriber", + "uuid", + "virtual_terminal", +] + [[package]] name = "minimal-lexical" version = "0.2.1" @@ -3056,6 +3876,23 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "moka" +version = "0.12.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "957228ad12042ee839f93c8f257b62b4c0ab5eaae1d4fa60de53b27c9d7c5046" +dependencies = [ + "crossbeam-channel", + "crossbeam-epoch", + "crossbeam-utils", + "equivalent", + "parking_lot", + "portable-atomic", + "smallvec", + "tagptr", + "uuid", +] + [[package]] name = "muda" version = "0.17.1" @@ -3077,6 +3914,59 @@ dependencies = [ "windows-sys 0.60.2", ] +[[package]] +name = "n0-error" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af4782b4baf92d686d161c15460c83d16ebcfd215918763903e9619842665cae" +dependencies = [ + "n0-error-macros", + "spez", +] + +[[package]] +name = "n0-error-macros" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03755949235714b2b307e5ae89dd8c1c2531fb127d9b8b7b4adf9c876cd3ed18" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "n0-future" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2ab99dfb861450e68853d34ae665243a88b8c493d01ba957321a1e9b2312bbe" +dependencies = [ + "cfg_aliases", + "derive_more 2.1.1", + "futures-buffered", + "futures-lite", + "futures-util", + "js-sys", + "pin-project", + "send_wrapper", + "tokio", + "tokio-util", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-time", +] + +[[package]] +name = "n0-watcher" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38795f7932e6e9d1c6e989270ef5b3ff24ebb910e2c9d4bed2d28d8bae3007dc" +dependencies = [ + "derive_more 2.1.1", + "n0-error", + "n0-future", +] + [[package]] name = "ndk" version = "0.9.0" @@ -3107,6 +3997,112 @@ dependencies = [ "jni-sys", ] +[[package]] +name = "netdev" +version = "0.40.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b0a0096d9613ee878dba89bbe595f079d373e3f1960d882e4f2f78ff9c30a0a" +dependencies = [ + "block2", + "dispatch2", + "dlopen2 0.5.0", + "ipnet", + "libc", + "mac-addr", + "netlink-packet-core", + "netlink-packet-route", + "netlink-sys", + "objc2-core-foundation", + "objc2-system-configuration", + "once_cell", + "plist", + "windows-sys 0.59.0", +] + +[[package]] +name = "netlink-packet-core" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3463cbb78394cb0141e2c926b93fc2197e473394b761986eca3b9da2c63ae0f4" +dependencies = [ + "paste", +] + +[[package]] +name = "netlink-packet-route" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df9854ea6ad14e3f4698a7f03b65bce0833dd2d81d594a0e4a984170537146b6" +dependencies = [ + "bitflags 2.10.0", + "libc", + "log", + "netlink-packet-core", +] + +[[package]] +name = "netlink-proto" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b65d130ee111430e47eed7896ea43ca693c387f097dd97376bffafbf25812128" +dependencies = [ + "bytes", + "futures", + "log", + "netlink-packet-core", + "netlink-sys", + "thiserror 2.0.18", +] + +[[package]] +name = "netlink-sys" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd6c30ed10fa69cc491d491b85cc971f6bdeb8e7367b7cde2ee6cc878d583fae" +dependencies = [ + "bytes", + "futures-util", + "libc", + "log", + "tokio", +] + +[[package]] +name = "netwatch" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b1b27babe89ef9f2237bc6c028bea24fa84163a1b6f8f17ff93573ebd7d861f" +dependencies = [ + "atomic-waker", + "bytes", + "cfg_aliases", + "derive_more 2.1.1", + "js-sys", + "libc", + "n0-error", + "n0-future", + "n0-watcher", + "netdev", + "netlink-packet-core", + "netlink-packet-route", + "netlink-proto", + "netlink-sys", + "noq-udp", + "objc2-core-foundation", + "objc2-system-configuration", + "pin-project-lite", + "serde", + "socket2", + "time", + "tokio", + "tokio-util", + "tracing", + "web-sys", + "windows 0.62.2", + "windows-result 0.4.1", + "wmi", +] + [[package]] name = "new_debug_unreachable" version = "1.0.6" @@ -3156,6 +4152,67 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "noq" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df966fb44ac763bc86da97fa6c811c54ae82ef656575949f93c6dae0c9f09bf" +dependencies = [ + "bytes", + "cfg_aliases", + "noq-proto", + "noq-udp", + "pin-project-lite", + "rustc-hash", + "rustls", + "socket2", + "thiserror 2.0.18", + "tokio", + "tokio-stream", + "tracing", + "web-time", +] + +[[package]] +name = "noq-proto" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c61b72abd670eebc05b5cf720e077b04a3ef3354bc7bc19f1c3524cb424db7b" +dependencies = [ + "aes-gcm", + "bytes", + "derive_more 2.1.1", + "enum-assoc", + "fastbloom", + "getrandom 0.3.4", + "identity-hash", + "lru-slab", + "rand 0.9.2", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "sorted-index-buffer", + "thiserror 2.0.18", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "noq-udp" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb9be4fedd6b98f3ba82ccd3506f4d0219fb723c3f97c67e12fe1494aa020e44" +dependencies = [ + "cfg_aliases", + "libc", + "socket2", + "tracing", + "windows-sys 0.61.2", +] + [[package]] name = "notify" version = "7.0.0" @@ -3184,6 +4241,21 @@ dependencies = [ "instant", ] +[[package]] +name = "ntimestamp" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c50f94c405726d3e0095e89e72f75ce7f6587b94a8bd8dc8054b73f65c0fd68c" +dependencies = [ + "base32", + "document-features", + "getrandom 0.2.17", + "httpdate", + "js-sys", + "once_cell", + "serde", +] + [[package]] name = "nu-ansi-term" version = "0.50.3" @@ -3307,6 +4379,7 @@ dependencies = [ "block2", "objc2", "objc2-core-foundation", + "objc2-core-graphics", "objc2-foundation", ] @@ -3317,7 +4390,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" dependencies = [ "bitflags 2.10.0", + "block2", "dispatch2", + "libc", "objc2", ] @@ -3384,6 +4459,31 @@ dependencies = [ "objc2-foundation", ] +[[package]] +name = "objc2-security" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "709fe137109bd1e8b5a99390f77a7d8b2961dafc1a1c5db8f2e60329ad6d895a" +dependencies = [ + "bitflags 2.10.0", + "objc2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-system-configuration" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7216bd11cbda54ccabcab84d523dc93b858ec75ecfb3a7d89513fa22464da396" +dependencies = [ + "bitflags 2.10.0", + "dispatch2", + "libc", + "objc2", + "objc2-core-foundation", + "objc2-security", +] + [[package]] name = "objc2-ui-kit" version = "0.3.2" @@ -3415,6 +4515,10 @@ name = "once_cell" version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +dependencies = [ + "critical-section", + "portable-atomic", +] [[package]] name = "once_cell_polyfill" @@ -3422,6 +4526,12 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + [[package]] name = "open" version = "5.3.3" @@ -3484,6 +4594,22 @@ dependencies = [ "system-deps", ] +[[package]] +name = "papaya" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "997ee03cd38c01469a7046643714f0ad28880bcb9e6679ff0666e24817ca19b7" +dependencies = [ + "equivalent", + "seize", +] + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + [[package]] name = "parking_lot" version = "0.12.5" @@ -3562,6 +4688,15 @@ dependencies = [ "base64ct", ] +[[package]] +name = "pem-rfc7468" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6305423e0e7738146434843d1694d621cce767262b2a86910beab705e4493d9" +dependencies = [ + "base64ct", +] + [[package]] name = "percent-encoding" version = "2.3.2" @@ -3608,7 +4743,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89815c69d36021a140146f26659a81d6c2afa33d216d736dd4be5381a7362220" dependencies = [ "pest", - "sha2", + "sha2 0.10.9", +] + +[[package]] +name = "pharos" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9567389417feee6ce15dd6527a8a1ecac205ef62c2932bcf3d9f6fc5b78b414" +dependencies = [ + "futures", + "rustc_version", ] [[package]] @@ -3798,6 +4943,26 @@ dependencies = [ "siphasher 1.0.2", ] +[[package]] +name = "pin-project" +version = "1.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1749c7ed4bcaf4c3d0a3efc28538844fb29bcdd7d2b67b2be7e20ba861ff517" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b20ed30f105399776b9c883e68e536ef602a16ae6f596d2c473591d6ad64c6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + [[package]] name = "pin-project-lite" version = "0.2.16" @@ -3810,15 +4975,34 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "pkarr" +version = "5.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7bfb9143bbba379f246211eb68074d78db9cc048e4c5701f3b0e6cb1ec67ca2" +dependencies = [ + "base32", + "bytes", + "cfg_aliases", + "document-features", + "ed25519-dalek", + "getrandom 0.4.1", + "ntimestamp", + "self_cell", + "serde", + "simple-dns", + "thiserror 2.0.18", +] + [[package]] name = "pkcs1" version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" dependencies = [ - "der", - "pkcs8", - "spki", + "der 0.7.10", + "pkcs8 0.10.2", + "spki 0.7.3", ] [[package]] @@ -3827,8 +5011,18 @@ version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" dependencies = [ - "der", - "spki", + "der 0.7.10", + "spki 0.7.3", +] + +[[package]] +name = "pkcs8" +version = "0.11.0-rc.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12922b6296c06eb741b02d7b5161e3aaa22864af38dfa025a1a3ba3f68c84577" +dependencies = [ + "der 0.8.0", + "spki 0.8.0", ] [[package]] @@ -3863,11 +5057,26 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "polyval" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" +dependencies = [ + "cfg-if", + "cpufeatures", + "opaque-debug", + "universal-hash", +] + [[package]] name = "portable-atomic" version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" +dependencies = [ + "serde", +] [[package]] name = "portable-pty" @@ -3890,6 +5099,61 @@ dependencies = [ "winreg 0.10.1", ] +[[package]] +name = "portmapper" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74748bc706fa6b6aebac6bbe0bbe0de806b384cb5c557ea974f771360a4e3858" +dependencies = [ + "base64 0.22.1", + "bytes", + "derive_more 2.1.1", + "futures-lite", + "futures-util", + "hyper-util", + "igd-next", + "iroh-metrics", + "libc", + "n0-error", + "netwatch", + "num_enum", + "rand 0.9.2", + "serde", + "smallvec", + "socket2", + "time", + "tokio", + "tokio-util", + "tower-layer", + "tracing", + "url", +] + +[[package]] +name = "postcard" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6764c3b5dd454e283a30e6dfe78e9b31096d9e32036b5d1eaac7a6119ccb9a24" +dependencies = [ + "cobs", + "embedded-io 0.4.0", + "embedded-io 0.6.1", + "heapless", + "postcard-derive", + "serde", +] + +[[package]] +name = "postcard-derive" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0232bd009a197ceec9cc881ba46f727fcd8060a2d8d6a9dde7a69030a6fe2bb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + [[package]] name = "potential_utf" version = "0.1.4" @@ -4239,7 +5503,7 @@ dependencies = [ "itertools", "kasuari", "lru", - "strum", + "strum 0.27.2", "thiserror 2.0.18", "unicode-segmentation", "unicode-truncate", @@ -4291,7 +5555,7 @@ dependencies = [ "itertools", "line-clipping", "ratatui-core", - "strum", + "strum 0.27.2", "time", "unicode-segmentation", "unicode-width", @@ -4425,12 +5689,14 @@ dependencies = [ "sync_wrapper", "tokio", "tokio-rustls", + "tokio-util", "tower", "tower-http", "tower-service", "url", "wasm-bindgen", "wasm-bindgen-futures", + "wasm-streams 0.4.2", "web-sys", "webpki-roots 1.0.6", ] @@ -4465,10 +5731,16 @@ dependencies = [ "url", "wasm-bindgen", "wasm-bindgen-futures", - "wasm-streams", + "wasm-streams 0.5.0", "web-sys", ] +[[package]] +name = "resolv-conf" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e061d1b48cb8d38042de4ae0a7a6401009d6143dc80d2e2d6f31f0bdd6470c7" + [[package]] name = "ring" version = "0.17.14" @@ -4500,16 +5772,16 @@ version = "0.9.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8573f03f5883dcaebdfcf4725caa1ecb9c15b2ef50c43a07b816e06799bb12d" dependencies = [ - "const-oid", - "digest", + "const-oid 0.9.6", + "digest 0.10.7", "num-bigint-dig", "num-integer", "num-traits", "pkcs1", - "pkcs8", + "pkcs8 0.10.2", "rand_core 0.6.4", - "signature", - "spki", + "signature 2.2.0", + "spki 0.7.3", "subtle", "zeroize", ] @@ -4556,7 +5828,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5bcdef0be6fe7f6fa333b1073c949729274b05f123a0ad7efcb8efd878e5c3b1" dependencies = [ "globset", - "sha2", + "sha2 0.10.9", "walkdir", ] @@ -4696,12 +5968,28 @@ dependencies = [ "syn 2.0.114", ] +[[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 = "seize" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b55fb86dfd3a2f5f76ea78310a88f96c4ea21a3031f8d212443d56123fd0521" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + [[package]] name = "selectors" version = "0.24.0" @@ -4739,6 +6027,12 @@ dependencies = [ "smallvec", ] +[[package]] +name = "self_cell" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b12e76d157a900eb52e81bc6e9f3069344290341720e9178cde2407113ac8d89" + [[package]] name = "semver" version = "1.0.27" @@ -4749,6 +6043,12 @@ dependencies = [ "serde_core", ] +[[package]] +name = "send_wrapper" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd0b0ec5f1c1ca621c432a25813d8d60c88abe6d3e08a3eb9cf37d97a0fe3d73" + [[package]] name = "serde" version = "1.0.228" @@ -4771,6 +6071,16 @@ dependencies = [ "typeid", ] +[[package]] +name = "serde_bytes" +version = "0.11.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5d440709e79d88e51ac01c4b72fc6cb7314017bb7da9eeff678aa94c10e3ea8" +dependencies = [ + "serde", + "serde_core", +] + [[package]] name = "serde_core" version = "1.0.228" @@ -4849,9 +6159,9 @@ dependencies = [ [[package]] name = "serde_spanned" -version = "1.0.4" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8bbf91e5a4d6315eee45e704372590b30e260ee83af6639d64557f51b067776" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" dependencies = [ "serde_core", ] @@ -4893,7 +6203,7 @@ version = "3.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3db8978e608f1fe7357e211969fd9abdcae80bac1ba7a3369bb7eb6b404eb65" dependencies = [ - "darling", + "darling 0.23.0", "proc-macro2", "quote", "syn 2.0.114", @@ -4990,7 +6300,7 @@ checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" dependencies = [ "cfg-if", "cpufeatures", - "digest", + "digest 0.10.7", ] [[package]] @@ -5001,7 +6311,18 @@ checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", "cpufeatures", - "digest", + "digest 0.10.7", +] + +[[package]] +name = "sha2" +version = "0.11.0-rc.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1e3878ab0f98e35b2df35fe53201d088299b41a6bb63e3e34dada2ac4abd924" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest 0.11.0-rc.10", ] [[package]] @@ -5103,21 +6424,42 @@ version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" dependencies = [ - "digest", + "digest 0.10.7", "rand_core 0.6.4", ] +[[package]] +name = "signature" +version = "3.0.0-rc.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f1880df446116126965eeec169136b2e0251dba37c6223bcc819569550edea3" + [[package]] name = "simd-adler32" version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + [[package]] name = "similar" version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa" +checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa" + +[[package]] +name = "simple-dns" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df350943049174c4ae8ced56c604e28270258faec12a6a48637a7655287c9ce0" +dependencies = [ + "bitflags 2.10.0", +] [[package]] name = "siphasher" @@ -5175,6 +6517,12 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "sorted-index-buffer" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea06cc588e43c632923a55450401b8f25e628131571d4e1baea1bdfdb2b5ed06" + [[package]] name = "soup3" version = "0.5.0" @@ -5201,6 +6549,17 @@ dependencies = [ "system-deps", ] +[[package]] +name = "spez" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c87e960f4dca2788eeb86bbdde8dd246be8948790b7618d656e68f9b720a86e8" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + [[package]] name = "spin" version = "0.9.8" @@ -5210,6 +6569,12 @@ dependencies = [ "lock_api", ] +[[package]] +name = "spin" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5fe4ccb98d9c292d56fec89a5e07da7fc4cf0dc11e156b41793132775d3e591" + [[package]] name = "spki" version = "0.7.3" @@ -5217,7 +6582,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" dependencies = [ "base64ct", - "der", + "der 0.7.10", +] + +[[package]] +name = "spki" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d9efca8738c78ee9484207732f728b1ef517bbb1833d6fc0879ca898a522f6f" +dependencies = [ + "base64ct", + "der 0.8.0", ] [[package]] @@ -5273,7 +6648,7 @@ dependencies = [ "percent-encoding", "serde", "serde_json", - "sha2", + "sha2 0.10.9", "smallvec", "sqlformat", "thiserror 1.0.69", @@ -5312,7 +6687,7 @@ dependencies = [ "quote", "serde", "serde_json", - "sha2", + "sha2 0.10.9", "sqlx-core", "sqlx-mysql", "sqlx-postgres", @@ -5336,7 +6711,7 @@ dependencies = [ "bytes", "chrono", "crc", - "digest", + "digest 0.10.7", "dotenvy", "either", "futures-channel", @@ -5357,7 +6732,7 @@ dependencies = [ "rsa", "serde", "sha1", - "sha2", + "sha2 0.10.9", "smallvec", "sqlx-core", "stringprep", @@ -5397,7 +6772,7 @@ dependencies = [ "rand 0.8.5", "serde", "serde_json", - "sha2", + "sha2 0.10.9", "smallvec", "sqlx-core", "stringprep", @@ -5522,7 +6897,16 @@ version = "0.27.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" dependencies = [ - "strum_macros", + "strum_macros 0.27.2", +] + +[[package]] +name = "strum" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9628de9b8791db39ceda2b119bbe13134770b56c138ec1d3af810d045c04f9bd" +dependencies = [ + "strum_macros 0.28.0", ] [[package]] @@ -5537,6 +6921,18 @@ dependencies = [ "syn 2.0.114", ] +[[package]] +name = "strum_macros" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab85eea0270ee17587ed4156089e10b9e6880ee688791d45a905f5b1ca36f664" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.114", +] + [[package]] name = "subtle" version = "2.6.1" @@ -5623,6 +7019,12 @@ dependencies = [ "version-compare", ] +[[package]] +name = "tagptr" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417" + [[package]] name = "tao" version = "0.34.6" @@ -5635,7 +7037,7 @@ dependencies = [ "core-graphics", "crossbeam-channel", "dispatch2", - "dlopen2", + "dlopen2 0.8.2", "dpi", "gdkwayland-sys", "gdkx11-sys", @@ -5655,7 +7057,7 @@ dependencies = [ "tao-macros", "unicode-segmentation", "url", - "windows", + "windows 0.61.3", "windows-core 0.61.2", "windows-version", "x11-dl", @@ -5726,7 +7128,7 @@ dependencies = [ "webkit2gtk", "webview2-com", "window-vibrancy", - "windows", + "windows 0.61.3", ] [[package]] @@ -5768,7 +7170,7 @@ dependencies = [ "semver", "serde", "serde_json", - "sha2", + "sha2 0.10.9", "syn 2.0.114", "tauri-utils", "thiserror 2.0.18", @@ -5867,7 +7269,7 @@ dependencies = [ "url", "webkit2gtk", "webview2-com", - "windows", + "windows 0.61.3", ] [[package]] @@ -5892,7 +7294,7 @@ dependencies = [ "url", "webkit2gtk", "webview2-com", - "windows", + "windows 0.61.3", "wry", ] @@ -6024,7 +7426,7 @@ dependencies = [ "pest", "pest_derive", "phf 0.11.3", - "sha2", + "sha2 0.10.9", "signal-hook", "siphasher 1.0.2", "terminfo", @@ -6098,6 +7500,7 @@ checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" dependencies = [ "deranged", "itoa", + "js-sys", "libc", "num-conv", "num_threads", @@ -6195,6 +7598,7 @@ dependencies = [ "futures-core", "pin-project-lite", "tokio", + "tokio-util", ] [[package]] @@ -6241,10 +7645,33 @@ dependencies = [ "bytes", "futures-core", "futures-sink", + "futures-util", "pin-project-lite", "tokio", ] +[[package]] +name = "tokio-websockets" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1b6348ebfaaecd771cecb69e832961d277f59845d4220a584701f72728152b7" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-core", + "futures-sink", + "getrandom 0.3.4", + "http", + "httparse", + "rand 0.9.2", + "ring", + "rustls-pki-types", + "simdutf8", + "tokio", + "tokio-rustls", + "tokio-util", +] + [[package]] name = "toml" version = "0.8.2" @@ -6265,13 +7692,28 @@ checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863" dependencies = [ "indexmap 2.13.0", "serde_core", - "serde_spanned 1.0.4", + "serde_spanned 1.1.1", "toml_datetime 0.7.5+spec-1.1.0", "toml_parser", "toml_writer", "winnow 0.7.14", ] +[[package]] +name = "toml" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81f3d15e84cbcd896376e6730314d59fb5a87f31e4b038454184435cd57defee" +dependencies = [ + "indexmap 2.13.0", + "serde_core", + "serde_spanned 1.1.1", + "toml_datetime 1.1.1+spec-1.1.0", + "toml_parser", + "toml_writer", + "winnow 1.0.1", +] + [[package]] name = "toml_datetime" version = "0.6.3" @@ -6292,9 +7734,9 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "1.0.0+spec-1.1.0" +version = "1.1.1+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32c2555c699578a4f59f0cc68e5116c8d7cabbd45e1409b989d4be085b53f13e" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" dependencies = [ "serde_core", ] @@ -6330,25 +7772,25 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7193cbd0ce53dc966037f54351dbbcf0d5a642c7f0038c382ef9e677ce8c13f2" dependencies = [ "indexmap 2.13.0", - "toml_datetime 1.0.0+spec-1.1.0", + "toml_datetime 1.1.1+spec-1.1.0", "toml_parser", "winnow 0.7.14", ] [[package]] name = "toml_parser" -version = "1.0.9+spec-1.1.0" +version = "1.1.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "702d4415e08923e7e1ef96cd5727c0dfed80b4d2fa25db9647fe5eb6f7c5a4c4" +checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" dependencies = [ - "winnow 0.7.14", + "winnow 1.0.1", ] [[package]] name = "toml_writer" -version = "1.0.6+spec-1.1.0" +version = "1.1.1+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab16f14aed21ee8bfd8ec22513f7287cd4a91aa92e44edfe2c17ddd004e92607" +checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db" [[package]] name = "toolpath" @@ -6853,6 +8295,16 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e" +[[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common 0.1.7", + "subtle", +] + [[package]] name = "untrusted" version = "0.9.0" @@ -6949,6 +8401,54 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" +[[package]] +name = "vergen" +version = "9.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b849a1f6d8639e8de261e81ee0fc881e3e3620db1af9f2e0da015d4382ceaf75" +dependencies = [ + "anyhow", + "derive_builder", + "rustversion", + "vergen-lib 9.1.0", +] + +[[package]] +name = "vergen-gitcl" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9dfc1de6eb2e08a4ddf152f1b179529638bedc0ea95e6d667c014506377aefe" +dependencies = [ + "anyhow", + "derive_builder", + "rustversion", + "time", + "vergen", + "vergen-lib 0.1.6", +] + +[[package]] +name = "vergen-lib" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b07e6010c0f3e59fcb164e0163834597da68d1f864e2b8ca49f74de01e9c166" +dependencies = [ + "anyhow", + "derive_builder", + "rustversion", +] + +[[package]] +name = "vergen-lib" +version = "9.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b34a29ba7e9c59e62f229ae1932fb1b8fb8a6fdcc99215a641913f5f5a59a569" +dependencies = [ + "anyhow", + "derive_builder", + "rustversion", +] + [[package]] name = "version-compare" version = "0.2.1" @@ -6963,7 +8463,7 @@ checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] name = "virtual_terminal" -version = "0.2.0" +version = "0.2.1" dependencies = [ "ciborium", "insta", @@ -7158,6 +8658,19 @@ dependencies = [ "wasmparser", ] +[[package]] +name = "wasm-streams" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + [[package]] name = "wasm-streams" version = "0.5.0" @@ -7285,7 +8798,7 @@ checksum = "7130243a7a5b33c54a444e54842e6a9e133de08b5ad7b5861cd8ed9a6a5bc96a" dependencies = [ "webview2-com-macros", "webview2-com-sys", - "windows", + "windows 0.61.3", "windows-core 0.61.2", "windows-implement", "windows-interface", @@ -7309,7 +8822,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "381336cfffd772377d291702245447a5251a2ffa5bad679c99e61bc48bacbf9c" dependencies = [ "thiserror 2.0.18", - "windows", + "windows 0.61.3", "windows-core 0.61.2", ] @@ -7331,7 +8844,7 @@ checksum = "692daff6d93d94e29e4114544ef6d5c942a7ed998b37abdc19b17136ea428eb7" dependencies = [ "getrandom 0.3.4", "mac_address", - "sha2", + "sha2 0.10.9", "thiserror 1.0.69", "uuid", ] @@ -7395,6 +8908,12 @@ dependencies = [ "wasite", ] +[[package]] +name = "widestring" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72069c3113ab32ab29e5584db3c6ec55d416895e60715417b5b883a357c3e471" + [[package]] name = "winapi" version = "0.3.9" @@ -7447,11 +8966,23 @@ version = "0.61.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" dependencies = [ - "windows-collections", + "windows-collections 0.2.0", "windows-core 0.61.2", - "windows-future", + "windows-future 0.2.1", "windows-link 0.1.3", - "windows-numerics", + "windows-numerics 0.2.0", +] + +[[package]] +name = "windows" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580" +dependencies = [ + "windows-collections 0.3.2", + "windows-core 0.62.2", + "windows-future 0.3.2", + "windows-numerics 0.3.1", ] [[package]] @@ -7463,6 +8994,15 @@ dependencies = [ "windows-core 0.61.2", ] +[[package]] +name = "windows-collections" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610" +dependencies = [ + "windows-core 0.62.2", +] + [[package]] name = "windows-core" version = "0.61.2" @@ -7497,7 +9037,18 @@ checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" dependencies = [ "windows-core 0.61.2", "windows-link 0.1.3", - "windows-threading", + "windows-threading 0.1.0", +] + +[[package]] +name = "windows-future" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb" +dependencies = [ + "windows-core 0.62.2", + "windows-link 0.2.1", + "windows-threading 0.2.1", ] [[package]] @@ -7544,6 +9095,27 @@ dependencies = [ "windows-link 0.1.3", ] +[[package]] +name = "windows-numerics" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26" +dependencies = [ + "windows-core 0.62.2", + "windows-link 0.2.1", +] + +[[package]] +name = "windows-registry" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" +dependencies = [ + "windows-link 0.2.1", + "windows-result 0.4.1", + "windows-strings 0.5.1", +] + [[package]] name = "windows-result" version = "0.3.4" @@ -7706,6 +9278,15 @@ dependencies = [ "windows-link 0.1.3", ] +[[package]] +name = "windows-threading" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37" +dependencies = [ + "windows-link 0.2.1", +] + [[package]] name = "windows-version" version = "0.1.7" @@ -7913,6 +9494,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "winnow" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09dac053f1cd375980747450bfc7250c264eaae0583872e845c0c7cd578872b5" + [[package]] name = "winreg" version = "0.10.1" @@ -8020,6 +9607,21 @@ dependencies = [ "wasmparser", ] +[[package]] +name = "wmi" +version = "0.18.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c81b85c57a57500e56669586496bf2abd5cf082b9d32995251185d105208b64" +dependencies = [ + "chrono", + "futures", + "log", + "serde", + "thiserror 2.0.18", + "windows 0.62.2", + "windows-core 0.62.2", +] + [[package]] name = "workshop" version = "0.45.1" @@ -8143,7 +9745,7 @@ dependencies = [ "once_cell", "percent-encoding", "raw-window-handle", - "sha2", + "sha2 0.10.9", "soup3", "tao-macros", "thiserror 2.0.18", @@ -8151,12 +9753,31 @@ dependencies = [ "webkit2gtk", "webkit2gtk-sys", "webview2-com", - "windows", + "windows 0.61.3", "windows-core 0.61.2", "windows-version", "x11-dl", ] +[[package]] +name = "ws_stream_wasm" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c173014acad22e83f16403ee360115b38846fe754e735c5d9d3803fe70c6abc" +dependencies = [ + "async_io_stream", + "futures", + "js-sys", + "log", + "pharos", + "rustc_version", + "send_wrapper", + "thiserror 2.0.18", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + [[package]] name = "x11" version = "2.21.0" @@ -8178,6 +9799,38 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "x11rb" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9993aa5be5a26815fe2c3eacfc1fde061fc1a1f094bf1ad2a18bf9c495dd7414" +dependencies = [ + "gethostname", + "rustix", + "x11rb-protocol", +] + +[[package]] +name = "x11rb-protocol" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea6fc2961e4ef194dcbfe56bb845534d0dc8098940c7e5c012a258bfec6701bd" + +[[package]] +name = "xml-rs" +version = "0.8.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ae8337f8a065cfc972643663ea4279e04e7256de865aa66fe25cec5fb912d3f" + +[[package]] +name = "xmltree" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7d8a75eaf6557bb84a65ace8609883db44a29951042ada9b393151532e41fcb" +dependencies = [ + "xml-rs", +] + [[package]] name = "yansi" version = "1.0.1" @@ -8207,6 +9860,12 @@ dependencies = [ "synstructure", ] +[[package]] +name = "z32" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2164e798d9e3d84ee2c91139ace54638059a3b23e361f5c11781c2c6459bde0f" + [[package]] name = "zerocopy" version = "0.8.39" @@ -8253,6 +9912,20 @@ name = "zeroize" version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] [[package]] name = "zerotrie" diff --git a/Cargo.toml b/Cargo.toml index 7aaeb14..cd8e3fb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,7 @@ members = [ "packages/pty_manager", "packages/tty_wrapper", "packages/virtual_terminal", + "packages/mindmeld", ] # workshop_ui/crate requires WORKSHOP_UI_PATH env var pointing to SvelteKit build output. # workshop_desktop requires Tauri system dependencies (WebKit, etc.). @@ -18,6 +19,7 @@ default-members = [ "packages/pty_manager", "packages/tty_wrapper", "packages/virtual_terminal", + "packages/mindmeld", ] resolver = "2" @@ -46,3 +48,6 @@ tower = "0.5" tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["env-filter"] } uuid = { version = "1.13.2", features = ["v4"] } +workshop = { path = "packages/workshop" } +pty_manager = { path = "packages/pty_manager" } +virtual_terminal = { path = "packages/virtual_terminal" } diff --git a/MODULE.bazel b/MODULE.bazel index 4f50050..8b3433b 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -278,6 +278,19 @@ crate_index.spec( package = "ratatui", version = "0.30", ) +crate_index.spec( + package = "iroh", + version = "0.97", +) +crate_index.spec( + package = "iroh-tickets", + version = "0.4", +) +crate_index.spec( + default_features = False, + package = "arboard", + version = "3", +) # File / directory utilities crate_index.spec( diff --git a/packages/mindmeld/BUILD.bazel b/packages/mindmeld/BUILD.bazel new file mode 100644 index 0000000..b039b78 --- /dev/null +++ b/packages/mindmeld/BUILD.bazel @@ -0,0 +1,30 @@ +load("@rules_rust//rust:defs.bzl", "rust_binary", "rust_test") + +package(default_visibility = ["//visibility:public"]) + +rust_binary( + name = "meld", + srcs = glob(["src/**/*.rs"]), + edition = "2024", + deps = [ + "//packages/pty_manager", + "//packages/virtual_terminal", + "@crate_index//:anyhow", + "@crate_index//:arboard", + "@crate_index//:clap", + "@crate_index//:iroh", + "@crate_index//:iroh-tickets", + "@crate_index//:ratatui", + "@crate_index//:serde", + "@crate_index//:tokio", + "@crate_index//:toml", + "@crate_index//:tracing", + "@crate_index//:tracing-subscriber", + "@crate_index//:uuid", + ], +) + +rust_test( + name = "meld_test", + crate = ":meld", +) diff --git a/packages/mindmeld/Cargo.toml b/packages/mindmeld/Cargo.toml new file mode 100644 index 0000000..d3e932f --- /dev/null +++ b/packages/mindmeld/Cargo.toml @@ -0,0 +1,30 @@ +[package] +name = "mindmeld" +version = "0.1.0" +edition = "2024" +description = "Peer-to-peer terminal sharing over iroh. Host a shell, share a ticket, pair-program with turn-based edit control." +license = "Apache-2.0" +repository = "https://github.com/empathic/workshop" +readme = "README.md" +keywords = ["terminal", "iroh", "p2p", "pair-programming", "tmux"] +categories = ["command-line-utilities", "network-programming", "development-tools"] + +[[bin]] +name = "meld" +path = "src/main.rs" + +[dependencies] +pty_manager = { workspace = true } +virtual_terminal = { workspace = true } +iroh = "0.97" +iroh-tickets = "0.4" +ratatui = "0.30" +arboard = { version = "3", default-features = false } +serde = { workspace = true } +toml = "1" +anyhow = { workspace = true } +clap = { workspace = true, features = ["derive"] } +tokio = { workspace = true, features = ["full"] } +tracing = { workspace = true } +tracing-subscriber = { workspace = true } +uuid = { workspace = true } diff --git a/packages/mindmeld/README.md b/packages/mindmeld/README.md new file mode 100644 index 0000000..024df57 --- /dev/null +++ b/packages/mindmeld/README.md @@ -0,0 +1,74 @@ +# mindmeld + +Peer-to-peer terminal sharing over [iroh](https://iroh.computer). One person hosts a shell; others connect read-only and can request edit access. No relay servers to run, no accounts, no ports to open — connections are established directly between peers via iroh's QUIC transport. + +## Install + +```sh +cargo install mindmeld +``` + +This installs the `meld` binary. + +## Usage + +**Host a session** (shares your `$SHELL` by default; runs the given command otherwise): + +```sh +meld host # shares $SHELL +meld host claude # shares a specific command +``` + +On startup, the join command is copied to your clipboard: + +``` +meld join +``` + +Send that to whoever should join. + +## Keybindings + +### Host + +| Key | Action | +| -------------- | ----------------------------------------------- | +| F9 | Accept a pending edit request / revoke the turn | +| F10 | Deny a pending edit request | +| Shift+PageUp | Scroll back | +| Shift+PageDown | Scroll forward | + +While no viewer holds the turn, the host types into the shared shell normally. + +### Viewer + +| Key | Action | +| -------- | ------------------------------------------------ | +| q | Quit | +| F9 | Request edit access / cancel request / release | +| PageUp | Scroll back | +| PageDown | Scroll forward | + +Once the host grants the turn, the viewer's keystrokes flow into the shared shell. Host F9 revokes. + +## Config + +`~/.meld/config.toml` stores your display name, prompted for on first run: + +```toml +name = "alice" +``` + +## Subprocess integration + +Meld exports two hooks so subprocesses inside the shared shell can attribute actions to the user currently driving the session. + +**`$MELD_SESSION_ID`** is set in the child environment to a per-session UUID. Presence of this variable tells a subprocess it's running inside a meld-shared shell. + +**`~/.meld/sessions/$MELD_SESSION_ID/active_user`** is a plain-text file holding the display name of whoever currently holds the edit turn (the host, or the viewer who was granted by F9). It's rewritten atomically on every turn change and deleted when the session ends. + +## How it works + +The host spawns a PTY and binds an iroh endpoint with the `meld/term/0` ALPN. Viewers dial the host's ticket, open a bidirectional QUIC stream, and receive a keyframe replay of the current screen followed by a live stream of PTY output. A single viewer at a time may hold the edit turn; their keystrokes are forwarded to the host's PTY. The PTY is resized to the smallest connected viewport so everyone sees the same layout. + +Wire protocol: `[tag: u8][len: u32 BE][payload]`. See `src/protocol.rs`. diff --git a/packages/mindmeld/src/config.rs b/packages/mindmeld/src/config.rs new file mode 100644 index 0000000..5dfaddf --- /dev/null +++ b/packages/mindmeld/src/config.rs @@ -0,0 +1,86 @@ +use std::path::PathBuf; + +use anyhow::Result; +use ratatui::crossterm::event::{self, Event, KeyCode}; +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, Default)] +pub struct Config { + #[serde(default)] + pub name: String, +} + +fn config_path() -> PathBuf { + let home = std::env::var("HOME").unwrap_or_else(|_| ".".into()); + PathBuf::from(home).join(".meld").join("config.toml") +} + +impl Config { + pub fn load() -> Option { + let content = std::fs::read_to_string(config_path()).ok()?; + let config: Config = toml::from_str(&content).ok()?; + if config.name.is_empty() { + None + } else { + Some(config) + } + } + + pub fn save(&self) -> Result<()> { + let path = config_path(); + if let Some(parent) = path.parent() { + std::fs::create_dir_all(parent)?; + } + std::fs::write(&path, toml::to_string_pretty(self)?)?; + Ok(()) + } +} + +pub fn ensure_name(terminal: &mut ratatui::DefaultTerminal) -> Result { + let default = Config::load() + .map(|c| c.name) + .unwrap_or_else(|| std::env::var("USER").unwrap_or_default()); + let mut input = default; + let mut cursor = input.len(); + + loop { + let prompt = format!("enter your name: {}", &input); + terminal.draw(|frame| { + let area = frame.area(); + let status_area = + ratatui::prelude::Rect::new(area.x, area.bottom().saturating_sub(1), area.width, 1); + frame.render_widget( + ratatui::widgets::Paragraph::new(crate::status_line(&prompt)), + status_area, + ); + let cursor_x = status_area.x + + "(meld) ".len() as u16 + + "enter your name: ".len() as u16 + + cursor as u16; + frame.set_cursor_position((cursor_x, status_area.y)); + })?; + + if let Event::Key(key) = event::read()? { + match key.code { + KeyCode::Enter if !input.is_empty() => { + let config = Config { + name: input.clone(), + }; + config.save()?; + return Ok(input); + } + KeyCode::Char(c) => { + input.insert(cursor, c); + cursor += 1; + } + KeyCode::Backspace if cursor > 0 => { + cursor -= 1; + input.remove(cursor); + } + KeyCode::Left if cursor > 0 => cursor -= 1, + KeyCode::Right if cursor < input.len() => cursor += 1, + _ => {} + } + } + } +} diff --git a/packages/mindmeld/src/host.rs b/packages/mindmeld/src/host.rs new file mode 100644 index 0000000..e983fb6 --- /dev/null +++ b/packages/mindmeld/src/host.rs @@ -0,0 +1,818 @@ +use std::collections::HashMap; +use std::sync::Arc; +use std::sync::atomic::{AtomicUsize, Ordering}; + +use anyhow::{Context, Result}; +use iroh::Endpoint; +use iroh::endpoint::presets; +use iroh_tickets::endpoint::EndpointTicket; +use ratatui::crossterm::event::{self, Event, KeyCode, KeyModifiers, MouseEventKind}; +use ratatui::prelude::*; +use ratatui::widgets::Paragraph; +use tokio::sync::{broadcast, mpsc, watch}; +use tracing::{info, warn}; + +use crate::protocol::{self, host as htag, viewer as vtag}; +use crate::session::Session; + +pub const ALPN: &[u8] = b"meld/term/0"; +const SCROLLBACK: usize = 10_000; + +/// Turn state broadcast from host main loop to viewer tasks. +#[derive(Clone, Default, Debug, PartialEq)] +struct TurnState { + holder: Option, + requester: Option, +} + +/// Events from viewer tasks to host main loop. +enum TurnEvent { + ViewerConnected { conn_id: String, name: String }, + ViewerDisconnected { conn_id: String }, + TurnRequested { conn_id: String }, + TurnReleased { conn_id: String }, +} + +/// Side-effects emitted by `HostState` transitions. +#[derive(Debug, PartialEq)] +enum Effect { + BroadcastTurn, + BroadcastDenied(String), + UpdateActiveUser, +} + +/// Authoritative turn + viewer state. All methods are pure: they mutate +/// the struct and return the side-effects the caller must perform. +struct HostState { + turn: TurnState, + viewer_names: HashMap, + host_name: String, + session_id: String, +} + +impl HostState { + fn new(host_name: String, session_id: String) -> Self { + Self { + turn: TurnState::default(), + viewer_names: HashMap::new(), + host_name, + session_id, + } + } + + /// F9: promote requester → holder, or revoke current holder. + fn accept_or_revoke(&mut self) -> Vec { + if self.turn.requester.is_some() { + self.turn.holder = self.turn.requester.take(); + vec![Effect::BroadcastTurn, Effect::UpdateActiveUser] + } else if self.turn.holder.is_some() { + self.turn.holder = None; + vec![Effect::BroadcastTurn, Effect::UpdateActiveUser] + } else { + vec![] + } + } + + /// F10: deny the current requester. + fn deny(&mut self) -> Vec { + if let Some(req) = self.turn.requester.take() { + vec![Effect::BroadcastDenied(req), Effect::BroadcastTurn] + } else { + vec![] + } + } + + fn viewer_connected(&mut self, conn_id: String, name: String) { + self.viewer_names.insert(conn_id, name); + } + + fn viewer_disconnected(&mut self, conn_id: &str) { + self.viewer_names.remove(conn_id); + } + + /// A viewer requests the turn. Denied if anyone already holds or is queued. + fn turn_requested(&mut self, conn_id: String) -> Vec { + if self.turn.holder.is_none() && self.turn.requester.is_none() { + self.turn.requester = Some(conn_id); + vec![Effect::BroadcastTurn] + } else { + vec![Effect::BroadcastDenied(conn_id)] + } + } + + /// A viewer released the turn (explicit release, or connection dropped). + fn turn_released(&mut self, conn_id: &str) -> Vec { + let mut effects = vec![]; + if self.turn.holder.as_deref() == Some(conn_id) { + self.turn.holder = None; + effects.push(Effect::BroadcastTurn); + effects.push(Effect::UpdateActiveUser); + } + if self.turn.requester.as_deref() == Some(conn_id) { + self.turn.requester = None; + } + effects + } + + /// Name of whoever currently drives the session (for `active_user` and UI). + fn active_user(&self) -> &str { + match &self.turn.holder { + Some(id) => self + .viewer_names + .get(id) + .map(String::as_str) + .unwrap_or(&self.host_name), + None => &self.host_name, + } + } +} + +fn apply_effects( + state: &HostState, + effects: Vec, + turn_tx: &watch::Sender, + denied_tx: &broadcast::Sender, +) { + for eff in effects { + match eff { + Effect::BroadcastTurn => { + let _ = turn_tx.send(state.turn.clone()); + } + Effect::BroadcastDenied(id) => { + let _ = denied_tx.send(id); + } + Effect::UpdateActiveUser => { + crate::session_dir::write_active_user(&state.session_id, state.active_user()); + } + } + } +} + +pub async fn run(command: Vec) -> Result<()> { + let (cmd, args) = resolve_command(command); + let cwd = std::env::current_dir()?.to_string_lossy().to_string(); + + let mut terminal = ratatui::init(); + ratatui::crossterm::execute!( + std::io::stdout(), + ratatui::crossterm::event::EnableMouseCapture + )?; + + let cleanup = || { + ratatui::crossterm::execute!( + std::io::stdout(), + ratatui::crossterm::event::DisableMouseCapture + ) + .ok(); + ratatui::restore(); + }; + + let host_name = crate::config::ensure_name(&mut terminal)?; + + crate::draw_message(&mut terminal, "starting...")?; + + let endpoint = Endpoint::builder(presets::N0) + .alpns(vec![ALPN.to_vec()]) + .bind() + .await + .context("failed to bind iroh endpoint")?; + endpoint.online().await; + + let ticket = EndpointTicket::new(endpoint.addr()); + let view_cmd = format!("meld join {ticket}"); + let copied = arboard::Clipboard::new() + .and_then(|mut cb| cb.set_text(&view_cmd)) + .is_ok(); + + let (mut cols, mut rows) = ratatui::crossterm::terminal::size()?; + let mut pty_rows = rows.saturating_sub(1).max(1); + + let session_id = uuid::Uuid::new_v4().to_string(); + crate::session_dir::write_active_user(&session_id, &host_name); + + let session = Session::spawn(&cmd, &args, &cwd, pty_rows, cols, SCROLLBACK, &session_id) + .context("failed to spawn session")?; + + let mut output_rx = session.subscribe_output().await?; + let mut dims_rx = session.subscribe_dims(); + session + .update_viewport_and_resize("host", pty_rows, cols) + .await?; + + let (eff_rows, eff_cols) = *dims_rx.borrow(); + let mut vt = + virtual_terminal::VirtualTerminal::new(eff_rows, eff_cols, 1024 * 1024, SCROLLBACK); + let mut scroll_offset: usize = 0; + + // Crossterm event reader thread + let (event_tx, mut event_rx) = mpsc::channel::(64); + std::thread::spawn(move || { + while let Ok(ev) = event::read() { + if event_tx.blocking_send(ev).is_err() { + break; + } + } + }); + + let viewer_count = Arc::new(AtomicUsize::new(0)); + let (turn_tx, turn_rx) = watch::channel(TurnState::default()); + let (denied_tx, _) = broadcast::channel::(16); + let (event_notify_tx, mut event_notify_rx) = mpsc::channel::(16); + let vc = viewer_count.clone(); + let etx = event_notify_tx.clone(); + let viewer_session = session.clone(); + let viewer_endpoint = endpoint.clone(); + let viewer_turn_rx = turn_rx.clone(); + let viewer_dims_rx = dims_rx.clone(); + let viewer_denied_tx = denied_tx.clone(); + tokio::spawn(async move { + accept_viewers( + viewer_endpoint, + viewer_session, + vc, + etx, + viewer_turn_rx, + viewer_dims_rx, + viewer_denied_tx, + ) + .await; + }); + drop(event_notify_tx); + + let mut state = HostState::new(host_name, session_id); + let banner_until = if copied { + Some(tokio::time::Instant::now() + std::time::Duration::from_secs(5)) + } else { + None + }; + + loop { + if scroll_offset > 0 { + vt.screen_mut().set_scrollback(scroll_offset); + scroll_offset = vt.screen().scrollback(); + } + + let n = viewer_count.load(Ordering::Relaxed); + let (eff_r, eff_c) = *dims_rx.borrow(); + let status = build_status(n, &state, scroll_offset, pty_rows, cols, eff_r, eff_c); + let show_banner = banner_until.is_some_and(|t| tokio::time::Instant::now() < t); + let screen = vt.screen(); + let cursor_pos = screen.cursor_position(); + let hide_cursor = screen.hide_cursor(); + + terminal.draw(|frame| { + let [content, status_area] = + Layout::vertical([Constraint::Min(1), Constraint::Length(1)]).areas(frame.area()); + + frame.render_widget(crate::PtyWidget { screen }, content); + frame.render_widget(Paragraph::new(status.clone()), status_area); + + if show_banner { + let msg = " viewer command copied to clipboard "; + let w = msg.len() as u16; + let x = content.right().saturating_sub(w); + let banner_area = Rect::new(x, content.y, w, 1); + frame.render_widget( + Paragraph::new(msg).style(Style::default().dim()), + banner_area, + ); + } + + if scroll_offset == 0 && !hide_cursor { + let (row, col) = cursor_pos; + frame.set_cursor_position((content.x + col, content.y + row)); + } + })?; + + if scroll_offset > 0 { + vt.screen_mut().set_scrollback(0); + } + + tokio::select! { + result = output_rx.recv() => { + match result { + Ok(output) => { + vt.process_output(&output); + drain_output(&mut output_rx, &mut vt); + scroll_offset = 0; + } + Err(broadcast::error::RecvError::Lagged(_)) => {} + Err(broadcast::error::RecvError::Closed) => break, + } + } + Some(ev) = event_rx.recv() => { + match ev { + Event::Key(key) => { + let shift = key.modifiers.contains(KeyModifiers::SHIFT); + if shift && key.code == KeyCode::PageUp { + scroll_offset += pty_rows as usize / 2; + } else if shift && key.code == KeyCode::PageDown { + scroll_offset = scroll_offset.saturating_sub(pty_rows as usize / 2); + } else if key.code == KeyCode::F(9) { + let effects = state.accept_or_revoke(); + apply_effects(&state, effects, &turn_tx, &denied_tx); + } else if key.code == KeyCode::F(10) { + let effects = state.deny(); + apply_effects(&state, effects, &turn_tx, &denied_tx); + } else if state.turn.holder.is_none() + && let Some(bytes) = crate::key_to_bytes(&key) + { + let text = String::from_utf8_lossy(&bytes); + let _ = session.write_input(&text).await; + scroll_offset = 0; + } + } + Event::Mouse(mouse) => match mouse.kind { + MouseEventKind::ScrollUp => scroll_offset += 3, + MouseEventKind::ScrollDown => { + scroll_offset = scroll_offset.saturating_sub(3); + } + _ => {} + }, + Event::Resize(new_cols, new_rows) => { + cols = new_cols; + rows = new_rows; + pty_rows = rows.saturating_sub(1).max(1); + let _ = session.update_viewport_and_resize("host", pty_rows, cols).await; + scroll_offset = 0; + } + _ => {} + } + } + Ok(()) = dims_rx.changed() => { + let (new_r, new_c) = *dims_rx.borrow(); + let (cur_r, cur_c) = vt.screen().size(); + if new_r != cur_r || new_c != cur_c { + vt.resize(new_r, new_c); + scroll_offset = 0; + } + } + Some(event) = event_notify_rx.recv() => { + match event { + TurnEvent::ViewerConnected { conn_id, name } => { + state.viewer_connected(conn_id, name); + } + TurnEvent::ViewerDisconnected { conn_id } => { + state.viewer_disconnected(&conn_id); + } + TurnEvent::TurnRequested { conn_id } => { + let effects = state.turn_requested(conn_id); + apply_effects(&state, effects, &turn_tx, &denied_tx); + } + TurnEvent::TurnReleased { conn_id } => { + let effects = state.turn_released(&conn_id); + apply_effects(&state, effects, &turn_tx, &denied_tx); + } + } + } + } + } + + cleanup(); + let _ = session.stop().await; + endpoint.close().await; + crate::session_dir::cleanup_session(&state.session_id); + Ok(()) +} + +fn build_status( + viewers: usize, + state: &HostState, + scroll_offset: usize, + local_rows: u16, + local_cols: u16, + eff_rows: u16, + eff_cols: u16, +) -> Line<'static> { + if scroll_offset > 0 { + return crate::status_line(&format!("↑ {} lines — type to return", scroll_offset)); + } + let dims_note = if eff_rows != local_rows || eff_cols != local_cols { + format!(" · {}×{}", eff_cols, eff_rows) + } else { + String::new() + }; + let display_name = |conn_id: &str| -> String { + state + .viewer_names + .get(conn_id) + .cloned() + .unwrap_or_else(|| conn_id[..8.min(conn_id.len())].to_string()) + }; + if let Some(ref id) = state.turn.requester { + return Line::from(vec![ + crate::meld_prefix(), + crate::fg( + format!( + "{} requesting edit · F9 accept · F10 deny", + display_name(id) + ), + Color::White, + ), + crate::dim(dims_note.clone()), + ]); + } + let msg = if let Some(ref id) = state.turn.holder { + format!("{} editing · F9 revoke", display_name(id)) + } else { + format!( + "hosting [{} viewer{}]", + viewers, + if viewers == 1 { "" } else { "s" } + ) + }; + crate::status_line(&format!("{msg}{dims_note}")) +} + +fn drain_output(rx: &mut broadcast::Receiver>, vt: &mut virtual_terminal::VirtualTerminal) { + loop { + match rx.try_recv() { + Ok(output) => vt.process_output(&output), + Err(broadcast::error::TryRecvError::Lagged(_)) => {} + _ => break, + } + } +} + +async fn accept_viewers( + endpoint: Endpoint, + session: Session, + viewer_count: Arc, + event_tx: mpsc::Sender, + turn_rx: watch::Receiver, + dims_rx: watch::Receiver<(u16, u16)>, + denied_tx: broadcast::Sender, +) { + while let Some(incoming) = endpoint.accept().await { + let conn = match incoming.await { + Ok(conn) => conn, + Err(e) => { + warn!("failed to accept connection: {e}"); + continue; + } + }; + let s = session.clone(); + let vc = viewer_count.clone(); + let etx = event_tx.clone(); + let trx = turn_rx.clone(); + let drx = dims_rx.clone(); + let denied_rx = denied_tx.subscribe(); + tokio::spawn(async move { + if let Err(e) = handle_viewer(conn, s, vc, etx, trx, drx, denied_rx).await { + info!("viewer disconnected: {e}"); + } + }); + } +} + +async fn handle_viewer( + conn: iroh::endpoint::Connection, + session: Session, + viewer_count: Arc, + event_tx: mpsc::Sender, + turn_rx: watch::Receiver, + dims_rx: watch::Receiver<(u16, u16)>, + denied_rx: broadcast::Receiver, +) -> Result<()> { + let conn_id = conn.remote_id().to_string(); + viewer_count.fetch_add(1, Ordering::Relaxed); + + let result = serve_viewer( + &conn, &session, &conn_id, &event_tx, turn_rx, dims_rx, denied_rx, + ) + .await; + + let _ = event_tx + .send(TurnEvent::TurnReleased { + conn_id: conn_id.clone(), + }) + .await; + viewer_count.fetch_sub(1, Ordering::Relaxed); + let _ = event_tx + .send(TurnEvent::ViewerDisconnected { + conn_id: conn_id.clone(), + }) + .await; + let _ = session.remove_client_and_resize(&conn_id).await; + info!("viewer disconnected: {}", &conn_id[..8]); + result +} + +async fn serve_viewer( + conn: &iroh::endpoint::Connection, + session: &Session, + conn_id: &str, + event_tx: &mpsc::Sender, + mut turn_rx: watch::Receiver, + mut dims_rx: watch::Receiver<(u16, u16)>, + mut denied_rx: broadcast::Receiver, +) -> Result<()> { + let (mut send, mut recv) = conn.accept_bi().await?; + + // Read Hello (viewer name) + let (tag, payload) = protocol::read_msg(&mut recv).await?; + anyhow::ensure!(tag == vtag::HELLO, "expected hello"); + let viewer_name = String::from_utf8_lossy(&payload).to_string(); + info!("viewer connected: {viewer_name}"); + + let _ = event_tx + .send(TurnEvent::ViewerConnected { + conn_id: conn_id.to_string(), + name: viewer_name, + }) + .await; + + // Read viewport + let (tag, payload) = protocol::read_msg(&mut recv).await?; + anyhow::ensure!( + tag == vtag::VIEWPORT && payload.len() == 4, + "expected viewport" + ); + let rows = u16::from_be_bytes([payload[0], payload[1]]).max(1); + let cols = u16::from_be_bytes([payload[2], payload[3]]).max(1); + + session + .update_viewport_and_resize(conn_id, rows, cols) + .await?; + + // Send replay + let replay = session.get_replay(rows).await; + protocol::write_msg(&mut send, htag::OUTPUT, &replay).await?; + + let mut output_rx = session.subscribe_output().await?; + let mut was_holder = false; + + loop { + tokio::select! { + result = output_rx.recv() => { + match result { + Ok(output) => { + protocol::write_msg(&mut send, htag::OUTPUT, &output).await?; + } + Err(broadcast::error::RecvError::Lagged(n)) => { + warn!("viewer {} lagged by {n}", &conn_id[..8]); + } + Err(broadcast::error::RecvError::Closed) => break, + } + } + result = protocol::read_msg(&mut recv) => { + let (tag, payload) = result?; + match tag { + vtag::VIEWPORT => { + if payload.len() == 4 { + let rows = u16::from_be_bytes([payload[0], payload[1]]).max(1); + let cols = u16::from_be_bytes([payload[2], payload[3]]).max(1); + let _ = session.update_viewport_and_resize(conn_id, rows, cols).await; + } + } + vtag::REQUEST_TURN => { + let _ = event_tx.send(TurnEvent::TurnRequested { + conn_id: conn_id.to_string(), + }).await; + } + vtag::INPUT => { + let state = turn_rx.borrow().clone(); + if state.holder.as_deref() == Some(conn_id) { + let text = String::from_utf8_lossy(&payload); + let _ = session.write_input(&text).await; + } + } + vtag::RELEASE_TURN => { + let _ = event_tx + .send(TurnEvent::TurnReleased { + conn_id: conn_id.to_string(), + }) + .await; + } + vtag::GOODBYE => break, + _ => {} + } + } + Ok(()) = dims_rx.changed() => { + let (r, c) = *dims_rx.borrow(); + let payload = [(r >> 8) as u8, r as u8, (c >> 8) as u8, c as u8]; + protocol::write_msg(&mut send, htag::DIMS_CHANGED, &payload).await?; + } + Ok(()) = turn_rx.changed() => { + let is_holder = turn_rx.borrow().holder.as_deref() == Some(conn_id); + if is_holder && !was_holder { + protocol::write_msg(&mut send, htag::TURN_GRANTED, &[]).await?; + was_holder = true; + } else if !is_holder && was_holder { + protocol::write_msg(&mut send, htag::TURN_REVOKED, &[]).await?; + was_holder = false; + } + } + Ok(denied_id) = denied_rx.recv() => { + if denied_id == conn_id { + protocol::write_msg(&mut send, htag::TURN_DENIED, &[]).await?; + } + } + } + } + + Ok(()) +} + +fn resolve_command(command: Vec) -> (String, Vec) { + if command.is_empty() { + let shell = std::env::var("SHELL").unwrap_or_else(|_| "/bin/bash".into()); + (shell, vec![]) + } else { + let cmd = command[0].clone(); + let args = command[1..].to_vec(); + (cmd, args) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn state() -> HostState { + HostState::new("host".into(), "sess".into()) + } + + #[test] + fn initial_state_is_empty() { + let s = state(); + assert_eq!(s.turn, TurnState::default()); + assert!(s.viewer_names.is_empty()); + assert_eq!(s.active_user(), "host"); + } + + #[test] + fn turn_requested_sets_requester_when_idle() { + let mut s = state(); + let effects = s.turn_requested("alice".into()); + assert_eq!(s.turn.requester.as_deref(), Some("alice")); + assert_eq!(effects, vec![Effect::BroadcastTurn]); + } + + #[test] + fn turn_requested_denied_when_holder_exists() { + let mut s = state(); + s.turn.holder = Some("bob".into()); + let effects = s.turn_requested("alice".into()); + assert_eq!(s.turn.requester, None); + assert_eq!(effects, vec![Effect::BroadcastDenied("alice".into())]); + } + + #[test] + fn turn_requested_denied_when_requester_already_queued() { + let mut s = state(); + s.turn.requester = Some("bob".into()); + let effects = s.turn_requested("alice".into()); + assert_eq!(s.turn.requester.as_deref(), Some("bob")); + assert_eq!(effects, vec![Effect::BroadcastDenied("alice".into())]); + } + + #[test] + fn f9_promotes_requester_to_holder() { + let mut s = state(); + s.viewer_connected("alice".into(), "Alice".into()); + s.turn_requested("alice".into()); + let effects = s.accept_or_revoke(); + assert_eq!(s.turn.holder.as_deref(), Some("alice")); + assert_eq!(s.turn.requester, None); + assert_eq!( + effects, + vec![Effect::BroadcastTurn, Effect::UpdateActiveUser] + ); + assert_eq!(s.active_user(), "Alice"); + } + + #[test] + fn f9_revokes_holder_when_no_requester() { + let mut s = state(); + s.viewer_connected("alice".into(), "Alice".into()); + s.turn.holder = Some("alice".into()); + let effects = s.accept_or_revoke(); + assert_eq!(s.turn.holder, None); + assert_eq!( + effects, + vec![Effect::BroadcastTurn, Effect::UpdateActiveUser] + ); + assert_eq!(s.active_user(), "host"); + } + + #[test] + fn f9_is_noop_when_idle() { + let mut s = state(); + let effects = s.accept_or_revoke(); + assert!(effects.is_empty()); + assert_eq!(s.turn, TurnState::default()); + } + + #[test] + fn f10_denies_requester_and_broadcasts_identity() { + let mut s = state(); + s.turn_requested("alice".into()); + let effects = s.deny(); + assert_eq!(s.turn.requester, None); + assert_eq!( + effects, + vec![ + Effect::BroadcastDenied("alice".into()), + Effect::BroadcastTurn, + ], + ); + } + + #[test] + fn f10_is_noop_when_no_requester() { + let mut s = state(); + let effects = s.deny(); + assert!(effects.is_empty()); + } + + #[test] + fn f10_does_not_affect_holder() { + let mut s = state(); + s.turn.holder = Some("alice".into()); + let effects = s.deny(); + assert!(effects.is_empty()); + assert_eq!(s.turn.holder.as_deref(), Some("alice")); + } + + #[test] + fn turn_released_clears_holder_and_broadcasts() { + let mut s = state(); + s.viewer_connected("alice".into(), "Alice".into()); + s.turn.holder = Some("alice".into()); + let effects = s.turn_released("alice"); + assert_eq!(s.turn.holder, None); + assert_eq!( + effects, + vec![Effect::BroadcastTurn, Effect::UpdateActiveUser] + ); + } + + #[test] + fn turn_released_by_requester_clears_silently() { + let mut s = state(); + s.turn.requester = Some("alice".into()); + let effects = s.turn_released("alice"); + // Requester change is not broadcast — the next turn_tx send will reflect it, + // but releasing the queue slot alone is not a turn-state change viewers care about. + assert_eq!(s.turn.requester, None); + assert!(effects.is_empty()); + } + + #[test] + fn turn_released_by_stranger_is_noop() { + let mut s = state(); + s.turn.holder = Some("alice".into()); + let effects = s.turn_released("eve"); + assert!(effects.is_empty()); + assert_eq!(s.turn.holder.as_deref(), Some("alice")); + } + + #[test] + fn turn_released_clears_both_slots_if_same_conn() { + let mut s = state(); + s.turn.holder = Some("alice".into()); + s.turn.requester = Some("alice".into()); + let effects = s.turn_released("alice"); + assert_eq!(s.turn, TurnState::default()); + assert_eq!( + effects, + vec![Effect::BroadcastTurn, Effect::UpdateActiveUser] + ); + } + + #[test] + fn viewer_disconnect_removes_name() { + let mut s = state(); + s.viewer_connected("alice".into(), "Alice".into()); + s.viewer_disconnected("alice"); + assert!(s.viewer_names.is_empty()); + } + + #[test] + fn active_user_falls_back_to_host_if_holder_name_missing() { + // If a viewer held the turn and then disconnected without the main + // loop seeing the release first, the name map might not have them. + let mut s = state(); + s.turn.holder = Some("ghost".into()); + assert_eq!(s.active_user(), "host"); + } + + #[test] + fn request_grant_revoke_round_trip() { + let mut s = state(); + s.viewer_connected("alice".into(), "Alice".into()); + + s.turn_requested("alice".into()); + assert_eq!(s.turn.requester.as_deref(), Some("alice")); + + let _ = s.accept_or_revoke(); + assert_eq!(s.turn.holder.as_deref(), Some("alice")); + assert_eq!(s.turn.requester, None); + assert_eq!(s.active_user(), "Alice"); + + let _ = s.accept_or_revoke(); + assert_eq!(s.turn.holder, None); + assert_eq!(s.active_user(), "host"); + } +} diff --git a/packages/mindmeld/src/main.rs b/packages/mindmeld/src/main.rs new file mode 100644 index 0000000..5d5c285 --- /dev/null +++ b/packages/mindmeld/src/main.rs @@ -0,0 +1,470 @@ +pub mod config; +pub mod host; +pub mod protocol; +pub mod session; +pub mod session_dir; +mod viewer; + +use anyhow::Result; +use clap::{Parser, Subcommand}; +use ratatui::crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; +use ratatui::prelude::*; +use ratatui::widgets::Widget; +use virtual_terminal::{vt100, walk_row}; + +#[derive(Parser)] +#[command(name = "meld", about = "P2P terminal sharing over iroh")] +struct Cli { + #[command(subcommand)] + command: Command, +} + +#[derive(Subcommand)] +enum Command { + /// Share a terminal session + Host { + /// Command to run (default: $SHELL or /bin/bash) + #[arg(trailing_var_arg = true)] + command: Vec, + }, + /// Join a shared terminal session (readonly by default) + Join { + /// iroh ticket from the host + ticket: String, + }, +} + +#[tokio::main] +async fn main() -> Result<()> { + tracing_subscriber::fmt() + .with_env_filter( + tracing_subscriber::EnvFilter::try_from_default_env() + .unwrap_or_else(|_| "off".parse().unwrap()), + ) + .with_writer(std::io::stderr) + .init(); + + let original_hook = std::panic::take_hook(); + std::panic::set_hook(Box::new(move |info| { + ratatui::restore(); + original_hook(info); + })); + + let cli = Cli::parse(); + match cli.command { + Command::Host { command } => host::run(command).await, + Command::Join { ticket } => viewer::run(&ticket).await, + } +} + +// --- Shared widgets --- + +pub struct PtyWidget<'a> { + pub screen: &'a vt100::Screen, +} + +impl Widget for PtyWidget<'_> { + fn render(self, area: Rect, buf: &mut Buffer) { + for row in 0..area.height { + for cell in walk_row(self.screen, row, area.width) { + let mut style = Style::default() + .fg(vt100_color_to_ratatui(cell.fg)) + .bg(vt100_color_to_ratatui(cell.bg)); + let mut modifiers = Modifier::empty(); + if cell.bold { + modifiers |= Modifier::BOLD; + } + if cell.italic { + modifiers |= Modifier::ITALIC; + } + if cell.underline { + modifiers |= Modifier::UNDERLINED; + } + if cell.inverse { + modifiers |= Modifier::REVERSED; + } + style = style.add_modifier(modifiers); + let x = area.x + cell.col; + let y = area.y + row; + if x < area.right() && y < area.bottom() { + buf[(x, y)].set_symbol(cell.contents).set_style(style); + } + } + } + } +} + +fn vt100_color_to_ratatui(color: vt100::Color) -> Color { + match color { + vt100::Color::Default => Color::Reset, + vt100::Color::Idx(n) => Color::Indexed(n), + vt100::Color::Rgb(r, g, b) => Color::Rgb(r, g, b), + } +} + +pub fn meld_prefix() -> Span<'static> { + Span::styled("(meld) ", Style::default().fg(Color::Magenta)) +} + +pub fn dim(text: impl Into) -> Span<'static> { + Span::styled(text.into(), Style::default().dim()) +} + +pub fn fg(text: impl Into, color: Color) -> Span<'static> { + Span::styled(text.into(), Style::default().fg(color)) +} + +pub fn status_line(rest: &str) -> Line<'static> { + Line::from(vec![meld_prefix(), dim(rest)]) +} + +pub fn draw_message(terminal: &mut ratatui::DefaultTerminal, msg: &str) -> anyhow::Result<()> { + use ratatui::widgets::Paragraph; + terminal.draw(|frame| { + let area = frame.area(); + frame.render_widget( + Paragraph::new(status_line(msg)), + Rect::new(area.x, area.bottom().saturating_sub(1), area.width, 1), + ); + })?; + Ok(()) +} + +/// xterm modifier encoding: 1 + shift + alt*2 + ctrl*4 (range 1..=8, where 1 = no mods). +fn modifier_code(m: KeyModifiers) -> u8 { + let mut code = 1; + if m.contains(KeyModifiers::SHIFT) { + code += 1; + } + if m.contains(KeyModifiers::ALT) { + code += 2; + } + if m.contains(KeyModifiers::CONTROL) { + code += 4; + } + code +} + +/// CSI-encoded sequence for keys that support xterm modifier encoding +/// (arrows, Home/End, page/insert/delete, F1-F12). Returns `None` for +/// other keycodes so the caller can handle them (chars, ctrl-letters, etc). +fn csi_key(code: KeyCode, mod_code: u8) -> Option> { + if let Some(letter) = match code { + KeyCode::Up => Some('A'), + KeyCode::Down => Some('B'), + KeyCode::Right => Some('C'), + KeyCode::Left => Some('D'), + KeyCode::Home => Some('H'), + KeyCode::End => Some('F'), + _ => None, + } { + return Some(if mod_code == 1 { + format!("\x1b[{letter}").into_bytes() + } else { + format!("\x1b[1;{mod_code}{letter}").into_bytes() + }); + } + + if let Some(letter) = match code { + KeyCode::F(1) => Some('P'), + KeyCode::F(2) => Some('Q'), + KeyCode::F(3) => Some('R'), + KeyCode::F(4) => Some('S'), + _ => None, + } { + return Some(if mod_code == 1 { + format!("\x1bO{letter}").into_bytes() + } else { + format!("\x1b[1;{mod_code}{letter}").into_bytes() + }); + } + + let num: Option = match code { + KeyCode::Insert => Some(2), + KeyCode::Delete => Some(3), + KeyCode::PageUp => Some(5), + KeyCode::PageDown => Some(6), + KeyCode::F(5) => Some(15), + KeyCode::F(6) => Some(17), + KeyCode::F(7) => Some(18), + KeyCode::F(8) => Some(19), + KeyCode::F(9) => Some(20), + KeyCode::F(10) => Some(21), + KeyCode::F(11) => Some(23), + KeyCode::F(12) => Some(24), + _ => None, + }; + num.map(|n| { + if mod_code == 1 { + format!("\x1b[{n}~").into_bytes() + } else { + format!("\x1b[{n};{mod_code}~").into_bytes() + } + }) +} + +/// Convert a crossterm KeyEvent to the byte sequence a PTY expects. +pub fn key_to_bytes(key: &KeyEvent) -> Option> { + let mod_code = modifier_code(key.modifiers); + if let Some(bytes) = csi_key(key.code, mod_code) { + return Some(bytes); + } + + let ctrl = key.modifiers.contains(KeyModifiers::CONTROL); + let alt = key.modifiers.contains(KeyModifiers::ALT); + let mut bytes = match key.code { + KeyCode::Char(' ') if ctrl => vec![0x00], + KeyCode::Char(c) if ctrl => match c { + 'a'..='z' => vec![(c as u8) - b'a' + 1], + '4'..='7' => vec![(c as u8) - b'4' + 0x1C], + _ => return None, + }, + KeyCode::Char(c) => { + let mut buf = [0u8; 4]; + c.encode_utf8(&mut buf).as_bytes().to_vec() + } + KeyCode::Enter => b"\r".to_vec(), + KeyCode::Backspace => vec![0x7f], + KeyCode::Esc => b"\x1b".to_vec(), + KeyCode::Tab => b"\t".to_vec(), + KeyCode::BackTab => b"\x1b[Z".to_vec(), + _ => return None, + }; + if alt { + bytes.insert(0, 0x1b); + } + Some(bytes) +} + +#[cfg(test)] +mod tests { + use super::*; + use ratatui::crossterm::event::KeyEventKind; + + fn k(code: KeyCode, modifiers: KeyModifiers) -> KeyEvent { + KeyEvent::new_with_kind(code, modifiers, KeyEventKind::Press) + } + + #[test] + fn plain_ascii() { + assert_eq!( + key_to_bytes(&k(KeyCode::Char('a'), KeyModifiers::NONE)), + Some(b"a".to_vec()) + ); + assert_eq!( + key_to_bytes(&k(KeyCode::Char('Z'), KeyModifiers::SHIFT)), + Some(b"Z".to_vec()) + ); + } + + #[test] + fn ctrl_letters() { + for (c, expected) in [('a', 0x01), ('m', 0x0d), ('z', 0x1a)] { + assert_eq!( + key_to_bytes(&k(KeyCode::Char(c), KeyModifiers::CONTROL)), + Some(vec![expected]), + ); + } + } + + #[test] + fn ctrl_space_is_nul() { + assert_eq!( + key_to_bytes(&k(KeyCode::Char(' '), KeyModifiers::CONTROL)), + Some(vec![0x00]), + ); + } + + #[test] + fn ctrl_digit_range() { + // 4/5/6/7 → 0x1C..0x1F (FS/GS/RS/US, the xterm mapping for C-\ C-] C-^ C-_). + assert_eq!( + key_to_bytes(&k(KeyCode::Char('4'), KeyModifiers::CONTROL)), + Some(vec![0x1C]) + ); + assert_eq!( + key_to_bytes(&k(KeyCode::Char('7'), KeyModifiers::CONTROL)), + Some(vec![0x1F]) + ); + } + + #[test] + fn alt_prepends_escape() { + assert_eq!( + key_to_bytes(&k(KeyCode::Char('a'), KeyModifiers::ALT)), + Some(vec![0x1b, b'a']), + ); + } + + #[test] + fn ctrl_alt_composes() { + assert_eq!( + key_to_bytes(&k( + KeyCode::Char('a'), + KeyModifiers::CONTROL | KeyModifiers::ALT + )), + Some(vec![0x1b, 0x01]), + ); + } + + #[test] + fn arrows() { + assert_eq!( + key_to_bytes(&k(KeyCode::Up, KeyModifiers::NONE)), + Some(b"\x1b[A".to_vec()) + ); + assert_eq!( + key_to_bytes(&k(KeyCode::Down, KeyModifiers::NONE)), + Some(b"\x1b[B".to_vec()) + ); + assert_eq!( + key_to_bytes(&k(KeyCode::Right, KeyModifiers::NONE)), + Some(b"\x1b[C".to_vec()) + ); + assert_eq!( + key_to_bytes(&k(KeyCode::Left, KeyModifiers::NONE)), + Some(b"\x1b[D".to_vec()) + ); + } + + #[test] + fn function_keys() { + assert_eq!( + key_to_bytes(&k(KeyCode::F(1), KeyModifiers::NONE)), + Some(b"\x1bOP".to_vec()) + ); + assert_eq!( + key_to_bytes(&k(KeyCode::F(5), KeyModifiers::NONE)), + Some(b"\x1b[15~".to_vec()) + ); + assert_eq!( + key_to_bytes(&k(KeyCode::F(12), KeyModifiers::NONE)), + Some(b"\x1b[24~".to_vec()) + ); + } + + #[test] + fn enter_backspace_esc_tab() { + assert_eq!( + key_to_bytes(&k(KeyCode::Enter, KeyModifiers::NONE)), + Some(b"\r".to_vec()) + ); + assert_eq!( + key_to_bytes(&k(KeyCode::Backspace, KeyModifiers::NONE)), + Some(vec![0x7f]) + ); + assert_eq!( + key_to_bytes(&k(KeyCode::Esc, KeyModifiers::NONE)), + Some(b"\x1b".to_vec()) + ); + assert_eq!( + key_to_bytes(&k(KeyCode::Tab, KeyModifiers::NONE)), + Some(b"\t".to_vec()) + ); + } + + #[test] + fn modifier_code_table() { + assert_eq!(modifier_code(KeyModifiers::NONE), 1); + assert_eq!(modifier_code(KeyModifiers::SHIFT), 2); + assert_eq!(modifier_code(KeyModifiers::ALT), 3); + assert_eq!(modifier_code(KeyModifiers::SHIFT | KeyModifiers::ALT), 4); + assert_eq!(modifier_code(KeyModifiers::CONTROL), 5); + assert_eq!( + modifier_code(KeyModifiers::CONTROL | KeyModifiers::SHIFT), + 6 + ); + assert_eq!(modifier_code(KeyModifiers::CONTROL | KeyModifiers::ALT), 7); + assert_eq!( + modifier_code(KeyModifiers::CONTROL | KeyModifiers::ALT | KeyModifiers::SHIFT), + 8 + ); + } + + #[test] + fn alt_arrow() { + assert_eq!( + key_to_bytes(&k(KeyCode::Up, KeyModifiers::ALT)), + Some(b"\x1b[1;3A".to_vec()), + ); + } + + #[test] + fn ctrl_arrow() { + assert_eq!( + key_to_bytes(&k(KeyCode::Right, KeyModifiers::CONTROL)), + Some(b"\x1b[1;5C".to_vec()), + ); + } + + #[test] + fn shift_arrow() { + assert_eq!( + key_to_bytes(&k(KeyCode::Left, KeyModifiers::SHIFT)), + Some(b"\x1b[1;2D".to_vec()), + ); + } + + #[test] + fn ctrl_alt_arrow() { + assert_eq!( + key_to_bytes(&k(KeyCode::Down, KeyModifiers::CONTROL | KeyModifiers::ALT)), + Some(b"\x1b[1;7B".to_vec()), + ); + } + + #[test] + fn modified_home_end() { + assert_eq!( + key_to_bytes(&k(KeyCode::Home, KeyModifiers::ALT)), + Some(b"\x1b[1;3H".to_vec()), + ); + assert_eq!( + key_to_bytes(&k(KeyCode::End, KeyModifiers::SHIFT)), + Some(b"\x1b[1;2F".to_vec()), + ); + } + + #[test] + fn modified_f1_through_f4() { + assert_eq!( + key_to_bytes(&k(KeyCode::F(1), KeyModifiers::SHIFT)), + Some(b"\x1b[1;2P".to_vec()), + ); + assert_eq!( + key_to_bytes(&k(KeyCode::F(4), KeyModifiers::CONTROL)), + Some(b"\x1b[1;5S".to_vec()), + ); + } + + #[test] + fn shift_f5() { + assert_eq!( + key_to_bytes(&k(KeyCode::F(5), KeyModifiers::SHIFT)), + Some(b"\x1b[15;2~".to_vec()), + ); + } + + #[test] + fn ctrl_shift_f12() { + assert_eq!( + key_to_bytes(&k( + KeyCode::F(12), + KeyModifiers::CONTROL | KeyModifiers::SHIFT + )), + Some(b"\x1b[24;6~".to_vec()), + ); + } + + #[test] + fn modified_pageup_delete() { + assert_eq!( + key_to_bytes(&k(KeyCode::PageUp, KeyModifiers::CONTROL)), + Some(b"\x1b[5;5~".to_vec()), + ); + assert_eq!( + key_to_bytes(&k(KeyCode::Delete, KeyModifiers::ALT)), + Some(b"\x1b[3;3~".to_vec()), + ); + } +} diff --git a/packages/mindmeld/src/protocol.rs b/packages/mindmeld/src/protocol.rs new file mode 100644 index 0000000..896af85 --- /dev/null +++ b/packages/mindmeld/src/protocol.rs @@ -0,0 +1,88 @@ +use anyhow::Result; +use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt}; + +/// Tags for viewer -> host messages. +pub mod viewer { + pub const VIEWPORT: u8 = 0x01; + pub const REQUEST_TURN: u8 = 0x02; + pub const INPUT: u8 = 0x03; + pub const RELEASE_TURN: u8 = 0x04; + pub const HELLO: u8 = 0x05; + pub const GOODBYE: u8 = 0x06; +} + +/// Tags for host -> viewer messages. +pub mod host { + pub const OUTPUT: u8 = 0x01; + pub const TURN_GRANTED: u8 = 0x02; + pub const TURN_REVOKED: u8 = 0x03; + pub const TURN_DENIED: u8 = 0x04; + pub const DIMS_CHANGED: u8 = 0x05; +} + +/// Frame layout: `[tag: u8][len: u32 BE][payload]`. +pub async fn write_msg(w: &mut W, tag: u8, payload: &[u8]) -> Result<()> { + let len = payload.len() as u32; + let mut header = [0u8; 5]; + header[0] = tag; + header[1..5].copy_from_slice(&len.to_be_bytes()); + w.write_all(&header).await?; + if !payload.is_empty() { + w.write_all(payload).await?; + } + Ok(()) +} + +pub async fn read_msg(r: &mut R) -> Result<(u8, Vec)> { + let mut header = [0u8; 5]; + r.read_exact(&mut header).await?; + let tag = header[0]; + let len = u32::from_be_bytes([header[1], header[2], header[3], header[4]]) as usize; + let mut payload = vec![0u8; len]; + if len > 0 { + r.read_exact(&mut payload).await?; + } + Ok((tag, payload)) +} + +#[cfg(test)] +mod tests { + use super::*; + use tokio::io::duplex; + + async fn roundtrip(tag: u8, payload: Vec) { + let (mut a, mut b) = duplex(1 << 20); + let expected = payload.clone(); + let writer = tokio::spawn(async move { + write_msg(&mut a, tag, &payload).await.unwrap(); + }); + let (got_tag, got_payload) = read_msg(&mut b).await.unwrap(); + writer.await.unwrap(); + assert_eq!(got_tag, tag); + assert_eq!(got_payload, expected); + } + + #[tokio::test] + async fn empty_payload() { + roundtrip(0x42, vec![]).await; + } + + #[tokio::test] + async fn small_payload() { + roundtrip(0x01, b"hello world".to_vec()).await; + } + + #[tokio::test] + async fn payload_above_u16_limit() { + // Regression: would have truncated under the old u16 length header. + let payload = vec![0xABu8; 128 * 1024]; + roundtrip(0x01, payload).await; + } + + #[tokio::test] + async fn preserves_all_tag_values() { + for tag in [0u8, 1, 127, 128, 255] { + roundtrip(tag, vec![tag; 7]).await; + } + } +} diff --git a/packages/mindmeld/src/session.rs b/packages/mindmeld/src/session.rs new file mode 100644 index 0000000..ad3e1ea --- /dev/null +++ b/packages/mindmeld/src/session.rs @@ -0,0 +1,275 @@ +use anyhow::Result; +use pty_manager::{PtyConfig, PtyHandle, PtyOutput}; +use tokio::sync::{broadcast, mpsc, oneshot, watch}; +use virtual_terminal::{ClientType, VirtualTerminal}; + +const MAX_DELTA_BYTES: usize = 1024 * 1024; + +enum Command { + WriteInput { + text: String, + respond_to: oneshot::Sender>, + }, + Resize { + rows: u16, + cols: u16, + respond_to: oneshot::Sender>, + }, + UpdateViewport { + id: String, + rows: u16, + cols: u16, + respond_to: oneshot::Sender>, + }, + RemoveClient { + id: String, + respond_to: oneshot::Sender>, + }, + GetReplay { + client_rows: u16, + respond_to: oneshot::Sender>, + }, + SubscribeOutput { + respond_to: oneshot::Sender>>, + }, + GetPid { + respond_to: oneshot::Sender>, + }, + Stop { + respond_to: oneshot::Sender>, + }, +} + +/// Handle to a running PTY session. Cheap to clone. +#[derive(Clone)] +pub struct Session { + tx: mpsc::Sender, + effective_dims: watch::Receiver<(u16, u16)>, +} + +impl Session { + /// Spawn a new PTY session. + pub fn spawn( + command: &str, + args: &[String], + working_dir: &str, + rows: u16, + cols: u16, + scrollback_lines: usize, + session_id: &str, + ) -> Result { + let config = PtyConfig { + command: command.to_string(), + args: args.to_vec(), + working_dir: Some(working_dir.to_string()), + env: vec![("MELD_SESSION_ID".into(), session_id.to_string())], + rows, + cols, + }; + let pty = pty_manager::pty::PtyActor::spawn(config)?; + let pty_output_rx = pty.subscribe(); + let (output_tx, _) = broadcast::channel::>(64); + let vt = VirtualTerminal::new(rows, cols, MAX_DELTA_BYTES, scrollback_lines); + let (cmd_tx, cmd_rx) = mpsc::channel(32); + let (dims_tx, dims_rx) = watch::channel((rows, cols)); + + tokio::spawn(actor_loop( + pty, + pty_output_rx, + vt, + output_tx, + cmd_rx, + dims_tx, + )); + + Ok(Self { + tx: cmd_tx, + effective_dims: dims_rx, + }) + } + + pub async fn write_input(&self, text: &str) -> Result { + let (tx, rx) = oneshot::channel(); + self.tx + .send(Command::WriteInput { + text: text.to_string(), + respond_to: tx, + }) + .await?; + Ok(rx.await??) + } + + pub async fn subscribe_output(&self) -> Result>> { + let (tx, rx) = oneshot::channel(); + self.tx + .send(Command::SubscribeOutput { respond_to: tx }) + .await?; + Ok(rx.await?) + } + + /// Subscribe to effective PTY dimension changes. + pub fn subscribe_dims(&self) -> watch::Receiver<(u16, u16)> { + self.effective_dims.clone() + } + + pub async fn update_viewport(&self, id: &str, rows: u16, cols: u16) -> Option<(u16, u16)> { + let (tx, rx) = oneshot::channel(); + let _ = self + .tx + .send(Command::UpdateViewport { + id: id.to_string(), + rows, + cols, + respond_to: tx, + }) + .await; + rx.await.ok().flatten() + } + + pub async fn remove_client(&self, id: &str) -> Option<(u16, u16)> { + let (tx, rx) = oneshot::channel(); + let _ = self + .tx + .send(Command::RemoveClient { + id: id.to_string(), + respond_to: tx, + }) + .await; + rx.await.ok().flatten() + } + + /// Update viewport and resize the PTY if effective dims changed. + pub async fn update_viewport_and_resize(&self, id: &str, rows: u16, cols: u16) -> Result<()> { + if let Some((eff_rows, eff_cols)) = self.update_viewport(id, rows, cols).await { + self.resize(eff_rows, eff_cols).await?; + } + Ok(()) + } + + pub async fn remove_client_and_resize(&self, id: &str) -> Result<()> { + if let Some((eff_rows, eff_cols)) = self.remove_client(id).await { + self.resize(eff_rows, eff_cols).await?; + } + Ok(()) + } + + pub async fn get_replay(&self, client_rows: u16) -> Vec { + let (tx, rx) = oneshot::channel(); + let _ = self + .tx + .send(Command::GetReplay { + client_rows, + respond_to: tx, + }) + .await; + rx.await.unwrap_or_default() + } + + pub async fn get_pid(&self) -> Option { + let (tx, rx) = oneshot::channel(); + let _ = self.tx.send(Command::GetPid { respond_to: tx }).await; + rx.await.ok().flatten() + } + + pub async fn stop(&self) -> Result<()> { + let (tx, rx) = oneshot::channel(); + let _ = self.tx.send(Command::Stop { respond_to: tx }).await; + rx.await??; + Ok(()) + } + + async fn resize(&self, rows: u16, cols: u16) -> Result<()> { + let (tx, rx) = oneshot::channel(); + let _ = self + .tx + .send(Command::Resize { + rows, + cols, + respond_to: tx, + }) + .await; + rx.await??; + Ok(()) + } +} + +async fn actor_loop( + pty: PtyHandle, + mut pty_output_rx: broadcast::Receiver, + mut vt: VirtualTerminal, + output_tx: broadcast::Sender>, + mut cmd_rx: mpsc::Receiver, + dims_tx: watch::Sender<(u16, u16)>, +) { + // pty_manager's PtyHandle holds an output_tx clone, so pty_output_rx never + // closes on its own when the child exits — only the reader thread's clone + // drops. And the pty actor only re-checks child exit after processing a + // message, and replies with cached state *before* that check. So the first + // tick after exit still reports running=true; detection needs ~2 ticks — + // keep the interval tight so the exit latency stays imperceptible. + let mut exit_check = tokio::time::interval(std::time::Duration::from_millis(100)); + exit_check.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip); + + loop { + tokio::select! { + result = pty_output_rx.recv() => { + match result { + Ok(pty_output) => { + vt.process_output(&pty_output.data); + let _ = output_tx.send(pty_output.data); + } + Err(broadcast::error::RecvError::Lagged(_)) => {} + Err(broadcast::error::RecvError::Closed) => break, + } + } + _ = exit_check.tick() => { + match pty.state().await { + Ok(state) if !state.running => break, + Err(_) => break, + _ => {} + } + } + cmd = cmd_rx.recv() => { + let Some(cmd) = cmd else { break }; + match cmd { + Command::WriteInput { text, respond_to } => { + let _ = respond_to.send(pty.write_str(&text).await); + } + Command::Resize { rows, cols, respond_to } => { + let result = pty.resize(rows, cols).await; + vt.resize(rows, cols); + let _ = dims_tx.send((rows, cols)); + let _ = respond_to.send(result); + } + Command::UpdateViewport { id, rows, cols, respond_to } => { + let result = vt.update_viewport(&id, rows, cols, ClientType::Terminal); + let _ = respond_to.send(result); + } + Command::RemoveClient { id, respond_to } => { + let result = vt.remove_client(&id); + let _ = respond_to.send(result); + } + Command::GetReplay { client_rows, respond_to } => { + let _ = respond_to.send(vt.replay(client_rows)); + } + Command::SubscribeOutput { respond_to } => { + let _ = respond_to.send(output_tx.subscribe()); + } + Command::GetPid { respond_to } => { + let pid = match pty.state().await { + Ok(state) => state.pid, + Err(_) => None, + }; + let _ = respond_to.send(pid); + } + Command::Stop { respond_to } => { + let result = pty.kill(None).await + .map_err(|e| anyhow::anyhow!(e)); + let _ = respond_to.send(result); + break; + } + } + } + } + } +} diff --git a/packages/mindmeld/src/session_dir.rs b/packages/mindmeld/src/session_dir.rs new file mode 100644 index 0000000..f0d593b --- /dev/null +++ b/packages/mindmeld/src/session_dir.rs @@ -0,0 +1,32 @@ +//! Per-session scratch directory under `~/.meld/sessions//`. +//! +//! The session id is also exported to the PTY child as `MELD_SESSION_ID`, so +//! subprocesses can plug in and attribute actions to whoever currently holds +//! the edit turn by reading `active_user`. + +use std::path::PathBuf; + +fn dir(session_id: &str) -> PathBuf { + let home = std::env::var("HOME").unwrap_or_else(|_| ".".into()); + PathBuf::from(home) + .join(".meld") + .join("sessions") + .join(session_id) +} + +/// Atomically publish the name of the user currently driving the session. +pub fn write_active_user(session_id: &str, name: &str) { + let dir = dir(session_id); + if std::fs::create_dir_all(&dir).is_err() { + return; + } + let path = dir.join("active_user"); + let tmp = dir.join("active_user.tmp"); + if std::fs::write(&tmp, name).is_ok() { + let _ = std::fs::rename(&tmp, &path); + } +} + +pub fn cleanup_session(session_id: &str) { + let _ = std::fs::remove_dir_all(dir(session_id)); +} diff --git a/packages/mindmeld/src/viewer.rs b/packages/mindmeld/src/viewer.rs new file mode 100644 index 0000000..bfbcf23 --- /dev/null +++ b/packages/mindmeld/src/viewer.rs @@ -0,0 +1,297 @@ +use anyhow::{Context, Result}; +use iroh::Endpoint; +use iroh::endpoint::presets; +use iroh_tickets::endpoint::EndpointTicket; +use ratatui::crossterm::event::{self, Event, KeyCode, MouseEventKind}; +use ratatui::prelude::*; +use ratatui::widgets::Paragraph; +use tokio::sync::mpsc; + +use crate::host::ALPN; +use crate::protocol::{self, host as htag, viewer as vtag}; + +const SCROLLBACK: usize = 10_000; + +#[derive(Clone, Copy, PartialEq)] +enum Mode { + ReadOnly, + Requesting, + Editing, +} + +pub async fn run(ticket_str: &str) -> Result<()> { + let ticket: EndpointTicket = ticket_str + .parse() + .map_err(|e| anyhow::anyhow!("invalid ticket: {e}"))?; + + let mut terminal = ratatui::init(); + ratatui::crossterm::execute!( + std::io::stdout(), + ratatui::crossterm::event::EnableMouseCapture + )?; + + let cleanup = || { + ratatui::crossterm::execute!( + std::io::stdout(), + ratatui::crossterm::event::DisableMouseCapture + ) + .ok(); + ratatui::restore(); + }; + + let viewer_name = crate::config::ensure_name(&mut terminal)?; + + let (event_tx, mut event_rx) = mpsc::channel::(64); + std::thread::spawn(move || { + while let Ok(ev) = event::read() { + if event_tx.blocking_send(ev).is_err() { + break; + } + } + }); + + crate::draw_message(&mut terminal, "connecting to host... q to quit")?; + + let endpoint = Endpoint::bind(presets::N0) + .await + .context("failed to bind iroh endpoint")?; + + let (conn, mut send, mut recv) = { + let mut connect_fut = std::pin::pin!(async { + let conn = endpoint + .connect(ticket.endpoint_addr().clone(), ALPN) + .await?; + let streams = conn.open_bi().await?; + Ok::<_, anyhow::Error>((conn, streams.0, streams.1)) + }); + loop { + tokio::select! { + result = &mut connect_fut => { + match result { + Ok(val) => break val, + Err(e) => { cleanup(); return Err(e); } + } + } + Some(ev) = event_rx.recv() => { + if matches!(ev, Event::Key(key) if matches!(key.code, KeyCode::Char('q') | KeyCode::Char('Q'))) { + cleanup(); + return Ok(()); + } + } + } + } + }; + + let (mut cols, mut rows) = ratatui::crossterm::terminal::size()?; + let mut pty_rows = rows.saturating_sub(1).max(1); + + protocol::write_msg(&mut send, vtag::HELLO, viewer_name.as_bytes()).await?; + let vp = viewport_bytes(pty_rows, cols); + protocol::write_msg(&mut send, vtag::VIEWPORT, &vp).await?; + + let mut eff_rows = pty_rows; + let mut eff_cols = cols; + let mut vt = + virtual_terminal::VirtualTerminal::new(eff_rows, eff_cols, 1024 * 1024, SCROLLBACK); + let mut scroll_offset: usize = 0; + let mut mode = Mode::ReadOnly; + let mut got_output = false; + let mut denied_until: Option = None; + + loop { + if scroll_offset > 0 { + vt.screen_mut().set_scrollback(scroll_offset); + scroll_offset = vt.screen().scrollback(); + } + + let show_denied = denied_until.is_some_and(|t| tokio::time::Instant::now() < t); + let status = if !got_output { + crate::status_line("connecting to host... q to quit") + } else if show_denied { + Line::from(vec![ + crate::meld_prefix(), + crate::fg("edit request denied", Color::White), + ]) + } else { + build_status(mode, scroll_offset, pty_rows, cols, eff_rows, eff_cols) + }; + let screen = vt.screen(); + let cursor_pos = screen.cursor_position(); + let hide_cursor = screen.hide_cursor(); + + terminal.draw(|frame| { + let [content, status_area] = + Layout::vertical([Constraint::Min(1), Constraint::Length(1)]).areas(frame.area()); + + frame.render_widget(crate::PtyWidget { screen }, content); + frame.render_widget(Paragraph::new(status.clone()), status_area); + + if got_output && scroll_offset == 0 && !hide_cursor { + let (row, col) = cursor_pos; + frame.set_cursor_position((content.x + col, content.y + row)); + } + })?; + + if scroll_offset > 0 { + vt.screen_mut().set_scrollback(0); + } + + let banner_deadline = denied_until + .unwrap_or_else(|| tokio::time::Instant::now() + std::time::Duration::from_secs(86400)); + + tokio::select! { + _ = tokio::time::sleep_until(banner_deadline), if denied_until.is_some() => { + denied_until = None; + } + result = protocol::read_msg(&mut recv) => { + let (tag, payload) = match result { + Ok(msg) => msg, + Err(_) => break, + }; + match tag { + htag::OUTPUT => { + vt.process_output(&payload); + scroll_offset = 0; + got_output = true; + } + htag::TURN_GRANTED => { + mode = Mode::Editing; + } + htag::TURN_REVOKED => { + mode = Mode::ReadOnly; + } + htag::TURN_DENIED => { + mode = Mode::ReadOnly; + denied_until = Some( + tokio::time::Instant::now() + std::time::Duration::from_secs(3), + ); + } + htag::DIMS_CHANGED if payload.len() == 4 => { + let new_r = u16::from_be_bytes([payload[0], payload[1]]); + let new_c = u16::from_be_bytes([payload[2], payload[3]]); + if new_r != eff_rows || new_c != eff_cols { + eff_rows = new_r; + eff_cols = new_c; + vt.resize(eff_rows, eff_cols); + scroll_offset = 0; + } + } + _ => {} + } + } + Some(ev) = event_rx.recv() => { + match ev { + Event::Key(key) => { + match mode { + Mode::ReadOnly => match key.code { + KeyCode::Char('q') | KeyCode::Char('Q') => break, + KeyCode::F(9) if got_output => { + protocol::write_msg(&mut send, vtag::REQUEST_TURN, &[]).await?; + mode = Mode::Requesting; + } + KeyCode::PageUp => { + scroll_offset += pty_rows as usize / 2; + } + KeyCode::PageDown => { + scroll_offset = scroll_offset.saturating_sub(pty_rows as usize / 2); + } + _ => {} + }, + Mode::Requesting => match key.code { + KeyCode::Char('q') | KeyCode::Char('Q') => { + protocol::write_msg(&mut send, vtag::RELEASE_TURN, &[]).await?; + break; + } + KeyCode::F(9) => { + protocol::write_msg(&mut send, vtag::RELEASE_TURN, &[]).await?; + mode = Mode::ReadOnly; + } + KeyCode::PageUp => { + scroll_offset += pty_rows as usize / 2; + } + KeyCode::PageDown => { + scroll_offset = scroll_offset.saturating_sub(pty_rows as usize / 2); + } + _ => {} + }, + Mode::Editing => match key.code { + KeyCode::F(9) => { + protocol::write_msg(&mut send, vtag::RELEASE_TURN, &[]).await?; + mode = Mode::ReadOnly; + } + KeyCode::PageUp => { + scroll_offset += pty_rows as usize / 2; + } + KeyCode::PageDown => { + scroll_offset = scroll_offset.saturating_sub(pty_rows as usize / 2); + } + _ => { + if let Some(bytes) = crate::key_to_bytes(&key) { + protocol::write_msg(&mut send, vtag::INPUT, &bytes).await?; + scroll_offset = 0; + } + } + }, + } + } + Event::Mouse(mouse) => match mouse.kind { + MouseEventKind::ScrollUp => scroll_offset += 3, + MouseEventKind::ScrollDown => { + scroll_offset = scroll_offset.saturating_sub(3); + } + _ => {} + }, + Event::Resize(new_cols, new_rows) => { + cols = new_cols; + rows = new_rows; + pty_rows = rows.saturating_sub(1).max(1); + vt.resize(pty_rows, cols); + let vp = viewport_bytes(pty_rows, cols); + protocol::write_msg(&mut send, vtag::VIEWPORT, &vp).await?; + scroll_offset = 0; + } + _ => {} + } + } + } + } + + let _ = protocol::write_msg(&mut send, vtag::GOODBYE, &[]).await; + let _ = send.finish(); + drop(send); + drop(recv); + conn.close(0u32.into(), b"done"); + cleanup(); + Ok(()) +} + +fn build_status( + mode: Mode, + scroll_offset: usize, + local_rows: u16, + local_cols: u16, + eff_rows: u16, + eff_cols: u16, +) -> Line<'static> { + if scroll_offset > 0 { + return crate::status_line(&format!( + "↑ {} lines — scroll down to return", + scroll_offset + )); + } + let dims_note = if eff_rows != local_rows || eff_cols != local_cols { + format!(" · {}×{}", eff_cols, eff_rows) + } else { + String::new() + }; + let msg = match mode { + Mode::ReadOnly => "viewing [readonly] F9 to request edit · q to exit", + Mode::Requesting => "requesting edit access... F9 to cancel", + Mode::Editing => "editing · F9 to release", + }; + crate::status_line(&format!("{msg}{dims_note}")) +} + +fn viewport_bytes(rows: u16, cols: u16) -> [u8; 4] { + [(rows >> 8) as u8, rows as u8, (cols >> 8) as u8, cols as u8] +} diff --git a/packages/virtual_terminal/Cargo.toml b/packages/virtual_terminal/Cargo.toml index 9f4699a..f2242d1 100644 --- a/packages/virtual_terminal/Cargo.toml +++ b/packages/virtual_terminal/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "virtual_terminal" -version = "0.2.0" +version = "0.2.1" edition = "2024" description = "Virtual terminal with vt100 screen buffer, keyframe/delta replay, and viewport negotiation" diff --git a/packages/virtual_terminal/src/lib.rs b/packages/virtual_terminal/src/lib.rs index 2cded86..3110cb3 100644 --- a/packages/virtual_terminal/src/lib.rs +++ b/packages/virtual_terminal/src/lib.rs @@ -4,6 +4,7 @@ //! generates keyframe snapshots, stores deltas (raw PTY output since last //! keyframe), and negotiates dimensions across multiple clients. +pub use vt100; pub mod recorder; pub use recorder::{VtEvent, VtRecorder, VtRecording, VtRecordingHeader}; @@ -128,6 +129,11 @@ impl VirtualTerminal { self.parser.screen() } + /// Mutable access to the vt100 screen. + pub fn screen_mut(&mut self) -> &mut vt100::Screen { + self.parser.screen_mut() + } + /// Current cursor position (row, col) — 0-indexed. pub fn cursor_position(&self) -> (u16, u16) { self.parser.screen().cursor_position()