diff --git a/Cargo.lock b/Cargo.lock index 0082044..96d27d0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "addr2line" @@ -164,126 +164,6 @@ dependencies = [ "wait-timeout", ] -[[package]] -name = "async-channel" -version = "1.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81953c529336010edd6d8e358f886d9581267795c61b19475b71314bffa46d35" -dependencies = [ - "concurrent-queue", - "event-listener 2.5.3", - "futures-core", -] - -[[package]] -name = "async-channel" -version = "2.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89b47800b0be77592da0afd425cc03468052844aff33b84e33cc696f64e77b6a" -dependencies = [ - "concurrent-queue", - "event-listener-strategy", - "futures-core", - "pin-project-lite", -] - -[[package]] -name = "async-executor" -version = "1.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb812ffb58524bdd10860d7d974e2f01cc0950c2438a74ee5ec2e2280c6c4ffa" -dependencies = [ - "async-task", - "concurrent-queue", - "fastrand", - "futures-lite", - "pin-project-lite", - "slab", -] - -[[package]] -name = "async-global-executor" -version = "2.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05b1b633a2115cd122d73b955eadd9916c18c8f510ec9cd1686404c60ad1c29c" -dependencies = [ - "async-channel 2.3.1", - "async-executor", - "async-io", - "async-lock", - "blocking", - "futures-lite", - "once_cell", -] - -[[package]] -name = "async-io" -version = "2.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43a2b323ccce0a1d90b449fd71f2a06ca7faa7c54c2751f06c9bd851fc061059" -dependencies = [ - "async-lock", - "cfg-if", - "concurrent-queue", - "futures-io", - "futures-lite", - "parking", - "polling", - "rustix 0.38.44", - "slab", - "tracing", - "windows-sys 0.59.0", -] - -[[package]] -name = "async-lock" -version = "3.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff6e472cdea888a4bd64f342f09b3f50e1886d32afe8df3d663c01140b811b18" -dependencies = [ - "event-listener 5.4.0", - "event-listener-strategy", - "pin-project-lite", -] - -[[package]] -name = "async-std" -version = "1.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "730294c1c08c2e0f85759590518f6333f0d5a0a766a27d519c1b244c3dfd8a24" -dependencies = [ - "async-channel 1.9.0", - "async-global-executor", - "async-io", - "async-lock", - "crossbeam-utils", - "futures-channel", - "futures-core", - "futures-io", - "futures-lite", - "gloo-timers", - "kv-log-macro", - "log", - "memchr", - "once_cell", - "pin-project-lite", - "pin-utils", - "slab", - "wasm-bindgen-futures", -] - -[[package]] -name = "async-task" -version = "4.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" - -[[package]] -name = "atomic-waker" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" - [[package]] name = "autocfg" version = "1.4.0" @@ -426,9 +306,6 @@ name = "bitflags" version = "2.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" -dependencies = [ - "serde", -] [[package]] name = "blahaj" @@ -436,7 +313,7 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5106bf2680d585dc5f29711b8aa5dde353180b8e14af89b7f0424f760c84e7ce" dependencies = [ - "hashbrown 0.15.3", + "hashbrown", "rand", "zeroize", ] @@ -459,19 +336,6 @@ dependencies = [ "generic-array 0.14.7", ] -[[package]] -name = "blocking" -version = "1.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "703f41c54fc768e63e091340b424302bb1c29ef4aa0c7f10fe849dfb114d29ea" -dependencies = [ - "async-channel 2.3.1", - "async-task", - "futures-io", - "futures-lite", - "piper", -] - [[package]] name = "blowfish" version = "0.9.1" @@ -735,15 +599,6 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" -[[package]] -name = "concurrent-queue" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" -dependencies = [ - "crossbeam-utils", -] - [[package]] name = "console" version = "0.15.11" @@ -787,18 +642,19 @@ dependencies = [ ] [[package]] -name = "crossbeam-utils" -version = "0.8.21" +name = "crossterm" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" - -[[package]] -name = "crossterm_winapi" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" +checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b" dependencies = [ - "winapi", + "bitflags 2.9.1", + "document-features", + "filedescriptor", + "mio", + "parking_lot", + "rustix 1.0.7", + "signal-hook", + "signal-hook-mio", ] [[package]] @@ -866,19 +722,6 @@ dependencies = [ "syn", ] -[[package]] -name = "dashmap" -version = "5.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" -dependencies = [ - "cfg-if", - "hashbrown 0.14.5", - "lock_api", - "once_cell", - "parking_lot_core", -] - [[package]] name = "dbl" version = "0.3.2" @@ -975,6 +818,15 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" +[[package]] +name = "document-features" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95249b50c6c185bee49034bcb378a49dc2b5dff0be90ff6616d31d64febab05d" +dependencies = [ + "litrs", +] + [[package]] name = "dsa" version = "0.6.3" @@ -1116,33 +968,6 @@ dependencies = [ "windows-sys 0.59.0", ] -[[package]] -name = "event-listener" -version = "2.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" - -[[package]] -name = "event-listener" -version = "5.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3492acde4c3fc54c845eaab3eed8bd00c7a7d881f78bfc801e43a93dec1331ae" -dependencies = [ - "concurrent-queue", - "parking", - "pin-project-lite", -] - -[[package]] -name = "event-listener-strategy" -version = "0.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" -dependencies = [ - "event-listener 5.4.0", - "pin-project-lite", -] - [[package]] name = "fastrand" version = "2.3.0" @@ -1213,112 +1038,28 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" -[[package]] -name = "futures" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" -dependencies = [ - "futures-channel", - "futures-core", - "futures-executor", - "futures-io", - "futures-sink", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-channel" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" -dependencies = [ - "futures-core", - "futures-sink", -] - [[package]] name = "futures-core" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" -[[package]] -name = "futures-executor" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" -dependencies = [ - "futures-core", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-io" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" - -[[package]] -name = "futures-lite" -version = "2.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5edaec856126859abb19ed65f39e90fea3a9574b9707f13539acf4abf7eb532" -dependencies = [ - "fastrand", - "futures-core", - "futures-io", - "parking", - "pin-project-lite", -] - -[[package]] -name = "futures-macro" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "futures-sink" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" - [[package]] name = "futures-task" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" -[[package]] -name = "futures-timer" -version = "3.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24" - [[package]] name = "futures-util" version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" dependencies = [ - "futures-channel", "futures-core", - "futures-io", - "futures-macro", - "futures-sink", "futures-task", - "memchr", "pin-project-lite", "pin-utils", - "slab", ] [[package]] @@ -1416,18 +1157,6 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" -[[package]] -name = "gloo-timers" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbb143cf96099802033e0d4f4963b19fd2e0b728bcf076cd9cf7f6634f092994" -dependencies = [ - "futures-channel", - "futures-core", - "js-sys", - "wasm-bindgen", -] - [[package]] name = "group" version = "0.13.0" @@ -1439,12 +1168,6 @@ dependencies = [ "subtle", ] -[[package]] -name = "hashbrown" -version = "0.14.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" - [[package]] name = "hashbrown" version = "0.15.3" @@ -1462,12 +1185,6 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" -[[package]] -name = "hermit-abi" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc" - [[package]] name = "hex" version = "0.4.3" @@ -1679,7 +1396,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e" dependencies = [ "equivalent", - "hashbrown 0.15.3", + "hashbrown", ] [[package]] @@ -1808,26 +1525,10 @@ name = "keyfork-bug" version = "0.1.1" [[package]] -name = "keyfork-crossterm" -version = "0.27.2" +name = "keyfork-crossterm-ioctl-shim" +version = "0.1.0" dependencies = [ - "async-std", - "bitflags 2.9.1", - "crossterm_winapi", - "filedescriptor", - "futures", - "futures-core", - "futures-timer", "libc", - "mio", - "parking_lot", - "serde", - "serde_json", - "serial_test", - "signal-hook", - "signal-hook-mio", - "tokio", - "winapi", ] [[package]] @@ -1915,8 +1616,9 @@ dependencies = [ name = "keyfork-prompt" version = "0.2.3" dependencies = [ + "crossterm", "keyfork-bug", - "keyfork-crossterm", + "keyfork-crossterm-ioctl-shim", "keyfork-mnemonic", "thiserror", ] @@ -2040,15 +1742,6 @@ dependencies = [ "thiserror", ] -[[package]] -name = "kv-log-macro" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0de8b303297635ad57c9f5059fd9cee7a47f8e8daa09df0fcd07dd39fb22977f" -dependencies = [ - "log", -] - [[package]] name = "lalrpop" version = "0.20.2" @@ -2144,6 +1837,12 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" +[[package]] +name = "litrs" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ce301924b7887e9d637144fdade93f9dfff9b60981d4ac161db09720d39aa5" + [[package]] name = "lock_api" version = "0.4.12" @@ -2159,9 +1858,6 @@ name = "log" version = "0.4.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" -dependencies = [ - "value-bag", -] [[package]] name = "lru" @@ -2169,7 +1865,7 @@ version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" dependencies = [ - "hashbrown 0.15.3", + "hashbrown", ] [[package]] @@ -2476,12 +2172,6 @@ dependencies = [ "sha2", ] -[[package]] -name = "parking" -version = "2.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" - [[package]] name = "parking_lot" version = "0.12.3" @@ -2609,17 +2299,6 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" -[[package]] -name = "piper" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96c8c490f422ef9a4efd2cb5b42b76c8613d7e7dfc1caf667b8a3350a5acc066" -dependencies = [ - "atomic-waker", - "fastrand", - "futures-io", -] - [[package]] name = "pkcs1" version = "0.4.1" @@ -2669,21 +2348,6 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" -[[package]] -name = "polling" -version = "3.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a604568c3202727d1507653cb121dbd627a58684eb09a820fd746bee38b4442f" -dependencies = [ - "cfg-if", - "concurrent-queue", - "hermit-abi", - "pin-project-lite", - "rustix 0.38.44", - "tracing", - "windows-sys 0.59.0", -] - [[package]] name = "polyval" version = "0.6.2" @@ -3141,31 +2805,6 @@ dependencies = [ "serde", ] -[[package]] -name = "serial_test" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e56dd856803e253c8f298af3f4d7eb0ae5e23a737252cd90bb4f3b435033b2d" -dependencies = [ - "dashmap", - "futures", - "lazy_static", - "log", - "parking_lot", - "serial_test_derive", -] - -[[package]] -name = "serial_test_derive" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91d129178576168c589c9ec973feedf7d3126c01ac2bf08795109aa35b69fb8f" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "sha1collisiondetection" version = "0.3.4" @@ -3255,15 +2894,6 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" -[[package]] -name = "slab" -version = "0.4.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" -dependencies = [ - "autocfg", -] - [[package]] name = "smallvec" version = "1.15.0" @@ -3476,7 +3106,6 @@ dependencies = [ "bytes", "libc", "mio", - "parking_lot", "pin-project-lite", "signal-hook-registry", "socket2", @@ -3686,12 +3315,6 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" -[[package]] -name = "value-bag" -version = "1.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "943ce29a8a743eb10d6082545d861b24f9d1b160b7d741e0f2cdf726bec909c5" - [[package]] name = "vcpkg" version = "0.2.15" @@ -3764,19 +3387,6 @@ dependencies = [ "wasm-bindgen-shared", ] -[[package]] -name = "wasm-bindgen-futures" -version = "0.4.50" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" -dependencies = [ - "cfg-if", - "js-sys", - "once_cell", - "wasm-bindgen", - "web-sys", -] - [[package]] name = "wasm-bindgen-macro" version = "0.2.100" @@ -3809,16 +3419,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "web-sys" -version = "0.3.77" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - [[package]] name = "which" version = "4.4.2" diff --git a/Cargo.toml b/Cargo.toml index 4099dbb..ef56ea3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,7 +16,7 @@ members = [ "crates/qrcode/keyfork-zbar-sys", "crates/util/keyfork-bin", "crates/util/keyfork-bug", - "crates/util/keyfork-crossterm", + "crates/util/keyfork-crossterm-ioctl-shim", "crates/util/keyfork-entropy", "crates/util/keyfork-frame", "crates/util/keyfork-mnemonic", @@ -26,10 +26,16 @@ members = [ "crates/tests", ] +[workspace.lints.rust] +missing_docs = { level = "warn" } + [workspace.lints.clippy] all = { level = "deny", priority = -1 } pedantic = { level = "warn", priority = -1 } +missing_errors_doc = { level = "warn" } +missing_panics_doc = { level = "warn" } + # used often in tests wildcard_imports = { level = "allow"} @@ -58,7 +64,6 @@ keyfork-zbar = { version = "0.1.0", path = "crates/qrcode/keyfork-zbar", registr keyfork-zbar-sys = { version = "0.1.0", path = "crates/qrcode/keyfork-zbar-sys", registry = "distrust", default-features = false } keyfork-bin = { version = "0.1.0", path = "crates/util/keyfork-bin", registry = "distrust", default-features = false } keyfork-bug = { version = "0.1.1", path = "crates/util/keyfork-bug", registry = "distrust", default-features = false } -keyfork-crossterm = { version = "0.27.1", path = "crates/util/keyfork-crossterm", registry = "distrust", default-features = false } keyfork-entropy = { version = "0.1.1", path = "crates/util/keyfork-entropy", registry = "distrust", default-features = false } keyfork-frame = { version = "0.1.0", path = "crates/util/keyfork-frame", registry = "distrust", default-features = false } keyfork-mnemonic = { version = "0.4.0", path = "crates/util/keyfork-mnemonic", registry = "distrust", default-features = false } diff --git a/crates/tests/src/lib.rs b/crates/tests/src/lib.rs index dbd00d1..f06ac02 100644 --- a/crates/tests/src/lib.rs +++ b/crates/tests/src/lib.rs @@ -1,2 +1,4 @@ +#![allow(missing_docs)] + #[cfg(test)] mod keyfork; diff --git a/crates/util/keyfork-crossterm-ioctl-shim/Cargo.toml b/crates/util/keyfork-crossterm-ioctl-shim/Cargo.toml new file mode 100644 index 0000000..1c02eca --- /dev/null +++ b/crates/util/keyfork-crossterm-ioctl-shim/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "keyfork-crossterm-ioctl-shim" +version = "0.1.0" +edition = "2024" + +[dependencies] +libc = "0.2.172" + +[lints] +workspace = true diff --git a/crates/util/keyfork-crossterm-ioctl-shim/src/lib.rs b/crates/util/keyfork-crossterm-ioctl-shim/src/lib.rs new file mode 100644 index 0000000..37e9085 --- /dev/null +++ b/crates/util/keyfork-crossterm-ioctl-shim/src/lib.rs @@ -0,0 +1,113 @@ +//! A shim for replacing some Crossterm methods with methods tied to a file descriptor. + +use libc::termios as Termios; +use std::{os::fd::RawFd, io::IsTerminal}; + +/// The provided file descriptor was not a terminal. +#[derive(Debug)] +pub struct NotATerminal; + +impl std::fmt::Display for NotATerminal { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str("The provided file descriptor is not a terminal") + } +} + +impl std::error::Error for NotATerminal {} + +/// A terminal controller. +pub struct TerminalIoctl { + fd: RawFd, + stored_termios: Option, +} + +type Result = std::result::Result; + +fn assert_io(result: i32) -> Result { + if result == -1 { + Err(std::io::Error::last_os_error()) + } else { + Ok(result) + } +} + +impl TerminalIoctl { + /// Construct a new controller for the given file descriptor. + /// + /// # Errors + /// + /// The method may return an error if the file descriptor is not a terminal. + pub fn new(fd: RawFd) -> Result { + // SAFETY: We do not invoke any function that closes the file descriptor, and + // the borrowed fd is dropped as the function returns. + let borrowed_fd = unsafe { + std::os::fd::BorrowedFd::borrow_raw(fd) + }; + if !borrowed_fd.is_terminal() { + return Err(std::io::Error::other(NotATerminal)); + } + + Ok(Self { + fd, + stored_termios: None, + }) + } + + fn get_termios(&self) -> Result { + let mut termios = unsafe { std::mem::zeroed() }; + assert_io(unsafe { libc::tcgetattr(self.fd, &mut termios) })?; + Ok(termios) + } + + /// Enable raw mode for the given terminal. + /// + /// Replaces: [`crossterm::terminal::enable_raw_mode`]. + /// + /// # Errors + /// + /// The method may return an error if + pub fn enable_raw_mode(&mut self) -> Result<()> { + if self.stored_termios.is_none() { + let mut termios = self.get_termios()?; + let original_mode_ios = termios; + + unsafe { libc::cfmakeraw(&mut termios) }; + assert_io(unsafe { libc::tcsetattr(self.fd, libc::TCSANOW, &termios) })?; + self.stored_termios = Some(original_mode_ios); + } + Ok(()) + } + + /// Disable raw mode for the given terminal. + /// + /// Replaces: [`crossterm::terminal::disable_raw_mode`]. + /// + /// # Errors + /// + /// The method may propagate errors encountered when interacting with the terminal. + pub fn disable_raw_mode(&mut self) -> Result<()> { + if let Some(termios) = self.stored_termios.take() { + assert_io(unsafe { libc::tcsetattr(self.fd, libc::TCSANOW, &termios) })?; + } + Ok(()) + } + + /// Return the size for the given terminal. + /// + /// Replaces: [`crossterm::terminal::size`]. + /// + /// # Errors + /// + /// The method may propagate errors encountered when interacting with the terminal. + pub fn size(&self) -> Result<(u16, u16)> { + let mut size = libc::winsize { + ws_row: 0, + ws_col: 0, + ws_xpixel: 0, + ws_ypixel: 0, + }; + + assert_io(unsafe { libc::ioctl(self.fd, libc::TIOCGWINSZ, &mut size) })?; + Ok((size.ws_col, size.ws_row)) + } +} diff --git a/crates/util/keyfork-crossterm/.travis.yml b/crates/util/keyfork-crossterm/.travis.yml deleted file mode 100644 index c18a687..0000000 --- a/crates/util/keyfork-crossterm/.travis.yml +++ /dev/null @@ -1,42 +0,0 @@ -# Build only pushed (merged) master or any pull request. This avoids the -# pull request to be build twice. -branches: - only: - - master - -language: rust - -rust: - - stable - - nightly - -os: - - linux - - windows - - osx - -git: - depth: 1 - quiet: true - -matrix: - allow_failures: - - rust: nightly - -before_script: - - export PATH=$PATH:/home/travis/.cargo/bin - - rustup component add rustfmt - - rustup component add clippy - -script: - - cargo fmt --version - - rustup --version - - rustc --version - - if [ "$TRAVIS_RUST_VERSION" = "stable" ]; then cargo fmt --all -- --check; fi - - cargo clippy -- -D clippy::all - - cargo build - - cargo test --lib -- --nocapture --test-threads 1 - - cargo test --lib --features serde -- --nocapture --test-threads 1 - - cargo test --lib --features event-stream -- --nocapture --test-threads 1 - - cargo test --all-features -- --nocapture --test-threads 1 - - if [ "$TRAVIS_RUST_VERSION" = "stable" ]; then cargo package; fi diff --git a/crates/util/keyfork-crossterm/CHANGELOG.md b/crates/util/keyfork-crossterm/CHANGELOG.md deleted file mode 100644 index 8c7f661..0000000 --- a/crates/util/keyfork-crossterm/CHANGELOG.md +++ /dev/null @@ -1,744 +0,0 @@ -# Version 0.27.1 - -## Added ⭐ -- Add support for (de)serializing `Reset` `Color` - -# Version 0.27 - -## Added ⭐ - -- Add `NO_COLOR` support (https://no-color.org/) -- Add option to force overwrite `NO_COLOR` (#802) -- Add support for scroll left/right events on windows and unix systems (#788). -- Add `window_size` function to fetch pixel width/height of screen for more sophisticated rendering in terminals. -- Add support for deserializing hex color strings to `Color` e.g #fffff. - -## Changes - -- Make the events module an optional feature `events` (to make crossterm more lightweight) (#776) - -## Breaking ⚠️ - -- Set minimum rustc version to 1.58 (#798) -- Change all error types to `std::io::Result` (#765) - -# Version 0.26.1 - -## Added ⭐ - -- Add synchronized output/update control (#756) -- Add kitty report alternate keys functionality (#754) -- Updates dev dependencies. - -## Fixed 🐛 -- Fix icorrect return in kitty keyboard enhancement check (#751) -- Fix panic when using `use-dev-tty` feature flag (#762) - -# Version 0.26.0 -## Added ⭐ - -- Add `SetCursorStyle` to set the cursor apearance and visibility. (#742) -- Add a function to check if kitty keyboard enhancement protocol is available. (#732) -- Add filedescriptors poll in order to move away from mio in the future (can be used via `use-dev-tty`). (#735) - -## Fixed 🐛 -- Improved F1-F4 handling for kitty keyboard protocol. (#736) -- Improved parsing of event types/modifiers with certain keys for kitty protocol. (#716) - -## Breaking ⚠️ -- Remove `SetCursorShape` in favour of `SetCursorStyle`. (#742) -- Make Windows resize event match `terminal::size` (#714) -- Rust 1.58 or later is now required. -- Add key release event for windows. (#745) - -# Version 0.25.0 -BREAKING: `Copy` trait is removed from `Event`, you can keep it by removing the "bracked-paste" feature flag. However this flag might be standardized in the future. -We removed the `Copy` from `Event` because the new `Paste` event, which contains a pasted string into the terminal, which is a non-copy string. - -- Add ability to paste a string in into the terminal and fetch the pasted string via events (see `Event::Paste` and `EnableBracketedPaste `). -- Add support for functional key codes from kitty keyboard protocol. Try out by `PushKeyboardEnhancementFlags`. This protocol allows for: - - See: https://sw.kovidgoyal.net/kitty/keyboard-protocol/#modifiers - - Press, Repeat, Release event kinds. - - SUPER, HYPER, META modifiers. - - Media keycodes - - Right/left SHIFT, Control, Alt, Super, Hyper, Meta - - IsoLevel3Shift, IsoLevel5Shift - - Capslock, scroll lock, numlock - - Printscreen, pauze, menue, keyboard begin. -- Create `SetStyle` command to allow setting various styling in one command. -- Terminal Focus events (see `Event::FocusGained` and `Event::FocusLost`) - -# Version 0.24.0 -- Add DoubleUnderlined, Undercurled, Underdots the text, Underdotted, Underdashes, Underdashed attributes and allow coloring their foreground / background color. -- Fix windows unicode character parsing, this fixed various key combinations and support typing unicode characters. -- Consistency and better documentation on mouse cursor operations (BREAKING CHANGE). - - MoveTo, MoveToColumn, MoveToRow are 0-based. (left top most cell is 0,0). Moving like this is absolute - - MoveToNextLine, MoveToPreviousLine, MoveUp, MoveDown, MoveRight, MoveLeft are 1-based,. Moving like this is relative. Moving 1 left means moving 1 left. Moving 0 to the left is not possible, wikipedia states that most terminals will just default to 1. -- terminal::size returns error when previously it returned (0,0). -- Remove println from serialisation code. -- Fix mouse up for middle and right buttons. -- Fix escape codes on Git-Bash + Windows Terminal / Alacritty / WezTerm. -- Add support for cursor keys in application mode. -# Version 0.23.2 -- Update signal-hook and mio to version 0.8. - -# Version 0.23.1 -- Fix control key parsing problem. - -# Version 0.23 -- Update dependencies. -- Add 0 check for all cursor functions to prevent undefined behaviour. -- Add CSIu key parsing for unix. -- Improve control character window key parsing supporting (e.g. CTRL [ and ]) -- Update library to 2021 edition. - -# Version 0.22.1 -- Update yanked version crossterm-winapi and move to crossterm-winapi 0.9.0. -- Changed panic to error when calling disable-mouse capture without setting it first. -- Update bitflags dependency. - -# Version 0.22 -- Fix serde Color serialisation/deserialization inconsistency. -- Update crossterm-winapi 0.8.1 to fix panic for certain mouse events - -# Version 0.21 -- Expose `is_raw` function. -- Add 'purge' option on unix system, this clears the entire screen buffer. -- Improve serialisation for color enum values. - -# Version 0.20 -- Update from signal-hook with 'mio-feature flag' to signal-hook-mio 0.2.1. -- Manually implements Eq, PartialEq and Hash for KeyEvent improving equality checks and hash calculation. -- `crossterm::ErrorKind` to `io::Error`. -- Added Cursor Shape Support. -- Add support for function keys F13...F20. -- Support taking any Display in `SetTitle` command. -- Remove lazy_static dependency. -- Remove extra Clone bounds in the style module. - - Add `MoveToRow` command. - - Remove writer parameter from execute_winapi - -# Version 0.19 -- Use single thread for async event reader. -- Patch timeout handling for event polling this was not working correctly. -- Add unix support for more key combinations mainly complex ones with ALT/SHIFT/CTRL. -- Derive `PartialEq` and `Eq` for ContentStyle -- Fix windows resize event size, this used to be the buffer size but is screen size now. -- Change `Command::ansi_code` to `Command::write_ansi`, this way the ansi code will be written to given formatter. - -# Version 0.18.2 -- Fix panic when only setting bold and redirecting stdout. -- Use `tty_fd` for set/get terminal attributes - -# Version 0.18.1 -- Fix enabling ANSI support when stdout is redirected -- Update crossterm-winapi to 0.6.2 - -# Version 0.18.0 -- Fix get position bug -- Fix windows 8 or lower write to user-given stdout instead of stdout. -- Make MoveCursor(Left/Right/Up/Dow) command with input 0 not move. -- Switch to futures-core to reduce dependencies. -- Command API restricts to only accept `std::io::Write` -- Make `supports_ansi` public -- Implement ALT + numbers windows systems. - -# Version 0.17.7 -- Fix cursor position retrieval bug linux. - -# Version 0.17.6 -- Add functionality to retrieve color based on passed ansi code. -- Switch from 'futures' to 'futures-util' crate to reduce dependency count -- Mio 0.7 update -- signal-hook update -- Make windows raw_mode act on CONIN$ -- Added From<(u8, u8, u8)> Trait to Color::Rgb Enum -- Implement Color::try_from() -- Implement styler traits for `&'a str` - -# Version 0.17.5 -- Improved support of keymodifier for linux, arrow keys, function keys, home keys etc. -- Add `SetTitle` command to change the terminal title. -- Mio 0.7 update - -# Version 0.17.4 -- Add macros for `Colorize` and `Styler` impls, add an impl for `String` -- Add shift modifier to uppercase char events on unix - -# Version 0.17.3 -- Fix get terminal size mac os, this did not report the correct size. - -# Version 0.17.2 -- Windows unicode support - -# Version 0.17.1 -- Reverted bug in 0.17.0: "Make terminal size function fallback to `STDOUT_FILENO` if `/dev/tty` is missing.". -- Support for querying whether the current instance is a TTY. - -# Version 0.17 -- Impl Display for MoveToColumn, MoveToNextLine, MoveToPreviousLine -- Make unix event reader always use `/dev/tty`. -- Direct write command ansi_codes into formatter instead of double allocation. -- Add NONE flag to KeyModifiers -- Add support for converting chars to StylizedContent -- Make terminal size function fallback to `STDOUT_FILENO` if `/dev/tty` is missing. - -# Version 0.16.0 -- Change attribute vector in `ContentStyle` to bitmask. -- Add `SetAttributes` command. -- Add `Attributes` type, which is a bitfield of enabled attributes. -- Remove `exit()`, was useless. - -# Version 0.15.0 -- Fix CTRL + J key combination. This used to return an ENTER event. -- Add a generic implementation `Command` for `&T: Command`. This allows commands to be queued by reference, as well as by value. -- Remove unnecessary `Clone` trait bounds from `StyledContent`. -- Add `StyledContent::style_mut`. -- Handle error correctly for `execute!` and `queue!`. -- Fix minor syntax bug in `execute!` and `queue!`. -- Change `ContentStyle::apply` to take self by value instead of reference, to prevent an unnecessary extra clone. -- Added basic trait implementations (`Debug`, `Clone`, `Copy`, etc) to all of the command structs -- `ResetColor` uses `&'static str` instead of `String` - -# Version 0.14.2 -- Fix TIOCGWINSZ for FreeBSD - -# Version 0.14.1 -- Made windows cursor position relative to the window instead absolute to the screen buffer windows. -- Fix windows bug with `queue` macro were it consumed a type and required an type to be `Copy`. - -# Version 0.14 - -- Replace the `input` module with brand new `event` module - - Terminal Resize Events - - Advanced modifier (SHIFT | ALT | CTRL) support for both mouse and key events and - - futures Stream (feature 'event-stream') - - Poll/read API - - It's **highly recommended** to read the - [Upgrade from 0.13 to 0.14](https://github.com/crossterm-rs/crossterm/wiki/Upgrade-from-0.13-to-0.14) - documentation -- Replace `docs/UPGRADE.md` with the [Upgrade Paths](https://github.com/crossterm-rs/crossterm/wiki#upgrade-paths) - documentation -- Add `MoveToColumn`, `MoveToPreviousLine`, `MoveToNextLine` commands -- Merge `screen` module into `terminal` - - Remove `screen::AlternateScreen` - - Remove `screen::Rawscreen` - * Move and rename `Rawscreen::into_raw_mode` and `Rawscreen::disable_raw_mode` to `terminal::enable_raw_mode` and `terminal::disable_raw_mode` - - Move `screen::EnterAlternateScreen` and `screen::LeaveAlternateScreen` to `terminal::EnterAlternateScreen` and `terminal::LeaveAlternateScreen` - - Replace `utils::Output` command with `style::Print` command -- Fix enable/disable mouse capture commands on Windows -- Allow trailing comma `queue!` & `execute!` macros - -# Version 0.13.3 - -- Remove thread from AsyncReader on Windows. -- Improve HANDLE management windows. - -# Version 0.13.2 - -- New `input::stop_reading_thread()` function - - Temporary workaround for the UNIX platform to stop the background - reading thread and close the file descriptor - - This function will be removed in the next version - -# Version 0.13.1 - -- Async Reader fix, join background thread and avoid looping forever on windows. - -# Version 0.13.0 - -**Major API-change, removed old-api** - -- Remove `Crossterm` type -- Remove `TerminalCursor`, `TerminalColor`, `Terminal` -- Remove `cursor()`, `color()` , `terminal()` -- Remove re-exports at root, accessible via `module::types` (`cursor::MoveTo`) -- `input` module - - Derive 'Copy' for 'KeyEvent' - - Add the `EnableMouseCapture` and `EnableMouseCapture` commands -- `cursor` module - - Introduce static function `crossterm::cursor::position` in place of `TerminalCursor::pos` - - Rename `Goto` to `MoveTo` - - Rename `Up` to `MoveLeft` - - Rename `Right` to `MoveRight` - - Rename `Down` to `MoveDown` - - Rename `BlinkOn` to `EnableBlinking` - - Rename `BlinkOff` to `DisableBlinking` - - Rename `ResetPos` to `ResetPosition` - - Rename `SavePos` to `SavePosition` -- `terminal` - - Introduce static function `crossterm::terminal::size` in place of `Terminal::size` - - Introduce static function `crossterm::terminal::exit` in place of `Terminal::exit` -- `style module` - - Rename `ObjectStyle` to `ContentStyle`. Now full names are used for methods - - Rename `StyledObject` to `StyledContent` and made members private - - Rename `PrintStyledFont` to `PrintStyledContent` - - Rename `attr` method to `attribute`. - - Rename `Attribute::NoInverse` to `NoReverse` - - Update documentation - - Made `Colored` private, user should use commands instead - - Rename `SetFg` -> `SetForegroundColor` - - Rename `SetBg` -> `SetBackgroundColor` - - Rename `SetAttr` -> `SetAttribute` - - Rename `ContentStyle::fg_color` -> `ContentStyle::foreground_color` - - Rename `ContentStyle::bg_color` -> `ContentStyle::background_color` - - Rename `ContentStyle::attrs` -> `ContentStyle::attributes` -- Improve documentation -- Unix terminal size calculation with TPUT - -# Version 0.12.1 - -- Move all the `crossterm_` crates code was moved to the `crossterm` crate - - `crossterm_cursor` is in the `cursor` module, etc. - - All these modules are public -- No public API breaking changes - -# Version 0.12.0 - -- Following crates are deprecated and no longer maintained - - `crossterm_cursor` - - `crossterm_input` - - `crossterm_screen` - - `crossterm_style` - - `crossterm_terminal` - - `crossterm_utils` - -## `crossterm_cursor` 0.4.0 - -- Fix examples link ([PR #6](https://github.com/crossterm-rs/crossterm-cursor/pull/6)) -- Sync documentation style ([PR #7](https://github.com/crossterm-rs/crossterm-cursor/pull/7)) -- Remove all references to the crossterm book ([PR #8](https://github.com/crossterm-rs/crossterm-cursor/pull/8)) -- Replace `RAW_MODE_ENABLED` with `is_raw_mode_enabled` ([PR #9](https://github.com/crossterm-rs/crossterm-cursor/pull/9)) -- Use `SyncReader` & `InputEvent::CursorPosition` for `pos_raw()` ([PR #10](https://github.com/crossterm-rs/crossterm-cursor/pull/10)) - -## `crossterm_input` 0.5.0 - -- Sync documentation style ([PR #4](https://github.com/crossterm-rs/crossterm-input/pull/4)) -- Sync `SyncReader::next()` Windows and UNIX behavior ([PR #5](https://github.com/crossterm-rs/crossterm-input/pull/5)) -- Remove all references to the crossterm book ([PR #6](https://github.com/crossterm-rs/crossterm-input/pull/6)) -- Mouse coordinates synchronized with the cursor ([PR #7](https://github.com/crossterm-rs/crossterm-input/pull/7)) - - Upper/left reported as `(0, 0)` -- Fix bug that read sync didn't block (Windows) ([PR #8](https://github.com/crossterm-rs/crossterm-input/pull/8)) -- Refactor UNIX readers ([PR #9](https://github.com/crossterm-rs/crossterm-input/pull/9)) - - AsyncReader produces mouse events - - One reading thread per application, not per `AsyncReader` - - Cursor position no longer consumed by another `AsyncReader` - - Implement sync reader for read_char (requires raw mode) - - Fix `SIGTTIN` when executed under the LLDB - - Add mio for reading from FD and more efficient polling (UNIX only) -- Sync UNIX and Windows vertical mouse position ([PR #11](https://github.com/crossterm-rs/crossterm-input/pull/11)) - - Top is always reported as `0` - -## `crossterm_screen` 0.3.2 - -- `to_alternate` switch back to main screen if it fails to switch into raw mode ([PR #4](https://github.com/crossterm-rs/crossterm-screen/pull/4)) -- Improve the documentation ([PR #5](https://github.com/crossterm-rs/crossterm-screen/pull/5)) - - Public API - - Include the book content in the documentation -- Remove all references to the crossterm book ([PR #6](https://github.com/crossterm-rs/crossterm-screen/pull/6)) -- New commands introduced ([PR #7](https://github.com/crossterm-rs/crossterm-screen/pull/7)) - - `EnterAlternateScreen` - - `LeaveAlternateScreen` -- Sync Windows and UNIX raw mode behavior ([PR #8](https://github.com/crossterm-rs/crossterm-screen/pull/8)) - -## `crossterm_style` 0.5.2 - -- Refactor ([PR #2](https://github.com/crossterm-rs/crossterm-style/pull/2)) - - Added unit tests - - Improved documentation and added book page to `lib.rs` - - Fixed bug with `SetBg` command, WinApi logic - - Fixed bug with `StyledObject`, used stdout for resetting terminal color - - Introduced `ResetColor` command -- Sync documentation style ([PR #3](https://github.com/crossterm-rs/crossterm-style/pull/3)) -- Remove all references to the crossterm book ([PR #4](https://github.com/crossterm-rs/crossterm-style/pull/4)) -- Windows 7 grey/white foreground/intensity swapped ([PR #5](https://github.com/crossterm-rs/crossterm-style/pull/5)) - -## `crossterm_terminal` 0.3.2 - -- Removed `crossterm_cursor::sys` dependency ([PR #2](https://github.com/crossterm-rs/crossterm-terminal/pull/2)) -- Internal refactoring & documentation ([PR #3](https://github.com/crossterm-rs/crossterm-terminal/pull/3)) -- Removed all references to the crossterm book ([PR #4](https://github.com/crossterm-rs/crossterm-terminal/pull/4)) - -## `crossterm_utils` 0.4.0 - -- Add deprecation note ([PR #3](https://github.com/crossterm-rs/crossterm-utils/pull/3)) -- Remove all references to the crossterm book ([PR #4](https://github.com/crossterm-rs/crossterm-utils/pull/4)) -- Remove unsafe static mut ([PR #5](https://github.com/crossterm-rs/crossterm-utils/pull/5)) - - `sys::unix::RAW_MODE_ENABLED` replaced with `sys::unix::is_raw_mode_enabled()` (breaking) - - New `lazy_static` dependency - -## `crossterm_winapi` 0.3.0 - -- Make read sync block for windows systems ([PR #2](https://github.com/crossterm-rs/crossterm-winapi/pull/2)) - -# Version 0.11.1 - -- Maintenance release -- All sub-crates were moved to their own repositories in the `crossterm-rs` organization - -# Version 0.11.0 - -As a preparation for crossterm 0.1.0 we have moved crossterm to an organisation called 'crossterm-rs'. - -### Code Quality - -- Code Cleanup: [warning-cleanup], [crossterm_style-cleanup], [crossterm_screen-cleanup], [crossterm_terminal-cleanup], [crossterm_utils-cleanup], [2018-cleanup], [api-cleanup-1], [api-cleanup-2], [api-cleanup-3] -- Examples: [example-cleanup_1], [example-cleanup_2], [example-fix], [commandbar-fix], [snake-game-improved] -- Fixed all broken tests and added tests - -### Important Changes - -- Return written bytes: [return-written-bytes] -- Added derives: `Debug` for `ObjectStyle` [debug-derive], Serialize/Deserialize for key events [serde] -- Improved error handling: - - Return `crossterm::Result` from all api's: [return_crossterm_result] - * `TerminalCursor::pos()` returns `Result<(u16, u16)>` - * `Terminal::size()` returns `Result<(u16, u16)>` - * `TerminalCursor::move_*` returns `crossterm::Result` - * `ExecutableCommand::queue` returns `crossterm::Result` - * `QueueableCommand::queue` returns `crossterm::Result` - * `get_available_color_count` returns no result - * `RawScreen::into_raw_mode` returns `crossterm::Result` instead of `io::Result` - * `RawScreen::disable_raw_mode` returns `crossterm::Result` instead of `io::Result` - * `AlternateScreen::to_alternate` returns `crossterm::Result` instead of `io::Result` - * `TerminalInput::read_line` returns `crossterm::Result` instead of `io::Result` - * `TerminalInput::read_char` returns `crossterm::Result` instead of `io::Result` - * Maybe I forgot something, a lot of functions have changed - - Removed all unwraps/expects from library -- Add KeyEvent::Enter and KeyEvent::Tab: [added-key-event-enter], [added-key-event-tab] -- Sync set/get terminal size behaviour: [fixed-get-set-terminal-size] -- Method renames: - * `AsyncReader::stop_reading()` to `stop()` - * `RawScreen::disable_raw_mode_on_drop` to `keep_raw_mode_on_drop` - * `TerminalCursor::reset_position()` to `restore_position()` - * `Command::get_anis_code()` to `ansi_code()` - * `available_color_count` to `available_color_count()` - * `Terminal::terminal_size` to `Terminal::size` - * `Console::get_handle` to `Console::handle` -- All `i16` values for indexing: set size, set cursor pos, scrolling synced to `u16` values -- Command API takes mutable self instead of self - -[serde]: https://github.com/crossterm-rs/crossterm/pull/190 - -[debug-derive]: https://github.com/crossterm-rs/crossterm/pull/192 -[example-fix]: https://github.com/crossterm-rs/crossterm/pull/193 -[commandbar-fix]: https://github.com/crossterm-rs/crossterm/pull/204 - -[warning-cleanup]: https://github.com/crossterm-rs/crossterm/pull/198 -[example-cleanup_1]: https://github.com/crossterm-rs/crossterm/pull/196 -[example-cleanup_2]: https://github.com/crossterm-rs/crossterm/pull/225 -[snake-game-improved]: https://github.com/crossterm-rs/crossterm/pull/231 -[crossterm_style-cleanup]: https://github.com/crossterm-rs/crossterm/pull/208 -[crossterm_screen-cleanup]: https://github.com/crossterm-rs/crossterm/pull/209 -[crossterm_terminal-cleanup]: https://github.com/crossterm-rs/crossterm/pull/210 -[crossterm_utils-cleanup]: https://github.com/crossterm-rs/crossterm/pull/211 -[2018-cleanup]: https://github.com/crossterm-rs/crossterm/pull/222 -[wild-card-cleanup]: https://github.com/crossterm-rs/crossterm/pull/224 - -[api-cleanup-1]: https://github.com/crossterm-rs/crossterm/pull/235 -[api-cleanup-2]: https://github.com/crossterm-rs/crossterm/pull/238 -[api-cleanup-3]: https://github.com/crossterm-rs/crossterm/pull/240 - -[return-written-bytes]: https://github.com/crossterm-rs/crossterm/pull/212 - -[return_crossterm_result]: https://github.com/crossterm-rs/crossterm/pull/232 -[added-key-event-tab]: https://github.com/crossterm-rs/crossterm/pull/239 -[added-key-event-enter]: https://github.com/crossterm-rs/crossterm/pull/236 -[fixed-get-set-terminal-size]: https://github.com/crossterm-rs/crossterm/pull/242 - -# Version 0.10.1 - -# Version 0.10.0 ~ yanked -- Implement command API, to have better performance and more control over how and when commands are executed. [PR](https://github.com/crossterm-rs/crossterm/commit/1a60924abd462ab169b6706aab68f4cca31d7bc2), [issue](https://github.com/crossterm-rs/crossterm/issues/171) -- Fix showing, hiding cursor windows implementation -- Remove some of the parsing logic from windows keys to ansi codes to key events [PR](https://github.com/crossterm-rs/crossterm/commit/762c3a9b8e3d1fba87acde237f8ed09e74cd9ecd) -- Made terminal size 1-based [PR](https://github.com/crossterm-rs/crossterm/commit/d689d7e8ed46a335474b8262bd76f21feaaf0c50) -- Add some derives - -# Version 0.9.6 - -- Copy for KeyEvent -- CTRL + Left, Down, Up, Right key support -- SHIFT + Left, Down, Up, Right key support -- Fixed UNIX cursor position bug [issue](https://github.com/crossterm-rs/crossterm/issues/140), [PR](https://github.com/crossterm-rs/crossterm/pull/152) - -# Version 0.9.5 - -- Prefetch buffer size for more efficient windows input reads. [PR](https://github.com/crossterm-rs/crossterm/pull/144) - -# Version 0.9.4 - -- Reset foreground and background color individually. [PR](https://github.com/crossterm-rs/crossterm/pull/138) -- Backtap input support. [PR](https://github.com/crossterm-rs/crossterm/pull/129) -- Corrected white/grey and added dark grey. -- Fixed getting cursor position with raw screen enabled. [PR](https://github.com/crossterm-rs/crossterm/pull/134) -- Removed one redundant stdout lock - -# Version 0.9.3 - -- Removed println from `SyncReader` - -## Version 0.9.2 - -- Terminal size linux was not 0-based -- Windows mouse input event position was 0-based and should be 1-based -- Result, ErrorKind are made re-exported -- Fixed some special key combination detections for UNIX systems -- Made FreeBSD compile - -## Version 0.9.1 - -- Fixed libc compile error - -## Version 0.9.0 (yanked) - -This release is all about moving to a stabilized API for 1.0. - -- Major refactor and cleanup. -- Improved performance; - - No locking when writing to stdout. - - UNIX doesn't have any dynamic dispatch anymore. - - Windows has improved the way to check if ANSI modes are enabled. - - Removed lot's of complex API calls: `from_screen`, `from_output` - - Removed `Arc` from all internal Api's. -- Removed termios dependency for UNIX systems. -- Upgraded deps. -- Removed about 1000 lines of code - - `TerminalOutput` - - `Screen` - - unsafe code - - Some duplicated code introduced by a previous refactor. -- Raw modes UNIX systems improved -- Added `NoItalic` attribute - -## Version 0.8.2 - -- Bug fix for sync reader UNIX. - -## Version 0.8.1 - -- Added public re-exports for input. - -# Version 0.8.0 - -- Introduced KeyEvents -- Introduced MouseEvents -- Upgraded crossterm_winapi 0.2 - -# Version 0.7.0 - -- Introduced more `Attributes` -- Introduced easier ways to style text [issue 87](https://github.com/crossterm-rs/crossterm/issues/87). -- Removed `ColorType` since it was unnecessary. - -# Version 0.6.0 - -- Introduced feature flags; input, cursor, style, terminal, screen. -- All modules are moved to their own crate. -- Introduced crossterm workspace -- Less dependencies. -- Improved namespaces. - -[PR 84](https://github.com/crossterm-rs/crossterm/pull/84) - -# Version 0.5.5 - -- Error module is made public [PR 78](https://github.com/crossterm-rs/crossterm/pull/78). - -# Version 0.5.4 - -- WinApi rewrite and correctly error handled [PR 67](https://github.com/crossterm-rs/crossterm/pull/67) -- Windows attribute support [PR 62](https://github.com/crossterm-rs/crossterm/pull/62) -- Readline bug fix windows systems [PR 62](https://github.com/crossterm-rs/crossterm/pull/62) -- Error handling improvement. -- General refactoring, all warnings removed. -- Documentation improvement. - -# Version 0.5.1 - -- Documentation refactor. -- Fixed broken API documentation [PR 53](https://github.com/crossterm-rs/crossterm/pull/53). - -# Version 0.5.0 - -- Added ability to pause the terminal [issue](https://github.com/crossterm-rs/crossterm/issues/39) -- RGB support for Windows 10 systems -- ANSI color value (255) color support -- More convenient API, no need to care about `Screen` unless working with when working with alternate or raw screen [PR](https://github.com/crossterm-rs/crossterm/pull/44) -- Implemented Display for styled object - -# Version 0.4.3 - -- Fixed bug [issue 41](https://github.com/crossterm-rs/crossterm/issues/41) - -# Version 0.4.2 - -- Added functionality to make a styled object writable to screen [issue 33](https://github.com/crossterm-rs/crossterm/issues/33) -- Added unit tests. -- Bugfix with getting terminal size unix. -- Bugfix with returning written bytes [pull request 31](https://github.com/crossterm-rs/crossterm/pull/31) -- removed methods calls: `as_any()` and `as_any_mut()` from `TerminalOutput` - -# Version 0.4.1 - -- Fixed resizing of ansi terminal with and height where in the wrong order. - -# Version 0.4.0 - -- Input support (read_line, read_char, read_async, read_until_async) -- Styling module improved -- Everything is multithreaded (`Send`, `Sync`) -- Performance enhancements: removed mutexes, removed state manager, removed context type removed unnecessarily RC types. -- Bug fix resetting console color. -- Bug fix whit undoing raw modes. -- More correct error handling. -- Overall command improvement. -- Overall refactor of code. - -# Version 0.3.0 - -This version has some braking changes check [upgrade manual](UPGRADE%20Manual.md) for more information about what is changed. -I think you should not switch to version `0.3.0` if you aren't going to use the AlternateScreen feature. -Because you will have some work to get to the new version of crossterm depending on your situation. - -Some Features crossterm 0.3.0 -- Alternate Screen for windows and unix systems. -- Raw screen for unix and windows systems [Issue 5](https://github.com/crossterm-rs/crossterm/issues/5).. -- Hiding an showing the cursor. -- Control over blinking of the terminal cursor (only some terminals are supporting this). -- The terminal state will be set to its original state when process ends [issue7](https://github.com/crossterm-rs/crossterm/issues/7). -- exit the current process. - -## Alternate screen - -This create supports alternate screen for both windows and unix systems. You can use - -*Nix style applications often utilize an alternate screen buffer, so that they can modify the entire contents of the buffer, without affecting the application that started them. -The alternate buffer is exactly the dimensions of the window, without any scrollback region. -For an example of this behavior, consider when vim is launched from bash. -Vim uses the entirety of the screen to edit the file, then returning to bash leaves the original buffer unchanged. - -I Highly recommend you to check the `examples/program_examples/first_depth_search` for seeing this in action. - -## Raw screen - -This crate now supports raw screen for both windows and unix systems. -What exactly is raw state: -- No line buffering. - Normally the terminals uses line buffering. This means that the input will be send to the terminal line by line. - With raw mode the input will be send one byte at a time. -- Input - All input has to be written manually by the programmer. -- Characters - The characters are not processed by the terminal driver, but are sent straight through. - Special character have no meaning, like backspace will not be interpret as backspace but instead will be directly send to the terminal. -With these modes you can easier design the terminal screen. - -## Some functionalities added - -- Hiding and showing terminal cursor -- Enable or disabling blinking of the cursor for unix systems (this is not widely supported) -- Restoring the terminal to original modes. -- Added a [wrapper](https://github.com/crossterm-rs/crossterm/blob/master/src/shared/crossterm.rs) for managing all the functionalities of crossterm `Crossterm`. -- Exit the current running process - -## Examples -Added [examples](https://github.com/crossterm-rs/crossterm/tree/master/examples) for each version of the crossterm version. -Also added a folder with some [real life examples](https://github.com/crossterm-rs/crossterm/tree/master/examples/program_examples). - -## Context - -What is the `Context` all about? This `Context` has several reasons why it is introduced into `crossterm version 0.3.0`. -These points are related to the features like `Alternatescreen` and managing the terminal state. - -- At first `Terminal state`: - - Because this is a terminal manipulating library there will be made changes to terminal when running an process. - If you stop the process you want the terminal back in its original state. - Therefore, I need to track the changes made to the terminal. - -- At second `Handle to the console` - - In Rust we can use `stdout()` to get an handle to the current default console handle. - For example when in unix systems you want to print something to the main screen you can use the following code: - - write!(std::io::stdout(), "{}", "some text"). - - But things change when we are in alternate screen modes. - We can not simply use `stdout()` to get a handle to the alternate screen, since this call returns the current default console handle (handle to mainscreen). - - Because of that we need to store an handle to the current screen. - This handle could be used to put into alternate screen modes and back into main screen modes. - Through this stored handle Crossterm can execute its command and write on and to the current screen whether it be alternate screen or main screen. - - For unix systems we store the handle gotten from `stdout()` for windows systems that are not supporting ANSI escape codes we store WinApi `HANDLE` struct witch will provide access to the current screen. - -So to recap this `Context` struct is a wrapper for a type that manges terminal state changes. -When this `Context` goes out of scope all changes made will be undone. -Also is this `Context` is a wrapper for access to the current console screen. - -Because Crossterm needs access to the above to types quite often I have chosen to add those two in one struct called `Context` so that this type could be shared throughout library. -Check this link for more info: [cleanup of rust code](https://stackoverflow.com/questions/48732387/how-can-i-run-clean-up-code-in-a-rust-library). -More info over writing to alternate screen buffer on windows and unix see this [link](https://github.com/crossterm-rs/crossterm/issues/17) - -__Now the user has to pass an context type to the modules of Crossterm like this:__ - - let context = Context::new(); - - let cursor = cursor(&context); - let terminal = terminal(&context); - let color = color(&context); - -Because this looks a little odd I will provide a type widths will manage the `Context` for you. You can call the different modules like the following: - - let crossterm = Crossterm::new(); - let color = crossterm.color(); - let cursor = crossterm.cursor(); - let terminal = crossterm.terminal(); - - -### Alternate screen -When you want to switch to alternate screen there are a couple of things to keep in mind for it to work correctly. -First off some code of how to switch to Alternate screen, for more info check the [alternate screen example](https://github.com/crossterm-rs/crossterm/blob/master/examples/alternate_screen.rs). - -_Create alternate screen from `Context`_ - - // create context. - let context = crossterm::Context::new(); - // create instance of Alternatescreen by the given context, this will also switch to it. - let mut screen = crossterm::AlternateScreen::from(context.clone()); - // write to the alternate screen. - write!(screen, "test"); - -_Create alternate screen from `Crossterm`:_ - - // create context. - let crossterm = ::crossterm::Crossterm::new(); - // create instance of Alternatescreen by the given reference to crossterm, this will also switch to it. - let mut screen = crossterm::AlternateScreen::from(&crossterm); - // write to the alternate screen. - write!(screen, "test"); - -like demonstrated above, to get the functionalities of `cursor(), color(), terminal()` also working on alternate screen. -You need to pass it the same `Context` as you have passed to the previous three called functions, -If you don't use the same `Context` in `cursor(), color(), terminal()` than these modules will be using the main screen and you will not see anything at the alternate screen. If you use the [Crossterm](https://github.com/crossterm-rs/crossterm/blob/master/src/shared/crossterm.rs) type you can get the `Context` from it by calling the crossterm.get_context() whereafter you can create the AlternateScreen from it. - -# Version 0.2.2 - -- Bug see [issue 15](https://github.com/crossterm-rs/crossterm/issues/15) - -# Version 0.2.1 - -- Default ANSI escape codes for windows machines, if windows does not support ANSI switch back to WinApi. -- method grammar mistake fixed [Issue 3](https://github.com/crossterm-rs/crossterm/issues/3) -- Some Refactorings in method names see [issue 4](https://github.com/crossterm-rs/crossterm/issues/4) -- Removed bin reference from crate [Issue 6](https://github.com/crossterm-rs/crossterm/issues/6) -- Get position unix fixed [issue 8](https://github.com/crossterm-rs/crossterm/issues/8) - -# Version 0.2 - -- 256 color support. -- Text Attributes like: bold, italic, underscore and crossed word etc. -- Custom ANSI color code input to set fore- and background color for unix. -- Storing the current cursor position and resetting to that stored cursor position later. -- Resizing the terminal. diff --git a/crates/util/keyfork-crossterm/Cargo.toml b/crates/util/keyfork-crossterm/Cargo.toml deleted file mode 100644 index 6dd170e..0000000 --- a/crates/util/keyfork-crossterm/Cargo.toml +++ /dev/null @@ -1,100 +0,0 @@ -[package] -name = "keyfork-crossterm" -version = "0.27.2" -# authors = ["T. Post"] -authors = ["Ryan Heywood "] -description = "A crossplatform terminal library for manipulating terminals." -repository = "https://git.distrust.co/public/keyfork" -# documentation = "https://docs.rs/crossterm/" -license = "MIT" -# keywords = ["event", "color", "cli", "input", "terminal"] -# exclude = ["target", "Cargo.lock"] -readme = "README.md" -edition = "2021" -rust-version = "1.58.0" -# categories = ["command-line-interface", "command-line-utilities"] - -[lints] -workspace = true - -# [lib] -# name = "crossterm" -# path = "src/lib.rs" - -# Build documentation with all features -> EventStream is available -[package.metadata.docs.rs] -all-features = true - -# Features -[features] -default = ["bracketed-paste", "windows", "events"] -windows = ["dep:winapi", "dep:crossterm_winapi"] # Disables winapi dependencies from being included into the binary (SHOULD NOT be disabled on windows). -bracketed-paste = [] # Enables triggering a `Event::Paste` when pasting text into the terminal. -event-stream = ["dep:futures-core", "events"] # Enables async events -use-dev-tty = ["filedescriptor"] # Enables raw file descriptor polling / selecting instead of mio. -events = ["dep:mio", "dep:signal-hook", "dep:signal-hook-mio"] # Enables reading input/events from the system. -serde = ["dep:serde", "bitflags/serde"] # Enables 'serde' for various types. - -# Shared dependencies -[dependencies] -bitflags = {version = "2.3" } -parking_lot = "0.12" - -# optional deps only added when requested -futures-core = { version = "0.3", optional = true, default-features = false } -serde = { version = "1.0", features = ["derive"], optional = true } - -# Windows dependencies -[target.'cfg(windows)'.dependencies.winapi] -version = "0.3.9" -features = ["winuser", "winerror"] -optional = true - -[target.'cfg(windows)'.dependencies] -crossterm_winapi = { version = "0.9.1", optional = true } - -# UNIX dependencies -[target.'cfg(unix)'.dependencies] -libc = "0.2" -signal-hook = { version = "0.3.17", optional = true } -filedescriptor = { version = "0.8", optional = true } -mio = { version = "1.0", features = ["os-poll"], optional = true } -signal-hook-mio = { version = "0.2.3", features = ["support-v1_0"], optional = true } - -# Dev dependencies (examples, ...) -[dev-dependencies] -tokio = { workspace = true, features = ["full"] } -futures = "0.3" -futures-timer = "3.0" -async-std = "1.12" -serde_json = { workspace = true } -serial_test = "2.0.0" - -# Examples -[[example]] -name = "event-read" -required-features = ["bracketed-paste", "events"] - -[[example]] -name = "event-match-modifiers" -required-features = ["bracketed-paste", "events"] - -[[example]] -name = "event-poll-read" -required-features = ["bracketed-paste", "events"] - -[[example]] -name = "event-stream-async-std" -required-features = ["event-stream", "events"] - -[[example]] -name = "event-stream-tokio" -required-features = ["event-stream", "events"] - -[[example]] -name = "event-read-char-line" -required-features = ["events"] - -[[example]] -name = "stderr" -required-features = ["events"] diff --git a/crates/util/keyfork-crossterm/LICENSE b/crates/util/keyfork-crossterm/LICENSE deleted file mode 100644 index 8b02a7f..0000000 --- a/crates/util/keyfork-crossterm/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2019 Timon - -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/crates/util/keyfork-crossterm/README.md b/crates/util/keyfork-crossterm/README.md deleted file mode 100644 index 80e69e5..0000000 --- a/crates/util/keyfork-crossterm/README.md +++ /dev/null @@ -1 +0,0 @@ -Forked from https://github.com/crossterm-rs/crossterm diff --git a/crates/util/keyfork-crossterm/README.md.old b/crates/util/keyfork-crossterm/README.md.old deleted file mode 100644 index b9f83f7..0000000 --- a/crates/util/keyfork-crossterm/README.md.old +++ /dev/null @@ -1,213 +0,0 @@ -

- -[![Donate](https://img.shields.io/badge/Donate-PayPal-green.svg)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=Z8QK6XU749JB2) ![Travis][s7] [![Latest Version][s1]][l1] [![MIT][s2]][l2] [![docs][s3]][l3] ![Lines of Code][s6] [![Join us on Discord][s5]][l5] - -# Cross-platform Terminal Manipulation Library - -Crossterm is a pure-rust, terminal manipulation library that makes it possible to write cross-platform text-based interfaces (see [features](#features)). It supports all UNIX and Windows terminals down to Windows 7 (not all terminals are tested, -see [Tested Terminals](#tested-terminals) for more info). - -## Table of Contents - -- [Cross-platform Terminal Manipulation Library](#cross-platform-terminal-manipulation-library) - - [Table of Contents](#table-of-contents) - - [Features](#features) - - [Tested Terminals](#tested-terminals) - - [Getting Started](#getting-started) - - [Feature Flags](#feature-flags) - - [Dependency Justification](#dependency-justification) - - [Other Resources](#other-resources) - - [Used By](#used-by) - - [Contributing](#contributing) - - [Authors](#authors) - - [License](#license) - -## Features - -- Cross-platform -- Multi-threaded (send, sync) -- Detailed documentation -- Few dependencies -- Full control over writing and flushing output buffer -- Is tty -- Cursor - - Move the cursor N times (up, down, left, right) - - Move to previous / next line - - Move to column - - Set/get the cursor position - - Store the cursor position and restore to it later - - Hide/show the cursor - - Enable/disable cursor blinking (not all terminals do support this feature) -- Styled output - - Foreground color (16 base colors) - - Background color (16 base colors) - - 256 (ANSI) color support (Windows 10 and UNIX only) - - RGB color support (Windows 10 and UNIX only) - - Text attributes like bold, italic, underscore, crossed, etc -- Terminal - - Clear (all lines, current line, from cursor down and up, until new line) - - Scroll up, down - - Set/get the terminal size - - Exit current process - - Alternate screen - - Raw screen - - Set terminal title - - Enable/disable line wrapping -- Event - - Input Events - - Mouse Events (press, release, position, button, drag) - - Terminal Resize Events - - Advanced modifier (SHIFT | ALT | CTRL) support for both mouse and key events and - - futures Stream (feature 'event-stream') - - Poll/read API - - - -### Tested Terminals - -- Console Host - - Windows 10 (Pro) - - Windows 8.1 (N) -- Ubuntu Desktop Terminal - - Ubuntu 17.10 - - Pop!_OS ( Ubuntu ) 20.04 -- (Arch, Manjaro) KDE Konsole -- (Arch, NixOS) Kitty -- Linux Mint -- (OpenSuse) Alacritty -- (Chrome OS) Crostini -- Apple - - macOS Monterey 12.7.1 (Intel-Chip) - -This crate supports all UNIX terminals and Windows terminals down to Windows 7; however, not all of the -terminals have been tested. If you have used this library for a terminal other than the above list without -issues, then feel free to add it to the above list - I really would appreciate it! - -## Getting Started -_see the [examples directory](examples/) and [documentation](https://docs.rs/crossterm/) for more advanced examples._ - -
- -Click to show Cargo.toml. - - -```toml -[dependencies] -crossterm = "0.27" -``` - -
-

- -```rust -use std::io::{stdout, Write}; - -use crossterm::{ - execute, - style::{Color, Print, ResetColor, SetBackgroundColor, SetForegroundColor}, - ExecutableCommand, Result, - event, -}; - -fn main() -> std::io::Result<()> { - // using the macro - execute!( - stdout(), - SetForegroundColor(Color::Blue), - SetBackgroundColor(Color::Red), - Print("Styled text here."), - ResetColor - )?; - - // or using functions - stdout() - .execute(SetForegroundColor(Color::Blue))? - .execute(SetBackgroundColor(Color::Red))? - .execute(Print("Styled text here."))? - .execute(ResetColor)?; - - Ok(()) -} -``` - -Checkout this [list](https://docs.rs/crossterm/latest/crossterm/index.html#supported-commands) with all possible commands. - -### Feature Flags - -```toml -[dependencies.crossterm] -version = "0.27" -features = ["event-stream"] -``` - -| Feature | Description | -|:---------------|:---------------------------------------------| -| `event-stream` | `futures::Stream` producing `Result`. | -| `serde` | (De)serializing of events. | -| `events` | Reading input/system events (enabled by default) | -| `filedescriptor` | Use raw filedescriptor for all events rather then mio dependency | - - -To use crossterm as a very thin layer you can disable the `events` feature or use `filedescriptor` feature. -This can disable `mio` / `signal-hook` / `signal-hook-mio` dependencies. - -### Dependency Justification - -| Dependency | Used for | Included | -|:---------------|:---------------------------------------------------------------------------------|:--------------------------------------| -| `bitflags` | `KeyModifiers`, those are differ based on input. | always | -| `parking_lot` | locking `RwLock`s with a timeout, const mutexes. | always | -| `libc` | UNIX terminal_size/raw modes/set_title and several other low level functionality. | optional (`events` feature), UNIX only | -| `Mio` | event readiness polling, waking up poller | optional (`events` feature), UNIX only | -| `signal-hook` | signal-hook is used to handle terminal resize SIGNAL with Mio. | optional (`events` feature),UNIX only | -| `winapi` | Used for low-level windows system calls which ANSI codes can't replace | windows only | -| `futures-core` | For async stream of events | only with `event-stream` feature flag | -| `serde` | ***ser***ializing and ***de***serializing of events | only with `serde` feature flag | - -### Other Resources - -- [API documentation](https://docs.rs/crossterm/) -- [Deprecated examples repository](https://github.com/crossterm-rs/examples) - -## Used By - -- [Broot](https://dystroy.org/broot/) -- [Cursive](https://github.com/gyscos/Cursive) -- [TUI](https://github.com/fdehau/tui-rs) -- [Rust-sloth](https://github.com/ecumene/rust-sloth) -- [Rusty-rain](https://github.com/cowboy8625/rusty-rain) - -## Contributing - -We highly appreciate when anyone contributes to this crate. Before you do, please, -read the [Contributing](docs/CONTRIBUTING.md) guidelines. - -## Authors - -* **Timon Post** - *Project Owner & creator* - -## License - -This project, `crossterm` and all its sub-crates: `crossterm_screen`, `crossterm_cursor`, `crossterm_style`, -`crossterm_input`, `crossterm_terminal`, `crossterm_winapi`, `crossterm_utils` are licensed under the MIT -License - see the [LICENSE](https://github.com/crossterm-rs/crossterm/blob/master/LICENSE) file for details. - -[s1]: https://img.shields.io/crates/v/crossterm.svg -[l1]: https://crates.io/crates/crossterm - -[s2]: https://img.shields.io/badge/license-MIT-blue.svg -[l2]: ./LICENSE - -[s3]: https://docs.rs/crossterm/badge.svg -[l3]: https://docs.rs/crossterm/ - -[s3]: https://docs.rs/crossterm/badge.svg -[l3]: https://docs.rs/crossterm/ - -[s5]: https://img.shields.io/discord/560857607196377088.svg?logo=discord -[l5]: https://discord.gg/K4nyTDB - -[s6]: https://tokei.rs/b1/github/crossterm-rs/crossterm?category=code -[s7]: https://travis-ci.org/crossterm-rs/crossterm.svg?branch=master diff --git a/crates/util/keyfork-crossterm/docs/.gitignore b/crates/util/keyfork-crossterm/docs/.gitignore deleted file mode 100644 index 7585238..0000000 --- a/crates/util/keyfork-crossterm/docs/.gitignore +++ /dev/null @@ -1 +0,0 @@ -book diff --git a/crates/util/keyfork-crossterm/docs/CONTRIBUTING.md b/crates/util/keyfork-crossterm/docs/CONTRIBUTING.md deleted file mode 100644 index d99d4e2..0000000 --- a/crates/util/keyfork-crossterm/docs/CONTRIBUTING.md +++ /dev/null @@ -1,65 +0,0 @@ -# Contributing - -I would appreciate any contributions to this crate. However, some things are handy to know. - -## Code Style - -### Import Order - -All imports are semantically grouped and ordered. The order is: - -- standard library (`use std::...`) -- external crates (`use rand::...`) -- current crate (`use crate::...`) -- parent module (`use super::..`) -- current module (`use self::...`) -- module declaration (`mod ...`) - -There must be an empty line between groups. An example: - -```rust -use crossterm_utils::{csi, write_cout, Result}; - -use crate::sys::{get_cursor_position, show_cursor}; - -use super::Cursor; -``` - -#### CLion Tips - -The CLion IDE does this for you (_Menu_ -> _Code_ -> _Optimize Imports_). Be aware that the CLion sorts -imports in a group in a different way when compared to the `rustfmt`. It's effectively two steps operation -to get proper grouping & sorting: - -* _Menu_ -> _Code_ -> _Optimize Imports_ - group & semantically order imports -* `cargo fmt` - fix ordering within the group - -Second step can be automated via _CLion_ -> _Preferences_ -> -_Languages & Frameworks_ -> _Rust_ -> _Rustfmt_ -> _Run rustfmt on save_. - -### Max Line Length - -| Type | Max line length | -|:---------------------|----------------:| -| Code | 100 | -| Comments in the code | 120 | -| Documentation | 120 | - -100 is the [`max_width`](https://github.com/rust-lang/rustfmt/blob/master/Configurations.md#max_width) -default value. - -120 is because of the GitHub. The editor & viewer width there is +- 123 characters. - -### Warnings - -The code must be warning free. It's quite hard to find an error if the build logs are polluted with warnings. -If you decide to silent a warning with (`#[allow(...)]`), please add a comment why it's required. - -Always consult the [Travis CI](https://travis-ci.org/crossterm-rs/crossterm/pull_requests) build logs. - -### Forbidden Warnings - -Search for `#![deny(...)]` in the code: - -* `unused_must_use` -* `unused_imports` diff --git a/crates/util/keyfork-crossterm/docs/crossterm_c.png b/crates/util/keyfork-crossterm/docs/crossterm_c.png deleted file mode 100644 index b4a40b1..0000000 Binary files a/crates/util/keyfork-crossterm/docs/crossterm_c.png and /dev/null differ diff --git a/crates/util/keyfork-crossterm/docs/crossterm_full.png b/crates/util/keyfork-crossterm/docs/crossterm_full.png deleted file mode 100644 index 1b1c448..0000000 Binary files a/crates/util/keyfork-crossterm/docs/crossterm_full.png and /dev/null differ diff --git a/crates/util/keyfork-crossterm/docs/crossterm_full.svg b/crates/util/keyfork-crossterm/docs/crossterm_full.svg deleted file mode 100644 index e8f9007..0000000 --- a/crates/util/keyfork-crossterm/docs/crossterm_full.svg +++ /dev/null @@ -1,103 +0,0 @@ - - - - -Created by potrace 1.15, written by Peter Selinger 2001-2017 - - - - - - - - - - - - - diff --git a/crates/util/keyfork-crossterm/docs/know-problems.md b/crates/util/keyfork-crossterm/docs/know-problems.md deleted file mode 100644 index 826aaee..0000000 --- a/crates/util/keyfork-crossterm/docs/know-problems.md +++ /dev/null @@ -1,14 +0,0 @@ -# Known Problems - -There are some problems I discovered during development. -And I don't think it has to do anything with crossterm but it has to do whit how terminals handle ANSI or WinApi. - -## WinAPI - -- Power shell does not interpreter 'DarkYellow' and is instead using gray instead, cmd is working perfectly fine. -- Power shell inserts an '\n' (enter) when the program starts, this enter is the one you pressed when running the command. -- After the program ran, power shell will reset the background and foreground colors. - -## UNIX-terminals - -The Arc and Manjaro KDE Konsole's are not seeming to resize the terminal instead they are resizing the buffer. diff --git a/crates/util/keyfork-crossterm/examples/README.md b/crates/util/keyfork-crossterm/examples/README.md deleted file mode 100644 index c9884ec..0000000 --- a/crates/util/keyfork-crossterm/examples/README.md +++ /dev/null @@ -1,40 +0,0 @@ -![Lines of Code][s7] [![MIT][s2]][l2] [![Join us on Discord][s5]][l5] - -# Crossterm Examples - -The examples are compatible with the latest release. - -## Structure - -``` -├── examples -│   └── interactive-test -│   └── event-* -│   └── stderr -``` -| File Name | Description | Topics | -|:----------------------------|:-------------------------------|:------------------------------------------| -| `examples/interactive-test` | interactive, walk through, demo | cursor, style, event | -| `event-*` | event reading demos | (async) event reading | -| `stderr` | crossterm over stderr demo | raw mode, alternate screen, custom output | -| `is_tty` | Is this instance a tty ? | tty | - -## Run examples - -```bash -$ cargo run --example [file name] -``` - -To run the interactive-demo go into the folder `examples/interactive-demo` and run `cargo run`. - -## License - -This project is licensed under the MIT License - see the [LICENSE.md](LICENSE) file for details. - -[s2]: https://img.shields.io/badge/license-MIT-blue.svg -[l2]: LICENSE - -[s5]: https://img.shields.io/discord/560857607196377088.svg?logo=discord -[l5]: https://discord.gg/K4nyTDB - -[s7]: https://travis-ci.org/crossterm-rs/examples.svg?branch=master diff --git a/crates/util/keyfork-crossterm/examples/event-match-modifiers.rs b/crates/util/keyfork-crossterm/examples/event-match-modifiers.rs deleted file mode 100644 index 5cb54c7..0000000 --- a/crates/util/keyfork-crossterm/examples/event-match-modifiers.rs +++ /dev/null @@ -1,68 +0,0 @@ -//! Demonstrates how to match on modifiers like: Control, alt, shift. -//! -//! cargo run --example event-match-modifiers - -use keyfork_crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers}; - -fn match_event(read_event: Event) { - match read_event { - // Match one one modifier: - Event::Key(KeyEvent { - modifiers: KeyModifiers::CONTROL, - code, - .. - }) => { - println!("Control + {:?}", code); - } - Event::Key(KeyEvent { - modifiers: KeyModifiers::SHIFT, - code, - .. - }) => { - println!("Shift + {:?}", code); - } - Event::Key(KeyEvent { - modifiers: KeyModifiers::ALT, - code, - .. - }) => { - println!("Alt + {:?}", code); - } - - // Match on multiple modifiers: - Event::Key(KeyEvent { - code, modifiers, .. - }) => { - if modifiers == (KeyModifiers::ALT | KeyModifiers::SHIFT) { - println!("Alt + Shift {:?}", code); - } else { - println!("({:?}) with key: {:?}", modifiers, code) - } - } - - _ => {} - } -} - -fn main() { - match_event(Event::Key(KeyEvent::new( - KeyCode::Char('z'), - KeyModifiers::CONTROL, - ))); - match_event(Event::Key(KeyEvent::new( - KeyCode::Left, - KeyModifiers::SHIFT, - ))); - match_event(Event::Key(KeyEvent::new( - KeyCode::Delete, - KeyModifiers::ALT, - ))); - match_event(Event::Key(KeyEvent::new( - KeyCode::Right, - KeyModifiers::ALT | KeyModifiers::SHIFT, - ))); - match_event(Event::Key(KeyEvent::new( - KeyCode::Home, - KeyModifiers::ALT | KeyModifiers::CONTROL, - ))); -} diff --git a/crates/util/keyfork-crossterm/examples/event-poll-read.rs b/crates/util/keyfork-crossterm/examples/event-poll-read.rs deleted file mode 100644 index 33e0fc2..0000000 --- a/crates/util/keyfork-crossterm/examples/event-poll-read.rs +++ /dev/null @@ -1,61 +0,0 @@ -//! Demonstrates how to match on modifiers like: Control, alt, shift. -//! -//! cargo run --example event-poll-read - -use std::{io, time::Duration}; - -use keyfork_crossterm::{ - cursor::position, - event::{poll, read, DisableMouseCapture, EnableMouseCapture, Event, KeyCode}, - execute, - terminal::{disable_raw_mode, enable_raw_mode}, -}; - -const HELP: &str = r#"Blocking poll() & non-blocking read() - - Keyboard, mouse and terminal resize events enabled - - Prints "." every second if there's no event - - Hit "c" to print current cursor position - - Use Esc to quit -"#; - -fn print_events() -> io::Result<()> { - loop { - // Wait up to 1s for another event - if poll(Duration::from_millis(1_000))? { - // It's guaranteed that read() won't block if `poll` returns `Ok(true)` - let event = read()?; - - println!("Event::{:?}\r", event); - - if event == Event::Key(KeyCode::Char('c').into()) { - println!("Cursor position: {:?}\r", position()); - } - - if event == Event::Key(KeyCode::Esc.into()) { - break; - } - } else { - // Timeout expired, no event for 1s - println!(".\r"); - } - } - - Ok(()) -} - -fn main() -> io::Result<()> { - println!("{}", HELP); - - enable_raw_mode()?; - - let mut stdout = io::stdout(); - execute!(stdout, EnableMouseCapture)?; - - if let Err(e) = print_events() { - println!("Error: {:?}\r", e); - } - - execute!(stdout, DisableMouseCapture)?; - - disable_raw_mode() -} diff --git a/crates/util/keyfork-crossterm/examples/event-read-char-line.rs b/crates/util/keyfork-crossterm/examples/event-read-char-line.rs deleted file mode 100644 index 50b1786..0000000 --- a/crates/util/keyfork-crossterm/examples/event-read-char-line.rs +++ /dev/null @@ -1,46 +0,0 @@ -//! Demonstrates how to block read characters or a full line. -//! Just note that crossterm is not required to do this and can be done with `io::stdin()`. -//! -//! cargo run --example event-read-char-line - -use std::io; - -use keyfork_crossterm::event::{self, Event, KeyCode, KeyEvent}; - -/// Read a character from input. -pub fn read_char() -> io::Result { - loop { - if let Event::Key(KeyEvent { - code: KeyCode::Char(c), - .. - }) = event::read()? - { - return Ok(c); - } - } -} - -/// Read a line from input. -pub fn read_line() -> io::Result { - let mut line = String::new(); - while let Event::Key(KeyEvent { code, .. }) = event::read()? { - match code { - KeyCode::Enter => { - break; - } - KeyCode::Char(c) => { - line.push(c); - } - _ => {} - } - } - - Ok(line) -} - -fn main() { - println!("read line:"); - println!("{:?}", read_line()); - println!("read char:"); - println!("{:?}", read_char()); -} diff --git a/crates/util/keyfork-crossterm/examples/event-read.rs b/crates/util/keyfork-crossterm/examples/event-read.rs deleted file mode 100644 index 8048e82..0000000 --- a/crates/util/keyfork-crossterm/examples/event-read.rs +++ /dev/null @@ -1,113 +0,0 @@ -//! Demonstrates how to block read events. -//! -//! cargo run --example event-read - -use std::io; - -use keyfork_crossterm::event::{ - poll, KeyboardEnhancementFlags, PopKeyboardEnhancementFlags, PushKeyboardEnhancementFlags, -}; -use keyfork_crossterm::{ - cursor::position, - event::{ - read, DisableBracketedPaste, DisableFocusChange, DisableMouseCapture, EnableBracketedPaste, - EnableFocusChange, EnableMouseCapture, Event, KeyCode, - }, - execute, queue, - terminal::{disable_raw_mode, enable_raw_mode}, -}; -use std::time::Duration; - -const HELP: &str = r#"Blocking read() - - Keyboard, mouse, focus and terminal resize events enabled - - Hit "c" to print current cursor position - - Use Esc to quit -"#; - -fn print_events() -> io::Result<()> { - loop { - // Blocking read - let event = read()?; - - println!("Event: {:?}\r", event); - - if event == Event::Key(KeyCode::Char('c').into()) { - println!("Cursor position: {:?}\r", position()); - } - - if let Event::Resize(x, y) = event { - let (original_size, new_size) = flush_resize_events((x, y)); - println!("Resize from: {:?}, to: {:?}\r", original_size, new_size); - } - - if event == Event::Key(KeyCode::Esc.into()) { - break; - } - } - - Ok(()) -} - -// Resize events can occur in batches. -// With a simple loop they can be flushed. -// This function will keep the first and last resize event. -fn flush_resize_events(first_resize: (u16, u16)) -> ((u16, u16), (u16, u16)) { - let mut last_resize = first_resize; - while let Ok(true) = poll(Duration::from_millis(50)) { - if let Ok(Event::Resize(x, y)) = read() { - last_resize = (x, y); - } - } - - (first_resize, last_resize) -} - -fn main() -> io::Result<()> { - println!("{}", HELP); - - enable_raw_mode()?; - - let mut stdout = io::stdout(); - - let supports_keyboard_enhancement = matches!( - keyfork_crossterm::terminal::supports_keyboard_enhancement(), - Ok(true) - ); - - if supports_keyboard_enhancement { - queue!( - stdout, - PushKeyboardEnhancementFlags( - KeyboardEnhancementFlags::DISAMBIGUATE_ESCAPE_CODES - | KeyboardEnhancementFlags::REPORT_ALL_KEYS_AS_ESCAPE_CODES - | KeyboardEnhancementFlags::REPORT_ALTERNATE_KEYS - | KeyboardEnhancementFlags::REPORT_EVENT_TYPES - ) - )?; - } - - execute!( - stdout, - EnableBracketedPaste, - EnableFocusChange, - EnableMouseCapture, - )?; - - if let Err(e) = print_events() { - println!("Error: {:?}\r", e); - } - - if supports_keyboard_enhancement { - queue!(stdout, PopKeyboardEnhancementFlags)?; - } - - execute!( - stdout, - DisableBracketedPaste, - PopKeyboardEnhancementFlags, - DisableFocusChange, - DisableMouseCapture - )?; - - disable_raw_mode() -} diff --git a/crates/util/keyfork-crossterm/examples/event-stream-async-std.rs b/crates/util/keyfork-crossterm/examples/event-stream-async-std.rs deleted file mode 100644 index fbf1946..0000000 --- a/crates/util/keyfork-crossterm/examples/event-stream-async-std.rs +++ /dev/null @@ -1,67 +0,0 @@ -//! Demonstrates how to read events asynchronously with async-std. -//! -//! cargo run --features="event-stream" --example event-stream-async-std - -use std::{io::stdout, time::Duration}; - -use futures::{future::FutureExt, select, StreamExt}; -use futures_timer::Delay; - -use keyfork_crossterm::{ - cursor::position, - event::{DisableMouseCapture, EnableMouseCapture, Event, EventStream, KeyCode}, - execute, - terminal::{disable_raw_mode, enable_raw_mode}, -}; - -const HELP: &str = r#"EventStream based on futures_util::stream::Stream with async-std - - Keyboard, mouse and terminal resize events enabled - - Prints "." every second if there's no event - - Hit "c" to print current cursor position - - Use Esc to quit -"#; - -async fn print_events() { - let mut reader = EventStream::new(); - - loop { - let mut delay = Delay::new(Duration::from_millis(1_000)).fuse(); - let mut event = reader.next().fuse(); - - select! { - _ = delay => { println!(".\r"); }, - maybe_event = event => { - match maybe_event { - Some(Ok(event)) => { - println!("Event::{:?}\r", event); - - if event == Event::Key(KeyCode::Char('c').into()) { - println!("Cursor position: {:?}\r", position()); - } - - if event == Event::Key(KeyCode::Esc.into()) { - break; - } - } - Some(Err(e)) => println!("Error: {:?}\r", e), - None => break, - } - } - }; - } -} - -fn main() -> std::io::Result<()> { - println!("{}", HELP); - - enable_raw_mode()?; - - let mut stdout = stdout(); - execute!(stdout, EnableMouseCapture)?; - - async_std::task::block_on(print_events()); - - execute!(stdout, DisableMouseCapture)?; - - disable_raw_mode() -} diff --git a/crates/util/keyfork-crossterm/examples/event-stream-tokio.rs b/crates/util/keyfork-crossterm/examples/event-stream-tokio.rs deleted file mode 100644 index eff2dbe..0000000 --- a/crates/util/keyfork-crossterm/examples/event-stream-tokio.rs +++ /dev/null @@ -1,68 +0,0 @@ -//! Demonstrates how to read events asynchronously with tokio. -//! -//! cargo run --features="event-stream" --example event-stream-tokio - -use std::{io::stdout, time::Duration}; - -use futures::{future::FutureExt, select, StreamExt}; -use futures_timer::Delay; - -use keyfork_crossterm::{ - cursor::position, - event::{DisableMouseCapture, EnableMouseCapture, Event, EventStream, KeyCode}, - execute, - terminal::{disable_raw_mode, enable_raw_mode}, -}; - -const HELP: &str = r#"EventStream based on futures_util::Stream with tokio - - Keyboard, mouse and terminal resize events enabled - - Prints "." every second if there's no event - - Hit "c" to print current cursor position - - Use Esc to quit -"#; - -async fn print_events() { - let mut reader = EventStream::new(); - - loop { - let mut delay = Delay::new(Duration::from_millis(1_000)).fuse(); - let mut event = reader.next().fuse(); - - select! { - _ = delay => { println!(".\r"); }, - maybe_event = event => { - match maybe_event { - Some(Ok(event)) => { - println!("Event::{:?}\r", event); - - if event == Event::Key(KeyCode::Char('c').into()) { - println!("Cursor position: {:?}\r", position()); - } - - if event == Event::Key(KeyCode::Esc.into()) { - break; - } - } - Some(Err(e)) => println!("Error: {:?}\r", e), - None => break, - } - } - }; - } -} - -#[tokio::main] -async fn main() -> std::io::Result<()> { - println!("{}", HELP); - - enable_raw_mode()?; - - let mut stdout = stdout(); - execute!(stdout, EnableMouseCapture)?; - - print_events().await; - - execute!(stdout, DisableMouseCapture)?; - - disable_raw_mode() -} diff --git a/crates/util/keyfork-crossterm/examples/interactive-demo/Cargo.toml b/crates/util/keyfork-crossterm/examples/interactive-demo/Cargo.toml deleted file mode 100644 index ddb511a..0000000 --- a/crates/util/keyfork-crossterm/examples/interactive-demo/Cargo.toml +++ /dev/null @@ -1,13 +0,0 @@ -[package] -name = "interactive-demo" -version = "0.0.1" -authors = ["T. Post", "Robert Vojta "] -edition = "2018" -description = "Interactive demo for crossterm." -license = "MIT" -exclude = ["target", "Cargo.lock"] -readme = "README.md" -publish = false - -[dependencies] -crossterm = { path = "../../" } diff --git a/crates/util/keyfork-crossterm/examples/interactive-demo/src/macros.rs b/crates/util/keyfork-crossterm/examples/interactive-demo/src/macros.rs deleted file mode 100644 index 3c1a970..0000000 --- a/crates/util/keyfork-crossterm/examples/interactive-demo/src/macros.rs +++ /dev/null @@ -1,29 +0,0 @@ -macro_rules! run_tests { - ( - $dst:expr, - $( - $testfn:ident - ),* - $(,)? - ) => { - use keyfork_crossterm::{queue, style, terminal, cursor}; - $( - queue!( - $dst, - style::ResetColor, - terminal::Clear(terminal::ClearType::All), - cursor::MoveTo(1, 1), - cursor::Show, - cursor::EnableBlinking - )?; - - $testfn($dst)?; - - match $crate::read_char() { - Ok('q') => return Ok(()), - Err(e) => return Err(e), - _ => { }, - }; - )* - } -} diff --git a/crates/util/keyfork-crossterm/examples/interactive-demo/src/main.rs b/crates/util/keyfork-crossterm/examples/interactive-demo/src/main.rs deleted file mode 100644 index 7fb8f1f..0000000 --- a/crates/util/keyfork-crossterm/examples/interactive-demo/src/main.rs +++ /dev/null @@ -1,104 +0,0 @@ -#![allow(clippy::cognitive_complexity)] - -use std::io; - -use keyfork_crossterm::event::KeyEventKind; -pub use keyfork_crossterm::{ - cursor, - event::{self, Event, KeyCode, KeyEvent}, - execute, queue, style, - terminal::{self, ClearType}, - Command, -}; - -#[macro_use] -mod macros; -mod test; - -const MENU: &str = r#"Crossterm interactive test - -Controls: - - - 'q' - quit interactive test (or return to this menu) - - any other key - continue with next step - -Available tests: - -1. cursor -2. color (foreground, background) -3. attributes (bold, italic, ...) -4. input -5. synchronized output - -Select test to run ('1', '2', ...) or hit 'q' to quit. -"#; - -fn run(w: &mut W) -> io::Result<()> -where - W: io::Write, -{ - execute!(w, terminal::EnterAlternateScreen)?; - - terminal::enable_raw_mode()?; - - loop { - queue!( - w, - style::ResetColor, - terminal::Clear(ClearType::All), - cursor::Hide, - cursor::MoveTo(1, 1) - )?; - - for line in MENU.split('\n') { - queue!(w, style::Print(line), cursor::MoveToNextLine(1))?; - } - - w.flush()?; - - match read_char()? { - '1' => test::cursor::run(w)?, - '2' => test::color::run(w)?, - '3' => test::attribute::run(w)?, - '4' => test::event::run(w)?, - '5' => test::synchronized_output::run(w)?, - 'q' => { - execute!(w, cursor::SetCursorStyle::DefaultUserShape).unwrap(); - break; - } - _ => {} - }; - } - - execute!( - w, - style::ResetColor, - cursor::Show, - terminal::LeaveAlternateScreen - )?; - - terminal::disable_raw_mode() -} - -pub fn read_char() -> std::io::Result { - loop { - if let Ok(Event::Key(KeyEvent { - code: KeyCode::Char(c), - kind: KeyEventKind::Press, - modifiers: _, - state: _, - })) = event::read() - { - return Ok(c); - } - } -} - -pub fn buffer_size() -> io::Result<(u16, u16)> { - terminal::size() -} - -fn main() -> std::io::Result<()> { - let mut stdout = io::stdout(); - run(&mut stdout) -} diff --git a/crates/util/keyfork-crossterm/examples/interactive-demo/src/test.rs b/crates/util/keyfork-crossterm/examples/interactive-demo/src/test.rs deleted file mode 100644 index 1db95a2..0000000 --- a/crates/util/keyfork-crossterm/examples/interactive-demo/src/test.rs +++ /dev/null @@ -1,5 +0,0 @@ -pub mod attribute; -pub mod color; -pub mod cursor; -pub mod event; -pub mod synchronized_output; diff --git a/crates/util/keyfork-crossterm/examples/interactive-demo/src/test/attribute.rs b/crates/util/keyfork-crossterm/examples/interactive-demo/src/test/attribute.rs deleted file mode 100644 index 8172466..0000000 --- a/crates/util/keyfork-crossterm/examples/interactive-demo/src/test/attribute.rs +++ /dev/null @@ -1,58 +0,0 @@ -#![allow(clippy::cognitive_complexity)] - -use keyfork_crossterm::{cursor, queue, style}; -use std::io::Write; - -const ATTRIBUTES: [(style::Attribute, style::Attribute); 10] = [ - (style::Attribute::Bold, style::Attribute::NormalIntensity), - (style::Attribute::Italic, style::Attribute::NoItalic), - (style::Attribute::Underlined, style::Attribute::NoUnderline), - ( - style::Attribute::DoubleUnderlined, - style::Attribute::NoUnderline, - ), - (style::Attribute::Undercurled, style::Attribute::NoUnderline), - (style::Attribute::Underdotted, style::Attribute::NoUnderline), - (style::Attribute::Underdashed, style::Attribute::NoUnderline), - (style::Attribute::Reverse, style::Attribute::NoReverse), - ( - style::Attribute::CrossedOut, - style::Attribute::NotCrossedOut, - ), - (style::Attribute::SlowBlink, style::Attribute::NoBlink), -]; - -fn test_set_display_attributes(w: &mut W) -> std::io::Result<()> -where - W: Write, -{ - queue!( - w, - style::Print("Display attributes"), - cursor::MoveToNextLine(2) - )?; - - for (on, off) in &ATTRIBUTES { - queue!( - w, - style::SetAttribute(*on), - style::Print(format!("{:>width$} ", format!("{:?}", on), width = 35)), - style::SetAttribute(*off), - style::Print(format!("{:>width$}", format!("{:?}", off), width = 35)), - style::ResetColor, - cursor::MoveToNextLine(1) - )?; - } - - w.flush()?; - - Ok(()) -} - -pub fn run(w: &mut W) -> std::io::Result<()> -where - W: Write, -{ - run_tests!(w, test_set_display_attributes,); - Ok(()) -} diff --git a/crates/util/keyfork-crossterm/examples/interactive-demo/src/test/color.rs b/crates/util/keyfork-crossterm/examples/interactive-demo/src/test/color.rs deleted file mode 100644 index d851958..0000000 --- a/crates/util/keyfork-crossterm/examples/interactive-demo/src/test/color.rs +++ /dev/null @@ -1,198 +0,0 @@ -#![allow(clippy::cognitive_complexity)] - -use keyfork_crossterm::{cursor, queue, style, style::Color}; -use std::io::Write; - -const COLORS: [Color; 21] = [ - Color::Black, - Color::DarkGrey, - Color::Grey, - Color::White, - Color::DarkRed, - Color::Red, - Color::DarkGreen, - Color::Green, - Color::DarkYellow, - Color::Yellow, - Color::DarkBlue, - Color::Blue, - Color::DarkMagenta, - Color::Magenta, - Color::DarkCyan, - Color::Cyan, - Color::AnsiValue(0), - Color::AnsiValue(15), - Color::Rgb { r: 255, g: 0, b: 0 }, - Color::Rgb { r: 0, g: 255, b: 0 }, - Color::Rgb { r: 0, g: 0, b: 255 }, -]; - -fn test_set_foreground_color(w: &mut W) -> std::io::Result<()> -where - W: Write, -{ - queue!( - w, - style::Print("Foreground colors on the black & white background"), - cursor::MoveToNextLine(2) - )?; - - for color in &COLORS { - queue!( - w, - style::SetForegroundColor(*color), - style::SetBackgroundColor(Color::Black), - style::Print(format!( - "{:>width$} ", - format!("{:?} ████████████", color), - width = 40 - )), - style::SetBackgroundColor(Color::White), - style::Print(format!( - "{:>width$}", - format!("{:?} ████████████", color), - width = 40 - )), - cursor::MoveToNextLine(1) - )?; - } - - w.flush()?; - - Ok(()) -} - -fn test_set_background_color(w: &mut W) -> std::io::Result<()> -where - W: Write, -{ - queue!( - w, - style::Print("Background colors with black & white foreground"), - cursor::MoveToNextLine(2) - )?; - - for color in &COLORS { - queue!( - w, - style::SetBackgroundColor(*color), - style::SetForegroundColor(Color::Black), - style::Print(format!( - "{:>width$} ", - format!("{:?} ▒▒▒▒▒▒▒▒▒▒▒▒", color), - width = 40 - )), - style::SetForegroundColor(Color::White), - style::Print(format!( - "{:>width$}", - format!("{:?} ▒▒▒▒▒▒▒▒▒▒▒▒", color), - width = 40 - )), - cursor::MoveToNextLine(1) - )?; - } - - w.flush()?; - - Ok(()) -} - -fn test_color_values_matrix_16x16(w: &mut W, title: &str, color: F) -> std::io::Result<()> -where - W: Write, - F: Fn(u16, u16) -> Color, -{ - queue!(w, style::Print(title))?; - - for idx in 0..=15 { - queue!( - w, - cursor::MoveTo(1, idx + 4), - style::Print(format!("{:>width$}", idx, width = 2)) - )?; - queue!( - w, - cursor::MoveTo(idx * 3 + 3, 3), - style::Print(format!("{:>width$}", idx, width = 3)) - )?; - } - - for row in 0..=15u16 { - queue!(w, cursor::MoveTo(4, row + 4))?; - for col in 0..=15u16 { - queue!( - w, - style::SetForegroundColor(color(col, row)), - style::Print("███") - )?; - } - queue!( - w, - style::SetForegroundColor(Color::White), - style::Print(format!("{:>width$} ..= ", row * 16, width = 3)), - style::Print(format!("{:>width$}", row * 16 + 15, width = 3)) - )?; - } - - w.flush()?; - - Ok(()) -} - -fn test_color_ansi_values(w: &mut W) -> std::io::Result<()> -where - W: Write, -{ - test_color_values_matrix_16x16(w, "Color::Ansi values", |col, row| { - Color::AnsiValue((row * 16 + col) as u8) - }) -} - -fn test_rgb_red_values(w: &mut W) -> std::io::Result<()> -where - W: Write, -{ - test_color_values_matrix_16x16(w, "Color::Rgb red values", |col, row| Color::Rgb { - r: (row * 16 + col) as u8, - g: 0_u8, - b: 0, - }) -} - -fn test_rgb_green_values(w: &mut W) -> std::io::Result<()> -where - W: Write, -{ - test_color_values_matrix_16x16(w, "Color::Rgb green values", |col, row| Color::Rgb { - r: 0, - g: (row * 16 + col) as u8, - b: 0, - }) -} - -fn test_rgb_blue_values(w: &mut W) -> std::io::Result<()> -where - W: Write, -{ - test_color_values_matrix_16x16(w, "Color::Rgb blue values", |col, row| Color::Rgb { - r: 0, - g: 0, - b: (row * 16 + col) as u8, - }) -} - -pub fn run(w: &mut W) -> std::io::Result<()> -where - W: Write, -{ - run_tests!( - w, - test_set_foreground_color, - test_set_background_color, - test_color_ansi_values, - test_rgb_red_values, - test_rgb_green_values, - test_rgb_blue_values, - ); - Ok(()) -} diff --git a/crates/util/keyfork-crossterm/examples/interactive-demo/src/test/cursor.rs b/crates/util/keyfork-crossterm/examples/interactive-demo/src/test/cursor.rs deleted file mode 100644 index 2adedff..0000000 --- a/crates/util/keyfork-crossterm/examples/interactive-demo/src/test/cursor.rs +++ /dev/null @@ -1,222 +0,0 @@ -#![allow(clippy::cognitive_complexity)] - -use std::io::Write; - -use keyfork_crossterm::{cursor, execute, queue, style, style::Stylize, Command}; -use std::thread; -use std::time::Duration; - -fn test_move_cursor_up(w: &mut W) -> std::io::Result<()> -where - W: Write, -{ - draw_cursor_box(w, "Move Up (2)", |_, _| cursor::MoveUp(2)) -} - -fn test_move_cursor_down(w: &mut W) -> std::io::Result<()> -where - W: Write, -{ - draw_cursor_box(w, "Move Down (2)", |_, _| cursor::MoveDown(2)) -} - -fn test_move_cursor_left(w: &mut W) -> std::io::Result<()> -where - W: Write, -{ - draw_cursor_box(w, "Move Left (2)", |_, _| cursor::MoveLeft(2)) -} - -fn test_move_cursor_right(w: &mut W) -> std::io::Result<()> -where - W: Write, -{ - draw_cursor_box(w, "Move Right (2)", |_, _| cursor::MoveRight(2)) -} - -fn test_move_cursor_to_previous_line(w: &mut W) -> std::io::Result<()> -where - W: Write, -{ - draw_cursor_box(w, "MoveToPreviousLine (1)", |_, _| { - cursor::MoveToPreviousLine(1) - }) -} - -fn test_move_cursor_to_next_line(w: &mut W) -> std::io::Result<()> -where - W: Write, -{ - draw_cursor_box(w, "MoveToNextLine (1)", |_, _| cursor::MoveToNextLine(1)) -} - -fn test_move_cursor_to_column(w: &mut W) -> std::io::Result<()> -where - W: Write, -{ - draw_cursor_box(w, "MoveToColumn (1)", |center_x, _| { - cursor::MoveToColumn(center_x + 1) - }) -} - -fn test_hide_cursor(w: &mut W) -> std::io::Result<()> -where - W: Write, -{ - execute!(w, style::Print("HideCursor"), cursor::Hide) -} - -fn test_show_cursor(w: &mut W) -> std::io::Result<()> -where - W: Write, -{ - execute!(w, style::Print("ShowCursor"), cursor::Show) -} - -fn test_cursor_blinking_block(w: &mut W) -> std::io::Result<()> -where - W: Write, -{ - execute!( - w, - style::Print("Blinking Block:"), - cursor::MoveLeft(2), - cursor::SetCursorStyle::BlinkingBlock, - ) -} - -fn test_cursor_blinking_underscore(w: &mut W) -> std::io::Result<()> -where - W: Write, -{ - execute!( - w, - style::Print("Blinking Underscore:"), - cursor::MoveLeft(2), - cursor::SetCursorStyle::BlinkingUnderScore, - ) -} - -fn test_cursor_blinking_bar(w: &mut W) -> std::io::Result<()> -where - W: Write, -{ - execute!( - w, - style::Print("Blinking bar:"), - cursor::MoveLeft(2), - cursor::SetCursorStyle::BlinkingBar, - ) -} - -fn test_move_cursor_to(w: &mut W) -> std::io::Result<()> -where - W: Write, -{ - draw_cursor_box( - w, - "MoveTo (x: 1, y: 1) removed from center", - |center_x, center_y| cursor::MoveTo(center_x + 1, center_y + 1), - ) -} - -fn test_save_restore_cursor_position(w: &mut W) -> std::io::Result<()> -where - W: Write, -{ - execute!(w, - cursor::MoveTo(0, 0), - style::Print("Save position, print character elsewhere, after three seconds restore to old position."), - cursor::MoveToNextLine(2), - style::Print("Save ->[ ]<- Position"), - cursor::MoveTo(8, 2), - cursor::SavePosition, - cursor::MoveTo(10,10), - style::Print("Move To ->[√]<- Position") - )?; - - thread::sleep(Duration::from_secs(3)); - - execute!(w, cursor::RestorePosition, style::Print("√")) -} - -/// Draws a box with an colored center, this center can be taken as a reference point after running the given cursor command. -fn draw_cursor_box(w: &mut W, description: &str, cursor_command: F) -> std::io::Result<()> -where - W: Write, - F: Fn(u16, u16) -> T, - T: Command, -{ - execute!( - w, - cursor::Hide, - cursor::MoveTo(0, 0), - style::SetForegroundColor(style::Color::Red), - style::Print(format!( - "Red box is the center. After the action: '{}' '√' is drawn to reflect the action from the center.", - description - )) - )?; - - let start_y = 2; - let width = 21; - let height = 11 + start_y; - let center_x = width / 2; - let center_y = (height + start_y) / 2; - - for row in start_y..=10 + start_y { - for column in 0..=width { - if (row == start_y || row == height - 1) || (column == 0 || column == width) { - queue!( - w, - cursor::MoveTo(column, row), - style::PrintStyledContent("▓".red()), - )?; - } else { - queue!( - w, - cursor::MoveTo(column, row), - style::PrintStyledContent("_".red().on_white()) - )?; - } - } - } - - queue!( - w, - cursor::MoveTo(center_x, center_y), - style::PrintStyledContent("▀".red().on_white()), - cursor::MoveTo(center_x, center_y), - )?; - queue!( - w, - cursor_command(center_x, center_y), - style::PrintStyledContent("√".magenta().on_white()) - )?; - w.flush()?; - Ok(()) -} - -pub fn run(w: &mut W) -> std::io::Result<()> -where - W: Write, -{ - run_tests!( - w, - test_hide_cursor, - test_show_cursor, - test_cursor_blinking_bar, - test_cursor_blinking_block, - test_cursor_blinking_underscore, - test_move_cursor_left, - test_move_cursor_right, - test_move_cursor_up, - test_move_cursor_down, - test_move_cursor_to, - test_move_cursor_to_next_line, - test_move_cursor_to_previous_line, - test_move_cursor_to_column, - test_save_restore_cursor_position - ); - Ok(()) -} diff --git a/crates/util/keyfork-crossterm/examples/interactive-demo/src/test/event.rs b/crates/util/keyfork-crossterm/examples/interactive-demo/src/test/event.rs deleted file mode 100644 index b0b0b35..0000000 --- a/crates/util/keyfork-crossterm/examples/interactive-demo/src/test/event.rs +++ /dev/null @@ -1,42 +0,0 @@ -#![allow(clippy::cognitive_complexity)] - -use keyfork_crossterm::{ - cursor::position, - event::{read, DisableMouseCapture, EnableMouseCapture, Event, KeyCode}, - execute, -}; -use std::io::{self, Write}; - -fn test_event(w: &mut W) -> io::Result<()> -where - W: io::Write, -{ - execute!(w, EnableMouseCapture)?; - - loop { - // Blocking read - let event = read()?; - - println!("Event::{:?}\r", event); - - if event == Event::Key(KeyCode::Char('c').into()) { - println!("Cursor position: {:?}\r", position()); - } - - if event == Event::Key(KeyCode::Char('q').into()) { - break; - } - } - - execute!(w, DisableMouseCapture)?; - - Ok(()) -} - -pub fn run(w: &mut W) -> std::io::Result<()> -where - W: Write, -{ - run_tests!(w, test_event); - Ok(()) -} diff --git a/crates/util/keyfork-crossterm/examples/interactive-demo/src/test/synchronized_output.rs b/crates/util/keyfork-crossterm/examples/interactive-demo/src/test/synchronized_output.rs deleted file mode 100644 index cfda19d..0000000 --- a/crates/util/keyfork-crossterm/examples/interactive-demo/src/test/synchronized_output.rs +++ /dev/null @@ -1,41 +0,0 @@ -use std::io::Write; - -use keyfork_crossterm::{cursor, execute, style::Print, SynchronizedUpdate}; - -fn render_slowly(w: &mut W) -> std::io::Result<()> -where - W: Write, -{ - for i in 1..10 { - execute!(w, Print(format!("{}", i)))?; - std::thread::sleep(std::time::Duration::from_millis(50)); - } - Ok(()) -} - -fn test_slow_rendering(w: &mut W) -> std::io::Result<()> -where - W: Write, -{ - execute!(w, Print("Rendering without synchronized update:"))?; - execute!(w, cursor::MoveToNextLine(1))?; - std::thread::sleep(std::time::Duration::from_millis(50)); - render_slowly(w)?; - - execute!(w, cursor::MoveToNextLine(1))?; - execute!(w, Print("Rendering with synchronized update:"))?; - execute!(w, cursor::MoveToNextLine(1))?; - std::thread::sleep(std::time::Duration::from_millis(50)); - w.sync_update(render_slowly)??; - - execute!(w, cursor::MoveToNextLine(1))?; - Ok(()) -} - -pub fn run(w: &mut W) -> std::io::Result<()> -where - W: Write, -{ - run_tests!(w, test_slow_rendering,); - Ok(()) -} diff --git a/crates/util/keyfork-crossterm/examples/is_tty.rs b/crates/util/keyfork-crossterm/examples/is_tty.rs deleted file mode 100644 index 044d8af..0000000 --- a/crates/util/keyfork-crossterm/examples/is_tty.rs +++ /dev/null @@ -1,20 +0,0 @@ -#![allow(missing_docs)] - -use keyfork_crossterm::{ - execute, - terminal::{size, SetSize}, - tty::IsTty, -}; -use std::io::{stdin, stdout}; - -fn main() { - println!("size: {:?}", size().unwrap()); - execute!(stdout(), SetSize(10, 10)).unwrap(); - println!("resized: {:?}", size().unwrap()); - - if stdin().is_tty() { - println!("Is TTY"); - } else { - println!("Is not TTY"); - } -} diff --git a/crates/util/keyfork-crossterm/examples/stderr.rs b/crates/util/keyfork-crossterm/examples/stderr.rs deleted file mode 100644 index a94be63..0000000 --- a/crates/util/keyfork-crossterm/examples/stderr.rs +++ /dev/null @@ -1,96 +0,0 @@ -//! This shows how an application can write on stderr -//! instead of stdout, thus making it possible to -//! the command API instead of the "old style" direct -//! unbuffered API. -//! -//! This particular example is only suited to Unix -//! for now. -//! -//! cargo run --example stderr - -use std::io; - -use keyfork_crossterm::{ - cursor::{Hide, MoveTo, Show}, - event, - event::{Event, KeyCode, KeyEvent}, - execute, queue, - style::Print, - terminal::{self, EnterAlternateScreen, LeaveAlternateScreen}, -}; - -const TEXT: &str = r#" -This screen is ran on stderr. -And when you hit enter, it prints on stdout. -This makes it possible to run an application and choose what will -be sent to any application calling yours. - -For example, assuming you build this example with - - cargo build --bin stderr - -and then you run it with - - cd "$(target/debug/stderr)" - -what the application prints on stdout is used as argument to cd. - -Try it out. - -Hit any key to quit this screen: - -1 will print `..` -2 will print `/` -3 will print `~` -Any other key will print this text (so that you may copy-paste) -"#; - -fn run_app(write: &mut W) -> io::Result -where - W: io::Write, -{ - queue!( - write, - EnterAlternateScreen, // enter alternate screen - Hide // hide the cursor - )?; - - let mut y = 1; - for line in TEXT.split('\n') { - queue!(write, MoveTo(1, y), Print(line.to_string()))?; - y += 1; - } - - write.flush()?; - - terminal::enable_raw_mode()?; - let user_char = read_char()?; // we wait for the user to hit a key - execute!(write, Show, LeaveAlternateScreen)?; // restore the cursor and leave the alternate screen - - terminal::disable_raw_mode()?; - - Ok(user_char) -} - -/// Read a character from input. -pub fn read_char() -> io::Result { - loop { - if let Event::Key(KeyEvent { - code: KeyCode::Char(c), - .. - }) = event::read()? - { - return Ok(c); - } - } -} - -// cargo run --example stderr -fn main() { - match run_app(&mut io::stderr()).unwrap() { - '1' => print!(".."), - '2' => print!("/"), - '3' => print!("~"), - _ => println!("{}", TEXT), - } -} diff --git a/crates/util/keyfork-crossterm/src/ansi_support.rs b/crates/util/keyfork-crossterm/src/ansi_support.rs deleted file mode 100644 index fc2f6f1..0000000 --- a/crates/util/keyfork-crossterm/src/ansi_support.rs +++ /dev/null @@ -1,46 +0,0 @@ -use std::sync::atomic::{AtomicBool, Ordering}; - -use crossterm_winapi::{ConsoleMode, Handle}; -use parking_lot::Once; -use winapi::um::wincon::ENABLE_VIRTUAL_TERMINAL_PROCESSING; - -/// Enable virtual terminal processing. -/// -/// This method attempts to enable virtual terminal processing for this -/// console. If there was a problem enabling it, then an error returned. -/// On success, the caller may assume that enabling it was successful. -/// -/// When virtual terminal processing is enabled, characters emitted to the -/// console are parsed for VT100 and similar control character sequences -/// that control color and other similar operations. -fn enable_vt_processing() -> std::io::Result<()> { - let mask = ENABLE_VIRTUAL_TERMINAL_PROCESSING; - - let console_mode = ConsoleMode::from(Handle::current_out_handle()?); - let old_mode = console_mode.mode()?; - - if old_mode & mask == 0 { - console_mode.set_mode(old_mode | mask)?; - } - - Ok(()) -} - -static SUPPORTS_ANSI_ESCAPE_CODES: AtomicBool = AtomicBool::new(false); -static INITIALIZER: Once = Once::new(); - -/// Checks if the current terminal supports ANSI escape sequences -pub fn supports_ansi() -> bool { - INITIALIZER.call_once(|| { - // Some terminals on Windows like GitBash can't use WinAPI calls directly - // so when we try to enable the ANSI-flag for Windows this won't work. - // Because of that we should check first if the TERM-variable is set - // and see if the current terminal is a terminal who does support ANSI. - let supported = enable_vt_processing().is_ok() - || std::env::var("TERM").map_or(false, |term| term != "dumb"); - - SUPPORTS_ANSI_ESCAPE_CODES.store(supported, Ordering::SeqCst); - }); - - SUPPORTS_ANSI_ESCAPE_CODES.load(Ordering::SeqCst) -} diff --git a/crates/util/keyfork-crossterm/src/command.rs b/crates/util/keyfork-crossterm/src/command.rs deleted file mode 100644 index 0425392..0000000 --- a/crates/util/keyfork-crossterm/src/command.rs +++ /dev/null @@ -1,295 +0,0 @@ -use std::fmt; -use std::io::{self, Write}; - -use crate::terminal::{BeginSynchronizedUpdate, EndSynchronizedUpdate}; - -/// An interface for a command that performs an action on the terminal. -/// -/// Crossterm provides a set of commands, -/// and there is no immediate reason to implement a command yourself. -/// In order to understand how to use and execute commands, -/// it is recommended that you take a look at [Command API](./index.html#command-api) chapter. -pub trait Command { - /// Write an ANSI representation of this command to the given writer. - /// An ANSI code can manipulate the terminal by writing it to the terminal buffer. - /// However, only Windows 10 and UNIX systems support this. - /// - /// This method does not need to be accessed manually, as it is used by the crossterm's [Command API](./index.html#command-api) - fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result; - - /// Execute this command. - /// - /// Windows versions lower than windows 10 do not support ANSI escape codes, - /// therefore a direct WinAPI call is made. - /// - /// This method does not need to be accessed manually, as it is used by the crossterm's [Command API](./index.html#command-api) - #[cfg(windows)] - fn execute_winapi(&self) -> io::Result<()>; - - /// Returns whether the ANSI code representation of this command is supported by windows. - /// - /// A list of supported ANSI escape codes - /// can be found [here](https://docs.microsoft.com/en-us/windows/console/console-virtual-terminal-sequences). - #[cfg(windows)] - fn is_ansi_code_supported(&self) -> bool { - super::ansi_support::supports_ansi() - } -} - -impl Command for &T { - fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result { - (**self).write_ansi(f) - } - - #[inline] - #[cfg(windows)] - fn execute_winapi(&self) -> io::Result<()> { - T::execute_winapi(self) - } - - #[cfg(windows)] - #[inline] - fn is_ansi_code_supported(&self) -> bool { - T::is_ansi_code_supported(self) - } -} - -/// An interface for types that can queue commands for further execution. -pub trait QueueableCommand { - /// Queues the given command for further execution. - fn queue(&mut self, command: impl Command) -> io::Result<&mut Self>; -} - -/// An interface for types that can directly execute commands. -pub trait ExecutableCommand { - /// Executes the given command directly. - fn execute(&mut self, command: impl Command) -> io::Result<&mut Self>; -} - -impl QueueableCommand for T { - /// Queues the given command for further execution. - /// - /// Queued commands will be executed in the following cases: - /// - /// * When `flush` is called manually on the given type implementing `io::Write`. - /// * The terminal will `flush` automatically if the buffer is full. - /// * Each line is flushed in case of `stdout`, because it is line buffered. - /// - /// # Arguments - /// - /// - [Command](./trait.Command.html) - /// - /// The command that you want to queue for later execution. - /// - /// # Examples - /// - /// ```rust - /// use std::io::{self, Write}; - /// use keyfork_crossterm::{QueueableCommand, style::Print}; - /// - /// fn main() -> io::Result<()> { - /// let mut stdout = io::stdout(); - /// - /// // `Print` will executed executed when `flush` is called. - /// stdout - /// .queue(Print("foo 1\n".to_string()))? - /// .queue(Print("foo 2".to_string()))?; - /// - /// // some other code (no execution happening here) ... - /// - /// // when calling `flush` on `stdout`, all commands will be written to the stdout and therefore executed. - /// stdout.flush()?; - /// - /// Ok(()) - /// - /// // ==== Output ==== - /// // foo 1 - /// // foo 2 - /// } - /// ``` - /// - /// Have a look over at the [Command API](./index.html#command-api) for more details. - /// - /// # Notes - /// - /// * In the case of UNIX and Windows 10, ANSI codes are written to the given 'writer'. - /// * In case of Windows versions lower than 10, a direct WinAPI call will be made. - /// The reason for this is that Windows versions lower than 10 do not support ANSI codes, - /// and can therefore not be written to the given `writer`. - /// Therefore, there is no difference between [execute](./trait.ExecutableCommand.html) - /// and [queue](./trait.QueueableCommand.html) for those old Windows versions. - fn queue(&mut self, command: impl Command) -> io::Result<&mut Self> { - #[cfg(windows)] - if !command.is_ansi_code_supported() { - // There may be queued commands in this writer, but `execute_winapi` will execute the - // command immediately. To prevent commands being executed out of order we flush the - // writer now. - self.flush()?; - command.execute_winapi()?; - return Ok(self); - } - - write_command_ansi(self, command)?; - Ok(self) - } -} - -impl ExecutableCommand for T { - /// Executes the given command directly. - /// - /// The given command its ANSI escape code will be written and flushed onto `Self`. - /// - /// # Arguments - /// - /// - [Command](./trait.Command.html) - /// - /// The command that you want to execute directly. - /// - /// # Example - /// - /// ```rust - /// use std::io; - /// use keyfork_crossterm::{ExecutableCommand, style::Print}; - /// - /// fn main() -> io::Result<()> { - /// // will be executed directly - /// io::stdout() - /// .execute(Print("sum:\n".to_string()))? - /// .execute(Print(format!("1 + 1= {} ", 1 + 1)))?; - /// - /// Ok(()) - /// - /// // ==== Output ==== - /// // sum: - /// // 1 + 1 = 2 - /// } - /// ``` - /// - /// Have a look over at the [Command API](./index.html#command-api) for more details. - /// - /// # Notes - /// - /// * In the case of UNIX and Windows 10, ANSI codes are written to the given 'writer'. - /// * In case of Windows versions lower than 10, a direct WinAPI call will be made. - /// The reason for this is that Windows versions lower than 10 do not support ANSI codes, - /// and can therefore not be written to the given `writer`. - /// Therefore, there is no difference between [execute](./trait.ExecutableCommand.html) - /// and [queue](./trait.QueueableCommand.html) for those old Windows versions. - fn execute(&mut self, command: impl Command) -> io::Result<&mut Self> { - self.queue(command)?; - self.flush()?; - Ok(self) - } -} - -/// An interface for types that support synchronized updates. -pub trait SynchronizedUpdate { - /// Performs a set of actions against the given type. - fn sync_update(&mut self, operations: impl FnOnce(&mut Self) -> T) -> io::Result; -} - -impl SynchronizedUpdate for W { - /// Performs a set of actions within a synchronous update. - /// - /// Updates will be suspended in the terminal, the function will be executed against self, - /// updates will be resumed, and a flush will be performed. - /// - /// # Arguments - /// - /// - Function - /// - /// A function that performs the operations that must execute in a synchronized update. - /// - /// # Examples - /// - /// ```rust - /// use std::io; - /// use keyfork_crossterm::{ExecutableCommand, SynchronizedUpdate, style::Print}; - /// - /// fn main() -> io::Result<()> { - /// let mut stdout = io::stdout(); - /// - /// stdout.sync_update(|stdout| { - /// stdout.execute(Print("foo 1\n".to_string()))?; - /// stdout.execute(Print("foo 2".to_string()))?; - /// // The effects of the print command will not be present in the terminal - /// // buffer, but not visible in the terminal. - /// std::io::Result::Ok(()) - /// })?; - /// - /// // The effects of the commands will be visible. - /// - /// Ok(()) - /// - /// // ==== Output ==== - /// // foo 1 - /// // foo 2 - /// } - /// ``` - /// - /// # Notes - /// - /// This command is performed only using ANSI codes, and will do nothing on terminals that do not support ANSI - /// codes, or this specific extension. - /// - /// When rendering the screen of the terminal, the Emulator usually iterates through each visible grid cell and - /// renders its current state. With applications updating the screen a at higher frequency this can cause tearing. - /// - /// This mode attempts to mitigate that. - /// - /// When the synchronization mode is enabled following render calls will keep rendering the last rendered state. - /// The terminal Emulator keeps processing incoming text and sequences. When the synchronized update mode is disabled - /// again the renderer may fetch the latest screen buffer state again, effectively avoiding the tearing effect - /// by unintentionally rendering in the middle a of an application screen update. - /// - fn sync_update(&mut self, operations: impl FnOnce(&mut Self) -> T) -> io::Result { - self.queue(BeginSynchronizedUpdate)?; - let result = operations(self); - self.execute(EndSynchronizedUpdate)?; - Ok(result) - } -} -/// Writes the ANSI representation of a command to the given writer. -fn write_command_ansi( - io: &mut (impl io::Write + ?Sized), - command: C, -) -> io::Result<()> { - struct Adapter { - inner: T, - res: io::Result<()>, - } - - impl fmt::Write for Adapter { - fn write_str(&mut self, s: &str) -> fmt::Result { - self.inner.write_all(s.as_bytes()).map_err(|e| { - self.res = Err(e); - fmt::Error - }) - } - } - - let mut adapter = Adapter { - inner: io, - res: Ok(()), - }; - - command - .write_ansi(&mut adapter) - .map_err(|fmt::Error| match adapter.res { - Ok(()) => panic!( - "<{}>::write_ansi incorrectly errored", - std::any::type_name::() - ), - Err(e) => e, - }) -} - -/// Executes the ANSI representation of a command, using the given `fmt::Write`. -pub(crate) fn execute_fmt(f: &mut impl fmt::Write, command: impl Command) -> fmt::Result { - #[cfg(windows)] - if !command.is_ansi_code_supported() { - return command.execute_winapi().map_err(|_| fmt::Error); - } - - command.write_ansi(f) -} diff --git a/crates/util/keyfork-crossterm/src/cursor.rs b/crates/util/keyfork-crossterm/src/cursor.rs deleted file mode 100644 index d797127..0000000 --- a/crates/util/keyfork-crossterm/src/cursor.rs +++ /dev/null @@ -1,504 +0,0 @@ -//! # Cursor -//! -//! The `cursor` module provides functionality to work with the terminal cursor. -//! -//! This documentation does not contain a lot of examples. The reason is that it's fairly -//! obvious how to use this crate. Although, we do provide -//! [examples](https://github.com/crossterm-rs/crossterm/tree/master/examples) repository -//! to demonstrate the capabilities. -//! -//! ## Examples -//! -//! Cursor actions can be performed with commands. -//! Please have a look at [command documentation](../index.html#command-api) for a more detailed documentation. -//! -//! ```no_run -//! use std::io::{self, Write}; -//! -//! use keyfork_crossterm::{ -//! ExecutableCommand, execute, -//! cursor::{DisableBlinking, EnableBlinking, MoveTo, RestorePosition, SavePosition} -//! }; -//! -//! fn main() -> io::Result<()> { -//! // with macro -//! execute!( -//! io::stdout(), -//! SavePosition, -//! MoveTo(10, 10), -//! EnableBlinking, -//! DisableBlinking, -//! RestorePosition -//! ); -//! -//! // with function -//! io::stdout() -//! .execute(MoveTo(11,11))? -//! .execute(RestorePosition); -//! -//! Ok(()) -//! } -//! ``` -//! -//! For manual execution control check out [crossterm::queue](../macro.queue.html). - -use std::fmt; - -use crate::{csi, impl_display, Command}; - -pub(crate) mod sys; - -#[cfg(feature = "events")] -pub use sys::position; - -/// A command that moves the terminal cursor to the given position (column, row). -/// -/// # Notes -/// * Top left cell is represented as `0,0`. -/// * Commands must be executed/queued for execution otherwise they do nothing. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct MoveTo(pub u16, pub u16); - -impl Command for MoveTo { - fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result { - write!(f, csi!("{};{}H"), self.1 + 1, self.0 + 1) - } - - #[cfg(windows)] - fn execute_winapi(&self) -> std::io::Result<()> { - sys::move_to(self.0, self.1) - } -} - -/// A command that moves the terminal cursor down the given number of lines, -/// and moves it to the first column. -/// -/// # Notes -/// * This command is 1 based, meaning `MoveToNextLine(1)` moves to the next line. -/// * Most terminals default 0 argument to 1. -/// * Commands must be executed/queued for execution otherwise they do nothing. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct MoveToNextLine(pub u16); - -impl Command for MoveToNextLine { - fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result { - write!(f, csi!("{}E"), self.0)?; - Ok(()) - } - - #[cfg(windows)] - fn execute_winapi(&self) -> std::io::Result<()> { - if self.0 != 0 { - sys::move_to_next_line(self.0)?; - } - Ok(()) - } -} - -/// A command that moves the terminal cursor up the given number of lines, -/// and moves it to the first column. -/// -/// # Notes -/// * This command is 1 based, meaning `MoveToPreviousLine(1)` moves to the previous line. -/// * Most terminals default 0 argument to 1. -/// * Commands must be executed/queued for execution otherwise they do nothing. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct MoveToPreviousLine(pub u16); - -impl Command for MoveToPreviousLine { - fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result { - write!(f, csi!("{}F"), self.0)?; - Ok(()) - } - - #[cfg(windows)] - fn execute_winapi(&self) -> std::io::Result<()> { - if self.0 != 0 { - sys::move_to_previous_line(self.0)?; - } - Ok(()) - } -} - -/// A command that moves the terminal cursor to the given column on the current row. -/// -/// # Notes -/// * This command is 0 based, meaning 0 is the leftmost column. -/// * Commands must be executed/queued for execution otherwise they do nothing. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct MoveToColumn(pub u16); - -impl Command for MoveToColumn { - fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result { - write!(f, csi!("{}G"), self.0 + 1)?; - Ok(()) - } - - #[cfg(windows)] - fn execute_winapi(&self) -> std::io::Result<()> { - sys::move_to_column(self.0) - } -} - -/// A command that moves the terminal cursor to the given row on the current column. -/// -/// # Notes -/// * This command is 0 based, meaning 0 is the topmost row. -/// * Commands must be executed/queued for execution otherwise they do nothing. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct MoveToRow(pub u16); - -impl Command for MoveToRow { - fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result { - write!(f, csi!("{}d"), self.0 + 1)?; - Ok(()) - } - - #[cfg(windows)] - fn execute_winapi(&self) -> std::io::Result<()> { - sys::move_to_row(self.0) - } -} - -/// A command that moves the terminal cursor a given number of rows up. -/// -/// # Notes -/// * This command is 1 based, meaning `MoveUp(1)` moves the cursor up one cell. -/// * Most terminals default 0 argument to 1. -/// * Commands must be executed/queued for execution otherwise they do nothing. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct MoveUp(pub u16); - -impl Command for MoveUp { - fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result { - write!(f, csi!("{}A"), self.0)?; - Ok(()) - } - - #[cfg(windows)] - fn execute_winapi(&self) -> std::io::Result<()> { - sys::move_up(self.0) - } -} - -/// A command that moves the terminal cursor a given number of columns to the right. -/// -/// # Notes -/// * This command is 1 based, meaning `MoveRight(1)` moves the cursor right one cell. -/// * Most terminals default 0 argument to 1. -/// * Commands must be executed/queued for execution otherwise they do nothing. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct MoveRight(pub u16); - -impl Command for MoveRight { - fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result { - write!(f, csi!("{}C"), self.0)?; - Ok(()) - } - - #[cfg(windows)] - fn execute_winapi(&self) -> std::io::Result<()> { - sys::move_right(self.0) - } -} - -/// A command that moves the terminal cursor a given number of rows down. -/// -/// # Notes -/// * This command is 1 based, meaning `MoveDown(1)` moves the cursor down one cell. -/// * Most terminals default 0 argument to 1. -/// * Commands must be executed/queued for execution otherwise they do nothing. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct MoveDown(pub u16); - -impl Command for MoveDown { - fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result { - write!(f, csi!("{}B"), self.0)?; - Ok(()) - } - - #[cfg(windows)] - fn execute_winapi(&self) -> std::io::Result<()> { - sys::move_down(self.0) - } -} - -/// A command that moves the terminal cursor a given number of columns to the left. -/// -/// # Notes -/// * This command is 1 based, meaning `MoveLeft(1)` moves the cursor left one cell. -/// * Most terminals default 0 argument to 1. -/// * Commands must be executed/queued for execution otherwise they do nothing. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct MoveLeft(pub u16); - -impl Command for MoveLeft { - fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result { - write!(f, csi!("{}D"), self.0)?; - Ok(()) - } - - #[cfg(windows)] - fn execute_winapi(&self) -> std::io::Result<()> { - sys::move_left(self.0) - } -} - -/// A command that saves the current terminal cursor position. -/// -/// See the [RestorePosition](./struct.RestorePosition.html) command. -/// -/// # Notes -/// -/// - The cursor position is stored globally. -/// - Commands must be executed/queued for execution otherwise they do nothing. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct SavePosition; - -impl Command for SavePosition { - fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result { - f.write_str("\x1B7") - } - - #[cfg(windows)] - fn execute_winapi(&self) -> std::io::Result<()> { - sys::save_position() - } -} - -/// A command that restores the saved terminal cursor position. -/// -/// See the [SavePosition](./struct.SavePosition.html) command. -/// -/// # Notes -/// -/// - The cursor position is stored globally. -/// - Commands must be executed/queued for execution otherwise they do nothing. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct RestorePosition; - -impl Command for RestorePosition { - fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result { - f.write_str("\x1B8") - } - - #[cfg(windows)] - fn execute_winapi(&self) -> std::io::Result<()> { - sys::restore_position() - } -} - -/// A command that hides the terminal cursor. -/// -/// # Notes -/// -/// - Commands must be executed/queued for execution otherwise they do nothing. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct Hide; - -impl Command for Hide { - fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result { - f.write_str(csi!("?25l")) - } - - #[cfg(windows)] - fn execute_winapi(&self) -> std::io::Result<()> { - sys::show_cursor(false) - } -} - -/// A command that shows the terminal cursor. -/// -/// # Notes -/// -/// - Commands must be executed/queued for execution otherwise they do nothing. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct Show; - -impl Command for Show { - fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result { - f.write_str(csi!("?25h")) - } - - #[cfg(windows)] - fn execute_winapi(&self) -> std::io::Result<()> { - sys::show_cursor(true) - } -} - -/// A command that enables blinking of the terminal cursor. -/// -/// # Notes -/// -/// - Some Unix terminals (ex: GNOME and Konsole) as well as Windows versions lower than Windows 10 do not support this functionality. -/// Use `SetCursorStyle` for better cross-compatibility. -/// - Commands must be executed/queued for execution otherwise they do nothing. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct EnableBlinking; -impl Command for EnableBlinking { - fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result { - f.write_str(csi!("?12h")) - } - #[cfg(windows)] - fn execute_winapi(&self) -> std::io::Result<()> { - Ok(()) - } -} - -/// A command that disables blinking of the terminal cursor. -/// -/// # Notes -/// -/// - Some Unix terminals (ex: GNOME and Konsole) as well as Windows versions lower than Windows 10 do not support this functionality. -/// Use `SetCursorStyle` for better cross-compatibility. -/// - Commands must be executed/queued for execution otherwise they do nothing. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct DisableBlinking; -impl Command for DisableBlinking { - fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result { - f.write_str(csi!("?12l")) - } - #[cfg(windows)] - fn execute_winapi(&self) -> std::io::Result<()> { - Ok(()) - } -} - -/// A command that sets the style of the cursor. -/// It uses two types of escape codes, one to control blinking, and the other the shape. -/// -/// # Note -/// -/// - Commands must be executed/queued for execution otherwise they do nothing. -#[derive(Clone, Copy)] -pub enum SetCursorStyle { - /// Default cursor shape configured by the user. - DefaultUserShape, - /// A blinking block cursor shape (■). - BlinkingBlock, - /// A non blinking block cursor shape (inverse of `BlinkingBlock`). - SteadyBlock, - /// A blinking underscore cursor shape(_). - BlinkingUnderScore, - /// A non blinking underscore cursor shape (inverse of `BlinkingUnderScore`). - SteadyUnderScore, - /// A blinking cursor bar shape (|) - BlinkingBar, - /// A steady cursor bar shape (inverse of `BlinkingBar`). - SteadyBar, -} - -impl Command for SetCursorStyle { - fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result { - match self { - SetCursorStyle::DefaultUserShape => f.write_str("\x1b[0 q"), - SetCursorStyle::BlinkingBlock => f.write_str("\x1b[1 q"), - SetCursorStyle::SteadyBlock => f.write_str("\x1b[2 q"), - SetCursorStyle::BlinkingUnderScore => f.write_str("\x1b[3 q"), - SetCursorStyle::SteadyUnderScore => f.write_str("\x1b[4 q"), - SetCursorStyle::BlinkingBar => f.write_str("\x1b[5 q"), - SetCursorStyle::SteadyBar => f.write_str("\x1b[6 q"), - } - } - - #[cfg(windows)] - fn execute_winapi(&self) -> std::io::Result<()> { - Ok(()) - } -} - -impl_display!(for MoveTo); -impl_display!(for MoveToColumn); -impl_display!(for MoveToRow); -impl_display!(for MoveToNextLine); -impl_display!(for MoveToPreviousLine); -impl_display!(for MoveUp); -impl_display!(for MoveDown); -impl_display!(for MoveLeft); -impl_display!(for MoveRight); -impl_display!(for SavePosition); -impl_display!(for RestorePosition); -impl_display!(for Hide); -impl_display!(for Show); -impl_display!(for EnableBlinking); -impl_display!(for DisableBlinking); -impl_display!(for SetCursorStyle); - -#[cfg(test)] -#[cfg(feature = "events")] -mod tests { - use std::io::{self, stdout}; - - use crate::execute; - - use super::{ - sys::position, MoveDown, MoveLeft, MoveRight, MoveTo, MoveUp, RestorePosition, SavePosition, - }; - - // Test is disabled, because it's failing on Travis - #[test] - #[ignore] - fn test_move_to() { - let (saved_x, saved_y) = position().unwrap(); - - execute!(stdout(), MoveTo(saved_x + 1, saved_y + 1)).unwrap(); - assert_eq!(position().unwrap(), (saved_x + 1, saved_y + 1)); - - execute!(stdout(), MoveTo(saved_x, saved_y)).unwrap(); - assert_eq!(position().unwrap(), (saved_x, saved_y)); - } - - // Test is disabled, because it's failing on Travis - #[test] - #[ignore] - fn test_move_right() { - let (saved_x, saved_y) = position().unwrap(); - execute!(io::stdout(), MoveRight(1)).unwrap(); - assert_eq!(position().unwrap(), (saved_x + 1, saved_y)); - } - - // Test is disabled, because it's failing on Travis - #[test] - #[ignore] - fn test_move_left() { - execute!(stdout(), MoveTo(2, 0), MoveLeft(2)).unwrap(); - assert_eq!(position().unwrap(), (0, 0)); - } - - // Test is disabled, because it's failing on Travis - #[test] - #[ignore] - fn test_move_up() { - execute!(stdout(), MoveTo(0, 2), MoveUp(2)).unwrap(); - assert_eq!(position().unwrap(), (0, 0)); - } - - // Test is disabled, because it's failing on Travis - #[test] - #[ignore] - fn test_move_down() { - execute!(stdout(), MoveTo(0, 0), MoveDown(2)).unwrap(); - - assert_eq!(position().unwrap(), (0, 2)); - } - - // Test is disabled, because it's failing on Travis - #[test] - #[ignore] - fn test_save_restore_position() { - let (saved_x, saved_y) = position().unwrap(); - - execute!( - stdout(), - SavePosition, - MoveTo(saved_x + 1, saved_y + 1), - RestorePosition - ) - .unwrap(); - - let (x, y) = position().unwrap(); - - assert_eq!(x, saved_x); - assert_eq!(y, saved_y); - } -} diff --git a/crates/util/keyfork-crossterm/src/cursor/sys.rs b/crates/util/keyfork-crossterm/src/cursor/sys.rs deleted file mode 100644 index f85a95c..0000000 --- a/crates/util/keyfork-crossterm/src/cursor/sys.rs +++ /dev/null @@ -1,19 +0,0 @@ -//! This module provides platform related functions. - -#[cfg(unix)] -#[cfg(feature = "events")] -pub use self::unix::position; -#[cfg(windows)] -pub use self::windows::position; -#[cfg(windows)] -pub(crate) use self::windows::{ - move_down, move_left, move_right, move_to, move_to_column, move_to_next_line, - move_to_previous_line, move_to_row, move_up, restore_position, save_position, show_cursor, -}; - -#[cfg(windows)] -pub(crate) mod windows; - -#[cfg(unix)] -#[cfg(feature = "events")] -pub(crate) mod unix; diff --git a/crates/util/keyfork-crossterm/src/cursor/sys/unix.rs b/crates/util/keyfork-crossterm/src/cursor/sys/unix.rs deleted file mode 100644 index 1220e2e..0000000 --- a/crates/util/keyfork-crossterm/src/cursor/sys/unix.rs +++ /dev/null @@ -1,56 +0,0 @@ -use std::{ - io::{self, Error, ErrorKind, Write}, - time::Duration, -}; - -use crate::{ - event::{filter::CursorPositionFilter, poll_internal, read_internal, InternalEvent}, - terminal::{disable_raw_mode, enable_raw_mode, sys::is_raw_mode_enabled}, -}; - -/// Returns the cursor position (column, row). -/// -/// The top left cell is represented as `(0, 0)`. -/// -/// On unix systems, this function will block and possibly time out while -/// [`crossterm::event::read`](crate::event::read()) or [`crossterm::event::poll`](crate::event::poll) are being called. -pub fn position() -> io::Result<(u16, u16)> { - if is_raw_mode_enabled() { - read_position_raw() - } else { - read_position() - } -} - -fn read_position() -> io::Result<(u16, u16)> { - enable_raw_mode()?; - let pos = read_position_raw(); - disable_raw_mode()?; - pos -} - -fn read_position_raw() -> io::Result<(u16, u16)> { - // Use `ESC [ 6 n` to and retrieve the cursor position. - let mut stdout = io::stdout(); - stdout.write_all(b"\x1B[6n")?; - stdout.flush()?; - - loop { - match poll_internal(Some(Duration::from_millis(2000)), &CursorPositionFilter) { - Ok(true) => { - if let Ok(InternalEvent::CursorPosition(x, y)) = - read_internal(&CursorPositionFilter) - { - return Ok((x, y)); - } - } - Ok(false) => { - return Err(Error::new( - ErrorKind::Other, - "The cursor position could not be read within a normal duration", - )); - } - Err(_) => {} - } - } -} diff --git a/crates/util/keyfork-crossterm/src/cursor/sys/windows.rs b/crates/util/keyfork-crossterm/src/cursor/sys/windows.rs deleted file mode 100644 index 84a4cd0..0000000 --- a/crates/util/keyfork-crossterm/src/cursor/sys/windows.rs +++ /dev/null @@ -1,341 +0,0 @@ -//! WinAPI related logic to cursor manipulation. - -use std::convert::TryFrom; -use std::io; -use std::sync::atomic::{AtomicU64, Ordering}; - -use crossterm_winapi::{result, Coord, Handle, HandleType, ScreenBuffer}; -use winapi::{ - shared::minwindef::{FALSE, TRUE}, - um::wincon::{SetConsoleCursorInfo, SetConsoleCursorPosition, CONSOLE_CURSOR_INFO, COORD}, -}; - -/// The position of the cursor, written when you save the cursor's position. -/// -/// This is `u64::MAX` initially. Otherwise, it stores the cursor's x position bit-shifted left 16 -/// times or-ed with the cursor's y position, where both are `i16`s. -static SAVED_CURSOR_POS: AtomicU64 = AtomicU64::new(u64::MAX); - -// The 'y' position of the cursor is not relative to the window but absolute to screen buffer. -// We can calculate the relative cursor position by subtracting the top position of the terminal window from the y position. -// This results in an 1-based coord zo subtract 1 to make cursor position 0-based. -pub fn parse_relative_y(y: i16) -> std::io::Result { - let window = ScreenBuffer::current()?.info()?; - - let window_size = window.terminal_window(); - let screen_size = window.terminal_size(); - - if y <= screen_size.height { - Ok(y) - } else { - Ok(y - window_size.top) - } -} - -/// Returns the cursor position (column, row). -/// -/// The top left cell is represented `0,0`. -pub fn position() -> io::Result<(u16, u16)> { - let cursor = ScreenBufferCursor::output()?; - let mut position = cursor.position()?; - // if position.y != 0 { - position.y = parse_relative_y(position.y)?; - // } - Ok(position.into()) -} - -pub(crate) fn show_cursor(show_cursor: bool) -> std::io::Result<()> { - ScreenBufferCursor::from(Handle::current_out_handle()?).set_visibility(show_cursor) -} - -pub(crate) fn move_to(column: u16, row: u16) -> std::io::Result<()> { - let cursor = ScreenBufferCursor::output()?; - cursor.move_to(column as i16, row as i16)?; - Ok(()) -} - -pub(crate) fn move_up(count: u16) -> std::io::Result<()> { - let (column, row) = position()?; - move_to(column, row - count)?; - Ok(()) -} - -pub(crate) fn move_right(count: u16) -> std::io::Result<()> { - let (column, row) = position()?; - move_to(column + count, row)?; - Ok(()) -} - -pub(crate) fn move_down(count: u16) -> std::io::Result<()> { - let (column, row) = position()?; - move_to(column, row + count)?; - Ok(()) -} - -pub(crate) fn move_left(count: u16) -> std::io::Result<()> { - let (column, row) = position()?; - move_to(column - count, row)?; - Ok(()) -} - -pub(crate) fn move_to_column(new_column: u16) -> std::io::Result<()> { - let (_, row) = position()?; - move_to(new_column, row)?; - Ok(()) -} - -pub(crate) fn move_to_row(new_row: u16) -> std::io::Result<()> { - let (col, _) = position()?; - move_to(col, new_row)?; - Ok(()) -} - -pub(crate) fn move_to_next_line(count: u16) -> std::io::Result<()> { - let (_, row) = position()?; - move_to(0, row + count)?; - Ok(()) -} - -pub(crate) fn move_to_previous_line(count: u16) -> std::io::Result<()> { - let (_, row) = position()?; - move_to(0, row - count)?; - Ok(()) -} - -pub(crate) fn save_position() -> std::io::Result<()> { - ScreenBufferCursor::output()?.save_position()?; - Ok(()) -} - -pub(crate) fn restore_position() -> std::io::Result<()> { - ScreenBufferCursor::output()?.restore_position()?; - Ok(()) -} - -/// WinAPI wrapper over terminal cursor behaviour. -struct ScreenBufferCursor { - screen_buffer: ScreenBuffer, -} - -impl ScreenBufferCursor { - fn output() -> std::io::Result { - Ok(ScreenBufferCursor { - screen_buffer: ScreenBuffer::from(Handle::new(HandleType::CurrentOutputHandle)?), - }) - } - - fn position(&self) -> std::io::Result { - Ok(self.screen_buffer.info()?.cursor_pos()) - } - - fn move_to(&self, x: i16, y: i16) -> std::io::Result<()> { - if x < 0 { - return Err(io::Error::new( - io::ErrorKind::Other, - format!("Argument Out of Range Exception when setting cursor position to X: {x}"), - )); - } - - if y < 0 { - return Err(io::Error::new( - io::ErrorKind::Other, - format!("Argument Out of Range Exception when setting cursor position to Y: {y}"), - )); - } - - let position = COORD { X: x, Y: y }; - - unsafe { - if result(SetConsoleCursorPosition( - **self.screen_buffer.handle(), - position, - )) - .is_err() - { - return Err(io::Error::last_os_error()); - } - } - Ok(()) - } - - fn set_visibility(&self, visible: bool) -> std::io::Result<()> { - let cursor_info = CONSOLE_CURSOR_INFO { - dwSize: 100, - bVisible: if visible { TRUE } else { FALSE }, - }; - - unsafe { - if result(SetConsoleCursorInfo( - **self.screen_buffer.handle(), - &cursor_info, - )) - .is_err() - { - return Err(io::Error::last_os_error()); - } - } - Ok(()) - } - - fn restore_position(&self) -> std::io::Result<()> { - if let Ok(val) = u32::try_from(SAVED_CURSOR_POS.load(Ordering::Relaxed)) { - let x = (val >> 16) as i16; - let y = val as i16; - self.move_to(x, y)?; - } - - Ok(()) - } - - fn save_position(&self) -> std::io::Result<()> { - let position = self.position()?; - - let bits = u64::from(u32::from(position.x as u16) << 16 | u32::from(position.y as u16)); - SAVED_CURSOR_POS.store(bits, Ordering::Relaxed); - - Ok(()) - } -} - -impl From for ScreenBufferCursor { - fn from(handle: Handle) -> Self { - ScreenBufferCursor { - screen_buffer: ScreenBuffer::from(handle), - } - } -} - -#[cfg(test)] -mod tests { - use super::{ - move_down, move_left, move_right, move_to, move_to_column, move_to_next_line, - move_to_previous_line, move_to_row, move_up, position, restore_position, save_position, - }; - use crate::terminal::sys::temp_screen_buffer; - use serial_test::serial; - - #[test] - #[serial] - fn test_move_to_winapi() { - let _test_screen = temp_screen_buffer().unwrap(); - - let (saved_x, saved_y) = position().unwrap(); - - move_to(saved_x + 1, saved_y + 1).unwrap(); - assert_eq!(position().unwrap(), (saved_x + 1, saved_y + 1)); - - move_to(saved_x, saved_y).unwrap(); - assert_eq!(position().unwrap(), (saved_x, saved_y)); - } - - #[test] - #[serial] - fn test_move_right_winapi() { - let _test_screen = temp_screen_buffer().unwrap(); - - let (saved_x, saved_y) = position().unwrap(); - move_right(1).unwrap(); - assert_eq!(position().unwrap(), (saved_x + 1, saved_y)); - } - - #[test] - #[serial] - fn test_move_left_winapi() { - let _test_screen = temp_screen_buffer().unwrap(); - - move_to(2, 0).unwrap(); - - move_left(2).unwrap(); - - assert_eq!(position().unwrap(), (0, 0)); - } - - #[test] - #[serial] - fn test_move_up_winapi() { - let _test_screen = temp_screen_buffer().unwrap(); - - move_to(0, 2).unwrap(); - - move_up(2).unwrap(); - - assert_eq!(position().unwrap(), (0, 0)); - } - - #[test] - #[serial] - fn test_move_to_next_line_winapi() { - let _test_screen = temp_screen_buffer().unwrap(); - - move_to(0, 2).unwrap(); - - move_to_next_line(2).unwrap(); - - assert_eq!(position().unwrap(), (0, 4)); - } - - #[test] - #[serial] - fn test_move_to_previous_line_winapi() { - let _test_screen = temp_screen_buffer().unwrap(); - - move_to(0, 2).unwrap(); - - move_to_previous_line(2).unwrap(); - - assert_eq!(position().unwrap(), (0, 0)); - } - - #[test] - #[serial] - fn test_move_to_column_winapi() { - let _test_screen = temp_screen_buffer().unwrap(); - - move_to(0, 2).unwrap(); - - move_to_column(12).unwrap(); - - assert_eq!(position().unwrap(), (12, 2)); - } - - #[test] - #[serial] - fn test_move_to_row_winapi() { - let _test_screen = temp_screen_buffer().unwrap(); - - move_to(0, 2).unwrap(); - - move_to_row(5).unwrap(); - - assert_eq!(position().unwrap(), (0, 5)); - } - - #[test] - #[serial] - fn test_move_down_winapi() { - let _test_screen = temp_screen_buffer().unwrap(); - - move_to(0, 0).unwrap(); - - move_down(2).unwrap(); - - assert_eq!(position().unwrap(), (0, 2)); - } - - #[test] - #[serial] - fn test_save_restore_position_winapi() { - let _test_screen = temp_screen_buffer().unwrap(); - - let (saved_x, saved_y) = position().unwrap(); - - save_position().unwrap(); - move_to(saved_x + 1, saved_y + 1).unwrap(); - restore_position().unwrap(); - - let (x, y) = position().unwrap(); - - assert_eq!(x, saved_x); - assert_eq!(y, saved_y); - } -} diff --git a/crates/util/keyfork-crossterm/src/event.rs b/crates/util/keyfork-crossterm/src/event.rs deleted file mode 100644 index f889cbf..0000000 --- a/crates/util/keyfork-crossterm/src/event.rs +++ /dev/null @@ -1,994 +0,0 @@ -//! # Event -//! -//! The `event` module provides the functionality to read keyboard, mouse and terminal resize events. -//! -//! * The [`read`](fn.read.html) function returns an [`Event`](enum.Event.html) immediately -//! (if available) or blocks until an [`Event`](enum.Event.html) is available. -//! -//! * The [`poll`](fn.poll.html) function allows you to check if there is or isn't an [`Event`](enum.Event.html) available -//! within the given period of time. In other words - if subsequent call to the [`read`](fn.read.html) -//! function will block or not. -//! -//! It's **not allowed** to call these functions from different threads or combine them with the -//! [`EventStream`](struct.EventStream.html). You're allowed to either: -//! -//! * use the [`read`](fn.read.html) & [`poll`](fn.poll.html) functions on any, but same, thread -//! * or the [`EventStream`](struct.EventStream.html). -//! -//! **Make sure to enable [raw mode](../terminal/index.html#raw-mode) in order for keyboard events to work properly** -//! -//! ## Mouse Events -//! -//! Mouse events are not enabled by default. You have to enable them with the -//! [`EnableMouseCapture`](struct.EnableMouseCapture.html) command. See [Command API](../index.html#command-api) -//! for more information. -//! -//! ## Examples -//! -//! Blocking read: -//! -//! ```no_run -//! use keyfork_crossterm::event::{read, Event}; -//! -//! fn print_events() -> std::io::Result<()> { -//! loop { -//! // `read()` blocks until an `Event` is available -//! match read()? { -//! Event::FocusGained => println!("FocusGained"), -//! Event::FocusLost => println!("FocusLost"), -//! Event::Key(event) => println!("{:?}", event), -//! Event::Mouse(event) => println!("{:?}", event), -//! #[cfg(feature = "bracketed-paste")] -//! Event::Paste(data) => println!("{:?}", data), -//! Event::Resize(width, height) => println!("New size {}x{}", width, height), -//! } -//! } -//! Ok(()) -//! } -//! ``` -//! -//! Non-blocking read: -//! -//! ```no_run -//! use std::{time::Duration, io}; -//! -//! use keyfork_crossterm::event::{poll, read, Event}; -//! -//! fn print_events() -> io::Result<()> { -//! loop { -//! // `poll()` waits for an `Event` for a given time period -//! if poll(Duration::from_millis(500))? { -//! // It's guaranteed that the `read()` won't block when the `poll()` -//! // function returns `true` -//! match read()? { -//! Event::FocusGained => println!("FocusGained"), -//! Event::FocusLost => println!("FocusLost"), -//! Event::Key(event) => println!("{:?}", event), -//! Event::Mouse(event) => println!("{:?}", event), -//! #[cfg(feature = "bracketed-paste")] -//! Event::Paste(data) => println!("Pasted {:?}", data), -//! Event::Resize(width, height) => println!("New size {}x{}", width, height), -//! } -//! } else { -//! // Timeout expired and no `Event` is available -//! } -//! } -//! Ok(()) -//! } -//! ``` -//! -//! Check the [examples](https://github.com/crossterm-rs/crossterm/tree/master/examples) folder for more of -//! them (`event-*`). - -pub(crate) mod filter; -pub(crate) mod read; -pub(crate) mod source; -#[cfg(feature = "event-stream")] -pub(crate) mod stream; -pub(crate) mod sys; -pub(crate) mod timeout; - -#[cfg(feature = "event-stream")] -pub use stream::EventStream; - -use crate::event::{ - filter::{EventFilter, Filter}, - read::InternalEventReader, - timeout::PollTimeout, -}; -use crate::{csi, Command}; -use parking_lot::{MappedMutexGuard, Mutex, MutexGuard}; -use std::fmt; -use std::time::Duration; - -use bitflags::bitflags; -use std::hash::{Hash, Hasher}; - -/// Static instance of `InternalEventReader`. -/// This needs to be static because there can be one event reader. -static INTERNAL_EVENT_READER: Mutex> = parking_lot::const_mutex(None); - -pub(crate) fn lock_internal_event_reader() -> MappedMutexGuard<'static, InternalEventReader> { - MutexGuard::map(INTERNAL_EVENT_READER.lock(), |reader| { - reader.get_or_insert_with(InternalEventReader::default) - }) -} -fn try_lock_internal_event_reader_for( - duration: Duration, -) -> Option> { - Some(MutexGuard::map( - INTERNAL_EVENT_READER.try_lock_for(duration)?, - |reader| reader.get_or_insert_with(InternalEventReader::default), - )) -} - -/// Checks if there is an [`Event`](enum.Event.html) available. -/// -/// Returns `Ok(true)` if an [`Event`](enum.Event.html) is available otherwise it returns `Ok(false)`. -/// -/// `Ok(true)` guarantees that subsequent call to the [`read`](fn.read.html) function -/// won't block. -/// -/// # Arguments -/// -/// * `timeout` - maximum waiting time for event availability -/// -/// # Examples -/// -/// Return immediately: -/// -/// ```no_run -/// use std::{time::Duration, io}; -/// use keyfork_crossterm::{event::poll}; -/// -/// fn is_event_available() -> io::Result { -/// // Zero duration says that the `poll` function must return immediately -/// // with an `Event` availability information -/// poll(Duration::from_secs(0)) -/// } -/// ``` -/// -/// Wait up to 100ms: -/// -/// ```no_run -/// use std::{time::Duration, io}; -/// -/// use keyfork_crossterm::event::poll; -/// -/// fn is_event_available() -> io::Result { -/// // Wait for an `Event` availability for 100ms. It returns immediately -/// // if an `Event` is/becomes available. -/// poll(Duration::from_millis(100)) -/// } -/// ``` -pub fn poll(timeout: Duration) -> std::io::Result { - poll_internal(Some(timeout), &EventFilter) -} - -/// Reads a single [`Event`](enum.Event.html). -/// -/// This function blocks until an [`Event`](enum.Event.html) is available. Combine it with the -/// [`poll`](fn.poll.html) function to get non-blocking reads. -/// -/// # Examples -/// -/// Blocking read: -/// -/// ```no_run -/// use keyfork_crossterm::event::read; -/// use std::io; -/// -/// fn print_events() -> io::Result { -/// loop { -/// // Blocks until an `Event` is available -/// println!("{:?}", read()?); -/// } -/// } -/// ``` -/// -/// Non-blocking read: -/// -/// ```no_run -/// use std::time::Duration; -/// use std::io; -/// -/// use keyfork_crossterm::event::{read, poll}; -/// -/// fn print_events() -> io::Result { -/// loop { -/// if poll(Duration::from_millis(100))? { -/// // It's guaranteed that `read` won't block, because `poll` returned -/// // `Ok(true)`. -/// println!("{:?}", read()?); -/// } else { -/// // Timeout expired, no `Event` is available -/// } -/// } -/// } -/// ``` -pub fn read() -> std::io::Result { - match read_internal(&EventFilter)? { - InternalEvent::Event(event) => Ok(event), - #[cfg(unix)] - _ => unreachable!(), - } -} - -/// Polls to check if there are any `InternalEvent`s that can be read within the given duration. -pub(crate) fn poll_internal(timeout: Option, filter: &F) -> std::io::Result -where - F: Filter, -{ - let (mut reader, timeout) = if let Some(timeout) = timeout { - let poll_timeout = PollTimeout::new(Some(timeout)); - if let Some(reader) = try_lock_internal_event_reader_for(timeout) { - (reader, poll_timeout.leftover()) - } else { - return Ok(false); - } - } else { - (lock_internal_event_reader(), None) - }; - reader.poll(timeout, filter) -} - -/// Reads a single `InternalEvent`. -pub(crate) fn read_internal(filter: &F) -> std::io::Result -where - F: Filter, -{ - let mut reader = lock_internal_event_reader(); - reader.read(filter) -} - -bitflags! { - /// Represents special flags that tell compatible terminals to add extra information to keyboard events. - /// - /// See for more information. - /// - /// Alternate keys and Unicode codepoints are not yet supported by crossterm. - #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize), serde(transparent))] - #[derive(Debug, PartialOrd, PartialEq, Eq, Clone, Copy, Hash)] - pub struct KeyboardEnhancementFlags: u8 { - /// Represent Escape and modified keys using CSI-u sequences, so they can be unambiguously - /// read. - const DISAMBIGUATE_ESCAPE_CODES = 0b0000_0001; - /// Add extra events with [`KeyEvent.kind`] set to [`KeyEventKind::Repeat`] or - /// [`KeyEventKind::Release`] when keys are autorepeated or released. - const REPORT_EVENT_TYPES = 0b0000_0010; - // Send [alternate keycodes](https://sw.kovidgoyal.net/kitty/keyboard-protocol/#key-codes) - // in addition to the base keycode. The alternate keycode overrides the base keycode in - // resulting `KeyEvent`s. - const REPORT_ALTERNATE_KEYS = 0b0000_0100; - /// Represent all keyboard events as CSI-u sequences. This is required to get repeat/release - /// events for plain-text keys. - const REPORT_ALL_KEYS_AS_ESCAPE_CODES = 0b0000_1000; - // Send the Unicode codepoint as well as the keycode. - // - // *Note*: this is not yet supported by crossterm. - // const REPORT_ASSOCIATED_TEXT = 0b0001_0000; - } -} - -/// A command that enables mouse event capturing. -/// -/// Mouse events can be captured with [read](./fn.read.html)/[poll](./fn.poll.html). -#[cfg(feature = "events")] -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct EnableMouseCapture; - -#[cfg(feature = "events")] -impl Command for EnableMouseCapture { - fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result { - f.write_str(concat!( - // Normal tracking: Send mouse X & Y on button press and release - csi!("?1000h"), - // Button-event tracking: Report button motion events (dragging) - csi!("?1002h"), - // Any-event tracking: Report all motion events - csi!("?1003h"), - // RXVT mouse mode: Allows mouse coordinates of >223 - csi!("?1015h"), - // SGR mouse mode: Allows mouse coordinates of >223, preferred over RXVT mode - csi!("?1006h"), - )) - } - - #[cfg(windows)] - fn execute_winapi(&self) -> std::io::Result<()> { - sys::windows::enable_mouse_capture() - } - - #[cfg(windows)] - fn is_ansi_code_supported(&self) -> bool { - false - } -} - -/// A command that disables mouse event capturing. -/// -/// Mouse events can be captured with [read](./fn.read.html)/[poll](./fn.poll.html). -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct DisableMouseCapture; - -impl Command for DisableMouseCapture { - fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result { - f.write_str(concat!( - // The inverse commands of EnableMouseCapture, in reverse order. - csi!("?1006l"), - csi!("?1015l"), - csi!("?1003l"), - csi!("?1002l"), - csi!("?1000l"), - )) - } - - #[cfg(windows)] - fn execute_winapi(&self) -> std::io::Result<()> { - sys::windows::disable_mouse_capture() - } - - #[cfg(windows)] - fn is_ansi_code_supported(&self) -> bool { - false - } -} - -/// A command that enables focus event emission. -/// -/// It should be paired with [`DisableFocusChange`] at the end of execution. -/// -/// Focus events can be captured with [read](./fn.read.html)/[poll](./fn.poll.html). -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct EnableFocusChange; - -impl Command for EnableFocusChange { - fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result { - f.write_str(csi!("?1004h")) - } - - #[cfg(windows)] - fn execute_winapi(&self) -> std::io::Result<()> { - // Focus events are always enabled on Windows - Ok(()) - } -} - -/// A command that disables focus event emission. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct DisableFocusChange; - -impl Command for DisableFocusChange { - fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result { - f.write_str(csi!("?1004l")) - } - - #[cfg(windows)] - fn execute_winapi(&self) -> std::io::Result<()> { - // Focus events can't be disabled on Windows - Ok(()) - } -} - -/// A command that enables [bracketed paste mode](https://en.wikipedia.org/wiki/Bracketed-paste). -/// -/// It should be paired with [`DisableBracketedPaste`] at the end of execution. -/// -/// This is not supported in older Windows terminals without -/// [virtual terminal sequences](https://docs.microsoft.com/en-us/windows/console/console-virtual-terminal-sequences). -#[cfg(feature = "bracketed-paste")] -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct EnableBracketedPaste; - -#[cfg(feature = "bracketed-paste")] -impl Command for EnableBracketedPaste { - fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result { - f.write_str(csi!("?2004h")) - } - - #[cfg(windows)] - fn execute_winapi(&self) -> std::io::Result<()> { - Err(std::io::Error::new( - std::io::ErrorKind::Unsupported, - "Bracketed paste not implemented in the legacy Windows API.", - )) - } -} - -/// A command that disables bracketed paste mode. -#[cfg(feature = "bracketed-paste")] -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct DisableBracketedPaste; - -#[cfg(feature = "bracketed-paste")] -impl Command for DisableBracketedPaste { - fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result { - f.write_str(csi!("?2004l")) - } - - #[cfg(windows)] - fn execute_winapi(&self) -> std::io::Result<()> { - Ok(()) - } -} - -/// A command that enables the [kitty keyboard protocol](https://sw.kovidgoyal.net/kitty/keyboard-protocol/), which adds extra information to keyboard events and removes ambiguity for modifier keys. -/// -/// It should be paired with [`PopKeyboardEnhancementFlags`] at the end of execution. -/// -/// Example usage: -/// ```no_run -/// use std::io::{Write, stdout}; -/// use keyfork_crossterm::execute; -/// use keyfork_crossterm::event::{ -/// KeyboardEnhancementFlags, -/// PushKeyboardEnhancementFlags, -/// PopKeyboardEnhancementFlags -/// }; -/// -/// let mut stdout = stdout(); -/// -/// execute!( -/// stdout, -/// PushKeyboardEnhancementFlags( -/// KeyboardEnhancementFlags::DISAMBIGUATE_ESCAPE_CODES -/// ) -/// ); -/// -/// // ... -/// -/// execute!(stdout, PopKeyboardEnhancementFlags); -/// ``` -/// -/// Note that, currently, only the following support this protocol: -/// * [kitty terminal](https://sw.kovidgoyal.net/kitty/) -/// * [foot terminal](https://codeberg.org/dnkl/foot/issues/319) -/// * [WezTerm terminal](https://wezfurlong.org/wezterm/config/lua/config/enable_kitty_keyboard.html) -/// * [notcurses library](https://github.com/dankamongmen/notcurses/issues/2131) -/// * [neovim text editor](https://github.com/neovim/neovim/pull/18181) -/// * [kakoune text editor](https://github.com/mawww/kakoune/issues/4103) -/// * [dte text editor](https://gitlab.com/craigbarnes/dte/-/issues/138) -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct PushKeyboardEnhancementFlags(pub KeyboardEnhancementFlags); - -impl Command for PushKeyboardEnhancementFlags { - fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result { - write!(f, "{}{}u", csi!(">"), self.0.bits()) - } - - #[cfg(windows)] - fn execute_winapi(&self) -> std::io::Result<()> { - use std::io; - - Err(io::Error::new( - io::ErrorKind::Unsupported, - "Keyboard progressive enhancement not implemented for the legacy Windows API.", - )) - } - - #[cfg(windows)] - fn is_ansi_code_supported(&self) -> bool { - false - } -} - -/// A command that disables extra kinds of keyboard events. -/// -/// Specifically, it pops one level of keyboard enhancement flags. -/// -/// See [`PushKeyboardEnhancementFlags`] and for more information. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct PopKeyboardEnhancementFlags; - -impl Command for PopKeyboardEnhancementFlags { - fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result { - f.write_str(csi!("<1u")) - } - - #[cfg(windows)] - fn execute_winapi(&self) -> std::io::Result<()> { - use std::io; - - Err(io::Error::new( - io::ErrorKind::Unsupported, - "Keyboard progressive enhancement not implemented for the legacy Windows API.", - )) - } - - #[cfg(windows)] - fn is_ansi_code_supported(&self) -> bool { - false - } -} - -/// Represents an event. -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -#[cfg_attr(not(feature = "bracketed-paste"), derive(Copy))] -#[derive(Debug, PartialOrd, PartialEq, Eq, Clone, Hash)] -pub enum Event { - /// The terminal gained focus - FocusGained, - /// The terminal lost focus - FocusLost, - /// A single key event with additional pressed modifiers. - Key(KeyEvent), - /// A single mouse event with additional pressed modifiers. - Mouse(MouseEvent), - /// A string that was pasted into the terminal. Only emitted if bracketed paste has been - /// enabled. - #[cfg(feature = "bracketed-paste")] - Paste(String), - /// An resize event with new dimensions after resize (columns, rows). - /// **Note** that resize events can occur in batches. - Resize(u16, u16), -} - -/// Represents a mouse event. -/// -/// # Platform-specific Notes -/// -/// ## Mouse Buttons -/// -/// Some platforms/terminals do not report mouse button for the -/// `MouseEventKind::Up` and `MouseEventKind::Drag` events. `MouseButton::Left` -/// is returned if we don't know which button was used. -/// -/// ## Key Modifiers -/// -/// Some platforms/terminals does not report all key modifiers -/// combinations for all mouse event types. For example - macOS reports -/// `Ctrl` + left mouse button click as a right mouse button click. -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -#[derive(Debug, PartialOrd, PartialEq, Eq, Clone, Copy, Hash)] -pub struct MouseEvent { - /// The kind of mouse event that was caused. - pub kind: MouseEventKind, - /// The column that the event occurred on. - pub column: u16, - /// The row that the event occurred on. - pub row: u16, - /// The key modifiers active when the event occurred. - pub modifiers: KeyModifiers, -} - -/// A mouse event kind. -/// -/// # Platform-specific Notes -/// -/// ## Mouse Buttons -/// -/// Some platforms/terminals do not report mouse button for the -/// `MouseEventKind::Up` and `MouseEventKind::Drag` events. `MouseButton::Left` -/// is returned if we don't know which button was used. -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -#[derive(Debug, PartialOrd, PartialEq, Eq, Clone, Copy, Hash)] -pub enum MouseEventKind { - /// Pressed mouse button. Contains the button that was pressed. - Down(MouseButton), - /// Released mouse button. Contains the button that was released. - Up(MouseButton), - /// Moved the mouse cursor while pressing the contained mouse button. - Drag(MouseButton), - /// Moved the mouse cursor while not pressing a mouse button. - Moved, - /// Scrolled mouse wheel downwards (towards the user). - ScrollDown, - /// Scrolled mouse wheel upwards (away from the user). - ScrollUp, - /// Scrolled mouse wheel left (mostly on a laptop touchpad). - ScrollLeft, - /// Scrolled mouse wheel right (mostly on a laptop touchpad). - ScrollRight, -} - -/// Represents a mouse button. -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -#[derive(Debug, PartialOrd, PartialEq, Eq, Clone, Copy, Hash)] -pub enum MouseButton { - /// Left mouse button. - Left, - /// Right mouse button. - Right, - /// Middle mouse button. - Middle, -} - -bitflags! { - /// Represents key modifiers (shift, control, alt, etc.). - /// - /// **Note:** `SUPER`, `HYPER`, and `META` can only be read if - /// [`KeyboardEnhancementFlags::DISAMBIGUATE_ESCAPE_CODES`] has been enabled with - /// [`PushKeyboardEnhancementFlags`]. - #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize), serde(transparent))] - #[derive(Debug, PartialOrd, PartialEq, Eq, Clone, Copy, Hash)] - pub struct KeyModifiers: u8 { - const SHIFT = 0b0000_0001; - const CONTROL = 0b0000_0010; - const ALT = 0b0000_0100; - const SUPER = 0b0000_1000; - const HYPER = 0b0001_0000; - const META = 0b0010_0000; - const NONE = 0b0000_0000; - } -} - -/// Represents a keyboard event kind. -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -#[derive(Debug, PartialOrd, PartialEq, Eq, Clone, Copy, Hash)] -pub enum KeyEventKind { - Press, - Repeat, - Release, -} - -bitflags! { - /// Represents extra state about the key event. - /// - /// **Note:** This state can only be read if - /// [`KeyboardEnhancementFlags::DISAMBIGUATE_ESCAPE_CODES`] has been enabled with - /// [`PushKeyboardEnhancementFlags`]. - #[derive(Debug, PartialOrd, PartialEq, Eq, Clone, Copy, Hash)] - #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize), serde(transparent))] - pub struct KeyEventState: u8 { - /// The key event origins from the keypad. - const KEYPAD = 0b0000_0001; - /// Caps Lock was enabled for this key event. - /// - /// **Note:** this is set for the initial press of Caps Lock itself. - const CAPS_LOCK = 0b0000_1000; - /// Num Lock was enabled for this key event. - /// - /// **Note:** this is set for the initial press of Num Lock itself. - const NUM_LOCK = 0b0000_1000; - const NONE = 0b0000_0000; - } -} - -/// Represents a key event. -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -#[derive(Debug, PartialOrd, Clone, Copy)] -pub struct KeyEvent { - /// The key itself. - pub code: KeyCode, - /// Additional key modifiers. - pub modifiers: KeyModifiers, - /// Kind of event. - /// - /// Only set if: - /// - Unix: [`KeyboardEnhancementFlags::REPORT_EVENT_TYPES`] has been enabled with [`PushKeyboardEnhancementFlags`]. - /// - Windows: always - pub kind: KeyEventKind, - /// Keyboard state. - /// - /// Only set if [`KeyboardEnhancementFlags::DISAMBIGUATE_ESCAPE_CODES`] has been enabled with - /// [`PushKeyboardEnhancementFlags`]. - pub state: KeyEventState, -} - -impl KeyEvent { - pub const fn new(code: KeyCode, modifiers: KeyModifiers) -> KeyEvent { - KeyEvent { - code, - modifiers, - kind: KeyEventKind::Press, - state: KeyEventState::empty(), - } - } - - pub const fn new_with_kind( - code: KeyCode, - modifiers: KeyModifiers, - kind: KeyEventKind, - ) -> KeyEvent { - KeyEvent { - code, - modifiers, - kind, - state: KeyEventState::empty(), - } - } - - pub const fn new_with_kind_and_state( - code: KeyCode, - modifiers: KeyModifiers, - kind: KeyEventKind, - state: KeyEventState, - ) -> KeyEvent { - KeyEvent { - code, - modifiers, - kind, - state, - } - } - - // modifies the KeyEvent, - // so that KeyModifiers::SHIFT is present iff - // an uppercase char is present. - fn normalize_case(mut self) -> KeyEvent { - let c = match self.code { - KeyCode::Char(c) => c, - _ => return self, - }; - - if c.is_ascii_uppercase() { - self.modifiers.insert(KeyModifiers::SHIFT); - } else if self.modifiers.contains(KeyModifiers::SHIFT) { - self.code = KeyCode::Char(c.to_ascii_uppercase()) - } - self - } -} - -impl From for KeyEvent { - fn from(code: KeyCode) -> Self { - KeyEvent { - code, - modifiers: KeyModifiers::empty(), - kind: KeyEventKind::Press, - state: KeyEventState::empty(), - } - } -} - -impl PartialEq for KeyEvent { - fn eq(&self, other: &KeyEvent) -> bool { - let KeyEvent { - code: lhs_code, - modifiers: lhs_modifiers, - kind: lhs_kind, - state: lhs_state, - } = self.normalize_case(); - let KeyEvent { - code: rhs_code, - modifiers: rhs_modifiers, - kind: rhs_kind, - state: rhs_state, - } = other.normalize_case(); - (lhs_code == rhs_code) - && (lhs_modifiers == rhs_modifiers) - && (lhs_kind == rhs_kind) - && (lhs_state == rhs_state) - } -} - -impl Eq for KeyEvent {} - -impl Hash for KeyEvent { - fn hash(&self, hash_state: &mut H) { - let KeyEvent { - code, - modifiers, - kind, - state, - } = self.normalize_case(); - code.hash(hash_state); - modifiers.hash(hash_state); - kind.hash(hash_state); - state.hash(hash_state); - } -} - -/// Represents a media key (as part of [`KeyCode::Media`]). -#[derive(Debug, PartialOrd, PartialEq, Eq, Clone, Copy, Hash)] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -pub enum MediaKeyCode { - /// Play media key. - Play, - /// Pause media key. - Pause, - /// Play/Pause media key. - PlayPause, - /// Reverse media key. - Reverse, - /// Stop media key. - Stop, - /// Fast-forward media key. - FastForward, - /// Rewind media key. - Rewind, - /// Next-track media key. - TrackNext, - /// Previous-track media key. - TrackPrevious, - /// Record media key. - Record, - /// Lower-volume media key. - LowerVolume, - /// Raise-volume media key. - RaiseVolume, - /// Mute media key. - MuteVolume, -} - -/// Represents a modifier key (as part of [`KeyCode::Modifier`]). -#[derive(Debug, PartialOrd, PartialEq, Eq, Clone, Copy, Hash)] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -pub enum ModifierKeyCode { - /// Left Shift key. - LeftShift, - /// Left Control key. - LeftControl, - /// Left Alt key. - LeftAlt, - /// Left Super key. - LeftSuper, - /// Left Hyper key. - LeftHyper, - /// Left Meta key. - LeftMeta, - /// Right Shift key. - RightShift, - /// Right Control key. - RightControl, - /// Right Alt key. - RightAlt, - /// Right Super key. - RightSuper, - /// Right Hyper key. - RightHyper, - /// Right Meta key. - RightMeta, - /// Iso Level3 Shift key. - IsoLevel3Shift, - /// Iso Level5 Shift key. - IsoLevel5Shift, -} - -/// Represents a key. -#[derive(Debug, PartialOrd, PartialEq, Eq, Clone, Copy, Hash)] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -pub enum KeyCode { - /// Backspace key. - Backspace, - /// Enter key. - Enter, - /// Left arrow key. - Left, - /// Right arrow key. - Right, - /// Up arrow key. - Up, - /// Down arrow key. - Down, - /// Home key. - Home, - /// End key. - End, - /// Page up key. - PageUp, - /// Page down key. - PageDown, - /// Tab key. - Tab, - /// Shift + Tab key. - BackTab, - /// Delete key. - Delete, - /// Insert key. - Insert, - /// F key. - /// - /// `KeyCode::F(1)` represents F1 key, etc. - F(u8), - /// A character. - /// - /// `KeyCode::Char('c')` represents `c` character, etc. - Char(char), - /// Null. - Null, - /// Escape key. - Esc, - /// Caps Lock key. - /// - /// **Note:** this key can only be read if - /// [`KeyboardEnhancementFlags::DISAMBIGUATE_ESCAPE_CODES`] has been enabled with - /// [`PushKeyboardEnhancementFlags`]. - CapsLock, - /// Scroll Lock key. - /// - /// **Note:** this key can only be read if - /// [`KeyboardEnhancementFlags::DISAMBIGUATE_ESCAPE_CODES`] has been enabled with - /// [`PushKeyboardEnhancementFlags`]. - ScrollLock, - /// Num Lock key. - /// - /// **Note:** this key can only be read if - /// [`KeyboardEnhancementFlags::DISAMBIGUATE_ESCAPE_CODES`] has been enabled with - /// [`PushKeyboardEnhancementFlags`]. - NumLock, - /// Print Screen key. - /// - /// **Note:** this key can only be read if - /// [`KeyboardEnhancementFlags::DISAMBIGUATE_ESCAPE_CODES`] has been enabled with - /// [`PushKeyboardEnhancementFlags`]. - PrintScreen, - /// Pause key. - /// - /// **Note:** this key can only be read if - /// [`KeyboardEnhancementFlags::DISAMBIGUATE_ESCAPE_CODES`] has been enabled with - /// [`PushKeyboardEnhancementFlags`]. - Pause, - /// Menu key. - /// - /// **Note:** this key can only be read if - /// [`KeyboardEnhancementFlags::DISAMBIGUATE_ESCAPE_CODES`] has been enabled with - /// [`PushKeyboardEnhancementFlags`]. - Menu, - /// The "Begin" key (often mapped to the 5 key when Num Lock is turned on). - /// - /// **Note:** this key can only be read if - /// [`KeyboardEnhancementFlags::DISAMBIGUATE_ESCAPE_CODES`] has been enabled with - /// [`PushKeyboardEnhancementFlags`]. - KeypadBegin, - /// A media key. - /// - /// **Note:** these keys can only be read if - /// [`KeyboardEnhancementFlags::DISAMBIGUATE_ESCAPE_CODES`] has been enabled with - /// [`PushKeyboardEnhancementFlags`]. - Media(MediaKeyCode), - /// A modifier key. - /// - /// **Note:** these keys can only be read if **both** - /// [`KeyboardEnhancementFlags::DISAMBIGUATE_ESCAPE_CODES`] and - /// [`KeyboardEnhancementFlags::REPORT_ALL_KEYS_AS_ESCAPE_CODES`] have been enabled with - /// [`PushKeyboardEnhancementFlags`]. - Modifier(ModifierKeyCode), -} - -/// An internal event. -/// -/// Encapsulates publicly available `Event` with additional internal -/// events that shouldn't be publicly available to the crate users. -#[derive(Debug, PartialOrd, PartialEq, Hash, Clone, Eq)] -pub(crate) enum InternalEvent { - /// An event. - Event(Event), - /// A cursor position (`col`, `row`). - #[cfg(unix)] - CursorPosition(u16, u16), - /// The progressive keyboard enhancement flags enabled by the terminal. - #[cfg(unix)] - KeyboardEnhancementFlags(KeyboardEnhancementFlags), - /// Attributes and architectural class of the terminal. - #[cfg(unix)] - PrimaryDeviceAttributes, -} - -#[cfg(test)] -mod tests { - use std::collections::hash_map::DefaultHasher; - use std::hash::{Hash, Hasher}; - - use super::{KeyCode, KeyEvent, KeyModifiers}; - - #[test] - fn test_equality() { - let lowercase_d_with_shift = KeyEvent::new(KeyCode::Char('d'), KeyModifiers::SHIFT); - let uppercase_d_with_shift = KeyEvent::new(KeyCode::Char('D'), KeyModifiers::SHIFT); - let uppercase_d = KeyEvent::new(KeyCode::Char('D'), KeyModifiers::NONE); - assert_eq!(lowercase_d_with_shift, uppercase_d_with_shift); - assert_eq!(uppercase_d, uppercase_d_with_shift); - } - - #[test] - fn test_hash() { - let lowercase_d_with_shift_hash = { - let mut hasher = DefaultHasher::new(); - KeyEvent::new(KeyCode::Char('d'), KeyModifiers::SHIFT).hash(&mut hasher); - hasher.finish() - }; - let uppercase_d_with_shift_hash = { - let mut hasher = DefaultHasher::new(); - KeyEvent::new(KeyCode::Char('D'), KeyModifiers::SHIFT).hash(&mut hasher); - hasher.finish() - }; - let uppercase_d_hash = { - let mut hasher = DefaultHasher::new(); - KeyEvent::new(KeyCode::Char('D'), KeyModifiers::NONE).hash(&mut hasher); - hasher.finish() - }; - assert_eq!(lowercase_d_with_shift_hash, uppercase_d_with_shift_hash); - assert_eq!(uppercase_d_hash, uppercase_d_with_shift_hash); - } -} diff --git a/crates/util/keyfork-crossterm/src/event/filter.rs b/crates/util/keyfork-crossterm/src/event/filter.rs deleted file mode 100644 index 2a594ae..0000000 --- a/crates/util/keyfork-crossterm/src/event/filter.rs +++ /dev/null @@ -1,116 +0,0 @@ -use crate::event::InternalEvent; - -/// Interface for filtering an `InternalEvent`. -pub(crate) trait Filter: Send + Sync + 'static { - /// Returns whether the given event fulfills the filter. - fn eval(&self, event: &InternalEvent) -> bool; -} - -#[cfg(unix)] -#[derive(Debug, Clone)] -pub(crate) struct CursorPositionFilter; - -#[cfg(unix)] -impl Filter for CursorPositionFilter { - fn eval(&self, event: &InternalEvent) -> bool { - matches!(*event, InternalEvent::CursorPosition(_, _)) - } -} - -#[cfg(unix)] -#[derive(Debug, Clone)] -pub(crate) struct KeyboardEnhancementFlagsFilter; - -#[cfg(unix)] -impl Filter for KeyboardEnhancementFlagsFilter { - fn eval(&self, event: &InternalEvent) -> bool { - // This filter checks for either a KeyboardEnhancementFlags response or - // a PrimaryDeviceAttributes response. If we receive the PrimaryDeviceAttributes - // response but not KeyboardEnhancementFlags, the terminal does not support - // progressive keyboard enhancement. - matches!( - *event, - InternalEvent::KeyboardEnhancementFlags(_) | InternalEvent::PrimaryDeviceAttributes - ) - } -} - -#[cfg(unix)] -#[derive(Debug, Clone)] -pub(crate) struct PrimaryDeviceAttributesFilter; - -#[cfg(unix)] -impl Filter for PrimaryDeviceAttributesFilter { - fn eval(&self, event: &InternalEvent) -> bool { - matches!(*event, InternalEvent::PrimaryDeviceAttributes) - } -} - -#[derive(Debug, Clone)] -pub(crate) struct EventFilter; - -impl Filter for EventFilter { - #[cfg(unix)] - fn eval(&self, event: &InternalEvent) -> bool { - matches!(*event, InternalEvent::Event(_)) - } - - #[cfg(windows)] - fn eval(&self, _: &InternalEvent) -> bool { - true - } -} - -#[allow(dead_code)] -#[derive(Debug, Clone)] -pub(crate) struct InternalEventFilter; - -impl Filter for InternalEventFilter { - fn eval(&self, _: &InternalEvent) -> bool { - true - } -} - -#[cfg(test)] -#[cfg(unix)] -mod tests { - use super::{ - super::Event, CursorPositionFilter, EventFilter, Filter, InternalEvent, - InternalEventFilter, KeyboardEnhancementFlagsFilter, PrimaryDeviceAttributesFilter, - }; - - #[test] - fn test_cursor_position_filter_filters_cursor_position() { - assert!(!CursorPositionFilter.eval(&InternalEvent::Event(Event::Resize(10, 10)))); - assert!(CursorPositionFilter.eval(&InternalEvent::CursorPosition(0, 0))); - } - - #[test] - fn test_keyboard_enhancement_status_filter_filters_keyboard_enhancement_status() { - assert!(!KeyboardEnhancementFlagsFilter.eval(&InternalEvent::Event(Event::Resize(10, 10)))); - assert!( - KeyboardEnhancementFlagsFilter.eval(&InternalEvent::KeyboardEnhancementFlags( - crate::event::KeyboardEnhancementFlags::DISAMBIGUATE_ESCAPE_CODES - )) - ); - assert!(KeyboardEnhancementFlagsFilter.eval(&InternalEvent::PrimaryDeviceAttributes)); - } - - #[test] - fn test_primary_device_attributes_filter_filters_primary_device_attributes() { - assert!(!PrimaryDeviceAttributesFilter.eval(&InternalEvent::Event(Event::Resize(10, 10)))); - assert!(PrimaryDeviceAttributesFilter.eval(&InternalEvent::PrimaryDeviceAttributes)); - } - - #[test] - fn test_event_filter_filters_events() { - assert!(EventFilter.eval(&InternalEvent::Event(Event::Resize(10, 10)))); - assert!(!EventFilter.eval(&InternalEvent::CursorPosition(0, 0))); - } - - #[test] - fn test_event_filter_filters_internal_events() { - assert!(InternalEventFilter.eval(&InternalEvent::Event(Event::Resize(10, 10)))); - assert!(InternalEventFilter.eval(&InternalEvent::CursorPosition(0, 0))); - } -} diff --git a/crates/util/keyfork-crossterm/src/event/read.rs b/crates/util/keyfork-crossterm/src/event/read.rs deleted file mode 100644 index 6ddbace..0000000 --- a/crates/util/keyfork-crossterm/src/event/read.rs +++ /dev/null @@ -1,430 +0,0 @@ -use std::{collections::vec_deque::VecDeque, io, time::Duration}; - -#[cfg(unix)] -use crate::event::source::unix::UnixInternalEventSource; -#[cfg(windows)] -use crate::event::source::windows::WindowsEventSource; -#[cfg(feature = "event-stream")] -use crate::event::sys::Waker; -use crate::event::{filter::Filter, source::EventSource, timeout::PollTimeout, InternalEvent}; - -/// Can be used to read `InternalEvent`s. -pub(crate) struct InternalEventReader { - events: VecDeque, - source: Option>, - skipped_events: Vec, -} - -impl Default for InternalEventReader { - fn default() -> Self { - #[cfg(windows)] - let source = WindowsEventSource::new(); - #[cfg(unix)] - let source = UnixInternalEventSource::new(); - - let source = source.ok().map(|x| Box::new(x) as Box); - - InternalEventReader { - source, - events: VecDeque::with_capacity(32), - skipped_events: Vec::with_capacity(32), - } - } -} - -impl InternalEventReader { - /// Returns a `Waker` allowing to wake/force the `poll` method to return `Ok(false)`. - #[cfg(feature = "event-stream")] - pub(crate) fn waker(&self) -> Waker { - self.source.as_ref().expect("reader source not set").waker() - } - - pub(crate) fn poll(&mut self, timeout: Option, filter: &F) -> io::Result - where - F: Filter, - { - for event in &self.events { - if filter.eval(event) { - return Ok(true); - } - } - - let event_source = match self.source.as_mut() { - Some(source) => source, - None => { - return Err(std::io::Error::new( - std::io::ErrorKind::Other, - "Failed to initialize input reader", - )) - } - }; - - let poll_timeout = PollTimeout::new(timeout); - - loop { - let maybe_event = match event_source.try_read(poll_timeout.leftover()) { - Ok(None) => None, - Ok(Some(event)) => { - if filter.eval(&event) { - Some(event) - } else { - self.skipped_events.push(event); - None - } - } - Err(e) => { - if e.kind() == io::ErrorKind::Interrupted { - return Ok(false); - } - - return Err(e); - } - }; - - if poll_timeout.elapsed() || maybe_event.is_some() { - self.events.extend(self.skipped_events.drain(..)); - - if let Some(event) = maybe_event { - self.events.push_front(event); - return Ok(true); - } - - return Ok(false); - } - } - } - - pub(crate) fn read(&mut self, filter: &F) -> io::Result - where - F: Filter, - { - let mut skipped_events = VecDeque::new(); - - loop { - while let Some(event) = self.events.pop_front() { - if filter.eval(&event) { - while let Some(event) = skipped_events.pop_front() { - self.events.push_back(event); - } - - return Ok(event); - } else { - // We can not directly write events back to `self.events`. - // If we did, we would put our self's into an endless loop - // that would enqueue -> dequeue -> enqueue etc. - // This happens because `poll` in this function will always return true if there are events in it's. - // And because we just put the non-fulfilling event there this is going to be the case. - // Instead we can store them into the temporary buffer, - // and then when the filter is fulfilled write all events back in order. - skipped_events.push_back(event); - } - } - - let _ = self.poll(None, filter)?; - } - } -} - -#[cfg(test)] -mod tests { - use std::io; - use std::{collections::VecDeque, time::Duration}; - - #[cfg(unix)] - use super::super::filter::CursorPositionFilter; - use super::{ - super::{filter::InternalEventFilter, Event}, - EventSource, InternalEvent, InternalEventReader, - }; - - #[test] - fn test_poll_fails_without_event_source() { - let mut reader = InternalEventReader { - events: VecDeque::new(), - source: None, - skipped_events: Vec::with_capacity(32), - }; - - assert!(reader.poll(None, &InternalEventFilter).is_err()); - assert!(reader - .poll(Some(Duration::from_secs(0)), &InternalEventFilter) - .is_err()); - assert!(reader - .poll(Some(Duration::from_secs(10)), &InternalEventFilter) - .is_err()); - } - - #[test] - fn test_poll_returns_true_for_matching_event_in_queue_at_front() { - let mut reader = InternalEventReader { - events: vec![InternalEvent::Event(Event::Resize(10, 10))].into(), - source: None, - skipped_events: Vec::with_capacity(32), - }; - - assert!(reader.poll(None, &InternalEventFilter).unwrap()); - } - - #[test] - #[cfg(unix)] - fn test_poll_returns_true_for_matching_event_in_queue_at_back() { - let mut reader = InternalEventReader { - events: vec![ - InternalEvent::Event(Event::Resize(10, 10)), - InternalEvent::CursorPosition(10, 20), - ] - .into(), - source: None, - skipped_events: Vec::with_capacity(32), - }; - - assert!(reader.poll(None, &CursorPositionFilter).unwrap()); - } - - #[test] - fn test_read_returns_matching_event_in_queue_at_front() { - const EVENT: InternalEvent = InternalEvent::Event(Event::Resize(10, 10)); - - let mut reader = InternalEventReader { - events: vec![EVENT].into(), - source: None, - skipped_events: Vec::with_capacity(32), - }; - - assert_eq!(reader.read(&InternalEventFilter).unwrap(), EVENT); - } - - #[test] - #[cfg(unix)] - fn test_read_returns_matching_event_in_queue_at_back() { - const CURSOR_EVENT: InternalEvent = InternalEvent::CursorPosition(10, 20); - - let mut reader = InternalEventReader { - events: vec![InternalEvent::Event(Event::Resize(10, 10)), CURSOR_EVENT].into(), - source: None, - skipped_events: Vec::with_capacity(32), - }; - - assert_eq!(reader.read(&CursorPositionFilter).unwrap(), CURSOR_EVENT); - } - - #[test] - #[cfg(unix)] - fn test_read_does_not_consume_skipped_event() { - const SKIPPED_EVENT: InternalEvent = InternalEvent::Event(Event::Resize(10, 10)); - const CURSOR_EVENT: InternalEvent = InternalEvent::CursorPosition(10, 20); - - let mut reader = InternalEventReader { - events: vec![SKIPPED_EVENT, CURSOR_EVENT].into(), - source: None, - skipped_events: Vec::with_capacity(32), - }; - - assert_eq!(reader.read(&CursorPositionFilter).unwrap(), CURSOR_EVENT); - assert_eq!(reader.read(&InternalEventFilter).unwrap(), SKIPPED_EVENT); - } - - #[test] - fn test_poll_timeouts_if_source_has_no_events() { - let source = FakeSource::default(); - - let mut reader = InternalEventReader { - events: VecDeque::new(), - source: Some(Box::new(source)), - skipped_events: Vec::with_capacity(32), - }; - - assert!(!reader - .poll(Some(Duration::from_secs(0)), &InternalEventFilter) - .unwrap()); - } - - #[test] - fn test_poll_returns_true_if_source_has_at_least_one_event() { - let source = FakeSource::with_events(&[InternalEvent::Event(Event::Resize(10, 10))]); - - let mut reader = InternalEventReader { - events: VecDeque::new(), - source: Some(Box::new(source)), - skipped_events: Vec::with_capacity(32), - }; - - assert!(reader.poll(None, &InternalEventFilter).unwrap()); - assert!(reader - .poll(Some(Duration::from_secs(0)), &InternalEventFilter) - .unwrap()); - } - - #[test] - fn test_reads_returns_event_if_source_has_at_least_one_event() { - const EVENT: InternalEvent = InternalEvent::Event(Event::Resize(10, 10)); - - let source = FakeSource::with_events(&[EVENT]); - - let mut reader = InternalEventReader { - events: VecDeque::new(), - source: Some(Box::new(source)), - skipped_events: Vec::with_capacity(32), - }; - - assert_eq!(reader.read(&InternalEventFilter).unwrap(), EVENT); - } - - #[test] - fn test_read_returns_events_if_source_has_events() { - const EVENT: InternalEvent = InternalEvent::Event(Event::Resize(10, 10)); - - let source = FakeSource::with_events(&[EVENT, EVENT, EVENT]); - - let mut reader = InternalEventReader { - events: VecDeque::new(), - source: Some(Box::new(source)), - skipped_events: Vec::with_capacity(32), - }; - - assert_eq!(reader.read(&InternalEventFilter).unwrap(), EVENT); - assert_eq!(reader.read(&InternalEventFilter).unwrap(), EVENT); - assert_eq!(reader.read(&InternalEventFilter).unwrap(), EVENT); - } - - #[test] - fn test_poll_returns_false_after_all_source_events_are_consumed() { - const EVENT: InternalEvent = InternalEvent::Event(Event::Resize(10, 10)); - - let source = FakeSource::with_events(&[EVENT, EVENT, EVENT]); - - let mut reader = InternalEventReader { - events: VecDeque::new(), - source: Some(Box::new(source)), - skipped_events: Vec::with_capacity(32), - }; - - assert_eq!(reader.read(&InternalEventFilter).unwrap(), EVENT); - assert_eq!(reader.read(&InternalEventFilter).unwrap(), EVENT); - assert_eq!(reader.read(&InternalEventFilter).unwrap(), EVENT); - assert!(!reader - .poll(Some(Duration::from_secs(0)), &InternalEventFilter) - .unwrap()); - } - - #[test] - fn test_poll_propagates_error() { - let mut reader = InternalEventReader { - events: VecDeque::new(), - source: Some(Box::new(FakeSource::new(&[]))), - skipped_events: Vec::with_capacity(32), - }; - - assert_eq!( - reader - .poll(Some(Duration::from_secs(0)), &InternalEventFilter) - .err() - .map(|e| format!("{:?}", &e.kind())), - Some(format!("{:?}", io::ErrorKind::Other)) - ); - } - - #[test] - fn test_read_propagates_error() { - let mut reader = InternalEventReader { - events: VecDeque::new(), - source: Some(Box::new(FakeSource::new(&[]))), - skipped_events: Vec::with_capacity(32), - }; - - assert_eq!( - reader - .read(&InternalEventFilter) - .err() - .map(|e| format!("{:?}", &e.kind())), - Some(format!("{:?}", io::ErrorKind::Other)) - ); - } - - #[test] - fn test_poll_continues_after_error() { - const EVENT: InternalEvent = InternalEvent::Event(Event::Resize(10, 10)); - - let source = FakeSource::new(&[EVENT, EVENT]); - - let mut reader = InternalEventReader { - events: VecDeque::new(), - source: Some(Box::new(source)), - skipped_events: Vec::with_capacity(32), - }; - - assert_eq!(reader.read(&InternalEventFilter).unwrap(), EVENT); - assert!(reader.read(&InternalEventFilter).is_err()); - assert!(reader - .poll(Some(Duration::from_secs(0)), &InternalEventFilter) - .unwrap()); - } - - #[test] - fn test_read_continues_after_error() { - const EVENT: InternalEvent = InternalEvent::Event(Event::Resize(10, 10)); - - let source = FakeSource::new(&[EVENT, EVENT]); - - let mut reader = InternalEventReader { - events: VecDeque::new(), - source: Some(Box::new(source)), - skipped_events: Vec::with_capacity(32), - }; - - assert_eq!(reader.read(&InternalEventFilter).unwrap(), EVENT); - assert!(reader.read(&InternalEventFilter).is_err()); - assert_eq!(reader.read(&InternalEventFilter).unwrap(), EVENT); - } - - #[derive(Default)] - struct FakeSource { - events: VecDeque, - error: Option, - } - - impl FakeSource { - fn new(events: &[InternalEvent]) -> FakeSource { - FakeSource { - events: events.to_vec().into(), - error: Some(io::Error::new(io::ErrorKind::Other, "")), - } - } - - fn with_events(events: &[InternalEvent]) -> FakeSource { - FakeSource { - events: events.to_vec().into(), - error: None, - } - } - } - - impl EventSource for FakeSource { - fn try_read(&mut self, _timeout: Option) -> io::Result> { - // Return error if set in case there's just one remaining event - if self.events.len() == 1 { - if let Some(error) = self.error.take() { - return Err(error); - } - } - - // Return all events from the queue - if let Some(event) = self.events.pop_front() { - return Ok(Some(event)); - } - - // Return error if there're no more events - if let Some(error) = self.error.take() { - return Err(error); - } - - // Timeout - Ok(None) - } - - #[cfg(feature = "event-stream")] - fn waker(&self) -> super::super::sys::Waker { - unimplemented!(); - } - } -} diff --git a/crates/util/keyfork-crossterm/src/event/source.rs b/crates/util/keyfork-crossterm/src/event/source.rs deleted file mode 100644 index ae0f05a..0000000 --- a/crates/util/keyfork-crossterm/src/event/source.rs +++ /dev/null @@ -1,27 +0,0 @@ -use std::{io, time::Duration}; - -#[cfg(feature = "event-stream")] -use super::sys::Waker; -use super::InternalEvent; - -#[cfg(unix)] -pub(crate) mod unix; -#[cfg(windows)] -pub(crate) mod windows; - -/// An interface for trying to read an `InternalEvent` within an optional `Duration`. -pub(crate) trait EventSource: Sync + Send { - /// Tries to read an `InternalEvent` within the given duration. - /// - /// # Arguments - /// - /// * `timeout` - `None` block indefinitely until an event is available, `Some(duration)` blocks - /// for the given timeout - /// - /// Returns `Ok(None)` if there's no event available and timeout expires. - fn try_read(&mut self, timeout: Option) -> io::Result>; - - /// Returns a `Waker` allowing to wake/force the `try_read` method to return `Ok(None)`. - #[cfg(feature = "event-stream")] - fn waker(&self) -> Waker; -} diff --git a/crates/util/keyfork-crossterm/src/event/source/unix.rs b/crates/util/keyfork-crossterm/src/event/source/unix.rs deleted file mode 100644 index 810bad3..0000000 --- a/crates/util/keyfork-crossterm/src/event/source/unix.rs +++ /dev/null @@ -1,11 +0,0 @@ -#[cfg(feature = "use-dev-tty")] -pub(crate) mod tty; - -#[cfg(not(feature = "use-dev-tty"))] -pub(crate) mod mio; - -#[cfg(feature = "use-dev-tty")] -pub(crate) use self::tty::UnixInternalEventSource; - -#[cfg(not(feature = "use-dev-tty"))] -pub(crate) use self::mio::UnixInternalEventSource; diff --git a/crates/util/keyfork-crossterm/src/event/source/unix/mio.rs b/crates/util/keyfork-crossterm/src/event/source/unix/mio.rs deleted file mode 100644 index 7458ba6..0000000 --- a/crates/util/keyfork-crossterm/src/event/source/unix/mio.rs +++ /dev/null @@ -1,234 +0,0 @@ -use std::{collections::VecDeque, io, time::Duration}; - -use mio::{unix::SourceFd, Events, Interest, Poll, Token}; -use signal_hook_mio::v1_0::Signals; - -#[cfg(feature = "event-stream")] -use crate::event::sys::Waker; -use crate::event::{ - source::EventSource, sys::unix::parse::parse_event, timeout::PollTimeout, Event, InternalEvent, -}; -use crate::terminal::sys::file_descriptor::{tty_fd, FileDesc}; - -// Tokens to identify file descriptor -const TTY_TOKEN: Token = Token(0); -const SIGNAL_TOKEN: Token = Token(1); -#[cfg(feature = "event-stream")] -const WAKE_TOKEN: Token = Token(2); - -// I (@zrzka) wasn't able to read more than 1_022 bytes when testing -// reading on macOS/Linux -> we don't need bigger buffer and 1k of bytes -// is enough. -const TTY_BUFFER_SIZE: usize = 1_024; - -pub(crate) struct UnixInternalEventSource { - poll: Poll, - events: Events, - parser: Parser, - tty_buffer: [u8; TTY_BUFFER_SIZE], - tty_fd: FileDesc, - signals: Signals, - #[cfg(feature = "event-stream")] - waker: Waker, -} - -impl UnixInternalEventSource { - pub fn new() -> io::Result { - UnixInternalEventSource::from_file_descriptor(tty_fd()?) - } - - pub(crate) fn from_file_descriptor(input_fd: FileDesc) -> io::Result { - let poll = Poll::new()?; - let registry = poll.registry(); - - let tty_raw_fd = input_fd.raw_fd(); - let mut tty_ev = SourceFd(&tty_raw_fd); - registry.register(&mut tty_ev, TTY_TOKEN, Interest::READABLE)?; - - let mut signals = Signals::new([signal_hook::consts::SIGWINCH])?; - registry.register(&mut signals, SIGNAL_TOKEN, Interest::READABLE)?; - - #[cfg(feature = "event-stream")] - let waker = Waker::new(registry, WAKE_TOKEN)?; - - Ok(UnixInternalEventSource { - poll, - events: Events::with_capacity(3), - parser: Parser::default(), - tty_buffer: [0u8; TTY_BUFFER_SIZE], - tty_fd: input_fd, - signals, - #[cfg(feature = "event-stream")] - waker, - }) - } -} - -impl EventSource for UnixInternalEventSource { - fn try_read(&mut self, timeout: Option) -> io::Result> { - if let Some(event) = self.parser.next() { - return Ok(Some(event)); - } - - let timeout = PollTimeout::new(timeout); - - loop { - if let Err(e) = self.poll.poll(&mut self.events, timeout.leftover()) { - // Mio will throw an interrupted error in case of cursor position retrieval. We need to retry until it succeeds. - // Previous versions of Mio (< 0.7) would automatically retry the poll call if it was interrupted (if EINTR was returned). - // https://docs.rs/mio/0.7.0/mio/struct.Poll.html#notes - if e.kind() == io::ErrorKind::Interrupted { - continue; - } else { - return Err(e); - } - }; - - if self.events.is_empty() { - // No readiness events = timeout - return Ok(None); - } - - for token in self.events.iter().map(|x| x.token()) { - match token { - TTY_TOKEN => { - loop { - match self.tty_fd.read(&mut self.tty_buffer, TTY_BUFFER_SIZE) { - Ok(read_count) => { - if read_count > 0 { - self.parser.advance( - &self.tty_buffer[..read_count], - read_count == TTY_BUFFER_SIZE, - ); - } - } - Err(e) => { - // No more data to read at the moment. We will receive another event - if e.kind() == io::ErrorKind::WouldBlock { - break; - } - // once more data is available to read. - else if e.kind() == io::ErrorKind::Interrupted { - continue; - } - } - }; - - if let Some(event) = self.parser.next() { - return Ok(Some(event)); - } - } - } - SIGNAL_TOKEN => { - for signal in self.signals.pending() { - match signal { - signal_hook::consts::SIGWINCH => { - // TODO Should we remove tput? - // - // This can take a really long time, because terminal::size can - // launch new process (tput) and then it parses its output. It's - // not a really long time from the absolute time point of view, but - // it's a really long time from the mio, async-std/tokio executor, ... - // point of view. - let new_size = crate::terminal::size()?; - return Ok(Some(InternalEvent::Event(Event::Resize( - new_size.0, new_size.1, - )))); - } - _ => unreachable!("Synchronize signal registration & handling"), - }; - } - } - #[cfg(feature = "event-stream")] - WAKE_TOKEN => { - return Err(std::io::Error::new( - std::io::ErrorKind::Interrupted, - "Poll operation was woken up by `Waker::wake`", - )); - } - _ => unreachable!("Synchronize Evented handle registration & token handling"), - } - } - - // Processing above can take some time, check if timeout expired - if timeout.elapsed() { - return Ok(None); - } - } - } - - #[cfg(feature = "event-stream")] - fn waker(&self) -> Waker { - self.waker.clone() - } -} - -// -// Following `Parser` structure exists for two reasons: -// -// * mimic anes Parser interface -// * move the advancing, parsing, ... stuff out of the `try_read` method -// -#[derive(Debug)] -struct Parser { - buffer: Vec, - internal_events: VecDeque, -} - -impl Default for Parser { - fn default() -> Self { - Parser { - // This buffer is used for -> 1 <- ANSI escape sequence. Are we - // aware of any ANSI escape sequence that is bigger? Can we make - // it smaller? - // - // Probably not worth spending more time on this as "there's a plan" - // to use the anes crate parser. - buffer: Vec::with_capacity(256), - // TTY_BUFFER_SIZE is 1_024 bytes. How many ANSI escape sequences can - // fit? What is an average sequence length? Let's guess here - // and say that the average ANSI escape sequence length is 8 bytes. Thus - // the buffer size should be 1024/8=128 to avoid additional allocations - // when processing large amounts of data. - // - // There's no need to make it bigger, because when you look at the `try_read` - // method implementation, all events are consumed before the next TTY_BUFFER - // is processed -> events pushed. - internal_events: VecDeque::with_capacity(128), - } - } -} - -impl Parser { - fn advance(&mut self, buffer: &[u8], more: bool) { - for (idx, byte) in buffer.iter().enumerate() { - let more = idx + 1 < buffer.len() || more; - - self.buffer.push(*byte); - - match parse_event(&self.buffer, more) { - Ok(Some(ie)) => { - self.internal_events.push_back(ie); - self.buffer.clear(); - } - Ok(None) => { - // Event can't be parsed, because we don't have enough bytes for - // the current sequence. Keep the buffer and process next bytes. - } - Err(_) => { - // Event can't be parsed (not enough parameters, parameter is not a number, ...). - // Clear the buffer and continue with another sequence. - self.buffer.clear(); - } - } - } - } -} - -impl Iterator for Parser { - type Item = InternalEvent; - - fn next(&mut self) -> Option { - self.internal_events.pop_front() - } -} diff --git a/crates/util/keyfork-crossterm/src/event/source/unix/tty.rs b/crates/util/keyfork-crossterm/src/event/source/unix/tty.rs deleted file mode 100644 index 320a12c..0000000 --- a/crates/util/keyfork-crossterm/src/event/source/unix/tty.rs +++ /dev/null @@ -1,265 +0,0 @@ -use std::os::unix::prelude::AsRawFd; -use std::{collections::VecDeque, io, os::unix::net::UnixStream, time::Duration}; - -use signal_hook::low_level::pipe; - -use crate::event::timeout::PollTimeout; -use crate::event::Event; -use filedescriptor::{poll, pollfd, POLLIN}; - -#[cfg(feature = "event-stream")] -use crate::event::sys::Waker; -use crate::event::{source::EventSource, sys::unix::parse::parse_event, InternalEvent}; -use crate::terminal::sys::file_descriptor::{tty_fd, FileDesc}; - -/// Holds a prototypical Waker and a receiver we can wait on when doing select(). -#[cfg(feature = "event-stream")] -struct WakePipe { - receiver: UnixStream, - waker: Waker, -} - -#[cfg(feature = "event-stream")] -impl WakePipe { - fn new() -> io::Result { - let (receiver, sender) = nonblocking_unix_pair()?; - Ok(WakePipe { - receiver, - waker: Waker::new(sender), - }) - } -} - -// I (@zrzka) wasn't able to read more than 1_022 bytes when testing -// reading on macOS/Linux -> we don't need bigger buffer and 1k of bytes -// is enough. -const TTY_BUFFER_SIZE: usize = 1_024; - -pub(crate) struct UnixInternalEventSource { - parser: Parser, - tty_buffer: [u8; TTY_BUFFER_SIZE], - tty: FileDesc, - winch_signal_receiver: UnixStream, - #[cfg(feature = "event-stream")] - wake_pipe: WakePipe, -} - -fn nonblocking_unix_pair() -> io::Result<(UnixStream, UnixStream)> { - let (receiver, sender) = UnixStream::pair()?; - receiver.set_nonblocking(true)?; - sender.set_nonblocking(true)?; - Ok((receiver, sender)) -} - -impl UnixInternalEventSource { - pub fn new() -> io::Result { - UnixInternalEventSource::from_file_descriptor(tty_fd()?) - } - - pub(crate) fn from_file_descriptor(input_fd: FileDesc) -> io::Result { - Ok(UnixInternalEventSource { - parser: Parser::default(), - tty_buffer: [0u8; TTY_BUFFER_SIZE], - tty: input_fd, - winch_signal_receiver: { - let (receiver, sender) = nonblocking_unix_pair()?; - // Unregistering is unnecessary because EventSource is a singleton - pipe::register(libc::SIGWINCH, sender)?; - receiver - }, - #[cfg(feature = "event-stream")] - wake_pipe: WakePipe::new()?, - }) - } -} - -/// read_complete reads from a non-blocking file descriptor -/// until the buffer is full or it would block. -/// -/// Similar to `std::io::Read::read_to_end`, except this function -/// only fills the given buffer and does not read beyond that. -fn read_complete(fd: &FileDesc, buf: &mut [u8]) -> io::Result { - loop { - match fd.read(buf, buf.len()) { - Ok(x) => return Ok(x), - Err(e) => match e.kind() { - io::ErrorKind::WouldBlock => return Ok(0), - io::ErrorKind::Interrupted => continue, - _ => return Err(e), - }, - } - } -} - -impl EventSource for UnixInternalEventSource { - fn try_read(&mut self, timeout: Option) -> io::Result> { - let timeout = PollTimeout::new(timeout); - - fn make_pollfd(fd: &F) -> pollfd { - pollfd { - fd: fd.as_raw_fd(), - events: POLLIN, - revents: 0, - } - } - - #[cfg(not(feature = "event-stream"))] - let mut fds = [ - make_pollfd(&self.tty), - make_pollfd(&self.winch_signal_receiver), - ]; - - #[cfg(feature = "event-stream")] - let mut fds = [ - make_pollfd(&self.tty), - make_pollfd(&self.winch_signal_receiver), - make_pollfd(&self.wake_pipe.receiver), - ]; - - while timeout.leftover().map_or(true, |t| !t.is_zero()) { - // check if there are buffered events from the last read - if let Some(event) = self.parser.next() { - return Ok(Some(event)); - } - match poll(&mut fds, timeout.leftover()) { - Err(filedescriptor::Error::Poll(e)) | Err(filedescriptor::Error::Io(e)) => { - match e.kind() { - // retry on EINTR - io::ErrorKind::Interrupted => continue, - _ => return Err(e), - } - } - Err(e) => { - return Err(std::io::Error::new( - std::io::ErrorKind::Other, - format!("got unexpected error while polling: {:?}", e), - )) - } - Ok(_) => (), - }; - if fds[0].revents & POLLIN != 0 { - loop { - let read_count = read_complete(&self.tty, &mut self.tty_buffer)?; - if read_count > 0 { - self.parser.advance( - &self.tty_buffer[..read_count], - read_count == TTY_BUFFER_SIZE, - ); - } - - if let Some(event) = self.parser.next() { - return Ok(Some(event)); - } - - if read_count == 0 { - break; - } - } - } - if fds[1].revents & POLLIN != 0 { - let fd = FileDesc::new(self.winch_signal_receiver.as_raw_fd(), false); - // drain the pipe - while read_complete(&fd, &mut [0; 1024])? != 0 {} - // TODO Should we remove tput? - // - // This can take a really long time, because terminal::size can - // launch new process (tput) and then it parses its output. It's - // not a really long time from the absolute time point of view, but - // it's a really long time from the mio, async-std/tokio executor, ... - // point of view. - let new_size = crate::terminal::size()?; - return Ok(Some(InternalEvent::Event(Event::Resize( - new_size.0, new_size.1, - )))); - } - - #[cfg(feature = "event-stream")] - if fds[2].revents & POLLIN != 0 { - let fd = FileDesc::new(self.wake_pipe.receiver.as_raw_fd(), false); - // drain the pipe - while read_complete(&fd, &mut [0; 1024])? != 0 {} - - return Err(std::io::Error::new( - std::io::ErrorKind::Interrupted, - "Poll operation was woken up by `Waker::wake`", - )); - } - } - Ok(None) - } - - #[cfg(feature = "event-stream")] - fn waker(&self) -> Waker { - self.wake_pipe.waker.clone() - } -} - -// -// Following `Parser` structure exists for two reasons: -// -// * mimic anes Parser interface -// * move the advancing, parsing, ... stuff out of the `try_read` method -// -#[derive(Debug)] -struct Parser { - buffer: Vec, - internal_events: VecDeque, -} - -impl Default for Parser { - fn default() -> Self { - Parser { - // This buffer is used for -> 1 <- ANSI escape sequence. Are we - // aware of any ANSI escape sequence that is bigger? Can we make - // it smaller? - // - // Probably not worth spending more time on this as "there's a plan" - // to use the anes crate parser. - buffer: Vec::with_capacity(256), - // TTY_BUFFER_SIZE is 1_024 bytes. How many ANSI escape sequences can - // fit? What is an average sequence length? Let's guess here - // and say that the average ANSI escape sequence length is 8 bytes. Thus - // the buffer size should be 1024/8=128 to avoid additional allocations - // when processing large amounts of data. - // - // There's no need to make it bigger, because when you look at the `try_read` - // method implementation, all events are consumed before the next TTY_BUFFER - // is processed -> events pushed. - internal_events: VecDeque::with_capacity(128), - } - } -} - -impl Parser { - fn advance(&mut self, buffer: &[u8], more: bool) { - for (idx, byte) in buffer.iter().enumerate() { - let more = idx + 1 < buffer.len() || more; - - self.buffer.push(*byte); - - match parse_event(&self.buffer, more) { - Ok(Some(ie)) => { - self.internal_events.push_back(ie); - self.buffer.clear(); - } - Ok(None) => { - // Event can't be parsed, because we don't have enough bytes for - // the current sequence. Keep the buffer and process next bytes. - } - Err(_) => { - // Event can't be parsed (not enough parameters, parameter is not a number, ...). - // Clear the buffer and continue with another sequence. - self.buffer.clear(); - } - } - } - } -} - -impl Iterator for Parser { - type Item = InternalEvent; - - fn next(&mut self) -> Option { - self.internal_events.pop_front() - } -} diff --git a/crates/util/keyfork-crossterm/src/event/source/windows.rs b/crates/util/keyfork-crossterm/src/event/source/windows.rs deleted file mode 100644 index 33ecffe..0000000 --- a/crates/util/keyfork-crossterm/src/event/source/windows.rs +++ /dev/null @@ -1,100 +0,0 @@ -use std::time::Duration; - -use crossterm_winapi::{Console, Handle, InputRecord}; - -use crate::event::{ - sys::windows::{parse::MouseButtonsPressed, poll::WinApiPoll}, - Event, -}; - -#[cfg(feature = "event-stream")] -use crate::event::sys::Waker; -use crate::event::{ - source::EventSource, - sys::windows::parse::{handle_key_event, handle_mouse_event}, - timeout::PollTimeout, - InternalEvent, -}; - -pub(crate) struct WindowsEventSource { - console: Console, - poll: WinApiPoll, - surrogate_buffer: Option, - mouse_buttons_pressed: MouseButtonsPressed, -} - -impl WindowsEventSource { - pub(crate) fn new() -> std::io::Result { - let console = Console::from(Handle::current_in_handle()?); - Ok(WindowsEventSource { - console, - - #[cfg(not(feature = "event-stream"))] - poll: WinApiPoll::new(), - #[cfg(feature = "event-stream")] - poll: WinApiPoll::new()?, - - surrogate_buffer: None, - mouse_buttons_pressed: MouseButtonsPressed::default(), - }) - } -} - -impl EventSource for WindowsEventSource { - fn try_read(&mut self, timeout: Option) -> std::io::Result> { - let poll_timeout = PollTimeout::new(timeout); - - loop { - if let Some(event_ready) = self.poll.poll(poll_timeout.leftover())? { - let number = self.console.number_of_console_input_events()?; - if event_ready && number != 0 { - let event = match self.console.read_single_input_event()? { - InputRecord::KeyEvent(record) => { - handle_key_event(record, &mut self.surrogate_buffer) - } - InputRecord::MouseEvent(record) => { - let mouse_event = - handle_mouse_event(record, &self.mouse_buttons_pressed); - self.mouse_buttons_pressed = MouseButtonsPressed { - left: record.button_state.left_button(), - right: record.button_state.right_button(), - middle: record.button_state.middle_button(), - }; - - mouse_event - } - InputRecord::WindowBufferSizeEvent(record) => { - // windows starts counting at 0, unix at 1, add one to replicate unix behaviour. - Some(Event::Resize( - record.size.x as u16 + 1, - record.size.y as u16 + 1, - )) - } - InputRecord::FocusEvent(record) => { - let event = if record.set_focus { - Event::FocusGained - } else { - Event::FocusLost - }; - Some(event) - } - _ => None, - }; - - if let Some(event) = event { - return Ok(Some(InternalEvent::Event(event))); - } - } - } - - if poll_timeout.elapsed() { - return Ok(None); - } - } - } - - #[cfg(feature = "event-stream")] - fn waker(&self) -> Waker { - self.poll.waker() - } -} diff --git a/crates/util/keyfork-crossterm/src/event/stream.rs b/crates/util/keyfork-crossterm/src/event/stream.rs deleted file mode 100644 index e74dcea..0000000 --- a/crates/util/keyfork-crossterm/src/event/stream.rs +++ /dev/null @@ -1,146 +0,0 @@ -use std::{ - io, - pin::Pin, - sync::{ - atomic::{AtomicBool, Ordering}, - mpsc::{self, SyncSender}, - Arc, - }, - task::{Context, Poll}, - thread, - time::Duration, -}; - -use futures_core::stream::Stream; - -use crate::event::{ - filter::EventFilter, lock_internal_event_reader, poll_internal, read_internal, sys::Waker, - Event, InternalEvent, -}; - -/// A stream of `Result`. -/// -/// **This type is not available by default. You have to use the `event-stream` feature flag -/// to make it available.** -/// -/// It implements the [Stream](futures_core::stream::Stream) -/// trait and allows you to receive [`Event`]s with [`async-std`](https://crates.io/crates/async-std) -/// or [`tokio`](https://crates.io/crates/tokio) crates. -/// -/// Check the [examples](https://github.com/crossterm-rs/crossterm/tree/master/examples) folder to see how to use -/// it (`event-stream-*`). -#[derive(Debug)] -pub struct EventStream { - poll_internal_waker: Waker, - stream_wake_task_executed: Arc, - stream_wake_task_should_shutdown: Arc, - task_sender: SyncSender, -} - -impl Default for EventStream { - fn default() -> Self { - let (task_sender, receiver) = mpsc::sync_channel::(1); - - thread::spawn(move || { - while let Ok(task) = receiver.recv() { - loop { - if let Ok(true) = poll_internal(None, &EventFilter) { - break; - } - - if task.stream_wake_task_should_shutdown.load(Ordering::SeqCst) { - break; - } - } - task.stream_wake_task_executed - .store(false, Ordering::SeqCst); - task.stream_waker.wake(); - } - }); - - EventStream { - poll_internal_waker: lock_internal_event_reader().waker(), - stream_wake_task_executed: Arc::new(AtomicBool::new(false)), - stream_wake_task_should_shutdown: Arc::new(AtomicBool::new(false)), - task_sender, - } - } -} - -impl EventStream { - /// Constructs a new instance of `EventStream`. - pub fn new() -> EventStream { - EventStream::default() - } -} - -struct Task { - stream_waker: std::task::Waker, - stream_wake_task_executed: Arc, - stream_wake_task_should_shutdown: Arc, -} - -// Note to future me -// -// We need two wakers in order to implement EventStream correctly. -// -// 1. futures::Stream waker -// -// Stream::poll_next can return Poll::Pending which means that there's no -// event available. We are going to spawn a thread with the -// poll_internal(None, &EventFilter) call. This call blocks until an -// event is available and then we have to wake up the executor with notification -// that the task can be resumed. -// -// 2. poll_internal waker -// -// There's no event available, Poll::Pending was returned, stream waker thread -// is up and sitting in the poll_internal. User wants to drop the EventStream. -// We have to wake up the poll_internal (force it to return Ok(false)) and quit -// the thread before we drop. -impl Stream for EventStream { - type Item = io::Result; - - fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { - let result = match poll_internal(Some(Duration::from_secs(0)), &EventFilter) { - Ok(true) => match read_internal(&EventFilter) { - Ok(InternalEvent::Event(event)) => Poll::Ready(Some(Ok(event))), - Err(e) => Poll::Ready(Some(Err(e))), - #[cfg(unix)] - _ => unreachable!(), - }, - Ok(false) => { - if !self - .stream_wake_task_executed - .compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst) - // https://github.com/rust-lang/rust/issues/80486#issuecomment-752244166 - .unwrap_or_else(|x| x) - { - let stream_waker = cx.waker().clone(); - let stream_wake_task_executed = self.stream_wake_task_executed.clone(); - let stream_wake_task_should_shutdown = - self.stream_wake_task_should_shutdown.clone(); - - stream_wake_task_should_shutdown.store(false, Ordering::SeqCst); - - let _ = self.task_sender.send(Task { - stream_waker, - stream_wake_task_executed, - stream_wake_task_should_shutdown, - }); - } - Poll::Pending - } - Err(e) => Poll::Ready(Some(Err(e))), - }; - result - } -} - -impl Drop for EventStream { - fn drop(&mut self) { - self.stream_wake_task_should_shutdown - .store(true, Ordering::SeqCst); - let _ = self.poll_internal_waker.wake(); - } -} diff --git a/crates/util/keyfork-crossterm/src/event/sys.rs b/crates/util/keyfork-crossterm/src/event/sys.rs deleted file mode 100644 index bd79307..0000000 --- a/crates/util/keyfork-crossterm/src/event/sys.rs +++ /dev/null @@ -1,9 +0,0 @@ -#[cfg(all(unix, feature = "event-stream"))] -pub(crate) use unix::waker::Waker; -#[cfg(all(windows, feature = "event-stream"))] -pub(crate) use windows::waker::Waker; - -#[cfg(unix)] -pub(crate) mod unix; -#[cfg(windows)] -pub(crate) mod windows; diff --git a/crates/util/keyfork-crossterm/src/event/sys/unix.rs b/crates/util/keyfork-crossterm/src/event/sys/unix.rs deleted file mode 100644 index 2106ca0..0000000 --- a/crates/util/keyfork-crossterm/src/event/sys/unix.rs +++ /dev/null @@ -1,5 +0,0 @@ -#[cfg(feature = "event-stream")] -pub(crate) mod waker; - -#[cfg(feature = "events")] -pub(crate) mod parse; diff --git a/crates/util/keyfork-crossterm/src/event/sys/unix/parse.rs b/crates/util/keyfork-crossterm/src/event/sys/unix/parse.rs deleted file mode 100644 index 2019b5f..0000000 --- a/crates/util/keyfork-crossterm/src/event/sys/unix/parse.rs +++ /dev/null @@ -1,1506 +0,0 @@ -use std::io; - -use crate::event::{ - Event, KeyCode, KeyEvent, KeyEventKind, KeyEventState, KeyModifiers, KeyboardEnhancementFlags, - MediaKeyCode, ModifierKeyCode, MouseButton, MouseEvent, MouseEventKind, -}; - -use super::super::super::InternalEvent; - -// Event parsing -// -// This code (& previous one) are kind of ugly. We have to think about this, -// because it's really not maintainable, no tests, etc. -// -// Every fn returns Result> -// -// Ok(None) -> wait for more bytes -// Err(_) -> failed to parse event, clear the buffer -// Ok(Some(event)) -> we have event, clear the buffer -// - -fn could_not_parse_event_error() -> io::Error { - io::Error::new(io::ErrorKind::Other, "Could not parse an event.") -} - -pub(crate) fn parse_event( - buffer: &[u8], - input_available: bool, -) -> io::Result> { - if buffer.is_empty() { - return Ok(None); - } - - match buffer[0] { - b'\x1B' => { - if buffer.len() == 1 { - if input_available { - // Possible Esc sequence - Ok(None) - } else { - Ok(Some(InternalEvent::Event(Event::Key(KeyCode::Esc.into())))) - } - } else { - match buffer[1] { - b'O' => { - if buffer.len() == 2 { - Ok(None) - } else { - match buffer[2] { - b'D' => { - Ok(Some(InternalEvent::Event(Event::Key(KeyCode::Left.into())))) - } - b'C' => Ok(Some(InternalEvent::Event(Event::Key( - KeyCode::Right.into(), - )))), - b'A' => { - Ok(Some(InternalEvent::Event(Event::Key(KeyCode::Up.into())))) - } - b'B' => { - Ok(Some(InternalEvent::Event(Event::Key(KeyCode::Down.into())))) - } - b'H' => { - Ok(Some(InternalEvent::Event(Event::Key(KeyCode::Home.into())))) - } - b'F' => { - Ok(Some(InternalEvent::Event(Event::Key(KeyCode::End.into())))) - } - // F1-F4 - val @ b'P'..=b'S' => Ok(Some(InternalEvent::Event(Event::Key( - KeyCode::F(1 + val - b'P').into(), - )))), - _ => Err(could_not_parse_event_error()), - } - } - } - b'[' => parse_csi(buffer), - b'\x1B' => Ok(Some(InternalEvent::Event(Event::Key(KeyCode::Esc.into())))), - _ => parse_event(&buffer[1..], input_available).map(|event_option| { - event_option.map(|event| { - if let InternalEvent::Event(Event::Key(key_event)) = event { - let mut alt_key_event = key_event; - alt_key_event.modifiers |= KeyModifiers::ALT; - InternalEvent::Event(Event::Key(alt_key_event)) - } else { - event - } - }) - }), - } - } - } - b'\r' => Ok(Some(InternalEvent::Event(Event::Key( - KeyCode::Enter.into(), - )))), - // Issue #371: \n = 0xA, which is also the keycode for Ctrl+J. The only reason we get - // newlines as input is because the terminal converts \r into \n for us. When we - // enter raw mode, we disable that, so \n no longer has any meaning - it's better to - // use Ctrl+J. Waiting to handle it here means it gets picked up later - b'\n' if !crate::terminal::sys::is_raw_mode_enabled() => Ok(Some(InternalEvent::Event( - Event::Key(KeyCode::Enter.into()), - ))), - b'\t' => Ok(Some(InternalEvent::Event(Event::Key(KeyCode::Tab.into())))), - b'\x7F' => Ok(Some(InternalEvent::Event(Event::Key( - KeyCode::Backspace.into(), - )))), - c @ b'\x01'..=b'\x1A' => Ok(Some(InternalEvent::Event(Event::Key(KeyEvent::new( - KeyCode::Char((c - 0x1 + b'a') as char), - KeyModifiers::CONTROL, - ))))), - c @ b'\x1C'..=b'\x1F' => Ok(Some(InternalEvent::Event(Event::Key(KeyEvent::new( - KeyCode::Char((c - 0x1C + b'4') as char), - KeyModifiers::CONTROL, - ))))), - b'\0' => Ok(Some(InternalEvent::Event(Event::Key(KeyEvent::new( - KeyCode::Char(' '), - KeyModifiers::CONTROL, - ))))), - _ => parse_utf8_char(buffer).map(|maybe_char| { - maybe_char - .map(KeyCode::Char) - .map(char_code_to_event) - .map(Event::Key) - .map(InternalEvent::Event) - }), - } -} - -// converts KeyCode to KeyEvent (adds shift modifier in case of uppercase characters) -fn char_code_to_event(code: KeyCode) -> KeyEvent { - let modifiers = match code { - KeyCode::Char(c) if c.is_uppercase() => KeyModifiers::SHIFT, - _ => KeyModifiers::empty(), - }; - KeyEvent::new(code, modifiers) -} - -pub(crate) fn parse_csi(buffer: &[u8]) -> io::Result> { - assert!(buffer.starts_with(&[b'\x1B', b'['])); // ESC [ - - if buffer.len() == 2 { - return Ok(None); - } - - let input_event = match buffer[2] { - b'[' => { - if buffer.len() == 3 { - None - } else { - match buffer[3] { - // NOTE (@imdaveho): cannot find when this occurs; - // having another '[' after ESC[ not a likely scenario - val @ b'A'..=b'E' => Some(Event::Key(KeyCode::F(1 + val - b'A').into())), - _ => return Err(could_not_parse_event_error()), - } - } - } - b'D' => Some(Event::Key(KeyCode::Left.into())), - b'C' => Some(Event::Key(KeyCode::Right.into())), - b'A' => Some(Event::Key(KeyCode::Up.into())), - b'B' => Some(Event::Key(KeyCode::Down.into())), - b'H' => Some(Event::Key(KeyCode::Home.into())), - b'F' => Some(Event::Key(KeyCode::End.into())), - b'Z' => Some(Event::Key(KeyEvent::new_with_kind( - KeyCode::BackTab, - KeyModifiers::SHIFT, - KeyEventKind::Press, - ))), - b'M' => return parse_csi_normal_mouse(buffer), - b'<' => return parse_csi_sgr_mouse(buffer), - b'I' => Some(Event::FocusGained), - b'O' => Some(Event::FocusLost), - b';' => return parse_csi_modifier_key_code(buffer), - // 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' => Some(Event::Key(KeyCode::F(1).into())), - b'Q' => Some(Event::Key(KeyCode::F(2).into())), - b'S' => Some(Event::Key(KeyCode::F(4).into())), - b'?' => match buffer[buffer.len() - 1] { - b'u' => return parse_csi_keyboard_enhancement_flags(buffer), - b'c' => return parse_csi_primary_device_attributes(buffer), - _ => None, - }, - b'0'..=b'9' => { - // Numbered escape code. - if buffer.len() == 3 { - None - } else { - // The final byte of a CSI sequence can be in the range 64-126, so - // let's keep reading anything else. - let last_byte = buffer[buffer.len() - 1]; - if !(64..=126).contains(&last_byte) { - None - } else { - #[cfg(feature = "bracketed-paste")] - if buffer.starts_with(b"\x1B[200~") { - return parse_csi_bracketed_paste(buffer); - } - match last_byte { - b'M' => return parse_csi_rxvt_mouse(buffer), - b'~' => return parse_csi_special_key_code(buffer), - b'u' => return parse_csi_u_encoded_key_code(buffer), - b'R' => return parse_csi_cursor_position(buffer), - _ => return parse_csi_modifier_key_code(buffer), - } - } - } - } - _ => return Err(could_not_parse_event_error()), - }; - - Ok(input_event.map(InternalEvent::Event)) -} - -pub(crate) fn next_parsed(iter: &mut dyn Iterator) -> io::Result -where - T: std::str::FromStr, -{ - iter.next() - .ok_or_else(could_not_parse_event_error)? - .parse::() - .map_err(|_| could_not_parse_event_error()) -} - -fn modifier_and_kind_parsed(iter: &mut dyn Iterator) -> io::Result<(u8, u8)> { - let mut sub_split = iter - .next() - .ok_or_else(could_not_parse_event_error)? - .split(':'); - - let modifier_mask = next_parsed::(&mut sub_split)?; - - if let Ok(kind_code) = next_parsed::(&mut sub_split) { - Ok((modifier_mask, kind_code)) - } else { - Ok((modifier_mask, 1)) - } -} - -pub(crate) fn parse_csi_cursor_position(buffer: &[u8]) -> io::Result> { - // ESC [ Cy ; Cx R - // Cy - cursor row number (starting from 1) - // Cx - cursor column number (starting from 1) - assert!(buffer.starts_with(&[b'\x1B', b'['])); // ESC [ - assert!(buffer.ends_with(&[b'R'])); - - let s = std::str::from_utf8(&buffer[2..buffer.len() - 1]) - .map_err(|_| could_not_parse_event_error())?; - - let mut split = s.split(';'); - - let y = next_parsed::(&mut split)? - 1; - let x = next_parsed::(&mut split)? - 1; - - Ok(Some(InternalEvent::CursorPosition(x, y))) -} - -fn parse_csi_keyboard_enhancement_flags(buffer: &[u8]) -> io::Result> { - // ESC [ ? flags u - assert!(buffer.starts_with(&[b'\x1B', b'[', b'?'])); // ESC [ ? - assert!(buffer.ends_with(&[b'u'])); - - if buffer.len() < 5 { - return Ok(None); - } - - let bits = buffer[3]; - let mut flags = KeyboardEnhancementFlags::empty(); - - if bits & 1 != 0 { - flags |= KeyboardEnhancementFlags::DISAMBIGUATE_ESCAPE_CODES; - } - if bits & 2 != 0 { - flags |= KeyboardEnhancementFlags::REPORT_EVENT_TYPES; - } - if bits & 4 != 0 { - flags |= KeyboardEnhancementFlags::REPORT_ALTERNATE_KEYS; - } - if bits & 8 != 0 { - flags |= KeyboardEnhancementFlags::REPORT_ALL_KEYS_AS_ESCAPE_CODES; - } - // *Note*: this is not yet supported by crossterm. - // if bits & 16 != 0 { - // flags |= KeyboardEnhancementFlags::REPORT_ASSOCIATED_TEXT; - // } - - Ok(Some(InternalEvent::KeyboardEnhancementFlags(flags))) -} - -fn parse_csi_primary_device_attributes(buffer: &[u8]) -> io::Result> { - // ESC [ 64 ; attr1 ; attr2 ; ... ; attrn ; c - assert!(buffer.starts_with(&[b'\x1B', b'[', b'?'])); - assert!(buffer.ends_with(&[b'c'])); - - // This is a stub for parsing the primary device attributes. This response is not - // exposed in the crossterm API so we don't need to parse the individual attributes yet. - // See - - Ok(Some(InternalEvent::PrimaryDeviceAttributes)) -} - -fn parse_modifiers(mask: u8) -> KeyModifiers { - let modifier_mask = mask.saturating_sub(1); - let mut modifiers = KeyModifiers::empty(); - if modifier_mask & 1 != 0 { - modifiers |= KeyModifiers::SHIFT; - } - if modifier_mask & 2 != 0 { - modifiers |= KeyModifiers::ALT; - } - if modifier_mask & 4 != 0 { - modifiers |= KeyModifiers::CONTROL; - } - if modifier_mask & 8 != 0 { - modifiers |= KeyModifiers::SUPER; - } - if modifier_mask & 16 != 0 { - modifiers |= KeyModifiers::HYPER; - } - if modifier_mask & 32 != 0 { - modifiers |= KeyModifiers::META; - } - modifiers -} - -fn parse_modifiers_to_state(mask: u8) -> KeyEventState { - let modifier_mask = mask.saturating_sub(1); - let mut state = KeyEventState::empty(); - if modifier_mask & 64 != 0 { - state |= KeyEventState::CAPS_LOCK; - } - if modifier_mask & 128 != 0 { - state |= KeyEventState::NUM_LOCK; - } - state -} - -fn parse_key_event_kind(kind: u8) -> KeyEventKind { - match kind { - 1 => KeyEventKind::Press, - 2 => KeyEventKind::Repeat, - 3 => KeyEventKind::Release, - _ => KeyEventKind::Press, - } -} - -pub(crate) fn parse_csi_modifier_key_code(buffer: &[u8]) -> io::Result> { - assert!(buffer.starts_with(&[b'\x1B', b'['])); // ESC [ - // - let s = std::str::from_utf8(&buffer[2..buffer.len() - 1]) - .map_err(|_| could_not_parse_event_error())?; - let mut split = s.split(';'); - - split.next(); - - let (modifiers, kind) = - if let Ok((modifier_mask, kind_code)) = modifier_and_kind_parsed(&mut split) { - ( - parse_modifiers(modifier_mask), - parse_key_event_kind(kind_code), - ) - } else if buffer.len() > 3 { - ( - parse_modifiers( - (buffer[buffer.len() - 2] as char) - .to_digit(10) - .ok_or_else(could_not_parse_event_error)? as u8, - ), - KeyEventKind::Press, - ) - } else { - (KeyModifiers::NONE, KeyEventKind::Press) - }; - let key = buffer[buffer.len() - 1]; - - let keycode = match key { - 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::F(1), - b'Q' => KeyCode::F(2), - b'R' => KeyCode::F(3), - b'S' => KeyCode::F(4), - _ => return Err(could_not_parse_event_error()), - }; - - let input_event = Event::Key(KeyEvent::new_with_kind(keycode, modifiers, kind)); - - Ok(Some(InternalEvent::Event(input_event))) -} - -fn translate_functional_key_code(codepoint: u32) -> Option<(KeyCode, KeyEventState)> { - if let Some(keycode) = match codepoint { - 57399 => Some(KeyCode::Char('0')), - 57400 => Some(KeyCode::Char('1')), - 57401 => Some(KeyCode::Char('2')), - 57402 => Some(KeyCode::Char('3')), - 57403 => Some(KeyCode::Char('4')), - 57404 => Some(KeyCode::Char('5')), - 57405 => Some(KeyCode::Char('6')), - 57406 => Some(KeyCode::Char('7')), - 57407 => Some(KeyCode::Char('8')), - 57408 => Some(KeyCode::Char('9')), - 57409 => Some(KeyCode::Char('.')), - 57410 => Some(KeyCode::Char('/')), - 57411 => Some(KeyCode::Char('*')), - 57412 => Some(KeyCode::Char('-')), - 57413 => Some(KeyCode::Char('+')), - 57414 => Some(KeyCode::Enter), - 57415 => Some(KeyCode::Char('=')), - 57416 => Some(KeyCode::Char(',')), - 57417 => Some(KeyCode::Left), - 57418 => Some(KeyCode::Right), - 57419 => Some(KeyCode::Up), - 57420 => Some(KeyCode::Down), - 57421 => Some(KeyCode::PageUp), - 57422 => Some(KeyCode::PageDown), - 57423 => Some(KeyCode::Home), - 57424 => Some(KeyCode::End), - 57425 => Some(KeyCode::Insert), - 57426 => Some(KeyCode::Delete), - 57427 => Some(KeyCode::KeypadBegin), - _ => None, - } { - return Some((keycode, KeyEventState::KEYPAD)); - } - - if let Some(keycode) = match codepoint { - 57358 => Some(KeyCode::CapsLock), - 57359 => Some(KeyCode::ScrollLock), - 57360 => Some(KeyCode::NumLock), - 57361 => Some(KeyCode::PrintScreen), - 57362 => Some(KeyCode::Pause), - 57363 => Some(KeyCode::Menu), - 57376 => Some(KeyCode::F(13)), - 57377 => Some(KeyCode::F(14)), - 57378 => Some(KeyCode::F(15)), - 57379 => Some(KeyCode::F(16)), - 57380 => Some(KeyCode::F(17)), - 57381 => Some(KeyCode::F(18)), - 57382 => Some(KeyCode::F(19)), - 57383 => Some(KeyCode::F(20)), - 57384 => Some(KeyCode::F(21)), - 57385 => Some(KeyCode::F(22)), - 57386 => Some(KeyCode::F(23)), - 57387 => Some(KeyCode::F(24)), - 57388 => Some(KeyCode::F(25)), - 57389 => Some(KeyCode::F(26)), - 57390 => Some(KeyCode::F(27)), - 57391 => Some(KeyCode::F(28)), - 57392 => Some(KeyCode::F(29)), - 57393 => Some(KeyCode::F(30)), - 57394 => Some(KeyCode::F(31)), - 57395 => Some(KeyCode::F(32)), - 57396 => Some(KeyCode::F(33)), - 57397 => Some(KeyCode::F(34)), - 57398 => Some(KeyCode::F(35)), - 57428 => Some(KeyCode::Media(MediaKeyCode::Play)), - 57429 => Some(KeyCode::Media(MediaKeyCode::Pause)), - 57430 => Some(KeyCode::Media(MediaKeyCode::PlayPause)), - 57431 => Some(KeyCode::Media(MediaKeyCode::Reverse)), - 57432 => Some(KeyCode::Media(MediaKeyCode::Stop)), - 57433 => Some(KeyCode::Media(MediaKeyCode::FastForward)), - 57434 => Some(KeyCode::Media(MediaKeyCode::Rewind)), - 57435 => Some(KeyCode::Media(MediaKeyCode::TrackNext)), - 57436 => Some(KeyCode::Media(MediaKeyCode::TrackPrevious)), - 57437 => Some(KeyCode::Media(MediaKeyCode::Record)), - 57438 => Some(KeyCode::Media(MediaKeyCode::LowerVolume)), - 57439 => Some(KeyCode::Media(MediaKeyCode::RaiseVolume)), - 57440 => Some(KeyCode::Media(MediaKeyCode::MuteVolume)), - 57441 => Some(KeyCode::Modifier(ModifierKeyCode::LeftShift)), - 57442 => Some(KeyCode::Modifier(ModifierKeyCode::LeftControl)), - 57443 => Some(KeyCode::Modifier(ModifierKeyCode::LeftAlt)), - 57444 => Some(KeyCode::Modifier(ModifierKeyCode::LeftSuper)), - 57445 => Some(KeyCode::Modifier(ModifierKeyCode::LeftHyper)), - 57446 => Some(KeyCode::Modifier(ModifierKeyCode::LeftMeta)), - 57447 => Some(KeyCode::Modifier(ModifierKeyCode::RightShift)), - 57448 => Some(KeyCode::Modifier(ModifierKeyCode::RightControl)), - 57449 => Some(KeyCode::Modifier(ModifierKeyCode::RightAlt)), - 57450 => Some(KeyCode::Modifier(ModifierKeyCode::RightSuper)), - 57451 => Some(KeyCode::Modifier(ModifierKeyCode::RightHyper)), - 57452 => Some(KeyCode::Modifier(ModifierKeyCode::RightMeta)), - 57453 => Some(KeyCode::Modifier(ModifierKeyCode::IsoLevel3Shift)), - 57454 => Some(KeyCode::Modifier(ModifierKeyCode::IsoLevel5Shift)), - _ => None, - } { - return Some((keycode, KeyEventState::empty())); - } - - None -} - -pub(crate) fn parse_csi_u_encoded_key_code(buffer: &[u8]) -> io::Result> { - assert!(buffer.starts_with(&[b'\x1B', b'['])); // ESC [ - assert!(buffer.ends_with(&[b'u'])); - - // This function parses `CSI … u` sequences. These are sequences defined in either - // the `CSI u` (a.k.a. "Fix Keyboard Input on Terminals - Please", https://www.leonerd.org.uk/hacks/fixterms/) - // or Kitty Keyboard Protocol (https://sw.kovidgoyal.net/kitty/keyboard-protocol/) specifications. - // This CSI sequence is a tuple of semicolon-separated numbers. - let s = std::str::from_utf8(&buffer[2..buffer.len() - 1]) - .map_err(|_| could_not_parse_event_error())?; - let mut split = 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 = split - .next() - .ok_or_else(could_not_parse_event_error)? - .split(':'); - - let codepoint = codepoints - .next() - .ok_or_else(could_not_parse_event_error)? - .parse::() - .map_err(|_| could_not_parse_event_error())?; - - let (mut modifiers, kind, state_from_modifiers) = - if let Ok((modifier_mask, kind_code)) = modifier_and_kind_parsed(&mut split) { - ( - parse_modifiers(modifier_mask), - parse_key_event_kind(kind_code), - parse_modifiers_to_state(modifier_mask), - ) - } else { - (KeyModifiers::NONE, KeyEventKind::Press, KeyEventState::NONE) - }; - - let (mut keycode, state_from_keycode) = { - if let Some((special_key_code, state)) = translate_functional_key_code(codepoint) { - (special_key_code, state) - } else if let Some(c) = char::from_u32(codepoint) { - ( - match c { - '\x1B' => KeyCode::Esc, - '\r' => KeyCode::Enter, - // Issue #371: \n = 0xA, which is also the keycode for Ctrl+J. The only reason we get - // newlines as input is because the terminal converts \r into \n for us. When we - // enter raw mode, we disable that, so \n no longer has any meaning - it's better to - // use Ctrl+J. Waiting to handle it here means it gets picked up later - '\n' if !crate::terminal::sys::is_raw_mode_enabled() => KeyCode::Enter, - '\t' => { - if modifiers.contains(KeyModifiers::SHIFT) { - KeyCode::BackTab - } else { - KeyCode::Tab - } - } - '\x7F' => KeyCode::Backspace, - _ => KeyCode::Char(c), - }, - KeyEventState::empty(), - ) - } else { - return Err(could_not_parse_event_error()); - } - }; - - if let KeyCode::Modifier(modifier_keycode) = keycode { - match modifier_keycode { - ModifierKeyCode::LeftAlt | ModifierKeyCode::RightAlt => { - modifiers.set(KeyModifiers::ALT, true) - } - ModifierKeyCode::LeftControl | ModifierKeyCode::RightControl => { - modifiers.set(KeyModifiers::CONTROL, true) - } - ModifierKeyCode::LeftShift | ModifierKeyCode::RightShift => { - modifiers.set(KeyModifiers::SHIFT, true) - } - ModifierKeyCode::LeftSuper | ModifierKeyCode::RightSuper => { - modifiers.set(KeyModifiers::SUPER, true) - } - ModifierKeyCode::LeftHyper | ModifierKeyCode::RightHyper => { - modifiers.set(KeyModifiers::HYPER, true) - } - ModifierKeyCode::LeftMeta | ModifierKeyCode::RightMeta => { - modifiers.set(KeyModifiers::META, true) - } - _ => {} - } - } - - // 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(KeyModifiers::SHIFT) { - if let Some(shifted_c) = codepoints - .next() - .and_then(|codepoint| codepoint.parse::().ok()) - .and_then(char::from_u32) - { - keycode = KeyCode::Char(shifted_c); - modifiers.set(KeyModifiers::SHIFT, false); - } - } - - let input_event = Event::Key(KeyEvent::new_with_kind_and_state( - keycode, - modifiers, - kind, - state_from_keycode | state_from_modifiers, - )); - - Ok(Some(InternalEvent::Event(input_event))) -} - -pub(crate) fn parse_csi_special_key_code(buffer: &[u8]) -> io::Result> { - assert!(buffer.starts_with(&[b'\x1B', b'['])); // ESC [ - assert!(buffer.ends_with(&[b'~'])); - - let s = std::str::from_utf8(&buffer[2..buffer.len() - 1]) - .map_err(|_| could_not_parse_event_error())?; - let mut split = s.split(';'); - - // This CSI sequence can be a list of semicolon-separated numbers. - let first = next_parsed::(&mut split)?; - - let (modifiers, kind, state) = - if let Ok((modifier_mask, kind_code)) = modifier_and_kind_parsed(&mut split) { - ( - parse_modifiers(modifier_mask), - parse_key_event_kind(kind_code), - parse_modifiers_to_state(modifier_mask), - ) - } else { - (KeyModifiers::NONE, KeyEventKind::Press, KeyEventState::NONE) - }; - - let keycode = 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::F(v - 10), - v @ 17..=21 => KeyCode::F(v - 11), - v @ 23..=26 => KeyCode::F(v - 12), - v @ 28..=29 => KeyCode::F(v - 15), - v @ 31..=34 => KeyCode::F(v - 17), - _ => return Err(could_not_parse_event_error()), - }; - - let input_event = Event::Key(KeyEvent::new_with_kind_and_state( - keycode, modifiers, kind, state, - )); - - Ok(Some(InternalEvent::Event(input_event))) -} - -pub(crate) fn parse_csi_rxvt_mouse(buffer: &[u8]) -> io::Result> { - // rxvt mouse encoding: - // ESC [ Cb ; Cx ; Cy ; M - - assert!(buffer.starts_with(&[b'\x1B', b'['])); // ESC [ - assert!(buffer.ends_with(&[b'M'])); - - let s = std::str::from_utf8(&buffer[2..buffer.len() - 1]) - .map_err(|_| could_not_parse_event_error())?; - let mut split = s.split(';'); - - let cb = next_parsed::(&mut split)? - .checked_sub(32) - .ok_or_else(could_not_parse_event_error)?; - let (kind, modifiers) = parse_cb(cb)?; - - let cx = next_parsed::(&mut split)? - 1; - let cy = next_parsed::(&mut split)? - 1; - - Ok(Some(InternalEvent::Event(Event::Mouse(MouseEvent { - kind, - column: cx, - row: cy, - modifiers, - })))) -} - -pub(crate) fn parse_csi_normal_mouse(buffer: &[u8]) -> io::Result> { - // Normal mouse encoding: ESC [ M CB Cx Cy (6 characters only). - - assert!(buffer.starts_with(&[b'\x1B', b'[', b'M'])); // ESC [ M - - if buffer.len() < 6 { - return Ok(None); - } - - let cb = buffer[3] - .checked_sub(32) - .ok_or_else(could_not_parse_event_error)?; - let (kind, modifiers) = parse_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 cx = u16::from(buffer[4].saturating_sub(32)) - 1; - let cy = u16::from(buffer[5].saturating_sub(32)) - 1; - - Ok(Some(InternalEvent::Event(Event::Mouse(MouseEvent { - kind, - column: cx, - row: cy, - modifiers, - })))) -} - -pub(crate) fn parse_csi_sgr_mouse(buffer: &[u8]) -> io::Result> { - // ESC [ < Cb ; Cx ; Cy (;) (M or m) - - assert!(buffer.starts_with(&[b'\x1B', b'[', b'<'])); // ESC [ < - - if !buffer.ends_with(&[b'm']) && !buffer.ends_with(&[b'M']) { - return Ok(None); - } - - let s = std::str::from_utf8(&buffer[3..buffer.len() - 1]) - .map_err(|_| could_not_parse_event_error())?; - let mut split = s.split(';'); - - let cb = next_parsed::(&mut split)?; - let (kind, modifiers) = parse_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 cx = next_parsed::(&mut split)? - 1; - let cy = next_parsed::(&mut split)? - 1; - - // 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. - let kind = if buffer.last() == Some(&b'm') { - match kind { - MouseEventKind::Down(button) => MouseEventKind::Up(button), - other => other, - } - } else { - kind - }; - - Ok(Some(InternalEvent::Event(Event::Mouse(MouseEvent { - kind, - column: cx, - row: cy, - modifiers, - })))) -} - -/// 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 -/// - button number -/// - shift -/// - meta (alt) -/// - control -/// - mouse is dragging -/// - button number -/// - button number -fn parse_cb(cb: u8) -> io::Result<(MouseEventKind, KeyModifiers)> { - let button_number = (cb & 0b0000_0011) | ((cb & 0b1100_0000) >> 4); - let dragging = cb & 0b0010_0000 == 0b0010_0000; - - let kind = match (button_number, dragging) { - (0, false) => MouseEventKind::Down(MouseButton::Left), - (1, false) => MouseEventKind::Down(MouseButton::Middle), - (2, false) => MouseEventKind::Down(MouseButton::Right), - (0, true) => MouseEventKind::Drag(MouseButton::Left), - (1, true) => MouseEventKind::Drag(MouseButton::Middle), - (2, true) => MouseEventKind::Drag(MouseButton::Right), - (3, false) => MouseEventKind::Up(MouseButton::Left), - (3, true) | (4, true) | (5, true) => MouseEventKind::Moved, - (4, false) => MouseEventKind::ScrollUp, - (5, false) => MouseEventKind::ScrollDown, - (6, false) => MouseEventKind::ScrollLeft, - (7, false) => MouseEventKind::ScrollRight, - // We do not support other buttons. - _ => return Err(could_not_parse_event_error()), - }; - - let mut modifiers = KeyModifiers::empty(); - - if cb & 0b0000_0100 == 0b0000_0100 { - modifiers |= KeyModifiers::SHIFT; - } - if cb & 0b0000_1000 == 0b0000_1000 { - modifiers |= KeyModifiers::ALT; - } - if cb & 0b0001_0000 == 0b0001_0000 { - modifiers |= KeyModifiers::CONTROL; - } - - Ok((kind, modifiers)) -} - -#[cfg(feature = "bracketed-paste")] -pub(crate) fn parse_csi_bracketed_paste(buffer: &[u8]) -> io::Result> { - // ESC [ 2 0 0 ~ pasted text ESC 2 0 1 ~ - assert!(buffer.starts_with(b"\x1B[200~")); - - if !buffer.ends_with(b"\x1b[201~") { - Ok(None) - } else { - let paste = String::from_utf8_lossy(&buffer[6..buffer.len() - 6]).to_string(); - Ok(Some(InternalEvent::Event(Event::Paste(paste)))) - } -} - -pub(crate) fn parse_utf8_char(buffer: &[u8]) -> io::Result> { - match std::str::from_utf8(buffer) { - Ok(s) => { - let ch = s.chars().next().ok_or_else(could_not_parse_event_error)?; - - Ok(Some(ch)) - } - Err(_) => { - // from_utf8 failed, but we have to check if we need more bytes for code point - // and if all the bytes we have no are valid - - let required_bytes = match buffer[0] { - // https://en.wikipedia.org/wiki/UTF-8#Description - (0x00..=0x7F) => 1, // 0xxxxxxx - (0xC0..=0xDF) => 2, // 110xxxxx 10xxxxxx - (0xE0..=0xEF) => 3, // 1110xxxx 10xxxxxx 10xxxxxx - (0xF0..=0xF7) => 4, // 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx - (0x80..=0xBF) | (0xF8..=0xFF) => return Err(could_not_parse_event_error()), - }; - - // More than 1 byte, check them for 10xxxxxx pattern - if required_bytes > 1 && buffer.len() > 1 { - for byte in &buffer[1..] { - if byte & !0b0011_1111 != 0b1000_0000 { - return Err(could_not_parse_event_error()); - } - } - } - - if buffer.len() < required_bytes { - // All bytes looks good so far, but we need more of them - Ok(None) - } else { - Err(could_not_parse_event_error()) - } - } - } -} - -#[cfg(test)] -mod tests { - use crate::event::{KeyEventState, KeyModifiers, MouseButton, MouseEvent}; - - use super::*; - - #[test] - fn test_esc_key() { - assert_eq!( - parse_event(b"\x1B", false).unwrap(), - Some(InternalEvent::Event(Event::Key(KeyCode::Esc.into()))), - ); - } - - #[test] - fn test_possible_esc_sequence() { - assert_eq!(parse_event(b"\x1B", true).unwrap(), None,); - } - - #[test] - fn test_alt_key() { - assert_eq!( - parse_event(b"\x1Bc", false).unwrap(), - Some(InternalEvent::Event(Event::Key(KeyEvent::new( - KeyCode::Char('c'), - KeyModifiers::ALT - )))), - ); - } - - #[test] - fn test_alt_shift() { - assert_eq!( - parse_event(b"\x1BH", false).unwrap(), - Some(InternalEvent::Event(Event::Key(KeyEvent::new( - KeyCode::Char('H'), - KeyModifiers::ALT | KeyModifiers::SHIFT - )))), - ); - } - - #[test] - fn test_alt_ctrl() { - assert_eq!( - parse_event(b"\x1B\x14", false).unwrap(), - Some(InternalEvent::Event(Event::Key(KeyEvent::new( - KeyCode::Char('t'), - KeyModifiers::ALT | KeyModifiers::CONTROL - )))), - ); - } - - #[test] - fn test_parse_event_subsequent_calls() { - // The main purpose of this test is to check if we're passing - // correct slice to other parse_ functions. - - // parse_csi_cursor_position - assert_eq!( - parse_event(b"\x1B[20;10R", false).unwrap(), - Some(InternalEvent::CursorPosition(9, 19)) - ); - - // parse_csi - assert_eq!( - parse_event(b"\x1B[D", false).unwrap(), - Some(InternalEvent::Event(Event::Key(KeyCode::Left.into()))), - ); - - // parse_csi_modifier_key_code - assert_eq!( - parse_event(b"\x1B[2D", false).unwrap(), - Some(InternalEvent::Event(Event::Key(KeyEvent::new( - KeyCode::Left, - KeyModifiers::SHIFT - )))) - ); - - // parse_csi_special_key_code - assert_eq!( - parse_event(b"\x1B[3~", false).unwrap(), - Some(InternalEvent::Event(Event::Key(KeyCode::Delete.into()))), - ); - - // parse_csi_bracketed_paste - #[cfg(feature = "bracketed-paste")] - assert_eq!( - parse_event(b"\x1B[200~on and on and on\x1B[201~", false).unwrap(), - Some(InternalEvent::Event(Event::Paste( - "on and on and on".to_string() - ))), - ); - - // parse_csi_rxvt_mouse - assert_eq!( - parse_event(b"\x1B[32;30;40;M", false).unwrap(), - Some(InternalEvent::Event(Event::Mouse(MouseEvent { - kind: MouseEventKind::Down(MouseButton::Left), - column: 29, - row: 39, - modifiers: KeyModifiers::empty(), - }))) - ); - - // parse_csi_normal_mouse - assert_eq!( - parse_event(b"\x1B[M0\x60\x70", false).unwrap(), - Some(InternalEvent::Event(Event::Mouse(MouseEvent { - kind: MouseEventKind::Down(MouseButton::Left), - column: 63, - row: 79, - modifiers: KeyModifiers::CONTROL, - }))) - ); - - // parse_csi_sgr_mouse - assert_eq!( - parse_event(b"\x1B[<0;20;10;M", false).unwrap(), - Some(InternalEvent::Event(Event::Mouse(MouseEvent { - kind: MouseEventKind::Down(MouseButton::Left), - column: 19, - row: 9, - modifiers: KeyModifiers::empty(), - }))) - ); - - // parse_utf8_char - assert_eq!( - parse_event("Ž".as_bytes(), false).unwrap(), - Some(InternalEvent::Event(Event::Key(KeyEvent::new( - KeyCode::Char('Ž'), - KeyModifiers::SHIFT - )))), - ); - } - - #[test] - fn test_parse_event() { - assert_eq!( - parse_event(b"\t", false).unwrap(), - Some(InternalEvent::Event(Event::Key(KeyCode::Tab.into()))), - ); - } - - #[test] - fn test_parse_csi_cursor_position() { - assert_eq!( - parse_csi_cursor_position(b"\x1B[20;10R").unwrap(), - Some(InternalEvent::CursorPosition(9, 19)) - ); - } - - #[test] - fn test_parse_csi() { - assert_eq!( - parse_csi(b"\x1B[D").unwrap(), - Some(InternalEvent::Event(Event::Key(KeyCode::Left.into()))), - ); - } - - #[test] - fn test_parse_csi_modifier_key_code() { - assert_eq!( - parse_csi_modifier_key_code(b"\x1B[2D").unwrap(), - Some(InternalEvent::Event(Event::Key(KeyEvent::new( - KeyCode::Left, - KeyModifiers::SHIFT - )))), - ); - } - - #[test] - fn test_parse_csi_special_key_code() { - assert_eq!( - parse_csi_special_key_code(b"\x1B[3~").unwrap(), - Some(InternalEvent::Event(Event::Key(KeyCode::Delete.into()))), - ); - } - - #[test] - fn test_parse_csi_special_key_code_multiple_values_not_supported() { - assert_eq!( - parse_csi_special_key_code(b"\x1B[3;2~").unwrap(), - Some(InternalEvent::Event(Event::Key(KeyEvent::new( - KeyCode::Delete, - KeyModifiers::SHIFT - )))), - ); - } - - #[cfg(feature = "bracketed-paste")] - #[test] - fn test_parse_csi_bracketed_paste() { - // - assert_eq!( - parse_event(b"\x1B[200~o", false).unwrap(), - None, - "A partial bracketed paste isn't parsed" - ); - assert_eq!( - parse_event(b"\x1B[200~o\x1B[2D", false).unwrap(), - None, - "A partial bracketed paste containing another escape code isn't parsed" - ); - assert_eq!( - parse_event(b"\x1B[200~o\x1B[2D\x1B[201~", false).unwrap(), - Some(InternalEvent::Event(Event::Paste("o\x1B[2D".to_string()))) - ); - } - - #[test] - fn test_parse_csi_focus() { - assert_eq!( - parse_csi(b"\x1B[O").unwrap(), - Some(InternalEvent::Event(Event::FocusLost)) - ); - } - - #[test] - fn test_parse_csi_rxvt_mouse() { - assert_eq!( - parse_csi_rxvt_mouse(b"\x1B[32;30;40;M").unwrap(), - Some(InternalEvent::Event(Event::Mouse(MouseEvent { - kind: MouseEventKind::Down(MouseButton::Left), - column: 29, - row: 39, - modifiers: KeyModifiers::empty(), - }))) - ); - } - - #[test] - fn test_parse_csi_normal_mouse() { - assert_eq!( - parse_csi_normal_mouse(b"\x1B[M0\x60\x70").unwrap(), - Some(InternalEvent::Event(Event::Mouse(MouseEvent { - kind: MouseEventKind::Down(MouseButton::Left), - column: 63, - row: 79, - modifiers: KeyModifiers::CONTROL, - }))) - ); - } - - #[test] - fn test_parse_csi_sgr_mouse() { - assert_eq!( - parse_csi_sgr_mouse(b"\x1B[<0;20;10;M").unwrap(), - Some(InternalEvent::Event(Event::Mouse(MouseEvent { - kind: MouseEventKind::Down(MouseButton::Left), - column: 19, - row: 9, - modifiers: KeyModifiers::empty(), - }))) - ); - assert_eq!( - parse_csi_sgr_mouse(b"\x1B[<0;20;10M").unwrap(), - Some(InternalEvent::Event(Event::Mouse(MouseEvent { - kind: MouseEventKind::Down(MouseButton::Left), - column: 19, - row: 9, - modifiers: KeyModifiers::empty(), - }))) - ); - assert_eq!( - parse_csi_sgr_mouse(b"\x1B[<0;20;10;m").unwrap(), - Some(InternalEvent::Event(Event::Mouse(MouseEvent { - kind: MouseEventKind::Up(MouseButton::Left), - column: 19, - row: 9, - modifiers: KeyModifiers::empty(), - }))) - ); - assert_eq!( - parse_csi_sgr_mouse(b"\x1B[<0;20;10m").unwrap(), - Some(InternalEvent::Event(Event::Mouse(MouseEvent { - kind: MouseEventKind::Up(MouseButton::Left), - column: 19, - row: 9, - modifiers: KeyModifiers::empty(), - }))) - ); - } - - #[test] - fn test_utf8() { - // https://www.php.net/manual/en/reference.pcre.pattern.modifiers.php#54805 - - // 'Valid ASCII' => "a", - assert_eq!(parse_utf8_char(b"a").unwrap(), Some('a'),); - - // 'Valid 2 Octet Sequence' => "\xc3\xb1", - assert_eq!(parse_utf8_char(&[0xC3, 0xB1]).unwrap(), Some('ñ'),); - - // 'Invalid 2 Octet Sequence' => "\xc3\x28", - assert!(parse_utf8_char(&[0xC3, 0x28]).is_err()); - - // 'Invalid Sequence Identifier' => "\xa0\xa1", - assert!(parse_utf8_char(&[0xA0, 0xA1]).is_err()); - - // 'Valid 3 Octet Sequence' => "\xe2\x82\xa1", - assert_eq!( - parse_utf8_char(&[0xE2, 0x81, 0xA1]).unwrap(), - Some('\u{2061}'), - ); - - // 'Invalid 3 Octet Sequence (in 2nd Octet)' => "\xe2\x28\xa1", - assert!(parse_utf8_char(&[0xE2, 0x28, 0xA1]).is_err()); - - // 'Invalid 3 Octet Sequence (in 3rd Octet)' => "\xe2\x82\x28", - assert!(parse_utf8_char(&[0xE2, 0x82, 0x28]).is_err()); - - // 'Valid 4 Octet Sequence' => "\xf0\x90\x8c\xbc", - assert_eq!( - parse_utf8_char(&[0xF0, 0x90, 0x8C, 0xBC]).unwrap(), - Some('𐌼'), - ); - - // 'Invalid 4 Octet Sequence (in 2nd Octet)' => "\xf0\x28\x8c\xbc", - assert!(parse_utf8_char(&[0xF0, 0x28, 0x8C, 0xBC]).is_err()); - - // 'Invalid 4 Octet Sequence (in 3rd Octet)' => "\xf0\x90\x28\xbc", - assert!(parse_utf8_char(&[0xF0, 0x90, 0x28, 0xBC]).is_err()); - - // 'Invalid 4 Octet Sequence (in 4th Octet)' => "\xf0\x28\x8c\x28", - assert!(parse_utf8_char(&[0xF0, 0x28, 0x8C, 0x28]).is_err()); - } - - #[test] - fn test_parse_char_event_lowercase() { - assert_eq!( - parse_event(b"c", false).unwrap(), - Some(InternalEvent::Event(Event::Key(KeyEvent::new( - KeyCode::Char('c'), - KeyModifiers::empty() - )))), - ); - } - - #[test] - fn test_parse_char_event_uppercase() { - assert_eq!( - parse_event(b"C", false).unwrap(), - Some(InternalEvent::Event(Event::Key(KeyEvent::new( - KeyCode::Char('C'), - KeyModifiers::SHIFT - )))), - ); - } - - #[test] - fn test_parse_basic_csi_u_encoded_key_code() { - assert_eq!( - parse_csi_u_encoded_key_code(b"\x1B[97u").unwrap(), - Some(InternalEvent::Event(Event::Key(KeyEvent::new( - KeyCode::Char('a'), - KeyModifiers::empty() - )))), - ); - assert_eq!( - parse_csi_u_encoded_key_code(b"\x1B[97;2u").unwrap(), - Some(InternalEvent::Event(Event::Key(KeyEvent::new( - KeyCode::Char('A'), - KeyModifiers::SHIFT - )))), - ); - assert_eq!( - parse_csi_u_encoded_key_code(b"\x1B[97;7u").unwrap(), - Some(InternalEvent::Event(Event::Key(KeyEvent::new( - KeyCode::Char('a'), - KeyModifiers::ALT | KeyModifiers::CONTROL - )))), - ); - } - - #[test] - fn test_parse_basic_csi_u_encoded_key_code_special_keys() { - assert_eq!( - parse_csi_u_encoded_key_code(b"\x1B[13u").unwrap(), - Some(InternalEvent::Event(Event::Key(KeyEvent::new( - KeyCode::Enter, - KeyModifiers::empty() - )))), - ); - assert_eq!( - parse_csi_u_encoded_key_code(b"\x1B[27u").unwrap(), - Some(InternalEvent::Event(Event::Key(KeyEvent::new( - KeyCode::Esc, - KeyModifiers::empty() - )))), - ); - assert_eq!( - parse_csi_u_encoded_key_code(b"\x1B[57358u").unwrap(), - Some(InternalEvent::Event(Event::Key(KeyEvent::new( - KeyCode::CapsLock, - KeyModifiers::empty() - )))), - ); - assert_eq!( - parse_csi_u_encoded_key_code(b"\x1B[57376u").unwrap(), - Some(InternalEvent::Event(Event::Key(KeyEvent::new( - KeyCode::F(13), - KeyModifiers::empty() - )))), - ); - assert_eq!( - parse_csi_u_encoded_key_code(b"\x1B[57428u").unwrap(), - Some(InternalEvent::Event(Event::Key(KeyEvent::new( - KeyCode::Media(MediaKeyCode::Play), - KeyModifiers::empty() - )))), - ); - assert_eq!( - parse_csi_u_encoded_key_code(b"\x1B[57441u").unwrap(), - Some(InternalEvent::Event(Event::Key(KeyEvent::new( - KeyCode::Modifier(ModifierKeyCode::LeftShift), - KeyModifiers::SHIFT, - )))), - ); - } - - #[test] - fn test_parse_csi_u_encoded_keypad_code() { - assert_eq!( - parse_csi_u_encoded_key_code(b"\x1B[57399u").unwrap(), - Some(InternalEvent::Event(Event::Key( - KeyEvent::new_with_kind_and_state( - KeyCode::Char('0'), - KeyModifiers::empty(), - KeyEventKind::Press, - KeyEventState::KEYPAD, - ) - ))), - ); - assert_eq!( - parse_csi_u_encoded_key_code(b"\x1B[57419u").unwrap(), - Some(InternalEvent::Event(Event::Key( - KeyEvent::new_with_kind_and_state( - KeyCode::Up, - KeyModifiers::empty(), - KeyEventKind::Press, - KeyEventState::KEYPAD, - ) - ))), - ); - } - - #[test] - fn test_parse_csi_u_encoded_key_code_with_types() { - assert_eq!( - parse_csi_u_encoded_key_code(b"\x1B[97;1u").unwrap(), - Some(InternalEvent::Event(Event::Key(KeyEvent::new_with_kind( - KeyCode::Char('a'), - KeyModifiers::empty(), - KeyEventKind::Press, - )))), - ); - assert_eq!( - parse_csi_u_encoded_key_code(b"\x1B[97;1:1u").unwrap(), - Some(InternalEvent::Event(Event::Key(KeyEvent::new_with_kind( - KeyCode::Char('a'), - KeyModifiers::empty(), - KeyEventKind::Press, - )))), - ); - assert_eq!( - parse_csi_u_encoded_key_code(b"\x1B[97;5:1u").unwrap(), - Some(InternalEvent::Event(Event::Key(KeyEvent::new_with_kind( - KeyCode::Char('a'), - KeyModifiers::CONTROL, - KeyEventKind::Press, - )))), - ); - assert_eq!( - parse_csi_u_encoded_key_code(b"\x1B[97;1:2u").unwrap(), - Some(InternalEvent::Event(Event::Key(KeyEvent::new_with_kind( - KeyCode::Char('a'), - KeyModifiers::empty(), - KeyEventKind::Repeat, - )))), - ); - assert_eq!( - parse_csi_u_encoded_key_code(b"\x1B[97;1:3u").unwrap(), - Some(InternalEvent::Event(Event::Key(KeyEvent::new_with_kind( - KeyCode::Char('a'), - KeyModifiers::empty(), - KeyEventKind::Release, - )))), - ); - } - - #[test] - fn test_parse_csi_u_encoded_key_code_has_modifier_on_modifier_press() { - assert_eq!( - parse_csi_u_encoded_key_code(b"\x1B[57449u").unwrap(), - Some(InternalEvent::Event(Event::Key(KeyEvent::new_with_kind( - KeyCode::Modifier(ModifierKeyCode::RightAlt), - KeyModifiers::ALT, - KeyEventKind::Press, - )))), - ); - assert_eq!( - parse_csi_u_encoded_key_code(b"\x1B[57449;3:3u").unwrap(), - Some(InternalEvent::Event(Event::Key(KeyEvent::new_with_kind( - KeyCode::Modifier(ModifierKeyCode::RightAlt), - KeyModifiers::ALT, - KeyEventKind::Release, - )))), - ); - assert_eq!( - parse_csi_u_encoded_key_code(b"\x1B[57450u").unwrap(), - Some(InternalEvent::Event(Event::Key(KeyEvent::new( - KeyCode::Modifier(ModifierKeyCode::RightSuper), - KeyModifiers::SUPER, - )))), - ); - assert_eq!( - parse_csi_u_encoded_key_code(b"\x1B[57451u").unwrap(), - Some(InternalEvent::Event(Event::Key(KeyEvent::new( - KeyCode::Modifier(ModifierKeyCode::RightHyper), - KeyModifiers::HYPER, - )))), - ); - assert_eq!( - parse_csi_u_encoded_key_code(b"\x1B[57452u").unwrap(), - Some(InternalEvent::Event(Event::Key(KeyEvent::new( - KeyCode::Modifier(ModifierKeyCode::RightMeta), - KeyModifiers::META, - )))), - ); - } - - #[test] - fn test_parse_csi_u_encoded_key_code_with_extra_modifiers() { - assert_eq!( - parse_csi_u_encoded_key_code(b"\x1B[97;9u").unwrap(), - Some(InternalEvent::Event(Event::Key(KeyEvent::new( - KeyCode::Char('a'), - KeyModifiers::SUPER - )))), - ); - assert_eq!( - parse_csi_u_encoded_key_code(b"\x1B[97;17u").unwrap(), - Some(InternalEvent::Event(Event::Key(KeyEvent::new( - KeyCode::Char('a'), - KeyModifiers::HYPER, - )))), - ); - assert_eq!( - parse_csi_u_encoded_key_code(b"\x1B[97;33u").unwrap(), - Some(InternalEvent::Event(Event::Key(KeyEvent::new( - KeyCode::Char('a'), - KeyModifiers::META, - )))), - ); - } - - #[test] - fn test_parse_csi_u_encoded_key_code_with_extra_state() { - assert_eq!( - parse_csi_u_encoded_key_code(b"\x1B[97;65u").unwrap(), - Some(InternalEvent::Event(Event::Key( - KeyEvent::new_with_kind_and_state( - KeyCode::Char('a'), - KeyModifiers::empty(), - KeyEventKind::Press, - KeyEventState::CAPS_LOCK, - ) - ))), - ); - assert_eq!( - parse_csi_u_encoded_key_code(b"\x1B[49;129u").unwrap(), - Some(InternalEvent::Event(Event::Key( - KeyEvent::new_with_kind_and_state( - KeyCode::Char('1'), - KeyModifiers::empty(), - KeyEventKind::Press, - KeyEventState::NUM_LOCK, - ) - ))), - ); - } - - #[test] - fn test_parse_csi_u_with_shifted_keycode() { - assert_eq!( - // A-S-9 is equivalent to A-( - parse_event(b"\x1B[57:40;4u", false).unwrap(), - Some(InternalEvent::Event(Event::Key(KeyEvent::new( - KeyCode::Char('('), - KeyModifiers::ALT, - )))), - ); - assert_eq!( - // A-S-minus is equivalent to A-_ - parse_event(b"\x1B[45:95;4u", false).unwrap(), - Some(InternalEvent::Event(Event::Key(KeyEvent::new( - KeyCode::Char('_'), - KeyModifiers::ALT, - )))), - ); - } - - #[test] - fn test_parse_csi_special_key_code_with_types() { - assert_eq!( - parse_event(b"\x1B[;1:3B", false).unwrap(), - Some(InternalEvent::Event(Event::Key(KeyEvent::new_with_kind( - KeyCode::Down, - KeyModifiers::empty(), - KeyEventKind::Release, - )))), - ); - assert_eq!( - parse_event(b"\x1B[1;1:3B", false).unwrap(), - Some(InternalEvent::Event(Event::Key(KeyEvent::new_with_kind( - KeyCode::Down, - KeyModifiers::empty(), - KeyEventKind::Release, - )))), - ); - } - - #[test] - fn test_parse_csi_numbered_escape_code_with_types() { - assert_eq!( - parse_event(b"\x1B[5;1:3~", false).unwrap(), - Some(InternalEvent::Event(Event::Key(KeyEvent::new_with_kind( - KeyCode::PageUp, - KeyModifiers::empty(), - KeyEventKind::Release, - )))), - ); - assert_eq!( - parse_event(b"\x1B[6;5:3~", false).unwrap(), - Some(InternalEvent::Event(Event::Key(KeyEvent::new_with_kind( - KeyCode::PageDown, - KeyModifiers::CONTROL, - KeyEventKind::Release, - )))), - ); - } -} diff --git a/crates/util/keyfork-crossterm/src/event/sys/unix/waker.rs b/crates/util/keyfork-crossterm/src/event/sys/unix/waker.rs deleted file mode 100644 index 1c55f3b..0000000 --- a/crates/util/keyfork-crossterm/src/event/sys/unix/waker.rs +++ /dev/null @@ -1,11 +0,0 @@ -#[cfg(feature = "use-dev-tty")] -pub(crate) mod tty; - -#[cfg(not(feature = "use-dev-tty"))] -pub(crate) mod mio; - -#[cfg(feature = "use-dev-tty")] -pub(crate) use self::tty::Waker; - -#[cfg(not(feature = "use-dev-tty"))] -pub(crate) use self::mio::Waker; diff --git a/crates/util/keyfork-crossterm/src/event/sys/unix/waker/mio.rs b/crates/util/keyfork-crossterm/src/event/sys/unix/waker/mio.rs deleted file mode 100644 index 025db73..0000000 --- a/crates/util/keyfork-crossterm/src/event/sys/unix/waker/mio.rs +++ /dev/null @@ -1,34 +0,0 @@ -use std::sync::{Arc, Mutex}; - -use ::mio::{Registry, Token}; - -/// Allows to wake up the `mio::Poll::poll()` method. -/// This type wraps `mio::Waker`, for more information see its documentation. -#[derive(Clone, Debug)] -pub(crate) struct Waker { - inner: Arc>, -} - -impl Waker { - /// Create a new `Waker`. - pub(crate) fn new(registry: &Registry, waker_token: Token) -> std::io::Result { - Ok(Self { - inner: Arc::new(Mutex::new(mio::Waker::new(registry, waker_token)?)), - }) - } - - /// Wake up the [`Poll`] associated with this `Waker`. - /// - /// Readiness is set to `Ready::readable()`. - pub(crate) fn wake(&self) -> std::io::Result<()> { - self.inner.lock().unwrap().wake() - } - - /// Resets the state so the same waker can be reused. - /// - /// This function is not impl - #[allow(dead_code, clippy::clippy::unnecessary_wraps)] - pub(crate) fn reset(&self) -> std::io::Result<()> { - Ok(()) - } -} diff --git a/crates/util/keyfork-crossterm/src/event/sys/unix/waker/tty.rs b/crates/util/keyfork-crossterm/src/event/sys/unix/waker/tty.rs deleted file mode 100644 index 249d406..0000000 --- a/crates/util/keyfork-crossterm/src/event/sys/unix/waker/tty.rs +++ /dev/null @@ -1,28 +0,0 @@ -use std::{ - io::{self, Write}, - os::unix::net::UnixStream, - sync::{Arc, Mutex}, -}; - -/// Allows to wake up the EventSource::try_read() method. -#[derive(Clone, Debug)] -pub(crate) struct Waker { - inner: Arc>, -} - -impl Waker { - /// Create a new `Waker`. - pub(crate) fn new(writer: UnixStream) -> Self { - Self { - inner: Arc::new(Mutex::new(writer)), - } - } - - /// Wake up the [`Poll`] associated with this `Waker`. - /// - /// Readiness is set to `Ready::readable()`. - pub(crate) fn wake(&self) -> io::Result<()> { - self.inner.lock().unwrap().write(&[0])?; - Ok(()) - } -} diff --git a/crates/util/keyfork-crossterm/src/event/sys/windows.rs b/crates/util/keyfork-crossterm/src/event/sys/windows.rs deleted file mode 100644 index d405ae8..0000000 --- a/crates/util/keyfork-crossterm/src/event/sys/windows.rs +++ /dev/null @@ -1,48 +0,0 @@ -//! This is a WINDOWS specific implementation for input related action. - -use std::convert::TryFrom; -use std::io; -use std::sync::atomic::{AtomicU64, Ordering}; - -use crossterm_winapi::{ConsoleMode, Handle}; - -pub(crate) mod parse; -pub(crate) mod poll; -#[cfg(feature = "event-stream")] -pub(crate) mod waker; - -const ENABLE_MOUSE_MODE: u32 = 0x0010 | 0x0080 | 0x0008; - -/// This is a either `u64::MAX` if it's uninitialized or a valid `u32` that stores the original -/// console mode if it's initialized. -static ORIGINAL_CONSOLE_MODE: AtomicU64 = AtomicU64::new(u64::MAX); - -/// Initializes the default console color. It will will be skipped if it has already been initialized. -fn init_original_console_mode(original_mode: u32) { - let _ = ORIGINAL_CONSOLE_MODE.compare_exchange( - u64::MAX, - u64::from(original_mode), - Ordering::Relaxed, - Ordering::Relaxed, - ); -} - -/// Returns the original console color, make sure to call `init_console_color` before calling this function. Otherwise this function will panic. -fn original_console_mode() -> std::io::Result { - u32::try_from(ORIGINAL_CONSOLE_MODE.load(Ordering::Relaxed)) - .map_err(|_| io::Error::new(io::ErrorKind::Other, "Initial console modes not set")) -} - -pub(crate) fn enable_mouse_capture() -> std::io::Result<()> { - let mode = ConsoleMode::from(Handle::current_in_handle()?); - init_original_console_mode(mode.mode()?); - mode.set_mode(ENABLE_MOUSE_MODE)?; - - Ok(()) -} - -pub(crate) fn disable_mouse_capture() -> std::io::Result<()> { - let mode = ConsoleMode::from(Handle::current_in_handle()?); - mode.set_mode(original_console_mode()?)?; - Ok(()) -} diff --git a/crates/util/keyfork-crossterm/src/event/sys/windows/parse.rs b/crates/util/keyfork-crossterm/src/event/sys/windows/parse.rs deleted file mode 100644 index 97677ec..0000000 --- a/crates/util/keyfork-crossterm/src/event/sys/windows/parse.rs +++ /dev/null @@ -1,378 +0,0 @@ -use crossterm_winapi::{ControlKeyState, EventFlags, KeyEventRecord, ScreenBuffer}; -use winapi::um::{ - wincon::{ - CAPSLOCK_ON, LEFT_ALT_PRESSED, LEFT_CTRL_PRESSED, RIGHT_ALT_PRESSED, RIGHT_CTRL_PRESSED, - SHIFT_PRESSED, - }, - winuser::{ - GetForegroundWindow, GetKeyboardLayout, GetWindowThreadProcessId, ToUnicodeEx, VK_BACK, - VK_CONTROL, VK_DELETE, VK_DOWN, VK_END, VK_ESCAPE, VK_F1, VK_F24, VK_HOME, VK_INSERT, - VK_LEFT, VK_MENU, VK_NEXT, VK_NUMPAD0, VK_NUMPAD9, VK_PRIOR, VK_RETURN, VK_RIGHT, VK_SHIFT, - VK_TAB, VK_UP, - }, -}; - -use crate::event::{ - Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers, MouseButton, MouseEvent, MouseEventKind, -}; - -#[derive(Default)] -pub struct MouseButtonsPressed { - pub(crate) left: bool, - pub(crate) right: bool, - pub(crate) middle: bool, -} - -pub(crate) fn handle_mouse_event( - mouse_event: crossterm_winapi::MouseEvent, - buttons_pressed: &MouseButtonsPressed, -) -> Option { - if let Ok(Some(event)) = parse_mouse_event_record(&mouse_event, buttons_pressed) { - return Some(Event::Mouse(event)); - } - - None -} - -enum WindowsKeyEvent { - KeyEvent(KeyEvent), - Surrogate(u16), -} - -pub(crate) fn handle_key_event( - key_event: KeyEventRecord, - surrogate_buffer: &mut Option, -) -> Option { - let windows_key_event = parse_key_event_record(&key_event)?; - match windows_key_event { - WindowsKeyEvent::KeyEvent(key_event) => { - // Discard any buffered surrogate value if another valid key event comes before the - // next surrogate value. - *surrogate_buffer = None; - Some(Event::Key(key_event)) - } - WindowsKeyEvent::Surrogate(new_surrogate) => { - let ch = handle_surrogate(surrogate_buffer, new_surrogate)?; - let modifiers = KeyModifiers::from(&key_event.control_key_state); - let key_event = KeyEvent::new(KeyCode::Char(ch), modifiers); - Some(Event::Key(key_event)) - } - } -} - -fn handle_surrogate(surrogate_buffer: &mut Option, new_surrogate: u16) -> Option { - match *surrogate_buffer { - Some(buffered_surrogate) => { - *surrogate_buffer = None; - std::char::decode_utf16([buffered_surrogate, new_surrogate]) - .next() - .unwrap() - .ok() - } - None => { - *surrogate_buffer = Some(new_surrogate); - None - } - } -} - -impl From<&ControlKeyState> for KeyModifiers { - fn from(state: &ControlKeyState) -> Self { - let shift = state.has_state(SHIFT_PRESSED); - let alt = state.has_state(LEFT_ALT_PRESSED | RIGHT_ALT_PRESSED); - let control = state.has_state(LEFT_CTRL_PRESSED | RIGHT_CTRL_PRESSED); - - let mut modifier = KeyModifiers::empty(); - - if shift { - modifier |= KeyModifiers::SHIFT; - } - if control { - modifier |= KeyModifiers::CONTROL; - } - if alt { - modifier |= KeyModifiers::ALT; - } - - modifier - } -} - -enum CharCase { - LowerCase, - UpperCase, -} - -fn try_ensure_char_case(ch: char, desired_case: CharCase) -> char { - match desired_case { - CharCase::LowerCase if ch.is_uppercase() => { - let mut iter = ch.to_lowercase(); - // Unwrap is safe; iterator yields one or more chars. - let ch_lower = iter.next().unwrap(); - if iter.next().is_none() { - ch_lower - } else { - ch - } - } - CharCase::UpperCase if ch.is_lowercase() => { - let mut iter = ch.to_uppercase(); - // Unwrap is safe; iterator yields one or more chars. - let ch_upper = iter.next().unwrap(); - if iter.next().is_none() { - ch_upper - } else { - ch - } - } - _ => ch, - } -} - -// Attempts to return the character for a key event accounting for the user's keyboard layout. -// The returned character (if any) is capitalized (if applicable) based on shift and capslock state. -// Returns None if the key doesn't map to a character or if it is a dead key. -// We use the *currently* active keyboard layout (if it can be determined). This layout may not -// correspond to the keyboard layout that was active when the user typed their input, since console -// applications get their input asynchronously from the terminal. By the time a console application -// can process a key input, the user may have changed the active layout. In this case, the character -// returned might not correspond to what the user expects, but there is no way for a console -// application to know what the keyboard layout actually was for a key event, so this is our best -// effort. If a console application processes input in a timely fashion, then it is unlikely that a -// user has time to change their keyboard layout before a key event is processed. -fn get_char_for_key(key_event: &KeyEventRecord) -> Option { - let virtual_key_code = key_event.virtual_key_code as u32; - let virtual_scan_code = key_event.virtual_scan_code as u32; - let key_state = [0u8; 256]; - let mut utf16_buf = [0u16, 16]; - let dont_change_kernel_keyboard_state = 0x4; - - // Best-effort attempt at determining the currently active keyboard layout. - // At the time of writing, this works for a console application running in Windows Terminal, but - // doesn't work under a Conhost terminal. For Conhost, the window handle returned by - // GetForegroundWindow() does not appear to actually be the foreground window which has the - // keyboard layout associated with it (or perhaps it is, but also has special protection that - // doesn't allow us to query it). - // When this determination fails, the returned keyboard layout handle will be null, which is an - // acceptable input for ToUnicodeEx, as that argument is optional. In this case ToUnicodeEx - // appears to use the keyboard layout associated with the current thread, which will be the - // layout that was inherited when the console application started (or possibly when the current - // thread was spawned). This is then unfortunately not updated when the user changes their - // keyboard layout in the terminal, but it's what we get. - let active_keyboard_layout = unsafe { - let foreground_window = GetForegroundWindow(); - let foreground_thread = GetWindowThreadProcessId(foreground_window, std::ptr::null_mut()); - GetKeyboardLayout(foreground_thread) - }; - - let ret = unsafe { - ToUnicodeEx( - virtual_key_code, - virtual_scan_code, - key_state.as_ptr(), - utf16_buf.as_mut_ptr(), - utf16_buf.len() as i32, - dont_change_kernel_keyboard_state, - active_keyboard_layout, - ) - }; - - // -1 indicates a dead key. - // 0 indicates no character for this key. - if ret < 1 { - return None; - } - - let mut ch_iter = std::char::decode_utf16(utf16_buf.into_iter().take(ret as usize)); - let mut ch = ch_iter.next()?.ok()?; - if ch_iter.next().is_some() { - // Key doesn't map to a single char. - return None; - } - - let is_shift_pressed = key_event.control_key_state.has_state(SHIFT_PRESSED); - let is_capslock_on = key_event.control_key_state.has_state(CAPSLOCK_ON); - let desired_case = if is_shift_pressed ^ is_capslock_on { - CharCase::UpperCase - } else { - CharCase::LowerCase - }; - ch = try_ensure_char_case(ch, desired_case); - Some(ch) -} - -fn parse_key_event_record(key_event: &KeyEventRecord) -> Option { - let modifiers = KeyModifiers::from(&key_event.control_key_state); - let virtual_key_code = key_event.virtual_key_code as i32; - - // We normally ignore all key release events, but we will make an exception for an Alt key - // release if it carries a u_char value, as this indicates an Alt code. - let is_alt_code = virtual_key_code == VK_MENU && !key_event.key_down && key_event.u_char != 0; - if is_alt_code { - let utf16 = key_event.u_char; - match utf16 { - surrogate @ 0xD800..=0xDFFF => { - return Some(WindowsKeyEvent::Surrogate(surrogate)); - } - unicode_scalar_value => { - // Unwrap is safe: We tested for surrogate values above and those are the only - // u16 values that are invalid when directly interpreted as unicode scalar - // values. - let ch = std::char::from_u32(unicode_scalar_value as u32).unwrap(); - let key_code = KeyCode::Char(ch); - let kind = if key_event.key_down { - KeyEventKind::Press - } else { - KeyEventKind::Release - }; - let key_event = KeyEvent::new_with_kind(key_code, modifiers, kind); - return Some(WindowsKeyEvent::KeyEvent(key_event)); - } - } - } - - // Don't generate events for numpad key presses when they're producing Alt codes. - let is_numpad_numeric_key = (VK_NUMPAD0..=VK_NUMPAD9).contains(&virtual_key_code); - let is_only_alt_modifier = modifiers.contains(KeyModifiers::ALT) - && !modifiers.contains(KeyModifiers::SHIFT | KeyModifiers::CONTROL); - if is_only_alt_modifier && is_numpad_numeric_key { - return None; - } - - let parse_result = match virtual_key_code { - VK_SHIFT | VK_CONTROL | VK_MENU => None, - VK_BACK => Some(KeyCode::Backspace), - VK_ESCAPE => Some(KeyCode::Esc), - VK_RETURN => Some(KeyCode::Enter), - VK_F1..=VK_F24 => Some(KeyCode::F((key_event.virtual_key_code - 111) as u8)), - VK_LEFT => Some(KeyCode::Left), - VK_UP => Some(KeyCode::Up), - VK_RIGHT => Some(KeyCode::Right), - VK_DOWN => Some(KeyCode::Down), - VK_PRIOR => Some(KeyCode::PageUp), - VK_NEXT => Some(KeyCode::PageDown), - VK_HOME => Some(KeyCode::Home), - VK_END => Some(KeyCode::End), - VK_DELETE => Some(KeyCode::Delete), - VK_INSERT => Some(KeyCode::Insert), - VK_TAB if modifiers.contains(KeyModifiers::SHIFT) => Some(KeyCode::BackTab), - VK_TAB => Some(KeyCode::Tab), - _ => { - let utf16 = key_event.u_char; - match utf16 { - 0x00..=0x1f => { - // Some key combinations generate either no u_char value or generate control - // codes. To deliver back a KeyCode::Char(...) event we want to know which - // character the key normally maps to on the user's keyboard layout. - // The keys that intentionally generate control codes (ESC, ENTER, TAB, etc.) - // are handled by their virtual key codes above. - get_char_for_key(key_event).map(KeyCode::Char) - } - surrogate @ 0xD800..=0xDFFF => { - return Some(WindowsKeyEvent::Surrogate(surrogate)); - } - unicode_scalar_value => { - // Unwrap is safe: We tested for surrogate values above and those are the only - // u16 values that are invalid when directly interpreted as unicode scalar - // values. - let ch = std::char::from_u32(unicode_scalar_value as u32).unwrap(); - Some(KeyCode::Char(ch)) - } - } - } - }; - - if let Some(key_code) = parse_result { - let kind = if key_event.key_down { - KeyEventKind::Press - } else { - KeyEventKind::Release - }; - let key_event = KeyEvent::new_with_kind(key_code, modifiers, kind); - return Some(WindowsKeyEvent::KeyEvent(key_event)); - } - - None -} - -// The 'y' position of a mouse event or resize event is not relative to the window but absolute to screen buffer. -// This means that when the mouse cursor is at the top left it will be x: 0, y: 2295 (e.g. y = number of cells conting from the absolute buffer height) instead of relative x: 0, y: 0 to the window. -pub fn parse_relative_y(y: i16) -> std::io::Result { - let window_size = ScreenBuffer::current()?.info()?.terminal_window(); - Ok(y - window_size.top) -} - -fn parse_mouse_event_record( - event: &crossterm_winapi::MouseEvent, - buttons_pressed: &MouseButtonsPressed, -) -> std::io::Result> { - let modifiers = KeyModifiers::from(&event.control_key_state); - - let xpos = event.mouse_position.x as u16; - let ypos = parse_relative_y(event.mouse_position.y)? as u16; - - let button_state = event.button_state; - - let kind = match event.event_flags { - EventFlags::PressOrRelease | EventFlags::DoubleClick => { - if button_state.left_button() && !buttons_pressed.left { - Some(MouseEventKind::Down(MouseButton::Left)) - } else if !button_state.left_button() && buttons_pressed.left { - Some(MouseEventKind::Up(MouseButton::Left)) - } else if button_state.right_button() && !buttons_pressed.right { - Some(MouseEventKind::Down(MouseButton::Right)) - } else if !button_state.right_button() && buttons_pressed.right { - Some(MouseEventKind::Up(MouseButton::Right)) - } else if button_state.middle_button() && !buttons_pressed.middle { - Some(MouseEventKind::Down(MouseButton::Middle)) - } else if !button_state.middle_button() && buttons_pressed.middle { - Some(MouseEventKind::Up(MouseButton::Middle)) - } else { - None - } - } - EventFlags::MouseMoved => { - let button = if button_state.right_button() { - MouseButton::Right - } else if button_state.middle_button() { - MouseButton::Middle - } else { - MouseButton::Left - }; - if button_state.release_button() { - Some(MouseEventKind::Moved) - } else { - Some(MouseEventKind::Drag(button)) - } - } - EventFlags::MouseWheeled => { - // Vertical scroll - // from https://docs.microsoft.com/en-us/windows/console/mouse-event-record-str - // if `button_state` is negative then the wheel was rotated backward, toward the user. - if button_state.scroll_down() { - Some(MouseEventKind::ScrollDown) - } else if button_state.scroll_up() { - Some(MouseEventKind::ScrollUp) - } else { - None - } - } - EventFlags::MouseHwheeled => { - if button_state.scroll_left() { - Some(MouseEventKind::ScrollLeft) - } else if button_state.scroll_right() { - Some(MouseEventKind::ScrollRight) - } else { - None - } - } - _ => None, - }; - - Ok(kind.map(|kind| MouseEvent { - kind, - column: xpos, - row: ypos, - modifiers, - })) -} diff --git a/crates/util/keyfork-crossterm/src/event/sys/windows/poll.rs b/crates/util/keyfork-crossterm/src/event/sys/windows/poll.rs deleted file mode 100644 index 84d9bf7..0000000 --- a/crates/util/keyfork-crossterm/src/event/sys/windows/poll.rs +++ /dev/null @@ -1,86 +0,0 @@ -use std::io; -use std::time::Duration; - -use crossterm_winapi::Handle; -use winapi::{ - shared::winerror::WAIT_TIMEOUT, - um::{ - synchapi::WaitForMultipleObjects, - winbase::{INFINITE, WAIT_ABANDONED_0, WAIT_FAILED, WAIT_OBJECT_0}, - }, -}; - -#[cfg(feature = "event-stream")] -pub(crate) use super::waker::Waker; - -#[derive(Debug)] -pub(crate) struct WinApiPoll { - #[cfg(feature = "event-stream")] - waker: Waker, -} - -impl WinApiPoll { - #[cfg(not(feature = "event-stream"))] - pub(crate) fn new() -> WinApiPoll { - WinApiPoll {} - } - - #[cfg(feature = "event-stream")] - pub(crate) fn new() -> std::io::Result { - Ok(WinApiPoll { - waker: Waker::new()?, - }) - } -} - -impl WinApiPoll { - pub fn poll(&mut self, timeout: Option) -> std::io::Result> { - let dw_millis = if let Some(duration) = timeout { - duration.as_millis() as u32 - } else { - INFINITE - }; - - let console_handle = Handle::current_in_handle()?; - - #[cfg(feature = "event-stream")] - let semaphore = self.waker.semaphore(); - #[cfg(feature = "event-stream")] - let handles = &[*console_handle, **semaphore.handle()]; - #[cfg(not(feature = "event-stream"))] - let handles = &[*console_handle]; - - let output = - unsafe { WaitForMultipleObjects(handles.len() as u32, handles.as_ptr(), 0, dw_millis) }; - - match output { - output if output == WAIT_OBJECT_0 => { - // input handle triggered - Ok(Some(true)) - } - #[cfg(feature = "event-stream")] - output if output == WAIT_OBJECT_0 + 1 => { - // semaphore handle triggered - let _ = self.waker.reset(); - Err(io::Error::new( - io::ErrorKind::Interrupted, - "Poll operation was woken up by `Waker::wake`", - )) - } - WAIT_TIMEOUT | WAIT_ABANDONED_0 => { - // timeout elapsed - Ok(None) - } - WAIT_FAILED => Err(io::Error::last_os_error()), - _ => Err(io::Error::new( - io::ErrorKind::Other, - "WaitForMultipleObjects returned unexpected result.", - )), - } - } - - #[cfg(feature = "event-stream")] - pub fn waker(&self) -> Waker { - self.waker.clone() - } -} diff --git a/crates/util/keyfork-crossterm/src/event/sys/windows/waker.rs b/crates/util/keyfork-crossterm/src/event/sys/windows/waker.rs deleted file mode 100644 index 9ec582b..0000000 --- a/crates/util/keyfork-crossterm/src/event/sys/windows/waker.rs +++ /dev/null @@ -1,40 +0,0 @@ -use std::sync::{Arc, Mutex}; - -use crossterm_winapi::Semaphore; - -/// Allows to wake up the `WinApiPoll::poll()` method. -#[derive(Clone, Debug)] -pub(crate) struct Waker { - inner: Arc>, -} - -impl Waker { - /// Creates a new waker. - /// - /// `Waker` is based on the `Semaphore`. You have to use the semaphore - /// handle along with the `WaitForMultipleObjects`. - pub(crate) fn new() -> std::io::Result { - let inner = Semaphore::new()?; - - Ok(Self { - inner: Arc::new(Mutex::new(inner)), - }) - } - - /// Wakes the `WaitForMultipleObjects`. - pub(crate) fn wake(&self) -> std::io::Result<()> { - self.inner.lock().unwrap().release()?; - Ok(()) - } - - /// Replaces the current semaphore with a new one allowing us to reuse the same `Waker`. - pub(crate) fn reset(&self) -> std::io::Result<()> { - *self.inner.lock().unwrap() = Semaphore::new()?; - Ok(()) - } - - /// Returns the semaphore associated with the waker. - pub(crate) fn semaphore(&self) -> Semaphore { - self.inner.lock().unwrap().clone() - } -} diff --git a/crates/util/keyfork-crossterm/src/event/timeout.rs b/crates/util/keyfork-crossterm/src/event/timeout.rs deleted file mode 100644 index f266d28..0000000 --- a/crates/util/keyfork-crossterm/src/event/timeout.rs +++ /dev/null @@ -1,92 +0,0 @@ -use std::time::{Duration, Instant}; - -/// Keeps track of the elapsed time since the moment the polling started. -#[derive(Debug, Clone)] -pub struct PollTimeout { - timeout: Option, - start: Instant, -} - -impl PollTimeout { - /// Constructs a new `PollTimeout` with the given optional `Duration`. - pub fn new(timeout: Option) -> PollTimeout { - PollTimeout { - timeout, - start: Instant::now(), - } - } - - /// Returns whether the timeout has elapsed. - /// - /// It always returns `false` if the initial timeout was set to `None`. - pub fn elapsed(&self) -> bool { - self.timeout - .map(|timeout| self.start.elapsed() >= timeout) - .unwrap_or(false) - } - - /// Returns the timeout leftover (initial timeout duration - elapsed duration). - pub fn leftover(&self) -> Option { - self.timeout.map(|timeout| { - let elapsed = self.start.elapsed(); - - if elapsed >= timeout { - Duration::from_secs(0) - } else { - timeout - elapsed - } - }) - } -} - -#[cfg(test)] -mod tests { - use std::time::{Duration, Instant}; - - use super::PollTimeout; - - #[test] - pub fn test_timeout_without_duration_does_not_have_leftover() { - let timeout = PollTimeout::new(None); - assert_eq!(timeout.leftover(), None) - } - - #[test] - pub fn test_timeout_without_duration_never_elapses() { - let timeout = PollTimeout::new(None); - assert!(!timeout.elapsed()); - } - - #[test] - pub fn test_timeout_elapses() { - const TIMEOUT_MILLIS: u64 = 100; - - let timeout = PollTimeout { - timeout: Some(Duration::from_millis(TIMEOUT_MILLIS)), - start: Instant::now() - Duration::from_millis(2 * TIMEOUT_MILLIS), - }; - - assert!(timeout.elapsed()); - } - - #[test] - pub fn test_elapsed_timeout_has_zero_leftover() { - const TIMEOUT_MILLIS: u64 = 100; - - let timeout = PollTimeout { - timeout: Some(Duration::from_millis(TIMEOUT_MILLIS)), - start: Instant::now() - Duration::from_millis(2 * TIMEOUT_MILLIS), - }; - - assert!(timeout.elapsed()); - assert_eq!(timeout.leftover(), Some(Duration::from_millis(0))); - } - - #[test] - pub fn test_not_elapsed_timeout_has_positive_leftover() { - let timeout = PollTimeout::new(Some(Duration::from_secs(60))); - - assert!(!timeout.elapsed()); - assert!(timeout.leftover().unwrap() > Duration::from_secs(0)); - } -} diff --git a/crates/util/keyfork-crossterm/src/lib.rs b/crates/util/keyfork-crossterm/src/lib.rs deleted file mode 100644 index 4bcafcf..0000000 --- a/crates/util/keyfork-crossterm/src/lib.rs +++ /dev/null @@ -1,262 +0,0 @@ -#![allow(missing_docs, clippy::missing_errors_doc, clippy::missing_panics_doc)] -#![deny(unused_imports, unused_must_use)] -#![allow(clippy::pedantic, clippy::all, unexpected_cfgs)] - -//! # Cross-platform Terminal Manipulation Library -//! -//! Crossterm is a pure-rust, terminal manipulation library that makes it possible to write cross-platform text-based interfaces. -//! -//! This crate supports all UNIX and Windows terminals down to Windows 7 (not all terminals are tested -//! see [Tested Terminals](https://github.com/crossterm-rs/crossterm#tested-terminals) -//! for more info). -//! -//! ## Command API -//! -//! The command API makes the use of `crossterm` much easier and offers more control over when and how a -//! command is executed. A command is just an action you can perform on the terminal e.g. cursor movement. -//! -//! The command API offers: -//! -//! * Better Performance. -//! * Complete control over when to flush. -//! * Complete control over where the ANSI escape commands are executed to. -//! * Way easier and nicer API. -//! -//! There are two ways to use the API command: -//! -//! * Functions can execute commands on types that implement Write. Functions are easier to use and debug. -//! There is a disadvantage, and that is that there is a boilerplate code involved. -//! * Macros are generally seen as more difficult and aren't always well supported by editors but offer an API with less boilerplate code. If you are -//! not afraid of macros, this is a recommendation. -//! -//! Linux and Windows 10 systems support ANSI escape codes. Those ANSI escape codes are strings or rather a -//! byte sequence. When we `write` and `flush` those to the terminal we can perform some action. -//! For older windows systems a WinAPI call is made. -//! -//! ### Supported Commands -//! -//! - Module [`cursor`](cursor/index.html) -//! - Visibility - [`Show`](cursor/struct.Show.html), [`Hide`](cursor/struct.Hide.html) -//! - Appearance - [`EnableBlinking`](cursor/struct.EnableBlinking.html), -//! [`DisableBlinking`](cursor/struct.DisableBlinking.html), -//! [`SetCursorStyle`](cursor/enum.SetCursorStyle.html) -//! - Position - -//! [`SavePosition`](cursor/struct.SavePosition.html), [`RestorePosition`](cursor/struct.RestorePosition.html), -//! [`MoveUp`](cursor/struct.MoveUp.html), [`MoveDown`](cursor/struct.MoveDown.html), -//! [`MoveLeft`](cursor/struct.MoveLeft.html), [`MoveRight`](cursor/struct.MoveRight.html), -//! [`MoveTo`](cursor/struct.MoveTo.html), [`MoveToColumn`](cursor/struct.MoveToColumn.html),[`MoveToRow`](cursor/struct.MoveToRow.html), -//! [`MoveToNextLine`](cursor/struct.MoveToNextLine.html), [`MoveToPreviousLine`](cursor/struct.MoveToPreviousLine.html) -//! - Module [`event`](event/index.html) -//! - Keyboard events - -//! [`PushKeyboardEnhancementFlags`](event/struct.PushKeyboardEnhancementFlags.html), -//! [`PopKeyboardEnhancementFlags`](event/struct.PopKeyboardEnhancementFlags.html) -//! - Mouse events - [`EnableMouseCapture`](event/struct.EnableMouseCapture.html), -//! [`DisableMouseCapture`](event/struct.DisableMouseCapture.html) -//! - Module [`style`](style/index.html) -//! - Colors - [`SetForegroundColor`](style/struct.SetForegroundColor.html), -//! [`SetBackgroundColor`](style/struct.SetBackgroundColor.html), -//! [`ResetColor`](style/struct.ResetColor.html), [`SetColors`](style/struct.SetColors.html) -//! - Attributes - [`SetAttribute`](style/struct.SetAttribute.html), [`SetAttributes`](style/struct.SetAttributes.html), -//! [`PrintStyledContent`](style/struct.PrintStyledContent.html) -//! - Module [`terminal`](terminal/index.html) -//! - Scrolling - [`ScrollUp`](terminal/struct.ScrollUp.html), -//! [`ScrollDown`](terminal/struct.ScrollDown.html) -//! - Miscellaneous - [`Clear`](terminal/struct.Clear.html), -//! [`SetSize`](terminal/struct.SetSize.html), -//! [`SetTitle`](terminal/struct.SetTitle.html), -//! [`DisableLineWrap`](terminal/struct.DisableLineWrap.html), -//! [`EnableLineWrap`](terminal/struct.EnableLineWrap.html) -//! - Alternate screen - [`EnterAlternateScreen`](terminal/struct.EnterAlternateScreen.html), -//! [`LeaveAlternateScreen`](terminal/struct.LeaveAlternateScreen.html) -//! -//! ### Command Execution -//! -//! There are two different ways to execute commands: -//! -//! * [Lazy Execution](#lazy-execution) -//! * [Direct Execution](#direct-execution) -//! -//! #### Lazy Execution -//! -//! Flushing bytes to the terminal buffer is a heavy system call. If we perform a lot of actions with the terminal, -//! we want to do this periodically - like with a TUI editor - so that we can flush more data to the terminal buffer -//! at the same time. -//! -//! Crossterm offers the possibility to do this with `queue`. -//! With `queue` you can queue commands, and when you call [Write::flush][flush] these commands will be executed. -//! -//! You can pass a custom buffer implementing [std::io::Write][write] to this `queue` operation. -//! The commands will be executed on that buffer. -//! The most common buffer is [std::io::stdout][stdout] however, [std::io::stderr][stderr] is used sometimes as well. -//! -//! ##### Examples -//! -//! A simple demonstration that shows the command API in action with cursor commands. -//! -//! Functions: -//! -//! ```no_run -//! use std::io::{Write, stdout}; -//! use keyfork_crossterm::{QueueableCommand, cursor}; -//! -//! let mut stdout = stdout(); -//! stdout.queue(cursor::MoveTo(5,5)); -//! -//! // some other code ... -//! -//! stdout.flush(); -//! ``` -//! -//! The [queue](./trait.QueueableCommand.html) function returns itself, therefore you can use this to queue another -//! command. Like `stdout.queue(Goto(5,5)).queue(Clear(ClearType::All))`. -//! -//! Macros: -//! -//! ```no_run -//! use std::io::{Write, stdout}; -//! use keyfork_crossterm::{queue, QueueableCommand, cursor}; -//! -//! let mut stdout = stdout(); -//! queue!(stdout, cursor::MoveTo(5, 5)); -//! -//! // some other code ... -//! -//! // move operation is performed only if we flush the buffer. -//! stdout.flush(); -//! ``` -//! -//! You can pass more than one command into the [queue](./macro.queue.html) macro like -//! `queue!(stdout, MoveTo(5, 5), Clear(ClearType::All))` and -//! they will be executed in the given order from left to right. -//! -//! #### Direct Execution -//! -//! For many applications it is not at all important to be efficient with 'flush' operations. -//! For this use case there is the `execute` operation. -//! This operation executes the command immediately, and calls the `flush` under water. -//! -//! You can pass a custom buffer implementing [std::io::Write][write] to this `execute` operation. -//! The commands will be executed on that buffer. -//! The most common buffer is [std::io::stdout][stdout] however, [std::io::stderr][stderr] is used sometimes as well. -//! -//! ##### Examples -//! -//! Functions: -//! -//! ```no_run -//! use std::io::{Write, stdout}; -//! use keyfork_crossterm::{ExecutableCommand, cursor}; -//! -//! let mut stdout = stdout(); -//! stdout.execute(cursor::MoveTo(5,5)); -//! ``` -//! The [execute](./trait.ExecutableCommand.html) function returns itself, therefore you can use this to queue -//! another command. Like `stdout.execute(Goto(5,5))?.execute(Clear(ClearType::All))`. -//! -//! Macros: -//! -//! ```no_run -//! use std::io::{stdout, Write}; -//! use keyfork_crossterm::{execute, ExecutableCommand, cursor}; -//! -//! let mut stdout = stdout(); -//! execute!(stdout, cursor::MoveTo(5, 5)); -//! ``` -//! You can pass more than one command into the [execute](./macro.execute.html) macro like -//! `execute!(stdout, MoveTo(5, 5), Clear(ClearType::All))` and they will be executed in the given order from -//! left to right. -//! -//! ## Examples -//! -//! Print a rectangle colored with magenta and use both direct execution and lazy execution. -//! -//! Functions: -//! -//! ```no_run -//! use std::io::{self, Write}; -//! use keyfork_crossterm::{ -//! ExecutableCommand, QueueableCommand, -//! terminal, cursor, style::{self, Stylize} -//! }; -//! -//! fn main() -> io::Result<()> { -//! let mut stdout = io::stdout(); -//! -//! stdout.execute(terminal::Clear(terminal::ClearType::All))?; -//! -//! for y in 0..40 { -//! for x in 0..150 { -//! if (y == 0 || y == 40 - 1) || (x == 0 || x == 150 - 1) { -//! // in this loop we are more efficient by not flushing the buffer. -//! stdout -//! .queue(cursor::MoveTo(x,y))? -//! .queue(style::PrintStyledContent( "█".magenta()))?; -//! } -//! } -//! } -//! stdout.flush()?; -//! Ok(()) -//! } -//! ``` -//! -//! Macros: -//! -//! ```no_run -//! use std::io::{self, Write}; -//! use keyfork_crossterm::{ -//! execute, queue, -//! style::{self, Stylize}, cursor, terminal -//! }; -//! -//! fn main() -> io::Result<()> { -//! let mut stdout = io::stdout(); -//! -//! execute!(stdout, terminal::Clear(terminal::ClearType::All))?; -//! -//! for y in 0..40 { -//! for x in 0..150 { -//! if (y == 0 || y == 40 - 1) || (x == 0 || x == 150 - 1) { -//! // in this loop we are more efficient by not flushing the buffer. -//! queue!(stdout, cursor::MoveTo(x,y), style::PrintStyledContent( "█".magenta()))?; -//! } -//! } -//! } -//! stdout.flush()?; -//! Ok(()) -//! } -//!``` -//! -//! [write]: https://doc.rust-lang.org/std/io/trait.Write.html -//! [stdout]: https://doc.rust-lang.org/std/io/fn.stdout.html -//! [stderr]: https://doc.rust-lang.org/std/io/fn.stderr.html -//! [flush]: https://doc.rust-lang.org/std/io/trait.Write.html#tymethod.flush - -pub use crate::command::{Command, ExecutableCommand, QueueableCommand, SynchronizedUpdate}; - -/// A module to work with the terminal cursor -pub mod cursor; -/// A module to read events. -#[cfg(feature = "events")] -pub mod event; -/// A module to apply attributes and colors on your text. -pub mod style; -/// A module to work with the terminal. -pub mod terminal; - -/// A module to query if the current instance is a tty. -pub mod tty; - -#[cfg(windows)] -/// A module that exposes one function to check if the current terminal supports ANSI sequences. -pub mod ansi_support; -mod command; -pub(crate) mod macros; - -#[cfg(all(windows, not(feature = "windows")))] -compile_error!("Compiling on Windows with \"windows\" feature disabled. Feature \"windows\" should only be disabled when project will never be compiled on Windows."); - -#[cfg(all(winapi, not(feature = "winapi")))] -compile_error!("Compiling on Windows with \"winapi\" feature disabled. Feature \"winapi\" should only be disabled when project will never be compiled on Windows."); - -#[cfg(all(crossterm_winapi, not(feature = "crossterm_winapi")))] -compile_error!("Compiling on Windows with \"crossterm_winapi\" feature disabled. Feature \"crossterm_winapi\" should only be disabled when project will never be compiled on Windows."); diff --git a/crates/util/keyfork-crossterm/src/macros.rs b/crates/util/keyfork-crossterm/src/macros.rs deleted file mode 100644 index 6ceb414..0000000 --- a/crates/util/keyfork-crossterm/src/macros.rs +++ /dev/null @@ -1,370 +0,0 @@ -/// Append a the first few characters of an ANSI escape code to the given string. -#[macro_export] -#[doc(hidden)] -macro_rules! csi { - ($( $l:expr ),*) => { concat!("\x1B[", $( $l ),*) }; -} - -/// Queues one or more command(s) for further execution. -/// -/// Queued commands must be flushed to the underlying device to be executed. -/// This generally happens in the following cases: -/// -/// * When `flush` is called manually on the given type implementing `io::Write`. -/// * The terminal will `flush` automatically if the buffer is full. -/// * Each line is flushed in case of `stdout`, because it is line buffered. -/// -/// # Arguments -/// -/// - [std::io::Writer](std::io::Write) -/// -/// ANSI escape codes are written on the given 'writer', after which they are flushed. -/// -/// - [Command](./trait.Command.html) -/// -/// One or more commands -/// -/// # Examples -/// -/// ```rust -/// use std::io::{Write, stdout}; -/// use keyfork_crossterm::{queue, style::Print}; -/// -/// let mut stdout = stdout(); -/// -/// // `Print` will executed executed when `flush` is called. -/// queue!(stdout, Print("foo".to_string())); -/// -/// // some other code (no execution happening here) ... -/// -/// // when calling `flush` on `stdout`, all commands will be written to the stdout and therefore executed. -/// stdout.flush(); -/// -/// // ==== Output ==== -/// // foo -/// ``` -/// -/// Have a look over at the [Command API](./index.html#command-api) for more details. -/// -/// # Notes -/// -/// In case of Windows versions lower than 10, a direct WinAPI call will be made. -/// The reason for this is that Windows versions lower than 10 do not support ANSI codes, -/// and can therefore not be written to the given `writer`. -/// Therefore, there is no difference between [execute](macro.execute.html) -/// and [queue](macro.queue.html) for those old Windows versions. -/// -#[macro_export] -macro_rules! queue { - ($writer:expr $(, $command:expr)* $(,)?) => {{ - use ::std::io::Write; - - // This allows the macro to take both mut impl Write and &mut impl Write. - Ok($writer.by_ref()) - $(.and_then(|writer| $crate::QueueableCommand::queue(writer, $command)))* - .map(|_| ()) - }} -} - -/// Executes one or more command(s). -/// -/// # Arguments -/// -/// - [std::io::Writer](std::io::Write) -/// -/// ANSI escape codes are written on the given 'writer', after which they are flushed. -/// -/// - [Command](./trait.Command.html) -/// -/// One or more commands -/// -/// # Examples -/// -/// ```rust -/// use std::io::{Write, stdout}; -/// use keyfork_crossterm::{execute, style::Print}; -/// -/// // will be executed directly -/// execute!(stdout(), Print("sum:\n".to_string())); -/// -/// // will be executed directly -/// execute!(stdout(), Print("1 + 1 = ".to_string()), Print((1+1).to_string())); -/// -/// // ==== Output ==== -/// // sum: -/// // 1 + 1 = 2 -/// ``` -/// -/// Have a look over at the [Command API](./index.html#command-api) for more details. -/// -/// # Notes -/// -/// * In the case of UNIX and Windows 10, ANSI codes are written to the given 'writer'. -/// * In case of Windows versions lower than 10, a direct WinAPI call will be made. -/// The reason for this is that Windows versions lower than 10 do not support ANSI codes, -/// and can therefore not be written to the given `writer`. -/// Therefore, there is no difference between [execute](macro.execute.html) -/// and [queue](macro.queue.html) for those old Windows versions. -#[macro_export] -macro_rules! execute { - ($writer:expr $(, $command:expr)* $(,)? ) => {{ - use ::std::io::Write; - - // Queue each command, then flush - $crate::queue!($writer $(, $command)*) - .and_then(|()| { - ::std::io::Write::flush($writer.by_ref()) - }) - }} -} - -#[doc(hidden)] -#[macro_export] -macro_rules! impl_display { - (for $($t:ty),+) => { - $(impl ::std::fmt::Display for $t { - fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { - $crate::command::execute_fmt(f, self) - } - })* - } -} - -#[doc(hidden)] -#[macro_export] -macro_rules! impl_from { - ($from:path, $to:expr) => { - impl From<$from> for ErrorKind { - fn from(e: $from) -> Self { - $to(e) - } - } - }; -} - -#[cfg(test)] -mod tests { - use std::io; - use std::str; - - // Helper for execute tests to confirm flush - #[derive(Default, Debug, Clone)] - struct FakeWrite { - buffer: String, - flushed: bool, - } - - impl io::Write for FakeWrite { - fn write(&mut self, content: &[u8]) -> io::Result { - let content = str::from_utf8(content) - .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?; - self.buffer.push_str(content); - self.flushed = false; - Ok(content.len()) - } - - fn flush(&mut self) -> io::Result<()> { - self.flushed = true; - Ok(()) - } - } - - #[cfg(not(windows))] - mod unix { - use std::fmt; - - use super::FakeWrite; - use crate::command::Command; - - pub struct FakeCommand; - - impl Command for FakeCommand { - fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result { - f.write_str("cmd") - } - } - - #[test] - fn test_queue_one() { - let mut result = FakeWrite::default(); - queue!(&mut result, FakeCommand).unwrap(); - assert_eq!(&result.buffer, "cmd"); - assert!(!result.flushed); - } - - #[test] - fn test_queue_many() { - let mut result = FakeWrite::default(); - queue!(&mut result, FakeCommand, FakeCommand).unwrap(); - assert_eq!(&result.buffer, "cmdcmd"); - assert!(!result.flushed); - } - - #[test] - fn test_queue_trailing_comma() { - let mut result = FakeWrite::default(); - queue!(&mut result, FakeCommand, FakeCommand,).unwrap(); - assert_eq!(&result.buffer, "cmdcmd"); - assert!(!result.flushed); - } - - #[test] - fn test_execute_one() { - let mut result = FakeWrite::default(); - execute!(&mut result, FakeCommand).unwrap(); - assert_eq!(&result.buffer, "cmd"); - assert!(result.flushed); - } - - #[test] - fn test_execute_many() { - let mut result = FakeWrite::default(); - execute!(&mut result, FakeCommand, FakeCommand).unwrap(); - assert_eq!(&result.buffer, "cmdcmd"); - assert!(result.flushed); - } - - #[test] - fn test_execute_trailing_comma() { - let mut result = FakeWrite::default(); - execute!(&mut result, FakeCommand, FakeCommand,).unwrap(); - assert_eq!(&result.buffer, "cmdcmd"); - assert!(result.flushed); - } - } - - #[cfg(windows)] - mod windows { - use std::fmt; - - use std::cell::RefCell; - - use super::FakeWrite; - use crate::command::Command; - - // We need to test two different APIs: WinAPI and the write api. We - // don't know until runtime which we're supporting (via - // Command::is_ansi_code_supported), so we have to test them both. The - // CI environment hopefully includes both versions of windows. - - // WindowsEventStream is a place for execute_winapi to push strings, - // when called. - type WindowsEventStream = Vec<&'static str>; - - struct FakeCommand<'a> { - // Need to use a refcell because we want execute_winapi to be able - // push to the vector, but execute_winapi take &self. - stream: RefCell<&'a mut WindowsEventStream>, - value: &'static str, - } - - impl<'a> FakeCommand<'a> { - fn new(stream: &'a mut WindowsEventStream, value: &'static str) -> Self { - Self { - value, - stream: RefCell::new(stream), - } - } - } - - impl<'a> Command for FakeCommand<'a> { - fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result { - f.write_str(self.value) - } - - fn execute_winapi(&self) -> std::io::Result<()> { - self.stream.borrow_mut().push(self.value); - Ok(()) - } - } - - // Helper function for running tests against either WinAPI or an - // io::Write. - // - // This function will execute the `test` function, which should - // queue some commands against the given FakeWrite and - // WindowsEventStream. It will then test that the correct data sink - // was populated. It does not currently check is_ansi_code_supported; - // for now it simply checks that one of the two streams was correctly - // populated. - // - // If the stream was populated, it tests that the two arrays are equal. - // If the writer was populated, it tests that the contents of the - // write buffer are equal to the concatenation of `stream_result`. - fn test_harness( - stream_result: &[&'static str], - test: impl FnOnce(&mut FakeWrite, &mut WindowsEventStream) -> std::io::Result<()>, - ) { - let mut stream = WindowsEventStream::default(); - let mut writer = FakeWrite::default(); - - if let Err(err) = test(&mut writer, &mut stream) { - panic!("Error returned from test function: {:?}", err); - } - - // We need this for type inference, for whatever reason. - const EMPTY_RESULT: [&str; 0] = []; - - // TODO: confirm that the correct sink was used, based on - // is_ansi_code_supported - match (writer.buffer.is_empty(), stream.is_empty()) { - (true, true) if stream_result == EMPTY_RESULT => {} - (true, true) => panic!( - "Neither the event stream nor the writer were populated. Expected {:?}", - stream_result - ), - - // writer is populated - (false, true) => { - // Concat the stream result to find the string result - let result: String = stream_result.iter().copied().collect(); - assert_eq!(result, writer.buffer); - assert_eq!(&stream, &EMPTY_RESULT); - } - - // stream is populated - (true, false) => { - assert_eq!(stream, stream_result); - assert_eq!(writer.buffer, ""); - } - - // Both are populated - (false, false) => panic!( - "Both the writer and the event stream were written to.\n\ - Only one should be used, based on is_ansi_code_supported.\n\ - stream: {stream:?}\n\ - writer: {writer:?}", - stream = stream, - writer = writer, - ), - } - } - - #[test] - fn test_queue_one() { - test_harness(&["cmd1"], |writer, stream| { - queue!(writer, FakeCommand::new(stream, "cmd1")) - }) - } - - #[test] - fn test_queue_some() { - test_harness(&["cmd1", "cmd2"], |writer, stream| { - queue!( - writer, - FakeCommand::new(stream, "cmd1"), - FakeCommand::new(stream, "cmd2"), - ) - }) - } - - #[test] - fn test_many_queues() { - test_harness(&["cmd1", "cmd2", "cmd3"], |writer, stream| { - queue!(writer, FakeCommand::new(stream, "cmd1"))?; - queue!(writer, FakeCommand::new(stream, "cmd2"))?; - queue!(writer, FakeCommand::new(stream, "cmd3")) - }) - } - } -} diff --git a/crates/util/keyfork-crossterm/src/style.rs b/crates/util/keyfork-crossterm/src/style.rs deleted file mode 100644 index aad11f8..0000000 --- a/crates/util/keyfork-crossterm/src/style.rs +++ /dev/null @@ -1,510 +0,0 @@ -//! # Style -//! -//! The `style` module provides a functionality to apply attributes and colors on your text. -//! -//! This documentation does not contain a lot of examples. The reason is that it's fairly -//! obvious how to use this crate. Although, we do provide -//! [examples](https://github.com/crossterm-rs/crossterm/tree/master/examples) repository -//! to demonstrate the capabilities. -//! -//! ## Platform-specific Notes -//! -//! Not all features are supported on all terminals/platforms. You should always consult -//! platform-specific notes of the following types: -//! -//! * [Color](enum.Color.html#platform-specific-notes) -//! * [Attribute](enum.Attribute.html#platform-specific-notes) -//! -//! ## Examples -//! -//! A few examples of how to use the style module. -//! -//! ### Colors -//! -//! How to change the terminal text color. -//! -//! Command API: -//! -//! Using the Command API to color text. -//! -//! ```no_run -//! use std::io::{self, Write}; -//! use keyfork_crossterm::execute; -//! use keyfork_crossterm::style::{Print, SetForegroundColor, SetBackgroundColor, ResetColor, Color, Attribute}; -//! -//! fn main() -> io::Result<()> { -//! execute!( -//! io::stdout(), -//! // Blue foreground -//! SetForegroundColor(Color::Blue), -//! // Red background -//! SetBackgroundColor(Color::Red), -//! // Print text -//! Print("Blue text on Red.".to_string()), -//! // Reset to default colors -//! ResetColor -//! ) -//! } -//! ``` -//! -//! Functions: -//! -//! Using functions from [`Stylize`](crate::style::Stylize) on a `String` or `&'static str` to color -//! it. -//! -//! ```no_run -//! use keyfork_crossterm::style::Stylize; -//! -//! println!("{}", "Red foreground color & blue background.".red().on_blue()); -//! ``` -//! -//! ### Attributes -//! -//! How to apply terminal attributes to text. -//! -//! Command API: -//! -//! Using the Command API to set attributes. -//! -//! ```no_run -//! use std::io::{self, Write}; -//! -//! use keyfork_crossterm::execute; -//! use keyfork_crossterm::style::{Attribute, Print, SetAttribute}; -//! -//! fn main() -> io::Result<()> { -//! execute!( -//! io::stdout(), -//! // Set to bold -//! SetAttribute(Attribute::Bold), -//! Print("Bold text here.".to_string()), -//! // Reset all attributes -//! SetAttribute(Attribute::Reset) -//! ) -//! } -//! ``` -//! -//! Functions: -//! -//! Using [`Stylize`](crate::style::Stylize) functions on a `String` or `&'static str` to set -//! attributes to it. -//! -//! ```no_run -//! use keyfork_crossterm::style::Stylize; -//! -//! println!("{}", "Bold".bold()); -//! println!("{}", "Underlined".underlined()); -//! println!("{}", "Negative".negative()); -//! ``` -//! -//! Displayable: -//! -//! [`Attribute`](enum.Attribute.html) implements [Display](https://doc.rust-lang.org/beta/std/fmt/trait.Display.html) and therefore it can be formatted like: -//! -//! ```no_run -//! use keyfork_crossterm::style::Attribute; -//! -//! println!( -//! "{} Underlined {} No Underline", -//! Attribute::Underlined, -//! Attribute::NoUnderline -//! ); -//! ``` - -use std::{ - env, - fmt::{self, Display}, -}; - -use crate::command::execute_fmt; -use crate::{csi, impl_display, Command}; - -pub use self::{ - attributes::Attributes, - content_style::ContentStyle, - styled_content::StyledContent, - stylize::Stylize, - types::{Attribute, Color, Colored, Colors}, -}; - -mod attributes; -mod content_style; -mod styled_content; -mod stylize; -mod sys; -mod types; - -/// Creates a `StyledContent`. -/// -/// This could be used to style any type that implements `Display` with colors and text attributes. -/// -/// See [`StyledContent`](struct.StyledContent.html) for more info. -/// -/// # Examples -/// -/// ```no_run -/// use keyfork_crossterm::style::{style, Stylize, Color}; -/// -/// let styled_content = style("Blue colored text on yellow background") -/// .with(Color::Blue) -/// .on(Color::Yellow); -/// -/// println!("{}", styled_content); -/// ``` -pub fn style(val: D) -> StyledContent { - ContentStyle::new().apply(val) -} - -/// Returns available color count. -/// -/// # Notes -/// -/// This does not always provide a good result. -pub fn available_color_count() -> u16 { - env::var("TERM") - .map(|x| if x.contains("256color") { 256 } else { 8 }) - .unwrap_or(8) -} - -/// Forces colored output on or off globally, overriding NO_COLOR. -/// -/// # Notes -/// -/// crossterm supports NO_COLOR () to disabled colored output. -/// -/// This API allows applications to override that behavior and force colorized output -/// even if NO_COLOR is set. -pub fn force_color_output(enabled: bool) { - Colored::set_ansi_color_disabled(!enabled) -} - -/// A command that sets the the foreground color. -/// -/// See [`Color`](enum.Color.html) for more info. -/// -/// [`SetColors`](struct.SetColors.html) can also be used to set both the foreground and background -/// color in one command. -/// -/// # Notes -/// -/// Commands must be executed/queued for execution otherwise they do nothing. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct SetForegroundColor(pub Color); - -impl Command for SetForegroundColor { - fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result { - write!(f, csi!("{}m"), Colored::ForegroundColor(self.0)) - } - - #[cfg(windows)] - fn execute_winapi(&self) -> std::io::Result<()> { - sys::windows::set_foreground_color(self.0) - } -} - -/// A command that sets the the background color. -/// -/// See [`Color`](enum.Color.html) for more info. -/// -/// [`SetColors`](struct.SetColors.html) can also be used to set both the foreground and background -/// color with one command. -/// -/// # Notes -/// -/// Commands must be executed/queued for execution otherwise they do nothing. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct SetBackgroundColor(pub Color); - -impl Command for SetBackgroundColor { - fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result { - write!(f, csi!("{}m"), Colored::BackgroundColor(self.0)) - } - - #[cfg(windows)] - fn execute_winapi(&self) -> std::io::Result<()> { - sys::windows::set_background_color(self.0) - } -} - -/// A command that sets the the underline color. -/// -/// See [`Color`](enum.Color.html) for more info. -/// -/// [`SetColors`](struct.SetColors.html) can also be used to set both the foreground and background -/// color with one command. -/// -/// # Notes -/// -/// Commands must be executed/queued for execution otherwise they do nothing. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct SetUnderlineColor(pub Color); - -impl Command for SetUnderlineColor { - fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result { - write!(f, csi!("{}m"), Colored::UnderlineColor(self.0)) - } - - #[cfg(windows)] - fn execute_winapi(&self) -> std::io::Result<()> { - Err(std::io::Error::new( - std::io::ErrorKind::Other, - "SetUnderlineColor not supported by winapi.", - )) - } -} - -/// A command that optionally sets the foreground and/or background color. -/// -/// For example: -/// ```no_run -/// use std::io::{stdout, Write}; -/// -/// use keyfork_crossterm::execute; -/// use keyfork_crossterm::style::{Color::{Green, Black}, Colors, Print, SetColors}; -/// -/// execute!( -/// stdout(), -/// SetColors(Colors::new(Green, Black)), -/// Print("Hello, world!".to_string()), -/// ).unwrap(); -/// ``` -/// -/// See [`Colors`](struct.Colors.html) for more info. -/// -/// # Notes -/// -/// Commands must be executed/queued for execution otherwise they do nothing. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct SetColors(pub Colors); - -impl Command for SetColors { - fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result { - if let Some(color) = self.0.foreground { - SetForegroundColor(color).write_ansi(f)?; - } - if let Some(color) = self.0.background { - SetBackgroundColor(color).write_ansi(f)?; - } - Ok(()) - } - - #[cfg(windows)] - fn execute_winapi(&self) -> std::io::Result<()> { - if let Some(color) = self.0.foreground { - sys::windows::set_foreground_color(color)?; - } - if let Some(color) = self.0.background { - sys::windows::set_background_color(color)?; - } - Ok(()) - } -} - -/// A command that sets an attribute. -/// -/// See [`Attribute`](enum.Attribute.html) for more info. -/// -/// # Notes -/// -/// Commands must be executed/queued for execution otherwise they do nothing. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct SetAttribute(pub Attribute); - -impl Command for SetAttribute { - fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result { - write!(f, csi!("{}m"), self.0.sgr()) - } - - #[cfg(windows)] - fn execute_winapi(&self) -> std::io::Result<()> { - // attributes are not supported by WinAPI. - Ok(()) - } -} - -/// A command that sets several attributes. -/// -/// See [`Attributes`](struct.Attributes.html) for more info. -/// -/// # Notes -/// -/// Commands must be executed/queued for execution otherwise they do nothing. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct SetAttributes(pub Attributes); - -impl Command for SetAttributes { - fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result { - for attr in Attribute::iterator() { - if self.0.has(attr) { - SetAttribute(attr).write_ansi(f)?; - } - } - Ok(()) - } - - #[cfg(windows)] - fn execute_winapi(&self) -> std::io::Result<()> { - // attributes are not supported by WinAPI. - Ok(()) - } -} - -/// A command that sets a style (colors and attributes). -/// -/// # Notes -/// -/// Commands must be executed/queued for execution otherwise they do nothing. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct SetStyle(pub ContentStyle); - -impl Command for SetStyle { - fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result { - if let Some(bg) = self.0.background_color { - execute_fmt(f, SetBackgroundColor(bg)).map_err(|_| fmt::Error)?; - } - if let Some(fg) = self.0.foreground_color { - execute_fmt(f, SetForegroundColor(fg)).map_err(|_| fmt::Error)?; - } - if let Some(ul) = self.0.underline_color { - execute_fmt(f, SetUnderlineColor(ul)).map_err(|_| fmt::Error)?; - } - if !self.0.attributes.is_empty() { - execute_fmt(f, SetAttributes(self.0.attributes)).map_err(|_| fmt::Error)?; - } - - Ok(()) - } - - #[cfg(windows)] - fn execute_winapi(&self) -> std::io::Result<()> { - panic!("tried to execute SetStyle command using WinAPI, use ANSI instead"); - } - - #[cfg(windows)] - fn is_ansi_code_supported(&self) -> bool { - true - } -} - -/// A command that prints styled content. -/// -/// See [`StyledContent`](struct.StyledContent.html) for more info. -/// -/// # Notes -/// -/// Commands must be executed/queued for execution otherwise they do nothing. -#[derive(Debug, Copy, Clone)] -pub struct PrintStyledContent(pub StyledContent); - -impl Command for PrintStyledContent { - fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result { - let style = self.0.style(); - - let mut reset_background = false; - let mut reset_foreground = false; - let mut reset = false; - - if let Some(bg) = style.background_color { - execute_fmt(f, SetBackgroundColor(bg)).map_err(|_| fmt::Error)?; - reset_background = true; - } - if let Some(fg) = style.foreground_color { - execute_fmt(f, SetForegroundColor(fg)).map_err(|_| fmt::Error)?; - reset_foreground = true; - } - if let Some(ul) = style.underline_color { - execute_fmt(f, SetUnderlineColor(ul)).map_err(|_| fmt::Error)?; - reset_foreground = true; - } - - if !style.attributes.is_empty() { - execute_fmt(f, SetAttributes(style.attributes)).map_err(|_| fmt::Error)?; - reset = true; - } - - write!(f, "{}", self.0.content())?; - - if reset { - // NOTE: This will reset colors even though self has no colors, hence produce unexpected - // resets. - // TODO: reset the set attributes only. - execute_fmt(f, ResetColor).map_err(|_| fmt::Error)?; - } else { - // NOTE: Since the above bug, we do not need to reset colors when we reset attributes. - if reset_background { - execute_fmt(f, SetBackgroundColor(Color::Reset)).map_err(|_| fmt::Error)?; - } - if reset_foreground { - execute_fmt(f, SetForegroundColor(Color::Reset)).map_err(|_| fmt::Error)?; - } - } - - Ok(()) - } - - #[cfg(windows)] - fn execute_winapi(&self) -> std::io::Result<()> { - Ok(()) - } -} - -/// A command that resets the colors back to default. -/// -/// # Notes -/// -/// Commands must be executed/queued for execution otherwise they do nothing. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct ResetColor; - -impl Command for ResetColor { - fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result { - f.write_str(csi!("0m")) - } - - #[cfg(windows)] - fn execute_winapi(&self) -> std::io::Result<()> { - sys::windows::reset() - } -} - -/// A command that prints the given displayable type. -/// -/// Commands must be executed/queued for execution otherwise they do nothing. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct Print(pub T); - -impl Command for Print { - fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result { - write!(f, "{}", self.0) - } - - #[cfg(windows)] - fn execute_winapi(&self) -> std::io::Result<()> { - panic!("tried to execute Print command using WinAPI, use ANSI instead"); - } - - #[cfg(windows)] - fn is_ansi_code_supported(&self) -> bool { - true - } -} - -impl Display for Print { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - self.0.fmt(f) - } -} - -impl_display!(for SetForegroundColor); -impl_display!(for SetBackgroundColor); -impl_display!(for SetColors); -impl_display!(for SetAttribute); -impl_display!(for PrintStyledContent); -impl_display!(for PrintStyledContent<&'static str>); -impl_display!(for ResetColor); - -/// Utility function for ANSI parsing in Color and Colored. -/// Gets the next element of `iter` and tries to parse it as a `u8`. -fn parse_next_u8<'a>(iter: &mut impl Iterator) -> Option { - iter.next().and_then(|s| s.parse().ok()) -} diff --git a/crates/util/keyfork-crossterm/src/style/attributes.rs b/crates/util/keyfork-crossterm/src/style/attributes.rs deleted file mode 100644 index 83dea3e..0000000 --- a/crates/util/keyfork-crossterm/src/style/attributes.rs +++ /dev/null @@ -1,150 +0,0 @@ -use std::ops::{BitAnd, BitOr, BitXor}; - -use crate::style::Attribute; - -/// a bitset for all possible attributes -#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] -pub struct Attributes(u32); - -impl From for Attributes { - fn from(attribute: Attribute) -> Self { - Self(attribute.bytes()) - } -} - -impl From<&[Attribute]> for Attributes { - fn from(arr: &[Attribute]) -> Self { - let mut attributes = Attributes::default(); - for &attr in arr { - attributes.set(attr); - } - attributes - } -} - -impl BitAnd for Attributes { - type Output = Self; - fn bitand(self, rhs: Attribute) -> Self { - Self(self.0 & rhs.bytes()) - } -} -impl BitAnd for Attributes { - type Output = Self; - fn bitand(self, rhs: Self) -> Self { - Self(self.0 & rhs.0) - } -} - -impl BitOr for Attributes { - type Output = Self; - fn bitor(self, rhs: Attribute) -> Self { - Self(self.0 | rhs.bytes()) - } -} -impl BitOr for Attributes { - type Output = Self; - fn bitor(self, rhs: Self) -> Self { - Self(self.0 | rhs.0) - } -} - -impl BitXor for Attributes { - type Output = Self; - fn bitxor(self, rhs: Attribute) -> Self { - Self(self.0 ^ rhs.bytes()) - } -} -impl BitXor for Attributes { - type Output = Self; - fn bitxor(self, rhs: Self) -> Self { - Self(self.0 ^ rhs.0) - } -} - -impl Attributes { - /// Returns the empty bitset. - #[inline(always)] - pub const fn none() -> Self { - Self(0) - } - - /// Returns a copy of the bitset with the given attribute set. - /// If it's already set, this returns the bitset unmodified. - #[inline(always)] - pub const fn with(self, attribute: Attribute) -> Self { - Self(self.0 | attribute.bytes()) - } - - /// Returns a copy of the bitset with the given attribute unset. - /// If it's not set, this returns the bitset unmodified. - #[inline(always)] - pub const fn without(self, attribute: Attribute) -> Self { - Self(self.0 & !attribute.bytes()) - } - - /// Sets the attribute. - /// If it's already set, this does nothing. - #[inline(always)] - pub fn set(&mut self, attribute: Attribute) { - self.0 |= attribute.bytes(); - } - - /// Unsets the attribute. - /// If it's not set, this changes nothing. - #[inline(always)] - pub fn unset(&mut self, attribute: Attribute) { - self.0 &= !attribute.bytes(); - } - - /// Sets the attribute if it's unset, unset it - /// if it is set. - #[inline(always)] - pub fn toggle(&mut self, attribute: Attribute) { - self.0 ^= attribute.bytes(); - } - - /// Returns whether the attribute is set. - #[inline(always)] - pub const fn has(self, attribute: Attribute) -> bool { - self.0 & attribute.bytes() != 0 - } - - /// Sets all the passed attributes. Removes none. - #[inline(always)] - pub fn extend(&mut self, attributes: Attributes) { - self.0 |= attributes.0; - } - - /// Returns whether there is no attribute set. - #[inline(always)] - pub const fn is_empty(self) -> bool { - self.0 == 0 - } -} - -#[cfg(test)] -mod tests { - use super::{Attribute, Attributes}; - - #[test] - fn test_attributes() { - let mut attributes: Attributes = Attribute::Bold.into(); - assert!(attributes.has(Attribute::Bold)); - attributes.set(Attribute::Italic); - assert!(attributes.has(Attribute::Italic)); - attributes.unset(Attribute::Italic); - assert!(!attributes.has(Attribute::Italic)); - attributes.toggle(Attribute::Bold); - assert!(attributes.is_empty()); - } - - #[test] - fn test_attributes_const() { - const ATTRIBUTES: Attributes = Attributes::none() - .with(Attribute::Bold) - .with(Attribute::Italic) - .without(Attribute::Bold); - assert!(!ATTRIBUTES.has(Attribute::Bold)); - assert!(ATTRIBUTES.has(Attribute::Italic)); - } -} diff --git a/crates/util/keyfork-crossterm/src/style/content_style.rs b/crates/util/keyfork-crossterm/src/style/content_style.rs deleted file mode 100644 index 6e99bb6..0000000 --- a/crates/util/keyfork-crossterm/src/style/content_style.rs +++ /dev/null @@ -1,43 +0,0 @@ -//! This module contains the `content style` that can be applied to an `styled content`. - -use std::fmt::Display; - -use crate::style::{Attributes, Color, StyledContent}; - -/// The style that can be put on content. -#[derive(Debug, Copy, Clone, Default, PartialEq, Eq)] -pub struct ContentStyle { - /// The foreground color. - pub foreground_color: Option, - /// The background color. - pub background_color: Option, - /// The underline color. - pub underline_color: Option, - /// List of attributes. - pub attributes: Attributes, -} - -impl ContentStyle { - /// Creates a `StyledContent` by applying the style to the given `val`. - #[inline] - pub fn apply(self, val: D) -> StyledContent { - StyledContent::new(self, val) - } - - /// Creates a new `ContentStyle`. - #[inline] - pub fn new() -> ContentStyle { - ContentStyle::default() - } -} - -impl AsRef for ContentStyle { - fn as_ref(&self) -> &Self { - self - } -} -impl AsMut for ContentStyle { - fn as_mut(&mut self) -> &mut Self { - self - } -} diff --git a/crates/util/keyfork-crossterm/src/style/styled_content.rs b/crates/util/keyfork-crossterm/src/style/styled_content.rs deleted file mode 100644 index 2c38b7e..0000000 --- a/crates/util/keyfork-crossterm/src/style/styled_content.rs +++ /dev/null @@ -1,77 +0,0 @@ -//! This module contains the logic to style some content. - -use std::fmt::{self, Display, Formatter}; - -use super::{ContentStyle, PrintStyledContent}; - -/// The style with the content to be styled. -/// -/// # Examples -/// -/// ```rust -/// use keyfork_crossterm::style::{style, Color, Attribute, Stylize}; -/// -/// let styled = "Hello there" -/// .with(Color::Yellow) -/// .on(Color::Blue) -/// .attribute(Attribute::Bold); -/// -/// println!("{}", styled); -/// ``` -#[derive(Copy, Clone, Debug, Eq, PartialEq)] -pub struct StyledContent { - /// The style (colors, content attributes). - style: ContentStyle, - /// A content to apply the style on. - content: D, -} - -impl StyledContent { - /// Creates a new `StyledContent`. - #[inline] - pub fn new(style: ContentStyle, content: D) -> StyledContent { - StyledContent { style, content } - } - - /// Returns the content. - #[inline] - pub fn content(&self) -> &D { - &self.content - } - - /// Returns the style. - #[inline] - pub fn style(&self) -> &ContentStyle { - &self.style - } - - /// Returns a mutable reference to the style, so that it can be further - /// manipulated - #[inline] - pub fn style_mut(&mut self) -> &mut ContentStyle { - &mut self.style - } -} - -impl AsRef for StyledContent { - fn as_ref(&self) -> &ContentStyle { - &self.style - } -} -impl AsMut for StyledContent { - fn as_mut(&mut self) -> &mut ContentStyle { - &mut self.style - } -} - -impl Display for StyledContent { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - crate::command::execute_fmt( - f, - PrintStyledContent(StyledContent { - style: self.style, - content: &self.content, - }), - ) - } -} diff --git a/crates/util/keyfork-crossterm/src/style/stylize.rs b/crates/util/keyfork-crossterm/src/style/stylize.rs deleted file mode 100644 index 0b51771..0000000 --- a/crates/util/keyfork-crossterm/src/style/stylize.rs +++ /dev/null @@ -1,200 +0,0 @@ -use std::fmt::Display; - -use super::{style, Attribute, Color, ContentStyle, StyledContent}; - -macro_rules! stylize_method { - ($method_name:ident Attribute::$attribute:ident) => { - calculated_docs! { - #[doc = concat!( - "Applies the [`", - stringify!($attribute), - "`](Attribute::", - stringify!($attribute), - ") attribute to the text.", - )] - fn $method_name(self) -> Self::Styled { - self.attribute(Attribute::$attribute) - } - } - }; - ($method_name_fg:ident, $method_name_bg:ident, $method_name_ul:ident Color::$color:ident) => { - calculated_docs! { - #[doc = concat!( - "Sets the foreground color to [`", - stringify!($color), - "`](Color::", - stringify!($color), - ")." - )] - fn $method_name_fg(self) -> Self::Styled { - self.with(Color::$color) - } - - #[doc = concat!( - "Sets the background color to [`", - stringify!($color), - "`](Color::", - stringify!($color), - ")." - )] - fn $method_name_bg(self) -> Self::Styled { - self.on(Color::$color) - } - - #[doc = concat!( - "Sets the underline color to [`", - stringify!($color), - "`](Color::", - stringify!($color), - ")." - )] - fn $method_name_ul(self) -> Self::Styled { - self.underline(Color::$color) - } - } - }; -} - -/// Provides a set of methods to set attributes and colors. -/// -/// # Examples -/// -/// ```no_run -/// use keyfork_crossterm::style::Stylize; -/// -/// println!("{}", "Bold text".bold()); -/// println!("{}", "Underlined text".underlined()); -/// println!("{}", "Negative text".negative()); -/// println!("{}", "Red on blue".red().on_blue()); -/// ``` -pub trait Stylize: Sized { - /// This type with styles applied. - type Styled: AsRef + AsMut; - - /// Styles this type. - fn stylize(self) -> Self::Styled; - - /// Sets the foreground color. - fn with(self, color: Color) -> Self::Styled { - let mut styled = self.stylize(); - styled.as_mut().foreground_color = Some(color); - styled - } - - /// Sets the background color. - fn on(self, color: Color) -> Self::Styled { - let mut styled = self.stylize(); - styled.as_mut().background_color = Some(color); - styled - } - - /// Sets the underline color. - fn underline(self, color: Color) -> Self::Styled { - let mut styled = self.stylize(); - styled.as_mut().underline_color = Some(color); - styled - } - - /// Styles the content with the attribute. - fn attribute(self, attr: Attribute) -> Self::Styled { - let mut styled = self.stylize(); - styled.as_mut().attributes.set(attr); - styled - } - - stylize_method!(reset Attribute::Reset); - stylize_method!(bold Attribute::Bold); - stylize_method!(underlined Attribute::Underlined); - stylize_method!(reverse Attribute::Reverse); - stylize_method!(dim Attribute::Dim); - stylize_method!(italic Attribute::Italic); - stylize_method!(negative Attribute::Reverse); - stylize_method!(slow_blink Attribute::SlowBlink); - stylize_method!(rapid_blink Attribute::RapidBlink); - stylize_method!(hidden Attribute::Hidden); - stylize_method!(crossed_out Attribute::CrossedOut); - - stylize_method!(black, on_black, underline_black Color::Black); - stylize_method!(dark_grey, on_dark_grey, underline_dark_grey Color::DarkGrey); - stylize_method!(red, on_red, underline_red Color::Red); - stylize_method!(dark_red, on_dark_red, underline_dark_red Color::DarkRed); - stylize_method!(green, on_green, underline_green Color::Green); - stylize_method!(dark_green, on_dark_green, underline_dark_green Color::DarkGreen); - stylize_method!(yellow, on_yellow, underline_yellow Color::Yellow); - stylize_method!(dark_yellow, on_dark_yellow, underline_dark_yellow Color::DarkYellow); - stylize_method!(blue, on_blue, underline_blue Color::Blue); - stylize_method!(dark_blue, on_dark_blue, underline_dark_blue Color::DarkBlue); - stylize_method!(magenta, on_magenta, underline_magenta Color::Magenta); - stylize_method!(dark_magenta, on_dark_magenta, underline_dark_magenta Color::DarkMagenta); - stylize_method!(cyan, on_cyan, underline_cyan Color::Cyan); - stylize_method!(dark_cyan, on_dark_cyan, underline_dark_cyan Color::DarkCyan); - stylize_method!(white, on_white, underline_white Color::White); - stylize_method!(grey, on_grey, underline_grey Color::Grey); -} - -macro_rules! impl_stylize_for_display { - ($($t:ty),*) => { $( - impl Stylize for $t { - type Styled = StyledContent; - #[inline] - fn stylize(self) -> Self::Styled { - style(self) - } - } - )* } -} -impl_stylize_for_display!(String, char, &str); - -impl Stylize for ContentStyle { - type Styled = Self; - #[inline] - fn stylize(self) -> Self::Styled { - self - } -} -impl Stylize for StyledContent { - type Styled = StyledContent; - fn stylize(self) -> Self::Styled { - self - } -} - -// Workaround for https://github.com/rust-lang/rust/issues/78835 -macro_rules! calculated_docs { - ($(#[doc = $doc:expr] $item:item)*) => { $(#[doc = $doc] $item)* }; -} -// Remove once https://github.com/rust-lang/rust-clippy/issues/7106 stabilizes. -#[allow(clippy::single_component_path_imports)] -#[allow(clippy::useless_attribute)] -use calculated_docs; - -#[cfg(test)] -mod tests { - use super::super::{Attribute, Color, ContentStyle, Stylize}; - - #[test] - fn set_fg_bg_add_attr() { - let style = ContentStyle::new() - .with(Color::Blue) - .on(Color::Red) - .attribute(Attribute::Bold); - - assert_eq!(style.foreground_color, Some(Color::Blue)); - assert_eq!(style.background_color, Some(Color::Red)); - assert!(style.attributes.has(Attribute::Bold)); - - let mut styled_content = style.apply("test"); - - styled_content = styled_content - .with(Color::Green) - .on(Color::Magenta) - .attribute(Attribute::NoItalic); - - let style = styled_content.style(); - - assert_eq!(style.foreground_color, Some(Color::Green)); - assert_eq!(style.background_color, Some(Color::Magenta)); - assert!(style.attributes.has(Attribute::Bold)); - assert!(style.attributes.has(Attribute::NoItalic)); - } -} diff --git a/crates/util/keyfork-crossterm/src/style/sys.rs b/crates/util/keyfork-crossterm/src/style/sys.rs deleted file mode 100644 index 5a54276..0000000 --- a/crates/util/keyfork-crossterm/src/style/sys.rs +++ /dev/null @@ -1,2 +0,0 @@ -#[cfg(windows)] -pub(crate) mod windows; diff --git a/crates/util/keyfork-crossterm/src/style/sys/windows.rs b/crates/util/keyfork-crossterm/src/style/sys/windows.rs deleted file mode 100644 index f5e48b7..0000000 --- a/crates/util/keyfork-crossterm/src/style/sys/windows.rs +++ /dev/null @@ -1,204 +0,0 @@ -use std::convert::TryFrom; -use std::sync::atomic::{AtomicU32, Ordering}; - -use crossterm_winapi::{Console, Handle, HandleType, ScreenBuffer}; -use winapi::um::wincon; - -use super::super::{Color, Colored}; - -const FG_GREEN: u16 = wincon::FOREGROUND_GREEN; -const FG_RED: u16 = wincon::FOREGROUND_RED; -const FG_BLUE: u16 = wincon::FOREGROUND_BLUE; -const FG_INTENSITY: u16 = wincon::FOREGROUND_INTENSITY; - -const BG_GREEN: u16 = wincon::BACKGROUND_GREEN; -const BG_RED: u16 = wincon::BACKGROUND_RED; -const BG_BLUE: u16 = wincon::BACKGROUND_BLUE; -const BG_INTENSITY: u16 = wincon::BACKGROUND_INTENSITY; - -pub(crate) fn set_foreground_color(fg_color: Color) -> std::io::Result<()> { - init_console_color()?; - - let color_value: u16 = Colored::ForegroundColor(fg_color).into(); - - let screen_buffer = ScreenBuffer::current()?; - let csbi = screen_buffer.info()?; - - // Notice that the color values are stored in wAttribute. - // So we need to use bitwise operators to check if the values exists or to get current console colors. - let attrs = csbi.attributes(); - let bg_color = attrs & 0x0070; - let mut color = color_value | bg_color; - - // background intensity is a separate value in attrs, - // we need to check if this was applied to the current bg color. - if (attrs & wincon::BACKGROUND_INTENSITY) != 0 { - color |= wincon::BACKGROUND_INTENSITY; - } - - Console::from(screen_buffer.handle().clone()).set_text_attribute(color)?; - Ok(()) -} - -pub(crate) fn set_background_color(bg_color: Color) -> std::io::Result<()> { - init_console_color()?; - - let color_value: u16 = Colored::BackgroundColor(bg_color).into(); - - let screen_buffer = ScreenBuffer::current()?; - let csbi = screen_buffer.info()?; - - // Notice that the color values are stored in wAttribute. - // So we need to use bitwise operators to check if the values exists or to get current console colors. - let attrs = csbi.attributes(); - let fg_color = attrs & 0x0007; - let mut color = fg_color | color_value; - - // Foreground intensity is a separate value in attrs, - // So we need to check if this was applied to the current fg color. - if (attrs & wincon::FOREGROUND_INTENSITY) != 0 { - color |= wincon::FOREGROUND_INTENSITY; - } - - Console::from(screen_buffer.handle().clone()).set_text_attribute(color)?; - Ok(()) -} - -pub(crate) fn reset() -> std::io::Result<()> { - if let Ok(original_color) = u16::try_from(ORIGINAL_CONSOLE_COLOR.load(Ordering::Relaxed)) { - Console::from(Handle::new(HandleType::CurrentOutputHandle)?) - .set_text_attribute(original_color)?; - } - - Ok(()) -} - -/// Initializes the default console color. It will will be skipped if it has already been initialized. -pub(crate) fn init_console_color() -> std::io::Result<()> { - if ORIGINAL_CONSOLE_COLOR.load(Ordering::Relaxed) == u32::MAX { - let screen_buffer = ScreenBuffer::current()?; - let attr = screen_buffer.info()?.attributes(); - ORIGINAL_CONSOLE_COLOR.store(u32::from(attr), Ordering::Relaxed); - } - - Ok(()) -} - -/// Returns the original console color, make sure to call `init_console_color` before calling this function. Otherwise this function will panic. -pub(crate) fn original_console_color() -> u16 { - u16::try_from(ORIGINAL_CONSOLE_COLOR.load(Ordering::Relaxed)) - // safe unwrap, initial console color was set with `init_console_color` in `WinApiColor::new()` - .expect("Initial console color not set") -} - -// This is either a valid u16 in which case it stores the original console color or it is u32::MAX -// in which case it is uninitialized. -static ORIGINAL_CONSOLE_COLOR: AtomicU32 = AtomicU32::new(u32::MAX); - -impl From for u16 { - /// Returns the WinAPI color value (u16) from the `Colored` struct. - fn from(colored: Colored) -> Self { - match colored { - Colored::ForegroundColor(color) => { - match color { - Color::Black => 0, - Color::DarkGrey => FG_INTENSITY, - Color::Red => FG_INTENSITY | FG_RED, - Color::DarkRed => FG_RED, - Color::Green => FG_INTENSITY | FG_GREEN, - Color::DarkGreen => FG_GREEN, - Color::Yellow => FG_INTENSITY | FG_GREEN | FG_RED, - Color::DarkYellow => FG_GREEN | FG_RED, - Color::Blue => FG_INTENSITY | FG_BLUE, - Color::DarkBlue => FG_BLUE, - Color::Magenta => FG_INTENSITY | FG_RED | FG_BLUE, - Color::DarkMagenta => FG_RED | FG_BLUE, - Color::Cyan => FG_INTENSITY | FG_GREEN | FG_BLUE, - Color::DarkCyan => FG_GREEN | FG_BLUE, - Color::White => FG_INTENSITY | FG_RED | FG_GREEN | FG_BLUE, - Color::Grey => FG_RED | FG_GREEN | FG_BLUE, - - Color::Reset => { - // safe unwrap, initial console color was set with `init_console_color`. - let original_color = original_console_color(); - - const REMOVE_BG_MASK: u16 = BG_INTENSITY | BG_RED | BG_GREEN | BG_BLUE; - // remove all background values from the original color, we don't want to reset those. - - original_color & !REMOVE_BG_MASK - } - - /* WinAPI will be used for systems that do not support ANSI, those are windows version less then 10. RGB and 255 (AnsiBValue) colors are not supported in that case.*/ - Color::Rgb { .. } => 0, - Color::AnsiValue(_val) => 0, - } - } - Colored::BackgroundColor(color) => { - match color { - Color::Black => 0, - Color::DarkGrey => BG_INTENSITY, - Color::Red => BG_INTENSITY | BG_RED, - Color::DarkRed => BG_RED, - Color::Green => BG_INTENSITY | BG_GREEN, - Color::DarkGreen => BG_GREEN, - Color::Yellow => BG_INTENSITY | BG_GREEN | BG_RED, - Color::DarkYellow => BG_GREEN | BG_RED, - Color::Blue => BG_INTENSITY | BG_BLUE, - Color::DarkBlue => BG_BLUE, - Color::Magenta => BG_INTENSITY | BG_RED | BG_BLUE, - Color::DarkMagenta => BG_RED | BG_BLUE, - Color::Cyan => BG_INTENSITY | BG_GREEN | BG_BLUE, - Color::DarkCyan => BG_GREEN | BG_BLUE, - Color::White => BG_INTENSITY | BG_RED | BG_GREEN | BG_BLUE, - Color::Grey => BG_RED | BG_GREEN | BG_BLUE, - - Color::Reset => { - let original_color = original_console_color(); - - const REMOVE_FG_MASK: u16 = FG_INTENSITY | FG_RED | FG_GREEN | FG_BLUE; - // remove all foreground values from the original color, we don't want to reset those. - - original_color & !REMOVE_FG_MASK - } - /* WinAPI will be used for systems that do not support ANSI, those are windows version less then 10. RGB and 255 (AnsiBValue) colors are not supported in that case.*/ - Color::Rgb { .. } => 0, - Color::AnsiValue(_val) => 0, - } - } - Colored::UnderlineColor(_) => 0, - } - } -} - -#[cfg(test)] -mod tests { - use std::sync::atomic::Ordering; - - use crate::style::sys::windows::set_foreground_color; - - use super::{ - Color, Colored, BG_INTENSITY, BG_RED, FG_INTENSITY, FG_RED, ORIGINAL_CONSOLE_COLOR, - }; - - #[test] - fn test_parse_fg_color() { - let colored = Colored::ForegroundColor(Color::Red); - assert_eq!(Into::::into(colored), FG_INTENSITY | FG_RED); - } - - #[test] - fn test_parse_bg_color() { - let colored = Colored::BackgroundColor(Color::Red); - assert_eq!(Into::::into(colored), BG_INTENSITY | BG_RED); - } - - #[test] - fn test_original_console_color_is_set() { - assert_eq!(ORIGINAL_CONSOLE_COLOR.load(Ordering::Relaxed), u32::MAX); - - // will call `init_console_color` - set_foreground_color(Color::Blue).unwrap(); - - assert_ne!(ORIGINAL_CONSOLE_COLOR.load(Ordering::Relaxed), u32::MAX); - } -} diff --git a/crates/util/keyfork-crossterm/src/style/types.rs b/crates/util/keyfork-crossterm/src/style/types.rs deleted file mode 100644 index 7cd7d6e..0000000 --- a/crates/util/keyfork-crossterm/src/style/types.rs +++ /dev/null @@ -1,6 +0,0 @@ -pub use self::{attribute::Attribute, color::Color, colored::Colored, colors::Colors}; - -mod attribute; -mod color; -mod colored; -mod colors; diff --git a/crates/util/keyfork-crossterm/src/style/types/attribute.rs b/crates/util/keyfork-crossterm/src/style/types/attribute.rs deleted file mode 100644 index 0802b2f..0000000 --- a/crates/util/keyfork-crossterm/src/style/types/attribute.rs +++ /dev/null @@ -1,183 +0,0 @@ -use std::fmt::Display; - -#[cfg(feature = "serde")] -use serde::{Deserialize, Serialize}; - -use super::super::SetAttribute; - -// This macro generates the Attribute enum, its iterator -// function, and the static array containing the sgr code -// of each attribute -macro_rules! Attribute { - ( - $( - $(#[$inner:ident $($args:tt)*])* - $name:ident = $sgr:expr, - )* - ) => { - /// Represents an attribute. - /// - /// # Platform-specific Notes - /// - /// * Only UNIX and Windows 10 terminals do support text attributes. - /// * Keep in mind that not all terminals support all attributes. - /// * Crossterm implements almost all attributes listed in the - /// [SGR parameters](https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_parameters). - /// - /// | Attribute | Windows | UNIX | Notes | - /// | :-- | :--: | :--: | :-- | - /// | `Reset` | ✓ | ✓ | | - /// | `Bold` | ✓ | ✓ | | - /// | `Dim` | ✓ | ✓ | | - /// | `Italic` | ? | ? | Not widely supported, sometimes treated as inverse. | - /// | `Underlined` | ✓ | ✓ | | - /// | `SlowBlink` | ? | ? | Not widely supported, sometimes treated as inverse. | - /// | `RapidBlink` | ? | ? | Not widely supported. MS-DOS ANSI.SYS; 150+ per minute. | - /// | `Reverse` | ✓ | ✓ | | - /// | `Hidden` | ✓ | ✓ | Also known as Conceal. | - /// | `Fraktur` | ✗ | ✓ | Legible characters, but marked for deletion. | - /// | `DefaultForegroundColor` | ? | ? | Implementation specific (according to standard). | - /// | `DefaultBackgroundColor` | ? | ? | Implementation specific (according to standard). | - /// | `Framed` | ? | ? | Not widely supported. | - /// | `Encircled` | ? | ? | This should turn on the encircled attribute. | - /// | `OverLined` | ? | ? | This should draw a line at the top of the text. | - /// - /// # Examples - /// - /// Basic usage: - /// - /// ```no_run - /// use keyfork_crossterm::style::Attribute; - /// - /// println!( - /// "{} Underlined {} No Underline", - /// Attribute::Underlined, - /// Attribute::NoUnderline - /// ); - /// ``` - /// - /// Style existing text: - /// - /// ```no_run - /// use keyfork_crossterm::style::Stylize; - /// - /// println!("{}", "Bold text".bold()); - /// println!("{}", "Underlined text".underlined()); - /// println!("{}", "Negative text".negative()); - /// ``` - #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] - #[derive(Copy, Clone, Debug, PartialEq, Eq, Ord, PartialOrd, Hash)] - #[non_exhaustive] - pub enum Attribute { - $( - $(#[$inner $($args)*])* - $name, - )* - } - - pub static SGR: &'static[i16] = &[ - $($sgr,)* - ]; - - impl Attribute { - /// Iterates over all the variants of the Attribute enum. - pub fn iterator() -> impl Iterator { - use self::Attribute::*; - [ $($name,)* ].iter().copied() - } - } - } -} - -Attribute! { - /// Resets all the attributes. - Reset = 0, - /// Increases the text intensity. - Bold = 1, - /// Decreases the text intensity. - Dim = 2, - /// Emphasises the text. - Italic = 3, - /// Underlines the text. - Underlined = 4, - - // Other types of underlining - /// Double underlines the text. - DoubleUnderlined = 2, - /// Undercurls the text. - Undercurled = 3, - /// Underdots the text. - Underdotted = 4, - /// Underdashes the text. - Underdashed = 5, - - /// Makes the text blinking (< 150 per minute). - SlowBlink = 5, - /// Makes the text blinking (>= 150 per minute). - RapidBlink = 6, - /// Swaps foreground and background colors. - Reverse = 7, - /// Hides the text (also known as Conceal). - Hidden = 8, - /// Crosses the text. - CrossedOut = 9, - /// Sets the [Fraktur](https://en.wikipedia.org/wiki/Fraktur) typeface. - /// - /// Mostly used for [mathematical alphanumeric symbols](https://en.wikipedia.org/wiki/Mathematical_Alphanumeric_Symbols). - Fraktur = 20, - /// Turns off the `Bold` attribute. - Inconsistent - Prefer to use NormalIntensity - NoBold = 21, - /// Switches the text back to normal intensity (no bold, italic). - NormalIntensity = 22, - /// Turns off the `Italic` attribute. - NoItalic = 23, - /// Turns off the `Underlined` attribute. - NoUnderline = 24, - /// Turns off the text blinking (`SlowBlink` or `RapidBlink`). - NoBlink = 25, - /// Turns off the `Reverse` attribute. - NoReverse = 27, - /// Turns off the `Hidden` attribute. - NoHidden = 28, - /// Turns off the `CrossedOut` attribute. - NotCrossedOut = 29, - /// Makes the text framed. - Framed = 51, - /// Makes the text encircled. - Encircled = 52, - /// Draws a line at the top of the text. - OverLined = 53, - /// Turns off the `Frame` and `Encircled` attributes. - NotFramedOrEncircled = 54, - /// Turns off the `OverLined` attribute. - NotOverLined = 55, -} - -impl Display for Attribute { - fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", SetAttribute(*self))?; - Ok(()) - } -} - -impl Attribute { - /// Returns a u32 with one bit set, which is the - /// signature of this attribute in the Attributes - /// bitset. - /// - /// The +1 enables storing Reset (whose index is 0) - /// in the bitset Attributes. - #[inline(always)] - pub const fn bytes(self) -> u32 { - 1 << ((self as u32) + 1) - } - /// Returns the SGR attribute value. - /// - /// See - pub fn sgr(self) -> String { - if (self as usize) > 4 && (self as usize) < 9 { - return "4:".to_string() + SGR[self as usize].to_string().as_str(); - } - SGR[self as usize].to_string() - } -} diff --git a/crates/util/keyfork-crossterm/src/style/types/color.rs b/crates/util/keyfork-crossterm/src/style/types/color.rs deleted file mode 100644 index 0ff27b4..0000000 --- a/crates/util/keyfork-crossterm/src/style/types/color.rs +++ /dev/null @@ -1,524 +0,0 @@ -use std::{ - convert::{AsRef, TryFrom}, - str::FromStr, -}; - -#[cfg(feature = "serde")] -use std::fmt; - -use crate::style::parse_next_u8; - -/// Represents a color. -/// -/// # Platform-specific Notes -/// -/// The following list of 16 base colors are available for almost all terminals (Windows 7 and 8 included). -/// -/// | Light | Dark | -/// | :--------- | :------------ | -/// | `DarkGrey` | `Black` | -/// | `Red` | `DarkRed` | -/// | `Green` | `DarkGreen` | -/// | `Yellow` | `DarkYellow` | -/// | `Blue` | `DarkBlue` | -/// | `Magenta` | `DarkMagenta` | -/// | `Cyan` | `DarkCyan` | -/// | `White` | `Grey` | -/// -/// Most UNIX terminals and Windows 10 consoles support additional colors. -/// See [`Color::Rgb`] or [`Color::AnsiValue`] for more info. -#[derive(Copy, Clone, Debug, PartialEq, Eq, Ord, PartialOrd, Hash)] -pub enum Color { - /// Resets the terminal color. - Reset, - - /// Black color. - Black, - - /// Dark grey color. - DarkGrey, - - /// Light red color. - Red, - - /// Dark red color. - DarkRed, - - /// Light green color. - Green, - - /// Dark green color. - DarkGreen, - - /// Light yellow color. - Yellow, - - /// Dark yellow color. - DarkYellow, - - /// Light blue color. - Blue, - - /// Dark blue color. - DarkBlue, - - /// Light magenta color. - Magenta, - - /// Dark magenta color. - DarkMagenta, - - /// Light cyan color. - Cyan, - - /// Dark cyan color. - DarkCyan, - - /// White color. - White, - - /// Grey color. - Grey, - - /// An RGB color. See [RGB color model](https://en.wikipedia.org/wiki/RGB_color_model) for more info. - /// - /// Most UNIX terminals and Windows 10 supported only. - /// See [Platform-specific notes](enum.Color.html#platform-specific-notes) for more info. - Rgb { r: u8, g: u8, b: u8 }, - - /// An ANSI color. See [256 colors - cheat sheet](https://jonasjacek.github.io/colors/) for more info. - /// - /// Most UNIX terminals and Windows 10 supported only. - /// See [Platform-specific notes](enum.Color.html#platform-specific-notes) for more info. - AnsiValue(u8), -} - -impl Color { - /// Parses an ANSI color sequence. - /// - /// # Examples - /// - /// ``` - /// use keyfork_crossterm::style::Color; - /// - /// assert_eq!(Color::parse_ansi("5;0"), Some(Color::Black)); - /// assert_eq!(Color::parse_ansi("5;26"), Some(Color::AnsiValue(26))); - /// assert_eq!(Color::parse_ansi("2;50;60;70"), Some(Color::Rgb { r: 50, g: 60, b: 70 })); - /// assert_eq!(Color::parse_ansi("invalid color"), None); - /// ``` - /// - /// Currently, 3/4 bit color values aren't supported so return `None`. - /// - /// See also: [`Colored::parse_ansi`](crate::style::Colored::parse_ansi). - pub fn parse_ansi(ansi: &str) -> Option { - Self::parse_ansi_iter(&mut ansi.split(';')) - } - - /// The logic for parse_ansi, takes an iterator of the sequences terms (the numbers between the - /// ';'). It's a separate function so it can be used by both Color::parse_ansi and - /// colored::parse_ansi. - /// Tested in Colored tests. - pub(crate) fn parse_ansi_iter<'a>(values: &mut impl Iterator) -> Option { - let color = match parse_next_u8(values)? { - // 8 bit colors: `5;` - 5 => { - let n = parse_next_u8(values)?; - - use Color::*; - [ - Black, // 0 - DarkRed, // 1 - DarkGreen, // 2 - DarkYellow, // 3 - DarkBlue, // 4 - DarkMagenta, // 5 - DarkCyan, // 6 - Grey, // 7 - DarkGrey, // 8 - Red, // 9 - Green, // 10 - Yellow, // 11 - Blue, // 12 - Magenta, // 13 - Cyan, // 14 - White, // 15 - ] - .get(n as usize) - .copied() - .unwrap_or(Color::AnsiValue(n)) - } - - // 24 bit colors: `2;;;` - 2 => Color::Rgb { - r: parse_next_u8(values)?, - g: parse_next_u8(values)?, - b: parse_next_u8(values)?, - }, - - _ => return None, - }; - // If there's another value, it's unexpected so return None. - if values.next().is_some() { - return None; - } - Some(color) - } -} - -impl TryFrom<&str> for Color { - type Error = (); - - /// Try to create a `Color` from the string representation. This returns an error if the string does not match. - fn try_from(src: &str) -> Result { - let src = src.to_lowercase(); - - match src.as_ref() { - "reset" => Ok(Color::Reset), - "black" => Ok(Color::Black), - "dark_grey" => Ok(Color::DarkGrey), - "red" => Ok(Color::Red), - "dark_red" => Ok(Color::DarkRed), - "green" => Ok(Color::Green), - "dark_green" => Ok(Color::DarkGreen), - "yellow" => Ok(Color::Yellow), - "dark_yellow" => Ok(Color::DarkYellow), - "blue" => Ok(Color::Blue), - "dark_blue" => Ok(Color::DarkBlue), - "magenta" => Ok(Color::Magenta), - "dark_magenta" => Ok(Color::DarkMagenta), - "cyan" => Ok(Color::Cyan), - "dark_cyan" => Ok(Color::DarkCyan), - "white" => Ok(Color::White), - "grey" => Ok(Color::Grey), - _ => Err(()), - } - } -} - -impl FromStr for Color { - type Err = (); - - /// Creates a `Color` from the string representation. - /// - /// # Notes - /// - /// * Returns `Color::White` in case of an unknown color. - /// * Does not return `Err` and you can safely unwrap. - fn from_str(src: &str) -> Result { - Ok(Color::try_from(src).unwrap_or(Color::White)) - } -} - -impl From<(u8, u8, u8)> for Color { - /// Creates a 'Color' from the tuple representation. - fn from(val: (u8, u8, u8)) -> Self { - let (r, g, b) = val; - Self::Rgb { r, g, b } - } -} - -#[cfg(feature = "serde")] -impl serde::ser::Serialize for Color { - fn serialize(&self, serializer: S) -> Result - where - S: serde::ser::Serializer, - { - let str = match *self { - Color::Reset => "reset", - Color::Black => "black", - Color::DarkGrey => "dark_grey", - Color::Red => "red", - Color::DarkRed => "dark_red", - Color::Green => "green", - Color::DarkGreen => "dark_green", - Color::Yellow => "yellow", - Color::DarkYellow => "dark_yellow", - Color::Blue => "blue", - Color::DarkBlue => "dark_blue", - Color::Magenta => "magenta", - Color::DarkMagenta => "dark_magenta", - Color::Cyan => "cyan", - Color::DarkCyan => "dark_cyan", - Color::White => "white", - Color::Grey => "grey", - _ => "", - }; - - if str == "" { - match *self { - Color::AnsiValue(value) => { - return serializer.serialize_str(&format!("ansi_({})", value)); - } - Color::Rgb { r, g, b } => { - return serializer.serialize_str(&format!("rgb_({},{},{})", r, g, b)); - } - _ => { - return Err(serde::ser::Error::custom("Could not serialize enum type")); - } - } - } else { - return serializer.serialize_str(str); - } - } -} - -#[cfg(feature = "serde")] -impl<'de> serde::de::Deserialize<'de> for Color { - fn deserialize(deserializer: D) -> Result - where - D: serde::de::Deserializer<'de>, - { - struct ColorVisitor; - impl<'de> serde::de::Visitor<'de> for ColorVisitor { - type Value = Color; - fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { - formatter.write_str( - "`reset`, `black`, `blue`, `dark_blue`, `cyan`, `dark_cyan`, `green`, `dark_green`, `grey`, `dark_grey`, `magenta`, `dark_magenta`, `red`, `dark_red`, `white`, `yellow`, `dark_yellow`, `ansi_(value)`, or `rgb_(r,g,b)` or `#rgbhex`", - ) - } - fn visit_str(self, value: &str) -> Result - where - E: serde::de::Error, - { - if let Ok(c) = Color::try_from(value) { - Ok(c) - } else { - if value.contains("ansi") { - // strip away `ansi_(..)' and get the inner value between parenthesis. - let results = value.replace("ansi_(", "").replace(")", ""); - - let ansi_val = results.parse::(); - - if let Ok(ansi) = ansi_val { - return Ok(Color::AnsiValue(ansi)); - } - } else if value.contains("rgb") { - // strip away `rgb_(..)' and get the inner values between parenthesis. - let results = value - .replace("rgb_(", "") - .replace(")", "") - .split(',') - .map(|x| x.to_string()) - .collect::>(); - - if results.len() == 3 { - let r = results[0].parse::(); - let g = results[1].parse::(); - let b = results[2].parse::(); - - if r.is_ok() && g.is_ok() && b.is_ok() { - return Ok(Color::Rgb { - r: r.unwrap(), - g: g.unwrap(), - b: b.unwrap(), - }); - } - } - } else if let Some(hex) = value.strip_prefix('#') { - if hex.is_ascii() && hex.len() == 6 { - let r = u8::from_str_radix(&hex[0..2], 16); - let g = u8::from_str_radix(&hex[2..4], 16); - let b = u8::from_str_radix(&hex[4..6], 16); - - if r.is_ok() && g.is_ok() && b.is_ok() { - return Ok(Color::Rgb { - r: r.unwrap(), - g: g.unwrap(), - b: b.unwrap(), - }); - } - } - } - - Err(E::invalid_value(serde::de::Unexpected::Str(value), &self)) - } - } - } - - deserializer.deserialize_str(ColorVisitor) - } -} - -#[cfg(test)] -mod tests { - use super::Color; - - #[test] - fn test_known_color_conversion() { - assert_eq!("reset".parse(), Ok(Color::Reset)); - assert_eq!("grey".parse(), Ok(Color::Grey)); - assert_eq!("dark_grey".parse(), Ok(Color::DarkGrey)); - assert_eq!("red".parse(), Ok(Color::Red)); - assert_eq!("dark_red".parse(), Ok(Color::DarkRed)); - assert_eq!("green".parse(), Ok(Color::Green)); - assert_eq!("dark_green".parse(), Ok(Color::DarkGreen)); - assert_eq!("yellow".parse(), Ok(Color::Yellow)); - assert_eq!("dark_yellow".parse(), Ok(Color::DarkYellow)); - assert_eq!("blue".parse(), Ok(Color::Blue)); - assert_eq!("dark_blue".parse(), Ok(Color::DarkBlue)); - assert_eq!("magenta".parse(), Ok(Color::Magenta)); - assert_eq!("dark_magenta".parse(), Ok(Color::DarkMagenta)); - assert_eq!("cyan".parse(), Ok(Color::Cyan)); - assert_eq!("dark_cyan".parse(), Ok(Color::DarkCyan)); - assert_eq!("white".parse(), Ok(Color::White)); - assert_eq!("black".parse(), Ok(Color::Black)); - } - - #[test] - fn test_unknown_color_conversion_yields_white() { - assert_eq!("foo".parse(), Ok(Color::White)); - } - - #[test] - fn test_know_rgb_color_conversion() { - assert_eq!(Color::from((0, 0, 0)), Color::Rgb { r: 0, g: 0, b: 0 }); - assert_eq!( - Color::from((255, 255, 255)), - Color::Rgb { - r: 255, - g: 255, - b: 255 - } - ); - } -} - -#[cfg(test)] -#[cfg(feature = "serde")] -mod serde_tests { - use super::Color; - use serde_json; - - #[test] - fn test_deserial_known_color_conversion() { - assert_eq!( - serde_json::from_str::("\"Reset\"").unwrap(), - Color::Reset - ); - assert_eq!( - serde_json::from_str::("\"reset\"").unwrap(), - Color::Reset - ); - assert_eq!( - serde_json::from_str::("\"Red\"").unwrap(), - Color::Red - ); - assert_eq!( - serde_json::from_str::("\"red\"").unwrap(), - Color::Red - ); - assert_eq!( - serde_json::from_str::("\"dark_red\"").unwrap(), - Color::DarkRed - ); - assert_eq!( - serde_json::from_str::("\"grey\"").unwrap(), - Color::Grey - ); - assert_eq!( - serde_json::from_str::("\"dark_grey\"").unwrap(), - Color::DarkGrey - ); - assert_eq!( - serde_json::from_str::("\"green\"").unwrap(), - Color::Green - ); - assert_eq!( - serde_json::from_str::("\"dark_green\"").unwrap(), - Color::DarkGreen - ); - assert_eq!( - serde_json::from_str::("\"yellow\"").unwrap(), - Color::Yellow - ); - assert_eq!( - serde_json::from_str::("\"dark_yellow\"").unwrap(), - Color::DarkYellow - ); - assert_eq!( - serde_json::from_str::("\"blue\"").unwrap(), - Color::Blue - ); - assert_eq!( - serde_json::from_str::("\"dark_blue\"").unwrap(), - Color::DarkBlue - ); - assert_eq!( - serde_json::from_str::("\"magenta\"").unwrap(), - Color::Magenta - ); - assert_eq!( - serde_json::from_str::("\"dark_magenta\"").unwrap(), - Color::DarkMagenta - ); - assert_eq!( - serde_json::from_str::("\"cyan\"").unwrap(), - Color::Cyan - ); - assert_eq!( - serde_json::from_str::("\"dark_cyan\"").unwrap(), - Color::DarkCyan - ); - assert_eq!( - serde_json::from_str::("\"white\"").unwrap(), - Color::White - ); - assert_eq!( - serde_json::from_str::("\"black\"").unwrap(), - Color::Black - ); - } - - #[test] - fn test_deserial_unknown_color_conversion() { - assert!(serde_json::from_str::("\"unknown\"").is_err()); - } - - #[test] - fn test_deserial_ansi_value() { - assert_eq!( - serde_json::from_str::("\"ansi_(255)\"").unwrap(), - Color::AnsiValue(255) - ); - } - - #[test] - fn test_deserial_unvalid_ansi_value() { - assert!(serde_json::from_str::("\"ansi_(256)\"").is_err()); - assert!(serde_json::from_str::("\"ansi_(-1)\"").is_err()); - } - - #[test] - fn test_deserial_rgb() { - assert_eq!( - serde_json::from_str::("\"rgb_(255,255,255)\"").unwrap(), - Color::from((255, 255, 255)) - ); - } - - #[test] - fn test_deserial_unvalid_rgb() { - assert!(serde_json::from_str::("\"rgb_(255,255,255,255)\"").is_err()); - assert!(serde_json::from_str::("\"rgb_(256,255,255)\"").is_err()); - } - - #[test] - fn test_deserial_rgb_hex() { - assert_eq!( - serde_json::from_str::("\"#ffffff\"").unwrap(), - Color::from((255, 255, 255)) - ); - assert_eq!( - serde_json::from_str::("\"#FFFFFF\"").unwrap(), - Color::from((255, 255, 255)) - ); - } - - #[test] - fn test_deserial_unvalid_rgb_hex() { - assert!(serde_json::from_str::("\"#FFFFFFFF\"").is_err()); - assert!(serde_json::from_str::("\"#FFGFFF\"").is_err()); - // Ferris is 4 bytes so this will be considered the correct length. - assert!(serde_json::from_str::("\"#ff🦀\"").is_err()); - } -} diff --git a/crates/util/keyfork-crossterm/src/style/types/colored.rs b/crates/util/keyfork-crossterm/src/style/types/colored.rs deleted file mode 100644 index 2236449..0000000 --- a/crates/util/keyfork-crossterm/src/style/types/colored.rs +++ /dev/null @@ -1,320 +0,0 @@ -use parking_lot::Once; -use std::fmt::{self, Formatter}; -use std::sync::atomic::{AtomicBool, Ordering}; - -#[cfg(feature = "serde")] -use serde::{Deserialize, Serialize}; - -use crate::style::{parse_next_u8, Color}; - -/// Represents a foreground or background color. -/// -/// This can be converted to a [Colors](struct.Colors.html) by calling `into()` and applied -/// using the [SetColors](struct.SetColors.html) command. -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -#[derive(Copy, Clone, Debug, PartialEq, Eq, Ord, PartialOrd, Hash)] -pub enum Colored { - /// A foreground color. - ForegroundColor(Color), - /// A background color. - BackgroundColor(Color), - /// An underline color. - /// Imporant: doesnt work on windows 10 or lower. - UnderlineColor(Color), -} - -static ANSI_COLOR_DISABLED: AtomicBool = AtomicBool::new(false); -static INITIALIZER: Once = Once::new(); - -impl Colored { - /// Parse an ANSI foreground or background color. - /// This is the string that would appear within an `ESC [ m` escape sequence, as found in - /// various configuration files. - /// - /// # Examples - /// - /// ``` - /// use keyfork_crossterm::style::{Colored::{self, ForegroundColor, BackgroundColor}, Color}; - /// - /// assert_eq!(Colored::parse_ansi("38;5;0"), Some(ForegroundColor(Color::Black))); - /// assert_eq!(Colored::parse_ansi("38;5;26"), Some(ForegroundColor(Color::AnsiValue(26)))); - /// assert_eq!(Colored::parse_ansi("48;2;50;60;70"), Some(BackgroundColor(Color::Rgb { r: 50, g: 60, b: 70 }))); - /// assert_eq!(Colored::parse_ansi("49"), Some(BackgroundColor(Color::Reset))); - /// assert_eq!(Colored::parse_ansi("invalid color"), None); - /// ``` - /// - /// Currently, 3/4 bit color values aren't supported so return `None`. - /// - /// See also: [`Color::parse_ansi`]. - pub fn parse_ansi(ansi: &str) -> Option { - use Colored::{BackgroundColor, ForegroundColor, UnderlineColor}; - - let values = &mut ansi.split(';'); - - let output = match parse_next_u8(values)? { - 38 => return Color::parse_ansi_iter(values).map(ForegroundColor), - 48 => return Color::parse_ansi_iter(values).map(BackgroundColor), - 58 => return Color::parse_ansi_iter(values).map(UnderlineColor), - - 39 => ForegroundColor(Color::Reset), - 49 => BackgroundColor(Color::Reset), - 59 => UnderlineColor(Color::Reset), - - _ => return None, - }; - - if values.next().is_some() { - return None; - } - - Some(output) - } - - /// Checks whether ansi color sequences are disabled by setting of NO_COLOR - /// in environment as per - pub fn ansi_color_disabled() -> bool { - !std::env::var("NO_COLOR") - .unwrap_or("".to_string()) - .is_empty() - } - - pub fn ansi_color_disabled_memoized() -> bool { - INITIALIZER.call_once(|| { - ANSI_COLOR_DISABLED.store(Self::ansi_color_disabled(), Ordering::SeqCst); - }); - - ANSI_COLOR_DISABLED.load(Ordering::SeqCst) - } - - pub fn set_ansi_color_disabled(val: bool) { - // Force the one-time initializer to run. - _ = Self::ansi_color_disabled_memoized(); - ANSI_COLOR_DISABLED.store(val, Ordering::SeqCst); - } -} - -impl fmt::Display for Colored { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - let color; - - if Self::ansi_color_disabled_memoized() { - return Ok(()); - } - - match *self { - Colored::ForegroundColor(new_color) => { - if new_color == Color::Reset { - return f.write_str("39"); - } else { - f.write_str("38;")?; - color = new_color; - } - } - Colored::BackgroundColor(new_color) => { - if new_color == Color::Reset { - return f.write_str("49"); - } else { - f.write_str("48;")?; - color = new_color; - } - } - Colored::UnderlineColor(new_color) => { - if new_color == Color::Reset { - return f.write_str("59"); - } else { - f.write_str("58;")?; - color = new_color; - } - } - } - - match color { - Color::Black => f.write_str("5;0"), - Color::DarkGrey => f.write_str("5;8"), - Color::Red => f.write_str("5;9"), - Color::DarkRed => f.write_str("5;1"), - Color::Green => f.write_str("5;10"), - Color::DarkGreen => f.write_str("5;2"), - Color::Yellow => f.write_str("5;11"), - Color::DarkYellow => f.write_str("5;3"), - Color::Blue => f.write_str("5;12"), - Color::DarkBlue => f.write_str("5;4"), - Color::Magenta => f.write_str("5;13"), - Color::DarkMagenta => f.write_str("5;5"), - Color::Cyan => f.write_str("5;14"), - Color::DarkCyan => f.write_str("5;6"), - Color::White => f.write_str("5;15"), - Color::Grey => f.write_str("5;7"), - Color::Rgb { r, g, b } => write!(f, "2;{r};{g};{b}"), - Color::AnsiValue(val) => write!(f, "5;{val}"), - _ => Ok(()), - } - } -} - -#[cfg(test)] -mod tests { - use crate::style::{Color, Colored}; - - fn check_format_color(colored: Colored, expected: &str) { - Colored::set_ansi_color_disabled(true); - assert_eq!(colored.to_string(), ""); - Colored::set_ansi_color_disabled(false); - assert_eq!(colored.to_string(), expected); - } - - #[test] - fn test_format_fg_color() { - let colored = Colored::ForegroundColor(Color::Red); - check_format_color(colored, "38;5;9"); - } - - #[test] - fn test_format_bg_color() { - let colored = Colored::BackgroundColor(Color::Red); - check_format_color(colored, "48;5;9"); - } - - #[test] - fn test_format_reset_fg_color() { - let colored = Colored::ForegroundColor(Color::Reset); - check_format_color(colored, "39"); - } - - #[test] - fn test_format_reset_bg_color() { - let colored = Colored::BackgroundColor(Color::Reset); - check_format_color(colored, "49"); - } - - #[test] - fn test_format_fg_rgb_color() { - let colored = Colored::BackgroundColor(Color::Rgb { r: 1, g: 2, b: 3 }); - check_format_color(colored, "48;2;1;2;3"); - } - - #[test] - fn test_format_fg_ansi_color() { - let colored = Colored::ForegroundColor(Color::AnsiValue(255)); - check_format_color(colored, "38;5;255"); - } - - #[test] - fn test_parse_ansi_fg() { - test_parse_ansi(Colored::ForegroundColor) - } - - #[test] - fn test_parse_ansi_bg() { - test_parse_ansi(Colored::ForegroundColor) - } - - /// Used for test_parse_ansi_fg and test_parse_ansi_bg - fn test_parse_ansi(bg_or_fg: impl Fn(Color) -> Colored) { - /// Formats a re-parses `color` to check the result. - macro_rules! test { - ($color:expr) => { - let colored = bg_or_fg($color); - assert_eq!(Colored::parse_ansi(&format!("{}", colored)), Some(colored)); - }; - } - - use Color::*; - - test!(Reset); - test!(Black); - test!(DarkGrey); - test!(Red); - test!(DarkRed); - test!(Green); - test!(DarkGreen); - test!(Yellow); - test!(DarkYellow); - test!(Blue); - test!(DarkBlue); - test!(Magenta); - test!(DarkMagenta); - test!(Cyan); - test!(DarkCyan); - test!(White); - test!(Grey); - - // n in 0..=15 will give us the color values above back. - for n in 16..=255 { - test!(AnsiValue(n)); - } - - for r in 0..=255 { - for g in [0, 2, 18, 19, 60, 100, 200, 250, 254, 255].iter().copied() { - for b in [0, 12, 16, 99, 100, 161, 200, 255].iter().copied() { - test!(Rgb { r, g, b }); - } - } - } - } - - #[test] - fn test_parse_invalid_ansi_color() { - /// Checks that trying to parse `s` yields None. - fn test(s: &str) { - assert_eq!(Colored::parse_ansi(s), None); - } - test(""); - test(";"); - test(";;"); - test(";;"); - test("0"); - test("1"); - test("12"); - test("100"); - test("100048949345"); - test("39;"); - test("49;"); - test("39;2"); - test("49;2"); - test("38"); - test("38;"); - test("38;0"); - test("38;5"); - test("38;5;0;"); - test("38;5;0;2"); - test("38;5;80;"); - test("38;5;80;2"); - test("38;5;257"); - test("38;2"); - test("38;2;"); - test("38;2;0"); - test("38;2;0;2"); - test("38;2;0;2;257"); - test("38;2;0;2;25;"); - test("38;2;0;2;25;3"); - test("48"); - test("48;"); - test("48;0"); - test("48;5"); - test("48;5;0;"); - test("48;5;0;2"); - test("48;5;80;"); - test("48;5;80;2"); - test("48;5;257"); - test("48;2"); - test("48;2;"); - test("48;2;0"); - test("48;2;0;2"); - test("48;2;0;2;257"); - test("48;2;0;2;25;"); - test("48;2;0;2;25;3"); - } - - #[test] - fn test_no_color() { - std::env::set_var("NO_COLOR", "1"); - assert!(Colored::ansi_color_disabled()); - std::env::set_var("NO_COLOR", "XXX"); - assert!(Colored::ansi_color_disabled()); - std::env::set_var("NO_COLOR", ""); - assert!(!Colored::ansi_color_disabled()); - std::env::remove_var("NO_COLOR"); - assert!(!Colored::ansi_color_disabled()); - } -} diff --git a/crates/util/keyfork-crossterm/src/style/types/colors.rs b/crates/util/keyfork-crossterm/src/style/types/colors.rs deleted file mode 100644 index 72ef952..0000000 --- a/crates/util/keyfork-crossterm/src/style/types/colors.rs +++ /dev/null @@ -1,234 +0,0 @@ -use crate::style::{Color, Colored}; - -/// Represents, optionally, a foreground and/or a background color. -/// -/// It can be applied using the `SetColors` command. -/// -/// It can also be created from a [Colored](enum.Colored.html) value or a tuple of -/// `(Color, Color)` in the order `(foreground, background)`. -/// -/// The [then](#method.then) method can be used to combine `Colors` values. -/// -/// For example: -/// ```no_run -/// use keyfork_crossterm::style::{Color, Colors, Colored}; -/// -/// // An example color, loaded from a config, file in ANSI format. -/// let config_color = "38;2;23;147;209"; -/// -/// // Default to green text on a black background. -/// let default_colors = Colors::new(Color::Green, Color::Black); -/// // Load a colored value from a config and override the default colors -/// let colors = match Colored::parse_ansi(config_color) { -/// Some(colored) => default_colors.then(&colored.into()), -/// None => default_colors, -/// }; -/// ``` -/// -/// See [Color](enum.Color.html). -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct Colors { - pub foreground: Option, - pub background: Option, -} - -impl Colors { - /// Returns a new `Color` which, when applied, has the same effect as applying `self` and *then* - /// `other`. - pub fn then(&self, other: &Colors) -> Colors { - Colors { - foreground: other.foreground.or(self.foreground), - background: other.background.or(self.background), - } - } -} - -impl Colors { - pub fn new(foreground: Color, background: Color) -> Colors { - Colors { - foreground: Some(foreground), - background: Some(background), - } - } -} - -impl From for Colors { - fn from(colored: Colored) -> Colors { - match colored { - Colored::ForegroundColor(color) => Colors { - foreground: Some(color), - background: None, - }, - Colored::BackgroundColor(color) => Colors { - foreground: None, - background: Some(color), - }, - Colored::UnderlineColor(color) => Colors { - foreground: None, - background: Some(color), - }, - } - } -} - -#[cfg(test)] -mod tests { - use crate::style::{Color, Colors}; - - #[test] - fn test_colors_then() { - use Color::*; - - assert_eq!( - Colors { - foreground: None, - background: None, - } - .then(&Colors { - foreground: None, - background: None, - }), - Colors { - foreground: None, - background: None, - } - ); - - assert_eq!( - Colors { - foreground: None, - background: None, - } - .then(&Colors { - foreground: Some(Black), - background: None, - }), - Colors { - foreground: Some(Black), - background: None, - } - ); - - assert_eq!( - Colors { - foreground: None, - background: None, - } - .then(&Colors { - foreground: None, - background: Some(Grey), - }), - Colors { - foreground: None, - background: Some(Grey), - } - ); - - assert_eq!( - Colors { - foreground: None, - background: None, - } - .then(&Colors::new(White, Grey)), - Colors::new(White, Grey), - ); - - assert_eq!( - Colors { - foreground: None, - background: Some(Blue), - } - .then(&Colors::new(White, Grey)), - Colors::new(White, Grey), - ); - - assert_eq!( - Colors { - foreground: Some(Blue), - background: None, - } - .then(&Colors::new(White, Grey)), - Colors::new(White, Grey), - ); - - assert_eq!( - Colors::new(Blue, Green).then(&Colors::new(White, Grey)), - Colors::new(White, Grey), - ); - - assert_eq!( - Colors { - foreground: Some(Blue), - background: Some(Green), - } - .then(&Colors { - foreground: None, - background: Some(Grey), - }), - Colors { - foreground: Some(Blue), - background: Some(Grey), - } - ); - - assert_eq!( - Colors { - foreground: Some(Blue), - background: Some(Green), - } - .then(&Colors { - foreground: Some(White), - background: None, - }), - Colors { - foreground: Some(White), - background: Some(Green), - } - ); - - assert_eq!( - Colors { - foreground: Some(Blue), - background: Some(Green), - } - .then(&Colors { - foreground: None, - background: None, - }), - Colors { - foreground: Some(Blue), - background: Some(Green), - } - ); - - assert_eq!( - Colors { - foreground: None, - background: Some(Green), - } - .then(&Colors { - foreground: None, - background: None, - }), - Colors { - foreground: None, - background: Some(Green), - } - ); - - assert_eq!( - Colors { - foreground: Some(Blue), - background: None, - } - .then(&Colors { - foreground: None, - background: None, - }), - Colors { - foreground: Some(Blue), - background: None, - } - ); - } -} diff --git a/crates/util/keyfork-crossterm/src/terminal.rs b/crates/util/keyfork-crossterm/src/terminal.rs deleted file mode 100644 index bfd87fe..0000000 --- a/crates/util/keyfork-crossterm/src/terminal.rs +++ /dev/null @@ -1,622 +0,0 @@ -//! # Terminal -//! -//! The `terminal` module provides functionality to work with the terminal. -//! -//! This documentation does not contain a lot of examples. The reason is that it's fairly -//! obvious how to use this crate. Although, we do provide -//! [examples](https://github.com/crossterm-rs/crossterm/tree/master/examples) repository -//! to demonstrate the capabilities. -//! -//! Most terminal actions can be performed with commands. -//! Please have a look at [command documentation](../index.html#command-api) for a more detailed documentation. -//! -//! ## Screen Buffer -//! -//! A screen buffer is a two-dimensional array of character -//! and color data which is displayed in a terminal screen. -//! -//! The terminal has several of those buffers and is able to switch between them. -//! The default screen in which you work is called the 'main screen'. -//! The other screens are called the 'alternative screen'. -//! -//! It is important to understand that crossterm does not yet support creating screens, -//! or switch between more than two buffers, and only offers the ability to change -//! between the 'alternate' and 'main screen'. -//! -//! ### Alternate Screen -//! -//! By default, you will be working on the main screen. -//! There is also another screen called the 'alternative' screen. -//! This screen is slightly different from the main screen. -//! For example, it has the exact dimensions of the terminal window, -//! without any scroll-back area. -//! -//! Crossterm offers the possibility to switch to the 'alternative' screen, -//! make some modifications, and move back to the 'main' screen again. -//! The main screen will stay intact and will have the original data as we performed all -//! operations on the alternative screen. -//! -//! An good example of this is Vim. -//! When it is launched from bash, a whole new buffer is used to modify a file. -//! Then, when the modification is finished, it closes again and continues on the main screen. -//! -//! ### Raw Mode -//! -//! By default, the terminal functions in a certain way. -//! For example, it will move the cursor to the beginning of the next line when the input hits the end of a line. -//! Or that the backspace is interpreted for character removal. -//! -//! Sometimes these default modes are irrelevant, -//! and in this case, we can turn them off. -//! This is what happens when you enable raw modes. -//! -//! Those modes will be set when enabling raw modes: -//! -//! - Input will not be forwarded to screen -//! - Input will not be processed on enter press -//! - Input will not be line buffered (input sent byte-by-byte to input buffer) -//! - Special keys like backspace and CTRL+C will not be processed by terminal driver -//! - New line character will not be processed therefore `println!` can't be used, use `write!` instead -//! -//! Raw mode can be enabled/disabled with the [enable_raw_mode](terminal::enable_raw_mode) and [disable_raw_mode](terminal::disable_raw_mode) functions. -//! -//! ## Examples -//! -//! ```no_run -//! use std::io::{self, Write}; -//! use keyfork_crossterm::{execute, terminal::{ScrollUp, SetSize, size}}; -//! -//! fn main() -> io::Result<()> { -//! let (cols, rows) = size()?; -//! // Resize terminal and scroll up. -//! execute!( -//! io::stdout(), -//! SetSize(10, 10), -//! ScrollUp(5) -//! )?; -//! -//! // Be a good citizen, cleanup -//! execute!(io::stdout(), SetSize(cols, rows))?; -//! Ok(()) -//! } -//! ``` -//! -//! For manual execution control check out [crossterm::queue](../macro.queue.html). - -use std::{fmt, io, os}; - -#[cfg(windows)] -use crossterm_winapi::{ConsoleMode, Handle, ScreenBuffer}; -#[cfg(feature = "serde")] -use serde::{Deserialize, Serialize}; -#[cfg(windows)] -use winapi::um::wincon::ENABLE_WRAP_AT_EOL_OUTPUT; - -pub trait TerminalIoctl { - fn enable_raw_mode(&mut self) -> io::Result<()>; - - fn disable_raw_mode(&mut self) -> io::Result<()>; - - fn size(&self) -> io::Result<(u16, u16)>; - - fn window_size(&self) -> io::Result; -} - -#[cfg(unix)] -pub struct FdTerminal { - fd: i32, - stored_termios: Option, -} - -impl From for FdTerminal -where - T: os::fd::AsRawFd, -{ - fn from(value: T) -> Self { - Self { - fd: value.as_raw_fd(), - stored_termios: None, - } - } -} - -#[cfg(unix)] -impl TerminalIoctl for FdTerminal { - fn enable_raw_mode(&mut self) -> io::Result<()> { - if self.stored_termios.is_none() { - let termios = sys::fd_enable_raw_mode(self.fd)?; - let _ = self.stored_termios.insert(termios); - } - Ok(()) - } - - fn disable_raw_mode(&mut self) -> io::Result<()> { - if let Some(termios) = self.stored_termios.take() { - sys::fd_disable_raw_mode(self.fd, termios)?; - } - Ok(()) - } - - fn size(&self) -> io::Result<(u16, u16)> { - sys::fd_size(self.fd) - } - - fn window_size(&self) -> io::Result { - sys::fd_window_size(self.fd) - } -} - -#[doc(no_inline)] -use crate::Command; -use crate::{csi, impl_display}; - -pub(crate) mod sys; - -#[cfg(feature = "events")] -pub use sys::supports_keyboard_enhancement; - -/// Tells whether the raw mode is enabled. -/// -/// Please have a look at the [raw mode](./index.html#raw-mode) section. -pub fn is_raw_mode_enabled() -> io::Result { - #[cfg(unix)] - { - Ok(sys::is_raw_mode_enabled()) - } - - #[cfg(windows)] - { - sys::is_raw_mode_enabled() - } -} - -/// Enables raw mode. -/// -/// Please have a look at the [raw mode](./index.html#raw-mode) section. -pub fn enable_raw_mode() -> io::Result<()> { - sys::enable_raw_mode() -} - -/// Disables raw mode. -/// -/// Please have a look at the [raw mode](./index.html#raw-mode) section. -pub fn disable_raw_mode() -> io::Result<()> { - sys::disable_raw_mode() -} - -/// Returns the terminal size `(columns, rows)`. -/// -/// The top left cell is represented `(1, 1)`. -pub fn size() -> io::Result<(u16, u16)> { - sys::size() -} - -#[derive(Debug)] -pub struct WindowSize { - pub rows: u16, - pub columns: u16, - pub width: u16, - pub height: u16, -} - -/// Returns the terminal size `[WindowSize]`. -/// -/// The width and height in pixels may not be reliably implemented or default to 0. -/// For unix, documents them as "unused". -/// For windows it is not implemented. -pub fn window_size() -> io::Result { - sys::window_size() -} - -/// Disables line wrapping. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct DisableLineWrap; - -impl Command for DisableLineWrap { - fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result { - f.write_str(csi!("?7l")) - } - - #[cfg(windows)] - fn execute_winapi(&self) -> io::Result<()> { - let screen_buffer = ScreenBuffer::current()?; - let console_mode = ConsoleMode::from(screen_buffer.handle().clone()); - let new_mode = console_mode.mode()? & !ENABLE_WRAP_AT_EOL_OUTPUT; - console_mode.set_mode(new_mode)?; - Ok(()) - } -} - -/// Enable line wrapping. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct EnableLineWrap; - -impl Command for EnableLineWrap { - fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result { - f.write_str(csi!("?7h")) - } - - #[cfg(windows)] - fn execute_winapi(&self) -> io::Result<()> { - let screen_buffer = ScreenBuffer::current()?; - let console_mode = ConsoleMode::from(screen_buffer.handle().clone()); - let new_mode = console_mode.mode()? | ENABLE_WRAP_AT_EOL_OUTPUT; - console_mode.set_mode(new_mode)?; - Ok(()) - } -} - -/// A command that switches to alternate screen. -/// -/// # Notes -/// -/// * Commands must be executed/queued for execution otherwise they do nothing. -/// * Use [LeaveAlternateScreen](./struct.LeaveAlternateScreen.html) command to leave the entered alternate screen. -/// -/// # Examples -/// -/// ```no_run -/// use std::io::{self, Write}; -/// use keyfork_crossterm::{execute, terminal::{EnterAlternateScreen, LeaveAlternateScreen}}; -/// -/// fn main() -> io::Result<()> { -/// execute!(io::stdout(), EnterAlternateScreen)?; -/// -/// // Do anything on the alternate screen -/// -/// execute!(io::stdout(), LeaveAlternateScreen) -/// } -/// ``` -/// -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct EnterAlternateScreen; - -impl Command for EnterAlternateScreen { - fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result { - f.write_str(csi!("?1049h")) - } - - #[cfg(windows)] - fn execute_winapi(&self) -> io::Result<()> { - let alternate_screen = ScreenBuffer::create()?; - alternate_screen.show()?; - Ok(()) - } -} - -/// A command that switches back to the main screen. -/// -/// # Notes -/// -/// * Commands must be executed/queued for execution otherwise they do nothing. -/// * Use [EnterAlternateScreen](./struct.EnterAlternateScreen.html) to enter the alternate screen. -/// -/// # Examples -/// -/// ```no_run -/// use std::io::{self, Write}; -/// use keyfork_crossterm::{execute, terminal::{EnterAlternateScreen, LeaveAlternateScreen}}; -/// -/// fn main() -> io::Result<()> { -/// execute!(io::stdout(), EnterAlternateScreen)?; -/// -/// // Do anything on the alternate screen -/// -/// execute!(io::stdout(), LeaveAlternateScreen) -/// } -/// ``` -/// -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct LeaveAlternateScreen; - -impl Command for LeaveAlternateScreen { - fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result { - f.write_str(csi!("?1049l")) - } - - #[cfg(windows)] - fn execute_winapi(&self) -> io::Result<()> { - let screen_buffer = ScreenBuffer::from(Handle::current_out_handle()?); - screen_buffer.show()?; - Ok(()) - } -} - -/// Different ways to clear the terminal buffer. -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -#[derive(Copy, Clone, Debug, PartialEq, Eq, Ord, PartialOrd, Hash)] -pub enum ClearType { - /// All cells. - All, - /// All plus history - Purge, - /// All cells from the cursor position downwards. - FromCursorDown, - /// All cells from the cursor position upwards. - FromCursorUp, - /// All cells at the cursor row. - CurrentLine, - /// All cells from the cursor position until the new line. - UntilNewLine, -} - -/// A command that scrolls the terminal screen a given number of rows up. -/// -/// # Notes -/// -/// Commands must be executed/queued for execution otherwise they do nothing. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct ScrollUp(pub u16); - -impl Command for ScrollUp { - fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result { - if self.0 != 0 { - write!(f, csi!("{}S"), self.0)?; - } - Ok(()) - } - - #[cfg(windows)] - fn execute_winapi(&self) -> io::Result<()> { - sys::scroll_up(self.0) - } -} - -/// A command that scrolls the terminal screen a given number of rows down. -/// -/// # Notes -/// -/// Commands must be executed/queued for execution otherwise they do nothing. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct ScrollDown(pub u16); - -impl Command for ScrollDown { - fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result { - if self.0 != 0 { - write!(f, csi!("{}T"), self.0)?; - } - Ok(()) - } - - #[cfg(windows)] - fn execute_winapi(&self) -> io::Result<()> { - sys::scroll_down(self.0) - } -} - -/// A command that clears the terminal screen buffer. -/// -/// See the [`ClearType`](enum.ClearType.html) enum. -/// -/// # Notes -/// -/// Commands must be executed/queued for execution otherwise they do nothing. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct Clear(pub ClearType); - -impl Command for Clear { - fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result { - f.write_str(match self.0 { - ClearType::All => csi!("2J"), - ClearType::Purge => csi!("3J"), - ClearType::FromCursorDown => csi!("J"), - ClearType::FromCursorUp => csi!("1J"), - ClearType::CurrentLine => csi!("2K"), - ClearType::UntilNewLine => csi!("K"), - }) - } - - #[cfg(windows)] - fn execute_winapi(&self) -> io::Result<()> { - sys::clear(self.0) - } -} - -/// A command that sets the terminal buffer size `(columns, rows)`. -/// -/// # Notes -/// -/// Commands must be executed/queued for execution otherwise they do nothing. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct SetSize(pub u16, pub u16); - -impl Command for SetSize { - fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result { - write!(f, csi!("8;{};{}t"), self.1, self.0) - } - - #[cfg(windows)] - fn execute_winapi(&self) -> io::Result<()> { - sys::set_size(self.0, self.1) - } -} - -/// A command that sets the terminal title -/// -/// # Notes -/// -/// Commands must be executed/queued for execution otherwise they do nothing. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct SetTitle(pub T); - -impl Command for SetTitle { - fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result { - write!(f, "\x1B]0;{}\x07", &self.0) - } - - #[cfg(windows)] - fn execute_winapi(&self) -> io::Result<()> { - sys::set_window_title(&self.0) - } -} - -/// A command that instructs the terminal emulator to begin a synchronized frame. -/// -/// # Notes -/// -/// * Commands must be executed/queued for execution otherwise they do nothing. -/// * Use [EndSynchronizedUpdate](./struct.EndSynchronizedUpdate.html) command to leave the entered alternate screen. -/// -/// When rendering the screen of the terminal, the Emulator usually iterates through each visible grid cell and -/// renders its current state. With applications updating the screen at a higher frequency this can cause tearing. -/// -/// This mode attempts to mitigate that. -/// -/// When the synchronization mode is enabled following render calls will keep rendering the last rendered state. -/// The terminal Emulator keeps processing incoming text and sequences. When the synchronized update mode is disabled -/// again the renderer may fetch the latest screen buffer state again, effectively avoiding the tearing effect -/// by unintentionally rendering in the middle a of an application screen update. -/// -/// # Examples -/// -/// ```no_run -/// use std::io::{self, Write}; -/// use keyfork_crossterm::{execute, terminal::{BeginSynchronizedUpdate, EndSynchronizedUpdate}}; -/// -/// fn main() -> io::Result<()> { -/// execute!(io::stdout(), BeginSynchronizedUpdate)?; -/// -/// // Anything performed here will not be rendered until EndSynchronizedUpdate is called. -/// -/// execute!(io::stdout(), EndSynchronizedUpdate)?; -/// Ok(()) -/// } -/// ``` -/// -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct BeginSynchronizedUpdate; - -impl Command for BeginSynchronizedUpdate { - fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result { - f.write_str(csi!("?2026h")) - } - - #[cfg(windows)] - fn execute_winapi(&self) -> io::Result<()> { - Ok(()) - } - - #[cfg(windows)] - #[inline] - fn is_ansi_code_supported(&self) -> bool { - true - } -} - -/// A command that instructs the terminal to end a synchronized frame. -/// -/// # Notes -/// -/// * Commands must be executed/queued for execution otherwise they do nothing. -/// * Use [BeginSynchronizedUpdate](./struct.BeginSynchronizedUpdate.html) to enter the alternate screen. -/// -/// When rendering the screen of the terminal, the Emulator usually iterates through each visible grid cell and -/// renders its current state. With applications updating the screen a at higher frequency this can cause tearing. -/// -/// This mode attempts to mitigate that. -/// -/// When the synchronization mode is enabled following render calls will keep rendering the last rendered state. -/// The terminal Emulator keeps processing incoming text and sequences. When the synchronized update mode is disabled -/// again the renderer may fetch the latest screen buffer state again, effectively avoiding the tearing effect -/// by unintentionally rendering in the middle a of an application screen update. -/// -/// # Examples -/// -/// ```no_run -/// use std::io::{self, Write}; -/// use keyfork_crossterm::{execute, terminal::{BeginSynchronizedUpdate, EndSynchronizedUpdate}}; -/// -/// fn main() -> io::Result<()> { -/// execute!(io::stdout(), BeginSynchronizedUpdate)?; -/// -/// // Anything performed here will not be rendered until EndSynchronizedUpdate is called. -/// -/// execute!(io::stdout(), EndSynchronizedUpdate)?; -/// Ok(()) -/// } -/// ``` -/// -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct EndSynchronizedUpdate; - -impl Command for EndSynchronizedUpdate { - fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result { - f.write_str(csi!("?2026l")) - } - - #[cfg(windows)] - fn execute_winapi(&self) -> io::Result<()> { - Ok(()) - } - - #[cfg(windows)] - #[inline] - fn is_ansi_code_supported(&self) -> bool { - true - } -} - -impl_display!(for ScrollUp); -impl_display!(for ScrollDown); -impl_display!(for SetSize); -impl_display!(for Clear); - -#[cfg(test)] -mod tests { - use std::{io::stdout, thread, time}; - - use crate::execute; - - use super::*; - - // Test is disabled, because it's failing on Travis CI - #[test] - #[ignore] - fn test_resize_ansi() { - let (width, height) = size().unwrap(); - - execute!(stdout(), SetSize(35, 35)).unwrap(); - - // see issue: https://github.com/eminence/terminal-size/issues/11 - thread::sleep(time::Duration::from_millis(30)); - - assert_eq!((35, 35), size().unwrap()); - - // reset to previous size - execute!(stdout(), SetSize(width, height)).unwrap(); - - // see issue: https://github.com/eminence/terminal-size/issues/11 - thread::sleep(time::Duration::from_millis(30)); - - assert_eq!((width, height), size().unwrap()); - } - - #[test] - fn test_raw_mode() { - // check we start from normal mode (may fail on some test harnesses) - assert!(!is_raw_mode_enabled().unwrap()); - - // enable the raw mode - if enable_raw_mode().is_err() { - // Enabling raw mode doesn't work on the ci - // So we just ignore it - return; - } - - // check it worked (on unix it doesn't really check the underlying - // tty but rather check that the code is consistent) - assert!(is_raw_mode_enabled().unwrap()); - - // enable it again, this should not change anything - enable_raw_mode().unwrap(); - - // check we're still in raw mode - assert!(is_raw_mode_enabled().unwrap()); - - // now let's disable it - disable_raw_mode().unwrap(); - - // check we're back to normal mode - assert!(!is_raw_mode_enabled().unwrap()); - } -} diff --git a/crates/util/keyfork-crossterm/src/terminal/sys.rs b/crates/util/keyfork-crossterm/src/terminal/sys.rs deleted file mode 100644 index a857382..0000000 --- a/crates/util/keyfork-crossterm/src/terminal/sys.rs +++ /dev/null @@ -1,28 +0,0 @@ -//! This module provides platform related functions. - -#[cfg(unix)] -#[cfg(feature = "events")] -pub use self::unix::supports_keyboard_enhancement; -#[cfg(unix)] -pub(crate) use self::unix::{ - disable_raw_mode, enable_raw_mode, fd_disable_raw_mode, fd_enable_raw_mode, fd_size, - fd_window_size, is_raw_mode_enabled, size, window_size, -}; -#[cfg(windows)] -#[cfg(feature = "events")] -pub use self::windows::supports_keyboard_enhancement; -#[cfg(all(windows, test))] -pub(crate) use self::windows::temp_screen_buffer; -#[cfg(windows)] -pub(crate) use self::windows::{ - clear, disable_raw_mode, enable_raw_mode, is_raw_mode_enabled, scroll_down, scroll_up, - set_size, set_window_title, size, window_size, -}; - -#[cfg(windows)] -mod windows; - -#[cfg(unix)] -pub mod file_descriptor; -#[cfg(unix)] -mod unix; diff --git a/crates/util/keyfork-crossterm/src/terminal/sys/file_descriptor.rs b/crates/util/keyfork-crossterm/src/terminal/sys/file_descriptor.rs deleted file mode 100644 index 8df9620..0000000 --- a/crates/util/keyfork-crossterm/src/terminal/sys/file_descriptor.rs +++ /dev/null @@ -1,89 +0,0 @@ -use std::{ - fs, io, - os::unix::{ - io::{IntoRawFd, RawFd}, - prelude::AsRawFd, - }, -}; - -use libc::size_t; - -/// A file descriptor wrapper. -/// -/// It allows to retrieve raw file descriptor, write to the file descriptor and -/// mainly it closes the file descriptor once dropped. -#[derive(Debug)] -pub struct FileDesc { - fd: RawFd, - close_on_drop: bool, -} - -impl FileDesc { - /// Constructs a new `FileDesc` with the given `RawFd`. - /// - /// # Arguments - /// - /// * `fd` - raw file descriptor - /// * `close_on_drop` - specify if the raw file descriptor should be closed once the `FileDesc` is dropped - pub fn new(fd: RawFd, close_on_drop: bool) -> FileDesc { - FileDesc { fd, close_on_drop } - } - - pub fn read(&self, buffer: &mut [u8], size: usize) -> io::Result { - let result = unsafe { - libc::read( - self.fd, - buffer.as_mut_ptr() as *mut libc::c_void, - size as size_t, - ) - }; - - if result < 0 { - Err(io::Error::last_os_error()) - } else { - Ok(result as usize) - } - } - - /// Returns the underlying file descriptor. - pub fn raw_fd(&self) -> RawFd { - self.fd - } -} - -impl Drop for FileDesc { - fn drop(&mut self) { - if self.close_on_drop { - // Note that errors are ignored when closing a file descriptor. The - // reason for this is that if an error occurs we don't actually know if - // the file descriptor was closed or not, and if we retried (for - // something like EINTR), we might close another valid file descriptor - // opened after we closed ours. - let _ = unsafe { libc::close(self.fd) }; - } - } -} - -impl AsRawFd for FileDesc { - fn as_raw_fd(&self) -> RawFd { - self.raw_fd() - } -} - -/// Creates a file descriptor pointing to the standard input or `/dev/tty`. -pub fn tty_fd() -> io::Result { - let (fd, close_on_drop) = if unsafe { libc::isatty(libc::STDIN_FILENO) == 1 } { - (libc::STDIN_FILENO, false) - } else { - ( - fs::OpenOptions::new() - .read(true) - .write(true) - .open("/dev/tty")? - .into_raw_fd(), - true, - ) - }; - - Ok(FileDesc::new(fd, close_on_drop)) -} diff --git a/crates/util/keyfork-crossterm/src/terminal/sys/unix.rs b/crates/util/keyfork-crossterm/src/terminal/sys/unix.rs deleted file mode 100644 index fbe4b0a..0000000 --- a/crates/util/keyfork-crossterm/src/terminal/sys/unix.rs +++ /dev/null @@ -1,274 +0,0 @@ -//! UNIX related logic for terminal manipulation. - -use crate::terminal::{ - sys::file_descriptor::{tty_fd, FileDesc}, - WindowSize, -}; -use libc::{ - cfmakeraw, ioctl, tcgetattr, tcsetattr, termios as Termios, winsize, STDOUT_FILENO, TCSANOW, - TIOCGWINSZ, -}; -use parking_lot::Mutex; -use std::fs::File; - -use std::os::unix::io::{IntoRawFd, RawFd}; - -use std::{io, mem, process}; - -// Some(Termios) -> we're in the raw mode and this is the previous mode -// None -> we're not in the raw mode -static TERMINAL_MODE_PRIOR_RAW_MODE: Mutex> = parking_lot::const_mutex(None); - -pub(crate) fn is_raw_mode_enabled() -> bool { - TERMINAL_MODE_PRIOR_RAW_MODE.lock().is_some() -} - -impl From for WindowSize { - fn from(size: winsize) -> WindowSize { - WindowSize { - columns: size.ws_col, - rows: size.ws_row, - width: size.ws_xpixel, - height: size.ws_ypixel, - } - } -} - -pub(crate) fn fd_window_size(fd: i32) -> io::Result { - let mut size = winsize { - ws_row: 0, - ws_col: 0, - ws_xpixel: 0, - ws_ypixel: 0, - }; - - #[allow(clippy::useless_conversion)] - if wrap_with_result(unsafe { ioctl(fd, TIOCGWINSZ.into(), &mut size) }).is_ok() { - return Ok(size.into()); - } - - Err(std::io::Error::last_os_error()) -} - -#[allow(clippy::useless_conversion)] -pub(crate) fn window_size() -> io::Result { - // http://rosettacode.org/wiki/Terminal_control/Dimensions#Library:_BSD_libc - let mut size = winsize { - ws_row: 0, - ws_col: 0, - ws_xpixel: 0, - ws_ypixel: 0, - }; - - let file = File::open("/dev/tty").map(|file| (FileDesc::new(file.into_raw_fd(), true))); - let fd = if let Ok(file) = &file { - file.raw_fd() - } else { - // Fallback to libc::STDOUT_FILENO if /dev/tty is missing - STDOUT_FILENO - }; - - if wrap_with_result(unsafe { ioctl(fd, TIOCGWINSZ.into(), &mut size) }).is_ok() { - return Ok(size.into()); - } - - Err(std::io::Error::last_os_error().into()) -} - -pub(crate) fn fd_size(fd: i32) -> io::Result<(u16, u16)> { - fd_window_size(fd).map(|WindowSize { rows, columns, .. }| (columns, rows)) -} - -#[allow(clippy::useless_conversion)] -pub(crate) fn size() -> io::Result<(u16, u16)> { - if let Ok(window_size) = window_size() { - return Ok((window_size.columns, window_size.rows)); - } - - tput_size().ok_or_else(|| std::io::Error::last_os_error().into()) -} - -pub(crate) fn fd_enable_raw_mode(fd: i32) -> io::Result { - let mut ios = get_terminal_attr(fd)?; - let original_mode_ios = ios; - - raw_terminal_attr(&mut ios); - set_terminal_attr(fd, &ios)?; - Ok(original_mode_ios) -} - -pub(crate) fn enable_raw_mode() -> io::Result<()> { - let mut original_mode = TERMINAL_MODE_PRIOR_RAW_MODE.lock(); - - if original_mode.is_some() { - return Ok(()); - } - - let tty = tty_fd()?; - let fd = tty.raw_fd(); - let mut ios = get_terminal_attr(fd)?; - let original_mode_ios = ios; - - raw_terminal_attr(&mut ios); - set_terminal_attr(fd, &ios)?; - - // Keep it last - set the original mode only if we were able to switch to the raw mode - *original_mode = Some(original_mode_ios); - - Ok(()) -} - -pub(crate) fn fd_disable_raw_mode(fd: i32, termios: Termios) -> io::Result<()> { - set_terminal_attr(fd, &termios)?; - Ok(()) -} - -/// Reset the raw mode. -/// -/// More precisely, reset the whole termios mode to what it was before the first call -/// to [enable_raw_mode]. If you don't mess with termios outside of crossterm, it's -/// effectively disabling the raw mode and doing nothing else. -pub(crate) fn disable_raw_mode() -> io::Result<()> { - let mut original_mode = TERMINAL_MODE_PRIOR_RAW_MODE.lock(); - - if let Some(original_mode_ios) = original_mode.as_ref() { - let tty = tty_fd()?; - set_terminal_attr(tty.raw_fd(), original_mode_ios)?; - // Keep it last - remove the original mode only if we were able to switch back - *original_mode = None; - } - - Ok(()) -} - -/// Queries the terminal's support for progressive keyboard enhancement. -/// -/// On unix systems, this function will block and possibly time out while -/// [`crossterm::event::read`](crate::event::read()) or [`crossterm::event::poll`](crate::event::poll) are being called. -#[cfg(feature = "events")] -pub fn supports_keyboard_enhancement() -> io::Result { - if is_raw_mode_enabled() { - read_supports_keyboard_enhancement_raw() - } else { - read_supports_keyboard_enhancement_flags() - } -} - -#[cfg(feature = "events")] -fn read_supports_keyboard_enhancement_flags() -> io::Result { - enable_raw_mode()?; - let flags = read_supports_keyboard_enhancement_raw(); - disable_raw_mode()?; - flags -} - -#[cfg(feature = "events")] -fn read_supports_keyboard_enhancement_raw() -> io::Result { - use crate::event::{ - filter::{KeyboardEnhancementFlagsFilter, PrimaryDeviceAttributesFilter}, - poll_internal, read_internal, InternalEvent, - }; - use std::io::Write; - use std::time::Duration; - - // This is the recommended method for testing support for the keyboard enhancement protocol. - // We send a query for the flags supported by the terminal and then the primary device attributes - // query. If we receive the primary device attributes response but not the keyboard enhancement - // flags, none of the flags are supported. - // - // See - - // ESC [ ? u Query progressive keyboard enhancement flags (kitty protocol). - // ESC [ c Query primary device attributes. - const QUERY: &[u8] = b"\x1B[?u\x1B[c"; - - let result = File::open("/dev/tty").and_then(|mut file| { - file.write_all(QUERY)?; - file.flush() - }); - if result.is_err() { - let mut stdout = io::stdout(); - stdout.write_all(QUERY)?; - stdout.flush()?; - } - - loop { - match poll_internal( - Some(Duration::from_millis(2000)), - &KeyboardEnhancementFlagsFilter, - ) { - Ok(true) => { - match read_internal(&KeyboardEnhancementFlagsFilter) { - Ok(InternalEvent::KeyboardEnhancementFlags(_current_flags)) => { - // Flush the PrimaryDeviceAttributes out of the event queue. - read_internal(&PrimaryDeviceAttributesFilter).ok(); - return Ok(true); - } - _ => return Ok(false), - } - } - Ok(false) => { - return Err(io::Error::new( - io::ErrorKind::Other, - "The keyboard enhancement status could not be read within a normal duration", - )); - } - Err(_) => {} - } - } -} - -/// execute tput with the given argument and parse -/// the output as a u16. -/// -/// The arg should be "cols" or "lines" -fn tput_value(arg: &str) -> Option { - let output = process::Command::new("tput").arg(arg).output().ok()?; - let value = output - .stdout - .into_iter() - .filter_map(|b| char::from(b).to_digit(10)) - .fold(0, |v, n| v * 10 + n as u16); - - if value > 0 { - Some(value) - } else { - None - } -} - -/// Returns the size of the screen as determined by tput. -/// -/// This alternate way of computing the size is useful -/// when in a subshell. -fn tput_size() -> Option<(u16, u16)> { - match (tput_value("cols"), tput_value("lines")) { - (Some(w), Some(h)) => Some((w, h)), - _ => None, - } -} - -// Transform the given mode into an raw mode (non-canonical) mode. -fn raw_terminal_attr(termios: &mut Termios) { - unsafe { cfmakeraw(termios) } -} - -fn get_terminal_attr(fd: RawFd) -> io::Result { - unsafe { - let mut termios = mem::zeroed(); - wrap_with_result(tcgetattr(fd, &mut termios))?; - Ok(termios) - } -} - -fn set_terminal_attr(fd: RawFd, termios: &Termios) -> io::Result<()> { - wrap_with_result(unsafe { tcsetattr(fd, TCSANOW, termios) }) -} - -fn wrap_with_result(result: i32) -> io::Result<()> { - if result == -1 { - Err(io::Error::last_os_error()) - } else { - Ok(()) - } -} diff --git a/crates/util/keyfork-crossterm/src/terminal/sys/windows.rs b/crates/util/keyfork-crossterm/src/terminal/sys/windows.rs deleted file mode 100644 index 2e408b5..0000000 --- a/crates/util/keyfork-crossterm/src/terminal/sys/windows.rs +++ /dev/null @@ -1,471 +0,0 @@ -//! WinAPI related logic for terminal manipulation. - -use std::fmt::{self, Write}; -use std::io::{self}; - -use crossterm_winapi::{Console, ConsoleMode, Coord, Handle, ScreenBuffer, Size}; -use winapi::{ - shared::minwindef::DWORD, - um::wincon::{SetConsoleTitleW, ENABLE_ECHO_INPUT, ENABLE_LINE_INPUT, ENABLE_PROCESSED_INPUT}, -}; - -use crate::{ - cursor, - terminal::{ClearType, WindowSize}, -}; - -/// bits which can't be set in raw mode -const NOT_RAW_MODE_MASK: DWORD = ENABLE_LINE_INPUT | ENABLE_ECHO_INPUT | ENABLE_PROCESSED_INPUT; - -pub(crate) fn is_raw_mode_enabled() -> std::io::Result { - let console_mode = ConsoleMode::from(Handle::current_in_handle()?); - - let dw_mode = console_mode.mode()?; - - Ok( - // check none of the "not raw" bits is set - dw_mode & NOT_RAW_MODE_MASK == 0, - ) -} - -pub(crate) fn enable_raw_mode() -> std::io::Result<()> { - let console_mode = ConsoleMode::from(Handle::current_in_handle()?); - - let dw_mode = console_mode.mode()?; - - let new_mode = dw_mode & !NOT_RAW_MODE_MASK; - - console_mode.set_mode(new_mode)?; - - Ok(()) -} - -pub(crate) fn disable_raw_mode() -> std::io::Result<()> { - let console_mode = ConsoleMode::from(Handle::current_in_handle()?); - - let dw_mode = console_mode.mode()?; - - let new_mode = dw_mode | NOT_RAW_MODE_MASK; - - console_mode.set_mode(new_mode)?; - - Ok(()) -} - -pub(crate) fn size() -> io::Result<(u16, u16)> { - let terminal_size = ScreenBuffer::current()?.info()?.terminal_size(); - // windows starts counting at 0, unix at 1, add one to replicated unix behaviour. - Ok(( - (terminal_size.width + 1) as u16, - (terminal_size.height + 1) as u16, - )) -} - -pub(crate) fn window_size() -> io::Result { - Err(io::Error::new( - io::ErrorKind::Unsupported, - "Window pixel size not implemented for the Windows API.", - )) -} - -/// Queries the terminal's support for progressive keyboard enhancement. -/// -/// This always returns `Ok(false)` on Windows. -#[cfg(feature = "events")] -pub fn supports_keyboard_enhancement() -> std::io::Result { - Ok(false) -} - -pub(crate) fn clear(clear_type: ClearType) -> std::io::Result<()> { - let screen_buffer = ScreenBuffer::current()?; - let csbi = screen_buffer.info()?; - - let pos = csbi.cursor_pos(); - let buffer_size = csbi.buffer_size(); - let current_attribute = csbi.attributes(); - - match clear_type { - ClearType::All => { - clear_entire_screen(buffer_size, current_attribute)?; - } - ClearType::FromCursorDown => clear_after_cursor(pos, buffer_size, current_attribute)?, - ClearType::FromCursorUp => clear_before_cursor(pos, buffer_size, current_attribute)?, - ClearType::CurrentLine => clear_current_line(pos, buffer_size, current_attribute)?, - ClearType::UntilNewLine => clear_until_line(pos, buffer_size, current_attribute)?, - _ => { - clear_entire_screen(buffer_size, current_attribute)?; - } //TODO: make purge flush the entire screen buffer not just the visible window. - }; - Ok(()) -} - -pub(crate) fn scroll_up(row_count: u16) -> std::io::Result<()> { - let csbi = ScreenBuffer::current()?; - let mut window = csbi.info()?.terminal_window(); - - // check whether the window is too close to the screen buffer top - let count = row_count as i16; - if window.top >= count { - window.top -= count; // move top down - window.bottom -= count; // move bottom down - - Console::output()?.set_console_info(true, window)?; - } - Ok(()) -} - -pub(crate) fn scroll_down(row_count: u16) -> std::io::Result<()> { - let screen_buffer = ScreenBuffer::current()?; - let csbi = screen_buffer.info()?; - let mut window = csbi.terminal_window(); - let buffer_size = csbi.buffer_size(); - - // check whether the window is too close to the screen buffer top - let count = row_count as i16; - if window.bottom < buffer_size.height - count { - window.top += count; // move top down - window.bottom += count; // move bottom down - - Console::output()?.set_console_info(true, window)?; - } - Ok(()) -} - -pub(crate) fn set_size(width: u16, height: u16) -> std::io::Result<()> { - if width <= 1 { - return Err(io::Error::new( - io::ErrorKind::InvalidInput, - "terminal width must be at least 1", - )); - } - - if height <= 1 { - return Err(io::Error::new( - io::ErrorKind::InvalidInput, - "terminal height must be at least 1", - )); - } - - // get the position of the current console window - let screen_buffer = ScreenBuffer::current()?; - let console = Console::from(screen_buffer.handle().clone()); - let csbi = screen_buffer.info()?; - - let current_size = csbi.buffer_size(); - let window = csbi.terminal_window(); - - let mut new_size = Size::new(current_size.width, current_size.height); - - // If the buffer is smaller than this new window size, resize the - // buffer to be large enough. Include window position. - let mut resize_buffer = false; - - let width = width as i16; - if current_size.width < window.left + width { - if window.left >= i16::max_value() - width { - return Err(io::Error::new( - io::ErrorKind::InvalidInput, - "terminal width too large", - )); - } - - new_size.width = window.left + width; - resize_buffer = true; - } - let height = height as i16; - if current_size.height < window.top + height { - if window.top >= i16::max_value() - height { - return Err(io::Error::new( - io::ErrorKind::InvalidInput, - "terminal height too large", - )); - } - - new_size.height = window.top + height; - resize_buffer = true; - } - - if resize_buffer { - screen_buffer.set_size(new_size.width - 1, new_size.height - 1)?; - } - - let mut window = window; - - // preserve the position, but change the size. - window.bottom = window.top + height - 1; - window.right = window.left + width - 1; - console.set_console_info(true, window)?; - - // if we resized the buffer, un-resize it. - if resize_buffer { - screen_buffer.set_size(current_size.width - 1, current_size.height - 1)?; - } - - let bounds = console.largest_window_size()?; - - if width > bounds.x { - return Err(io::Error::new( - io::ErrorKind::InvalidInput, - format!("terminal width {width} too large"), - )); - } - if height > bounds.y { - return Err(io::Error::new( - io::ErrorKind::InvalidInput, - format!("terminal height {height} too large"), - )); - } - - Ok(()) -} - -pub(crate) fn set_window_title(title: impl fmt::Display) -> std::io::Result<()> { - struct Utf16Encoder(Vec); - impl Write for Utf16Encoder { - fn write_str(&mut self, s: &str) -> fmt::Result { - self.0.extend(s.encode_utf16()); - Ok(()) - } - } - - let mut title_utf16 = Utf16Encoder(Vec::new()); - write!(title_utf16, "{title}").expect("formatting failed"); - title_utf16.0.push(0); - let title = title_utf16.0; - - let result = unsafe { SetConsoleTitleW(title.as_ptr()) }; - if result != 0 { - Ok(()) - } else { - Err(io::Error::last_os_error()) - } -} - -fn clear_after_cursor( - location: Coord, - buffer_size: Size, - current_attribute: u16, -) -> std::io::Result<()> { - let (mut x, mut y) = (location.x, location.y); - - // if cursor position is at the outer right position - if x > buffer_size.width { - y += 1; - x = 0; - } - - // location where to start clearing - let start_location = Coord::new(x, y); - - // get sum cells before cursor - let cells_to_write = buffer_size.width as u32 * buffer_size.height as u32; - - clear_winapi(start_location, cells_to_write, current_attribute) -} - -fn clear_before_cursor( - location: Coord, - buffer_size: Size, - current_attribute: u16, -) -> std::io::Result<()> { - let (xpos, ypos) = (location.x, location.y); - - // one cell after cursor position - let x = 0; - // one at row of cursor position - let y = 0; - - // location where to start clearing - let start_location = Coord::new(x, y); - - // get sum cells before cursor - let cells_to_write = (buffer_size.width as u32 * ypos as u32) + (xpos as u32 + 1); - - // clear everything before cursor position - clear_winapi(start_location, cells_to_write, current_attribute) -} - -fn clear_entire_screen(buffer_size: Size, current_attribute: u16) -> std::io::Result<()> { - // get sum cells before cursor - let cells_to_write = buffer_size.width as u32 * buffer_size.height as u32; - - // location where to start clearing - let start_location = Coord::new(0, 0); - - // clear the entire screen - clear_winapi(start_location, cells_to_write, current_attribute)?; - - // put the cursor back at cell 0,0 - cursor::sys::move_to(0, 0)?; - Ok(()) -} - -fn clear_current_line( - location: Coord, - buffer_size: Size, - current_attribute: u16, -) -> std::io::Result<()> { - // location where to start clearing - let start_location = Coord::new(0, location.y); - - // get sum cells before cursor - let cells_to_write = buffer_size.width as u32; - - // clear the whole current line - clear_winapi(start_location, cells_to_write, current_attribute)?; - - // put the cursor back at cell 1 on current row - cursor::sys::move_to(0, location.y as u16)?; - Ok(()) -} - -fn clear_until_line( - location: Coord, - buffer_size: Size, - current_attribute: u16, -) -> std::io::Result<()> { - let (x, y) = (location.x, location.y); - - // location where to start clearing - let start_location = Coord::new(x, y); - - // get sum cells before cursor - let cells_to_write = (buffer_size.width - x) as u32; - - // clear until the current line - clear_winapi(start_location, cells_to_write, current_attribute)?; - - // put the cursor back at original cursor position before we did the clearing - cursor::sys::move_to(x as u16, y as u16)?; - Ok(()) -} - -fn clear_winapi( - start_location: Coord, - cells_to_write: u32, - current_attribute: u16, -) -> std::io::Result<()> { - let console = Console::from(Handle::current_out_handle()?); - console.fill_whit_character(start_location, cells_to_write, ' ')?; - console.fill_whit_attribute(start_location, cells_to_write, current_attribute)?; - Ok(()) -} - -#[cfg(test)] -// Create a new screen buffer to avoid changing the terminal the test -// is running within. -pub fn temp_screen_buffer() -> std::io::Result { - let alternate_screen = ScreenBuffer::create()?; - alternate_screen.show().unwrap(); - Ok(alternate_screen) -} - -#[cfg(test)] -mod tests { - use std::{ffi::OsString, os::windows::ffi::OsStringExt}; - - use crossterm_winapi::ScreenBuffer; - use serial_test::serial; - use winapi::um::wincon::GetConsoleTitleW; - - use super::{scroll_down, scroll_up, set_size, set_window_title, size, temp_screen_buffer}; - - #[test] - #[serial] - fn test_resize_winapi_20_21() { - let _test_screen = temp_screen_buffer().unwrap(); - - let (width, height) = size().unwrap(); - - // The values 20 and 21 are arbitrary and different from each other - // just to see they're not crossed over. - set_size(20, 21).unwrap(); - assert_eq!((20, 21), size().unwrap()); - - // reset to previous size - set_size(width, height).unwrap(); - assert_eq!((width, height), size().unwrap()); - } - - // This is similar to test_resize_winapi_20_21() above. This verifies that - // another test of similar functionality runs independently (that a testing - // race condition has been addressed). - #[test] - #[serial] - #[ignore] - fn test_resize_winapi_30_31() { - let _test_screen = temp_screen_buffer().unwrap(); - - let (width, height) = size().unwrap(); - - set_size(30, 31).unwrap(); - assert_eq!((30, 31), size().unwrap()); - - // reset to previous size - set_size(width, height).unwrap(); - assert_eq!((width, height), size().unwrap()); - } - - // Test is disabled, because it's failing on Travis CI - #[test] - #[ignore] - fn test_scroll_down_winapi() { - let current_window = ScreenBuffer::current() - .unwrap() - .info() - .unwrap() - .terminal_window(); - - scroll_down(2).unwrap(); - - let new_window = ScreenBuffer::current() - .unwrap() - .info() - .unwrap() - .terminal_window(); - - assert_eq!(new_window.top, current_window.top + 2); - assert_eq!(new_window.bottom, current_window.bottom + 2); - } - - // Test is disabled, because it's failing on Travis CI - #[test] - #[ignore] - fn test_scroll_up_winapi() { - // move the terminal buffer down before moving it up - test_scroll_down_winapi(); - - let current_window = ScreenBuffer::current() - .unwrap() - .info() - .unwrap() - .terminal_window(); - - scroll_up(2).unwrap(); - - let new_window = ScreenBuffer::current() - .unwrap() - .info() - .unwrap() - .terminal_window(); - - assert_eq!(new_window.top, current_window.top - 2); - assert_eq!(new_window.bottom, current_window.bottom - 2); - } - - #[test] - #[serial] - fn test_set_title_winapi() { - let _test_screen = temp_screen_buffer().unwrap(); - - let test_title = "this is a crossterm test title"; - set_window_title(test_title).unwrap(); - - let mut raw = [0_u16; 128]; - let length = unsafe { GetConsoleTitleW(raw.as_mut_ptr(), raw.len() as u32) } as usize; - assert_ne!(0, length); - - let console_title = OsString::from_wide(&raw[..length]).into_string().unwrap(); - assert_eq!(test_title, &console_title[..]); - } -} diff --git a/crates/util/keyfork-crossterm/src/tty.rs b/crates/util/keyfork-crossterm/src/tty.rs deleted file mode 100644 index d5d05f6..0000000 --- a/crates/util/keyfork-crossterm/src/tty.rs +++ /dev/null @@ -1,46 +0,0 @@ -//! Making it a little more convenient and safe to query whether -//! something is a terminal teletype or not. -//! This module defines the IsTty trait and the is_tty method to -//! return true if the item represents a terminal. - -#[cfg(unix)] -use std::os::unix::io::AsRawFd; -#[cfg(windows)] -use std::os::windows::io::AsRawHandle; - -#[cfg(windows)] -use winapi::um::consoleapi::GetConsoleMode; - -/// Adds the `is_tty` method to types that might represent a terminal -/// -/// ```rust -/// use std::io::stdout; -/// use keyfork_crossterm::tty::IsTty; -/// -/// let is_tty: bool = stdout().is_tty(); -/// ``` -pub trait IsTty { - /// Returns true when an instance is a terminal teletype, otherwise false. - fn is_tty(&self) -> bool; -} - -/// On UNIX, the `isatty()` function returns true if a file -/// descriptor is a terminal. -#[cfg(unix)] -impl IsTty for S { - fn is_tty(&self) -> bool { - let fd = self.as_raw_fd(); - unsafe { libc::isatty(fd) == 1 } - } -} - -/// On windows, `GetConsoleMode` will return true if we are in a terminal. -/// Otherwise false. -#[cfg(windows)] -impl IsTty for S { - fn is_tty(&self) -> bool { - let mut mode = 0; - let ok = unsafe { GetConsoleMode(self.as_raw_handle() as *mut _, &mut mode) }; - ok == 1 - } -} diff --git a/crates/util/keyfork-prompt/Cargo.toml b/crates/util/keyfork-prompt/Cargo.toml index 9973a65..48ea9e2 100644 --- a/crates/util/keyfork-prompt/Cargo.toml +++ b/crates/util/keyfork-prompt/Cargo.toml @@ -16,7 +16,8 @@ default = ["mnemonic"] mnemonic = ["keyfork-mnemonic"] [dependencies] +crossterm = { version = "0.29.0", default-features = false, features = ["use-dev-tty", "events", "bracketed-paste"] } keyfork-bug = { workspace = true } -keyfork-crossterm = { workspace = true, default-features = false, features = ["use-dev-tty", "events", "bracketed-paste"] } +keyfork-crossterm-ioctl-shim = { version = "0.1.0", path = "../keyfork-crossterm-ioctl-shim" } keyfork-mnemonic = { workspace = true, optional = true } thiserror = { workspace = true } diff --git a/crates/util/keyfork-prompt/src/terminal.rs b/crates/util/keyfork-prompt/src/terminal.rs index 85531eb..bfaf9aa 100644 --- a/crates/util/keyfork-prompt/src/terminal.rs +++ b/crates/util/keyfork-prompt/src/terminal.rs @@ -10,14 +10,14 @@ use std::{ os::fd::AsRawFd, }; -use keyfork_crossterm::{ +use crossterm::{ cursor, event::{read, DisableBracketedPaste, EnableBracketedPaste, Event, KeyCode, KeyModifiers}, style::{Print, PrintStyledContent, Stylize}, - terminal::{self, EnterAlternateScreen, FdTerminal, LeaveAlternateScreen, TerminalIoctl}, - tty::IsTty, + terminal::{self, EnterAlternateScreen, LeaveAlternateScreen}, ExecutableCommand, QueueableCommand, }; +use keyfork_crossterm_ioctl_shim::TerminalIoctl; use keyfork_bug::bug; @@ -32,7 +32,7 @@ where { read: &'a mut BufReader, write: &'a mut W, - terminal: &'a mut FdTerminal, + terminal: &'a mut TerminalIoctl, } impl<'a, R, W> TerminalGuard<'a, R, W> @@ -40,7 +40,7 @@ where W: Write + AsRawFd, R: Read, { - fn new(read: &'a mut BufReader, write: &'a mut W, terminal: &'a mut FdTerminal) -> Self { + fn new(read: &'a mut BufReader, write: &'a mut W, terminal: &'a mut TerminalIoctl) -> Self { Self { read, write, @@ -62,28 +62,10 @@ where self.execute(EnableBracketedPaste)?; Ok(self) } -} -impl TerminalIoctl for TerminalGuard<'_, R, W> -where - R: Read, - W: Write + AsRawFd, -{ - fn enable_raw_mode(&mut self) -> std::io::Result<()> { - self.terminal.enable_raw_mode() - } - - fn disable_raw_mode(&mut self) -> std::io::Result<()> { - self.terminal.disable_raw_mode() - } - - fn size(&self) -> std::io::Result<(u16, u16)> { + fn size(&mut self) -> std::io::Result<(u16, u16)> { self.terminal.size() } - - fn window_size(&self) -> std::io::Result { - self.terminal.window_size() - } } impl Read for TerminalGuard<'_, R, W> @@ -159,7 +141,7 @@ where pub struct Terminal { read: BufReader, write: W, - terminal: FdTerminal, + terminal: TerminalIoctl, } impl Terminal @@ -172,12 +154,9 @@ where /// # Errors /// The function may error if the write handle is not a terminal. pub fn new(read_handle: R, write_handle: W) -> Result { - if !write_handle.is_tty() { - return Err(Error::NotATTY); - } Ok(Self { read: BufReader::new(read_handle), - terminal: FdTerminal::from(write_handle.as_raw_fd()), + terminal: TerminalIoctl::new(write_handle.as_raw_fd()).map_err(|_| Error::NotATTY)?, write: write_handle, }) }