diff --git a/CHANGELOG.md b/CHANGELOG.md index 4562efa8..58abd110 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/): - Bulk create ([#3793]) +### Changed + +- Rename `` to `` ([#3989]) +- Remove Legacy Console Mode on Windows ([#3989]) + ## [v26.5.6] ### Added @@ -1721,3 +1726,4 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/): [#3906]: https://github.com/sxyazi/yazi/pull/3906 [#3934]: https://github.com/sxyazi/yazi/pull/3934 [#3943]: https://github.com/sxyazi/yazi/pull/3943 +[#3989]: https://github.com/sxyazi/yazi/pull/3989 diff --git a/Cargo.lock b/Cargo.lock index 5b3ebe3f..3f0a6eda 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,16 +17,6 @@ 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 0.14.7", -] - [[package]] name = "aead" version = "0.6.0-rc.10" @@ -37,40 +27,16 @@ dependencies = [ "inout 0.2.2", ] -[[package]] -name = "aes" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" -dependencies = [ - "cfg-if", - "cipher 0.4.4", - "cpufeatures 0.2.17", -] - [[package]] name = "aes" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "66bd29a732b644c0431c6140f370d097879203d79b80c94a6747ba0872adaef8" dependencies = [ - "cipher 0.5.1", + "cipher", "cpubits", "cpufeatures 0.3.0", -] - -[[package]] -name = "aes-gcm" -version = "0.10.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" -dependencies = [ - "aead 0.5.2", - "aes 0.8.4", - "cipher 0.4.4", - "ctr 0.9.2", - "ghash 0.5.1", - "subtle", + "zeroize", ] [[package]] @@ -79,12 +45,13 @@ version = "0.11.0-rc.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e22c0c90bbe8d4f77c3ca9ddabe41a1f8382d6fc1f7cea89459d0f320371f972" dependencies = [ - "aead 0.6.0-rc.10", - "aes 0.9.0", - "cipher 0.5.1", - "ctr 0.10.0", - "ghash 0.6.0", + "aead", + "aes", + "cipher", + "ctr", + "ghash", "subtle", + "zeroize", ] [[package]] @@ -178,7 +145,7 @@ version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -189,7 +156,7 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -227,13 +194,13 @@ dependencies = [ [[package]] name = "argon2" -version = "0.5.3" +version = "0.6.0-rc.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072" +checksum = "7af50940b73bf4e16c15c448a2b121c63f2d68e3e54b6a8731673cb4aa0cdff5" dependencies = [ "base64ct", "blake2", - "cpufeatures 0.2.17", + "cpufeatures 0.3.0", "password-hash", ] @@ -354,13 +321,13 @@ checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" [[package]] name = "bcrypt-pbkdf" -version = "0.10.0" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6aeac2e1fe888769f34f05ac343bbef98b14d1ffb292ab69d4608b3abc86f2a2" +checksum = "144e573728da132683b9488acd528274c790e07fc06ff81ee29f9d8f8b1041e0" dependencies = [ "blowfish", - "pbkdf2 0.12.2", - "sha2 0.10.9", + "pbkdf2", + "sha2 0.11.0", ] [[package]] @@ -441,11 +408,11 @@ dependencies = [ [[package]] name = "blake2" -version = "0.10.6" +version = "0.11.0-rc.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" +checksum = "061f1a09225e328e1ffbb378d2d49923c0ca5fee19fb5ac1cc9c1e9d52b93690" dependencies = [ - "digest 0.10.7", + "digest 0.11.3", ] [[package]] @@ -464,6 +431,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cdd35008169921d80bc60d3d0ab416eecb028c4cd653352907921d95084790be" dependencies = [ "hybrid-array", + "zeroize", ] [[package]] @@ -486,12 +454,21 @@ dependencies = [ [[package]] name = "blowfish" -version = "0.9.1" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e412e2cd0f2b2d93e02543ceae7917b3c70331573df19ee046bcbc35e45e87d7" +checksum = "62ce3946557b35e71d1bbe07ec385073ce9eda05043f95de134eb578fcf1a298" dependencies = [ "byteorder", - "cipher 0.4.4", + "cipher", +] + +[[package]] +name = "bs58" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" +dependencies = [ + "tinyvec", ] [[package]] @@ -569,22 +546,13 @@ dependencies = [ "rustversion", ] -[[package]] -name = "cbc" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6" -dependencies = [ - "cipher 0.4.4", -] - [[package]] name = "cbc" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "98db6aeaef0eeef2c1e3ce9a27b739218825dae116076352ac3777076aa22225" dependencies = [ - "cipher 0.5.1", + "cipher", ] [[package]] @@ -611,17 +579,6 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" -[[package]] -name = "chacha20" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818" -dependencies = [ - "cfg-if", - "cipher 0.4.4", - "cpufeatures 0.2.17", -] - [[package]] name = "chacha20" version = "0.10.0" @@ -629,8 +586,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601" dependencies = [ "cfg-if", + "cipher", "cpufeatures 0.3.0", "rand_core 0.10.1", + "zeroize", ] [[package]] @@ -647,16 +606,6 @@ dependencies = [ "windows-link", ] -[[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 0.1.4", -] - [[package]] name = "cipher" version = "0.5.1" @@ -666,6 +615,7 @@ dependencies = [ "block-buffer 0.12.0", "crypto-common 0.2.1", "inout 0.2.2", + "zeroize", ] [[package]] @@ -692,9 +642,9 @@ dependencies = [ [[package]] name = "clap_complete" -version = "4.6.3" +version = "4.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "660c0520455b1013b9bcb0393d5f643d7e4454fb69c915b8d6d2aa0e9a45acc3" +checksum = "e0a7a9bfdb35811f9e59832f0f05975114d2251b415fb534108e6f34060fd772" dependencies = [ "clap", ] @@ -921,9 +871,6 @@ dependencies = [ "crossterm_winapi", "derive_more", "document-features", - "filedescriptor", - "futures-core", - "libc", "mio", "parking_lot", "rustix 1.1.4", @@ -950,9 +897,9 @@ checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" [[package]] name = "crypto-bigint" -version = "0.7.0-rc.28" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96dacf199529fb801ae62a9aafdc01b189e9504c0d1ee1512a4c16bcd8666a93" +checksum = "42a0d26b245348befa0c121944541476763dcc46ede886c88f9d12e1697d27c3" dependencies = [ "cpubits", "ctutils", @@ -988,9 +935,9 @@ dependencies = [ [[package]] name = "crypto-primes" -version = "0.7.0-pre.9" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6081ce8b60c0e533e2bba42771b94eb6149052115f4179744d5779883dc98583" +checksum = "21f41f23de7d24cdbda7f0c4d9c0351f99a4ceb258ef30e5c1927af8987ffe5a" dependencies = [ "crypto-bigint", "libm", @@ -1007,22 +954,13 @@ dependencies = [ "phf", ] -[[package]] -name = "ctr" -version = "0.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" -dependencies = [ - "cipher 0.4.4", -] - [[package]] name = "ctr" version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "17469f8eb9bdbfad10f71f4cfddfd38b01143520c0e717d8796ccb4d44d44e42" dependencies = [ - "cipher 0.5.1", + "cipher", ] [[package]] @@ -1181,7 +1119,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "71fd89660b2dc699704064e59e9dba0147b903e85319429e131620d022be411b" dependencies = [ "const-oid 0.10.2", - "pem-rfc7468 1.0.0", + "pem-rfc7468", "zeroize", ] @@ -1248,6 +1186,15 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "des" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "916a94e407b54f9034d71dd748234cd1e516ced6284009906ae246f177eafe5a" +dependencies = [ + "cipher", +] + [[package]] name = "digest" version = "0.10.7" @@ -1257,7 +1204,6 @@ dependencies = [ "block-buffer 0.10.4", "const-oid 0.9.6", "crypto-common 0.1.7", - "subtle", ] [[package]] @@ -1290,7 +1236,7 @@ dependencies = [ "libc", "option-ext", "redox_users", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -1310,9 +1256,9 @@ checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" [[package]] name = "ecdsa" -version = "0.17.0-rc.16" +version = "0.17.0-rc.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91bbdd377139884fafcad8dc43a760a3e1e681aa26db910257fa6535b70e1829" +checksum = "54fb064faabbee66e1fc8e5c5a9458d4269dc2d8b638fe86a425adb2510d1a96" dependencies = [ "der", "digest 0.11.3", @@ -1325,9 +1271,9 @@ dependencies = [ [[package]] name = "ed25519" -version = "3.0.0-rc.4" +version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6e914c7c52decb085cea910552e24c63ac019e3ab8bf001ff736da9a9d9d890" +checksum = "29fcf32e6c73d1079f83ab4d782de2d81620346a5f38c6237a86a22f8368980a" dependencies = [ "pkcs8", "signature", @@ -1335,9 +1281,9 @@ dependencies = [ [[package]] name = "ed25519-dalek" -version = "3.0.0-pre.6" +version = "3.0.0-pre.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "053618a4c3d3bc24f188aa660ae75a46eeab74ef07fb415c61431e5e7cd4749b" +checksum = "20449acd54b660981ae5caa2bcb56d1fe7f25f2e37a38ec507400fab034d4bb6" dependencies = [ "curve25519-dalek", "ed25519", @@ -1351,15 +1297,15 @@ dependencies = [ [[package]] name = "either" -version = "1.15.0" +version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" [[package]] name = "elliptic-curve" -version = "0.14.0-rc.28" +version = "0.14.0-rc.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bde7860544606d222fd6bd6d9f9a0773321bf78072a637e1d560a058c0031978" +checksum = "cda94f31325c4275e9706adecbb6f0650dee2f904c915a98e3d81adaaaa757aa" dependencies = [ "base16ct", "crypto-bigint", @@ -1368,7 +1314,7 @@ dependencies = [ "hkdf", "hybrid-array", "once_cell", - "pem-rfc7468 1.0.0", + "pem-rfc7468", "pkcs8", "rand_core 0.10.1", "rustcrypto-ff", @@ -1440,7 +1386,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -1750,23 +1696,13 @@ dependencies = [ "wasip3", ] -[[package]] -name = "ghash" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1" -dependencies = [ - "opaque-debug", - "polyval 0.6.2", -] - [[package]] name = "ghash" version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2eecf2d5dc9b66b732b97707a0210906b1d30523eb773193ab777c0c84b3e8d5" dependencies = [ - "polyval 0.7.1", + "polyval", ] [[package]] @@ -1837,9 +1773,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.17.0" +version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" dependencies = [ "allocator-api2", "equivalent", @@ -1878,16 +1814,7 @@ version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4aaa26c720c68b866f2c96ef5c1264b3e6f473fe5d4ce61cd44bbe913e553018" dependencies = [ - "hmac 0.13.0", -] - -[[package]] -name = "hmac" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" -dependencies = [ - "digest 0.10.7", + "hmac", ] [[package]] @@ -1901,9 +1828,9 @@ dependencies = [ [[package]] name = "hybrid-array" -version = "0.4.11" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08d46837a0ed51fe95bd3b05de33cd64a1ee88fc797477ca48446872504507c5" +checksum = "9155a582abd142abc056962c29e3ce5ff2ad5469f4246b537ed42c5deba857da" dependencies = [ "ctutils", "subtle", @@ -1923,7 +1850,7 @@ dependencies = [ "js-sys", "log", "wasm-bindgen", - "windows-core 0.62.2", + "windows-core 0.56.0", ] [[package]] @@ -2004,7 +1931,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" dependencies = [ "equivalent", - "hashbrown 0.17.0", + "hashbrown 0.17.1", "serde", "serde_core", ] @@ -2071,35 +1998,6 @@ dependencies = [ "syn 2.0.117", ] -[[package]] -name = "internal-russh-forked-ssh-key" -version = "0.6.18+upstream-0.6.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25f8a978272e3cbdf4768f7363eb1c8e1e6ba63c52a3ed05e29e222da4aec7cb" -dependencies = [ - "argon2", - "bcrypt-pbkdf", - "crypto-bigint", - "ecdsa", - "ed25519-dalek", - "hex", - "hmac 0.13.0", - "num-bigint-dig", - "p256", - "p384", - "p521", - "rand_core 0.10.1", - "rsa", - "sec1", - "sha1 0.11.0", - "sha2 0.11.0", - "signature", - "ssh-cipher", - "ssh-encoding", - "subtle", - "zeroize", -] - [[package]] name = "internal-russh-num-bigint" version = "0.5.0" @@ -2209,9 +2107,9 @@ dependencies = [ [[package]] name = "kqueue-sys" -version = "1.1.0" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7b65860415f949f23fa882e669f2dbd4a0f0eeb1acdd56790b30494afd7da2f" +checksum = "07293a4e297ac234359b510362495713f75ea345d5307140414f20c69ffeb087" dependencies = [ "bitflags 2.11.1", "libc", @@ -2228,9 +2126,6 @@ name = "lazy_static" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" -dependencies = [ - "spin", -] [[package]] name = "leb128fmt" @@ -2341,7 +2236,7 @@ version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a860605968fce16869fd239cf4237a82f3ac470723415db603b0e8b6c8d4fb9" dependencies = [ - "hashbrown 0.17.0", + "hashbrown 0.17.1", ] [[package]] @@ -2448,13 +2343,14 @@ dependencies = [ [[package]] name = "ml-kem" -version = "0.3.0-rc.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8198b5db27ac9773534c371751a59dc18aec8b80aa141e69abfdd1dec2e3f78c" +checksum = "5e15f3e5b957493873e396a66914e83e616b6afe335cdef7efe5c6e1216aba66" dependencies = [ "hybrid-array", "kem", "module-lattice", + "pkcs8", "rand_core 0.10.1", "sha3", ] @@ -2512,9 +2408,9 @@ dependencies = [ [[package]] name = "module-lattice" -version = "0.2.2" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc7c90d33a0dac244570c26461d761ffaeadb3bfc2b17cc625ae2185cafdffae" +checksum = "0c61b87c9683ab7cb1c6871d261ad5479b6b10ceb52c4352aaca3b5d35a8febe" dependencies = [ "ctutils", "hybrid-array", @@ -2552,9 +2448,9 @@ dependencies = [ [[package]] name = "nix" -version = "0.31.2" +version = "0.31.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d6d0705320c1e6ba1d912b5e37cf18071b6c2e9b7fa8215a1e8a7651966f5d3" +checksum = "cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d" dependencies = [ "bitflags 2.11.1", "cfg-if", @@ -2629,7 +2525,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -2642,22 +2538,6 @@ dependencies = [ "num-traits", ] -[[package]] -name = "num-bigint-dig" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e661dda6640fad38e827a6d4a310ff4763082116fe217f279885c97f511bb0b7" -dependencies = [ - "lazy_static", - "libm", - "num-integer", - "num-iter", - "num-traits", - "rand 0.8.6", - "serde", - "smallvec", -] - [[package]] name = "num-conv" version = "0.2.1" @@ -2684,17 +2564,6 @@ dependencies = [ "num-traits", ] -[[package]] -name = "num-iter" -version = "0.1.45" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" -dependencies = [ - "autocfg", - "num-integer", - "num-traits", -] - [[package]] name = "num-rational" version = "0.4.2" @@ -2809,12 +2678,6 @@ dependencies = [ "pkg-config", ] -[[package]] -name = "opaque-debug" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" - [[package]] name = "option-ext" version = "0.2.0" @@ -2852,9 +2715,9 @@ dependencies = [ [[package]] name = "p256" -version = "0.14.0-rc.7" +version = "0.14.0-rc.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "018bfbb86e05fd70a83e985921241035ee09fcd369c4a2c3680b389a01d2ad28" +checksum = "8b97e3bf0465157ae90975ff52dbeb1362ba618924878c9f74c25baa27a65f9a" dependencies = [ "ecdsa", "elliptic-curve", @@ -2865,9 +2728,9 @@ dependencies = [ [[package]] name = "p384" -version = "0.14.0-rc.7" +version = "0.14.0-rc.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c91df688211f5957dbe2ab599dcbcaade8d6d3cdc15c5b350d350d7d07ce423" +checksum = "437f30ebcb1e16ff48acead5f08bd69fbcdbc82421687bb48af5c315a0bfab03" dependencies = [ "ecdsa", "elliptic-curve", @@ -2879,9 +2742,9 @@ dependencies = [ [[package]] name = "p521" -version = "0.14.0-rc.7" +version = "0.14.0-rc.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de6cd9451de522549d36cc78a1b45a699a3d55a872e8ea0c8f0318e502d99e2c" +checksum = "4e9fd792bab86ecf6249561752fb5a413511f999887107dd054bbda5143743d7" dependencies = [ "base16ct", "ecdsa", @@ -2965,13 +2828,11 @@ dependencies = [ [[package]] name = "password-hash" -version = "0.5.0" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" +checksum = "aab41826031698d6ffcd9cff78ef56ef998e39dc7e5067cdfebe373842d4723b" dependencies = [ - "base64ct", - "rand_core 0.6.4", - "subtle", + "phc", ] [[package]] @@ -2986,16 +2847,6 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "35fb2e5f958ec131621fdd531e9fc186ed768cbe395337403ae56c17a74c68ec" -[[package]] -name = "pbkdf2" -version = "0.12.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" -dependencies = [ - "digest 0.10.7", - "hmac 0.12.1", -] - [[package]] name = "pbkdf2" version = "0.13.0" @@ -3003,16 +2854,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "112d82ceb8c5bf524d9af484d4e4970c9fd5a0cc15ba14ad93dccd28873b0629" dependencies = [ "digest 0.11.3", - "hmac 0.13.0", -] - -[[package]] -name = "pem-rfc7468" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" -dependencies = [ - "base64ct", + "hmac", ] [[package]] @@ -3073,6 +2915,16 @@ dependencies = [ "sha2 0.10.9", ] +[[package]] +name = "phc" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44dc769b75f93afdddd8c7fa12d685292ddeff1e66f7f0f3a234cf1818afe892" +dependencies = [ + "base64ct", + "ctutils", +] + [[package]] name = "phf" version = "0.11.3" @@ -3143,15 +2995,14 @@ dependencies = [ [[package]] name = "pkcs5" -version = "0.8.0-rc.13" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5a777c6e26664bc9504b3ce3f6133f8f20d9071f130a4f9fcbd3186959d8dd6" +checksum = "279a91971a1d8eb1260a30938eae3be9cb67b472dffecb222fbbbe2fd2dc1453" dependencies = [ - "aes 0.9.0", - "aes-gcm 0.11.0-rc.3", - "cbc 0.2.0", + "aes", + "cbc", "der", - "pbkdf2 0.13.0", + "pbkdf2", "rand_core 0.10.1", "scrypt", "sha2 0.11.0", @@ -3160,9 +3011,9 @@ dependencies = [ [[package]] name = "pkcs8" -version = "0.11.0-rc.11" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12922b6296c06eb741b02d7b5161e3aaa22864af38dfa025a1a3ba3f68c84577" +checksum = "451913da69c775a56034ea8d9003d27ee8948e12443eae7c038ba100a4f21cb7" dependencies = [ "der", "pkcs5", @@ -3204,25 +3055,13 @@ dependencies = [ [[package]] name = "poly1305" -version = "0.8.0" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" +checksum = "a00baa632505d05512f48a963e16051c54fda9a95cc9acea1a4e3c90991c4a2e" dependencies = [ - "cpufeatures 0.2.17", - "opaque-debug", - "universal-hash 0.5.1", -] - -[[package]] -name = "polyval" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" -dependencies = [ - "cfg-if", - "cpufeatures 0.2.17", - "opaque-debug", - "universal-hash 0.5.1", + "cpufeatures 0.3.0", + "universal-hash", + "zeroize", ] [[package]] @@ -3233,7 +3072,7 @@ checksum = "7dfc63250416fea14f5749b90725916a6c903f599d51cb635aa7a52bfd03eede" dependencies = [ "cpubits", "cpufeatures 0.3.0", - "universal-hash 0.6.1", + "universal-hash", ] [[package]] @@ -3269,9 +3108,9 @@ dependencies = [ [[package]] name = "primefield" -version = "0.14.0-rc.7" +version = "0.14.0-rc.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93401c13cc7ff24684571cfca9d3cf9ebabfaf3d4b7b9963ade41ec54da196b5" +checksum = "1b52e6ee42db392378a95622b463c9740631171d1efce43fa445a569c1600cb6" dependencies = [ "crypto-bigint", "crypto-common 0.2.1", @@ -3283,9 +3122,9 @@ dependencies = [ [[package]] name = "primeorder" -version = "0.14.0-rc.7" +version = "0.14.0-rc.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0c5c8a39bcd764bfedf456e8d55e115fe86dda3e0f555371849f2a41cbc9706" +checksum = "0556580e42c19833f5d232aca11a7687a503ee41f937b54f5ae1d50fc2a6a36a" dependencies = [ "elliptic-curve", ] @@ -3357,9 +3196,9 @@ dependencies = [ [[package]] name = "quantette" -version = "0.5.1" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c98fecda8b16396ff9adac67644a523dd1778c42b58606a29df5c31ca925d174" +checksum = "ba5d37e94c17b8870a5b936001d2845a6782a22ebdb1706c84294eca777f6729" dependencies = [ "bitvec", "bytemuck", @@ -3441,7 +3280,7 @@ version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207" dependencies = [ - "chacha20 0.10.0", + "chacha20", "getrandom 0.4.2", "rand_core 0.10.1", ] @@ -3500,7 +3339,6 @@ dependencies = [ "instability", "ratatui-core", "ratatui-crossterm", - "ratatui-macros", "ratatui-termion", "ratatui-termwiz", "ratatui-widgets", @@ -3541,16 +3379,6 @@ dependencies = [ "ratatui-core", ] -[[package]] -name = "ratatui-macros" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7f1342a13e83e4bb9d0b793d0ea762be633f9582048c892ae9041ef39c936f4" -dependencies = [ - "ratatui-core", - "ratatui-widgets", -] - [[package]] name = "ratatui-termion" version = "0.1.0" @@ -3736,7 +3564,7 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5236ce872cac07e0fb3969b0cbf468c7d2f37d432f1b627dcb7b8d34563fb0c3" dependencies = [ - "hmac 0.13.0", + "hmac", "subtle", ] @@ -3762,9 +3590,9 @@ dependencies = [ [[package]] name = "rsa" -version = "0.10.0-rc.16" +version = "0.10.0-rc.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fb9fd8c1edd9e6a2693623baf0fe77ff05ce022a5d7746900ffc38a15c233de" +checksum = "30b2aa4ba0d89f73d1e332df05be0eeab8840351c36ca5654341dfdb57bb3caf" dependencies = [ "const-oid 0.10.2", "crypto-bigint", @@ -3781,29 +3609,24 @@ dependencies = [ [[package]] name = "russh" -version = "0.60.2" +version = "0.61.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c9e358980fe9b079b99da387117864ee6f0a3fd02f39e5b5fde6af9c2895374" +checksum = "fba341d1fce5e094392f088499c5808d2a60d3a658f58fe480c26ac77b1c7dd3" dependencies = [ - "aead 0.6.0-rc.10", - "aes 0.8.4", - "aes 0.9.0", - "aes-gcm 0.11.0-rc.3", + "aes", "bitflags 2.11.1", - "block-padding 0.3.3", + "block-padding 0.4.2", "byteorder", "bytes", - "cbc 0.1.2", - "cbc 0.2.0", - "cipher 0.5.1", + "cbc", + "cipher", "crypto-bigint", - "ctr 0.10.0", - "ctr 0.9.2", + "ctr", "curve25519-dalek", "data-encoding", "delegate", "der", - "digest 0.10.7", + "digest 0.11.3", "ecdsa", "ed25519-dalek", "elliptic-curve", @@ -3811,13 +3634,11 @@ dependencies = [ "futures", "generic-array 1.4.1", "getrandom 0.2.17", - "ghash 0.6.0", + "ghash", "hex-literal", "hkdf", - "hmac 0.12.1", - "hmac 0.13.0", + "hmac", "inout 0.1.4", - "internal-russh-forked-ssh-key", "internal-russh-num-bigint", "keccak", "log", @@ -3829,12 +3650,11 @@ dependencies = [ "p384", "p521", "pageant", - "pbkdf2 0.12.2", - "pbkdf2 0.13.0", + "pbkdf2", "pkcs1", "pkcs5", "pkcs8", - "polyval 0.7.1", + "polyval", "rand 0.10.1", "rand_core 0.10.1", "ring", @@ -3844,30 +3664,29 @@ dependencies = [ "salsa20", "scrypt", "sec1", - "sha1 0.10.6", - "sha1 0.11.0", - "sha2 0.10.9", + "sha1", "sha2 0.11.0", "sha3", "signature", "spki", "ssh-encoding", + "ssh-key", "subtle", "thiserror 2.0.18", "tokio", "typenum", - "universal-hash 0.6.1", + "universal-hash", "zeroize", ] [[package]] name = "russh-cryptovec" -version = "0.59.0" +version = "0.61.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36140e8a20297bc2e8338807c3d9ca911f7fa49d7539cbcd6d48d3befd70efd8" +checksum = "443f6bbcfacb34a1aab2b12b99bf08e0c63abdc5a0db261901365df9d57fff51" dependencies = [ "log", - "nix 0.31.2", + "nix 0.31.3", "ssh-encoding", "windows-sys 0.61.2", ] @@ -3949,7 +3768,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.12.1", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -3966,9 +3785,9 @@ checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" [[package]] name = "safe_arch" -version = "0.9.3" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "629516c85c29fe757770fa03f2074cf1eac43d44c02a3de9fc2ef7b0e207dfdd" +checksum = "1f7caad094bd561859bcd467734a720c3c1f5d1f338995351fefe2190c45efed" dependencies = [ "bytemuck", ] @@ -3980,7 +3799,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2f874456e72520ff1375a06c588eaf074b0f01f9e9e1aada45bd9b7954a6e42c" dependencies = [ "cfg-if", - "cipher 0.5.1", + "cipher", ] [[package]] @@ -4029,7 +3848,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d87af57419b594aa23fa95f09f0e06d80d84ba01c26148c43844cad6ff4485f0" dependencies = [ "cfg-if", - "pbkdf2 0.13.0", + "pbkdf2", "salsa20", "sha2 0.11.0", ] @@ -4118,11 +3937,12 @@ dependencies = [ [[package]] name = "serde_with" -version = "3.19.0" +version = "3.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f05839ce67618e14a09b286535c0d9c94e85ef25469b0e13cb4f844e5593eb19" +checksum = "e72c1c2cb7b223fafb600a619537a871c2818583d619401b785e7c0b746ccde2" dependencies = [ "base64", + "bs58", "chrono", "hex", "indexmap 1.9.3", @@ -4137,9 +3957,9 @@ dependencies = [ [[package]] name = "serde_with_macros" -version = "3.19.0" +version = "3.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf2ebbe86054f9b45bc3881e865683ccfaccce97b9b4cb53f3039d67f355a334" +checksum = "b90c488738ecb4fb0262f41f43bc40efc5868d9fb744319ddf5f5317f417bfac" dependencies = [ "darling 0.23.0", "proc-macro2", @@ -4157,17 +3977,6 @@ dependencies = [ "serde", ] -[[package]] -name = "sha1" -version = "0.10.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" -dependencies = [ - "cfg-if", - "cpufeatures 0.2.17", - "digest 0.10.7", -] - [[package]] name = "sha1" version = "0.11.0" @@ -4335,20 +4144,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] -[[package]] -name = "spin" -version = "0.9.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" - [[package]] name = "spki" -version = "0.8.0-rc.4" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8baeff88f34ed0691978ec34440140e1572b68c7dd4a495fd14a3dc1944daa80" +checksum = "1d9efca8738c78ee9484207732f728b1ef517bbb1833d6fc0879ca898a522f6f" dependencies = [ "base64ct", "der", @@ -4356,31 +4159,63 @@ dependencies = [ [[package]] name = "ssh-cipher" -version = "0.2.0" +version = "0.3.0-rc.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "caac132742f0d33c3af65bfcde7f6aa8f62f0e991d80db99149eb9d44708784f" +checksum = "10db6f219196a8528f9ec904d9d45cdad692d65b0e57e72be4dedd1c5fddce36" dependencies = [ - "aes 0.8.4", - "aes-gcm 0.10.3", - "cbc 0.1.2", - "chacha20 0.9.1", - "cipher 0.4.4", - "ctr 0.9.2", + "aead", + "aes", + "aes-gcm", + "cbc", + "chacha20", + "cipher", + "ctr", + "ctutils", + "des", "poly1305", "ssh-encoding", - "subtle", + "zeroize", ] [[package]] name = "ssh-encoding" -version = "0.2.0" +version = "0.3.0-rc.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb9242b9ef4108a78e8cd1a2c98e193ef372437f8c22be363075233321dd4a15" +checksum = "7abf34aa716da5d5b4c496936d042ea282ab392092cd68a72ef6a8863ff8c96a" dependencies = [ "base64ct", "bytes", - "pem-rfc7468 0.7.0", - "sha2 0.10.9", + "crypto-bigint", + "ctutils", + "digest 0.11.3", + "pem-rfc7468", + "zeroize", +] + +[[package]] +name = "ssh-key" +version = "0.7.0-rc.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45735ce3dea95690e4a9e414c4cfde7f79835063c3dcd35881df85a84118e74b" +dependencies = [ + "argon2", + "bcrypt-pbkdf", + "ctutils", + "ed25519-dalek", + "hex", + "hmac", + "p256", + "p384", + "p521", + "rand_core 0.10.1", + "rsa", + "sec1", + "sha1", + "sha2 0.11.0", + "signature", + "ssh-cipher", + "ssh-encoding", + "zeroize", ] [[package]] @@ -4694,6 +4529,21 @@ dependencies = [ "time-core", ] +[[package]] +name = "tinyvec" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + [[package]] name = "tokio" version = "1.52.3" @@ -4945,16 +4795,6 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" -[[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 = "universal-hash" version = "0.6.1" @@ -5291,9 +5131,9 @@ dependencies = [ [[package]] name = "wide" -version = "0.8.3" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13ca908d26e4786149c48efcf6c0ea09ab0e06d1fe3c17dc1b4b0f1ca4a7e788" +checksum = "9a7714cd0430a663154667c74da5d09325c2387695bee18b3f7f72825aa3693a" dependencies = [ "bytemuck", "safe_arch", @@ -5321,7 +5161,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -5660,9 +5500,9 @@ checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" [[package]] name = "winnow" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ee1708bef14716a11bae175f579062d4554d95be2c6829f518df847b7b3fdd0" +checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1" [[package]] name = "wit-bindgen" @@ -5778,10 +5618,9 @@ name = "yazi-actor" version = "26.5.6" dependencies = [ "anyhow", - "crossterm 0.29.0", "either", "futures", - "hashbrown 0.17.0", + "hashbrown 0.17.1", "indexmap 2.14.0", "libc", "mlua", @@ -5808,6 +5647,7 @@ dependencies = [ "yazi-shim", "yazi-term", "yazi-tty", + "yazi-tui", "yazi-vfs", "yazi-watcher", "yazi-widgets", @@ -5820,7 +5660,6 @@ dependencies = [ "ansi-to-tui", "anyhow", "base64", - "crossterm 0.29.0", "image", "moxcms", "palette", @@ -5835,6 +5674,7 @@ dependencies = [ "yazi-macro", "yazi-shared", "yazi-shim", + "yazi-term", "yazi-tty", ] @@ -5844,9 +5684,8 @@ version = "26.5.6" dependencies = [ "ansi-to-tui", "anyhow", - "crossterm 0.29.0", "futures", - "hashbrown 0.17.0", + "hashbrown 0.17.1", "libc", "mlua", "paste", @@ -5864,6 +5703,7 @@ dependencies = [ "yazi-macro", "yazi-shared", "yazi-shim", + "yazi-term", "yazi-vfs", "yazi-widgets", ] @@ -5877,7 +5717,7 @@ dependencies = [ "clap_complete_fig", "clap_complete_nushell", "futures", - "hashbrown 0.17.0", + "hashbrown 0.17.1", "regex", "vergen-gitcl", "yazi-adapter", @@ -5887,6 +5727,7 @@ dependencies = [ "yazi-macro", "yazi-shared", "yazi-shim", + "yazi-term", "yazi-vfs", ] @@ -5907,8 +5748,7 @@ dependencies = [ "clap_complete", "clap_complete_fig", "clap_complete_nushell", - "crossterm 0.29.0", - "hashbrown 0.17.0", + "hashbrown 0.17.1", "serde", "serde_json", "tokio", @@ -5920,7 +5760,7 @@ dependencies = [ "yazi-fs", "yazi-macro", "yazi-shared", - "yazi-shim", + "yazi-term", ] [[package]] @@ -5939,9 +5779,8 @@ dependencies = [ "anyhow", "arc-swap", "bitflags 2.11.1", - "crossterm 0.29.0", "globset", - "hashbrown 0.17.0", + "hashbrown 0.17.1", "indexmap 2.14.0", "ratatui", "regex", @@ -5956,6 +5795,7 @@ dependencies = [ "yazi-macro", "yazi-shared", "yazi-shim", + "yazi-term", "yazi-tty", ] @@ -5964,9 +5804,8 @@ name = "yazi-core" version = "26.5.6" dependencies = [ "anyhow", - "crossterm 0.29.0", "dyn-clone", - "hashbrown 0.17.0", + "hashbrown 0.17.1", "indexmap 2.14.0", "mlua", "parking_lot", @@ -5984,7 +5823,6 @@ dependencies = [ "yazi-binding", "yazi-config", "yazi-dds", - "yazi-emulator", "yazi-fs", "yazi-macro", "yazi-prebuilt", @@ -5992,6 +5830,7 @@ dependencies = [ "yazi-scheduler", "yazi-shared", "yazi-shim", + "yazi-term", "yazi-vfs", "yazi-watcher", "yazi-widgets", @@ -6002,7 +5841,7 @@ name = "yazi-dds" version = "26.5.6" dependencies = [ "anyhow", - "hashbrown 0.17.0", + "hashbrown 0.17.1", "indexmap 2.14.0", "mlua", "ordered-float 5.3.0", @@ -6027,14 +5866,15 @@ name = "yazi-emulator" version = "26.5.6" dependencies = [ "anyhow", - "crossterm 0.29.0", "either", + "ratatui", "scopeguard", "tokio", "tracing", "yazi-macro", "yazi-shared", "yazi-shim", + "yazi-term", "yazi-tty", ] @@ -6055,7 +5895,6 @@ version = "26.5.6" dependencies = [ "anyhow", "better-panic", - "crossterm 0.29.0", "fdlimit", "futures", "libc", @@ -6077,7 +5916,6 @@ dependencies = [ "yazi-config", "yazi-core", "yazi-dds", - "yazi-emulator", "yazi-fs", "yazi-macro", "yazi-parser", @@ -6089,6 +5927,7 @@ dependencies = [ "yazi-shim", "yazi-term", "yazi-tty", + "yazi-tui", "yazi-vfs", "yazi-watcher", "yazi-widgets", @@ -6105,7 +5944,7 @@ dependencies = [ "dirs", "either", "foldhash 0.2.0", - "hashbrown 0.17.0", + "hashbrown 0.17.1", "libc", "objc2", "parking_lot", @@ -6141,8 +5980,7 @@ version = "26.5.6" dependencies = [ "anyhow", "bitflags 2.11.1", - "crossterm 0.29.0", - "hashbrown 0.17.0", + "hashbrown 0.17.1", "mlua", "paste", "ratatui", @@ -6159,6 +5997,7 @@ dependencies = [ "yazi-macro", "yazi-scheduler", "yazi-shared", + "yazi-term", "yazi-vfs", "yazi-widgets", ] @@ -6226,7 +6065,7 @@ name = "yazi-runner" version = "26.5.6" dependencies = [ "anyhow", - "hashbrown 0.17.0", + "hashbrown 0.17.1", "mlua", "parking_lot", "thiserror 2.0.18", @@ -6250,7 +6089,7 @@ dependencies = [ "anyhow", "async-priority-channel", "foldhash 0.2.0", - "hashbrown 0.17.0", + "hashbrown 0.17.1", "libc", "lru 0.18.0", "mlua", @@ -6268,6 +6107,7 @@ dependencies = [ "yazi-runner", "yazi-shared", "yazi-term", + "yazi-tui", "yazi-vfs", ] @@ -6288,11 +6128,10 @@ name = "yazi-shared" version = "26.5.6" dependencies = [ "anyhow", - "crossterm 0.29.0", "dyn-clone", "foldhash 0.2.0", "futures", - "hashbrown 0.17.0", + "hashbrown 0.17.1", "libc", "memchr", "ordered-float 5.3.0", @@ -6309,6 +6148,7 @@ dependencies = [ "windows-sys 0.61.2", "yazi-macro", "yazi-shim", + "yazi-term", ] [[package]] @@ -6316,8 +6156,7 @@ name = "yazi-shim" version = "26.5.6" dependencies = [ "arc-swap", - "crossterm 0.29.0", - "hashbrown 0.17.0", + "hashbrown 0.17.1", "mlua", "ratatui", "serde", @@ -6334,11 +6173,16 @@ name = "yazi-term" version = "26.5.6" dependencies = [ "anyhow", - "crossterm 0.29.0", + "base64", + "bitflags 2.11.1", + "futures", + "parking_lot", "ratatui", + "rustix 1.1.4", + "signal-hook 0.4.4", + "thiserror 2.0.18", "tokio", - "yazi-config", - "yazi-emulator", + "windows-sys 0.61.2", "yazi-macro", "yazi-shim", "yazi-tty", @@ -6353,8 +6197,24 @@ dependencies = [ "tracing", "windows-sys 0.61.2", "yazi-macro", + "yazi-shim", +] + +[[package]] +name = "yazi-tui" +version = "26.5.9" +dependencies = [ + "anyhow", + "ratatui", + "tokio", + "tracing", + "yazi-config", + "yazi-emulator", + "yazi-macro", "yazi-shared", "yazi-shim", + "yazi-term", + "yazi-tty", ] [[package]] @@ -6366,7 +6226,7 @@ dependencies = [ "deadpool", "either", "futures", - "hashbrown 0.17.0", + "hashbrown 0.17.1", "parking_lot", "russh", "tokio", @@ -6385,7 +6245,7 @@ name = "yazi-watcher" version = "26.5.6" dependencies = [ "futures", - "hashbrown 0.17.0", + "hashbrown 0.17.1", "notify", "parking_lot", "percent-encoding", @@ -6406,9 +6266,7 @@ name = "yazi-widgets" version = "26.5.6" dependencies = [ "anyhow", - "base64", "clipboard-win", - "crossterm 0.29.0", "futures", "mlua", "parking_lot", @@ -6421,6 +6279,7 @@ dependencies = [ "yazi-macro", "yazi-shared", "yazi-shim", + "yazi-term", "yazi-tty", ] diff --git a/Cargo.toml b/Cargo.toml index 3054f8fb..204c73da 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -43,14 +43,13 @@ bitflags = { version = "2.11.1", features = [ "serde" ] } chrono = "0.4.44" clap = { version = "4.6.1", features = [ "derive" ] } core-foundation-sys = "0.8.7" -crossterm = { version = "0.29.0", features = [ "event-stream" ] } dirs = "6.0.0" dyn-clone = "1.0.20" -either = { version = "1.15.0" } +either = { version = "1.16.0" } foldhash = "0.2.0" futures = "0.3.32" globset = "0.4.18" -hashbrown = { version = "0.17.0", features = [ "serde" ] } +hashbrown = { version = "0.17.1", features = [ "serde" ] } indexmap = { version = "2.14.0", features = [ "serde" ] } libc = "0.2.186" lru = "0.18.0" @@ -61,13 +60,13 @@ parking_lot = "0.12.5" paste = "1.0.15" percent-encoding = "2.3.2" rand = { version = "0.10.1", default-features = false, features = [ "std", "sys_rng" ] } -ratatui = { version = "0.30.0", features = [ "serde", "unstable-rendered-line-info", "unstable-widget-ref" ] } +ratatui = { version = "0.30.0", default-features = false, features = [ "layout-cache", "serde", "underline-color", "unstable-rendered-line-info", "unstable-widget-ref" ] } regex = "1.12.3" -russh = { version = "0.60.2", default-features = false, features = [ "ring", "rsa" ] } +russh = { version = "0.61.0", default-features = false, features = [ "ring", "rsa" ] } scopeguard = "1.2.0" serde = { version = "1.0.228", features = [ "derive" ] } serde_json = "1.0.149" -serde_with = "3.19.0" +serde_with = "3.20.0" strum = { version = "0.28.0", features = [ "derive" ] } syntect = { version = "5.3.0", default-features = false, features = [ "parsing", "plist-load", "regex-onig" ] } thiserror = "2.0.18" diff --git a/cspell.json b/cspell.json index e5d69993..289f3fd0 100644 --- a/cspell.json +++ b/cspell.json @@ -1 +1 @@ -{"words":["Punct","KEYMAP","splitn","crossterm","YAZI","peekable","ratatui","syntect","pbpaste","pbcopy","oneshot","Posix","Lsar","XADDOS","zoxide","cands","Deque","precache","imageops","IFBLK","IFCHR","IFDIR","IFIFO","IFLNK","IFMT","IFSOCK","IRGRP","IROTH","IRUSR","ISGID","ISUID","ISVTX","IWGRP","IWOTH","IWUSR","IXGRP","IXOTH","IXUSR","libc","winsize","TIOCGWINSZ","xpixel","ypixel","ioerr","appender","Catppuccin","macchiato","gitmodules","Dotfiles","bashprofile","vimrc","flac","webp","exiftool","mediainfo","ripgrep","indexmap","indexmap","unwatch","canonicalize","serde","fsevent","Ueberzug","iterm","wezterm","sixel","chafa","ueberzugpp","Konsole","Überzug","pkgs","pdftoppm","poppler","singlefile","jpegopt","EXIF","rustfmt","mktemp","nanos","xclip","xsel","natord","Mintty","nixos","nixpkgs","SIGTSTP","SIGCONT","SIGCONT","mlua","nonstatic","userdata","metatable","natsort","backstack","luajit","Succ","Succ","cand","fileencoding","foldmethod","lightgreen","darkgray","lightred","lightyellow","lightcyan","nushell","msvc","aarch","linemode","sxyazi","rsplit","ZELLIJ","bitflags","bitflags","USERPROFILE","Neovim","vergen","gitcl","Renderable","preloaders","prec","Upserting","prio","Ghostty","Catmull","Lanczos","cmds","unyank","scrolloff","headsup","unsub","uzers","scopeguard","SPDLOG","globset","filetime","magick","magick","prefetcher","Prework","prefetchers","PREWORKERS","conds","translit","rxvt","Urxvt","realpath","realname","REPARSE","hardlink","hardlinking","nlink","nlink","linemodes","SIGSTOP","sevenzip","rsplitn","replacen","DECSET","DECRQM","repeek","cwds","tcsi","Hyprland","Wayfire","SWAYSOCK","btime","nsec","codegen","gethostname","fchmod","fdfind","Rustc","rustc","ffprobe","vframes","luma","obase","outln","errln","tmtheme","twox","cfgs","fstype","objc","rdev","runloop","exfat","rclone","DECRQSS","DECSCUSR","libvterm","Uninit","lockin","rposition","resvg","foldhash","tilded","futs","chdir","hashbrown","JEMALLOC","RUSTFLAGS","RDONLY","GETPATH","fcntl","casefold","inodes","Splatable","casefied","thiserror","memchr","memmem","russh","deadpool","keepalive","nodelay","publickey","deadpool","initing","treelize","TOCTOU","fellback","watchee","Textlike","sstr","pointee"],"version":"0.2","language":"en","flagWords":[]} \ No newline at end of file +{"language":"en","words":["Punct","KEYMAP","splitn","crossterm","YAZI","peekable","ratatui","syntect","pbpaste","pbcopy","oneshot","Posix","Lsar","XADDOS","zoxide","cands","Deque","precache","imageops","IFBLK","IFCHR","IFDIR","IFIFO","IFLNK","IFMT","IFSOCK","IRGRP","IROTH","IRUSR","ISGID","ISUID","ISVTX","IWGRP","IWOTH","IWUSR","IXGRP","IXOTH","IXUSR","libc","winsize","TIOCGWINSZ","xpixel","ypixel","ioerr","appender","Catppuccin","macchiato","gitmodules","Dotfiles","bashprofile","vimrc","flac","webp","exiftool","mediainfo","ripgrep","indexmap","indexmap","unwatch","canonicalize","serde","fsevent","Ueberzug","iterm","wezterm","sixel","chafa","ueberzugpp","Konsole","Überzug","pkgs","pdftoppm","poppler","singlefile","jpegopt","EXIF","rustfmt","mktemp","nanos","xclip","xsel","natord","Mintty","nixos","nixpkgs","SIGTSTP","SIGCONT","SIGCONT","mlua","nonstatic","userdata","metatable","natsort","backstack","luajit","Succ","Succ","cand","fileencoding","foldmethod","lightgreen","darkgray","lightred","lightyellow","lightcyan","nushell","msvc","aarch","linemode","sxyazi","rsplit","ZELLIJ","bitflags","bitflags","USERPROFILE","Neovim","vergen","gitcl","Renderable","preloaders","prec","Upserting","prio","Ghostty","Catmull","Lanczos","cmds","unyank","scrolloff","headsup","unsub","uzers","scopeguard","SPDLOG","globset","filetime","magick","magick","prefetcher","Prework","prefetchers","PREWORKERS","conds","translit","rxvt","Urxvt","realpath","realname","REPARSE","hardlink","hardlinking","nlink","nlink","linemodes","SIGSTOP","sevenzip","rsplitn","replacen","DECSET","DECRQM","repeek","cwds","tcsi","Hyprland","Wayfire","SWAYSOCK","btime","nsec","codegen","gethostname","fchmod","fdfind","Rustc","rustc","ffprobe","vframes","luma","obase","outln","errln","tmtheme","twox","cfgs","fstype","objc","rdev","runloop","exfat","rclone","DECRQSS","DECSCUSR","libvterm","Uninit","lockin","rposition","resvg","foldhash","tilded","futs","chdir","hashbrown","JEMALLOC","RUSTFLAGS","RDONLY","GETPATH","fcntl","casefold","inodes","Splatable","casefied","thiserror","memchr","memmem","russh","deadpool","keepalive","nodelay","publickey","deadpool","initing","treelize","TOCTOU","fellback","watchee","Textlike","sstr","pointee","writef","wakeup","nonblocking","sigwinch","timespec","termios","tcgetattr","tcsetattr","tcgetwinsize","rustix","codepoint","codepoints","Raterm"],"version":"0.2","flagWords":[]} \ No newline at end of file diff --git a/yazi-actor/Cargo.toml b/yazi-actor/Cargo.toml index 48e577ac..c9b748d5 100644 --- a/yazi-actor/Cargo.toml +++ b/yazi-actor/Cargo.toml @@ -34,13 +34,13 @@ yazi-shared = { path = "../yazi-shared", version = "26.5.6" } yazi-shim = { path = "../yazi-shim", version = "26.5.6" } yazi-term = { path = "../yazi-term", version = "26.5.6" } yazi-tty = { path = "../yazi-tty", version = "26.5.9" } +yazi-tui = { path = "../yazi-tui", version = "26.5.6" } yazi-vfs = { path = "../yazi-vfs", version = "26.5.6" } yazi-watcher = { path = "../yazi-watcher", version = "26.5.6" } yazi-widgets = { path = "../yazi-widgets", version = "26.5.6" } # External dependencies anyhow = { workspace = true } -crossterm = { workspace = true } either = { workspace = true } futures = { workspace = true } hashbrown = { workspace = true } @@ -55,6 +55,3 @@ tracing = { workspace = true } [target."cfg(unix)".dependencies] libc = { workspace = true } - -[target.'cfg(target_os = "macos")'.dependencies] -crossterm = { workspace = true, features = [ "use-dev-tty", "libc" ] } diff --git a/yazi-actor/src/app/mouse.rs b/yazi-actor/src/app/mouse.rs index cc49e45d..7e121168 100644 --- a/yazi-actor/src/app/mouse.rs +++ b/yazi-actor/src/app/mouse.rs @@ -1,5 +1,4 @@ use anyhow::Result; -use crossterm::event::MouseEventKind; use mlua::{ObjectLike, Table}; use tracing::error; use yazi_actor::lives::Lives; @@ -8,6 +7,7 @@ use yazi_macro::succ; use yazi_parser::app::MouseForm; use yazi_plugin::LUA; use yazi_shared::data::Data; +use yazi_term::event::MouseEventKind; use crate::{Actor, Ctx}; diff --git a/yazi-actor/src/app/quit.rs b/yazi-actor/src/app/quit.rs index c00d6343..2c81556c 100644 --- a/yazi-actor/src/app/quit.rs +++ b/yazi-actor/src/app/quit.rs @@ -1,9 +1,11 @@ +use std::process; + use anyhow::Result; use yazi_boot::ARGS; use yazi_fs::provider::{Provider, local::Local}; use yazi_parser::app::QuitForm; use yazi_shared::{data::Data, strand::{StrandBuf, StrandLike, ToStrand}}; -use yazi_term::Term; +use yazi_tui::Raterm; use crate::{Actor, Ctx}; @@ -27,7 +29,8 @@ impl Actor for Quit { ); }); - Term::goodbye(|| opt.code); + Raterm::stop(); + process::exit(opt.code); } } diff --git a/yazi-actor/src/app/resume.rs b/yazi-actor/src/app/resume.rs index 94608eb2..de301bc3 100644 --- a/yazi-actor/src/app/resume.rs +++ b/yazi-actor/src/app/resume.rs @@ -1,28 +1,28 @@ use anyhow::Result; use yazi_macro::{act, render, succ}; -use yazi_parser::app::ResumeForm; +use yazi_parser::app::ReflowForm; use yazi_shared::data::Data; -use yazi_term::Term; +use yazi_tui::Raterm; use crate::{Actor, Ctx}; pub struct Resume; impl Actor for Resume { - type Form = ResumeForm; + type Form = ReflowForm; const NAME: &str = "resume"; fn act(cx: &mut Ctx, form: Self::Form) -> Result { cx.active_mut().preview.reset(); - *cx.term = Some(Term::start()?); + + drop(cx.term.take()); + *cx.term = Some(Raterm::start()?); // While the app resumes, it's possible that the terminal size has changed. // We need to trigger a resize, and render the UI based on the resized area. act!(app:resize, cx, form.reflow)?; - form.tx.send((true, form.replier))?; - act!(app:title, cx).ok(); succ!(render!()); } diff --git a/yazi-actor/src/app/stop.rs b/yazi-actor/src/app/stop.rs index ee67d1a9..aa714e3e 100644 --- a/yazi-actor/src/app/stop.rs +++ b/yazi-actor/src/app/stop.rs @@ -1,6 +1,6 @@ use anyhow::Result; use yazi_macro::succ; -use yazi_parser::app::StopForm; +use yazi_parser::VoidForm; use yazi_shared::data::Data; use crate::{Actor, Ctx}; @@ -8,20 +8,15 @@ use crate::{Actor, Ctx}; pub struct Stop; impl Actor for Stop { - type Form = StopForm; + type Form = VoidForm; const NAME: &str = "stop"; - fn act(cx: &mut Ctx, form: Self::Form) -> Result { + fn act(cx: &mut Ctx, _: Self::Form) -> Result { cx.active_mut().preview.reset_image(); - // We need to destroy the `term` first before stopping the `signals` - // to prevent any signal from triggering the term to render again - // while the app is being suspended. *cx.term = None; - form.tx.send((false, form.replier))?; - succ!(); } } diff --git a/yazi-actor/src/app/title.rs b/yazi-actor/src/app/title.rs index 555e11c7..e6011789 100644 --- a/yazi-actor/src/app/title.rs +++ b/yazi-actor/src/app/title.rs @@ -1,11 +1,11 @@ use anyhow::Result; -use crossterm::{execute, terminal::SetTitle}; use yazi_actor::Ctx; -use yazi_macro::succ; +use yazi_macro::{succ, writef}; use yazi_parser::{app::TitleForm, spark::SparkKind}; use yazi_shared::{Source, data::Data}; -use yazi_term::TermState; +use yazi_term::sequence::SetTitle; use yazi_tty::TTY; +use yazi_tui::RatermState; use crate::Actor; @@ -18,9 +18,9 @@ impl Actor for Title { fn act(cx: &mut Ctx, form: Self::Form) -> Result { let s = form.value.unwrap_or_else(|| format!("Yazi: {}", cx.tab().name()).into()); - execute!(TTY.writer(), SetTitle(&s))?; + writef!(TTY.writer(), "{}", SetTitle(&s))?; - yazi_term::STATE.set(TermState { title: !s.is_empty(), ..yazi_term::STATE.get() }); + yazi_tui::STATE.set(RatermState { title: !s.is_empty(), ..yazi_tui::STATE.get() }); succ!() } diff --git a/yazi-actor/src/context.rs b/yazi-actor/src/context.rs index febe97a6..1ad02d79 100644 --- a/yazi-actor/src/context.rs +++ b/yazi-actor/src/context.rs @@ -4,11 +4,11 @@ use anyhow::{Result, anyhow}; use yazi_core::{Core, mgr::Tabs, tab::{Folder, Tab}}; use yazi_fs::File; use yazi_shared::{Id, Source, event::Action, url::UrlBuf}; -use yazi_term::Term; +use yazi_tui::Raterm; pub struct Ctx<'a> { pub core: &'a mut Core, - pub term: &'a mut Option, + pub term: &'a mut Option, pub tab: usize, pub level: usize, pub source: Source, @@ -27,7 +27,7 @@ impl DerefMut for Ctx<'_> { } impl<'a> Ctx<'a> { - pub fn new(action: &Action, core: &'a mut Core, term: &'a mut Option) -> Result { + pub fn new(action: &Action, core: &'a mut Core, term: &'a mut Option) -> Result { let tab = if let Ok(id) = action.get::("tab") { core .mgr @@ -63,7 +63,7 @@ impl<'a> Ctx<'a> { } } - pub fn active(core: &'a mut Core, term: &'a mut Option) -> Self { + pub fn active(core: &'a mut Core, term: &'a mut Option) -> Self { let tab = core.mgr.tabs.cursor; Self { core, diff --git a/yazi-actor/src/mgr/bulk_create.rs b/yazi-actor/src/mgr/bulk_create.rs index f3d67491..1b6defa0 100644 --- a/yazi-actor/src/mgr/bulk_create.rs +++ b/yazi-actor/src/mgr/bulk_create.rs @@ -5,13 +5,13 @@ use scopeguard::defer; use yazi_binding::Permit; use yazi_config::{YAZI, opener::OpenerRule}; use yazi_fs::{File, FilesOp, Splatter, provider::{Provider, local::Local}}; -use yazi_macro::succ; +use yazi_macro::{succ, writef}; use yazi_parser::VoidForm; use yazi_proxy::TasksProxy; use yazi_scheduler::{AppProxy, NotifyProxy}; -use yazi_shared::{data::Data, strand::Strand, terminal_clear, url::{AsUrl, UrlBuf, UrlCow, UrlLike}}; +use yazi_shared::{data::Data, strand::Strand, url::{AsUrl, UrlBuf, UrlCow, UrlLike}}; use yazi_shim::path::CROSS_SEPARATOR; -use yazi_term::YIELD_TO_SUBPROCESS; +use yazi_term::{YIELD_TO_SUBPROCESS, sequence::EraseScreen}; use yazi_tty::TTY; use yazi_vfs::{VfsFile, provider}; use yazi_watcher::WATCHER; @@ -61,7 +61,7 @@ impl Actor for BulkCreate { impl BulkCreate { async fn r#do(cwd: UrlBuf, todo: Vec>) -> Result<()> { - terminal_clear(TTY.writer())?; + writef!(TTY.writer(), "{EraseScreen}\n")?; if todo.is_empty() { return Ok(()); } else if !Self::ask_continue(&todo, None)? { @@ -134,7 +134,7 @@ impl BulkCreate { async fn output_failed(failed: Vec<(Entry<'_>, anyhow::Error)>) -> Result<()> { let mut stdout = TTY.lockout(); - terminal_clear(&mut *stdout)?; + writeln!(stdout, "{EraseScreen}")?; writeln!(stdout, "Failed to create:")?; for (entry, err) in failed { @@ -161,7 +161,7 @@ impl<'a> Entry<'a> { None => (s, false), }; - Some(Self { path: path.into(), is_dir }).filter(|_| !path.is_empty()) + (!path.is_empty()).then_some(Self { path: path.into(), is_dir }) } } diff --git a/yazi-actor/src/mgr/bulk_rename.rs b/yazi-actor/src/mgr/bulk_rename.rs index d24cec4c..b472aa44 100644 --- a/yazi-actor/src/mgr/bulk_rename.rs +++ b/yazi-actor/src/mgr/bulk_rename.rs @@ -1,7 +1,6 @@ use std::{hash::Hash, io::{Read, Write}, ops::Deref, path::Path, sync::Arc}; use anyhow::{Result, anyhow}; -use crossterm::{execute, style::Print}; use hashbrown::HashMap; use scopeguard::defer; use tokio::io::AsyncWriteExt; @@ -9,12 +8,12 @@ use yazi_binding::Permit; use yazi_config::{YAZI, opener::OpenerRule}; use yazi_dds::Pubsub; use yazi_fs::{File, FilesOp, Splatter, max_common_root, path::skip_url, provider::{FileBuilder, Provider, local::{Gate, Local}}}; -use yazi_macro::{err, succ}; +use yazi_macro::{err, succ, writef}; use yazi_parser::VoidForm; use yazi_proxy::TasksProxy; use yazi_scheduler::{AppProxy, NotifyProxy}; -use yazi_shared::{data::Data, path::PathDyn, strand::{AsStrand, AsStrandJoin, Strand, StrandBuf, StrandLike}, terminal_clear, url::{AsUrl, UrlBuf, UrlCow, UrlLike}}; -use yazi_term::YIELD_TO_SUBPROCESS; +use yazi_shared::{data::Data, path::PathDyn, strand::{AsStrand, AsStrandJoin, Strand, StrandBuf, StrandLike}, url::{AsUrl, UrlBuf, UrlCow, UrlLike}}; +use yazi_term::{YIELD_TO_SUBPROCESS, sequence::EraseScreen}; use yazi_tty::TTY; use yazi_vfs::{VfsFile, maybe_exists, provider}; use yazi_watcher::WATCHER; @@ -100,11 +99,10 @@ impl BulkRename { selected: Vec, decision: Option, ) -> Result<()> { - terminal_clear(TTY.writer())?; + writef!(TTY.writer(), "{EraseScreen}\n")?; if old.len() != new.len() { #[rustfmt::skip] - let s = format!("Number of new and old file names mismatch (New: {}, Old: {}).\nPress to exit...", new.len(), old.len()); - execute!(TTY.writer(), Print(s))?; + writef!(TTY.writer(), "Number of new and old file names mismatch (New: {}, Old: {}).\nPress to exit...", new.len(), old.len())?; TTY.reader().read_exact(&mut [0])?; return Ok(()); @@ -186,7 +184,7 @@ impl BulkRename { async fn output_failed(failed: Vec<(Tuple, Tuple, anyhow::Error)>) -> Result<()> { let mut stdout = TTY.lockout(); - terminal_clear(&mut *stdout)?; + writeln!(stdout, "{EraseScreen}")?; writeln!(stdout, "Failed to rename:")?; for (old, new, err) in failed { diff --git a/yazi-actor/src/mgr/close.rs b/yazi-actor/src/mgr/close.rs index 61b23b8c..8dc553af 100644 --- a/yazi-actor/src/mgr/close.rs +++ b/yazi-actor/src/mgr/close.rs @@ -21,6 +21,6 @@ impl Actor for Close { } fn hook(cx: &Ctx, _form: &Self::Form) -> Option { - Some(SparkKind::KeyClose).filter(|_| cx.source().is_key()) + cx.source().is_key().then_some(SparkKind::KeyClose) } } diff --git a/yazi-actor/src/mgr/quit.rs b/yazi-actor/src/mgr/quit.rs index 1bd63114..02004dd0 100644 --- a/yazi-actor/src/mgr/quit.rs +++ b/yazi-actor/src/mgr/quit.rs @@ -59,7 +59,7 @@ impl Actor for Quit { } fn hook(cx: &Ctx, _form: &Self::Form) -> Option { - Some(SparkKind::KeyQuit).filter(|_| cx.source().is_key()) + cx.source().is_key().then_some(SparkKind::KeyQuit) } } diff --git a/yazi-actor/src/notify/tick.rs b/yazi-actor/src/notify/tick.rs index 1fa64854..1ee7b610 100644 --- a/yazi-actor/src/notify/tick.rs +++ b/yazi-actor/src/notify/tick.rs @@ -3,11 +3,11 @@ use std::time::Duration; use anyhow::Result; use ratatui::layout::Rect; use yazi_core::notify::Notify; -use yazi_emulator::Dimension; use yazi_macro::{render, render_partial, succ}; use yazi_parser::notify::TickForm; use yazi_proxy::NotifyProxy; use yazi_shared::data::Data; +use yazi_term::{Dimension, TERM}; use crate::{Actor, Ctx}; @@ -21,8 +21,8 @@ impl Actor for Tick { fn act(cx: &mut Ctx, form: Self::Form) -> Result { cx.notify.ticker.take().map(|h| h.abort()); - let Dimension { rows, columns, .. } = Dimension::available(); - let area = Notify::available(Rect { x: 0, y: 0, width: columns, height: rows }); + let Dimension { rows, cols, .. } = TERM.dimension(); + let area = Notify::available(Rect { x: 0, y: 0, width: cols, height: rows }); let limit = cx.notify.limit(area); if limit == 0 { diff --git a/yazi-actor/src/tasks/inspect.rs b/yazi-actor/src/tasks/inspect.rs index 781c7257..22150212 100644 --- a/yazi-actor/src/tasks/inspect.rs +++ b/yazi-actor/src/tasks/inspect.rs @@ -1,15 +1,15 @@ use std::io::Write; use anyhow::Result; -use crossterm::{execute, terminal::{disable_raw_mode, enable_raw_mode}}; use scopeguard::defer; use tokio::{io::{AsyncReadExt, stdin}, select, sync::mpsc, time}; use yazi_binding::Permit; -use yazi_macro::succ; +use yazi_term::TERM; +use yazi_macro::{succ, writef}; use yazi_parser::VoidForm; use yazi_scheduler::AppProxy; -use yazi_shared::{data::Data, terminal_clear}; -use yazi_term::YIELD_TO_SUBPROCESS; +use yazi_shared::data::Data; +use yazi_term::{YIELD_TO_SUBPROCESS, sequence::EraseScreen}; use yazi_tty::TTY; use crate::{Actor, Ctx}; @@ -39,24 +39,27 @@ impl Actor for Inspect { task.logs.clone() }; + // Stop the app and clear the terminal AppProxy::stop().await; - terminal_clear(TTY.writer()).ok(); + writeln!(TTY.writer(), "{EraseScreen}").ok(); + + // Print the buffered logs TTY.writer().write_all(buffered.as_bytes()).ok(); TTY.writer().flush().ok(); - defer! { disable_raw_mode().ok(); } - enable_raw_mode().ok(); + defer! { TERM.enter_cooked_mode().ok(); } + TERM.enter_raw_mode().ok(); let mut stdin = stdin(); // TODO: stdin let mut answer = 0; loop { select! { Some(line) = rx.recv() => { - execute!(TTY.writer(), crossterm::style::Print(line), crossterm::style::Print("\r\n")).ok(); + writef!(TTY.writer(), "{line}\r\n").ok(); } _ = time::sleep(time::Duration::from_millis(500)) => { if !ongoing.lock().exists(id) { - execute!(TTY.writer(), crossterm::style::Print("Task finished, press `q` to quit\r\n")).ok(); + writef!(TTY.writer(), "Task finished, press `q` to quit\r\n").ok(); break; } }, diff --git a/yazi-adapter/Cargo.toml b/yazi-adapter/Cargo.toml index e3844700..f3acea37 100644 --- a/yazi-adapter/Cargo.toml +++ b/yazi-adapter/Cargo.toml @@ -19,21 +19,18 @@ yazi-fs = { path = "../yazi-fs", version = "26.5.6" } yazi-macro = { path = "../yazi-macro", version = "26.5.6" } yazi-shared = { path = "../yazi-shared", version = "26.5.6" } yazi-shim = { path = "../yazi-shim", version = "26.5.6" } +yazi-term = { path = "../yazi-term", version = "26.5.6" } yazi-tty = { path = "../yazi-tty", version = "26.5.9" } # External dependencies ansi-to-tui = { workspace = true } anyhow = { workspace = true } base64 = { workspace = true } -crossterm = { workspace = true } image = { version = "0.25.10", default-features = false, features = [ "avif", "bmp", "dds", "exr", "ff", "gif", "hdr", "ico", "jpeg", "png", "pnm", "qoi", "tga", "tiff", "webp" ] } moxcms = "0.8.1" palette = { version = "0.7.6", default-features = false } -quantette = { version = "0.5.1", default-features = false } +quantette = { version = "0.6.0", default-features = false } ratatui = { workspace = true } strum = { workspace = true } tokio = { workspace = true } tracing = { workspace = true } - -[target.'cfg(target_os = "macos")'.dependencies] -crossterm = { workspace = true, features = [ "use-dev-tty", "libc" ] } diff --git a/yazi-adapter/src/drivers/chafa.rs b/yazi-adapter/src/drivers/chafa.rs index 0f2f2171..651eafc5 100644 --- a/yazi-adapter/src/drivers/chafa.rs +++ b/yazi-adapter/src/drivers/chafa.rs @@ -2,10 +2,10 @@ use std::{io::Write, path::PathBuf, process::Stdio}; use ansi_to_tui::IntoText; use anyhow::{Result, anyhow, bail}; -use crossterm::{cursor::MoveTo, queue}; use ratatui::layout::Rect; use tokio::process::Command; use yazi_emulator::Emulator; +use yazi_term::sequence::MoveTo; use crate::Adapter; @@ -62,7 +62,7 @@ impl Chafa { Emulator::move_lock((max.x, max.y), |w| { for (i, line) in lines.into_iter().enumerate() { w.write_all(line)?; - queue!(w, MoveTo(max.x, max.y + i as u16 + 1))?; + write!(w, "{}", MoveTo(max.x, max.y + i as u16 + 1))?; } Ok(area) }) @@ -72,7 +72,7 @@ impl Chafa { let s = " ".repeat(area.width as usize); Emulator::move_lock((0, 0), |w| { for y in area.top()..area.bottom() { - queue!(w, MoveTo(area.x, y))?; + write!(w, "{}", MoveTo(area.x, y))?; write!(w, "{s}")?; } Ok(()) diff --git a/yazi-adapter/src/drivers/iip.rs b/yazi-adapter/src/drivers/iip.rs index 8bcd3066..8859626b 100644 --- a/yazi-adapter/src/drivers/iip.rs +++ b/yazi-adapter/src/drivers/iip.rs @@ -2,11 +2,11 @@ use std::{fmt::Write, io::Write as ioWrite, path::PathBuf}; use anyhow::Result; use base64::{Engine, engine::{Config, general_purpose::STANDARD}}; -use crossterm::{cursor::MoveTo, queue}; use image::{DynamicImage, ExtendedColorType, ImageEncoder, codecs::{jpeg::JpegEncoder, png::PngEncoder}}; use ratatui::layout::Rect; use yazi_config::YAZI; use yazi_emulator::{CLOSE, Emulator, START}; +use yazi_term::sequence::MoveTo; use crate::{Image, adapter::Adapter}; @@ -30,7 +30,7 @@ impl Iip { let s = " ".repeat(area.width as usize); Emulator::move_lock((0, 0), |w| { for y in area.top()..area.bottom() { - queue!(w, MoveTo(area.x, y))?; + write!(w, "{}", MoveTo(area.x, y))?; write!(w, "{s}")?; } Ok(()) diff --git a/yazi-adapter/src/drivers/kgp.rs b/yazi-adapter/src/drivers/kgp.rs index fdab8251..9cb533f7 100644 --- a/yazi-adapter/src/drivers/kgp.rs +++ b/yazi-adapter/src/drivers/kgp.rs @@ -3,11 +3,11 @@ use std::{io::Write, path::PathBuf}; use anyhow::Result; use base64::{Engine, engine::general_purpose}; -use crossterm::{cursor::MoveTo, queue}; use image::DynamicImage; -use ratatui::layout::Rect; +use ratatui::{layout::Rect, style::Color}; use yazi_emulator::{CLOSE, ESCAPE, Emulator, START}; use yazi_shim::cell::SyncCell; +use yazi_term::sequence::{MoveTo, SetFg}; use crate::{adapter::Adapter, image::Image}; @@ -334,7 +334,7 @@ impl Kgp { let s = " ".repeat(area.width as usize); Emulator::move_lock((0, 0), |w| { for y in area.top()..area.bottom() { - queue!(w, MoveTo(area.x, y))?; + write!(w, "{}", MoveTo(area.x, y))?; write!(w, "{s}")?; } @@ -385,10 +385,10 @@ impl Kgp { let id = Self::image_id(); let (r, g, b) = ((id >> 16) & 0xff, (id >> 8) & 0xff, id & 0xff); - write!(buf, "\x1b[38;2;{r};{g};{b}m")?; + write!(buf, "{}", SetFg(Color::Rgb(r as u8, g as u8, b as u8)))?; for y in 0..area.height { - write!(buf, "\x1b[{};{}H", area.y + y + 1, area.x + 1)?; + write!(buf, "{}", MoveTo(area.x, area.y + y))?; for x in 0..area.width { write!(buf, "\u{10EEEE}")?; write!(buf, "{}", *DIACRITICS.get(y as usize).unwrap_or(&DIACRITICS[0]))?; diff --git a/yazi-adapter/src/drivers/sixel.rs b/yazi-adapter/src/drivers/sixel.rs index daa7ca00..1a39661e 100644 --- a/yazi-adapter/src/drivers/sixel.rs +++ b/yazi-adapter/src/drivers/sixel.rs @@ -1,12 +1,12 @@ use std::{io::Write, path::PathBuf}; use anyhow::{Result, bail}; -use crossterm::{cursor::MoveTo, queue}; use image::{DynamicImage, GenericImageView, RgbImage}; use palette::{Srgb, cast::ComponentsAs}; use quantette::{PaletteSize, color_map::IndexedColorMap, wu::{BinnerU8x3, WuU8x3}}; use ratatui::layout::Rect; use yazi_emulator::{CLOSE, ESCAPE, Emulator, START}; +use yazi_term::sequence::MoveTo; use crate::{Image, adapter::Adapter}; @@ -35,7 +35,7 @@ impl Sixel { let s = " ".repeat(area.width as usize); Emulator::move_lock((0, 0), |w| { for y in area.top()..area.bottom() { - queue!(w, MoveTo(area.x, y))?; + write!(w, "{}", MoveTo(area.x, y))?; write!(w, "{s}")?; } Ok(()) diff --git a/yazi-binding/Cargo.toml b/yazi-binding/Cargo.toml index 70886abe..1eb4fcc1 100644 --- a/yazi-binding/Cargo.toml +++ b/yazi-binding/Cargo.toml @@ -24,13 +24,13 @@ yazi-fs = { path = "../yazi-fs", version = "26.5.6" } yazi-macro = { path = "../yazi-macro", version = "26.5.6" } yazi-shared = { path = "../yazi-shared", version = "26.5.6" } yazi-shim = { path = "../yazi-shim", version = "26.5.6" } +yazi-term = { path = "../yazi-term", version = "26.5.6" } yazi-vfs = { path = "../yazi-vfs", version = "26.5.6" } yazi-widgets = { path = "../yazi-widgets", version = "26.5.6" } # External dependencies ansi-to-tui = { workspace = true } anyhow = { workspace = true } -crossterm = { workspace = true } futures = { workspace = true } hashbrown = { workspace = true } mlua = { workspace = true } @@ -45,8 +45,5 @@ unicode-width = { workspace = true } [target.'cfg(unix)'.dependencies] libc = { workspace = true } -[target.'cfg(target_os = "macos")'.dependencies] -crossterm = { workspace = true, features = [ "use-dev-tty", "libc" ] } - [target."cfg(windows)".dependencies] windows-sys = { version = "0.61.2", features = [ "Win32_Foundation", "Win32_System_JobObjects" ] } diff --git a/yazi-binding/src/mouse.rs b/yazi-binding/src/mouse.rs index 69a6b43c..bc959bf5 100644 --- a/yazi-binding/src/mouse.rs +++ b/yazi-binding/src/mouse.rs @@ -1,19 +1,19 @@ use std::ops::Deref; -use crossterm::event::MouseButton; use mlua::{UserData, UserDataFields}; +use yazi_term::event::{MouseButton, MouseEventKind}; #[derive(Clone, Copy)] -pub struct MouseEvent(crossterm::event::MouseEvent); +pub struct MouseEvent(yazi_term::event::MouseEvent); impl Deref for MouseEvent { - type Target = crossterm::event::MouseEvent; + type Target = yazi_term::event::MouseEvent; fn deref(&self) -> &Self::Target { &self.0 } } -impl From for MouseEvent { - fn from(event: crossterm::event::MouseEvent) -> Self { Self(event) } +impl From for MouseEvent { + fn from(event: yazi_term::event::MouseEvent) -> Self { Self(event) } } impl UserData for MouseEvent { @@ -21,15 +21,15 @@ impl UserData for MouseEvent { fields.add_field_method_get("x", |_, me| Ok(me.column)); fields.add_field_method_get("y", |_, me| Ok(me.row)); fields.add_field_method_get("is_left", |_, me| { - use crossterm::event::MouseEventKind as K; + use MouseEventKind as K; Ok(matches!(me.kind, K::Down(b) | K::Up(b) | K::Drag(b) if b == MouseButton::Left)) }); fields.add_field_method_get("is_right", |_, me| { - use crossterm::event::MouseEventKind as K; + use MouseEventKind as K; Ok(matches!(me.kind, K::Down(b) | K::Up(b) | K::Drag(b) if b == MouseButton::Right)) }); fields.add_field_method_get("is_middle", |_, me| { - use crossterm::event::MouseEventKind as K; + use MouseEventKind as K; Ok(matches!(me.kind, K::Down(b) | K::Up(b) | K::Drag(b) if b == MouseButton::Middle)) }); } diff --git a/yazi-boot/Cargo.toml b/yazi-boot/Cargo.toml index 5fb079a5..e226fade 100644 --- a/yazi-boot/Cargo.toml +++ b/yazi-boot/Cargo.toml @@ -20,6 +20,7 @@ yazi-fs = { path = "../yazi-fs", version = "26.5.6" } yazi-macro = { path = "../yazi-macro", version = "26.5.6" } yazi-shared = { path = "../yazi-shared", version = "26.5.6" } yazi-shim = { path = "../yazi-shim", version = "26.5.6" } +yazi-term = { path = "../yazi-term", version = "26.5.6" } yazi-vfs = { path = "../yazi-vfs", version = "26.5.6" } # External dependencies @@ -33,7 +34,7 @@ yazi-shared = { path = "../yazi-shared", version = "26.5.6" } # External dependencies clap = { workspace = true } -clap_complete = "4.6.3" +clap_complete = "4.6.5" clap_complete_fig = "4.5.2" clap_complete_nushell = "4.6.0" vergen-gitcl = { version = "9.1.0", features = [ "build", "rustc" ] } diff --git a/yazi-boot/src/actions/debug.rs b/yazi-boot/src/actions/debug.rs index 4db500cc..455bc3cb 100644 --- a/yazi-boot/src/actions/debug.rs +++ b/yazi-boot/src/actions/debug.rs @@ -5,6 +5,7 @@ use yazi_config::{THEME, YAZI}; use yazi_emulator::Mux; use yazi_fs::Xdg; use yazi_shared::timestamp_us; +use yazi_term::TERM; use super::Actions; @@ -39,7 +40,7 @@ impl Actions { writeln!(s, "\nAdapter")?; writeln!(s, " Adapter.matches : {:?}", yazi_adapter::ADAPTOR)?; - writeln!(s, " Dimension.available: {:?}", yazi_emulator::Dimension::available())?; + writeln!(s, " Dimension.available: {:?}", TERM.dimension())?; writeln!(s, "\nDesktop")?; writeln!(s, " XDG_SESSION_TYPE : {:?}", env::var_os("XDG_SESSION_TYPE"))?; diff --git a/yazi-cli/Cargo.toml b/yazi-cli/Cargo.toml index eaf05fce..8c628d0f 100644 --- a/yazi-cli/Cargo.toml +++ b/yazi-cli/Cargo.toml @@ -18,12 +18,11 @@ yazi-dds = { path = "../yazi-dds", version = "26.5.6" } yazi-fs = { path = "../yazi-fs", version = "26.5.6" } yazi-macro = { path = "../yazi-macro", version = "26.5.6" } yazi-shared = { path = "../yazi-shared", version = "26.5.6" } -yazi-shim = { path = "../yazi-shim", version = "26.5.6" } +yazi-term = { path = "../yazi-term", version = "26.5.6" } # External dependencies anyhow = { workspace = true } clap = { workspace = true } -crossterm = { workspace = true } hashbrown = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } @@ -37,16 +36,13 @@ yazi-shared = { path = "../yazi-shared", version = "26.5.6" } # External build dependencies anyhow = { workspace = true } clap = { workspace = true } -clap_complete = "4.6.3" +clap_complete = "4.6.5" clap_complete_fig = "4.5.2" clap_complete_nushell = "4.6.0" serde = { workspace = true } serde_json = { workspace = true } vergen-gitcl = { version = "9.1.0", features = [ "build" ] } -[target.'cfg(target_os = "macos")'.dependencies] -crossterm = { workspace = true, features = [ "use-dev-tty", "libc" ] } - [[bin]] name = "ya" path = "src/main.rs" diff --git a/yazi-cli/src/package/dependency.rs b/yazi-cli/src/package/dependency.rs index 08e78177..ff5927ce 100644 --- a/yazi-cli/src/package/dependency.rs +++ b/yazi-cli/src/package/dependency.rs @@ -1,4 +1,4 @@ -use std::{env, io::{self, BufWriter}, path::{Path, PathBuf}, str::FromStr}; +use std::{env, io, path::{Path, PathBuf}, str::FromStr}; use anyhow::{Result, bail}; use serde::{Deserialize, Deserializer, Serialize, Serializer}; @@ -48,20 +48,19 @@ impl Dependency { pub(super) fn header(&self, s: &str) -> Result<()> { use std::io::IsTerminal; - use crossterm::style::{Attribute, Print, SetAttributes}; - use yazi_shim::crossterm::If; + use yazi_macro::writef; + use yazi_term::sequence::{If, SetSgr}; let ansi = env::var_os("YA_FORCE_ANSI").is_some_and(|v| v == "1") || io::stdout().is_terminal(); - crossterm::execute!( - BufWriter::new(io::stdout()), - Print("\n"), - If(ansi, SetAttributes(Attribute::Reverse.into())), - If(ansi, SetAttributes(Attribute::Bold.into())), - Print(" "), - Print(s.replacen("{name}", &self.name, 1)), - Print(" "), - If(ansi, SetAttributes(Attribute::Reset.into())), - Print("\n\n"), + writef!( + io::stdout(), + "\n{}{}{} {} {}{}\n\n", + If(ansi, SetSgr::Reverse), + If(ansi, SetSgr::Bold), + " ", + s.replacen("{name}", &self.name, 1), + " ", + If(ansi, SetSgr::Reset), )?; Ok(()) } diff --git a/yazi-config/Cargo.toml b/yazi-config/Cargo.toml index cb6130d8..ea98433b 100644 --- a/yazi-config/Cargo.toml +++ b/yazi-config/Cargo.toml @@ -18,13 +18,13 @@ yazi-fs = { path = "../yazi-fs", version = "26.5.6" } yazi-macro = { path = "../yazi-macro", version = "26.5.6" } yazi-shared = { path = "../yazi-shared", version = "26.5.6" } yazi-shim = { path = "../yazi-shim", version = "26.5.6" } +yazi-term = { path = "../yazi-term", version = "26.5.6" } yazi-tty = { path = "../yazi-tty", version = "26.5.9" } # External dependencies anyhow = { workspace = true } arc-swap = { workspace = true } bitflags = { workspace = true } -crossterm = { workspace = true } globset = { workspace = true } hashbrown = { workspace = true } indexmap = { workspace = true } @@ -36,6 +36,3 @@ strum = { workspace = true } tokio = { workspace = true } toml = { workspace = true } tracing = { workspace = true } - -[target.'cfg(target_os = "macos")'.dependencies] -crossterm = { workspace = true, features = [ "use-dev-tty", "libc" ] } diff --git a/yazi-config/src/keymap/key.rs b/yazi-config/src/keymap/key.rs index 5b85c025..4cd39b47 100644 --- a/yazi-config/src/keymap/key.rs +++ b/yazi-config/src/keymap/key.rs @@ -1,7 +1,7 @@ use std::{fmt::{Display, Write}, str::FromStr}; use anyhow::bail; -use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; +use yazi_term::event::{KeyCode, KeyEvent, Modifiers}; #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] pub struct Key { @@ -30,29 +30,12 @@ impl Default for Key { impl From for Key { fn from(value: KeyEvent) -> Self { - // For alphabet: - // Unix : => Char("A") + SHIFT - // Windows : => Char("A") + SHIFT - // - // For non-alphabet: - // Unix : => Char("~") + NULL - // Windows : => Char("~") + SHIFT - // - // So we detect `Char("~") + SHIFT`, and change it to `Char("~") + NULL` - // for consistent behavior between OSs. - - let shift = match (value.code, value.modifiers) { - (KeyCode::Char(c), _) => c.is_ascii_uppercase(), - (KeyCode::BackTab, _) => false, - (_, m) => m.contains(KeyModifiers::SHIFT), - }; - Self { - code: value.code, - shift, - ctrl: value.modifiers.contains(KeyModifiers::CONTROL), - alt: value.modifiers.contains(KeyModifiers::ALT), - super_: value.modifiers.contains(KeyModifiers::SUPER), + code: value.code, + shift: value.modifiers.contains(Modifiers::SHIFT), + ctrl: value.modifiers.contains(Modifiers::CONTROL), + alt: value.modifiers.contains(Modifiers::ALT), + super_: value.modifiers.contains(Modifiers::SUPER), } } } @@ -92,29 +75,28 @@ impl FromStr for Key { "pageup" => key.code = KeyCode::PageUp, "pagedown" => key.code = KeyCode::PageDown, "tab" => key.code = KeyCode::Tab, - "backtab" => key.code = KeyCode::BackTab, "delete" => key.code = KeyCode::Delete, "insert" => key.code = KeyCode::Insert, - "f1" => key.code = KeyCode::F(1), - "f2" => key.code = KeyCode::F(2), - "f3" => key.code = KeyCode::F(3), - "f4" => key.code = KeyCode::F(4), - "f5" => key.code = KeyCode::F(5), - "f6" => key.code = KeyCode::F(6), - "f7" => key.code = KeyCode::F(7), - "f8" => key.code = KeyCode::F(8), - "f9" => key.code = KeyCode::F(9), - "f10" => key.code = KeyCode::F(10), - "f11" => key.code = KeyCode::F(11), - "f12" => key.code = KeyCode::F(12), - "f13" => key.code = KeyCode::F(13), - "f14" => key.code = KeyCode::F(14), - "f15" => key.code = KeyCode::F(15), - "f16" => key.code = KeyCode::F(16), - "f17" => key.code = KeyCode::F(17), - "f18" => key.code = KeyCode::F(18), - "f19" => key.code = KeyCode::F(19), - "esc" => key.code = KeyCode::Esc, + "f1" => key.code = KeyCode::Fn(1), + "f2" => key.code = KeyCode::Fn(2), + "f3" => key.code = KeyCode::Fn(3), + "f4" => key.code = KeyCode::Fn(4), + "f5" => key.code = KeyCode::Fn(5), + "f6" => key.code = KeyCode::Fn(6), + "f7" => key.code = KeyCode::Fn(7), + "f8" => key.code = KeyCode::Fn(8), + "f9" => key.code = KeyCode::Fn(9), + "f10" => key.code = KeyCode::Fn(10), + "f11" => key.code = KeyCode::Fn(11), + "f12" => key.code = KeyCode::Fn(12), + "f13" => key.code = KeyCode::Fn(13), + "f14" => key.code = KeyCode::Fn(14), + "f15" => key.code = KeyCode::Fn(15), + "f16" => key.code = KeyCode::Fn(16), + "f17" => key.code = KeyCode::Fn(17), + "f18" => key.code = KeyCode::Fn(18), + "f19" => key.code = KeyCode::Fn(19), + "esc" => key.code = KeyCode::Escape, _ => match next { s if it.peek().is_none() => { @@ -166,29 +148,28 @@ impl Display for Key { KeyCode::PageUp => "PageUp", KeyCode::PageDown => "PageDown", KeyCode::Tab => "Tab", - KeyCode::BackTab => "BackTab", KeyCode::Delete => "Delete", KeyCode::Insert => "Insert", - KeyCode::F(1) => "F1", - KeyCode::F(2) => "F2", - KeyCode::F(3) => "F3", - KeyCode::F(4) => "F4", - KeyCode::F(5) => "F5", - KeyCode::F(6) => "F6", - KeyCode::F(7) => "F7", - KeyCode::F(8) => "F8", - KeyCode::F(9) => "F9", - KeyCode::F(10) => "F10", - KeyCode::F(11) => "F11", - KeyCode::F(12) => "F12", - KeyCode::F(13) => "F13", - KeyCode::F(14) => "F14", - KeyCode::F(15) => "F15", - KeyCode::F(16) => "F16", - KeyCode::F(17) => "F17", - KeyCode::F(18) => "F18", - KeyCode::F(19) => "F19", - KeyCode::Esc => "Esc", + KeyCode::Fn(1) => "F1", + KeyCode::Fn(2) => "F2", + KeyCode::Fn(3) => "F3", + KeyCode::Fn(4) => "F4", + KeyCode::Fn(5) => "F5", + KeyCode::Fn(6) => "F6", + KeyCode::Fn(7) => "F7", + KeyCode::Fn(8) => "F8", + KeyCode::Fn(9) => "F9", + KeyCode::Fn(10) => "F10", + KeyCode::Fn(11) => "F11", + KeyCode::Fn(12) => "F12", + KeyCode::Fn(13) => "F13", + KeyCode::Fn(14) => "F14", + KeyCode::Fn(15) => "F15", + KeyCode::Fn(16) => "F16", + KeyCode::Fn(17) => "F17", + KeyCode::Fn(18) => "F18", + KeyCode::Fn(19) => "F19", + KeyCode::Escape => "Esc", KeyCode::Char(' ') => "Space", KeyCode::Char(c) => { diff --git a/yazi-config/src/lib.rs b/yazi-config/src/lib.rs index a28dfca3..0ee60811 100644 --- a/yazi-config/src/lib.rs +++ b/yazi-config/src/lib.rs @@ -5,6 +5,7 @@ yazi_macro::mod_flat!(icon layout mixing pattern platform preset priority select use std::io::{Read, Write}; use yazi_shim::{cell::{RoCell, SyncCell}, toml::{DeserializeOver, DeserializeOverWith}}; +use yazi_term::sequence::SetSgr; use yazi_tty::TTY; pub static YAZI: RoCell = RoCell::new(); @@ -74,14 +75,12 @@ fn wait_for_key(e: anyhow::Error) -> anyhow::Result<()> { writeln!(stdout, "\nCaused by:\n{src}")?; } - use crossterm::style::{Attribute, Print, SetAttributes}; - crossterm::execute!( + writeln!( stdout, - SetAttributes(Attribute::Reverse.into()), - SetAttributes(Attribute::Bold.into()), - Print("Press to continue with preset settings..."), - SetAttributes(Attribute::Reset.into()), - Print("\n"), + "{}{}Press to continue with preset settings...{}", + SetSgr::Reverse, + SetSgr::Bold, + SetSgr::Reset )?; TTY.reader().read_exact(&mut [0])?; diff --git a/yazi-config/src/mgr/mouse.rs b/yazi-config/src/mgr/mouse.rs index 218d4967..c6e74cf8 100644 --- a/yazi-config/src/mgr/mouse.rs +++ b/yazi-config/src/mgr/mouse.rs @@ -1,7 +1,7 @@ use anyhow::{Result, bail}; use bitflags::bitflags; -use crossterm::event::MouseEventKind; use serde::{Deserialize, Serialize}; +use yazi_term::event::MouseEventKind; bitflags! { #[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] @@ -45,8 +45,8 @@ impl From for Vec { } } -impl From for MouseEvents { - fn from(value: crossterm::event::MouseEventKind) -> Self { +impl From for MouseEvents { + fn from(value: MouseEventKind) -> Self { match value { MouseEventKind::Down(_) | MouseEventKind::Up(_) => Self::CLICK, MouseEventKind::ScrollDown | MouseEventKind::ScrollUp => Self::SCROLL, diff --git a/yazi-config/src/popup/position.rs b/yazi-config/src/popup/position.rs index 9e36a4c5..3ea8bd89 100644 --- a/yazi-config/src/popup/position.rs +++ b/yazi-config/src/popup/position.rs @@ -1,5 +1,5 @@ -use crossterm::terminal::WindowSize; use ratatui::layout::Rect; +use yazi_term::Dimension; use super::{Offset, Origin}; @@ -12,15 +12,15 @@ pub struct Position { impl Position { pub const fn new(origin: Origin, offset: Offset) -> Self { Self { origin, offset } } - pub fn rect(&self, WindowSize { columns, rows, .. }: WindowSize) -> Rect { + pub fn rect(&self, Dimension { cols, rows, .. }: Dimension) -> Rect { use Origin::*; let Offset { x, y, width, height } = self.offset; - let max_x = columns.saturating_sub(width); + let max_x = cols.saturating_sub(width); let new_x = match self.origin { TopLeft | BottomLeft => x.clamp(0, max_x as i16) as u16, TopCenter | BottomCenter | Center => { - (columns / 2).saturating_sub(width / 2).saturating_add_signed(x).clamp(0, max_x) + (cols / 2).saturating_sub(width / 2).saturating_add_signed(x).clamp(0, max_x) } TopRight | BottomRight => max_x.saturating_add_signed(x).clamp(0, max_x), Hovered => unreachable!(), @@ -37,18 +37,18 @@ impl Position { Rect { x: new_x, y: new_y, - width: width.min(columns.saturating_sub(new_x)), + width: width.min(cols.saturating_sub(new_x)), height: height.min(rows.saturating_sub(new_y)), } } - pub fn sticky(WindowSize { columns, rows, .. }: WindowSize, base: Rect, offset: Offset) -> Rect { + pub fn sticky(Dimension { cols, rows, .. }: Dimension, base: Rect, offset: Offset) -> Rect { let Offset { x, y, width, height } = offset; let above = base.y.saturating_add(base.height).saturating_add(height).saturating_add_signed(y) > rows; - let new_x = base.x.saturating_add_signed(x).clamp(0, columns.saturating_sub(width)); + let new_x = base.x.saturating_add_signed(x).clamp(0, cols.saturating_sub(width)); let new_y = if above { base.y.saturating_sub(height.saturating_sub(y.unsigned_abs())) } else { @@ -58,7 +58,7 @@ impl Position { Rect { x: new_x, y: new_y, - width: width.min(columns.saturating_sub(new_x)), + width: width.min(cols.saturating_sub(new_x)), height: height.min(rows.saturating_sub(new_y)), } } diff --git a/yazi-core/Cargo.toml b/yazi-core/Cargo.toml index 9feeb567..137c448b 100644 --- a/yazi-core/Cargo.toml +++ b/yazi-core/Cargo.toml @@ -21,20 +21,19 @@ yazi-adapter = { path = "../yazi-adapter", version = "26.5.6" } yazi-binding = { path = "../yazi-binding", version = "26.5.6" } yazi-config = { path = "../yazi-config", version = "26.5.6" } yazi-dds = { path = "../yazi-dds", version = "26.5.6" } -yazi-emulator = { path = "../yazi-emulator", version = "26.5.6" } yazi-fs = { path = "../yazi-fs", version = "26.5.6" } yazi-macro = { path = "../yazi-macro", version = "26.5.6" } yazi-runner = { path = "../yazi-runner", version = "26.5.6" } yazi-scheduler = { path = "../yazi-scheduler", version = "26.5.6" } yazi-shared = { path = "../yazi-shared", version = "26.5.6" } yazi-shim = { path = "../yazi-shim", version = "26.5.6" } +yazi-term = { path = "../yazi-term", version = "26.5.6" } yazi-vfs = { path = "../yazi-vfs", version = "26.5.6" } yazi-watcher = { path = "../yazi-watcher", version = "26.5.6" } yazi-widgets = { path = "../yazi-widgets", version = "26.5.6" } # External dependencies anyhow = { workspace = true } -crossterm = { workspace = true } dyn-clone = { workspace = true } hashbrown = { workspace = true } indexmap = { workspace = true } @@ -51,6 +50,3 @@ tokio-util = { workspace = true } tracing = { workspace = true } unicode-width = { workspace = true } yazi-prebuilt = "0.1.0" - -[target.'cfg(target_os = "macos")'.dependencies] -crossterm = { workspace = true, features = [ "use-dev-tty", "libc" ] } diff --git a/yazi-core/src/app/plugin.rs b/yazi-core/src/app/plugin.rs index 900186f3..3677cec4 100644 --- a/yazi-core/src/app/plugin.rs +++ b/yazi-core/src/app/plugin.rs @@ -99,7 +99,7 @@ impl Clone for Box { fn clone(&self) -> Self { dyn_clone::clone_box(&**self) } } -impl fmt::Debug for dyn PluginCallback { +impl Debug for dyn PluginCallback { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("PluginCallback").finish_non_exhaustive() } diff --git a/yazi-core/src/core.rs b/yazi-core/src/core.rs index 8df6f81a..fcb940cd 100644 --- a/yazi-core/src/core.rs +++ b/yazi-core/src/core.rs @@ -1,6 +1,6 @@ -use crossterm::cursor::SetCursorStyle; use ratatui::layout::{Position, Rect}; use yazi_shared::Layer; +use yazi_term::CursorStyle; use crate::{cmp::Cmp, confirm::Confirm, help::Help, input::Input, mgr::Mgr, notify::Notify, pick::Pick, tab::{Folder, Tab}, tasks::Tasks, which::Which}; @@ -31,7 +31,7 @@ impl Core { } } - pub fn cursor(&self) -> Option<(Position, SetCursorStyle)> { + pub fn cursor(&self) -> Option<(Position, CursorStyle)> { if self.input.visible { let Rect { x, y, .. } = self.mgr.area(self.input.position); return Some(( diff --git a/yazi-core/src/help/help.rs b/yazi-core/src/help/help.rs index 1cb4a05b..a6ef0c9a 100644 --- a/yazi-core/src/help/help.rs +++ b/yazi-core/src/help/help.rs @@ -1,10 +1,9 @@ use anyhow::Result; -use crossterm::{cursor::SetCursorStyle, event::KeyCode}; use unicode_width::UnicodeWidthStr; use yazi_config::{KEYMAP, YAZI, keymap::{Chord, Key}}; -use yazi_emulator::Dimension; use yazi_macro::{act, render, render_and}; use yazi_shared::Layer; +use yazi_term::{CursorStyle, TERM, event::KeyCode}; use yazi_widgets::Scrollable; use crate::help::HELP_MARGIN; @@ -27,7 +26,7 @@ impl Help { pub fn r#type(&mut self, key: &Key) -> Result { let Some(input) = &mut self.in_filter else { return Ok(false) }; match key { - Key { code: KeyCode::Esc, shift: false, ctrl: false, alt: false, super_: false } => { + Key { code: KeyCode::Escape, shift: false, ctrl: false, alt: false, super_: false } => { self.in_filter = None; render!(); } @@ -85,26 +84,22 @@ impl Help { return None; } if let Some(kw) = self.keyword() { - return Some((kw.width() as u16, Dimension::available().rows)); + return Some((kw.width() as u16, TERM.dimension().rows)); } None } pub fn rel_cursor(&self) -> usize { self.cursor - self.offset } - pub fn cursor_shape(&self) -> SetCursorStyle { - if YAZI.input.cursor_blink { - SetCursorStyle::BlinkingBlock - } else { - SetCursorStyle::SteadyBlock - } + pub fn cursor_shape(&self) -> CursorStyle { + if YAZI.input.cursor_blink { CursorStyle::BlinkingBlock } else { CursorStyle::SteadyBlock } } } impl Scrollable for Help { fn total(&self) -> usize { self.bindings.len() } - fn limit(&self) -> usize { Dimension::available().rows.saturating_sub(HELP_MARGIN) as usize } + fn limit(&self) -> usize { TERM.dimension().rows.saturating_sub(HELP_MARGIN) as usize } fn cursor_mut(&mut self) -> &mut usize { &mut self.cursor } diff --git a/yazi-core/src/mgr/mgr.rs b/yazi-core/src/mgr/mgr.rs index ad0cd876..437315ef 100644 --- a/yazi-core/src/mgr/mgr.rs +++ b/yazi-core/src/mgr/mgr.rs @@ -2,9 +2,9 @@ use std::iter; use ratatui::layout::Rect; use yazi_config::popup::{Origin, Position}; -use yazi_emulator::Dimension; use yazi_fs::Splatable; use yazi_shared::url::{AsUrl, Url, UrlBuf}; +use yazi_term::TERM; use yazi_watcher::Watcher; use super::{Batcher, Mimetype, Tabs, Yanked}; @@ -35,7 +35,7 @@ impl Mgr { if pos.origin == Origin::Hovered { self.active().hovered_rect_based(pos) } else { - pos.rect(Dimension::available().into()) + pos.rect(TERM.dimension()) } } diff --git a/yazi-core/src/tab/tab.rs b/yazi-core/src/tab/tab.rs index 156121e6..4f6cd367 100644 --- a/yazi-core/src/tab/tab.rs +++ b/yazi-core/src/tab/tab.rs @@ -4,9 +4,9 @@ use anyhow::Result; use ratatui::layout::Rect; use tokio::task::JoinHandle; use yazi_config::{LAYOUT, popup::{Origin, Position}}; -use yazi_emulator::Dimension; use yazi_fs::File; use yazi_shared::{Id, Ids, url::{UrlBuf, UrlLike}}; +use yazi_term::TERM; use super::{Backstack, Finder, Folder, History, Mode, Preference, Preview}; use crate::{spot::Spot, tab::Selected}; @@ -90,11 +90,11 @@ impl Tab { } pub fn hovered_rect_based(&self, pos: Position) -> Rect { - let ws = Dimension::available().into(); + let dim = TERM.dimension(); if let Some(r) = self.hovered_rect() { - Position::sticky(ws, r, pos.offset) + Position::sticky(dim, r, pos.offset) } else { - Position::new(Origin::TopCenter, pos.offset).rect(ws) + Position::new(Origin::TopCenter, pos.offset).rect(dim) } } diff --git a/yazi-core/src/tasks/tasks.rs b/yazi-core/src/tasks/tasks.rs index 7c46c599..1056e85b 100644 --- a/yazi-core/src/tasks/tasks.rs +++ b/yazi-core/src/tasks/tasks.rs @@ -1,8 +1,8 @@ use std::{sync::Arc, time::Duration}; use tokio::{task::JoinHandle, time::sleep}; -use yazi_emulator::Dimension; use yazi_scheduler::{Scheduler, TaskSnap, TaskSummary}; +use yazi_term::TERM; use super::{TASKS_BORDER, TASKS_PADDING, TASKS_PERCENT}; use crate::AppProxy; @@ -52,8 +52,8 @@ impl Tasks { } pub fn limit() -> usize { - ((Dimension::available().rows * TASKS_PERCENT / 100) - .saturating_sub(TASKS_BORDER + TASKS_PADDING) as usize) + ((TERM.dimension().rows * TASKS_PERCENT / 100).saturating_sub(TASKS_BORDER + TASKS_PADDING) + as usize) / 3 } diff --git a/yazi-dds/src/ember/cd.rs b/yazi-dds/src/ember/cd.rs index 246cad4e..2c790458 100644 --- a/yazi-dds/src/ember/cd.rs +++ b/yazi-dds/src/ember/cd.rs @@ -35,7 +35,7 @@ impl IntoLua for EmberCd<'_> { lua .create_table_from([ ("tab", self.tab.get().into_lua(lua)?), - ("url", Some(self.url).filter(|_| !self.dummy).map(yazi_binding::Url::new).into_lua(lua)?), + ("url", (!self.dummy).then_some(self.url).map(yazi_binding::Url::new).into_lua(lua)?), ])? .into_lua(lua) } diff --git a/yazi-emulator/Cargo.toml b/yazi-emulator/Cargo.toml index f31ea4ce..7f3c1fb8 100644 --- a/yazi-emulator/Cargo.toml +++ b/yazi-emulator/Cargo.toml @@ -16,15 +16,13 @@ workspace = true yazi-macro = { path = "../yazi-macro", version = "26.5.6" } yazi-shared = { path = "../yazi-shared", version = "26.5.6" } yazi-shim = { path = "../yazi-shim", version = "26.5.6" } +yazi-term = { path = "../yazi-term", version = "26.5.6" } yazi-tty = { path = "../yazi-tty", version = "26.5.9" } # External dependencies -anyhow = { workspace = true } -crossterm = { workspace = true } -either = { workspace = true } -scopeguard = { workspace = true } -tokio = { workspace = true } -tracing = { workspace = true } - -[target.'cfg(target_os = "macos")'.dependencies] -crossterm = { workspace = true, features = [ "use-dev-tty", "libc" ] } +anyhow = { workspace = true } +either = { workspace = true } +ratatui = { workspace = true } +scopeguard = { workspace = true } +tokio = { workspace = true } +tracing = { workspace = true } diff --git a/yazi-emulator/src/dimension.rs b/yazi-emulator/src/dimension.rs index dcbcb6fd..29e4e673 100644 --- a/yazi-emulator/src/dimension.rs +++ b/yazi-emulator/src/dimension.rs @@ -1,59 +1,16 @@ -use std::mem; - -use crossterm::terminal::WindowSize; +use yazi_term::TERM; use crate::EMULATOR; -#[derive(Clone, Copy, Debug)] -pub struct Dimension { - pub rows: u16, - pub columns: u16, - pub width: u16, - pub height: u16, -} - -impl From for Dimension { - fn from(s: WindowSize) -> Self { - Self { rows: s.rows, columns: s.columns, width: s.width, height: s.height } - } -} - -impl From for WindowSize { - fn from(d: Dimension) -> Self { - Self { rows: d.rows, columns: d.columns, width: d.width, height: d.height } - } -} +#[derive(Clone, Copy, Debug, Default)] +pub struct Dimension; impl Dimension { - pub fn available() -> Self { - let mut size = WindowSize { rows: 0, columns: 0, width: 0, height: 0 }; - if let Ok(s) = crossterm::terminal::window_size() { - _ = mem::replace(&mut size, s); - } - - if (size.columns == 0 || size.rows == 0) - && let Ok((cols, rows)) = crossterm::terminal::size() - { - size.columns = cols; - size.rows = rows; - } - - size.into() - } - - pub fn ratio(self) -> Option<(f64, f64)> { - if self.rows == 0 || self.columns == 0 || self.width == 0 || self.height == 0 { - None - } else { - Some((self.width as f64 / self.columns as f64, self.height as f64 / self.rows as f64)) - } - } - pub fn cell_size() -> Option<(f64, f64)> { let emu = &*EMULATOR; Some(if emu.force_16t { (emu.csi_16t.0 as f64, emu.csi_16t.1 as f64) - } else if let Some(r) = Self::available().ratio() { + } else if let Some(r) = TERM.dimension().ratio() { r } else if emu.csi_16t.0 != 0 && emu.csi_16t.1 != 0 { (emu.csi_16t.0 as f64, emu.csi_16t.1 as f64) diff --git a/yazi-emulator/src/emulator.rs b/yazi-emulator/src/emulator.rs index cf042132..4fa69927 100644 --- a/yazi-emulator/src/emulator.rs +++ b/yazi-emulator/src/emulator.rs @@ -1,15 +1,17 @@ -use std::{io::BufWriter, time::Duration}; +use std::{io::{self, BufWriter, Write}, time::Duration}; use anyhow::Result; -use crossterm::{cursor::{RestorePosition, SavePosition}, execute, style::Print, terminal::{disable_raw_mode, enable_raw_mode}}; use either::Either; +use ratatui::style::Color; use scopeguard::defer; use tokio::time::sleep; use tracing::{debug, error, warn}; +use yazi_macro::writef; use yazi_shim::cell::RoCell; +use yazi_term::{TERM, sequence::{HideCursor, If, KittyGraphicsQuery, MoveTo, RequestBgColor, RequestCellPixelSize, RequestDA1, RequestXtVersion, RestoreCursorPos, SaveCursorPos, SetFg, SetSgr, ShowCursor}}; use yazi_tty::{Handle, TTY}; -use crate::{Brand, Dimension, Mux, TMUX, Unknown}; +use crate::{Brand, Mux, TMUX, Unknown}; pub static EMULATOR: RoCell = RoCell::new(); @@ -36,25 +38,18 @@ impl Default for Emulator { impl Emulator { pub fn detect() -> Result { - defer! { disable_raw_mode().ok(); } - enable_raw_mode()?; + defer! { TERM.enter_cooked_mode().ok(); } + TERM.enter_raw_mode()?; let resort = Brand::from_env(); - let kgp_seq = if resort.is_none() { - Mux::csi("\x1b_Gi=31,s=1,v=1,a=q,t=d,f=24;AAAA\x1b\\") - } else { - "".into() - }; - - execute!( + writef!( TTY.writer(), - SavePosition, - Print(kgp_seq), // Detect KGP - Print(Mux::csi("\x1b[>q")), // Request terminal version - Print("\x1b[16t"), // Request cell size - Print("\x1b]11;?\x07"), // Request background color - Print(Mux::csi("\x1b[0c")), // Request device attributes - RestorePosition + "{SaveCursorPos}{}{}{}{}{}{RestoreCursorPos}", + If(resort.is_none(), Mux::wrap(KittyGraphicsQuery)), + Mux::wrap(RequestXtVersion), + RequestCellPixelSize, + RequestBgColor, + Mux::wrap(RequestDA1), )?; let resp = Self::read_until_da1(); @@ -83,28 +78,26 @@ impl Emulator { where F: FnOnce(&mut BufWriter) -> Result, { - use std::{io::Write, thread, time::Duration}; - - use crossterm::{cursor::{Hide, MoveTo, Show}, queue}; + use std::{thread, time::Duration}; let mut w = TTY.lockout(); // I really don't want to add this, // But tmux and ConPTY sometimes cause the cursor position to get out of sync. if TMUX.get() || cfg!(windows) { - execute!(w, SavePosition, MoveTo(x, y), Show)?; - execute!(w, MoveTo(x, y), Show)?; - execute!(w, MoveTo(x, y), Show)?; + writef!(w, "{SaveCursorPos}{}{ShowCursor}", MoveTo(x, y))?; + writef!(w, "{}{ShowCursor}", MoveTo(x, y))?; + writef!(w, "{}{ShowCursor}", MoveTo(x, y))?; thread::sleep(Duration::from_millis(1)); } else { - queue!(w, SavePosition, MoveTo(x, y))?; + write!(w, "{SaveCursorPos}{}", MoveTo(x, y))?; } let result = cb(&mut w); if TMUX.get() || cfg!(windows) { - queue!(w, Hide, RestorePosition)?; + write!(w, "{HideCursor}{RestoreCursorPos}")?; } else { - queue!(w, RestorePosition)?; + write!(w, "{RestoreCursorPos}")?; } w.flush()?; @@ -148,21 +141,13 @@ impl Emulator { } async fn error_to_user() { - use crossterm::style::{Attribute, Color, Print, ResetColor, SetAttributes, SetForegroundColor}; - sleep(Duration::from_millis(400)).await; - _ = crossterm::execute!( - std::io::stderr(), - SetForegroundColor(Color::Red), - SetAttributes(Attribute::Bold.into()), - Print("\r\nTerminal response timeout: "), - ResetColor, - SetAttributes(Attribute::Reset.into()), - // - Print("The request sent by Yazi didn't receive a correct response.\r\n"), - Print( - "Please check your terminal environment as per: https://yazi-rs.github.io/docs/faq#trt\r\n" - ), + _ = writef!( + io::stderr(), + "{}{}\r\nTerminal response timeout: {}The request sent by Yazi didn't receive a correct response.\r\nPlease check your terminal environment as per: https://yazi-rs.github.io/docs/faq#trt\r\n", + SetFg(Color::Red), + SetSgr::Bold, + SetSgr::Reset, ); } @@ -206,8 +191,6 @@ impl Emulator { return false; } - Dimension::available() - .ratio() - .is_none_or(|(rw, rh)| rw.floor() as u16 != w || rh.floor() as u16 != h) + TERM.dimension().ratio().is_none_or(|(rw, rh)| rw.floor() as u16 != w || rh.floor() as u16 != h) } } diff --git a/yazi-emulator/src/mux.rs b/yazi-emulator/src/mux.rs index 736e4d4f..c790d7a9 100644 --- a/yazi-emulator/src/mux.rs +++ b/yazi-emulator/src/mux.rs @@ -1,9 +1,10 @@ -use std::borrow::Cow; +use std::fmt::{self, Display}; use anyhow::Result; use tracing::error; -use yazi_macro::time; +use yazi_macro::{time, writef}; use yazi_shim::cell::SyncCell; +use yazi_term::sequence::RequestDeviceStatus; use yazi_tty::TTY; use crate::Emulator; @@ -16,15 +17,24 @@ pub static CLOSE: SyncCell<&'static str> = SyncCell::new(""); pub struct Mux; impl Mux { - pub fn csi(s: &str) -> Cow<'_, str> { - if TMUX.get() { - Cow::Owned(format!( - "{START}{}{CLOSE}", - s.trim_start_matches('\x1b').replace('\x1b', ESCAPE.get()), - )) - } else { - Cow::Borrowed(s) + pub fn wrap(s: T) -> impl Display { + struct Wrapper(T); + + impl Display for Wrapper { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if !TMUX.get() { + return self.0.fmt(f); + } + + write!( + f, + "{START}{}{CLOSE}", + self.0.to_string().trim_start_matches('\x1b').replace('\x1b', ESCAPE.get()) + ) + } } + + Wrapper(s) } pub fn tmux_passthrough() { @@ -56,7 +66,7 @@ impl Mux { pub fn tmux_drain() -> Result<()> { if TMUX.get() { - crossterm::execute!(TTY.writer(), crossterm::style::Print(Self::csi("\x1b[5n")))?; + writef!(TTY.writer(), "{}", Self::wrap(RequestDeviceStatus))?; _ = Emulator::read_until_dsr(); } Ok(()) diff --git a/yazi-fm/Cargo.toml b/yazi-fm/Cargo.toml index 74e0322f..3a931c4d 100644 --- a/yazi-fm/Cargo.toml +++ b/yazi-fm/Cargo.toml @@ -24,7 +24,6 @@ yazi-boot = { path = "../yazi-boot", version = "26.5.6" } yazi-config = { path = "../yazi-config", version = "26.5.6" } yazi-core = { path = "../yazi-core", version = "26.5.6" } yazi-dds = { path = "../yazi-dds", version = "26.5.6" } -yazi-emulator = { path = "../yazi-emulator", version = "26.5.6" } yazi-fs = { path = "../yazi-fs", version = "26.5.6" } yazi-macro = { path = "../yazi-macro", version = "26.5.6" } yazi-parser = { path = "../yazi-parser", version = "26.5.6" } @@ -36,6 +35,7 @@ yazi-shared = { path = "../yazi-shared", version = "26.5.6" } yazi-shim = { path = "../yazi-shim", version = "26.5.6" } yazi-term = { path = "../yazi-term", version = "26.5.6" } yazi-tty = { path = "../yazi-tty", version = "26.5.9" } +yazi-tui = { path = "../yazi-tui", version = "26.5.6" } yazi-vfs = { path = "../yazi-vfs", version = "26.5.6" } yazi-watcher = { path = "../yazi-watcher", version = "26.5.6" } yazi-widgets = { path = "../yazi-widgets", version = "26.5.6" } @@ -43,7 +43,6 @@ yazi-widgets = { path = "../yazi-widgets", version = "26.5.6" } # External dependencies anyhow = { workspace = true } better-panic = "0.3.0" -crossterm = { workspace = true } fdlimit = "0.3.0" futures = { workspace = true } mlua = { workspace = true } @@ -60,9 +59,6 @@ tracing-subscriber = { version = "0.3.23", features = [ "env-filter" ] } libc = { workspace = true } signal-hook-tokio = { version = "0.4.0", features = [ "futures-v0_3" ] } -[target.'cfg(target_os = "macos")'.dependencies] -crossterm = { workspace = true, features = [ "use-dev-tty", "libc" ] } - [target.'cfg(all(not(target_os = "macos"), not(target_os = "windows")))'.dependencies] tikv-jemallocator = "0.6.1" diff --git a/yazi-fm/src/app/app.rs b/yazi-fm/src/app/app.rs index 936cda24..ac044cdb 100644 --- a/yazi-fm/src/app/app.rs +++ b/yazi-fm/src/app/app.rs @@ -6,14 +6,13 @@ use yazi_actor::Ctx; use yazi_core::Core; use yazi_macro::act; use yazi_shared::{data::Data, event::{Event, NEED_RENDER}}; -use yazi_term::Term; +use yazi_tui::Raterm; use crate::{Dispatcher, Signals}; pub(crate) struct App { - pub(crate) core: Core, - pub(crate) term: Option, - pub(crate) signals: Signals, + pub(crate) core: Core, + pub(crate) term: Option, need_render: u8, pub(crate) last_render: Instant, @@ -21,11 +20,10 @@ pub(crate) struct App { } impl App { - fn make(term: Term, signals: Signals) -> Result { + fn make(term: Raterm) -> Result { Ok(Self { core: Core::make(), term: Some(term), - signals, need_render: 0, last_render: Instant::now(), @@ -34,12 +32,13 @@ impl App { } pub(crate) async fn serve() -> Result<()> { - let term = Term::start()?; - let (mut rx, signals) = (Event::take(), Signals::start()?); + let term = Raterm::start()?; + Signals::start()?; - let mut app = Self::make(term, signals)?; + let mut app = Self::make(term)?; app.bootstrap()?; + let mut rx = Event::take(); loop { if let Some(t) = app.next_render.take() { select! { diff --git a/yazi-fm/src/app/render.rs b/yazi-fm/src/app/render.rs index c27541fc..245c4c21 100644 --- a/yazi-fm/src/app/render.rs +++ b/yazi-fm/src/app/render.rs @@ -1,15 +1,16 @@ -use std::{sync::atomic::{AtomicU8, Ordering}, time::Instant}; +use std::{io::Write, sync::atomic::{AtomicU8, Ordering}, time::Instant}; use anyhow::Result; -use crossterm::{cursor::{MoveTo, SetCursorStyle, Show}, execute, queue, terminal::{BeginSynchronizedUpdate, EndSynchronizedUpdate}}; -use ratatui::{CompletedFrame, backend::{Backend, CrosstermBackend}, buffer::Buffer, layout::Position}; +use ratatui::{CompletedFrame, backend::Backend, buffer::Buffer, layout::Position}; use yazi_actor::{Ctx, lives::Lives}; use yazi_binding::runtime_scope; use yazi_config::LAYOUT; -use yazi_macro::{act, succ}; +use yazi_macro::{act, succ, writef}; use yazi_plugin::LUA; use yazi_shared::{data::Data, event::NEED_RENDER}; +use yazi_term::{CursorStyle, sequence::{BeginSyncUpdate, EndSyncUpdate, MoveTo, SetCursorStyle, ShowCursor}}; use yazi_tty::TTY; +use yazi_tui::RatermBackend; use yazi_widgets::COLLISION; use crate::{app::App, root::Root}; @@ -90,10 +91,10 @@ impl App { } let patches = frame.buffer.diff(&new); - CrosstermBackend::new(&mut *TTY.lockout()).draw(patches.into_iter()).ok(); + RatermBackend::new(&mut *TTY.lockout()).draw(patches.into_iter()).ok(); } - fn routine(push: bool, cursor: Option<(Position, SetCursorStyle)>) { + fn routine(push: bool, cursor: Option<(Position, CursorStyle)>) { static COUNT: AtomicU8 = AtomicU8::new(0); if push && COUNT.fetch_add(1, Ordering::Relaxed) != 0 { return; @@ -101,12 +102,18 @@ impl App { return; } - _ = if push { - queue!(TTY.writer(), BeginSynchronizedUpdate) + if push { + write!(TTY.writer(), "{BeginSyncUpdate}").ok(); } else if let Some((Position { x, y }, shape)) = cursor { - execute!(TTY.writer(), shape, MoveTo(x, y), Show, EndSynchronizedUpdate) + writef!( + TTY.writer(), + "{}{}{ShowCursor}{EndSyncUpdate}", + SetCursorStyle(shape as u8), + MoveTo(x, y), + ) + .ok(); } else { - execute!(TTY.writer(), EndSynchronizedUpdate) + writef!(TTY.writer(), "{EndSyncUpdate}").ok(); }; } } diff --git a/yazi-fm/src/cmp/cmp.rs b/yazi-fm/src/cmp/cmp.rs index 7c1d7349..df231612 100644 --- a/yazi-fm/src/cmp/cmp.rs +++ b/yazi-fm/src/cmp/cmp.rs @@ -3,8 +3,8 @@ use std::path::MAIN_SEPARATOR_STR; use ratatui::{buffer::Buffer, layout::Rect, widgets::{Block, BorderType, List, ListItem, Widget}}; use yazi_config::{THEME, popup::{Offset, Position}}; use yazi_core::Core; -use yazi_emulator::Dimension; use yazi_shared::strand::StrandLike; +use yazi_term::TERM; pub(crate) struct Cmp<'a> { core: &'a Core, @@ -38,7 +38,7 @@ impl Widget for Cmp<'_> { .collect(); let input_area = self.core.mgr.area(self.core.input.position); - let mut area = Position::sticky(Dimension::available().into(), input_area, Offset { + let mut area = Position::sticky(TERM.dimension(), input_area, Offset { x: 1, y: 0, width: input_area.width.saturating_sub(2), diff --git a/yazi-fm/src/dispatcher.rs b/yazi-fm/src/dispatcher.rs index 5bafc192..53b525dd 100644 --- a/yazi-fm/src/dispatcher.rs +++ b/yazi-fm/src/dispatcher.rs @@ -1,12 +1,12 @@ use std::sync::atomic::Ordering; use anyhow::Result; -use crossterm::event::{KeyEvent, MouseEvent}; use tracing::warn; use yazi_actor::Ctx; use yazi_config::keymap::Key; use yazi_macro::{act, emit}; use yazi_shared::event::{ActionCow, Event, NEED_RENDER}; +use yazi_term::event::{Event as TermEvent, KeyEvent, MouseEvent}; use yazi_widgets::input::InputMode; use crate::{Executor, Router, app::App}; @@ -23,11 +23,12 @@ impl<'a> Dispatcher<'a> { Event::Call(action) => Ok(self.dispatch_call(action)), Event::Seq(actions) => Ok(self.dispatch_seq(actions)), Event::Render(partial) => self.dispatch_render(partial), - Event::Key(key) => self.dispatch_key(key), - Event::Mouse(mouse) => self.dispatch_mouse(mouse), - Event::Resize => self.dispatch_resize(), - Event::Focus => self.dispatch_focus(), - Event::Paste(str) => self.dispatch_paste(str), + Event::Term(TermEvent::Key(key)) => self.dispatch_key(key), + Event::Term(TermEvent::Mouse(mouse)) => self.dispatch_mouse(mouse), + Event::Term(TermEvent::Resize(_)) => self.dispatch_resize(), + Event::Term(TermEvent::FocusIn) => self.dispatch_focus(), + Event::Term(TermEvent::FocusOut) => Ok(()), + Event::Term(TermEvent::Paste(str)) => self.dispatch_paste(str), }; if let Err(e) = &result { diff --git a/yazi-fm/src/executor.rs b/yazi-fm/src/executor.rs index 3a8c461f..7f7c6c5c 100644 --- a/yazi-fm/src/executor.rs +++ b/yazi-fm/src/executor.rs @@ -1,4 +1,4 @@ -use anyhow::{Context, Result}; +use anyhow::Result; use yazi_actor::Ctx; use yazi_macro::{act, succ}; use yazi_shared::{Layer, data::Data, event::ActionCow}; @@ -33,7 +33,7 @@ impl<'a> Executor<'a> { fn null(&mut self, _action: ActionCow) -> Result { succ!() } - fn app(&mut self, mut action: ActionCow) -> Result { + fn app(&mut self, action: ActionCow) -> Result { let cx = &mut Ctx::new(&action, &mut self.app.core, &mut self.app.term)?; macro_rules! on { @@ -51,19 +51,12 @@ impl<'a> Executor<'a> { on!(lua); on!(deprecate); on!(theme); + on!(stop); on!(quit); match &*action.name { "resize" => act!(app:resize, cx, crate::Root::reflow as fn(_) -> _), - "resume" => act!(app:resume, cx, yazi_parser::app::ResumeForm { - tx: self.app.signals.tx.clone(), - reflow: crate::Root::reflow, - replier: action.take_replier().context("Missing replier in ResumeForm")?, - }), - "stop" => act!(app:stop, cx, yazi_parser::app::StopForm { - tx: self.app.signals.tx.clone(), - replier: action.take_replier().context("Missing replier in StopForm")?, - }), + "resume" => act!(app:resume, cx, crate::Root::reflow as fn(_) -> _), _ => succ!(), } } diff --git a/yazi-fm/src/logs.rs b/yazi-fm/src/logs.rs index 6da1a09c..9c99175e 100644 --- a/yazi-fm/src/logs.rs +++ b/yazi-fm/src/logs.rs @@ -1,12 +1,14 @@ -use std::fs::File; +use std::{fs::File, io}; use anyhow::Context; -use crossterm::style::{Color, Print, ResetColor, SetForegroundColor}; +use ratatui::style::Color; use tracing_appender::non_blocking::WorkerGuard; use tracing_subscriber::EnvFilter; use yazi_fs::Xdg; +use yazi_macro::writef; use yazi_shared::LOG_LEVEL; use yazi_shim::cell::RoCell; +use yazi_term::sequence::{SetFg, SetSgr}; static _GUARD: RoCell = RoCell::new(); @@ -36,11 +38,11 @@ impl Logs { .init(); _GUARD.init(guard); - Ok(crossterm::execute!( - std::io::stderr(), - SetForegroundColor(Color::Yellow), - Print(format!("Running with log level `{level}`, logs are written to {log_path:?}\n")), - ResetColor + Ok(writef!( + io::stderr(), + "{}Running with log level `{level}`, logs are written to {log_path:?}\n{}", + SetFg(Color::Yellow), + SetSgr::Reset, )?) } } diff --git a/yazi-fm/src/main.rs b/yazi-fm/src/main.rs index eee14d5a..c3ea749e 100644 --- a/yazi-fm/src/main.rs +++ b/yazi-fm/src/main.rs @@ -16,7 +16,7 @@ async fn main() -> anyhow::Result<()> { yazi_tty::init(); - yazi_term::init(); + yazi_term::init()?; yazi_fs::init(); diff --git a/yazi-fm/src/panic.rs b/yazi-fm/src/panic.rs index efb194e3..87697b49 100644 --- a/yazi-fm/src/panic.rs +++ b/yazi-fm/src/panic.rs @@ -1,4 +1,4 @@ -use yazi_term::Term; +use yazi_tui::Raterm; pub(super) struct Panic; @@ -8,10 +8,8 @@ impl Panic { let hook = std::panic::take_hook(); std::panic::set_hook(Box::new(move |info| { - Term::goodbye(|| { - hook(info); - 1 - }); + Raterm::stop(); + hook(info); })); } } diff --git a/yazi-fm/src/signals.rs b/yazi-fm/src/signals.rs index b01d1a74..48e93859 100644 --- a/yazi-fm/src/signals.rs +++ b/yazi-fm/src/signals.rs @@ -1,24 +1,39 @@ use anyhow::Result; -use crossterm::event::{Event as CrosstermEvent, EventStream, KeyEvent, KeyEventKind}; -use futures::StreamExt; -use tokio::{select, sync::mpsc}; -use yazi_config::YAZI; -use yazi_shared::event::{Event, Replier}; +use tokio_stream::StreamExt; -pub(super) struct Signals { - pub(super) tx: mpsc::UnboundedSender<(bool, Replier)>, -} +pub(super) struct Signals; impl Signals { - pub(super) fn start() -> Result { - let (tx, rx) = mpsc::unbounded_channel(); - Self::spawn(rx)?; + pub(super) fn start() -> Result<()> { + #[cfg(unix)] + use libc::{SIGCONT, SIGHUP, SIGINT, SIGQUIT, SIGTERM, SIGTSTP}; - Ok(Self { tx }) + #[cfg(unix)] + let mut stream = signal_hook_tokio::Signals::new([ + // Interrupt signals (Ctrl-C, Ctrl-\) + SIGINT, SIGQUIT, // + // Hangup signal (Terminal closed) + SIGHUP, // + // Termination signal (kill) + SIGTERM, // + // Job control signals (Ctrl-Z, fg/bg) + SIGTSTP, SIGCONT, + ])?; + #[cfg(windows)] + let mut stream = tokio_stream::empty(); + + tokio::spawn(async move { + while let Some(n) = stream.next().await { + if !Self::handle(n).await { + break; + } + } + }); + Ok(()) } #[cfg(unix)] - fn handle_sys(n: libc::c_int) -> bool { + async fn handle(n: libc::c_int) -> bool { use libc::{SIGCONT, SIGHUP, SIGINT, SIGQUIT, SIGSTOP, SIGTERM, SIGTSTP}; use tracing::error; use yazi_term::YIELD_TO_SUBPROCESS; @@ -30,16 +45,14 @@ impl Signals { return false; } SIGTSTP => { - tokio::spawn(async move { - yazi_scheduler::AppProxy::stop().await; - if unsafe { libc::kill(0, SIGSTOP) } != 0 { - error!("Failed to stop the process:\n{}", std::io::Error::last_os_error()); - yazi_proxy::AppProxy::quit(Default::default()); - } - }); + yazi_scheduler::AppProxy::stop().await; + if unsafe { libc::kill(0, SIGSTOP) } != 0 { + error!("Failed to stop the process:\n{}", std::io::Error::last_os_error()); + yazi_proxy::AppProxy::quit(Default::default()); + } } SIGCONT if YIELD_TO_SUBPROCESS.try_acquire().is_ok() => { - tokio::spawn(yazi_scheduler::AppProxy::resume()); + yazi_scheduler::AppProxy::resume().await; } _ => {} } @@ -47,69 +60,5 @@ impl Signals { } #[cfg(windows)] - #[inline] - fn handle_sys(_: ()) -> bool { unreachable!() } - - fn handle_term(event: CrosstermEvent) { - match event { - CrosstermEvent::Key(key @ KeyEvent { kind: KeyEventKind::Press, .. }) => { - Event::Key(key).emit() - } - CrosstermEvent::Mouse(mouse) if YAZI.mgr.mouse_events.get().contains(mouse.kind.into()) => { - Event::Mouse(mouse).emit() - } - CrosstermEvent::Resize(..) => Event::Resize.emit(), - CrosstermEvent::FocusGained => Event::Focus.emit(), - CrosstermEvent::Paste(str) => Event::Paste(str).emit(), - _ => {} - } - } - - fn spawn(mut rx: mpsc::UnboundedReceiver<(bool, Replier)>) -> Result<()> { - #[cfg(unix)] - use libc::{SIGCONT, SIGHUP, SIGINT, SIGQUIT, SIGTERM, SIGTSTP}; - - #[cfg(unix)] - let mut sys = signal_hook_tokio::Signals::new([ - // Interrupt signals (Ctrl-C, Ctrl-\) - SIGINT, SIGQUIT, // - // Hangup signal (Terminal closed) - SIGHUP, // - // Termination signal (kill) - SIGTERM, // - // Job control signals (Ctrl-Z, fg/bg) - SIGTSTP, SIGCONT, - ])?; - #[cfg(windows)] - let mut sys = tokio_stream::empty(); - - let mut term = Some(EventStream::new()); - - tokio::spawn(async move { - loop { - if let Some(t) = &mut term { - select! { - biased; - Some((state, replier)) = rx.recv() => { - term = term.filter(|_| state); - replier.send(Ok(().into())).ok(); - }, - Some(n) = sys.next() => if !Self::handle_sys(n) { return }, - Some(Ok(e)) = t.next() => Self::handle_term(e) - } - } else { - select! { - biased; - Some((state, replier)) = rx.recv() => { - term = state.then(EventStream::new); - replier.send(Ok(().into())).ok(); - }, - Some(n) = sys.next() => if !Self::handle_sys(n) { return }, - } - } - } - }); - - Ok(()) - } + async fn handle(_: ()) -> bool { unreachable!() } } diff --git a/yazi-fs/src/cha/cha.rs b/yazi-fs/src/cha/cha.rs index 8e4f1dfe..537289c9 100644 --- a/yazi-fs/src/cha/cha.rs +++ b/yazi-fs/src/cha/cha.rs @@ -59,6 +59,7 @@ impl Cha { where U: AsUrl, { + #[allow(unused_mut)] let mut kind = ChaKind::DUMMY; let mode = r#type.map(ChaMode::from_bare).unwrap_or_default(); diff --git a/yazi-fs/src/provider/attrs.rs b/yazi-fs/src/provider/attrs.rs index 0746fa4d..5e25f9b0 100644 --- a/yazi-fs/src/provider/attrs.rs +++ b/yazi-fs/src/provider/attrs.rs @@ -52,9 +52,9 @@ impl TryFrom for std::fs::FileTimes { impl TryFrom for std::fs::Permissions { type Error = (); - fn try_from(value: Attrs) -> Result { + fn try_from(_value: Attrs) -> Result { #[cfg(unix)] - if let Some(mode) = value.mode { + if let Some(mode) = _value.mode { return Ok(mode.into()); } diff --git a/yazi-macro/src/stdio.rs b/yazi-macro/src/stdio.rs index fa80935b..6956d17c 100644 --- a/yazi-macro/src/stdio.rs +++ b/yazi-macro/src/stdio.rs @@ -13,3 +13,13 @@ macro_rules! errln { writeln!(std::io::stderr(), $($tt)*) }} } + +/// Like [`write!`] but immediately flushes the writer afterwards. +#[macro_export] +macro_rules! writef { + ($dst:expr, $($arg:tt)*) => {{ + use std::io::Write as _; + let dst = &mut $dst; + write!(dst, $($arg)*).and_then(|_| dst.flush()) + }}; +} diff --git a/yazi-parser/Cargo.toml b/yazi-parser/Cargo.toml index 7e0eb79b..3f987e48 100644 --- a/yazi-parser/Cargo.toml +++ b/yazi-parser/Cargo.toml @@ -26,13 +26,13 @@ yazi-fs = { path = "../yazi-fs", version = "26.5.6" } yazi-macro = { path = "../yazi-macro", version = "26.5.6" } yazi-scheduler = { path = "../yazi-scheduler", version = "26.5.6" } yazi-shared = { path = "../yazi-shared", version = "26.5.6" } +yazi-term = { path = "../yazi-term", version = "26.5.6" } yazi-vfs = { path = "../yazi-vfs", version = "26.5.6" } yazi-widgets = { path = "../yazi-widgets", version = "26.5.6" } # External dependencies anyhow = { workspace = true } bitflags = { workspace = true } -crossterm = { workspace = true } hashbrown = { workspace = true } mlua = { workspace = true } paste = { workspace = true } @@ -41,6 +41,3 @@ serde = { workspace = true } serde_with = { workspace = true } strum = { workspace = true } tokio = { workspace = true } - -[target.'cfg(target_os = "macos")'.dependencies] -crossterm = { workspace = true, features = [ "use-dev-tty", "libc" ] } diff --git a/yazi-parser/src/app/mod.rs b/yazi-parser/src/app/mod.rs index ce380fb9..881d723f 100644 --- a/yazi-parser/src/app/mod.rs +++ b/yazi-parser/src/app/mod.rs @@ -1 +1 @@ -yazi_macro::mod_flat!(deprecate lua mouse plugin quit reflow resume stop title update_progress); +yazi_macro::mod_flat!(deprecate lua mouse plugin quit reflow title update_progress); diff --git a/yazi-parser/src/app/mouse.rs b/yazi-parser/src/app/mouse.rs index cc28a4dc..77e8b1d4 100644 --- a/yazi-parser/src/app/mouse.rs +++ b/yazi-parser/src/app/mouse.rs @@ -1,5 +1,5 @@ -use crossterm::event::MouseEvent; use mlua::{ExternalError, FromLua, IntoLua, Lua, Value}; +use yazi_term::event::MouseEvent; #[derive(Debug)] pub struct MouseForm { diff --git a/yazi-parser/src/app/resume.rs b/yazi-parser/src/app/resume.rs deleted file mode 100644 index 8eafc84f..00000000 --- a/yazi-parser/src/app/resume.rs +++ /dev/null @@ -1,19 +0,0 @@ -use mlua::{ExternalError, FromLua, IntoLua, Lua, Table, Value}; -use ratatui::layout::Rect; -use tokio::sync::mpsc; -use yazi_shared::event::Replier; - -#[derive(Debug)] -pub struct ResumeForm { - pub tx: mpsc::UnboundedSender<(bool, Replier)>, - pub reflow: fn(Rect) -> mlua::Result, - pub replier: Replier, -} - -impl FromLua for ResumeForm { - fn from_lua(_: Value, _: &Lua) -> mlua::Result { Err("unsupported".into_lua_err()) } -} - -impl IntoLua for ResumeForm { - fn into_lua(self, _: &Lua) -> mlua::Result { Err("unsupported".into_lua_err()) } -} diff --git a/yazi-parser/src/app/stop.rs b/yazi-parser/src/app/stop.rs deleted file mode 100644 index bf8a3c18..00000000 --- a/yazi-parser/src/app/stop.rs +++ /dev/null @@ -1,17 +0,0 @@ -use mlua::{ExternalError, FromLua, IntoLua, Lua, Value}; -use tokio::sync::mpsc; -use yazi_shared::event::Replier; - -#[derive(Debug)] -pub struct StopForm { - pub tx: mpsc::UnboundedSender<(bool, Replier)>, - pub replier: Replier, -} - -impl FromLua for StopForm { - fn from_lua(_: Value, _: &Lua) -> mlua::Result { Err("unsupported".into_lua_err()) } -} - -impl IntoLua for StopForm { - fn into_lua(self, _: &Lua) -> mlua::Result { Err("unsupported".into_lua_err()) } -} diff --git a/yazi-parser/src/spark/spark.rs b/yazi-parser/src/spark/spark.rs index a6cbc703..a2f6234f 100644 --- a/yazi-parser/src/spark/spark.rs +++ b/yazi-parser/src/spark/spark.rs @@ -19,8 +19,8 @@ pub enum Spark<'a> { AppQuit(crate::app::QuitForm), AppReflow(crate::app::ReflowForm), AppResize(crate::app::ReflowForm), - AppResume(crate::app::ResumeForm), - AppStop(crate::app::StopForm), + AppResume(crate::app::ReflowForm), + AppStop(crate::VoidForm), AppTheme(crate::VoidForm), AppTitle(crate::app::TitleForm), AppUpdateProgress(crate::app::UpdateProgressForm), @@ -352,6 +352,7 @@ try_from_spark!( crate::VoidForm, app:bootstrap, app:focus, + app:stop, app:theme, mgr:back, mgr:bulk_rename, @@ -379,9 +380,7 @@ try_from_spark!(crate::app::LuaForm, app:lua); try_from_spark!(crate::app::MouseForm, app:mouse); try_from_spark!(crate::app::PluginForm, app:plugin, app:plugin_do); try_from_spark!(crate::app::QuitForm, app:quit, mgr:quit); -try_from_spark!(crate::app::ReflowForm, app:reflow, app:resize); -try_from_spark!(crate::app::ResumeForm, app:resume); -try_from_spark!(crate::app::StopForm, app:stop); +try_from_spark!(crate::app::ReflowForm, app:reflow, app:resize, app:resume); try_from_spark!(crate::app::TitleForm, app:title); try_from_spark!(crate::app::UpdateProgressForm, app:update_progress); try_from_spark!(crate::cmp::CloseForm, cmp:close); diff --git a/yazi-scheduler/Cargo.toml b/yazi-scheduler/Cargo.toml index 549e3b0b..1c28d1bb 100644 --- a/yazi-scheduler/Cargo.toml +++ b/yazi-scheduler/Cargo.toml @@ -25,6 +25,7 @@ yazi-macro = { path = "../yazi-macro", version = "26.5.6" } yazi-runner = { path = "../yazi-runner", version = "26.5.6" } yazi-shared = { path = "../yazi-shared", version = "26.5.6" } yazi-term = { path = "../yazi-term", version = "26.5.6" } +yazi-tui = { path = "../yazi-tui", version = "26.5.6" } yazi-vfs = { path = "../yazi-vfs", version = "26.5.6" } # External dependencies diff --git a/yazi-shared/Cargo.toml b/yazi-shared/Cargo.toml index fbd192e6..7dc674df 100644 --- a/yazi-shared/Cargo.toml +++ b/yazi-shared/Cargo.toml @@ -15,10 +15,10 @@ workspace = true [dependencies] yazi-macro = { path = "../yazi-macro", version = "26.5.6" } yazi-shim = { path = "../yazi-shim", version = "26.5.6" } +yazi-term = { path = "../yazi-term", version = "26.5.6" } # External dependencies anyhow = { workspace = true } -crossterm = { workspace = true } dyn-clone = { workspace = true } foldhash = { workspace = true } futures = { workspace = true } @@ -41,6 +41,3 @@ uzers = { workspace = true } [target.'cfg(windows)'.dependencies] windows-sys = { version = "0.61.2", features = [ "Win32_UI_Shell" ] } - -[target.'cfg(target_os = "macos")'.dependencies] -crossterm = { workspace = true, features = [ "use-dev-tty", "libc" ] } diff --git a/yazi-shared/src/data/any.rs b/yazi-shared/src/data/any.rs index 406d9a40..e11a284e 100644 --- a/yazi-shared/src/data/any.rs +++ b/yazi-shared/src/data/any.rs @@ -1,4 +1,4 @@ -use std::{any::Any, fmt}; +use std::{any::Any, fmt::{self, Debug}}; use dyn_clone::DynClone; @@ -21,7 +21,7 @@ impl Clone for Box { fn clone(&self) -> Self { dyn_clone::clone_box(&**self) } } -impl fmt::Debug for dyn DataAny { +impl Debug for dyn DataAny { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("DataAny").finish_non_exhaustive() } diff --git a/yazi-shared/src/event/event.rs b/yazi-shared/src/event/event.rs index 0fe19d28..776ac123 100644 --- a/yazi-shared/src/event/event.rs +++ b/yazi-shared/src/event/event.rs @@ -1,4 +1,3 @@ -use crossterm::event::{KeyEvent, MouseEvent}; use tokio::sync::mpsc; use yazi_shim::cell::RoCell; @@ -11,12 +10,8 @@ static RX: RoCell> = RoCell::new(); pub enum Event { Call(ActionCow), Seq(Vec), + Term(yazi_term::event::Event), Render(bool), - Key(KeyEvent), - Mouse(MouseEvent), - Resize, - Focus, - Paste(String), } impl Event { diff --git a/yazi-shared/src/lib.rs b/yazi-shared/src/lib.rs index 61039c03..98317f74 100644 --- a/yazi-shared/src/lib.rs +++ b/yazi-shared/src/lib.rs @@ -1,6 +1,6 @@ yazi_macro::mod_pub!(data event loc path pool scheme shell strand translit url wtf8); -yazi_macro::mod_flat!(alias bytes chars completion_token condition debounce env id kebab_cased_string last_value layer localset natsort non_empty_string os predictor snake_cased_string source terminal tests throttle time utf8); +yazi_macro::mod_flat!(alias bytes chars completion_token condition debounce env id kebab_cased_string last_value layer localset natsort non_empty_string os predictor snake_cased_string source tests throttle time); pub fn init() { LOCAL_SET.with(tokio::task::LocalSet::new); diff --git a/yazi-shared/src/terminal.rs b/yazi-shared/src/terminal.rs deleted file mode 100644 index c874a7fc..00000000 --- a/yazi-shared/src/terminal.rs +++ /dev/null @@ -1,12 +0,0 @@ -use std::io::Write; - -use crossterm::execute; - -#[inline] -pub fn terminal_clear(mut w: impl Write) -> std::io::Result<()> { - execute!( - w, - crossterm::terminal::Clear(crossterm::terminal::ClearType::All), - crossterm::style::Print("\n") - ) -} diff --git a/yazi-shared/src/url/url.rs b/yazi-shared/src/url/url.rs index 44afc8da..a17abc9a 100644 --- a/yazi-shared/src/url/url.rs +++ b/yazi-shared/src/url/url.rs @@ -368,10 +368,10 @@ impl<'a> Url<'a> { (U::Regular(_), U::Regular(_)) => Ok(prefix), (U::Search { .. }, U::Search { .. }) => Ok(prefix), (U::Archive { domain: a, .. }, U::Archive { domain: b, .. }) => { - Some(prefix).filter(|_| a == b).ok_or(Exotic) + (a == b).then_some(prefix).ok_or(Exotic) } (U::Sftp { domain: a, .. }, U::Sftp { domain: b, .. }) => { - Some(prefix).filter(|_| a == b).ok_or(Exotic) + (a == b).then_some(prefix).ok_or(Exotic) } // Both are local files @@ -380,16 +380,16 @@ impl<'a> Url<'a> { // Only the entry of archives is a local file (U::Regular(_), U::Archive { .. }) => { - Some(prefix).filter(|_| base.uri().is_empty()).ok_or(NotPrefix) + base.uri().is_empty().then_some(prefix).ok_or(NotPrefix) } (U::Search { .. }, U::Archive { .. }) => { - Some(prefix).filter(|_| base.uri().is_empty()).ok_or(NotPrefix) + base.uri().is_empty().then_some(prefix).ok_or(NotPrefix) } (U::Archive { .. }, U::Regular(_)) => { - Some(prefix).filter(|_| self.uri().is_empty()).ok_or(NotPrefix) + self.uri().is_empty().then_some(prefix).ok_or(NotPrefix) } (U::Archive { .. }, U::Search { .. }) => { - Some(prefix).filter(|_| self.uri().is_empty()).ok_or(NotPrefix) + self.uri().is_empty().then_some(prefix).ok_or(NotPrefix) } // Independent virtual file space @@ -414,10 +414,10 @@ impl<'a> Url<'a> { (U::Regular(_), U::Regular(_)) => Ok(suffix), (U::Search { .. }, U::Search { .. }) => Ok(suffix), (U::Archive { domain: a, .. }, U::Archive { domain: b, .. }) => { - Some(suffix).filter(|_| a == b).ok_or(Exotic) + (a == b).then_some(suffix).ok_or(Exotic) } (U::Sftp { domain: a, .. }, U::Sftp { domain: b, .. }) => { - Some(suffix).filter(|_| a == b).ok_or(Exotic) + (a == b).then_some(suffix).ok_or(Exotic) } // Both are local files @@ -426,16 +426,16 @@ impl<'a> Url<'a> { // Only the entry of archives is a local file (U::Regular(_), U::Archive { .. }) => { - Some(suffix).filter(|_| other.uri().is_empty()).ok_or(NotSuffix) + other.uri().is_empty().then_some(suffix).ok_or(NotSuffix) } (U::Search { .. }, U::Archive { .. }) => { - Some(suffix).filter(|_| other.uri().is_empty()).ok_or(NotSuffix) + other.uri().is_empty().then_some(suffix).ok_or(NotSuffix) } (U::Archive { .. }, U::Regular(_)) => { - Some(suffix).filter(|_| self.uri().is_empty()).ok_or(NotSuffix) + self.uri().is_empty().then_some(suffix).ok_or(NotSuffix) } (U::Archive { .. }, U::Search { .. }) => { - Some(suffix).filter(|_| self.uri().is_empty()).ok_or(NotSuffix) + self.uri().is_empty().then_some(suffix).ok_or(NotSuffix) } // Independent virtual file space diff --git a/yazi-shared/src/utf8.rs b/yazi-shared/src/utf8.rs deleted file mode 100644 index 6bf021d7..00000000 --- a/yazi-shared/src/utf8.rs +++ /dev/null @@ -1,53 +0,0 @@ -// https://tools.ietf.org/html/rfc3629 -const UTF8_CHAR_WIDTH: &[u8; 256] = &[ - // 1 2 3 4 5 6 7 8 9 A B C D E F - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 0 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 1 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 2 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 3 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 4 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 5 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 6 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 7 - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 8 - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 9 - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // A - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // B - 0, 0, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, // C - 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, // D - 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, // E - 4, 4, 4, 4, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // F -]; - -/// Given a first byte, determines how many bytes are in this UTF-8 character. -#[must_use] -#[inline] -pub const fn utf8_char_width(b: u8) -> usize { UTF8_CHAR_WIDTH[b as usize] as usize } - -/// Finds the closest `x` not exceeding `index` where [`is_char_boundary(x)`] is -/// `true`. -/// -/// This method can help you truncate a string so that it's still valid UTF-8, -/// but doesn't exceed a given number of bytes. Note that this is done purely at -/// the character level and can still visually split graphemes, even though the -/// underlying characters aren't split. For example, the emoji 🧑‍🔬 (scientist) -/// could be split so that the string only includes 🧑 (person) instead. -#[inline] -pub fn floor_char_boundary(s: &str, index: usize) -> usize { - if index >= s.len() { - s.len() - } else { - let lower_bound = index.saturating_sub(3); - let new_index = - s.as_bytes()[lower_bound..=index].iter().rposition(|&b| is_utf8_char_boundary(b)); - - // SAFETY: we know that the character boundary will be within four bytes - unsafe { lower_bound + new_index.unwrap_unchecked() } - } -} - -#[inline] -const fn is_utf8_char_boundary(b: u8) -> bool { - // This is bit magic equivalent to: b < 128 || b >= 192 - (b as i8) >= -0x40 -} diff --git a/yazi-shim/Cargo.toml b/yazi-shim/Cargo.toml index a98a4a5a..b6154dea 100644 --- a/yazi-shim/Cargo.toml +++ b/yazi-shim/Cargo.toml @@ -21,7 +21,6 @@ yazi-macro = { path = "../yazi-macro", version = "26.5.6" } # External dependencies arc-swap = { workspace = true } -crossterm = { workspace = true } hashbrown = { workspace = true } mlua = { workspace = true } ratatui = { workspace = true } @@ -33,6 +32,3 @@ unicode-width = { workspace = true } [dependencies.unicode-segmentation] version = "1.13.2" - -[target.'cfg(target_os = "macos")'.dependencies] -crossterm = { workspace = true, features = [ "use-dev-tty", "libc" ] } diff --git a/yazi-shim/src/crossterm/if.rs b/yazi-shim/src/crossterm/if.rs deleted file mode 100644 index 6aedf1c9..00000000 --- a/yazi-shim/src/crossterm/if.rs +++ /dev/null @@ -1,15 +0,0 @@ -pub struct If(pub bool, pub T); - -impl crossterm::Command for If { - fn write_ansi(&self, f: &mut impl std::fmt::Write) -> std::fmt::Result { - if self.0 { self.1.write_ansi(f) } else { Ok(()) } - } - - #[cfg(windows)] - fn execute_winapi(&self) -> std::io::Result<()> { - if self.0 { self.1.execute_winapi() } else { Ok(()) } - } - - #[cfg(windows)] - fn is_ansi_code_supported(&self) -> bool { self.1.is_ansi_code_supported() } -} diff --git a/yazi-shim/src/crossterm/mod.rs b/yazi-shim/src/crossterm/mod.rs deleted file mode 100644 index e6650ebf..00000000 --- a/yazi-shim/src/crossterm/mod.rs +++ /dev/null @@ -1 +0,0 @@ -yazi_macro::mod_flat!(r#if restore_background restore_cursor set_background); diff --git a/yazi-shim/src/crossterm/restore_background.rs b/yazi-shim/src/crossterm/restore_background.rs deleted file mode 100644 index ea61734a..00000000 --- a/yazi-shim/src/crossterm/restore_background.rs +++ /dev/null @@ -1,10 +0,0 @@ -pub struct RestoreBackground; - -impl crossterm::Command for RestoreBackground { - fn write_ansi(&self, f: &mut impl std::fmt::Write) -> std::fmt::Result { - write!(f, "\x1b]111\x1b\\") - } - - #[cfg(windows)] - fn execute_winapi(&self) -> std::io::Result<()> { Ok(()) } -} diff --git a/yazi-shim/src/crossterm/restore_cursor.rs b/yazi-shim/src/crossterm/restore_cursor.rs deleted file mode 100644 index 22d1ef01..00000000 --- a/yazi-shim/src/crossterm/restore_cursor.rs +++ /dev/null @@ -1,29 +0,0 @@ -use crossterm::cursor::SetCursorStyle; - -pub struct RestoreCursor { - pub shape: u8, - pub blink: bool, -} - -impl crossterm::Command for RestoreCursor { - fn write_ansi(&self, f: &mut impl std::fmt::Write) -> std::fmt::Result { - let (shape, shape_blink) = match self.shape { - u8::MAX => (0, None), - n => (n.max(1).div_ceil(2), Some(n.max(1) & 1 == 1)), - }; - - let blink = shape_blink.unwrap_or(self.blink); - Ok(match shape { - 2 if blink => SetCursorStyle::BlinkingUnderScore.write_ansi(f)?, - 2 if !blink => SetCursorStyle::SteadyUnderScore.write_ansi(f)?, - 3 if blink => SetCursorStyle::BlinkingBar.write_ansi(f)?, - 3 if !blink => SetCursorStyle::SteadyBar.write_ansi(f)?, - _ if blink => SetCursorStyle::DefaultUserShape.write_ansi(f)?, - _ if !blink => SetCursorStyle::SteadyBlock.write_ansi(f)?, - _ => unreachable!(), - }) - } - - #[cfg(windows)] - fn execute_winapi(&self) -> std::io::Result<()> { Ok(()) } -} diff --git a/yazi-shim/src/crossterm/set_background.rs b/yazi-shim/src/crossterm/set_background.rs deleted file mode 100644 index 92ae2990..00000000 --- a/yazi-shim/src/crossterm/set_background.rs +++ /dev/null @@ -1,10 +0,0 @@ -pub struct SetBackground<'a>(pub &'a str); - -impl crossterm::Command for SetBackground<'_> { - fn write_ansi(&self, f: &mut impl std::fmt::Write) -> std::fmt::Result { - if self.0.is_empty() { Ok(()) } else { write!(f, "\x1b]11;{}\x1b\\", self.0) } - } - - #[cfg(windows)] - fn execute_winapi(&self) -> std::io::Result<()> { Ok(()) } -} diff --git a/yazi-shim/src/lib.rs b/yazi-shim/src/lib.rs index cfabeb98..ad4cb83b 100644 --- a/yazi-shim/src/lib.rs +++ b/yazi-shim/src/lib.rs @@ -1,3 +1,6 @@ -yazi_macro::mod_pub!(arc_swap cell crossterm mlua path ratatui serde strum toml vec); +yazi_macro::mod_pub!(arc_swap cell mlua path ratatui serde strum toml vec); -yazi_macro::mod_flat!(twox); +yazi_macro::mod_flat!(twox utf8); + +#[cfg(windows)] +yazi_macro::mod_flat!(win32); diff --git a/yazi-shim/src/utf8.rs b/yazi-shim/src/utf8.rs new file mode 100644 index 00000000..dcd687c4 --- /dev/null +++ b/yazi-shim/src/utf8.rs @@ -0,0 +1,25 @@ +// https://tools.ietf.org/html/rfc3629 +const UTF8_CHAR_WIDTH: &[u8; 256] = &[ + // 1 2 3 4 5 6 7 8 9 A B C D E F + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 0 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 1 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 2 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 3 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 4 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 5 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 6 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 7 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 8 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 9 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // A + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // B + 0, 0, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, // C + 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, // D + 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, // E + 4, 4, 4, 4, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // F +]; + +/// Given a first byte, determines how many bytes are in this UTF-8 character. +#[must_use] +#[inline] +pub const fn utf8_char_width(b: u8) -> u8 { UTF8_CHAR_WIDTH[b as usize] } diff --git a/yazi-shim/src/win32.rs b/yazi-shim/src/win32.rs new file mode 100644 index 00000000..60fc0b12 --- /dev/null +++ b/yazi-shim/src/win32.rs @@ -0,0 +1,13 @@ +use std::io; + +/// Maps a Win32 BOOL return value to `io::Result<()>`; 0 becomes +/// `Err(last_os_error())`. +pub fn bool_ok(r: i32) -> io::Result<()> { + if r == 0 { Err(io::Error::last_os_error()) } else { Ok(()) } +} + +/// Maps a Win32 non-zero-success return value to `io::Result`; 0 becomes +/// `Err(last_os_error())`. +pub fn nz_ok(r: u32) -> io::Result { + if r == 0 { Err(io::Error::last_os_error()) } else { Ok(r) } +} diff --git a/yazi-term/Cargo.toml b/yazi-term/Cargo.toml index c4600bbe..7d2a506d 100644 --- a/yazi-term/Cargo.toml +++ b/yazi-term/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "yazi-term" -description = "Yazi terminal extensions" +description = "Cross-platform virtual terminal" version.workspace = true edition.workspace = true license.workspace = true @@ -13,17 +13,23 @@ rust-version.workspace = true workspace = true [dependencies] -yazi-config = { path = "../yazi-config", version = "26.5.6" } -yazi-emulator = { path = "../yazi-emulator", version = "26.5.6" } -yazi-macro = { path = "../yazi-macro", version = "26.5.6" } -yazi-shim = { path = "../yazi-shim", version = "26.5.6" } -yazi-tty = { path = "../yazi-tty", version = "26.5.9" } +yazi-macro = { path = "../yazi-macro", version = "26.5.6" } +yazi-shim = { path = "../yazi-shim", version = "26.5.6" } +yazi-tty = { path = "../yazi-tty", version = "26.5.9" } # External dependencies -anyhow = { workspace = true } -crossterm = { workspace = true } -ratatui = { workspace = true } -tokio = { workspace = true } +anyhow = { workspace = true } +base64 = { workspace = true } +bitflags = { workspace = true } +futures = { workspace = true } +parking_lot = { workspace = true } +ratatui = { workspace = true } +thiserror = { workspace = true } +tokio = { workspace = true } -[target.'cfg(target_os = "macos")'.dependencies] -crossterm = { workspace = true, features = [ "use-dev-tty", "libc" ] } +[target.'cfg(unix)'.dependencies] +signal-hook = "0.4.4" +rustix = { version = "1.1.4" , default-features = false, features = [ "std", "stdio", "termios", "event" ] } + +[target.'cfg(windows)'.dependencies] +windows-sys = { version = "0.61.2", features = [ "Win32_Globalization", "Win32_Security", "Win32_Storage_FileSystem", "Win32_System_Console", "Win32_System_IO", "Win32_System_Threading" ] } diff --git a/yazi-term/LICENSE b/yazi-term/LICENSE new file mode 100644 index 00000000..a6261adf --- /dev/null +++ b/yazi-term/LICENSE @@ -0,0 +1,22 @@ +MIT License + +Copyright (c) 2025 - Michael Davis +Copyright (c) 2026 - sxyazi + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/yazi-term/README.md b/yazi-term/README.md index 965513dc..0f088032 100644 --- a/yazi-term/README.md +++ b/yazi-term/README.md @@ -1,5 +1,3 @@ # yazi-term -This crate is part of [Yazi][source], and it is not supposed to be used outside, as there are no guarantees about the stability of its API. - -[source]: https://github.com/sxyazi/yazi +A fork of [Termina](https://github.com/helix-editor/termina) used by Yazi. diff --git a/yazi-term/src/dimension.rs b/yazi-term/src/dimension.rs new file mode 100644 index 00000000..9cb9c7ce --- /dev/null +++ b/yazi-term/src/dimension.rs @@ -0,0 +1,41 @@ +use std::io; + +#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] +pub struct Dimension { + pub cols: u16, + pub rows: u16, + pub width: u16, + pub height: u16, +} + +#[cfg(unix)] +impl From for Dimension { + fn from(size: rustix::termios::Winsize) -> Self { + Self { + cols: size.ws_col, + rows: size.ws_row, + width: size.ws_xpixel, + height: size.ws_ypixel, + } + } +} + +impl Dimension { + pub fn checked(self) -> io::Result { + if self.cols == 0 || self.rows == 0 { + Err(io::Error::other( + "failed to get terminal dimension with both ioctl and $LINES/$COLUMNS variables", + )) + } else { + Ok(self) + } + } + + pub fn ratio(self) -> Option<(f64, f64)> { + if self.rows == 0 || self.cols == 0 || self.width == 0 || self.height == 0 { + None + } else { + Some((self.width as f64 / self.cols as f64, self.height as f64 / self.rows as f64)) + } + } +} diff --git a/yazi-term/src/error.rs b/yazi-term/src/error.rs new file mode 100644 index 00000000..3d376b36 --- /dev/null +++ b/yazi-term/src/error.rs @@ -0,0 +1,21 @@ +use std::str; + +pub type Result = std::result::Result; + +#[derive(Debug, thiserror::Error)] +pub enum ParseError { + /// Malformed or unrecognized byte sequence. + #[error("invalid or unrecognized byte sequence")] + Invalid, + /// Recognised sequence that should not be emitted as an event. + #[error("recognised sequence that should be ignored")] + Ignored, + /// The sequence is not yet complete; more bytes are needed before it can be + /// parsed. + #[error("incomplete sequence; more bytes are needed")] + Incomplete, +} + +impl From for ParseError { + fn from(_: str::Utf8Error) -> Self { Self::Invalid } +} diff --git a/yazi-term/src/event/event.rs b/yazi-term/src/event/event.rs new file mode 100644 index 00000000..f83e0f05 --- /dev/null +++ b/yazi-term/src/event/event.rs @@ -0,0 +1,11 @@ +use crate::{Dimension, event::{KeyEvent, MouseEvent}}; + +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum Event { + Key(KeyEvent), + Mouse(MouseEvent), + Resize(Dimension), + FocusIn, + FocusOut, + Paste(String), +} diff --git a/yazi-term/src/event/keyboard.rs b/yazi-term/src/event/keyboard.rs new file mode 100644 index 00000000..7836230f --- /dev/null +++ b/yazi-term/src/event/keyboard.rs @@ -0,0 +1,268 @@ +use bitflags::bitflags; + +use crate::{ParseError, Result, bail, event::Modifiers}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct KeyEvent { + pub code: KeyCode, + pub kind: KeyEventKind, + pub modifiers: Modifiers, + pub state: KeyEventState, +} + +impl KeyEvent { + pub const fn new(code: KeyCode, modifiers: Modifiers) -> Self { + Self { code, kind: KeyEventKind::Press, modifiers, state: KeyEventState::empty() } + } +} + +impl From for KeyEvent { + fn from(code: KeyCode) -> Self { + Self { + code, + kind: KeyEventKind::Press, + modifiers: Modifiers::empty(), + state: KeyEventState::empty(), + } + } +} + +// --- Kind +#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] +pub enum KeyEventKind { + #[default] + Press, + Release, + Repeat, +} + +impl KeyEventKind { + pub(crate) fn from_vt_code(code: u8) -> Self { + match code { + 2 => Self::Repeat, + 3 => Self::Release, + _ => Self::Press, + } + } +} + +// --- State +bitflags! { + #[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] + pub struct KeyEventState: u8 { + const KEYPAD = 1; + const CAPS_LOCK = 2; + const NUM_LOCK = 4; + } +} + +impl KeyEventState { + pub(crate) fn from_vt_mask(mask: u8) -> Self { + let m = mask.saturating_sub(1); + let mut state = Self::empty(); + if m & 64 != 0 { + state |= Self::CAPS_LOCK; + } + if m & 128 != 0 { + state |= Self::NUM_LOCK; + } + state + } +} + +// --- Code +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +pub enum KeyCode { + Char(char), + Enter, + Backspace, + Tab, + Escape, + Left, + Right, + Up, + Down, + Home, + End, + PageUp, + PageDown, + Insert, + Delete, + KeypadBegin, + CapsLock, + ScrollLock, + NumLock, + PrintScreen, + Pause, + Menu, + Null, + Fn(u8), + Modifier(ModifierKeyCode), + Media(MediaKeyCode), +} + +impl KeyCode { + pub(crate) fn from_xterm_modifier(r#final: u8) -> Result { + Ok(match r#final { + b'A' => KeyCode::Up, + b'B' => KeyCode::Down, + b'C' => KeyCode::Right, + b'D' => KeyCode::Left, + b'F' => KeyCode::End, + b'H' => KeyCode::Home, + b'P' => KeyCode::Fn(1), + b'Q' => KeyCode::Fn(2), + b'R' => KeyCode::Fn(3), + b'S' => KeyCode::Fn(4), + _ => bail!(), + }) + } + + pub(crate) fn from_codepoint(codepoint: u32) -> Result<(Self, KeyEventState)> { + if let Ok(pair) = Self::from_kitty_codepoint(codepoint) { + return Ok(pair); + } + + let code = match char::from_u32(codepoint).ok_or(ParseError::Invalid)? { + '\x1B' => Self::Escape, + '\r' => Self::Enter, + '\t' => Self::Tab, + '\x7F' => Self::Backspace, + c => Self::Char(c), + }; + Ok((code, KeyEventState::empty())) + } + + fn from_kitty_codepoint(codepoint: u32) -> Result<(Self, KeyEventState)> { + // Keypad keys + if (57399..=57427).contains(&codepoint) { + let code = match codepoint { + 57399 => Self::Char('0'), + 57400 => Self::Char('1'), + 57401 => Self::Char('2'), + 57402 => Self::Char('3'), + 57403 => Self::Char('4'), + 57404 => Self::Char('5'), + 57405 => Self::Char('6'), + 57406 => Self::Char('7'), + 57407 => Self::Char('8'), + 57408 => Self::Char('9'), + 57409 => Self::Char('.'), + 57410 => Self::Char('/'), + 57411 => Self::Char('*'), + 57412 => Self::Char('-'), + 57413 => Self::Char('+'), + 57414 => Self::Enter, + 57415 => Self::Char('='), + 57416 => Self::Char(','), + 57417 => Self::Left, + 57418 => Self::Right, + 57419 => Self::Up, + 57420 => Self::Down, + 57421 => Self::PageUp, + 57422 => Self::PageDown, + 57423 => Self::Home, + 57424 => Self::End, + 57425 => Self::Insert, + 57426 => Self::Delete, + 57427 => Self::KeypadBegin, + _ => unreachable!(), + }; + return Ok((code, KeyEventState::KEYPAD)); + } + + // F13–F35 keys + if (57376..=57398).contains(&codepoint) { + return Ok((Self::Fn((codepoint - 57363) as u8), KeyEventState::empty())); + } + + let code = match codepoint { + 57358 => Self::CapsLock, + 57359 => Self::ScrollLock, + 57360 => Self::NumLock, + 57361 => Self::PrintScreen, + 57362 => Self::Pause, + 57363 => Self::Menu, + 57428 => Self::Media(MediaKeyCode::Play), + 57429 => Self::Media(MediaKeyCode::Pause), + 57430 => Self::Media(MediaKeyCode::PlayPause), + 57431 => Self::Media(MediaKeyCode::Reverse), + 57432 => Self::Media(MediaKeyCode::Stop), + 57433 => Self::Media(MediaKeyCode::FastForward), + 57434 => Self::Media(MediaKeyCode::Rewind), + 57435 => Self::Media(MediaKeyCode::NextTrack), + 57436 => Self::Media(MediaKeyCode::PreviousTrack), + 57437 => Self::Media(MediaKeyCode::Record), + 57438 => Self::Media(MediaKeyCode::LowerVolume), + 57439 => Self::Media(MediaKeyCode::RaiseVolume), + 57440 => Self::Media(MediaKeyCode::MuteVolume), + 57441 => Self::Modifier(ModifierKeyCode::LeftShift), + 57442 => Self::Modifier(ModifierKeyCode::LeftControl), + 57443 => Self::Modifier(ModifierKeyCode::LeftAlt), + 57444 => Self::Modifier(ModifierKeyCode::LeftSuper), + 57445 => Self::Modifier(ModifierKeyCode::LeftHyper), + 57446 => Self::Modifier(ModifierKeyCode::LeftMeta), + 57447 => Self::Modifier(ModifierKeyCode::RightShift), + 57448 => Self::Modifier(ModifierKeyCode::RightControl), + 57449 => Self::Modifier(ModifierKeyCode::RightAlt), + 57450 => Self::Modifier(ModifierKeyCode::RightSuper), + 57451 => Self::Modifier(ModifierKeyCode::RightHyper), + 57452 => Self::Modifier(ModifierKeyCode::RightMeta), + 57453 => Self::Modifier(ModifierKeyCode::IsoLevel3Shift), + 57454 => Self::Modifier(ModifierKeyCode::IsoLevel5Shift), + _ => bail!(), + }; + Ok((code, KeyEventState::empty())) + } +} + +// --- Modifier key +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +pub enum ModifierKeyCode { + LeftShift, + LeftControl, + LeftAlt, + LeftSuper, // Command/Windows/Super key + LeftHyper, + LeftMeta, + RightShift, + RightControl, + RightAlt, + RightSuper, // Command/Windows/Super key + RightHyper, + RightMeta, + IsoLevel3Shift, + IsoLevel5Shift, +} + +impl ModifierKeyCode { + pub(crate) fn to_modifier(self) -> Option { + match self { + Self::LeftShift | Self::RightShift => Some(Modifiers::SHIFT), + Self::LeftControl | Self::RightControl => Some(Modifiers::CONTROL), + Self::LeftAlt | Self::RightAlt => Some(Modifiers::ALT), + Self::LeftSuper | Self::RightSuper => Some(Modifiers::SUPER), + Self::LeftHyper | Self::RightHyper => Some(Modifiers::HYPER), + Self::LeftMeta | Self::RightMeta => Some(Modifiers::META), + _ => None, + } + } +} + +// --- Media key +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +pub enum MediaKeyCode { + Play, + Pause, + PlayPause, + Reverse, + Stop, + FastForward, + Rewind, + NextTrack, + PreviousTrack, + Record, + LowerVolume, + RaiseVolume, + MuteVolume, +} diff --git a/yazi-term/src/event/mod.rs b/yazi-term/src/event/mod.rs new file mode 100644 index 00000000..6a7bbaf9 --- /dev/null +++ b/yazi-term/src/event/mod.rs @@ -0,0 +1 @@ +yazi_macro::mod_flat!(event keyboard modifiers mouse); diff --git a/yazi-term/src/event/modifiers.rs b/yazi-term/src/event/modifiers.rs new file mode 100644 index 00000000..97ecd1f6 --- /dev/null +++ b/yazi-term/src/event/modifiers.rs @@ -0,0 +1,21 @@ +use bitflags::bitflags; + +bitflags! { + #[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] + pub struct Modifiers: u8 { + const SHIFT = 1; + const ALT = 2; + const CONTROL = 4; + const SUPER = 8; + const HYPER = 16; + const META = 32; + } +} + +impl Modifiers { + pub(crate) fn from_vt_mask(mask: u8) -> Self { Self::from_bits_truncate(mask.saturating_sub(1)) } + + pub(crate) fn for_char(c: char) -> Self { + if c.is_uppercase() { Self::SHIFT } else { Self::empty() } + } +} diff --git a/yazi-term/src/event/mouse.rs b/yazi-term/src/event/mouse.rs new file mode 100644 index 00000000..5530afde --- /dev/null +++ b/yazi-term/src/event/mouse.rs @@ -0,0 +1,76 @@ +use crate::{Result, bail, event::Modifiers}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct MouseEvent { + pub kind: MouseEventKind, + pub column: u16, + pub row: u16, + pub modifiers: Modifiers, +} + +// --- Kind +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum MouseEventKind { + Down(MouseButton), + Up(MouseButton), + Drag(MouseButton), + Moved, + ScrollDown, + ScrollUp, + ScrollLeft, + ScrollRight, +} + +impl MouseEventKind { + /// Cb is the byte of a mouse input that contains the button being used, the + /// key modifiers being held and whether the mouse is dragging or not. + /// + /// Bit layout of cb, from low to high: + /// - button number (bits 0–1) + /// - shift (bit 2) + /// - meta/alt (bit 3) + /// - control (bit 4) + /// - mouse is dragging (bit 5) + /// - button number (bits 6–7) + pub(crate) fn from_cb(cb: u8) -> Result<(Self, Modifiers)> { + let button = (cb & 0b0000_0011) | ((cb & 0b1100_0000) >> 4); + let dragging = cb & 0b0010_0000 == 0b0010_0000; + + let kind = match (button, dragging) { + (0, false) => Self::Down(MouseButton::Left), + (1, false) => Self::Down(MouseButton::Middle), + (2, false) => Self::Down(MouseButton::Right), + (0, true) => Self::Drag(MouseButton::Left), + (1, true) => Self::Drag(MouseButton::Middle), + (2, true) => Self::Drag(MouseButton::Right), + (3, false) => Self::Up(MouseButton::Left), + (3, true) | (4, true) | (5, true) => Self::Moved, + (4, false) => Self::ScrollUp, + (5, false) => Self::ScrollDown, + (6, false) => Self::ScrollLeft, + (7, false) => Self::ScrollRight, + _ => bail!(), + }; + + let mut modifiers = Modifiers::empty(); + if cb & 0b0000_0100 == 0b0000_0100 { + modifiers |= Modifiers::SHIFT; + } + if cb & 0b0000_1000 == 0b0000_1000 { + modifiers |= Modifiers::ALT; + } + if cb & 0b0001_0000 == 0b0001_0000 { + modifiers |= Modifiers::CONTROL; + } + + Ok((kind, modifiers)) + } +} + +// --- Button +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum MouseButton { + Left, + Right, + Middle, +} diff --git a/yazi-term/src/lib.rs b/yazi-term/src/lib.rs index 56110da7..9f4f9b38 100644 --- a/yazi-term/src/lib.rs +++ b/yazi-term/src/lib.rs @@ -1,3 +1,13 @@ -yazi_macro::mod_flat!(option semaphore state term); +mod macros; -pub fn init() { YIELD_TO_SUBPROCESS.init(tokio::sync::Semaphore::new(1)); } +yazi_macro::mod_flat!(dimension error semaphore style term timeout); + +yazi_macro::mod_pub!(event parser restorer sequence source stream terminal waker); + +pub fn init() -> anyhow::Result<()> { + YIELD_TO_SUBPROCESS.init(tokio::sync::Semaphore::new(1)); + + TERM.init(terminal::Terminal::new(&yazi_tty::TTY)?); + + Ok(()) +} diff --git a/yazi-term/src/macros.rs b/yazi-term/src/macros.rs new file mode 100644 index 00000000..4cf63e91 --- /dev/null +++ b/yazi-term/src/macros.rs @@ -0,0 +1,6 @@ +#[macro_export] +macro_rules! bail { + () => { + return Err($crate::ParseError::Invalid) + }; +} diff --git a/yazi-term/src/parser/csi.rs b/yazi-term/src/parser/csi.rs new file mode 100644 index 00000000..82a40b1a --- /dev/null +++ b/yazi-term/src/parser/csi.rs @@ -0,0 +1,279 @@ +use std::str::{self, FromStr}; + +use super::parser::Parser; +use crate::{ParseError, Result, bail, event::{Event, KeyCode, KeyEvent, KeyEventKind, KeyEventState, Modifiers, MouseEvent, MouseEventKind}}; + +impl Parser { + pub(super) fn parse_csi(&self) -> Result { + let seq = &self.seq; + debug_assert!(seq.starts_with(b"\x1B[")); + + if seq.len() == 2 { + return Err(ParseError::Incomplete); + } + + let first = seq[2]; + let second = seq.get(3); + let last = seq[seq.len() - 1]; + + Ok(match first { + b'[' => match second { + None => return Err(ParseError::Incomplete), + Some(b @ b'A'..=b'E') => Event::Key(KeyCode::Fn(1 + b - b'A').into()), + Some(_) => bail!(), + }, + b'D' => Event::Key(KeyCode::Left.into()), + b'C' => Event::Key(KeyCode::Right.into()), + b'A' => Event::Key(KeyCode::Up.into()), + b'B' => Event::Key(KeyCode::Down.into()), + b'H' => Event::Key(KeyCode::Home.into()), + b'F' => Event::Key(KeyCode::End.into()), + b'Z' => Event::Key(KeyEvent::new(KeyCode::Tab, Modifiers::SHIFT)), + b'M' => return self.parse_csi_normal_mouse(), + b'<' => return self.parse_csi_sgr_mouse(), + b'I' => Event::FocusIn, + b'O' => Event::FocusOut, + b';' => return self.parse_csi_modifier_key(), + // P, Q, and S for compatibility with Kitty keyboard protocol, + // as the 1 in 'CSI 1 P' etc. must be omitted if there are no + // modifiers pressed: + // https://sw.kovidgoyal.net/kitty/keyboard-protocol/#legacy-functional-keys + b'P' => Event::Key(KeyCode::Fn(1).into()), + b'Q' => Event::Key(KeyCode::Fn(2).into()), + b'S' => Event::Key(KeyCode::Fn(4).into()), + b'?' => match last { + b'u' | b'c' | b'n' | b'y' => return Err(ParseError::Ignored), + _ => bail!(), + }, + b'>' => match seq[seq.len() - 2..] { + [b' ', b'q'] => return Err(ParseError::Ignored), + _ => bail!(), + }, + b'0'..=b'9' if !(64..=126).contains(&last) => return Err(ParseError::Incomplete), + b'0'..=b'9' => match last { + b'M' => return self.parse_csi_rxvt_mouse(), + b'~' => return self.parse_csi_special_key(), + b'u' => return self.parse_csi_u_key(), + b'R' => return Err(ParseError::Ignored), + _ if self.seq.contains(&b';') => return self.parse_csi_modifier_key(), + _ => return self.parse_csi_modifier_legacy_key(), + }, + _ => bail!(), + }) + } + + pub(super) fn parse_csi_u_key(&self) -> Result { + let seq = &self.seq; + debug_assert!(seq.starts_with(b"\x1B[")); // CSI + debug_assert!(seq.ends_with(b"u")); + + let s = str::from_utf8(&seq[2..seq.len() - 1])?; + let mut it = s.split(';'); + + // In `CSI u`, this is parsed as: + // + // CSI codepoint ; modifiers u + // codepoint: ASCII Dec value + // + // The Kitty Keyboard Protocol extends this with optional components that can be + // enabled progressively. The full sequence is parsed as: + // + // CSI unicode-key-code:alternate-key-codes ; modifiers:event-type ; + // text-as-codepoints u + let mut codepoints = it.next().ok_or(ParseError::Invalid)?.split(':'); + + let (mut code, state_from_keycode) = KeyCode::from_codepoint(parse_next(&mut codepoints)?)?; + let (mut modifiers, kind, state_from_modifiers) = parse_mks(&mut it).unwrap_or_default(); + + if let KeyCode::Modifier(c) = code + && let Some(m) = c.to_modifier() + { + modifiers |= m; + } + + // When the "report alternate keys" flag is enabled in the Kitty Keyboard + // Protocol and the terminal sends a keyboard event containing shift, the + // sequence will contain an additional codepoint separated by a ':' character + // which contains the shifted character according to the keyboard layout. + if modifiers.contains(Modifiers::SHIFT) + && let Ok(Some(shifted)) = parse_next(&mut codepoints).map(char::from_u32) + { + code = KeyCode::Char(shifted); + modifiers.remove(Modifiers::SHIFT); + } + + Ok(Event::Key(KeyEvent { + code, + modifiers, + kind, + state: state_from_keycode | state_from_modifiers, + })) + } + + /// Parses `CSI [1;] modifier[:kind] final` — sequences that carry a + /// semicolon, e.g. `\x1B[;2A` (Shift+Up, leading 1 omitted) or `\x1B[1;2A` + /// (Shift+Up). + pub(super) fn parse_csi_modifier_key(&self) -> Result { + let seq = &self.seq; + debug_assert!(seq.starts_with(b"\x1B[")); // CSI + + let s = str::from_utf8(&seq[2..seq.len() - 1])?; + let mut it = s.split(';'); + it.next(); // skip leading "1" or empty string + + let (modifiers, kind, _) = parse_mks(&mut it).unwrap_or_default(); + let code = KeyCode::from_xterm_modifier(seq[seq.len() - 1])?; + Ok(Event::Key(KeyEvent { code, modifiers, kind, state: KeyEventState::empty() })) + } + + /// Parses legacy `CSI modifier final` - no semicolon, modifier digit + /// immediately before the final byte, e.g. `\x1B[2A` = Shift+Up. + pub(super) fn parse_csi_modifier_legacy_key(&self) -> Result { + let seq = &self.seq; + debug_assert!(seq.starts_with(b"\x1B[")); // CSI + + let modifier = seq[seq.len() - 2]; + if !modifier.is_ascii_digit() { + bail!(); + } + + Ok(Event::Key(KeyEvent { + code: KeyCode::from_xterm_modifier(seq[seq.len() - 1])?, + modifiers: Modifiers::from_vt_mask(modifier - b'0'), + kind: KeyEventKind::Press, + state: KeyEventState::empty(), + })) + } + + pub(super) fn parse_csi_special_key(&self) -> Result { + let seq = &self.seq; + debug_assert!(seq.starts_with(b"\x1B[")); // CSI + debug_assert!(seq.ends_with(b"~")); + + let s = str::from_utf8(&seq[2..seq.len() - 1])?; + let mut it = s.split(';'); + + // This CSI sequence can be a list of semicolon-separated numbers. + let first: u8 = parse_next(&mut it)?; + + let (modifiers, kind, state) = parse_mks(&mut it).unwrap_or_default(); + + let code = match first { + 1 | 7 => KeyCode::Home, + 2 => KeyCode::Insert, + 3 => KeyCode::Delete, + 4 | 8 => KeyCode::End, + 5 => KeyCode::PageUp, + 6 => KeyCode::PageDown, + v @ 11..=15 => KeyCode::Fn(v - 10), + v @ 17..=21 => KeyCode::Fn(v - 11), + v @ 23..=26 => KeyCode::Fn(v - 12), + v @ 28..=29 => KeyCode::Fn(v - 15), + v @ 31..=34 => KeyCode::Fn(v - 17), + _ => bail!(), + }; + + let event = Event::Key(KeyEvent { code, modifiers, kind, state }); + + Ok(event) + } + + // Parse rxvt mouse: CSI Cb ; Cx ; Cy ; M + pub(super) fn parse_csi_rxvt_mouse(&self) -> Result { + let seq = &self.seq; + debug_assert!(seq.starts_with(b"\x1B[")); // CSI + debug_assert!(seq.ends_with(b"M")); + + let s = str::from_utf8(&seq[2..seq.len() - 1])?; + let mut it = s.split(';'); + + let cb = parse_next::(&mut it)?.checked_sub(32).ok_or(ParseError::Invalid)?; + let (kind, modifiers) = MouseEventKind::from_cb(cb)?; + + let column = parse_next::(&mut it)?.checked_sub(1).ok_or(ParseError::Invalid)?; + let row = parse_next::(&mut it)?.checked_sub(1).ok_or(ParseError::Invalid)?; + + Ok(Event::Mouse(MouseEvent { kind, column, row, modifiers })) + } + + // Parse normal mouse: CSI M CB Cx Cy (6 characters only). + pub(super) fn parse_csi_normal_mouse(&self) -> Result { + let seq = &self.seq; + debug_assert!(seq.starts_with(b"\x1B[M")); // CSI M + + if seq.len() < 6 { + return Err(ParseError::Incomplete); + } + + let cb = seq[3].checked_sub(32).ok_or(ParseError::Invalid)?; + let (kind, modifiers) = MouseEventKind::from_cb(cb)?; + + // See http://www.xfree86.org/current/ctlseqs.html#Mouse%20Tracking + // Mouse positions are encoded as (value + 32), but the upper left + // character position on the terminal is denoted as 1,1. + // So, we need to subtract 32 + 1 (33) to keep it synced with the cursor. + let column = u16::from(seq[4].checked_sub(33).ok_or(ParseError::Invalid)?); + let row = u16::from(seq[5].checked_sub(33).ok_or(ParseError::Invalid)?); + + Ok(Event::Mouse(MouseEvent { kind, column, row, modifiers })) + } + + // Parse SGR mouse: CSI < Cb ; Cx ; Cy (;) (M or m) + pub(super) fn parse_csi_sgr_mouse(&self) -> Result { + let seq = &self.seq; + debug_assert!(seq.starts_with(b"\x1B[<")); // CSI < + + if !seq.ends_with(b"m") && !seq.ends_with(b"M") { + return Err(ParseError::Ignored); + } + + let s = str::from_utf8(&seq[3..seq.len() - 1])?; + let mut it = s.split(';'); + + let cb = parse_next(&mut it)?; + let (mut kind, modifiers) = MouseEventKind::from_cb(cb)?; + + // See http://www.xfree86.org/current/ctlseqs.html#Mouse%20Tracking + // The upper left character position on the terminal is denoted as 1,1. + // Subtract 1 to keep it synced with cursor + let column = parse_next::(&mut it)?.checked_sub(1).ok_or(ParseError::Invalid)?; + let row = parse_next::(&mut it)?.checked_sub(1).ok_or(ParseError::Invalid)?; + + // When button 3 in Cb is used to represent mouse release, you can't tell which + // button was released. SGR mode solves this by having the sequence end with a + // lowercase m if it's a button release and an uppercase M if it's a button + // press. + // + // We've already checked that the last character is a lowercase or uppercase M + // at the start of this function, so we just need one if. + if seq.ends_with(b"m") + && let MouseEventKind::Down(button) = kind + { + kind = MouseEventKind::Up(button); + } + + Ok(Event::Mouse(MouseEvent { kind, column, row, modifiers })) + } +} + +fn parse_next<'a, T>(iter: &mut impl Iterator) -> Result +where + T: FromStr, +{ + iter.next().ok_or(ParseError::Invalid)?.parse::().map_err(|_| ParseError::Invalid) +} + +fn parse_mks<'a, I>(mut it: I) -> Option<(Modifiers, KeyEventKind, KeyEventState)> +where + I: Iterator, +{ + let mut it = it.next()?.split(':'); + let mask: u8 = it.next()?.parse().ok()?; + let code: u8 = it.next().and_then(|s| s.parse().ok()).unwrap_or(1); + + Some(( + Modifiers::from_vt_mask(mask), + KeyEventKind::from_vt_code(code), + KeyEventState::from_vt_mask(mask), + )) +} diff --git a/yazi-term/src/parser/ground.rs b/yazi-term/src/parser/ground.rs new file mode 100644 index 00000000..60c1a8dc --- /dev/null +++ b/yazi-term/src/parser/ground.rs @@ -0,0 +1,24 @@ +use crate::{event::{KeyCode, KeyEvent, Modifiers}, parser::Parser}; + +impl Parser { + /// Maps a ground-state byte to a [`KeyEvent`], returning [`None`] for ESC + /// (`\x1B`), multi-byte UTF-8 lead bytes, and continuation / invalid bytes. + pub(super) fn parse_ground_key(b: u8) -> Option { + match b { + b'\r' => Some(KeyCode::Enter.into()), + b'\t' => Some(KeyCode::Tab.into()), + b'\x7F' => Some(KeyCode::Backspace.into()), + b'\0' => Some(KeyEvent::new(KeyCode::Char(' '), Modifiers::CONTROL)), + c @ b'\x01'..=b'\x1A' => { + Some(KeyEvent::new(KeyCode::Char((c - 0x1 + b'a') as char), Modifiers::CONTROL)) + } + c @ b'\x1C'..=b'\x1F' => { + Some(KeyEvent::new(KeyCode::Char((c - 0x1c + b'4') as char), Modifiers::CONTROL)) + } + b'\x20'..=b'\x7E' => { + Some(KeyEvent::new(KeyCode::Char(b as char), Modifiers::for_char(b as char))) + } + _ => None, + } + } +} diff --git a/yazi-term/src/parser/mod.rs b/yazi-term/src/parser/mod.rs new file mode 100644 index 00000000..cc7f5c59 --- /dev/null +++ b/yazi-term/src/parser/mod.rs @@ -0,0 +1,4 @@ +yazi_macro::mod_flat!(csi ground parser state); + +#[cfg(windows)] +yazi_macro::mod_flat!(windows); diff --git a/yazi-term/src/parser/parser.rs b/yazi-term/src/parser/parser.rs new file mode 100644 index 00000000..826a3c94 --- /dev/null +++ b/yazi-term/src/parser/parser.rs @@ -0,0 +1,260 @@ +use std::{collections::VecDeque, num::NonZeroU8, str}; + +use yazi_shim::utf8_char_width; + +use super::state::State; +use crate::event::{Event, KeyCode, KeyEvent, Modifiers}; + +#[derive(Debug)] +pub struct Parser { + state: State, + pub(super) seq: Vec, + pub(crate) events: VecDeque, +} + +impl Default for Parser { + fn default() -> Self { + Self { + state: State::Ground, + seq: Vec::with_capacity(64), + events: VecDeque::with_capacity(32), + } + } +} + +impl Parser { + pub fn parse(&mut self, bytes: &[u8]) { + for &b in bytes { + self.step(b); + } + } + + fn step(&mut self, b: u8) { + match self.state { + State::Ground => self.on_ground(b), + State::Esc => self.on_esc(b), + State::EscO => self.on_esco(b), + State::Csi => self.on_csi(b), + State::NormalMouse => self.on_normal_mouse(b), + State::BracketedPaste => self.on_bracketed_paste(b), + State::Osc | State::OscSt => self.on_osc(b), + State::Dcs | State::DcsSt => self.on_dcs(b), + State::Utf8(n) => self.on_utf8(b, n), + State::AltUtf8(n) => self.on_alt_utf8(b, n), + } + } + + /// Resolve any pending ambiguous state. + /// + /// Call this when no more input bytes are immediately available. If the + /// parser is waiting in the [`State::Esc`] state (a lone `\x1B` has + /// been seen but no follow-up bytes arrived), this emits a bare + /// [`KeyCode::Escape`] event and resets to [`State::Ground`]. + pub fn flush(&mut self) { + if self.state == State::Esc { + self.emit_key(KeyCode::Escape); + } + self.reset(); + } + + fn reset(&mut self) { + self.state = State::Ground; + self.seq.clear(); + } + + pub(crate) fn emit(&mut self, event: impl Into) { self.events.push_back(event.into()); } + + fn emit_key(&mut self, event: impl Into) { self.emit(Event::Key(event.into())); } + + pub fn pop(&mut self) -> Option { self.events.pop_front() } + + fn on_ground(&mut self, b: u8) { + match b { + b'\x1B' => { + self.seq.clear(); + self.seq.push(b'\x1B'); + self.state = State::Esc; + } + _ if let Some(key) = Self::parse_ground_key(b) => { + self.emit_key(key); + } + _ if let w @ 2..=4 = utf8_char_width(b) => { + self.seq.clear(); + self.seq.push(b); + self.state = State::Utf8(NonZeroU8::new(w - 1).unwrap()); + } + _ => {} + } + } + + fn on_esc(&mut self, b: u8) { + self.seq.push(b); + match b { + b'[' => self.state = State::Csi, + b']' => self.state = State::Osc, + b'P' => self.state = State::Dcs, + b'O' => self.state = State::EscO, + // ESC ESC: emit Escape for the first byte, the second is consumed. + b'\x1B' => { + self.emit_key(KeyCode::Escape); + self.reset(); + } + // Alt + + _ if let Some(mut key) = Self::parse_ground_key(b) => { + key.modifiers |= Modifiers::ALT; + self.emit_key(key); + self.reset(); + } + // seq = [ESC, b], collect w-1 more continuation bytes. + _ if let w @ 2..=4 = utf8_char_width(b) => { + self.state = State::AltUtf8(NonZeroU8::new(w - 1).unwrap()); + } + // invalid byte — discard + _ => self.reset(), + } + } + + fn on_esco(&mut self, b: u8) { + self.seq.push(b); + let event = match b { + b'A' => Event::Key(KeyCode::Up.into()), + b'B' => Event::Key(KeyCode::Down.into()), + b'C' => Event::Key(KeyCode::Right.into()), + b'D' => Event::Key(KeyCode::Left.into()), + b'F' => Event::Key(KeyCode::End.into()), + b'H' => Event::Key(KeyCode::Home.into()), + v @ b'P'..=b'S' => Event::Key(KeyCode::Fn(1 + v - b'P').into()), + _ => { + self.reset(); + return; + } + }; + self.emit(event); + self.reset(); + } + + fn on_csi(&mut self, b: u8) { + self.seq.push(b); + + // Bytes below 0x40 are parameter or intermediate bytes; keep accumulating. + if !(0x40..=0x7e).contains(&b) { + return; + } + + // The Linux console sends `\x1B[[x` for F1–F5. The inner `[` (0x5B) is + // technically in the final-byte range but must not trigger early dispatch. + if b == b'[' && self.seq.len() == 3 { + return; + } + + // `\x1B[M` (no params) = X10 normal-mouse encoding; 3 raw bytes follow. + if b == b'M' && self.seq.len() == 3 { + self.state = State::NormalMouse; + return; + } + + // `\x1B[200~` = start of bracketed-paste mode. + if self.seq == b"\x1B[200~" { + self.state = State::BracketedPaste; + return; + } + + if let Ok(e) = self.parse_csi() { + self.emit(e); + } + self.reset(); + } + + fn on_normal_mouse(&mut self, b: u8) { + self.seq.push(b); + + // Need `\x1B[M` + 3 raw bytes = 6 bytes total. + if self.seq.len() < 6 { + return; + } + + if let Ok(e) = self.parse_csi_normal_mouse() { + self.emit(e); + } + self.reset(); + } + + fn on_bracketed_paste(&mut self, b: u8) { + self.seq.push(b); + + if self.seq.ends_with(b"\x1b[201~") { + // seq = b"\x1B[200~" + paste_content + b"\x1B[201~" + let paste = String::from_utf8_lossy(&self.seq[6..self.seq.len() - 6]).into_owned(); + self.emit(Event::Paste(paste)); + self.reset(); + } + } + + fn on_osc(&mut self, b: u8) { + self.seq.push(b); + + match (self.state, b) { + (State::Osc, b'\x07') => self.reset(), // BEL — OSC complete (discard) + (State::Osc, b'\x1B') => self.state = State::OscSt, + (State::Osc, _) => {} // keep accumulating + (State::OscSt, b'\\') => self.reset(), // ST (`\x1B\\`) — OSC complete (discard) + (State::OscSt, b'\x1B') => {} // another ESC — stay in OscSt + (State::OscSt, _) => self.state = State::Osc, // not ST — resume OSC + _ => unreachable!(), + } + } + + fn on_dcs(&mut self, b: u8) { + self.seq.push(b); + + match (self.state, b) { + (State::Dcs, b'\x1B') => self.state = State::DcsSt, + (State::Dcs, _) => {} + (State::DcsSt, b'\\') => self.reset(), // ST — DCS complete (discard) + (State::DcsSt, b'\x1B') => {} // another ESC — stay in DcsSt + (State::DcsSt, _) => self.state = State::Dcs, // not ST — resume DCS + _ => unreachable!(), + } + } + + fn on_utf8(&mut self, b: u8, remaining: NonZeroU8) { + if b & 0xc0 != 0x80 { + self.reset(); + self.on_ground(b); + return; // Not a continuation byte — abandon the sequence and retry from ground. + } + + self.seq.push(b); + if remaining.get() != 1 { + return self.state = State::Utf8(NonZeroU8::new(remaining.get() - 1).unwrap()); + } + + if let Ok(s) = str::from_utf8(&self.seq) + && let Some(c) = s.chars().next() + { + self.emit_key(KeyEvent::new(KeyCode::Char(c), Modifiers::for_char(c))); + } + self.reset(); + } + + fn on_alt_utf8(&mut self, b: u8, remaining: NonZeroU8) { + if b & 0xc0 != 0x80 { + self.reset(); + self.on_ground(b); + return; // Not a continuation byte — abandon Alt+UTF-8 and retry from ground. + } + + self.seq.push(b); + if remaining.get() != 1 { + return self.state = State::AltUtf8(NonZeroU8::new(remaining.get() - 1).unwrap()); + } + + // seq = [ESC, …utf8_bytes…] + if let Ok(s) = str::from_utf8(&self.seq[1..]) + && let Some(c) = s.chars().next() + { + self.emit_key(KeyEvent::new(KeyCode::Char(c), Modifiers::ALT | Modifiers::for_char(c))); + } + self.reset(); + } +} diff --git a/yazi-term/src/parser/state.rs b/yazi-term/src/parser/state.rs new file mode 100644 index 00000000..49780ccb --- /dev/null +++ b/yazi-term/src/parser/state.rs @@ -0,0 +1,29 @@ +use std::num::NonZeroU8; + +#[derive(Debug, Clone, Copy, PartialEq)] +pub(crate) enum State { + /// Normal ground state. + Ground, + /// Saw `\x1B`, waiting for the next byte. + Esc, + /// Saw `\x1B O`, waiting for the SS3 key byte. + EscO, + /// Inside a CSI sequence (`\x1B[` … final-byte in 0x40–0x7E). + Csi, + /// Saw `\x1B[M`, collecting 3 raw mouse bytes (CB Cx Cy). + NormalMouse, + /// Inside bracketed paste (`\x1B[200~` … `\x1B[201~`). + BracketedPaste, + /// Inside an OSC sequence (`\x1B]` … BEL or ST). + Osc, + /// Inside OSC, just saw `\x1B` (potential start of ST = `\x1B\\`). + OscSt, + /// Inside a DCS sequence (`\x1BP` … ST). + Dcs, + /// Inside DCS, just saw `\x1B` (potential start of ST). + DcsSt, + /// Mid-UTF-8 character in ground: `n` continuation bytes still needed. + Utf8(NonZeroU8), + /// `\x1B` + mid-UTF-8 character: `n` continuation bytes still needed. + AltUtf8(NonZeroU8), +} diff --git a/yazi-term/src/parser/windows.rs b/yazi-term/src/parser/windows.rs new file mode 100644 index 00000000..fe9265ad --- /dev/null +++ b/yazi-term/src/parser/windows.rs @@ -0,0 +1,36 @@ +use windows_sys::Win32::System::Console; + +use crate::{Dimension, event::Event, parser::Parser}; + +impl Parser { + pub(crate) fn parse_input_records(&mut self, records: &[Console::INPUT_RECORD]) { + for record in records { + match record.EventType as u32 { + Console::KEY_EVENT => { + let event = unsafe { record.Event.KeyEvent }; + let ch = unsafe { event.uChar.AsciiChar } as u8; + + if event.bKeyDown != 0 && ch != 0 { + self.parse(&[ch]); + } + } + Console::WINDOW_BUFFER_SIZE_EVENT => { + let event = unsafe { record.Event.WindowBufferSizeEvent }; + if event.dwSize.X <= 0 || event.dwSize.Y <= 0 { + continue; + } + + self.events.push_back(Event::Resize(Dimension { + rows: event.dwSize.Y as u16, + cols: event.dwSize.X as u16, + width: 0, + height: 0, + })); + } + _ => {} + } + } + + self.flush(); + } +} diff --git a/yazi-term/src/restorer/mod.rs b/yazi-term/src/restorer/mod.rs new file mode 100644 index 00000000..b57f5db9 --- /dev/null +++ b/yazi-term/src/restorer/mod.rs @@ -0,0 +1,5 @@ +#[cfg(unix)] +yazi_macro::mod_flat!(unix); + +#[cfg(windows)] +yazi_macro::mod_flat!(windows); diff --git a/yazi-term/src/restorer/unix.rs b/yazi-term/src/restorer/unix.rs new file mode 100644 index 00000000..f5b52782 --- /dev/null +++ b/yazi-term/src/restorer/unix.rs @@ -0,0 +1,20 @@ +use std::io::{self, Write}; + +use rustix::termios::{self, OptionalActions, Termios}; +use yazi_tty::Tty; + +#[derive(Clone)] +pub struct Restorer { + pub(crate) termios: Termios, +} + +impl Restorer { + pub(crate) fn new(tty: &Tty) -> io::Result { + Ok(Self { termios: rustix::termios::tcgetattr(tty.writer())? }) + } + + pub fn restore(&self, tty: &Tty) { + tty.writer().flush().ok(); + termios::tcsetattr(tty.writer(), OptionalActions::Now, &self.termios).ok(); + } +} diff --git a/yazi-term/src/restorer/windows.rs b/yazi-term/src/restorer/windows.rs new file mode 100644 index 00000000..4571638b --- /dev/null +++ b/yazi-term/src/restorer/windows.rs @@ -0,0 +1,38 @@ +use std::{io::{self, Write}, os::windows::io::AsRawHandle}; + +use windows_sys::Win32::System::Console::{CONSOLE_MODE, GetConsoleCP, GetConsoleMode, GetConsoleOutputCP, SetConsoleCP, SetConsoleMode, SetConsoleOutputCP}; +use yazi_shim::{bool_ok, nz_ok}; +use yazi_tty::Tty; + +#[derive(Clone)] +pub struct Restorer { + pub(crate) input_mode: CONSOLE_MODE, + pub(crate) output_mode: CONSOLE_MODE, + pub(crate) input_cp: u32, + pub(crate) output_cp: u32, +} + +impl Restorer { + pub(crate) fn new(tty: &Tty) -> io::Result { + let mut input_mode = 0; + bool_ok(unsafe { GetConsoleMode(tty.reader().as_raw_handle(), &mut input_mode) })?; + + let mut output_mode = 0; + bool_ok(unsafe { GetConsoleMode(tty.writer().as_raw_handle(), &mut output_mode) })?; + + let input_cp = nz_ok(unsafe { GetConsoleCP() })?; + let output_cp = nz_ok(unsafe { GetConsoleOutputCP() })?; + + Ok(Self { input_mode, output_mode, input_cp, output_cp }) + } + + pub fn restore(&self, tty: &Tty) { + tty.writer().flush().ok(); + unsafe { + SetConsoleCP(self.input_cp); + SetConsoleMode(tty.reader().as_raw_handle(), self.input_mode); + SetConsoleOutputCP(self.output_cp); + SetConsoleMode(tty.writer().as_raw_handle(), self.output_mode); + } + } +} diff --git a/yazi-term/src/sequence/clipboard.rs b/yazi-term/src/sequence/clipboard.rs new file mode 100644 index 00000000..9ef6be39 --- /dev/null +++ b/yazi-term/src/sequence/clipboard.rs @@ -0,0 +1,20 @@ +use std::fmt::{self, Display}; + +use base64::{Engine, engine::general_purpose}; + +/// Set clipboard content via OSC 52 +pub struct SetClipboard { + content: String, +} + +impl SetClipboard { + pub fn new(content: impl AsRef<[u8]>) -> Self { + Self { content: general_purpose::STANDARD.encode(content) } + } +} + +impl Display for SetClipboard { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "\x1b]52;c;{}\x1b\\", self.content) + } +} diff --git a/yazi-term/src/sequence/csi_u.rs b/yazi-term/src/sequence/csi_u.rs new file mode 100644 index 00000000..f92a7820 --- /dev/null +++ b/yazi-term/src/sequence/csi_u.rs @@ -0,0 +1,26 @@ +use std::fmt::{self, Display}; + +use bitflags::bitflags; + +/// Pop keyboard enhancement flags +pub struct PopKeyboardFlags; + +impl Display for PopKeyboardFlags { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str("\x1b[<1u") } +} + +// Push keyboard enhancement flags +bitflags! { + #[derive(Clone, Copy, Debug, Eq, PartialEq)] + pub struct PushKeyboardFlags: u8 { + const DISAMBIGUATE_ESCAPE_CODES = 1; + const REPORT_EVENT_TYPES = 2; + const REPORT_ALTERNATE_KEYS = 4; + const REPORT_ALL_KEYS_AS_ESCAPE_CODES = 8; + const REPORT_ASSOCIATED_TEXT = 16; + } +} + +impl Display for PushKeyboardFlags { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "\x1b[>{}u", self.bits()) } +} diff --git a/yazi-term/src/sequence/cursor.rs b/yazi-term/src/sequence/cursor.rs new file mode 100644 index 00000000..67036c19 --- /dev/null +++ b/yazi-term/src/sequence/cursor.rs @@ -0,0 +1,82 @@ +use std::fmt::{self, Display}; + +use crate::style::CursorStyle; + +/// Move cursor to 0-based (col, row) position +pub struct MoveTo(pub u16, pub u16); + +impl Display for MoveTo { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "\x1b[{};{}H", self.1 + 1, self.0 + 1) + } +} + +/// Show cursor +pub struct ShowCursor; + +impl Display for ShowCursor { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str("\x1b[?25h") } +} + +/// Hide cursor +pub struct HideCursor; + +impl Display for HideCursor { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str("\x1b[?25l") } +} + +/// Save cursor position +pub struct SaveCursorPos; + +impl Display for SaveCursorPos { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str("\x1b[s") } +} + +/// Restore cursor position +pub struct RestoreCursorPos; + +impl Display for RestoreCursorPos { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str("\x1b[u") } +} + +/// Set terminal window title +pub struct SetTitle<'a>(pub &'a str); + +impl Display for SetTitle<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "\x1b]2;{}\x1b\\", self.0) } +} + +/// Set cursor style +pub struct SetCursorStyle(pub u8); + +impl Display for SetCursorStyle { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "\x1b[{} q", self.0) } +} + +/// Restore cursor shape and blink state +pub struct RestoreCursorStyle { + pub shape: u8, + pub blink: bool, +} + +impl Display for RestoreCursorStyle { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let (shape, shape_blink) = match self.shape { + u8::MAX => (0, None), + n => (n.max(1).div_ceil(2), Some(n.max(1) & 1 == 1)), + }; + + let blink = shape_blink.unwrap_or(self.blink); + let style = match shape { + 2 if blink => CursorStyle::BlinkingUnderline, + 2 if !blink => CursorStyle::SteadyUnderline, + 3 if blink => CursorStyle::BlinkingBar, + 3 if !blink => CursorStyle::SteadyBar, + _ if blink => CursorStyle::Default, + _ if !blink => CursorStyle::SteadyBlock, + _ => unreachable!(), + }; + + SetCursorStyle(style as u8).fmt(f) + } +} diff --git a/yazi-term/src/sequence/erase.rs b/yazi-term/src/sequence/erase.rs new file mode 100644 index 00000000..dafd896a --- /dev/null +++ b/yazi-term/src/sequence/erase.rs @@ -0,0 +1,25 @@ +use std::fmt::{self, Display}; + +use ratatui::backend::ClearType; + +/// Erase entire display +pub struct EraseScreen; + +impl Display for EraseScreen { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { EraseRegion(ClearType::All).fmt(f) } +} + +/// Erase specified region of the display +pub struct EraseRegion(pub ClearType); + +impl Display for EraseRegion { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self.0 { + ClearType::All => f.write_str("\x1b[2J"), + ClearType::AfterCursor => f.write_str("\x1b[0J"), + ClearType::BeforeCursor => f.write_str("\x1b[1J"), + ClearType::CurrentLine => f.write_str("\x1b[2K"), + ClearType::UntilNewLine => f.write_str("\x1b[0K"), + } + } +} diff --git a/yazi-term/src/sequence/if.rs b/yazi-term/src/sequence/if.rs new file mode 100644 index 00000000..cb54c7eb --- /dev/null +++ b/yazi-term/src/sequence/if.rs @@ -0,0 +1,9 @@ +use std::fmt::{self, Display}; + +pub struct If(pub bool, pub T); + +impl Display for If { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if self.0 { write!(f, "{}", self.1) } else { Ok(()) } + } +} diff --git a/yazi-term/src/sequence/mod.rs b/yazi-term/src/sequence/mod.rs new file mode 100644 index 00000000..5d166474 --- /dev/null +++ b/yazi-term/src/sequence/mod.rs @@ -0,0 +1 @@ +yazi_macro::mod_flat!(clipboard csi_u cursor erase mode query r#if restore_background set_background style sync); diff --git a/yazi-term/src/sequence/mode.rs b/yazi-term/src/sequence/mode.rs new file mode 100644 index 00000000..afecfc53 --- /dev/null +++ b/yazi-term/src/sequence/mode.rs @@ -0,0 +1,61 @@ +use std::fmt::{self, Display}; + +/// Enter alternate screen buffer +pub struct EnterAlternateScreen; + +impl Display for EnterAlternateScreen { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str("\x1b[?1049h") } +} + +/// Leave alternate screen buffer +pub struct LeaveAlternateScreen; + +impl Display for LeaveAlternateScreen { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str("\x1b[?1049l") } +} + +/// Enable bracketed paste mode +pub struct EnableBracketedPaste; + +impl Display for EnableBracketedPaste { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str("\x1b[?2004h") } +} + +/// Disable bracketed paste mode +pub struct DisableBracketedPaste; + +impl Display for DisableBracketedPaste { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str("\x1b[?2004l") } +} + +/// Enable focus change reporting +pub struct EnableFocusChange; + +impl Display for EnableFocusChange { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str("\x1b[?1004h") } +} + +/// Disable focus change reporting +pub struct DisableFocusChange; + +impl Display for DisableFocusChange { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str("\x1b[?1004l") } +} + +/// Enable mouse capture (X10 + ButtonEvent + UrXvt + SGR) +pub struct EnableMouseCapture; + +impl Display for EnableMouseCapture { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str("\x1b[?1000h\x1b[?1002h\x1b[?1015h\x1b[?1006h") + } +} + +/// Disable mouse capture (SGR + UrXvt + ButtonEvent + X10) +pub struct DisableMouseCapture; + +impl Display for DisableMouseCapture { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str("\x1b[?1006l\x1b[?1015l\x1b[?1002l\x1b[?1000l") + } +} diff --git a/yazi-term/src/sequence/query.rs b/yazi-term/src/sequence/query.rs new file mode 100644 index 00000000..f43ba3f7 --- /dev/null +++ b/yazi-term/src/sequence/query.rs @@ -0,0 +1,66 @@ +use std::fmt::{self, Display}; + +/// XTVERSION request (secondary DA) +pub struct RequestXtVersion; + +impl Display for RequestXtVersion { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str("\x1b[>q") } +} + +/// Request character cell pixel size +pub struct RequestCellPixelSize; + +impl Display for RequestCellPixelSize { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str("\x1b[16t") } +} + +/// Request background color via OSC 11 +pub struct RequestBgColor; + +impl Display for RequestBgColor { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str("\x1b]11;?\x07") } +} + +/// Request device attributes DA1 +pub struct RequestDA1; + +impl Display for RequestDA1 { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str("\x1b[0c") } +} + +/// Query Kitty graphics protocol capabilities +pub struct KittyGraphicsQuery; + +impl Display for KittyGraphicsQuery { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str("\x1b_Gi=31,s=1,v=1,a=q,t=d,f=24;AAAA\x1b\\") + } +} + +/// Request cursor style via DECRQSS (DECSCUSR) +pub struct RequestCursorStyle; + +impl Display for RequestCursorStyle { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str("\x1bP$q q\x1b\\") } +} + +/// Request cursor blink status via DECRQM (DECSET 12) +pub struct RequestCursorBlink; + +impl Display for RequestCursorBlink { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str("\x1b[?12$p") } +} + +/// Request keyboard enhancement flags (CSI u query) +pub struct RequestKeyboardFlags; + +impl Display for RequestKeyboardFlags { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str("\x1b[?u") } +} + +/// Device Status Report (DSR) +pub struct RequestDeviceStatus; + +impl Display for RequestDeviceStatus { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str("\x1b[5n") } +} diff --git a/yazi-term/src/sequence/restore_background.rs b/yazi-term/src/sequence/restore_background.rs new file mode 100644 index 00000000..136fe8ac --- /dev/null +++ b/yazi-term/src/sequence/restore_background.rs @@ -0,0 +1,8 @@ +use std::fmt::{self, Display}; + +/// Restore background color to default +pub struct RestoreBackground; + +impl Display for RestoreBackground { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "\x1b]111\x1b\\") } +} diff --git a/yazi-term/src/sequence/set_background.rs b/yazi-term/src/sequence/set_background.rs new file mode 100644 index 00000000..f6d944aa --- /dev/null +++ b/yazi-term/src/sequence/set_background.rs @@ -0,0 +1,10 @@ +use std::fmt::{self, Display}; + +/// Set background color to a RGB value or named color +pub struct SetBackground<'a>(pub &'a str); + +impl Display for SetBackground<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if self.0.is_empty() { Ok(()) } else { write!(f, "\x1b]11;{}\x1b\\", self.0) } + } +} diff --git a/yazi-term/src/sequence/style.rs b/yazi-term/src/sequence/style.rs new file mode 100644 index 00000000..15e0988c --- /dev/null +++ b/yazi-term/src/sequence/style.rs @@ -0,0 +1,126 @@ +use std::fmt::{self, Display}; + +use ratatui::style::Color; + +/// Set foreground color from a `Color`. +pub struct SetFg(pub Color); + +impl Display for SetFg { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self.0 { + Color::Reset => f.write_str("\x1b[39m"), + Color::Black => f.write_str("\x1b[30m"), + Color::Red => f.write_str("\x1b[31m"), + Color::Green => f.write_str("\x1b[32m"), + Color::Yellow => f.write_str("\x1b[33m"), + Color::Blue => f.write_str("\x1b[34m"), + Color::Magenta => f.write_str("\x1b[35m"), + Color::Cyan => f.write_str("\x1b[36m"), + Color::Gray => f.write_str("\x1b[37m"), + Color::DarkGray => f.write_str("\x1b[90m"), + Color::LightRed => f.write_str("\x1b[91m"), + Color::LightGreen => f.write_str("\x1b[92m"), + Color::LightYellow => f.write_str("\x1b[93m"), + Color::LightBlue => f.write_str("\x1b[94m"), + Color::LightMagenta => f.write_str("\x1b[95m"), + Color::LightCyan => f.write_str("\x1b[96m"), + Color::White => f.write_str("\x1b[97m"), + Color::Rgb(r, g, b) => write!(f, "\x1b[38;2;{r};{g};{b}m"), + Color::Indexed(n) => write!(f, "\x1b[38;5;{n}m"), + } + } +} + +/// Set background color from a `Color`. +pub struct SetBg(pub Color); + +impl Display for SetBg { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self.0 { + Color::Reset => f.write_str("\x1b[49m"), + Color::Black => f.write_str("\x1b[40m"), + Color::Red => f.write_str("\x1b[41m"), + Color::Green => f.write_str("\x1b[42m"), + Color::Yellow => f.write_str("\x1b[43m"), + Color::Blue => f.write_str("\x1b[44m"), + Color::Magenta => f.write_str("\x1b[45m"), + Color::Cyan => f.write_str("\x1b[46m"), + Color::Gray => f.write_str("\x1b[47m"), + Color::DarkGray => f.write_str("\x1b[100m"), + Color::LightRed => f.write_str("\x1b[101m"), + Color::LightGreen => f.write_str("\x1b[102m"), + Color::LightYellow => f.write_str("\x1b[103m"), + Color::LightBlue => f.write_str("\x1b[104m"), + Color::LightMagenta => f.write_str("\x1b[105m"), + Color::LightCyan => f.write_str("\x1b[106m"), + Color::White => f.write_str("\x1b[107m"), + Color::Rgb(r, g, b) => write!(f, "\x1b[48;2;{r};{g};{b}m"), + Color::Indexed(n) => write!(f, "\x1b[48;5;{n}m"), + } + } +} + +/// Set underline color from a `Color`. +pub struct SetUnderlineColor(pub Color); + +impl Display for SetUnderlineColor { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self.0 { + Color::Reset => f.write_str("\x1b[59m"), + Color::Black => write!(f, "\x1b[58;5;0m"), + Color::Red => write!(f, "\x1b[58;5;1m"), + Color::Green => write!(f, "\x1b[58;5;2m"), + Color::Yellow => write!(f, "\x1b[58;5;3m"), + Color::Blue => write!(f, "\x1b[58;5;4m"), + Color::Magenta => write!(f, "\x1b[58;5;5m"), + Color::Cyan => write!(f, "\x1b[58;5;6m"), + Color::Gray => write!(f, "\x1b[58;5;7m"), + Color::DarkGray => write!(f, "\x1b[58;5;8m"), + Color::LightRed => write!(f, "\x1b[58;5;9m"), + Color::LightGreen => write!(f, "\x1b[58;5;10m"), + Color::LightYellow => write!(f, "\x1b[58;5;11m"), + Color::LightBlue => write!(f, "\x1b[58;5;12m"), + Color::LightMagenta => write!(f, "\x1b[58;5;13m"), + Color::LightCyan => write!(f, "\x1b[58;5;14m"), + Color::White => write!(f, "\x1b[58;5;15m"), + Color::Rgb(r, g, b) => write!(f, "\x1b[58;2;{r};{g};{b}m"), + Color::Indexed(n) => write!(f, "\x1b[58;5;{n}m"), + } + } +} + +/// SGR text attribute. +#[derive(Clone, Copy)] +#[repr(u8)] +pub enum SetSgr { + Reset = 0, + Bold = 1, + Dim = 2, + Italic = 3, + Underlined = 4, + SlowBlink = 5, + RapidBlink = 6, + Reverse = 7, + Hidden = 8, + CrossedOut = 9, + NormalIntensity = 22, + NoItalic = 23, + NoUnderline = 24, + NoBlink = 25, + NoReverse = 27, + NoHidden = 28, + NotCrossedOut = 29, +} + +impl Display for SetSgr { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "\x1b[{}m", *self as u8) } +} + +/// Reset all colors and SGR attributes +pub struct ResetAttrs; + +impl Display for ResetAttrs { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}{}{}", SetFg(Color::Reset), SetBg(Color::Reset), SetSgr::Reset) + } +} diff --git a/yazi-term/src/sequence/sync.rs b/yazi-term/src/sequence/sync.rs new file mode 100644 index 00000000..9b5f802a --- /dev/null +++ b/yazi-term/src/sequence/sync.rs @@ -0,0 +1,15 @@ +use std::fmt::{self, Display}; + +/// Begin synchronized update (DEC 2026) +pub struct BeginSyncUpdate; + +impl Display for BeginSyncUpdate { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str("\x1b[?2026h") } +} + +/// End synchronized update (DEC 2026) +pub struct EndSyncUpdate; + +impl Display for EndSyncUpdate { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str("\x1b[?2026l") } +} diff --git a/yazi-term/src/source/common.rs b/yazi-term/src/source/common.rs new file mode 100644 index 00000000..050f2015 --- /dev/null +++ b/yazi-term/src/source/common.rs @@ -0,0 +1,30 @@ +use std::{io, time::Duration}; + +use crate::{Timeout, event::Event, source::EventSource}; + +impl<'a> EventSource<'a> { + pub fn try_poll(&self, timeout: Option, mut filter: F) -> io::Result + where + F: FnMut(&Event) -> bool, + { + let timeout = Timeout::new(timeout); + + loop { + let mut parser = self.parser.lock(); + if let Some(i) = parser.events.iter().position(&mut filter) { + return Ok(parser.events.remove(i).unwrap()); + } + + drop(parser); + match self.try_fill(timeout) { + Ok(()) => {} + Err(e) if e.kind() == io::ErrorKind::Interrupted => continue, + Err(e) => return Err(e), + } + + if timeout.elapsed() { + return Err(io::Error::from(io::ErrorKind::TimedOut)); + } + } + } +} diff --git a/yazi-term/src/source/mod.rs b/yazi-term/src/source/mod.rs new file mode 100644 index 00000000..f98ee622 --- /dev/null +++ b/yazi-term/src/source/mod.rs @@ -0,0 +1,7 @@ +yazi_macro::mod_flat!(common); + +#[cfg(unix)] +yazi_macro::mod_flat!(unix); + +#[cfg(windows)] +yazi_macro::mod_flat!(windows); diff --git a/yazi-term/src/source/unix.rs b/yazi-term/src/source/unix.rs new file mode 100644 index 00000000..ed1c2862 --- /dev/null +++ b/yazi-term/src/source/unix.rs @@ -0,0 +1,154 @@ +use std::{io::{self, Read}, os::{fd::{AsFd, BorrowedFd}, unix::net::UnixStream}, time::Duration}; + +use parking_lot::Mutex; +use rustix::{event::Timespec, termios}; +use signal_hook::consts::SIGWINCH; +use yazi_tty::{TtyReader, TtyWriter}; + +use crate::{Timeout, event::Event, parser::Parser, waker::Waker}; + +#[derive(Debug)] +pub struct EventSource<'a> { + pub(super) parser: Mutex, + reader: TtyReader<'a>, + writer: TtyWriter<'a>, + waker: Waker, + + sigwinch_id: signal_hook::SigId, + sigwinch_read: UnixStream, +} + +impl Drop for EventSource<'_> { + fn drop(&mut self) { signal_hook::low_level::unregister(self.sigwinch_id); } +} + +impl<'a> EventSource<'a> { + pub(crate) fn new(reader: TtyReader<'a>, writer: TtyWriter<'a>) -> io::Result { + let (sigwinch_read, sigwinch_write) = UnixStream::pair()?; + let sigwinch_id = signal_hook::low_level::pipe::register(SIGWINCH, sigwinch_write)?; + sigwinch_read.set_nonblocking(true)?; + + Ok(Self { + parser: Mutex::new(Parser::default()), + reader, + writer, + waker: Waker::new()?, + + sigwinch_id, + sigwinch_read, + }) + } + + pub fn wake(&self) -> io::Result<()> { self.waker.wake() } + + pub(crate) fn try_fill(&self, timeout: Timeout) -> io::Result<()> { + let mut reader = self.reader.lock(); + let [read_ready, sigwinch_ready, wakeup_ready] = + poll([reader.as_fd(), self.sigwinch_read.as_fd(), self.waker.as_fd()], timeout.leftover())?; + + // Stop waiting for events. + if wakeup_ready { + while read_complete(&*self.waker, &mut [0u8; 1024])? != 0 {} + return Err(io::Error::from(io::ErrorKind::UnexpectedEof)); + } + + // More input is ready. + if read_ready { + let mut buf = [0u8; 1024]; + let len = read_complete(&mut *reader, &mut buf)?; + + let mut parser = self.parser.lock(); + parser.parse(&buf[..len]); + + if len > 0 && len < buf.len() { + parser.flush(); + } + return Ok(()); + } + + // SIGWINCH signal received, indicating a terminal resize. + if sigwinch_ready { + while read_complete(&self.sigwinch_read, &mut [0; 1024])? != 0 {} + self.parser.lock().emit(Event::Resize(termios::tcgetwinsize(self.writer)?.into())); + } + + Ok(()) + } +} + +fn read_complete(mut reader: F, buf: &mut [u8]) -> io::Result { + loop { + match reader.read(buf) { + Ok(len) => return Ok(len), + Err(e) => match e.kind() { + io::ErrorKind::WouldBlock => return Ok(0), + io::ErrorKind::Interrupted => continue, + _ => return Err(e), + }, + } + } +} + +/// A small abstraction over platform specific polling behavior. +/// +/// macOS `poll(2)` doesn't work on file descriptors to `/dev/tty` so we need to +/// use `select(2)` instead. This provides a function which abstracts over the +/// parts of `poll(2)` and `select(2)` we want. Specifically we are looking for +/// `POLLIN` events from `poll(2)` and we consider that to be "ready." +/// +/// This module is not meant to be generic. We consider `POLLIN` to be "ready" +/// and do not look at other poll flags. For the sake of simplicity we also only +/// allow polling exactly three FDs at a time - the exact amount we need for the +/// event source. +fn poll(fds: [BorrowedFd<'_>; 3], timeout: Option) -> io::Result<[bool; 3]> { + #[cfg(not(target_os = "macos"))] + fn poll3(fds: [BorrowedFd<'_>; 3], timeout: Option<&Timespec>) -> io::Result<[bool; 3]> { + use rustix::event::{PollFd, PollFlags}; + let mut fds = [ + PollFd::new(&fds[0], PollFlags::IN), + PollFd::new(&fds[1], PollFlags::IN), + PollFd::new(&fds[2], PollFlags::IN), + ]; + + rustix::event::poll(&mut fds, timeout)?; + + Ok([ + fds[0].revents().contains(PollFlags::IN), + fds[1].revents().contains(PollFlags::IN), + fds[2].revents().contains(PollFlags::IN), + ]) + } + + #[cfg(target_os = "macos")] + fn select3(fds: [BorrowedFd<'_>; 3], timeout: Option<&Timespec>) -> io::Result<[bool; 3]> { + use std::os::fd::AsRawFd; + + use rustix::event::{FdSetElement, FdSetIter, fd_set_insert, fd_set_num_elements}; + + let fds = [fds[0].as_raw_fd(), fds[1].as_raw_fd(), fds[2].as_raw_fd()]; + let nfds = fds.iter().copied().max().unwrap() + 1; + + let mut set = vec![FdSetElement::default(); fd_set_num_elements(fds.len(), nfds)]; + for fd in fds { + fd_set_insert(&mut set, fd); + } + unsafe { rustix::event::select(nfds, Some(&mut set), None, None, timeout) }?; + + let mut result = [false; 3]; + for (fd, ready) in fds.iter().copied().zip(result.iter_mut()) { + if FdSetIter::new(&set).any(|x| x == fd) { + *ready = true; + } + } + Ok(result) + } + + let timespec = timeout.map(|t| t.try_into()).transpose().map_err(|_| { + io::Error::new(io::ErrorKind::InvalidInput, "timeout is too large for the platform") + })?; + + #[cfg(not(target_os = "macos"))] + return poll3(fds, timespec.as_ref()); + #[cfg(target_os = "macos")] + return select3(fds, timespec.as_ref()); +} diff --git a/yazi-term/src/source/windows.rs b/yazi-term/src/source/windows.rs new file mode 100644 index 00000000..a0cc3bde --- /dev/null +++ b/yazi-term/src/source/windows.rs @@ -0,0 +1,55 @@ +use std::{io, mem, os::windows::io::{AsRawHandle, RawHandle}}; + +use parking_lot::Mutex; +use windows_sys::Win32::{Foundation::{WAIT_FAILED, WAIT_OBJECT_0, WAIT_TIMEOUT}, System::{Console::{INPUT_RECORD, ReadConsoleInputA}, Threading::{INFINITE, WaitForMultipleObjects}}}; +use yazi_tty::TtyReader; + +use crate::{Timeout, parser::Parser, waker::Waker}; + +pub struct EventSource<'a> { + pub(super) parser: Mutex, + reader: TtyReader<'a>, + waker: Waker, +} + +impl<'a> EventSource<'a> { + pub(crate) fn new(reader: TtyReader<'a>) -> io::Result { + Ok(Self { reader, parser: Mutex::new(Parser::default()), waker: Waker::new()? }) + } + + pub fn wake(&self) -> io::Result<()> { self.waker.wake() } + + pub(crate) fn try_fill(&self, timeout: Timeout) -> io::Result<()> { + let reader = self.reader.lock(); + let millis = timeout.leftover().map(|dur| dur.as_millis() as u32).unwrap_or(INFINITE); + + let mut handles = [reader.as_raw_handle(), self.waker.as_raw_handle()]; + match unsafe { WaitForMultipleObjects(handles.len() as u32, handles.as_mut_ptr(), 0, millis) } { + // More input is ready. + WAIT_OBJECT_0 => Ok(()), + // Stop waiting for events. + r if r == WAIT_OBJECT_0 + 1 => Err(io::Error::from(io::ErrorKind::UnexpectedEof)), + // Timeout expired. + WAIT_TIMEOUT => Err(io::Error::from(io::ErrorKind::TimedOut)), + // An error occurred. + WAIT_FAILED => Err(io::Error::last_os_error()), + // Unexpected return value. + _ => Err(io::Error::other("WaitForMultipleObjects returned unexpected value")), + }?; + + let (buf, len) = read_console(reader.as_raw_handle())?; + self.parser.lock().parse_input_records(&buf[..len]); + + Ok(()) + } +} + +fn read_console(handle: RawHandle) -> io::Result<([INPUT_RECORD; 128], usize)> { + let mut buf = [unsafe { mem::zeroed::() }; 128]; + let mut len = 0u32; + + match unsafe { ReadConsoleInputA(handle, buf.as_mut_ptr(), buf.len() as u32, &mut len) } { + 0 => Err(io::Error::last_os_error()), + _ => Ok((buf, len as usize)), + } +} diff --git a/yazi-term/src/stream/mod.rs b/yazi-term/src/stream/mod.rs new file mode 100644 index 00000000..2a8b17f1 --- /dev/null +++ b/yazi-term/src/stream/mod.rs @@ -0,0 +1 @@ +yazi_macro::mod_flat!(stream); diff --git a/yazi-term/src/stream/stream.rs b/yazi-term/src/stream/stream.rs new file mode 100644 index 00000000..0f311008 --- /dev/null +++ b/yazi-term/src/stream/stream.rs @@ -0,0 +1,58 @@ +use std::{io, pin::Pin, sync::Arc, task::{Context, Poll}, thread}; + +use futures::Stream; +use tokio::sync::mpsc; + +use crate::{event::Event, source::EventSource, terminal::Terminal}; + +pub struct EventStream { + rx: Option>>, +} + +impl From<&'static Terminal<'_>> for EventStream { + fn from(value: &'static Terminal<'_>) -> Self { Self::new(value.source.clone(), |_| true) } +} + +impl EventStream { + pub fn new(source: Arc>, filter: F) -> Self + where + F: Fn(&Event) -> bool + Send + 'static, + { + let (tx, rx) = mpsc::unbounded_channel(); + + thread::spawn(move || { + loop { + match source.try_poll(None, |_| true) { + Ok(event) => { + if !(filter)(&event) { + continue; + } + if tx.send(Ok(event)).is_err() { + break; + } + } + Err(e) if e.kind() == io::ErrorKind::Interrupted => break, + Err(e) => { + tx.send(Err(e)).ok(); + break; + } + } + } + }); + + Self { rx: Some(rx) } + } + + pub fn take(&mut self) -> Option>> { self.rx.take() } +} + +impl Stream for EventStream { + type Item = io::Result; + + fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + match &mut Pin::into_inner(self).rx { + Some(rx) => rx.poll_recv(cx), + None => Poll::Ready(None), + } + } +} diff --git a/yazi-term/src/style.rs b/yazi-term/src/style.rs new file mode 100644 index 00000000..10157c6e --- /dev/null +++ b/yazi-term/src/style.rs @@ -0,0 +1,11 @@ +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] +pub enum CursorStyle { + #[default] + Default = 0, + BlinkingBlock = 1, + SteadyBlock = 2, + BlinkingUnderline = 3, + SteadyUnderline = 4, + BlinkingBar = 5, + SteadyBar = 6, +} diff --git a/yazi-term/src/term.rs b/yazi-term/src/term.rs index 7d206150..5c908c4e 100644 --- a/yazi-term/src/term.rs +++ b/yazi-term/src/term.rs @@ -1,146 +1,5 @@ -use std::{io, ops::Deref}; +use yazi_shim::cell::RoCell; -use anyhow::Result; -use crossterm::{event::{DisableBracketedPaste, DisableFocusChange, DisableMouseCapture, EnableBracketedPaste, EnableFocusChange, EnableMouseCapture, KeyboardEnhancementFlags, PopKeyboardEnhancementFlags, PushKeyboardEnhancementFlags}, execute, queue, style::Print, terminal::{EnterAlternateScreen, LeaveAlternateScreen, SetTitle, disable_raw_mode, enable_raw_mode}}; -use ratatui::{CompletedFrame, Frame, Terminal, backend::CrosstermBackend, buffer::Buffer, layout::Rect}; -use yazi_emulator::{Emulator, Mux, TMUX}; -use yazi_shim::{cell::SyncCell, crossterm::{If, RestoreBackground, RestoreCursor, SetBackground}}; -use yazi_tty::{TTY, TtyWriter}; +use crate::terminal::Terminal; -use crate::{TermOption, TermState}; - -pub static STATE: SyncCell = SyncCell::new(TermState::default()); - -pub struct Term { - inner: Terminal>>, - last_area: Rect, - last_buffer: Buffer, -} - -impl Term { - pub fn start() -> Result { - let opt = TermOption::default(); - let mut term = Self { - inner: Terminal::new(CrosstermBackend::new(TTY.writer()))?, - last_area: Default::default(), - last_buffer: Default::default(), - }; - - enable_raw_mode()?; - static FIRST: SyncCell = SyncCell::new(false); - if FIRST.replace(true) && yazi_emulator::TMUX.get() { - yazi_emulator::Mux::tmux_passthrough(); - } - - execute!( - TTY.writer(), - If(!TMUX.get(), EnterAlternateScreen), - Print("\x1bP$q q\x1b\\"), // Request cursor shape (DECRQSS query for DECSCUSR) - Print("\x1b[?12$p"), // Request cursor blink status (DECRQM query for DECSET 12) - Print("\x1b[?u"), // Request keyboard enhancement flags (CSI u) - Print("\x1b[0c"), // Request device attributes - If(TMUX.get(), EnterAlternateScreen), - SetBackground(&opt.bg), // Set app background - EnableBracketedPaste, - EnableFocusChange, - If(opt.mouse, EnableMouseCapture), - )?; - - let resp = Emulator::read_until_da1(); - Mux::tmux_drain()?; - - STATE.set(TermState::new(&resp, &opt)); - if STATE.get().csi_u { - _ = queue!( - TTY.writer(), - PushKeyboardEnhancementFlags( - KeyboardEnhancementFlags::DISAMBIGUATE_ESCAPE_CODES - | KeyboardEnhancementFlags::REPORT_ALTERNATE_KEYS, - ) - ); - } - - term.inner.hide_cursor()?; - term.inner.clear()?; - term.inner.flush()?; - Ok(term) - } - - fn stop(&mut self) -> Result<()> { - let state = STATE.get(); - - execute!( - TTY.writer(), - If(state.mouse, DisableMouseCapture), - If(state.bg, RestoreBackground), - If(state.csi_u, PopKeyboardEnhancementFlags), - RestoreCursor { shape: state.cursor_shape, blink: state.cursor_blink }, - If(state.title, SetTitle("")), - DisableFocusChange, - DisableBracketedPaste, - LeaveAlternateScreen, - )?; - - self.inner.show_cursor()?; - Ok(disable_raw_mode()?) - } - - pub fn goodbye(f: impl FnOnce() -> i32) -> ! { - let state = STATE.get(); - - execute!( - TTY.writer(), - If(state.mouse, DisableMouseCapture), - If(state.bg, RestoreBackground), - If(state.csi_u, PopKeyboardEnhancementFlags), - RestoreCursor { shape: state.cursor_shape, blink: state.cursor_blink }, - If(state.title, SetTitle("")), - DisableFocusChange, - DisableBracketedPaste, - LeaveAlternateScreen, - crossterm::cursor::Show - ) - .ok(); - - disable_raw_mode().ok(); - - std::process::exit(f()); - } - - pub fn draw(&mut self, f: impl FnOnce(&mut Frame)) -> io::Result> { - let last = self.inner.draw(f)?; - - self.last_area = last.area; - self.last_buffer = last.buffer.clone(); - Ok(last) - } - - pub fn draw_partial(&mut self, f: impl FnOnce(&mut Frame)) -> io::Result> { - self.inner.draw(|frame| { - let buffer = frame.buffer_mut(); - for y in self.last_area.top()..self.last_area.bottom() { - for x in self.last_area.left()..self.last_area.right() { - let mut cell = self.last_buffer[(x, y)].clone(); - cell.skip = false; - buffer[(x, y)] = cell; - } - } - - f(frame); - }) - } - - pub fn can_partial(&mut self) -> bool { - self.inner.autoresize().is_ok() && self.last_area == self.inner.get_frame().area() - } -} - -impl Drop for Term { - fn drop(&mut self) { self.stop().ok(); } -} - -impl Deref for Term { - type Target = Terminal>>; - - fn deref(&self) -> &Self::Target { &self.inner } -} +pub static TERM: RoCell> = RoCell::new(); diff --git a/yazi-term/src/terminal/mod.rs b/yazi-term/src/terminal/mod.rs new file mode 100644 index 00000000..b57f5db9 --- /dev/null +++ b/yazi-term/src/terminal/mod.rs @@ -0,0 +1,5 @@ +#[cfg(unix)] +yazi_macro::mod_flat!(unix); + +#[cfg(windows)] +yazi_macro::mod_flat!(windows); diff --git a/yazi-term/src/terminal/unix.rs b/yazi-term/src/terminal/unix.rs new file mode 100644 index 00000000..f28327d1 --- /dev/null +++ b/yazi-term/src/terminal/unix.rs @@ -0,0 +1,61 @@ +use std::{env, io, sync::Arc}; + +use rustix::termios; +use yazi_tty::Tty; + +use crate::{Dimension, restorer::Restorer, source::EventSource}; + +pub struct Terminal<'a> { + tty: &'a Tty, + pub source: Arc>, + pub restorer: Restorer, +} + +impl Drop for Terminal<'_> { + fn drop(&mut self) { self.restorer.restore(self.tty); } +} + +impl<'a> Terminal<'a> { + pub fn new(tty: &'a Tty) -> io::Result { + let source = Arc::new(EventSource::new(tty.reader(), tty.writer())?); + let restorer = Restorer::new(tty)?; + + let term = Self { tty, source, restorer }; + term.setup()?; + + Ok(term) + } + + pub fn setup(&self) -> io::Result<()> { Ok(()) } + + pub fn dimension(&self) -> Dimension { + let mut dim = Dimension::default(); + if let Ok(size) = termios::tcgetwinsize(self.tty.writer()) { + dim = size.into(); + } + + if dim.cols == 0 || dim.rows == 0 { + if let Ok(Ok(n)) = env::var("LINES").map(|l| l.parse()) { + dim.rows = n; + } + if let Ok(Ok(n)) = env::var("COLUMNS").map(|c| c.parse()) { + dim.cols = n; + } + } + + dim + } + + pub fn enter_raw_mode(&self) -> io::Result<()> { + let mut termios = self.restorer.termios.clone(); + termios.make_raw(); + + termios::tcsetattr(self.tty.writer(), termios::OptionalActions::Flush, &termios)?; + Ok(()) + } + + pub fn enter_cooked_mode(&self) -> io::Result<()> { + termios::tcsetattr(self.tty.writer(), termios::OptionalActions::Now, &self.restorer.termios)?; + Ok(()) + } +} diff --git a/yazi-term/src/terminal/windows.rs b/yazi-term/src/terminal/windows.rs new file mode 100644 index 00000000..cbe0d5af --- /dev/null +++ b/yazi-term/src/terminal/windows.rs @@ -0,0 +1,90 @@ +use std::{io, mem, os::windows::prelude::*, sync::Arc}; + +use windows_sys::Win32::{Globalization::CP_UTF8, System::Console::{CONSOLE_MODE, CONSOLE_SCREEN_BUFFER_INFO, DISABLE_NEWLINE_AUTO_RETURN, ENABLE_ECHO_INPUT, ENABLE_LINE_INPUT, ENABLE_MOUSE_INPUT, ENABLE_PROCESSED_INPUT, ENABLE_VIRTUAL_TERMINAL_INPUT, ENABLE_VIRTUAL_TERMINAL_PROCESSING, ENABLE_WINDOW_INPUT, GetConsoleScreenBufferInfo, SetConsoleCP, SetConsoleMode, SetConsoleOutputCP}}; +use yazi_shim::bool_ok; +use yazi_tty::Tty; + +use crate::{Dimension, restorer::Restorer, source::EventSource}; + +pub struct Terminal<'a> { + tty: &'a Tty, + pub source: Arc>, + pub restorer: Restorer, +} + +impl Drop for Terminal<'_> { + fn drop(&mut self) { self.restorer.restore(self.tty); } +} + +impl<'a> Terminal<'a> { + pub fn new(tty: &'a Tty) -> io::Result { + let source = Arc::new(EventSource::new(tty.reader())?); + let restorer = Restorer::new(tty)?; + + let term = Self { tty, source, restorer }; + term.setup()?; + + Ok(term) + } + + pub fn setup(&self) -> io::Result<()> { + self.set_input_cp(CP_UTF8)?; + self.set_output_cp(CP_UTF8)?; + Ok(()) + } + + pub fn dimension(&self) -> Dimension { + let mut info: CONSOLE_SCREEN_BUFFER_INFO = unsafe { mem::zeroed() }; + if unsafe { GetConsoleScreenBufferInfo(self.tty.writer().as_raw_handle(), &mut info) } == 0 { + return Dimension::default(); + } + + Dimension { + rows: (info.srWindow.Bottom - info.srWindow.Top) as u16 + 1, + cols: (info.srWindow.Right - info.srWindow.Left) as u16 + 1, + width: 0, + height: 0, + } + } + + pub fn enter_raw_mode(&self) -> io::Result<()> { + self.set_input_mode( + (self.restorer.input_mode + & !(ENABLE_ECHO_INPUT | ENABLE_LINE_INPUT | ENABLE_PROCESSED_INPUT)) + | ENABLE_VIRTUAL_TERMINAL_INPUT + | ENABLE_MOUSE_INPUT + | ENABLE_WINDOW_INPUT, + )?; + + self.set_output_mode( + self.restorer.output_mode | ENABLE_VIRTUAL_TERMINAL_PROCESSING | DISABLE_NEWLINE_AUTO_RETURN, + ) + } + + pub fn enter_cooked_mode(&self) -> io::Result<()> { + self.set_output_mode( + self.restorer.output_mode + & !(ENABLE_VIRTUAL_TERMINAL_PROCESSING | DISABLE_NEWLINE_AUTO_RETURN), + )?; + + self.set_input_mode( + (self.restorer.input_mode + & !(ENABLE_VIRTUAL_TERMINAL_INPUT | ENABLE_MOUSE_INPUT | ENABLE_WINDOW_INPUT)) + | ENABLE_ECHO_INPUT + | ENABLE_LINE_INPUT + | ENABLE_PROCESSED_INPUT, + ) + } + + fn set_input_cp(&self, cp: u32) -> io::Result<()> { bool_ok(unsafe { SetConsoleCP(cp) }) } + + fn set_output_cp(&self, cp: u32) -> io::Result<()> { bool_ok(unsafe { SetConsoleOutputCP(cp) }) } + + fn set_input_mode(&self, mode: CONSOLE_MODE) -> io::Result<()> { + bool_ok(unsafe { SetConsoleMode(self.tty.reader().as_raw_handle(), mode) }) + } + + fn set_output_mode(&self, mode: CONSOLE_MODE) -> io::Result<()> { + bool_ok(unsafe { SetConsoleMode(self.tty.writer().as_raw_handle(), mode) }) + } +} diff --git a/yazi-term/src/timeout.rs b/yazi-term/src/timeout.rs new file mode 100644 index 00000000..885c4044 --- /dev/null +++ b/yazi-term/src/timeout.rs @@ -0,0 +1,22 @@ +use std::time::{Duration, Instant}; + +#[derive(Clone, Copy, Debug)] +pub(crate) struct Timeout { + timeout: Option, + start: Instant, +} + +impl Timeout { + pub(crate) fn new(timeout: Option) -> Self { Self { timeout, start: Instant::now() } } + + pub(crate) fn elapsed(&self) -> bool { + self.timeout.map(|timeout| self.start.elapsed() >= timeout).unwrap_or(false) + } + + pub(crate) fn leftover(&self) -> Option { + self.timeout.map(|timeout| { + let elapsed = self.start.elapsed(); + timeout.checked_sub(elapsed).unwrap_or(Duration::ZERO) + }) + } +} diff --git a/yazi-term/src/waker/mod.rs b/yazi-term/src/waker/mod.rs new file mode 100644 index 00000000..b57f5db9 --- /dev/null +++ b/yazi-term/src/waker/mod.rs @@ -0,0 +1,5 @@ +#[cfg(unix)] +yazi_macro::mod_flat!(unix); + +#[cfg(windows)] +yazi_macro::mod_flat!(windows); diff --git a/yazi-term/src/waker/unix.rs b/yazi-term/src/waker/unix.rs new file mode 100644 index 00000000..aab42f68 --- /dev/null +++ b/yazi-term/src/waker/unix.rs @@ -0,0 +1,25 @@ +use std::{io::{self, Write}, ops::Deref, os::unix::net::UnixStream}; + +#[derive(Debug)] +pub(crate) struct Waker { + reader: UnixStream, + writer: UnixStream, +} + +impl Deref for Waker { + type Target = UnixStream; + + fn deref(&self) -> &Self::Target { &self.reader } +} + +impl Waker { + pub(crate) fn new() -> io::Result { + let (reader, writer) = UnixStream::pair()?; + reader.set_nonblocking(true)?; + writer.set_nonblocking(true)?; + + Ok(Self { reader, writer }) + } + + pub fn wake(&self) -> io::Result<()> { Write::write_all(&mut &self.writer, &[0]) } +} diff --git a/yazi-term/src/waker/windows.rs b/yazi-term/src/waker/windows.rs new file mode 100644 index 00000000..32d27965 --- /dev/null +++ b/yazi-term/src/waker/windows.rs @@ -0,0 +1,33 @@ +use std::{io, ops::Deref, os::windows::io::{AsRawHandle, FromRawHandle, OwnedHandle}, ptr, sync::Arc}; + +use windows_sys::Win32::System::Threading; + +#[derive(Debug)] +pub(crate) struct Waker { + handle: Arc, +} + +impl Deref for Waker { + type Target = OwnedHandle; + + fn deref(&self) -> &Self::Target { &self.handle } +} + +impl Waker { + pub(crate) fn new() -> io::Result { + let handle = unsafe { Threading::CreateEventW(ptr::null(), 0, 0, ptr::null()) }; + if handle.is_null() { + Err(io::Error::last_os_error()) + } else { + Ok(Self { handle: Arc::new(unsafe { OwnedHandle::from_raw_handle(handle) }) }) + } + } + + pub fn wake(&self) -> io::Result<()> { + if unsafe { Threading::SetEvent(self.handle.as_raw_handle()) } == 0 { + Err(io::Error::last_os_error()) + } else { + Ok(()) + } + } +} diff --git a/yazi-tty/Cargo.toml b/yazi-tty/Cargo.toml index d62e588e..cd598d63 100644 --- a/yazi-tty/Cargo.toml +++ b/yazi-tty/Cargo.toml @@ -13,9 +13,8 @@ rust-version.workspace = true workspace = true [dependencies] -yazi-macro = { path = "../yazi-macro", version = "26.5.6" } -yazi-shared = { path = "../yazi-shared", version = "26.5.6" } -yazi-shim = { path = "../yazi-shim", version = "26.5.6" } +yazi-macro = { path = "../yazi-macro", version = "26.5.6" } +yazi-shim = { path = "../yazi-shim", version = "26.5.6" } # External dependencies parking_lot = { workspace = true } diff --git a/yazi-tty/src/handle.rs b/yazi-tty/src/handle.rs index 270350cf..acf6415e 100644 --- a/yazi-tty/src/handle.rs +++ b/yazi-tty/src/handle.rs @@ -1,19 +1,21 @@ -use std::{io::{Error, ErrorKind, Read, Write}, ptr, time::Duration}; +use std::{io::{Error, Read, Write}, ptr, time::Duration}; use tracing::error; +#[derive(Debug)] pub struct Handle { #[cfg(unix)] - inner: std::os::fd::RawFd, + inner: std::os::fd::RawFd, #[cfg(windows)] - inner: std::os::windows::io::RawHandle, - close: bool, - #[cfg(windows)] - out_utf8: bool, - #[cfg(windows)] - incomplete_utf8: super::IncompleteUtf8, + inner: std::os::windows::io::RawHandle, + close: bool, } +// Windows HANDLEs are kernel object references and it is safe to use them in +// any threads. +#[cfg(windows)] +unsafe impl Send for Handle {} + impl Drop for Handle { fn drop(&mut self) { #[cfg(unix)] @@ -49,19 +51,21 @@ impl Read for Handle { fn read(&mut self, buf: &mut [u8]) -> std::io::Result { #[cfg(unix)] { - use std::os::{fd::IntoRawFd, unix::io::FromRawFd}; - let mut f = unsafe { std::fs::File::from_raw_fd(self.inner) }; - let result = f.read(buf); - _ = f.into_raw_fd(); - result + match unsafe { libc::read(self.inner, buf.as_mut_ptr() as *mut _, buf.len()) } { + -1 => Err(Error::last_os_error()), + n => Ok(n as usize), + } } #[cfg(windows)] { - use std::os::windows::io::{FromRawHandle, IntoRawHandle}; - let mut f = unsafe { std::fs::File::from_raw_handle(self.inner) }; - let result = f.read(buf); - _ = f.into_raw_handle(); - result + use windows_sys::Win32::Storage::FileSystem::ReadFile; + use yazi_shim::bool_ok; + + let mut len = 0; + bool_ok(unsafe { + ReadFile(self.inner, buf.as_mut_ptr(), buf.len() as u32, &mut len, ptr::null_mut()) + })?; + Ok(len as usize) } } } @@ -70,29 +74,39 @@ impl Write for Handle { fn write(&mut self, buf: &[u8]) -> std::io::Result { #[cfg(unix)] { - use std::os::{fd::IntoRawFd, unix::io::FromRawFd}; - let mut f = unsafe { std::fs::File::from_raw_fd(self.inner) }; - let result = f.write(buf); - _ = f.into_raw_fd(); - result + match unsafe { libc::write(self.inner, buf.as_ptr() as *const _, buf.len()) } { + -1 => Err(Error::last_os_error()), + n => Ok(n as usize), + } } #[cfg(windows)] { - use std::os::windows::io::{FromRawHandle, IntoRawHandle}; - if self.out_utf8 { - let mut f = unsafe { std::fs::File::from_raw_handle(self.inner) }; - let result = f.write(buf); - _ = f.into_raw_handle(); - result - } else { - super::write_console_utf16(buf, &mut self.incomplete_utf8, self.inner) - } + use windows_sys::Win32::Storage::FileSystem::WriteFile; + use yazi_shim::bool_ok; + + let mut len = 0; + bool_ok(unsafe { + WriteFile(self.inner, buf.as_ptr(), buf.len() as u32, &mut len, ptr::null_mut()) + })?; + Ok(len as usize) } } fn flush(&mut self) -> std::io::Result<()> { Ok(()) } } +#[cfg(unix)] +impl std::os::fd::AsFd for Handle { + fn as_fd(&self) -> std::os::fd::BorrowedFd<'_> { + unsafe { std::os::fd::BorrowedFd::borrow_raw(self.inner) } + } +} + +#[cfg(unix)] +impl std::os::fd::AsRawFd for Handle { + fn as_raw_fd(&self) -> std::os::fd::RawFd { self.inner } +} + #[cfg(unix)] impl std::os::fd::IntoRawFd for Handle { fn into_raw_fd(mut self) -> std::os::fd::RawFd { @@ -101,6 +115,11 @@ impl std::os::fd::IntoRawFd for Handle { } } +#[cfg(windows)] +impl std::os::windows::io::AsRawHandle for Handle { + fn as_raw_handle(&self) -> std::os::windows::io::RawHandle { self.inner } +} + #[cfg(windows)] impl std::os::windows::io::IntoRawHandle for Handle { fn into_raw_handle(mut self) -> std::os::windows::io::RawHandle { @@ -111,7 +130,7 @@ impl std::os::windows::io::IntoRawHandle for Handle { #[cfg(unix)] impl Handle { - pub(super) fn new(out: bool) -> Self { + pub fn new(out: bool) -> Self { use std::{fs::OpenOptions, os::fd::IntoRawFd}; use libc::{STDIN_FILENO, STDOUT_FILENO}; @@ -150,15 +169,6 @@ impl Handle { } } - pub(super) fn read_u8(&mut self) -> std::io::Result { - let mut b = 0; - match unsafe { libc::read(self.inner, &mut b as *mut _ as *mut _, 1) } { - -1 => Err(Error::last_os_error()), - 0 => Err(Error::from(ErrorKind::UnexpectedEof)), - _ => Ok(b), - } - } - pub fn try_clone(&self) -> std::io::Result { match unsafe { libc::dup(self.inner) } { -1 => Err(Error::last_os_error()), @@ -169,10 +179,10 @@ impl Handle { #[cfg(windows)] impl Handle { - pub(super) fn new(out: bool) -> Self { + pub fn new(out: bool) -> Self { use std::{io::{Error, stdin, stdout}, os::windows::io::AsRawHandle}; - use windows_sys::Win32::{Foundation::{GENERIC_READ, GENERIC_WRITE, INVALID_HANDLE_VALUE}, Globalization::CP_UTF8, Storage::FileSystem::{CreateFileW, FILE_SHARE_READ, FILE_SHARE_WRITE, OPEN_EXISTING}, System::Console::GetConsoleOutputCP}; + use windows_sys::Win32::{Foundation::{GENERIC_READ, GENERIC_WRITE, INVALID_HANDLE_VALUE}, Storage::FileSystem::{CreateFileW, FILE_SHARE_READ, FILE_SHARE_WRITE, OPEN_EXISTING}}; let name: Vec = if out { "CONOUT$\0" } else { "CONIN$\0" }.encode_utf16().collect(); let result = unsafe { @@ -188,12 +198,7 @@ impl Handle { }; if result != INVALID_HANDLE_VALUE { - return Self { - inner: result, - close: true, - out_utf8: unsafe { GetConsoleOutputCP() } == CP_UTF8, - incomplete_utf8: Default::default(), - }; + return Self { inner: result, close: true }; } error!( @@ -202,10 +207,8 @@ impl Handle { Error::last_os_error() ); Self { - inner: if out { stdout().as_raw_handle() } else { stdin().as_raw_handle() }, - close: false, - out_utf8: unsafe { GetConsoleOutputCP() } == CP_UTF8, - incomplete_utf8: Default::default(), + inner: if out { stdout().as_raw_handle() } else { stdin().as_raw_handle() }, + close: false, } } @@ -220,21 +223,6 @@ impl Handle { } } - pub(super) fn read_u8(&mut self) -> std::io::Result { - use windows_sys::Win32::Storage::FileSystem::ReadFile; - - let mut buf = 0; - let mut bytes = 0; - let success = unsafe { ReadFile(self.inner, &mut buf, 1, &mut bytes, ptr::null_mut()) }; - - if success == 0 { - return Err(Error::last_os_error()); - } else if bytes == 0 { - return Err(Error::from(ErrorKind::UnexpectedEof)); - } - Ok(buf) - } - pub fn try_clone(&self) -> std::io::Result { use windows_sys::Win32::{Foundation::{DUPLICATE_SAME_ACCESS, DuplicateHandle, HANDLE}, System::Threading::GetCurrentProcess}; @@ -252,15 +240,6 @@ impl Handle { ) }; - if status == 0 { - Err(Error::last_os_error()) - } else { - Ok(Self { - inner: handle, - close: true, - out_utf8: self.out_utf8, - incomplete_utf8: Default::default(), - }) - } + if status == 0 { Err(Error::last_os_error()) } else { Ok(Self { inner: handle, close: true }) } } } diff --git a/yazi-tty/src/lib.rs b/yazi-tty/src/lib.rs index ca166654..c78441b8 100644 --- a/yazi-tty/src/lib.rs +++ b/yazi-tty/src/lib.rs @@ -1,7 +1,4 @@ -yazi_macro::mod_flat!(handle tty); - -#[cfg(windows)] -yazi_macro::mod_flat!(windows); +yazi_macro::mod_flat!(handle reader tty writer); pub static TTY: yazi_shim::cell::RoCell = yazi_shim::cell::RoCell::new(); diff --git a/yazi-tty/src/reader.rs b/yazi-tty/src/reader.rs new file mode 100644 index 00000000..8060ddd8 --- /dev/null +++ b/yazi-tty/src/reader.rs @@ -0,0 +1,37 @@ +use std::{fmt::Debug, io::Read, ops::Deref}; + +use parking_lot::Mutex; + +use crate::Handle; + +#[derive(Clone, Copy)] +pub struct TtyReader<'a>(pub(super) &'a Mutex); + +impl Deref for TtyReader<'_> { + type Target = Mutex; + + fn deref(&self) -> &Self::Target { self.0 } +} + +impl Debug for TtyReader<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.write_str("TtyReader") } +} + +impl Read for TtyReader<'_> { + fn read(&mut self, buf: &mut [u8]) -> std::io::Result { self.0.lock().read(buf) } +} + +#[cfg(unix)] +impl std::os::fd::AsFd for TtyReader<'_> { + fn as_fd(&self) -> std::os::fd::BorrowedFd<'_> { + use std::os::fd::{AsRawFd, BorrowedFd}; + + let raw = self.lock().as_raw_fd(); + unsafe { BorrowedFd::borrow_raw(raw) } + } +} + +#[cfg(windows)] +impl std::os::windows::io::AsRawHandle for TtyReader<'_> { + fn as_raw_handle(&self) -> std::os::windows::io::RawHandle { self.lock().as_raw_handle() } +} diff --git a/yazi-tty/src/tty.rs b/yazi-tty/src/tty.rs index 2ce95464..d55f5118 100644 --- a/yazi-tty/src/tty.rs +++ b/yazi-tty/src/tty.rs @@ -1,8 +1,9 @@ -use std::{io::{BufWriter, Error, ErrorKind, Read, Write}, time::{Duration, Instant}}; +use std::{io::{BufWriter, Error, ErrorKind, Read}, time::{Duration, Instant}}; use parking_lot::{Mutex, MutexGuard}; use super::Handle; +use crate::{TtyReader, TtyWriter}; pub struct Tty { stdin: Mutex, @@ -43,10 +44,11 @@ impl Tty { continue; } - let b = stdin.read_u8()?; - buf.push(b); + let mut b = [0u8]; + stdin.read_exact(&mut b)?; + buf.push(b[0]); - if predicate(b, &buf) { + if predicate(b[0], &buf) { break; } } @@ -57,25 +59,3 @@ impl Tty { (buf, result) } } - -// --- Reader -pub struct TtyReader<'a>(&'a Mutex); - -impl Read for TtyReader<'_> { - fn read(&mut self, buf: &mut [u8]) -> std::io::Result { self.0.lock().read(buf) } -} - -// --- Writer -pub struct TtyWriter<'a>(&'a Mutex>); - -impl std::io::Write for TtyWriter<'_> { - fn write(&mut self, buf: &[u8]) -> std::io::Result { self.0.lock().write(buf) } - - fn flush(&mut self) -> std::io::Result<()> { self.0.lock().flush() } -} - -impl std::fmt::Write for TtyWriter<'_> { - fn write_str(&mut self, s: &str) -> std::fmt::Result { - self.0.lock().write_all(s.as_bytes()).map_err(|_| std::fmt::Error) - } -} diff --git a/yazi-tty/src/windows.rs b/yazi-tty/src/windows.rs deleted file mode 100644 index 2be4ee3b..00000000 --- a/yazi-tty/src/windows.rs +++ /dev/null @@ -1,158 +0,0 @@ -// Copied from https://github.com/rust-lang/rust/blob/master/library/std/src/sys/pal/windows/stdio.rs -use std::{mem::MaybeUninit, os::windows::io::RawHandle, str}; - -use windows_sys::Win32::{Globalization::{CP_UTF8, MB_ERR_INVALID_CHARS, MultiByteToWideChar}, System::Console::WriteConsoleW}; -use yazi_shared::{floor_char_boundary, utf8_char_width}; - -// Apparently Windows doesn't handle large reads on stdin or writes to -// stdout/stderr well (see #13304 for details). -// -// From MSDN (2011): "The storage for this buffer is allocated from a shared -// heap for the process that is 64 KB in size. The maximum size of the buffer -// will depend on heap usage." -// -// We choose the cap at 8 KiB because libuv does the same, and it seems to be -// acceptable so far. -const MAX_BUFFER_SIZE: usize = 8192; - -#[derive(Default)] -pub(super) struct IncompleteUtf8 { - bytes: [u8; 4], - len: u8, -} - -pub(super) fn write_console_utf16( - data: &[u8], - incomplete_utf8: &mut IncompleteUtf8, - handle: RawHandle, -) -> std::io::Result { - if incomplete_utf8.len > 0 { - assert!(incomplete_utf8.len < 4, "Unexpected number of bytes for incomplete UTF-8 codepoint."); - if data[0] >> 6 != 0b10 { - // not a continuation byte - reject - incomplete_utf8.len = 0; - return Err(std::io::Error::new( - std::io::ErrorKind::InvalidData, - "Windows stdio in console mode does not support writing non-UTF-8 byte sequences", - )); - } - incomplete_utf8.bytes[incomplete_utf8.len as usize] = data[0]; - incomplete_utf8.len += 1; - let char_width = utf8_char_width(incomplete_utf8.bytes[0]); - if (incomplete_utf8.len as usize) < char_width { - // more bytes needed - return Ok(1); - } - let s = str::from_utf8(&incomplete_utf8.bytes[0..incomplete_utf8.len as usize]); - incomplete_utf8.len = 0; - match s { - Ok(s) => { - assert_eq!(char_width, s.len()); - let written = write_valid_utf8_to_console(handle, s)?; - assert_eq!(written, s.len()); // guaranteed by write_valid_utf8_to_console() for single codepoint writes - return Ok(1); - } - Err(_) => { - return Err(std::io::Error::new( - std::io::ErrorKind::InvalidData, - "Windows stdio in console mode does not support writing non-UTF-8 byte sequences", - )); - } - } - } - - // As the console is meant for presenting text, we assume bytes of `data` are - // encoded as UTF-8, which needs to be encoded as UTF-16. - // - // If the data is not valid UTF-8 we write out as many bytes as are valid. - // If the first byte is invalid it is either first byte of a multi-byte sequence - // but the provided byte slice is too short or it is the first byte of an - // invalid multi-byte sequence. - let len = std::cmp::min(data.len(), MAX_BUFFER_SIZE / 2); - let utf8 = match str::from_utf8(&data[..len]) { - Ok(s) => s, - Err(ref e) if e.valid_up_to() == 0 => { - let first_byte_char_width = utf8_char_width(data[0]); - if first_byte_char_width > 1 && data.len() < first_byte_char_width { - incomplete_utf8.bytes[0] = data[0]; - incomplete_utf8.len = 1; - return Ok(1); - } else { - return Err(std::io::Error::new( - std::io::ErrorKind::InvalidData, - "Windows stdio in console mode does not support writing non-UTF-8 byte sequences", - )); - } - } - Err(e) => str::from_utf8(&data[..e.valid_up_to()]).unwrap(), - }; - - write_valid_utf8_to_console(handle, utf8) -} - -fn write_valid_utf8_to_console(handle: RawHandle, utf8: &str) -> std::io::Result { - debug_assert!(!utf8.is_empty()); - - let mut utf16 = [MaybeUninit::::uninit(); MAX_BUFFER_SIZE / 2]; - let utf8 = &utf8[..floor_char_boundary(utf8, utf16.len())]; - - let utf16: &[u16] = unsafe { - // Note that this theoretically checks validity twice in the (most common) case - // where the underlying byte sequence is valid utf-8 (given the check in - // `write()`). - let result = MultiByteToWideChar( - CP_UTF8, - MB_ERR_INVALID_CHARS, - utf8.as_ptr(), - utf8.len() as i32, - utf16.as_mut_ptr() as *mut _, - utf16.len() as i32, - ); - assert!(result != 0, "Unexpected error in MultiByteToWideChar"); - - // Safety: MultiByteToWideChar initializes `result` values. - &*(&utf16[..result as usize] as *const [MaybeUninit] as *const [u16]) - }; - - let mut written = write_u16s(handle, utf16)?; - - // Figure out how many bytes of as UTF-8 were written away as UTF-16. - if written == utf16.len() { - Ok(utf8.len()) - } else { - // Make sure we didn't end up writing only half of a surrogate pair (even though - // the chance is tiny). Because it is not possible for user code to re-slice - // `data` in such a way that a missing surrogate can be produced (and also - // because of the UTF-8 validation above), write the missing surrogate out - // now. Buffering it would mean we have to lie about the number of bytes - // written. - let first_code_unit_remaining = utf16[written]; - if matches!(first_code_unit_remaining, 0xdcee..=0xdfff) { - // low surrogate - // We just hope this works, and give up otherwise - let _ = write_u16s(handle, &utf16[written..written + 1]); - written += 1; - } - // Calculate the number of bytes of `utf8` that were actually written. - let mut count = 0; - for ch in utf16[..written].iter() { - count += match ch { - 0x0000..=0x007f => 1, - 0x0080..=0x07ff => 2, - 0xdcee..=0xdfff => 1, // Low surrogate. We already counted 3 bytes for the other. - _ => 3, - }; - } - debug_assert!(String::from_utf16(&utf16[..written]).unwrap() == utf8[..count]); - Ok(count) - } -} - -fn write_u16s(handle: RawHandle, data: &[u16]) -> std::io::Result { - debug_assert!(data.len() < u32::MAX as usize); - let mut written = 0; - let result = unsafe { - WriteConsoleW(handle, data.as_ptr(), data.len() as u32, &mut written, std::ptr::null_mut()) - }; - if result == 0 { Err(std::io::Error::last_os_error()) } else { Ok(written as usize) } -} diff --git a/yazi-tty/src/writer.rs b/yazi-tty/src/writer.rs new file mode 100644 index 00000000..ca997d63 --- /dev/null +++ b/yazi-tty/src/writer.rs @@ -0,0 +1,47 @@ +use std::{fmt::{self, Debug}, io::{self, BufWriter, Write}, ops::Deref}; + +use parking_lot::Mutex; + +use crate::Handle; + +#[derive(Clone, Copy)] +pub struct TtyWriter<'a>(pub(super) &'a Mutex>); + +impl Deref for TtyWriter<'_> { + type Target = Mutex>; + + fn deref(&self) -> &Self::Target { self.0 } +} + +impl Debug for TtyWriter<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str("TtyWriter") } +} + +impl io::Write for TtyWriter<'_> { + fn write(&mut self, buf: &[u8]) -> io::Result { self.0.lock().write(buf) } + + fn flush(&mut self) -> io::Result<()> { self.0.lock().flush() } +} + +impl fmt::Write for TtyWriter<'_> { + fn write_str(&mut self, s: &str) -> fmt::Result { + self.0.lock().write_all(s.as_bytes()).map_err(|_| fmt::Error) + } +} + +#[cfg(unix)] +impl std::os::fd::AsFd for TtyWriter<'_> { + fn as_fd(&self) -> std::os::fd::BorrowedFd<'_> { + use std::os::fd::{AsRawFd, BorrowedFd}; + + let raw = self.lock().get_ref().as_raw_fd(); + unsafe { BorrowedFd::borrow_raw(raw) } + } +} + +#[cfg(windows)] +impl std::os::windows::io::AsRawHandle for TtyWriter<'_> { + fn as_raw_handle(&self) -> std::os::windows::io::RawHandle { + self.lock().get_ref().as_raw_handle() + } +} diff --git a/yazi-tui/Cargo.toml b/yazi-tui/Cargo.toml new file mode 100644 index 00000000..f9f5c717 --- /dev/null +++ b/yazi-tui/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "yazi-tui" +description = "Yazi terminal user interface" +version = "26.5.9" +edition.workspace = true +license.workspace = true +authors.workspace = true +homepage.workspace = true +repository.workspace = true +rust-version.workspace = true + +[lints] +workspace = true + +[dependencies] +yazi-config = { path = "../yazi-config", version = "26.5.6" } +yazi-emulator = { path = "../yazi-emulator", version = "26.5.6" } +yazi-macro = { path = "../yazi-macro", version = "26.5.6" } +yazi-shared = { path = "../yazi-shared", version = "26.5.6" } +yazi-shim = { path = "../yazi-shim", version = "26.5.6" } +yazi-term = { path = "../yazi-term", version = "26.5.6" } +yazi-tty = { path = "../yazi-tty", version = "26.5.9" } + +# External dependencies +anyhow = { workspace = true } +tracing = { workspace = true } +ratatui = { workspace = true } +tokio = { workspace = true } diff --git a/yazi-tui/README.md b/yazi-tui/README.md new file mode 100644 index 00000000..46eddeeb --- /dev/null +++ b/yazi-tui/README.md @@ -0,0 +1,5 @@ +# yazi-tui + +This crate is part of [Yazi][source], and it is not supposed to be used outside, as there are no guarantees about the stability of its API. + +[source]: https://github.com/sxyazi/yazi diff --git a/yazi-tui/src/backend.rs b/yazi-tui/src/backend.rs new file mode 100644 index 00000000..f76fba60 --- /dev/null +++ b/yazi-tui/src/backend.rs @@ -0,0 +1,186 @@ +use std::{io::{self, Write}, ops::{Deref, DerefMut}}; + +use ratatui::{backend::{Backend, ClearType, WindowSize}, buffer::Cell, layout::{Position, Size}, style::{Color, Modifier}}; +use yazi_macro::writef; +use yazi_term::{TERM, sequence::{EraseRegion, HideCursor, MoveTo, ResetAttrs, SetBg, SetFg, SetSgr, SetUnderlineColor, ShowCursor}}; + +pub struct RatermBackend { + writer: W, + cursor_pos: Position, +} + +impl RatermBackend { + pub fn new(writer: W) -> Self { Self { writer, cursor_pos: Position::default() } } +} + +impl Deref for RatermBackend { + type Target = W; + + fn deref(&self) -> &Self::Target { &self.writer } +} + +impl DerefMut for RatermBackend { + fn deref_mut(&mut self) -> &mut Self::Target { &mut self.writer } +} + +impl Backend for RatermBackend +where + W: Write, +{ + type Error = io::Error; + + fn draw<'a, I>(&mut self, content: I) -> io::Result<()> + where + I: Iterator, + { + let mut fg = Color::Reset; + let mut bg = Color::Reset; + let mut underline_color = Color::Reset; + let mut modifier = Modifier::empty(); + let mut last_pos: Option = None; + + for (x, y, cell) in content { + // Move the cursor if the previous location was not (x - 1, y) + if !matches!(last_pos, Some(p) if x == p.x + 1 && y == p.y) { + write!(self.writer, "{}", MoveTo(x, y))?; + } + last_pos = Some(Position { x, y }); + + if cell.modifier != modifier { + write_modifier_diff(&mut self.writer, modifier, cell.modifier)?; + modifier = cell.modifier; + } + + if cell.fg != fg { + write!(self.writer, "{}", SetFg(cell.fg))?; + fg = cell.fg; + } + + if cell.bg != bg { + write!(self.writer, "{}", SetBg(cell.bg))?; + bg = cell.bg; + } + + if cell.underline_color != underline_color { + write!(self.writer, "{}", SetUnderlineColor(cell.underline_color))?; + underline_color = cell.underline_color; + } + + write!(self.writer, "{}", cell.symbol())?; + } + + write!(self.writer, "{ResetAttrs}") + } + + fn hide_cursor(&mut self) -> io::Result<()> { write!(self.writer, "{HideCursor}") } + + fn show_cursor(&mut self) -> io::Result<()> { write!(self.writer, "{ShowCursor}") } + + fn get_cursor_position(&mut self) -> io::Result { Ok(self.cursor_pos) } + + fn set_cursor_position>(&mut self, position: P) -> io::Result<()> { + let Position { x, y } = position.into(); + write!(self.writer, "{}", MoveTo(x, y))?; + self.cursor_pos = Position { x, y }; + self.writer.flush() + } + + fn clear(&mut self) -> io::Result<()> { self.clear_region(ClearType::All) } + + fn clear_region(&mut self, clear_type: ClearType) -> io::Result<()> { + writef!(self.writer, "{}", EraseRegion(clear_type)) + } + + fn append_lines(&mut self, n: u16) -> io::Result<()> { + for _ in 0..n { + writeln!(self.writer)?; + } + self.writer.flush() + } + + fn size(&self) -> io::Result { + let dim = TERM.dimension().checked()?; + Ok(Size { width: dim.cols, height: dim.rows }) + } + + fn window_size(&mut self) -> io::Result { + let dim = TERM.dimension().checked()?; + Ok(WindowSize { + columns_rows: Size { width: dim.cols, height: dim.rows }, + pixels: Size { width: dim.width, height: dim.height }, + }) + } + + fn flush(&mut self) -> io::Result<()> { self.writer.flush() } +} + +/// Apply the diff between two `Modifier` sets by emitting SGR codes. +fn write_modifier_diff(w: &mut impl Write, from: Modifier, to: Modifier) -> io::Result<()> { + let removed = from - to; + if removed.contains(Modifier::REVERSED) { + write!(w, "{}", SetSgr::NoReverse)?; + } + + let reset_intensity = removed.contains(Modifier::BOLD) || removed.contains(Modifier::DIM); + if reset_intensity { + // Bold and Dim are both reset by applying the Normal intensity + write!(w, "{}", SetSgr::NormalIntensity)?; + + // The remaining Bold and Dim attributes must be + // reapplied after the intensity reset above. + if to.contains(Modifier::DIM) { + write!(w, "{}", SetSgr::Dim)?; + } + + if to.contains(Modifier::BOLD) { + write!(w, "{}", SetSgr::Bold)?; + } + } + + if removed.contains(Modifier::ITALIC) { + write!(w, "{}", SetSgr::NoItalic)?; + } + if removed.contains(Modifier::UNDERLINED) { + write!(w, "{}", SetSgr::NoUnderline)?; + } + if removed.contains(Modifier::CROSSED_OUT) { + write!(w, "{}", SetSgr::NotCrossedOut)?; + } + if removed.contains(Modifier::HIDDEN) { + write!(w, "{}", SetSgr::NoHidden)?; + } + if removed.contains(Modifier::SLOW_BLINK) || removed.contains(Modifier::RAPID_BLINK) { + write!(w, "{}", SetSgr::NoBlink)?; + } + + let added = to - from; + if added.contains(Modifier::REVERSED) { + write!(w, "{}", SetSgr::Reverse)?; + } + if added.contains(Modifier::BOLD) && !reset_intensity { + write!(w, "{}", SetSgr::Bold)?; + } + if added.contains(Modifier::ITALIC) { + write!(w, "{}", SetSgr::Italic)?; + } + if added.contains(Modifier::UNDERLINED) { + write!(w, "{}", SetSgr::Underlined)?; + } + if added.contains(Modifier::DIM) && !reset_intensity { + write!(w, "{}", SetSgr::Dim)?; + } + if added.contains(Modifier::CROSSED_OUT) { + write!(w, "{}", SetSgr::CrossedOut)?; + } + if added.contains(Modifier::HIDDEN) { + write!(w, "{}", SetSgr::Hidden)?; + } + if added.contains(Modifier::SLOW_BLINK) { + write!(w, "{}", SetSgr::SlowBlink)?; + } + if added.contains(Modifier::RAPID_BLINK) { + write!(w, "{}", SetSgr::RapidBlink)?; + } + + Ok(()) +} diff --git a/yazi-tui/src/lib.rs b/yazi-tui/src/lib.rs new file mode 100644 index 00000000..30e0fe56 --- /dev/null +++ b/yazi-tui/src/lib.rs @@ -0,0 +1 @@ +yazi_macro::mod_flat!(backend option raterm state); diff --git a/yazi-term/src/option.rs b/yazi-tui/src/option.rs similarity index 74% rename from yazi-term/src/option.rs rename to yazi-tui/src/option.rs index 6b090d87..bdfc81da 100644 --- a/yazi-term/src/option.rs +++ b/yazi-tui/src/option.rs @@ -1,11 +1,11 @@ use yazi_config::{THEME, YAZI}; -pub(super) struct TermOption { +pub(super) struct RatermOption { pub bg: String, pub mouse: bool, } -impl Default for TermOption { +impl Default for RatermOption { fn default() -> Self { Self { bg: THEME.app.bg_color(), mouse: !YAZI.mgr.mouse_events.get().is_empty() } } diff --git a/yazi-tui/src/raterm.rs b/yazi-tui/src/raterm.rs new file mode 100644 index 00000000..1fd82217 --- /dev/null +++ b/yazi-tui/src/raterm.rs @@ -0,0 +1,137 @@ +use std::{io::{self, Write}, ops::Deref}; + +use anyhow::Result; +use ratatui::{CompletedFrame, Frame, Terminal, buffer::Buffer, layout::Rect}; +use yazi_config::YAZI; +use yazi_emulator::{Emulator, Mux, TMUX}; +use yazi_macro::writef; +use yazi_shim::cell::SyncCell; +use yazi_term::{TERM, event::{Event, KeyEventKind}, sequence::{DisableBracketedPaste, DisableFocusChange, DisableMouseCapture, EnableBracketedPaste, EnableFocusChange, EnableMouseCapture, EnterAlternateScreen, If, LeaveAlternateScreen, PopKeyboardFlags, PushKeyboardFlags, RequestCursorBlink, RequestCursorStyle, RequestDA1, RequestKeyboardFlags, RestoreBackground, RestoreCursorStyle, SetBackground, SetTitle, ShowCursor}, stream::EventStream}; +use yazi_tty::{TTY, TtyWriter}; + +use crate::{RatermBackend, RatermOption, RatermState}; + +pub static STATE: SyncCell = SyncCell::new(RatermState::default()); + +pub struct Raterm { + inner: Terminal>>, + stream: EventStream, + last_area: Rect, + last_buffer: Buffer, +} + +impl Deref for Raterm { + type Target = Terminal>>; + + fn deref(&self) -> &Self::Target { &self.inner } +} + +impl Drop for Raterm { + fn drop(&mut self) { Self::stop(); } +} + +impl Raterm { + pub fn start() -> Result { + TERM.setup()?; + TERM.enter_raw_mode()?; + static FIRST: SyncCell = SyncCell::new(false); + if FIRST.replace(true) && yazi_emulator::TMUX.get() { + yazi_emulator::Mux::tmux_passthrough(); + } + + let opt = RatermOption::default(); + writef!( + TTY.writer(), + "{}{RequestCursorStyle}{RequestCursorBlink}{RequestKeyboardFlags}{RequestDA1}{}{}{EnableBracketedPaste}{EnableFocusChange}{}", + If(!TMUX.get(), EnterAlternateScreen), + If(TMUX.get(), EnterAlternateScreen), + SetBackground(&opt.bg), + If(opt.mouse, EnableMouseCapture), + )?; + + let resp = Emulator::read_until_da1(); + Mux::tmux_drain()?; + + STATE.set(RatermState::new(&resp, &opt)); + if STATE.get().csi_u { + write!( + TTY.writer(), + "{}", + PushKeyboardFlags::DISAMBIGUATE_ESCAPE_CODES | PushKeyboardFlags::REPORT_ALTERNATE_KEYS, + )?; + } + + let mut term = Self { + inner: Terminal::new(RatermBackend::new(TTY.writer()))?, + stream: EventStream::from(&*TERM), + last_area: Default::default(), + last_buffer: Default::default(), + }; + + term.inner.hide_cursor()?; + term.inner.clear()?; + term.inner.flush()?; + term.spawn(); + Ok(term) + } + + pub fn stop() { + let state = STATE.get(); + + _ = writef!( + TTY.writer(), + "{}{}{}{}{}{DisableFocusChange}{DisableBracketedPaste}{LeaveAlternateScreen}{ShowCursor}", + If(state.mouse, DisableMouseCapture), + If(state.bg, RestoreBackground), + If(state.csi_u, PopKeyboardFlags), + RestoreCursorStyle { shape: state.cursor_shape, blink: state.cursor_blink }, + If(state.title, SetTitle("")), + ); + + TERM.source.wake().ok(); + TERM.restorer.restore(&TTY); + } + + fn spawn(&mut self) { + let mut rx = self.stream.take().unwrap(); + + tokio::spawn(async move { + while let Some(Ok(event)) = rx.recv().await { + match event { + Event::Key(key) if key.kind != KeyEventKind::Press => continue, + Event::Mouse(mouse) if !YAZI.mgr.mouse_events.get().contains(mouse.kind.into()) => { + continue; + } + _ => yazi_shared::event::Event::Term(event).emit(), + } + } + }); + } + + pub fn draw(&mut self, f: impl FnOnce(&mut Frame)) -> io::Result> { + let last = self.inner.draw(f)?; + + self.last_area = last.area; + self.last_buffer = last.buffer.clone(); + Ok(last) + } + + pub fn draw_partial(&mut self, f: impl FnOnce(&mut Frame)) -> io::Result> { + self.inner.draw(|frame| { + let buffer = frame.buffer_mut(); + for y in self.last_area.top()..self.last_area.bottom() { + for x in self.last_area.left()..self.last_area.right() { + let mut cell = self.last_buffer[(x, y)].clone(); + cell.skip = false; + buffer[(x, y)] = cell; + } + } + + f(frame); + }) + } + + pub fn can_partial(&mut self) -> bool { + self.inner.autoresize().is_ok() && self.last_area == self.inner.get_frame().area() + } +} diff --git a/yazi-term/src/state.rs b/yazi-tui/src/state.rs similarity index 85% rename from yazi-term/src/state.rs rename to yazi-tui/src/state.rs index bc486237..e096e031 100644 --- a/yazi-term/src/state.rs +++ b/yazi-tui/src/state.rs @@ -1,7 +1,7 @@ -use crate::TermOption; +use crate::RatermOption; #[derive(Clone, Copy)] -pub struct TermState { +pub struct RatermState { pub bg: bool, pub csi_u: bool, pub mouse: bool, @@ -10,7 +10,7 @@ pub struct TermState { pub cursor_blink: bool, } -impl TermState { +impl RatermState { pub(super) const fn default() -> Self { Self { bg: false, @@ -22,7 +22,7 @@ impl TermState { } } - pub(super) fn new(resp: &str, opt: &TermOption) -> Self { + pub(super) fn new(resp: &str, opt: &RatermOption) -> Self { let csi_u = resp.contains("\x1b[?0u"); let cursor_shape = resp diff --git a/yazi-vfs/src/provider/sftp/conn.rs b/yazi-vfs/src/provider/sftp/conn.rs index 9c9762ff..bfc03290 100644 --- a/yazi-vfs/src/provider/sftp/conn.rs +++ b/yazi-vfs/src/provider/sftp/conn.rs @@ -1,5 +1,6 @@ use std::{io, sync::Arc, time::{Duration, SystemTime}}; +use chrono::DateTime; use russh::keys::{PrivateKeyWithHashAlg, agent::AgentIdentity}; use yazi_config::vfs::ServiceSftp; use yazi_fs::provider::local::Local; @@ -186,14 +187,14 @@ impl Conn { .verify_signature() .map_err(|e| cfg_err!("Certificate signature verification failed: {e}"))?; - let now: chrono::DateTime = SystemTime::now().into(); - let start: chrono::DateTime = cert.valid_after_time().into(); - let end: chrono::DateTime = cert.valid_before_time().into(); - if now < start || now > end { + let now: DateTime = SystemTime::now().into(); + let start: Option> = cert.valid_after_time().map(Into::into); + let end: Option> = cert.valid_before_time().map(Into::into); + if start.is_some_and(|d| now < d) || end.is_some_and(|d| now > d) { return Err(cfg_err!( "Certificate is out of the validity range of '{}' to '{}'", - start.to_rfc2822(), - end.to_rfc2822() + start.map(|d| d.to_rfc2822()).as_deref().unwrap_or("unknown"), + end.map(|d| d.to_rfc2822()).as_deref().unwrap_or("unknown") )); } } diff --git a/yazi-watcher/src/watched.rs b/yazi-watcher/src/watched.rs index d2d58ead..f74f7a47 100644 --- a/yazi-watcher/src/watched.rs +++ b/yazi-watcher/src/watched.rs @@ -52,7 +52,7 @@ impl Watched { let path = path.percent_decode(SchemeKind::Sftp).ok()?; let path = PathBufDyn::from_components( SchemeKind::Sftp, - Some(Component::RootDir).filter(|_| abs).into_iter().chain(path.components()), + abs.then_some(Component::RootDir).into_iter().chain(path.components()), ) .ok()?; diff --git a/yazi-widgets/Cargo.toml b/yazi-widgets/Cargo.toml index 9f6cce3a..0dd98c4c 100644 --- a/yazi-widgets/Cargo.toml +++ b/yazi-widgets/Cargo.toml @@ -22,12 +22,11 @@ yazi-config = { path = "../yazi-config", version = "26.5.6" } yazi-macro = { path = "../yazi-macro", version = "26.5.6" } yazi-shared = { path = "../yazi-shared", version = "26.5.6" } yazi-shim = { path = "../yazi-shim", version = "26.5.6" } +yazi-term = { path = "../yazi-term", version = "26.5.6" } yazi-tty = { path = "../yazi-tty", version = "26.5.9" } # External dependencies anyhow = { workspace = true } -base64 = { workspace = true } -crossterm = { workspace = true } futures = { workspace = true } mlua = { workspace = true } parking_lot = { workspace = true } @@ -38,6 +37,3 @@ unicode-width = { workspace = true } [target."cfg(windows)".dependencies] clipboard-win = "5.4.1" - -[target.'cfg(target_os = "macos")'.dependencies] -crossterm = { workspace = true, features = [ "use-dev-tty", "libc" ] } diff --git a/yazi-widgets/src/clipboard.rs b/yazi-widgets/src/clipboard.rs index f8f7e1b6..7365b003 100644 --- a/yazi-widgets/src/clipboard.rs +++ b/yazi-widgets/src/clipboard.rs @@ -53,12 +53,13 @@ impl Clipboard { pub async fn set(&self, s: impl AsRef<[u8]>) { use std::process::Stdio; - use crossterm::execute; use tokio::{io::AsyncWriteExt, process::Command}; + use yazi_macro::writef; + use yazi_term::sequence::SetClipboard; use yazi_tty::TTY; s.as_ref().clone_into(&mut self.content.lock()); - execute!(TTY.writer(), osc52::SetClipboard::new(s.as_ref())).ok(); + writef!(TTY.writer(), "{}", SetClipboard::new(s.as_ref())).ok(); let all = [ ("pbcopy", &[][..]), @@ -103,25 +104,3 @@ impl Clipboard { .ok(); } } - -#[cfg(unix)] -mod osc52 { - use base64::{Engine, engine::general_purpose}; - - #[derive(Debug)] - pub struct SetClipboard { - content: String, - } - - impl SetClipboard { - pub fn new(content: &[u8]) -> Self { - Self { content: general_purpose::STANDARD.encode(content) } - } - } - - impl crossterm::Command for SetClipboard { - fn write_ansi(&self, f: &mut impl std::fmt::Write) -> std::fmt::Result { - write!(f, "\x1b]52;c;{}\x1b\\", self.content) - } - } -} diff --git a/yazi-widgets/src/input/input.rs b/yazi-widgets/src/input/input.rs index 936fbda6..22269482 100644 --- a/yazi-widgets/src/input/input.rs +++ b/yazi-widgets/src/input/input.rs @@ -1,12 +1,12 @@ use std::{borrow::Cow, ops::Range}; use anyhow::Result; -use crossterm::cursor::SetCursorStyle; use tokio::sync::mpsc; use yazi_config::YAZI; use yazi_macro::act; use yazi_shared::Ids; use yazi_shim::path::CROSS_SEPARATOR; +use yazi_term::CursorStyle; use super::{InputSnap, InputSnaps, mode::InputMode, op::InputOp}; use crate::{CLIPBOARD, input::{InputEvent, InputOpt}}; @@ -99,7 +99,7 @@ impl Input { if let Some(tx) = self.tx.as_ref().filter(|_| self.completion) { tx.send(InputEvent::Trigger( self.partition().0.to_owned(), - Some(self.ticket.current()).filter(|_| !force), + (!force).then_some(self.ticket.current()), )) .ok(); } @@ -121,16 +121,16 @@ impl Input { pub fn cursor(&self) -> u16 { self.snap().width(self.snap().offset..self.snap().cursor) } - pub fn cursor_shape(&self) -> SetCursorStyle { + pub fn cursor_shape(&self) -> CursorStyle { use InputMode as M; match self.mode() { - M::Normal if YAZI.input.cursor_blink => SetCursorStyle::BlinkingBlock, - M::Normal if !YAZI.input.cursor_blink => SetCursorStyle::SteadyBlock, - M::Insert if YAZI.input.cursor_blink => SetCursorStyle::BlinkingBar, - M::Insert if !YAZI.input.cursor_blink => SetCursorStyle::SteadyBar, - M::Replace if YAZI.input.cursor_blink => SetCursorStyle::BlinkingUnderScore, - M::Replace if !YAZI.input.cursor_blink => SetCursorStyle::SteadyUnderScore, + M::Normal if YAZI.input.cursor_blink => CursorStyle::BlinkingBlock, + M::Normal if !YAZI.input.cursor_blink => CursorStyle::SteadyBlock, + M::Insert if YAZI.input.cursor_blink => CursorStyle::BlinkingBar, + M::Insert if !YAZI.input.cursor_blink => CursorStyle::SteadyBar, + M::Replace if YAZI.input.cursor_blink => CursorStyle::BlinkingUnderline, + M::Replace if !YAZI.input.cursor_blink => CursorStyle::SteadyUnderline, M::Normal | M::Insert | M::Replace => unreachable!(), } }