├── .envrc ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE-MIT ├── LICENSE-MPL ├── README.md ├── docs └── windows-vt.md ├── examples ├── detect-features.rs ├── event-read.rs └── window-title.rs ├── flake.lock ├── flake.nix ├── shell.nix └── src ├── base64.rs ├── escape.rs ├── escape ├── csi.rs ├── dcs.rs └── osc.rs ├── event.rs ├── event ├── reader.rs ├── source.rs ├── source │ ├── unix.rs │ └── windows.rs └── stream.rs ├── lib.rs ├── parse.rs ├── style.rs ├── terminal.rs └── terminal ├── unix.rs └── windows.rs /.envrc: -------------------------------------------------------------------------------- 1 | watch_file flake.lock 2 | watch_file shell.nix 3 | 4 | # try to use flakes, if it fails use normal nix (ie. shell.nix) 5 | use flake || use nix 6 | eval "$shellHook" 7 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | pull_request: 4 | push: 5 | env: 6 | MSRV: "1.70" 7 | jobs: 8 | check: 9 | name: Check 10 | strategy: 11 | matrix: 12 | toolchain: 13 | - MSRV 14 | - stable 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: Checkout sources 18 | uses: actions/checkout@v4 19 | 20 | - name: Install toolchain 21 | uses: dtolnay/rust-toolchain@master 22 | with: 23 | toolchain: ${{ matrix.toolchain == 'MSRV' && env.MSRV || 'stable' }} 24 | 25 | - uses: Swatinem/rust-cache@v2 26 | 27 | - name: Run cargo check 28 | run: | 29 | rustc --version 30 | cargo check 31 | 32 | test: 33 | name: Test 34 | runs-on: ${{ matrix.os }} 35 | strategy: 36 | matrix: 37 | os: [ubuntu-latest, macos-latest, windows-latest, ubuntu-24.04-arm] 38 | steps: 39 | - name: Checkout sources 40 | uses: actions/checkout@v4 41 | 42 | - name: Install MSRV toolchain 43 | uses: dtolnay/rust-toolchain@master 44 | with: 45 | toolchain: "${{ env.MSRV }}" 46 | 47 | - uses: Swatinem/rust-cache@v2 48 | 49 | - name: Check rust version 50 | run: rustc --version 51 | 52 | - name: Run cargo test 53 | run: cargo test 54 | 55 | lints: 56 | name: Lints 57 | runs-on: ${{ matrix.os }} 58 | strategy: 59 | matrix: 60 | os: [ubuntu-latest, macos-latest, windows-latest] 61 | steps: 62 | - name: Checkout sources 63 | uses: actions/checkout@v4 64 | 65 | - name: Install MSRV toolchain 66 | uses: dtolnay/rust-toolchain@master 67 | with: 68 | toolchain: "${{ env.MSRV }}" 69 | components: rustfmt, clippy 70 | 71 | - uses: Swatinem/rust-cache@v2 72 | 73 | - name: Check rust version 74 | run: rustc --version 75 | 76 | - name: Run cargo fmt 77 | run: cargo fmt --check 78 | 79 | - name: Run cargo clippy with default features enabled 80 | run: cargo clippy -- -D warnings 81 | 82 | - name: Run cargo clippy with all features enabled 83 | run: cargo clippy --all-features -- -D warnings 84 | 85 | - name: Run cargo doc 86 | run: cargo doc --no-deps --document-private-items 87 | env: 88 | RUSTDOCFLAGS: -D warnings 89 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /result 3 | /.direnv 4 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "autocfg" 7 | version = "1.4.0" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" 10 | 11 | [[package]] 12 | name = "bitflags" 13 | version = "2.9.0" 14 | source = "registry+https://github.com/rust-lang/crates.io-index" 15 | checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" 16 | 17 | [[package]] 18 | name = "cfg-if" 19 | version = "1.0.0" 20 | source = "registry+https://github.com/rust-lang/crates.io-index" 21 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 22 | 23 | [[package]] 24 | name = "errno" 25 | version = "0.3.10" 26 | source = "registry+https://github.com/rust-lang/crates.io-index" 27 | checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" 28 | dependencies = [ 29 | "libc", 30 | "windows-sys", 31 | ] 32 | 33 | [[package]] 34 | name = "futures-core" 35 | version = "0.3.31" 36 | source = "registry+https://github.com/rust-lang/crates.io-index" 37 | checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" 38 | 39 | [[package]] 40 | name = "libc" 41 | version = "0.2.171" 42 | source = "registry+https://github.com/rust-lang/crates.io-index" 43 | checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6" 44 | 45 | [[package]] 46 | name = "linux-raw-sys" 47 | version = "0.9.3" 48 | source = "registry+https://github.com/rust-lang/crates.io-index" 49 | checksum = "fe7db12097d22ec582439daf8618b8fdd1a7bef6270e9af3b1ebcd30893cf413" 50 | 51 | [[package]] 52 | name = "lock_api" 53 | version = "0.4.12" 54 | source = "registry+https://github.com/rust-lang/crates.io-index" 55 | checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" 56 | dependencies = [ 57 | "autocfg", 58 | "scopeguard", 59 | ] 60 | 61 | [[package]] 62 | name = "parking_lot" 63 | version = "0.12.3" 64 | source = "registry+https://github.com/rust-lang/crates.io-index" 65 | checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" 66 | dependencies = [ 67 | "lock_api", 68 | "parking_lot_core", 69 | ] 70 | 71 | [[package]] 72 | name = "parking_lot_core" 73 | version = "0.9.10" 74 | source = "registry+https://github.com/rust-lang/crates.io-index" 75 | checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" 76 | dependencies = [ 77 | "cfg-if", 78 | "libc", 79 | "redox_syscall", 80 | "smallvec", 81 | "windows-targets", 82 | ] 83 | 84 | [[package]] 85 | name = "redox_syscall" 86 | version = "0.5.10" 87 | source = "registry+https://github.com/rust-lang/crates.io-index" 88 | checksum = "0b8c0c260b63a8219631167be35e6a988e9554dbd323f8bd08439c8ed1302bd1" 89 | dependencies = [ 90 | "bitflags", 91 | ] 92 | 93 | [[package]] 94 | name = "rustix" 95 | version = "1.0.5" 96 | source = "registry+https://github.com/rust-lang/crates.io-index" 97 | checksum = "d97817398dd4bb2e6da002002db259209759911da105da92bec29ccb12cf58bf" 98 | dependencies = [ 99 | "bitflags", 100 | "errno", 101 | "libc", 102 | "linux-raw-sys", 103 | "windows-sys", 104 | ] 105 | 106 | [[package]] 107 | name = "scopeguard" 108 | version = "1.2.0" 109 | source = "registry+https://github.com/rust-lang/crates.io-index" 110 | checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" 111 | 112 | [[package]] 113 | name = "signal-hook" 114 | version = "0.3.17" 115 | source = "registry+https://github.com/rust-lang/crates.io-index" 116 | checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801" 117 | dependencies = [ 118 | "libc", 119 | "signal-hook-registry", 120 | ] 121 | 122 | [[package]] 123 | name = "signal-hook-registry" 124 | version = "1.4.2" 125 | source = "registry+https://github.com/rust-lang/crates.io-index" 126 | checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" 127 | dependencies = [ 128 | "libc", 129 | ] 130 | 131 | [[package]] 132 | name = "smallvec" 133 | version = "1.14.0" 134 | source = "registry+https://github.com/rust-lang/crates.io-index" 135 | checksum = "7fcf8323ef1faaee30a44a340193b1ac6814fd9b7b4e88e9d4519a3e4abe1cfd" 136 | 137 | [[package]] 138 | name = "termina" 139 | version = "0.1.0-beta.1" 140 | dependencies = [ 141 | "bitflags", 142 | "futures-core", 143 | "parking_lot", 144 | "rustix", 145 | "signal-hook", 146 | "windows-sys", 147 | ] 148 | 149 | [[package]] 150 | name = "windows-sys" 151 | version = "0.59.0" 152 | source = "registry+https://github.com/rust-lang/crates.io-index" 153 | checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 154 | dependencies = [ 155 | "windows-targets", 156 | ] 157 | 158 | [[package]] 159 | name = "windows-targets" 160 | version = "0.52.6" 161 | source = "registry+https://github.com/rust-lang/crates.io-index" 162 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 163 | dependencies = [ 164 | "windows_aarch64_gnullvm", 165 | "windows_aarch64_msvc", 166 | "windows_i686_gnu", 167 | "windows_i686_gnullvm", 168 | "windows_i686_msvc", 169 | "windows_x86_64_gnu", 170 | "windows_x86_64_gnullvm", 171 | "windows_x86_64_msvc", 172 | ] 173 | 174 | [[package]] 175 | name = "windows_aarch64_gnullvm" 176 | version = "0.52.6" 177 | source = "registry+https://github.com/rust-lang/crates.io-index" 178 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 179 | 180 | [[package]] 181 | name = "windows_aarch64_msvc" 182 | version = "0.52.6" 183 | source = "registry+https://github.com/rust-lang/crates.io-index" 184 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 185 | 186 | [[package]] 187 | name = "windows_i686_gnu" 188 | version = "0.52.6" 189 | source = "registry+https://github.com/rust-lang/crates.io-index" 190 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 191 | 192 | [[package]] 193 | name = "windows_i686_gnullvm" 194 | version = "0.52.6" 195 | source = "registry+https://github.com/rust-lang/crates.io-index" 196 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 197 | 198 | [[package]] 199 | name = "windows_i686_msvc" 200 | version = "0.52.6" 201 | source = "registry+https://github.com/rust-lang/crates.io-index" 202 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 203 | 204 | [[package]] 205 | name = "windows_x86_64_gnu" 206 | version = "0.52.6" 207 | source = "registry+https://github.com/rust-lang/crates.io-index" 208 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 209 | 210 | [[package]] 211 | name = "windows_x86_64_gnullvm" 212 | version = "0.52.6" 213 | source = "registry+https://github.com/rust-lang/crates.io-index" 214 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 215 | 216 | [[package]] 217 | name = "windows_x86_64_msvc" 218 | version = "0.52.6" 219 | source = "registry+https://github.com/rust-lang/crates.io-index" 220 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 221 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "termina" 3 | version = "0.1.0-beta.1" 4 | authors = ["Michael Davis "] 5 | description = "A cross-platform VT manipulation library" 6 | readme = "README.md" 7 | repository = "https://github.com/helix-editor/termina" 8 | edition = "2021" 9 | license = "MIT OR MPL-2.0" 10 | rust-version = "1.70" 11 | 12 | [features] 13 | default = [] 14 | event-stream = ["dep:futures-core"] 15 | 16 | [dependencies] 17 | parking_lot = "0.12" 18 | bitflags = "2" 19 | futures-core = { version = "0.3", optional = true } 20 | 21 | [target.'cfg(unix)'.dependencies] 22 | signal-hook = "0.3" 23 | 24 | [target.'cfg(unix)'.dependencies.rustix] 25 | version = "1" 26 | default-features = false 27 | features = [ 28 | "std", 29 | "stdio", 30 | "termios", 31 | "event", 32 | ] 33 | 34 | [target.'cfg(windows)'.dependencies.windows-sys] 35 | version = "0.59" 36 | default-features = false 37 | # https://microsoft.github.io/windows-rs/features/#/0.59.0/search 38 | features = [ 39 | # Interaction with the legacy and modern console APIs. 40 | "Win32_System_Console", 41 | # Writing files (including console handles). 42 | "Win32_Storage_FileSystem", 43 | "Win32_System_IO", 44 | # Polling for input. 45 | "Win32_System_Threading", 46 | "Win32_Security", 47 | ] 48 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright (c) 2025 Michael Davis 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /LICENSE-MPL: -------------------------------------------------------------------------------- 1 | Mozilla Public License Version 2.0 2 | ================================== 3 | 4 | 1. Definitions 5 | -------------- 6 | 7 | 1.1. "Contributor" 8 | means each individual or legal entity that creates, contributes to 9 | the creation of, or owns Covered Software. 10 | 11 | 1.2. "Contributor Version" 12 | means the combination of the Contributions of others (if any) used 13 | by a Contributor and that particular Contributor's Contribution. 14 | 15 | 1.3. "Contribution" 16 | means Covered Software of a particular Contributor. 17 | 18 | 1.4. "Covered Software" 19 | means Source Code Form to which the initial Contributor has attached 20 | the notice in Exhibit A, the Executable Form of such Source Code 21 | Form, and Modifications of such Source Code Form, in each case 22 | including portions thereof. 23 | 24 | 1.5. "Incompatible With Secondary Licenses" 25 | means 26 | 27 | (a) that the initial Contributor has attached the notice described 28 | in Exhibit B to the Covered Software; or 29 | 30 | (b) that the Covered Software was made available under the terms of 31 | version 1.1 or earlier of the License, but not also under the 32 | terms of a Secondary License. 33 | 34 | 1.6. "Executable Form" 35 | means any form of the work other than Source Code Form. 36 | 37 | 1.7. "Larger Work" 38 | means a work that combines Covered Software with other material, in 39 | a separate file or files, that is not Covered Software. 40 | 41 | 1.8. "License" 42 | means this document. 43 | 44 | 1.9. "Licensable" 45 | means having the right to grant, to the maximum extent possible, 46 | whether at the time of the initial grant or subsequently, any and 47 | all of the rights conveyed by this License. 48 | 49 | 1.10. "Modifications" 50 | means any of the following: 51 | 52 | (a) any file in Source Code Form that results from an addition to, 53 | deletion from, or modification of the contents of Covered 54 | Software; or 55 | 56 | (b) any new file in Source Code Form that contains any Covered 57 | Software. 58 | 59 | 1.11. "Patent Claims" of a Contributor 60 | means any patent claim(s), including without limitation, method, 61 | process, and apparatus claims, in any patent Licensable by such 62 | Contributor that would be infringed, but for the grant of the 63 | License, by the making, using, selling, offering for sale, having 64 | made, import, or transfer of either its Contributions or its 65 | Contributor Version. 66 | 67 | 1.12. "Secondary License" 68 | means either the GNU General Public License, Version 2.0, the GNU 69 | Lesser General Public License, Version 2.1, the GNU Affero General 70 | Public License, Version 3.0, or any later versions of those 71 | licenses. 72 | 73 | 1.13. "Source Code Form" 74 | means the form of the work preferred for making modifications. 75 | 76 | 1.14. "You" (or "Your") 77 | means an individual or a legal entity exercising rights under this 78 | License. For legal entities, "You" includes any entity that 79 | controls, is controlled by, or is under common control with You. For 80 | purposes of this definition, "control" means (a) the power, direct 81 | or indirect, to cause the direction or management of such entity, 82 | whether by contract or otherwise, or (b) ownership of more than 83 | fifty percent (50%) of the outstanding shares or beneficial 84 | ownership of such entity. 85 | 86 | 2. License Grants and Conditions 87 | -------------------------------- 88 | 89 | 2.1. Grants 90 | 91 | Each Contributor hereby grants You a world-wide, royalty-free, 92 | non-exclusive license: 93 | 94 | (a) under intellectual property rights (other than patent or trademark) 95 | Licensable by such Contributor to use, reproduce, make available, 96 | modify, display, perform, distribute, and otherwise exploit its 97 | Contributions, either on an unmodified basis, with Modifications, or 98 | as part of a Larger Work; and 99 | 100 | (b) under Patent Claims of such Contributor to make, use, sell, offer 101 | for sale, have made, import, and otherwise transfer either its 102 | Contributions or its Contributor Version. 103 | 104 | 2.2. Effective Date 105 | 106 | The licenses granted in Section 2.1 with respect to any Contribution 107 | become effective for each Contribution on the date the Contributor first 108 | distributes such Contribution. 109 | 110 | 2.3. Limitations on Grant Scope 111 | 112 | The licenses granted in this Section 2 are the only rights granted under 113 | this License. No additional rights or licenses will be implied from the 114 | distribution or licensing of Covered Software under this License. 115 | Notwithstanding Section 2.1(b) above, no patent license is granted by a 116 | Contributor: 117 | 118 | (a) for any code that a Contributor has removed from Covered Software; 119 | or 120 | 121 | (b) for infringements caused by: (i) Your and any other third party's 122 | modifications of Covered Software, or (ii) the combination of its 123 | Contributions with other software (except as part of its Contributor 124 | Version); or 125 | 126 | (c) under Patent Claims infringed by Covered Software in the absence of 127 | its Contributions. 128 | 129 | This License does not grant any rights in the trademarks, service marks, 130 | or logos of any Contributor (except as may be necessary to comply with 131 | the notice requirements in Section 3.4). 132 | 133 | 2.4. Subsequent Licenses 134 | 135 | No Contributor makes additional grants as a result of Your choice to 136 | distribute the Covered Software under a subsequent version of this 137 | License (see Section 10.2) or under the terms of a Secondary License (if 138 | permitted under the terms of Section 3.3). 139 | 140 | 2.5. Representation 141 | 142 | Each Contributor represents that the Contributor believes its 143 | Contributions are its original creation(s) or it has sufficient rights 144 | to grant the rights to its Contributions conveyed by this License. 145 | 146 | 2.6. Fair Use 147 | 148 | This License is not intended to limit any rights You have under 149 | applicable copyright doctrines of fair use, fair dealing, or other 150 | equivalents. 151 | 152 | 2.7. Conditions 153 | 154 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted 155 | in Section 2.1. 156 | 157 | 3. Responsibilities 158 | ------------------- 159 | 160 | 3.1. Distribution of Source Form 161 | 162 | All distribution of Covered Software in Source Code Form, including any 163 | Modifications that You create or to which You contribute, must be under 164 | the terms of this License. You must inform recipients that the Source 165 | Code Form of the Covered Software is governed by the terms of this 166 | License, and how they can obtain a copy of this License. You may not 167 | attempt to alter or restrict the recipients' rights in the Source Code 168 | Form. 169 | 170 | 3.2. Distribution of Executable Form 171 | 172 | If You distribute Covered Software in Executable Form then: 173 | 174 | (a) such Covered Software must also be made available in Source Code 175 | Form, as described in Section 3.1, and You must inform recipients of 176 | the Executable Form how they can obtain a copy of such Source Code 177 | Form by reasonable means in a timely manner, at a charge no more 178 | than the cost of distribution to the recipient; and 179 | 180 | (b) You may distribute such Executable Form under the terms of this 181 | License, or sublicense it under different terms, provided that the 182 | license for the Executable Form does not attempt to limit or alter 183 | the recipients' rights in the Source Code Form under this License. 184 | 185 | 3.3. Distribution of a Larger Work 186 | 187 | You may create and distribute a Larger Work under terms of Your choice, 188 | provided that You also comply with the requirements of this License for 189 | the Covered Software. If the Larger Work is a combination of Covered 190 | Software with a work governed by one or more Secondary Licenses, and the 191 | Covered Software is not Incompatible With Secondary Licenses, this 192 | License permits You to additionally distribute such Covered Software 193 | under the terms of such Secondary License(s), so that the recipient of 194 | the Larger Work may, at their option, further distribute the Covered 195 | Software under the terms of either this License or such Secondary 196 | License(s). 197 | 198 | 3.4. Notices 199 | 200 | You may not remove or alter the substance of any license notices 201 | (including copyright notices, patent notices, disclaimers of warranty, 202 | or limitations of liability) contained within the Source Code Form of 203 | the Covered Software, except that You may alter any license notices to 204 | the extent required to remedy known factual inaccuracies. 205 | 206 | 3.5. Application of Additional Terms 207 | 208 | You may choose to offer, and to charge a fee for, warranty, support, 209 | indemnity or liability obligations to one or more recipients of Covered 210 | Software. However, You may do so only on Your own behalf, and not on 211 | behalf of any Contributor. You must make it absolutely clear that any 212 | such warranty, support, indemnity, or liability obligation is offered by 213 | You alone, and You hereby agree to indemnify every Contributor for any 214 | liability incurred by such Contributor as a result of warranty, support, 215 | indemnity or liability terms You offer. You may include additional 216 | disclaimers of warranty and limitations of liability specific to any 217 | jurisdiction. 218 | 219 | 4. Inability to Comply Due to Statute or Regulation 220 | --------------------------------------------------- 221 | 222 | If it is impossible for You to comply with any of the terms of this 223 | License with respect to some or all of the Covered Software due to 224 | statute, judicial order, or regulation then You must: (a) comply with 225 | the terms of this License to the maximum extent possible; and (b) 226 | describe the limitations and the code they affect. Such description must 227 | be placed in a text file included with all distributions of the Covered 228 | Software under this License. Except to the extent prohibited by statute 229 | or regulation, such description must be sufficiently detailed for a 230 | recipient of ordinary skill to be able to understand it. 231 | 232 | 5. Termination 233 | -------------- 234 | 235 | 5.1. The rights granted under this License will terminate automatically 236 | if You fail to comply with any of its terms. However, if You become 237 | compliant, then the rights granted under this License from a particular 238 | Contributor are reinstated (a) provisionally, unless and until such 239 | Contributor explicitly and finally terminates Your grants, and (b) on an 240 | ongoing basis, if such Contributor fails to notify You of the 241 | non-compliance by some reasonable means prior to 60 days after You have 242 | come back into compliance. Moreover, Your grants from a particular 243 | Contributor are reinstated on an ongoing basis if such Contributor 244 | notifies You of the non-compliance by some reasonable means, this is the 245 | first time You have received notice of non-compliance with this License 246 | from such Contributor, and You become compliant prior to 30 days after 247 | Your receipt of the notice. 248 | 249 | 5.2. If You initiate litigation against any entity by asserting a patent 250 | infringement claim (excluding declaratory judgment actions, 251 | counter-claims, and cross-claims) alleging that a Contributor Version 252 | directly or indirectly infringes any patent, then the rights granted to 253 | You by any and all Contributors for the Covered Software under Section 254 | 2.1 of this License shall terminate. 255 | 256 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all 257 | end user license agreements (excluding distributors and resellers) which 258 | have been validly granted by You or Your distributors under this License 259 | prior to termination shall survive termination. 260 | 261 | ************************************************************************ 262 | * * 263 | * 6. Disclaimer of Warranty * 264 | * ------------------------- * 265 | * * 266 | * Covered Software is provided under this License on an "as is" * 267 | * basis, without warranty of any kind, either expressed, implied, or * 268 | * statutory, including, without limitation, warranties that the * 269 | * Covered Software is free of defects, merchantable, fit for a * 270 | * particular purpose or non-infringing. The entire risk as to the * 271 | * quality and performance of the Covered Software is with You. * 272 | * Should any Covered Software prove defective in any respect, You * 273 | * (not any Contributor) assume the cost of any necessary servicing, * 274 | * repair, or correction. This disclaimer of warranty constitutes an * 275 | * essential part of this License. No use of any Covered Software is * 276 | * authorized under this License except under this disclaimer. * 277 | * * 278 | ************************************************************************ 279 | 280 | ************************************************************************ 281 | * * 282 | * 7. Limitation of Liability * 283 | * -------------------------- * 284 | * * 285 | * Under no circumstances and under no legal theory, whether tort * 286 | * (including negligence), contract, or otherwise, shall any * 287 | * Contributor, or anyone who distributes Covered Software as * 288 | * permitted above, be liable to You for any direct, indirect, * 289 | * special, incidental, or consequential damages of any character * 290 | * including, without limitation, damages for lost profits, loss of * 291 | * goodwill, work stoppage, computer failure or malfunction, or any * 292 | * and all other commercial damages or losses, even if such party * 293 | * shall have been informed of the possibility of such damages. This * 294 | * limitation of liability shall not apply to liability for death or * 295 | * personal injury resulting from such party's negligence to the * 296 | * extent applicable law prohibits such limitation. Some * 297 | * jurisdictions do not allow the exclusion or limitation of * 298 | * incidental or consequential damages, so this exclusion and * 299 | * limitation may not apply to You. * 300 | * * 301 | ************************************************************************ 302 | 303 | 8. Litigation 304 | ------------- 305 | 306 | Any litigation relating to this License may be brought only in the 307 | courts of a jurisdiction where the defendant maintains its principal 308 | place of business and such litigation shall be governed by laws of that 309 | jurisdiction, without reference to its conflict-of-law provisions. 310 | Nothing in this Section shall prevent a party's ability to bring 311 | cross-claims or counter-claims. 312 | 313 | 9. Miscellaneous 314 | ---------------- 315 | 316 | This License represents the complete agreement concerning the subject 317 | matter hereof. If any provision of this License is held to be 318 | unenforceable, such provision shall be reformed only to the extent 319 | necessary to make it enforceable. Any law or regulation which provides 320 | that the language of a contract shall be construed against the drafter 321 | shall not be used to construe this License against a Contributor. 322 | 323 | 10. Versions of the License 324 | --------------------------- 325 | 326 | 10.1. New Versions 327 | 328 | Mozilla Foundation is the license steward. Except as provided in Section 329 | 10.3, no one other than the license steward has the right to modify or 330 | publish new versions of this License. Each version will be given a 331 | distinguishing version number. 332 | 333 | 10.2. Effect of New Versions 334 | 335 | You may distribute the Covered Software under the terms of the version 336 | of the License under which You originally received the Covered Software, 337 | or under the terms of any subsequent version published by the license 338 | steward. 339 | 340 | 10.3. Modified Versions 341 | 342 | If you create software not governed by this License, and you want to 343 | create a new license for such software, you may create and use a 344 | modified version of this License if you rename the license and remove 345 | any references to the name of the license steward (except to note that 346 | such modified license differs from this License). 347 | 348 | 10.4. Distributing Source Code Form that is Incompatible With Secondary 349 | Licenses 350 | 351 | If You choose to distribute Source Code Form that is Incompatible With 352 | Secondary Licenses under the terms of this version of the License, the 353 | notice described in Exhibit B of this License must be attached. 354 | 355 | Exhibit A - Source Code Form License Notice 356 | ------------------------------------------- 357 | 358 | This Source Code Form is subject to the terms of the Mozilla Public 359 | License, v. 2.0. If a copy of the MPL was not distributed with this 360 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 361 | 362 | If it is not possible or desirable to put the notice in a particular 363 | file, then You may include the notice in a location (such as a LICENSE 364 | file in a relevant directory) where a recipient would be likely to look 365 | for such a notice. 366 | 367 | You may add additional accurate notices of copyright ownership. 368 | 369 | Exhibit B - "Incompatible With Secondary Licenses" Notice 370 | --------------------------------------------------------- 371 | 372 | This Source Code Form is "Incompatible With Secondary Licenses", as 373 | defined by the Mozilla Public License, v. 2.0. 374 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Termina 2 | 3 | [![Crates.io](https://img.shields.io/crates/v/termina.svg)](https://crates.io/crates/termina) 4 | [![Documentation](https://docs.rs/termina/badge.svg)](https://docs.rs/termina) 5 | 6 | A cross-platform "virtual terminal" (VT) manipulation library. 7 | 8 | Termina only "speaks text/VT" but aims to work on Windows as well as *NIX. This is made possible by Microsoft's investment into [ConPTY](https://devblogs.microsoft.com/commandline/windows-command-line-introducing-the-windows-pseudo-console-conpty/). This means that Termina requires 64-bit Windows 10.0.17763 (released around Fall 2018) or later ([same as WezTerm](https://wezterm.org/install/windows.html)). 9 | 10 | Termina is a cross between [Crossterm](https://github.com/crossterm-rs/crossterm) and [TermWiz](https://github.com/wezterm/wezterm/blob/a87358516004a652ad840bc1661bdf65ffc89b43/termwiz/README.md) with a lower level API which exposes escape codes to consuming applications. The aim is to scale well in the long run as terminals introduce VT extensions like the [Kitty Keyboard Protocol](https://sw.kovidgoyal.net/kitty/keyboard-protocol/) or [Contour's Dark/Light mode detection](https://contour-terminal.org/vt-extensions/color-palette-update-notifications/) - those requiring minimal changes in Termina and also allowing flexibility in how applications detect and handle these extensions. See `examples/event-read.rs` for a look at a basic API. 11 | 12 | ## Credit 13 | 14 | Termina contains significant code sourced and/or modified from other projects, especially Crossterm and TermWiz. See "CREDIT" comments in the source for details on what was copied and what modifications were made. Since all copied code is licensed under MIT, Termina is offered under the MIT license as well at your option. 15 | 16 |
Crossterm license... 17 | 18 | ``` 19 | MIT License 20 | 21 | Copyright (c) 2019 Timon 22 | 23 | Permission is hereby granted, free of charge, to any person obtaining a copy 24 | of this software and associated documentation files (the "Software"), to deal 25 | in the Software without restriction, including without limitation the rights 26 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 27 | copies of the Software, and to permit persons to whom the Software is 28 | furnished to do so, subject to the following conditions: 29 | 30 | The above copyright notice and this permission notice shall be included in all 31 | copies or substantial portions of the Software. 32 | 33 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 34 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 35 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 36 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 37 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 38 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 39 | SOFTWARE. 40 | ``` 41 | 42 |
43 | 44 |
TermWiz license... 45 | 46 | ``` 47 | MIT License 48 | 49 | Copyright (c) 2018 Wez Furlong 50 | 51 | Permission is hereby granted, free of charge, to any person obtaining a copy 52 | of this software and associated documentation files (the "Software"), to deal 53 | in the Software without restriction, including without limitation the rights 54 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 55 | copies of the Software, and to permit persons to whom the Software is 56 | furnished to do so, subject to the following conditions: 57 | 58 | The above copyright notice and this permission notice shall be included in all 59 | copies or substantial portions of the Software. 60 | 61 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 62 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 63 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 64 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 65 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 66 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 67 | SOFTWARE. 68 | ``` 69 | 70 |
71 | 72 | ## License 73 | 74 | Licensed under either of: 75 | 76 | * Mozilla Public License, v. 2.0, ([LICENSE-MPL](./LICENSE-MPL) or http://mozilla.org/MPL/2.0/) 77 | * MIT license ([LICENSE-MIT](./LICENSE-MIT) or https://opensource.org/licenses/MIT) 78 | 79 | at your option. 80 | -------------------------------------------------------------------------------- /docs/windows-vt.md: -------------------------------------------------------------------------------- 1 | # Reading Virtual Terminal sequences from the Windows Console 2 | 3 | Since the addition of [ConPTY](https://devblogs.microsoft.com/commandline/windows-command-line-introducing-the-windows-pseudo-console-conpty/), it is now possible to interact with the Windows Console API with escape sequences like a *NIX PTY. There aren't many good examples on doing this though. This doc will show code snippets for reading VT sequences from the Windows Console. 4 | 5 | This doc assumes you have Rust experience, especially with calling into system APIs, for example the `libc`, `rustix` or `windows-sys` crates. In this doc I will gloss over all error handling. To reference how these system calls are handled in Termina see `src/terminal/windows.rs` and `src/event/source/windows.rs`. 6 | 7 | ### Opening handles 8 | 9 | The first step is opening the input and output Console handles. 10 | 11 | ```rust 12 | use std::{io, fs}; 13 | 14 | fn open_pty() -> io::Result<(OwnedHandle, OwnedHandle)> { 15 | let input = fs::OpenOptions::new().read(true).write(true).open("CONIN$")?.into(); 16 | let output = fs::OpenOptions::new().read(true).write(true).open("CONOUT$")?.into(); 17 | Ok((input, output)) 18 | } 19 | ``` 20 | 21 | ### Setting code pages 22 | 23 | Then we want to set the "code pages" (encodings) that we will speak to/from these handles using the `SetConsoleCP` and `SetConsoleOutputCP` functions from the Windows Console API. When "speaking VT" we want this encoding to be UTF-8 so we use the `CP_UTF8` code page ID. 24 | 25 | ```rust 26 | use std::os::windows::io::AsRawHandle; 27 | use windows_sys::Win32::{Globalization::CP_UTF8, System::Console} 28 | 29 | unsafe { Console::SetConsoleCP(input.as_raw_handle(), CP_UTF8) }; 30 | unsafe { Console::SetConsoleOutputCP(output.as_raw_handle(), CP_UTF8) }; 31 | ``` 32 | 33 | ### Setting console modes 34 | 35 | Then we have to alter the [console modes](https://learn.microsoft.com/en-us/windows/console/high-level-console-modes) of the handles. This is somewhat similar to [termios(3)](https://www.man7.org/linux/man-pages/man3/termios.3.html) in *NIX which is used to set raw or cooked modes. There are a few flags we want to enable/disable off the bat. 36 | 37 | ```rust 38 | use windows_sys::Win32::System::Console; 39 | 40 | let mut original_input_mode = 0; 41 | let mut original_output_mode = 0; 42 | 43 | unsafe { 44 | Console::GetConsoleMode(input.as_raw_handle(), &mut original_input_mode); 45 | Console::GetConsoleMode(output.as_raw_handle(), &mut original_output_mode); 46 | } 47 | 48 | let desired_input_mode = original_input_mode | Console::ENABLE_VIRTUAL_TERMINAL_INPUT; 49 | let desired_output_mode = original_output_mode 50 | | Console::ENABLE_VIRTUAL_TERMINAL_PROCESSING 51 | | Console::DISABLE_NEWLINE_AUTO_RETURN; 52 | 53 | unsafe { 54 | Console::SetConsoleMode(input.as_raw_handle(), desired_input_mode); 55 | Console::SetConsoleMode(output.as_raw_handle(), desired_output_mode); 56 | } 57 | ``` 58 | 59 | Now we've done what the Microsoft docs say: we've instructed our input and output handles to speak UTF-8 encoded VT. This is all the setup we need and now we can talk about reading and writing our input and output handles. 60 | 61 | ### Writing VT 62 | 63 | Writing escape sequences is fairly straightforward. We'll add a bit of machinery around the `OwnedHandle` used for the output so we can implement the `std::io::Write` trait and use `write!` in the future. 64 | 65 | ```rust 66 | use windows_sys::Win32::Storage::FileSystem::WriteFile; 67 | use std::ptr; 68 | 69 | struct OutputHandle { 70 | handle: OwnedHandle, 71 | } 72 | 73 | impl io::Write for OutputHandle { 74 | fn write(&mut self, buf: &[u8]) -> io::Result { 75 | let mut num_written = 0; 76 | if unsafe { 77 | WriteFile( 78 | self.handle.as_raw_handle(), 79 | buf.as_ptr(), 80 | buf.len() as u32, 81 | &mut num_written, 82 | ptr::null_mut(); 83 | ) 84 | } == 0 { 85 | Err(io::Error::last_error()) 86 | } else { 87 | Ok(num_written as usize) 88 | } 89 | } 90 | 91 | fn flush(&mut self) -> io::Result<()> { 92 | Ok(()) 93 | } 94 | } 95 | ``` 96 | 97 | Now we can write escape sequences like so: 98 | 99 | ```rust 100 | let output = OutputHandle { handle: output }; 101 | write!(output, "\x1b[32mHello, world!\x1b[0m"); 102 | ``` 103 | 104 | So this works just like how you'd interact with a *NIX PTY: you write the UTF-8 encoded VT bytes to the output file/device. 105 | 106 | ### Reading VT 107 | 108 | Reading is trickier. We could similarly use `ReadFile` or `ReadConsole` on the `CONIN$` handle but the Microsoft docs have a [note about this](https://learn.microsoft.com/en-us/windows/console/classic-vs-vt#exceptions-for-using-windows-console-apis): 109 | 110 | > Applications that must be aware of window size changes will still need to use `ReadConsoleInput` to receive them interleaved with key events as `ReadConsole` alone will discard them. 111 | 112 | `ReadConsoleInput` would otherwise be considered legacy: it's what you would use to read events before ConPTY. We need it though if we want to receive resizing events. Using `ReadConsole` or `ReadFile` would cause these events to be discarded. So how does this work exactly then? 113 | 114 | ```rust 115 | use windows_sys::Win32::System::Console::{ 116 | GetNumberOfConsoleInputEvents, KEY_EVENT, INPUT_RECORD, ReadConsoleInputA, 117 | WINDOW_BUFFER_SIZE_EVENT, 118 | }; 119 | use std::mem; 120 | 121 | let mut num_to_read = 0; 122 | unsafe { GetNumberOfConsoleInputEvents(output.handle.as_raw_handle(), &mut num_to_read) }; 123 | let mut records = Vec::with_capacity(num_to_read as usize); 124 | let zeroed: INPUT_RECORD = unsafe { mem::zeroed() }; 125 | records.resize(num_to_read as usize, zeroed); 126 | let mut num_read = 0; 127 | unsafe { 128 | ReadConsoleInputA(output.handle.as_raw_handle(), records.as_mut_ptr(), new_to_read, &mut num_read); 129 | records.set_len(num_read as usize); 130 | } 131 | 132 | let mut buffer = Vec::new(); 133 | for record in records { 134 | match record.EventType as u32 { 135 | KEY_EVENT => { 136 | let record = unsafe { record.Event.KeyEvent }; 137 | if record.bKeyDown == 0 { 138 | continue; 139 | } 140 | buffer.push(unsafe { record.uChar.AsciiChar } as u8); 141 | } 142 | WINDOW_BUFFER_SIZE_EVENT => { 143 | let record = unsafe { record.Event.WindowBufferSizeEvent }; 144 | // NOTE: Unix sizes are one-indexed. We `+ 1` to normalize to that convention. 145 | let rows = (record.dwSize.Y + 1) as u16; 146 | let cols = (record.dwSize.X + 1) as u16; 147 | todo!("resized to ({rows}, {cols})"); 148 | } 149 | _ => (), 150 | } 151 | } 152 | ``` 153 | 154 | First we check the number of available records with `GetNumberOfConsoleInputEvents` and then fill a buffer of [`INPUT_RECORD`](https://learn.microsoft.com/en-us/windows/console/input-record-str)s with `ReadConsoleInputA`. The [Microsoft docs say](https://learn.microsoft.com/en-us/windows/console/classic-vs-vt#unicode) that `ReadConsoleInputA` is the way to receive UTF-8 encoded text after we've enabled `CP_UTF8`: 155 | 156 | > UTF-8 support in the console can be utilized via the `A` variant of Console APIs against console handles after setting the codepage to `65001` or `CP_UTF8` with the `SetConsoleOutputCP` and `SetConsoleCP` methods, as appropriate. 157 | 158 | In that buffer of input records be care about two events: key events and window resizes. For [`KEY_EVENT_RECORD`](https://learn.microsoft.com/en-us/windows/console/key-event-record-str)s we don't actually care about the virtual key codes. We would if we were reading with the legacy Console API but since we've enabled VT processing on the input, we can expect that the key event record is actually just a byte that we should add to our input buffer. For example if we type the character 'a' then we can expect that the byte 97 arrives as this `record.uChar.AsciiChar`. 159 | 160 | That's the story for reading. Now the input buffer can be parsed the same as the bytes read from a *NIX PTY device. 161 | -------------------------------------------------------------------------------- /examples/detect-features.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | io::{self, Write as _}, 3 | time::Duration, 4 | }; 5 | 6 | use termina::{ 7 | escape::{ 8 | csi::{self, Csi}, 9 | dcs::{self, Dcs}, 10 | }, 11 | style::RgbColor, 12 | Event, PlatformTerminal, Terminal, 13 | }; 14 | 15 | const TEST_COLOR: RgbColor = RgbColor::new(150, 150, 150); 16 | 17 | #[derive(Debug, Default, Clone, Copy)] 18 | struct Features { 19 | kitty_keyboard: bool, 20 | sychronized_output: bool, 21 | true_color: bool, 22 | extended_underlines: bool, 23 | } 24 | 25 | fn main() -> io::Result<()> { 26 | let mut terminal = PlatformTerminal::new()?; 27 | terminal.enter_raw_mode()?; 28 | 29 | write!( 30 | terminal, 31 | "{}{}{}{}{}{}{}", 32 | // Kitty keyboard 33 | Csi::Keyboard(csi::Keyboard::QueryFlags), 34 | // Synchronized output 35 | Csi::Mode(csi::Mode::QueryDecPrivateMode(csi::DecPrivateMode::Code( 36 | csi::DecPrivateModeCode::SynchronizedOutput 37 | ))), 38 | // True color and while we're at it, extended underlines: 39 | // 40 | Csi::Sgr(csi::Sgr::Background(TEST_COLOR.into())), 41 | Csi::Sgr(csi::Sgr::UnderlineColor(TEST_COLOR.into())), 42 | Dcs::Request(dcs::DcsRequest::GraphicRendition), 43 | Csi::Sgr(csi::Sgr::Reset), 44 | // Finally request the primary device attributes 45 | Csi::Device(csi::Device::RequestPrimaryDeviceAttributes), 46 | )?; 47 | terminal.flush()?; 48 | 49 | let mut features = Features::default(); 50 | loop { 51 | if !terminal.poll(Event::is_escape, Some(Duration::from_millis(100)))? { 52 | eprintln!("Did not receive any responses to queries in 100ms\r"); 53 | break; 54 | } 55 | 56 | match terminal.read(Event::is_escape)? { 57 | Event::Csi(Csi::Keyboard(csi::Keyboard::ReportFlags(_))) => { 58 | features.kitty_keyboard = true 59 | } 60 | Event::Csi(Csi::Mode(csi::Mode::ReportDecPrivateMode { 61 | mode: csi::DecPrivateMode::Code(csi::DecPrivateModeCode::SynchronizedOutput), 62 | setting, 63 | })) => { 64 | features.sychronized_output = matches!( 65 | setting, 66 | csi::DecModeSetting::Set | csi::DecModeSetting::Reset 67 | ); 68 | } 69 | Event::Dcs(Dcs::Response { 70 | value: dcs::DcsResponse::GraphicRendition(sgrs), 71 | .. 72 | }) => { 73 | features.true_color = sgrs.contains(&csi::Sgr::Background(TEST_COLOR.into())); 74 | features.extended_underlines = 75 | sgrs.contains(&csi::Sgr::UnderlineColor(TEST_COLOR.into())); 76 | } 77 | Event::Csi(Csi::Device(csi::Device::DeviceAttributes(_))) => break, 78 | other => eprintln!("unexpected event: {other:?}\r"), 79 | } 80 | } 81 | println!("Detected features: {features:?}"); 82 | 83 | Ok(()) 84 | } 85 | -------------------------------------------------------------------------------- /examples/event-read.rs: -------------------------------------------------------------------------------- 1 | // CREDIT: This module is mostly based on crossterm's `event-read` example with minor 2 | // modifications to adapt to the termina API. 3 | // 4 | use std::{ 5 | io::{self, Write as _}, 6 | time::Duration, 7 | }; 8 | 9 | use termina::{ 10 | escape::csi::{self, KittyKeyboardFlags}, 11 | event::{KeyCode, KeyEvent}, 12 | Event, PlatformTerminal, Terminal, WindowSize, 13 | }; 14 | 15 | const HELP: &str = r#"Blocking read() 16 | - Keyboard, mouse, focus and terminal resize events enabled 17 | - Hit "c" to print current cursor position 18 | - Use Esc to quit 19 | "#; 20 | 21 | macro_rules! decset { 22 | ($mode:ident) => { 23 | csi::Csi::Mode(csi::Mode::SetDecPrivateMode(csi::DecPrivateMode::Code( 24 | csi::DecPrivateModeCode::$mode, 25 | ))) 26 | }; 27 | } 28 | macro_rules! decreset { 29 | ($mode:ident) => { 30 | csi::Csi::Mode(csi::Mode::ResetDecPrivateMode(csi::DecPrivateMode::Code( 31 | csi::DecPrivateModeCode::$mode, 32 | ))) 33 | }; 34 | } 35 | 36 | fn main() -> io::Result<()> { 37 | println!("{HELP}"); 38 | 39 | let mut terminal = PlatformTerminal::new()?; 40 | terminal.enter_raw_mode()?; 41 | 42 | write!( 43 | terminal, 44 | "{}{}{}{}{}{}{}{}", 45 | csi::Csi::Keyboard(csi::Keyboard::PushFlags( 46 | KittyKeyboardFlags::DISAMBIGUATE_ESCAPE_CODES 47 | | KittyKeyboardFlags::REPORT_ALTERNATE_KEYS 48 | )), 49 | decset!(FocusTracking), 50 | decset!(BracketedPaste), 51 | decset!(MouseTracking), 52 | decset!(ButtonEventMouse), 53 | decset!(AnyEventMouse), 54 | decset!(RXVTMouse), 55 | decset!(SGRMouse), 56 | )?; 57 | terminal.flush()?; 58 | 59 | let mut size = terminal.get_dimensions()?; 60 | loop { 61 | let event = terminal.read(|event| !event.is_escape())?; 62 | 63 | println!("Event: {event:?}\r"); 64 | 65 | match event { 66 | Event::Key(KeyEvent { 67 | code: KeyCode::Escape, 68 | .. 69 | }) => break, 70 | Event::Key(KeyEvent { 71 | code: KeyCode::Char('c'), 72 | .. 73 | }) => { 74 | write!( 75 | terminal, 76 | "{}", 77 | csi::Csi::Cursor(csi::Cursor::RequestActivePositionReport), 78 | )?; 79 | terminal.flush()?; 80 | let filter = |event: &Event| { 81 | matches!( 82 | event, 83 | Event::Csi(csi::Csi::Cursor(csi::Cursor::ActivePositionReport { .. })) 84 | ) 85 | }; 86 | if terminal.poll(filter, Some(Duration::from_millis(50)))? { 87 | let Event::Csi(csi::Csi::Cursor(csi::Cursor::ActivePositionReport { 88 | line, 89 | col, 90 | })) = terminal.read(filter)? 91 | else { 92 | unreachable!() 93 | }; 94 | println!( 95 | "Cursor position: {:?}\r", 96 | (line.get_zero_based(), col.get_zero_based()) 97 | ); 98 | } else { 99 | eprintln!("Failed to read the cursor position within 50msec\r"); 100 | } 101 | } 102 | Event::WindowResized(dimensions) => { 103 | let new_size = flush_resize_events(&terminal, dimensions); 104 | println!("Resize from {size:?} to {new_size:?}\r"); 105 | size = new_size; 106 | } 107 | _ => (), 108 | } 109 | } 110 | 111 | write!( 112 | terminal, 113 | "{}{}{}{}{}{}{}{}", 114 | csi::Csi::Keyboard(csi::Keyboard::PopFlags(1)), 115 | decreset!(FocusTracking), 116 | decreset!(BracketedPaste), 117 | decreset!(MouseTracking), 118 | decreset!(ButtonEventMouse), 119 | decreset!(AnyEventMouse), 120 | decreset!(RXVTMouse), 121 | decreset!(SGRMouse), 122 | )?; 123 | 124 | Ok(()) 125 | } 126 | 127 | fn flush_resize_events(terminal: &PlatformTerminal, original_size: WindowSize) -> WindowSize { 128 | let mut size = original_size; 129 | let filter = |event: &Event| matches!(event, Event::WindowResized { .. }); 130 | while let Ok(true) = terminal.poll(filter, Some(Duration::from_millis(50))) { 131 | if let Ok(Event::WindowResized(dimensions)) = terminal.read(filter) { 132 | size = dimensions; 133 | } 134 | } 135 | size 136 | } 137 | -------------------------------------------------------------------------------- /examples/window-title.rs: -------------------------------------------------------------------------------- 1 | use std::io::{self, Write as _}; 2 | 3 | use termina::{ 4 | escape::{ 5 | csi::{self, Csi}, 6 | osc::Osc, 7 | }, 8 | PlatformTerminal, Terminal as _, 9 | }; 10 | 11 | fn main() -> io::Result<()> { 12 | let mut terminal = PlatformTerminal::new()?; 13 | terminal.enter_raw_mode()?; 14 | 15 | write!( 16 | terminal, 17 | "{}{}{}{}Check the window/tab title of your terminal. Press any key to exit. ", 18 | Csi::Mode(csi::Mode::SetDecPrivateMode(csi::DecPrivateMode::Code( 19 | csi::DecPrivateModeCode::ClearAndEnableAlternateScreen 20 | ))), 21 | // Save the current title to the terminal's stack. 22 | Csi::Window(Box::new(csi::Window::PushIconAndWindowTitle)), 23 | Osc::SetIconNameAndWindowTitle("Hello, world! - termina"), 24 | Csi::Cursor(csi::Cursor::Position { 25 | line: Default::default(), 26 | col: Default::default(), 27 | }), 28 | )?; 29 | terminal.flush()?; 30 | let _ = terminal.read(|event| matches!(event, termina::Event::Key(_))); 31 | 32 | write!( 33 | terminal, 34 | "{}{}", 35 | // Restore the title from the terminal's stack. 36 | Csi::Window(Box::new(csi::Window::PopIconAndWindowTitle)), 37 | Csi::Mode(csi::Mode::ResetDecPrivateMode(csi::DecPrivateMode::Code( 38 | csi::DecPrivateModeCode::ClearAndEnableAlternateScreen, 39 | ))), 40 | )?; 41 | 42 | Ok(()) 43 | } 44 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "nixpkgs": { 4 | "locked": { 5 | "lastModified": 1743315132, 6 | "narHash": "sha256-6hl6L/tRnwubHcA4pfUUtk542wn2Om+D4UnDhlDW9BE=", 7 | "owner": "nixos", 8 | "repo": "nixpkgs", 9 | "rev": "52faf482a3889b7619003c0daec593a1912fddc1", 10 | "type": "github" 11 | }, 12 | "original": { 13 | "owner": "nixos", 14 | "ref": "nixos-unstable", 15 | "repo": "nixpkgs", 16 | "type": "github" 17 | } 18 | }, 19 | "nixpkgs_2": { 20 | "locked": { 21 | "lastModified": 1736320768, 22 | "narHash": "sha256-nIYdTAiKIGnFNugbomgBJR+Xv5F1ZQU+HfaBqJKroC0=", 23 | "owner": "NixOS", 24 | "repo": "nixpkgs", 25 | "rev": "4bc9c909d9ac828a039f288cf872d16d38185db8", 26 | "type": "github" 27 | }, 28 | "original": { 29 | "owner": "NixOS", 30 | "ref": "nixpkgs-unstable", 31 | "repo": "nixpkgs", 32 | "type": "github" 33 | } 34 | }, 35 | "root": { 36 | "inputs": { 37 | "nixpkgs": "nixpkgs", 38 | "rust-overlay": "rust-overlay" 39 | } 40 | }, 41 | "rust-overlay": { 42 | "inputs": { 43 | "nixpkgs": "nixpkgs_2" 44 | }, 45 | "locked": { 46 | "lastModified": 1743388531, 47 | "narHash": "sha256-OBcNE+2/TD1AMgq8HKMotSQF8ZPJEFGZdRoBJ7t/HIc=", 48 | "owner": "oxalica", 49 | "repo": "rust-overlay", 50 | "rev": "011de3c895927300651d9c2cb8e062adf17aa665", 51 | "type": "github" 52 | }, 53 | "original": { 54 | "owner": "oxalica", 55 | "repo": "rust-overlay", 56 | "type": "github" 57 | } 58 | } 59 | }, 60 | "root": "root", 61 | "version": 7 62 | } 63 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "A cross-platform VT manipulation library"; 3 | 4 | inputs = { 5 | nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable"; 6 | rust-overlay.url = "github:oxalica/rust-overlay"; 7 | }; 8 | 9 | outputs = 10 | { nixpkgs, rust-overlay, ... }: 11 | let 12 | inherit (nixpkgs) lib; 13 | forEachSystem = lib.genAttrs lib.systems.flakeExposed; 14 | in 15 | { 16 | devShell = forEachSystem ( 17 | system: 18 | let 19 | pkgs = import nixpkgs { 20 | inherit system; 21 | overlays = [ rust-overlay.overlays.default ]; 22 | }; 23 | toolchain = pkgs.rust-bin.stable.latest.default; 24 | in 25 | pkgs.mkShell { 26 | nativeBuildInputs = 27 | with pkgs; 28 | [ 29 | (toolchain.override { 30 | extensions = [ 31 | "rust-src" 32 | "clippy" 33 | "llvm-tools-preview" 34 | ]; 35 | }) 36 | rust-analyzer 37 | ] 38 | ++ (lib.optionals stdenv.isLinux [ 39 | cargo-llvm-cov 40 | cargo-flamegraph 41 | valgrind 42 | ]); 43 | RUST_BACKTRACE = "1"; 44 | } 45 | ); 46 | }; 47 | } 48 | -------------------------------------------------------------------------------- /shell.nix: -------------------------------------------------------------------------------- 1 | # Flake's devShell for non-flake-enabled nix instances 2 | let 3 | compat = builtins.fetchTarball { 4 | url = "https://github.com/edolstra/flake-compat/archive/b4a34015c698c7793d592d66adbab377907a2be8.tar.gz"; 5 | sha256 = "sha256:1qc703yg0babixi6wshn5wm2kgl5y1drcswgszh4xxzbrwkk9sv7"; 6 | }; 7 | in 8 | (import compat { src = ./.; }).shellNix.default 9 | -------------------------------------------------------------------------------- /src/base64.rs: -------------------------------------------------------------------------------- 1 | // CREDIT: 2 | // A minimal base64 implementation to keep from pulling in a crate for just that. It's based on 3 | // https://github.com/marshallpierce/rust-base64 but without all the customization options. 4 | // The biggest portion comes from 5 | // https://github.com/marshallpierce/rust-base64/blob/a675443d327e175f735a37f574de803d6a332591/src/engine/naive.rs#L42 6 | // Thanks, rust-base64! 7 | 8 | // CREDIT: this was yanked from the Helix codebase: 9 | // 10 | // Also see . 11 | 12 | // The MIT License (MIT) 13 | 14 | // Copyright (c) 2015 Alice Maz 15 | 16 | // Permission is hereby granted, free of charge, to any person obtaining a copy 17 | // of this software and associated documentation files (the "Software"), to deal 18 | // in the Software without restriction, including without limitation the rights 19 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 20 | // copies of the Software, and to permit persons to whom the Software is 21 | // furnished to do so, subject to the following conditions: 22 | 23 | // The above copyright notice and this permission notice shall be included in 24 | // all copies or substantial portions of the Software. 25 | 26 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 27 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 28 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 29 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 30 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 31 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 32 | // THE SOFTWARE. 33 | 34 | use core::ops::{BitAnd, BitOr, Shl, Shr}; 35 | 36 | const PAD_BYTE: u8 = b'='; 37 | const ENCODE_TABLE: &[u8] = 38 | "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/".as_bytes(); 39 | const LOW_SIX_BITS: u32 = 0x3F; 40 | 41 | pub fn encode(input: &[u8]) -> String { 42 | let rem = input.len() % 3; 43 | let complete_chunks = input.len() / 3; 44 | let remainder_chunk = usize::from(rem != 0); 45 | let encoded_size = (complete_chunks + remainder_chunk) * 4; 46 | 47 | let mut output = vec![0; encoded_size]; 48 | 49 | // complete chunks first 50 | let complete_chunk_len = input.len() - rem; 51 | 52 | let mut input_index = 0_usize; 53 | let mut output_index = 0_usize; 54 | while input_index < complete_chunk_len { 55 | let chunk = &input[input_index..input_index + 3]; 56 | 57 | // populate low 24 bits from 3 bytes 58 | let chunk_int: u32 = 59 | (chunk[0] as u32).shl(16) | (chunk[1] as u32).shl(8) | (chunk[2] as u32); 60 | // encode 4x 6-bit output bytes 61 | output[output_index] = ENCODE_TABLE[chunk_int.shr(18) as usize]; 62 | output[output_index + 1] = ENCODE_TABLE[chunk_int.shr(12_u8).bitand(LOW_SIX_BITS) as usize]; 63 | output[output_index + 2] = ENCODE_TABLE[chunk_int.shr(6_u8).bitand(LOW_SIX_BITS) as usize]; 64 | output[output_index + 3] = ENCODE_TABLE[chunk_int.bitand(LOW_SIX_BITS) as usize]; 65 | 66 | input_index += 3; 67 | output_index += 4; 68 | } 69 | 70 | // then leftovers 71 | if rem == 2 { 72 | let chunk = &input[input_index..input_index + 2]; 73 | 74 | // high six bits of chunk[0] 75 | output[output_index] = ENCODE_TABLE[chunk[0].shr(2) as usize]; 76 | // bottom 2 bits of [0], high 4 bits of [1] 77 | output[output_index + 1] = ENCODE_TABLE 78 | [(chunk[0].shl(4_u8).bitor(chunk[1].shr(4_u8)) as u32).bitand(LOW_SIX_BITS) as usize]; 79 | // bottom 4 bits of [1], with the 2 bottom bits as zero 80 | output[output_index + 2] = 81 | ENCODE_TABLE[(chunk[1].shl(2_u8) as u32).bitand(LOW_SIX_BITS) as usize]; 82 | output[output_index + 3] = PAD_BYTE; 83 | } else if rem == 1 { 84 | let byte = input[input_index]; 85 | output[output_index] = ENCODE_TABLE[byte.shr(2) as usize]; 86 | output[output_index + 1] = 87 | ENCODE_TABLE[(byte.shl(4_u8) as u32).bitand(LOW_SIX_BITS) as usize]; 88 | output[output_index + 2] = PAD_BYTE; 89 | output[output_index + 3] = PAD_BYTE; 90 | } 91 | String::from_utf8(output).expect("Invalid UTF8") 92 | } 93 | 94 | #[cfg(test)] 95 | mod tests { 96 | fn compare_encode(expected: &str, target: &[u8]) { 97 | assert_eq!(expected, super::encode(target)); 98 | } 99 | 100 | #[test] 101 | fn encode_rfc4648_0() { 102 | compare_encode("", b""); 103 | } 104 | 105 | #[test] 106 | fn encode_rfc4648_1() { 107 | compare_encode("Zg==", b"f"); 108 | } 109 | 110 | #[test] 111 | fn encode_rfc4648_2() { 112 | compare_encode("Zm8=", b"fo"); 113 | } 114 | 115 | #[test] 116 | fn encode_rfc4648_3() { 117 | compare_encode("Zm9v", b"foo"); 118 | } 119 | 120 | #[test] 121 | fn encode_rfc4648_4() { 122 | compare_encode("Zm9vYg==", b"foob"); 123 | } 124 | 125 | #[test] 126 | fn encode_rfc4648_5() { 127 | compare_encode("Zm9vYmE=", b"fooba"); 128 | } 129 | 130 | #[test] 131 | fn encode_rfc4648_6() { 132 | compare_encode("Zm9vYmFy", b"foobar"); 133 | } 134 | 135 | #[test] 136 | fn encode_all_ascii() { 137 | let mut ascii = Vec::::with_capacity(128); 138 | 139 | for i in 0..128 { 140 | ascii.push(i); 141 | } 142 | 143 | compare_encode( 144 | "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8gISIjJCUmJygpKissLS4vMDEyMzQ1Njc4OTo7P\ 145 | D0+P0BBQkNERUZHSElKS0xNTk9QUVJTVFVWV1hZWltcXV5fYGFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6e3x9fn8\ 146 | =", 147 | &ascii, 148 | ); 149 | } 150 | 151 | #[test] 152 | fn encode_all_bytes() { 153 | let mut bytes = Vec::::with_capacity(256); 154 | 155 | for i in 0..255 { 156 | bytes.push(i); 157 | } 158 | bytes.push(255); //bug with "overflowing" ranges? 159 | 160 | compare_encode( 161 | "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8gISIjJCUmJygpKissLS4vMDEyMzQ1Njc4OTo7P\ 162 | D0+P0BBQkNERUZHSElKS0xNTk9QUVJTVFVWV1hZWltcXV5fYGFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6e3x9fn\ 163 | +AgYKDhIWGh4iJiouMjY6PkJGSk5SVlpeYmZqbnJ2en6ChoqOkpaanqKmqq6ytrq+wsbKztLW2t7i5uru8vb6\ 164 | /wMHCw8TFxsfIycrLzM3Oz9DR0tPU1dbX2Nna29zd3t/g4eLj5OXm5+jp6uvs7e7v8PHy8/T19vf4+fr7/P3+/w==", 165 | &bytes, 166 | ); 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /src/escape.rs: -------------------------------------------------------------------------------- 1 | //! ANSI escape sequences. 2 | 3 | // CREDIT: this tree of modules is mostly yanked from the equivalents in TermWiz with some 4 | // stylistic edits and additions/subtractions of some escape sequences. 5 | 6 | pub mod csi; 7 | pub mod dcs; 8 | pub mod osc; 9 | 10 | // Originally yanked from 11 | pub const CSI: &str = "\x1b["; 12 | pub const OSC: &str = "\x1b]"; 13 | pub const ST: &str = "\x1b\\"; 14 | pub const SS3: &str = "\x1bO"; 15 | pub const DCS: &str = "\x1bP"; 16 | -------------------------------------------------------------------------------- /src/escape/dcs.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::{self, Display}; 2 | 3 | use crate::style::CursorStyle; 4 | 5 | #[derive(Debug, Clone, PartialEq, Eq)] 6 | pub enum Dcs { 7 | // DECRQSS: 8 | Request(DcsRequest), 9 | // DECRPSS: 10 | Response { 11 | is_request_valid: bool, 12 | value: DcsResponse, 13 | }, 14 | } 15 | 16 | impl Display for Dcs { 17 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 18 | // DCS 19 | f.write_str(super::DCS)?; 20 | match self { 21 | // DCS $ q D...D ST 22 | Self::Request(request) => write!(f, "$q{request}")?, 23 | // DCS Ps $ r D...D ST 24 | Self::Response { 25 | is_request_valid, 26 | value, 27 | } => write!(f, "{}$r{value}", if *is_request_valid { 1 } else { 0 })?, 28 | } 29 | // ST 30 | f.write_str(super::ST) 31 | } 32 | } 33 | 34 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 35 | pub enum DcsRequest { 36 | ActiveStatusDisplay, 37 | AttributeChangeExtent, 38 | CharacterAttribute, 39 | ConformanceLevel, 40 | ColumnsPerPage, 41 | LinesPerPage, 42 | NumberOfLinesPerScreen, 43 | StatusLineType, 44 | LeftAndRightMargins, 45 | TopAndBottomMargins, 46 | /// SGR 47 | GraphicRendition, 48 | SetUpLanguage, 49 | PrinterType, 50 | RefreshRate, 51 | DigitalPrintedDataType, 52 | ProPrinterCharacterSet, 53 | CommunicationSpeed, 54 | CommunicationPort, 55 | ScrollSpeed, 56 | CursorStyle, 57 | KeyClickVolume, 58 | WarningBellVolume, 59 | MarginBellVolume, 60 | LockKeyStyle, 61 | FlowControlType, 62 | DisconnectDelayTime, 63 | TransmitRateLimit, 64 | PortParameter, 65 | } 66 | 67 | impl Display for DcsRequest { 68 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 69 | match self { 70 | Self::ActiveStatusDisplay => f.write_str("$}"), 71 | Self::AttributeChangeExtent => write!(f, "*x"), 72 | Self::CharacterAttribute => write!(f, "\"q"), 73 | Self::ConformanceLevel => write!(f, "\"p"), 74 | Self::ColumnsPerPage => write!(f, "$|"), 75 | Self::LinesPerPage => write!(f, "t"), 76 | Self::NumberOfLinesPerScreen => write!(f, "*|"), 77 | Self::StatusLineType => write!(f, "$~"), 78 | Self::LeftAndRightMargins => write!(f, "s"), 79 | Self::TopAndBottomMargins => write!(f, "r"), 80 | Self::GraphicRendition => write!(f, "m"), 81 | Self::SetUpLanguage => write!(f, "p"), 82 | Self::PrinterType => write!(f, "$s"), 83 | Self::RefreshRate => write!(f, "\"t"), 84 | Self::DigitalPrintedDataType => write!(f, "(p"), 85 | Self::ProPrinterCharacterSet => write!(f, "*p"), 86 | Self::CommunicationSpeed => write!(f, "*r"), 87 | Self::CommunicationPort => write!(f, "*u"), 88 | // NOTE: space char is intentional - written as SP in 89 | // 90 | Self::ScrollSpeed => write!(f, " p"), 91 | Self::CursorStyle => write!(f, " q"), 92 | Self::KeyClickVolume => write!(f, " r"), 93 | Self::WarningBellVolume => write!(f, " t"), 94 | Self::MarginBellVolume => write!(f, " u"), 95 | Self::LockKeyStyle => write!(f, " v"), 96 | Self::FlowControlType => write!(f, "*s"), 97 | Self::DisconnectDelayTime => write!(f, "$q"), 98 | Self::TransmitRateLimit => write!(f, "\"u"), 99 | Self::PortParameter => write!(f, "+w"), 100 | } 101 | } 102 | } 103 | 104 | #[derive(Debug, Clone, PartialEq, Eq)] 105 | pub enum DcsResponse { 106 | /// SGR 107 | GraphicRendition(Vec), 108 | CursorStyle(CursorStyle), 109 | // There are others but adding them would mean adding a lot of parsing code... 110 | } 111 | 112 | impl Display for DcsResponse { 113 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 114 | match self { 115 | Self::GraphicRendition(sgrs) => { 116 | let mut first = true; 117 | for sgr in sgrs { 118 | if !first { 119 | write!(f, ";")?; 120 | } 121 | first = false; 122 | write!(f, "{sgr}")?; 123 | } 124 | Ok(()) 125 | } 126 | Self::CursorStyle(style) => write!(f, "{style} q"), 127 | } 128 | } 129 | } 130 | 131 | #[cfg(test)] 132 | mod test { 133 | use super::*; 134 | 135 | #[test] 136 | fn encoding() { 137 | assert_eq!( 138 | Dcs::Request(DcsRequest::GraphicRendition).to_string(), 139 | "\x1bP$qm\x1b\\" 140 | ); 141 | assert_eq!( 142 | Dcs::Request(DcsRequest::CursorStyle).to_string(), 143 | "\x1bP$q q\x1b\\" 144 | ); 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /src/escape/osc.rs: -------------------------------------------------------------------------------- 1 | // CREDIT: this is a quite shallow copy of . 2 | // I've replaced some macros and the base64 implementation however, as well as make the commands 3 | // borrow a `str` instead of own a `String`. 4 | 5 | use std::fmt::{self, Display}; 6 | 7 | use crate::base64; 8 | 9 | pub enum Osc<'a> { 10 | SetIconNameAndWindowTitle(&'a str), 11 | SetWindowTitle(&'a str), 12 | SetWindowTitleSun(&'a str), 13 | SetIconName(&'a str), 14 | SetIconNameSun(&'a str), 15 | ClearSelection(Selection), 16 | QuerySelection(Selection), 17 | SetSelection(Selection, &'a str), 18 | // TODO: I didn't copy many available commands yet... 19 | } 20 | 21 | impl Display for Osc<'_> { 22 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 23 | f.write_str(super::OSC)?; 24 | match self { 25 | Self::SetIconNameAndWindowTitle(s) => write!(f, "0;{s}")?, 26 | Self::SetWindowTitle(s) => write!(f, "2;{s}")?, 27 | Self::SetWindowTitleSun(s) => write!(f, "l{s}")?, 28 | Self::SetIconName(s) => write!(f, "1;{s}")?, 29 | Self::SetIconNameSun(s) => write!(f, "L{s}")?, 30 | Self::ClearSelection(selection) => write!(f, "52;{selection}")?, 31 | Self::QuerySelection(selection) => write!(f, "52;{selection};?")?, 32 | Self::SetSelection(selection, content) => { 33 | // TODO: it'd be nice to avoid allocating a string to base64 encode. 34 | write!(f, "52;{selection};{}", base64::encode(content.as_bytes()))? 35 | } 36 | } 37 | f.write_str(super::ST)?; 38 | Ok(()) 39 | } 40 | } 41 | 42 | bitflags::bitflags! { 43 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 44 | pub struct Selection : u16 { 45 | const NONE = 0; 46 | const CLIPBOARD = 1<<1; 47 | const PRIMARY=1<<2; 48 | const SELECT=1<<3; 49 | const CUT0=1<<4; 50 | const CUT1=1<<5; 51 | const CUT2=1<<6; 52 | const CUT3=1<<7; 53 | const CUT4=1<<8; 54 | const CUT5=1<<9; 55 | const CUT6=1<<10; 56 | const CUT7=1<<11; 57 | const CUT8=1<<12; 58 | const CUT9=1<<13; 59 | } 60 | } 61 | 62 | impl Display for Selection { 63 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 64 | if self.contains(Self::CLIPBOARD) { 65 | write!(f, "c")?; 66 | } 67 | if self.contains(Self::PRIMARY) { 68 | write!(f, "p")?; 69 | } 70 | if self.contains(Self::SELECT) { 71 | write!(f, "s")?; 72 | } 73 | if self.contains(Self::CUT0) { 74 | write!(f, "0")?; 75 | } 76 | if self.contains(Self::CUT1) { 77 | write!(f, "1")?; 78 | } 79 | if self.contains(Self::CUT2) { 80 | write!(f, "2")?; 81 | } 82 | if self.contains(Self::CUT3) { 83 | write!(f, "3")?; 84 | } 85 | if self.contains(Self::CUT4) { 86 | write!(f, "4")?; 87 | } 88 | if self.contains(Self::CUT5) { 89 | write!(f, "5")?; 90 | } 91 | if self.contains(Self::CUT6) { 92 | write!(f, "6")?; 93 | } 94 | if self.contains(Self::CUT7) { 95 | write!(f, "7")?; 96 | } 97 | if self.contains(Self::CUT8) { 98 | write!(f, "8")?; 99 | } 100 | if self.contains(Self::CUT9) { 101 | write!(f, "9")?; 102 | } 103 | Ok(()) 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/event.rs: -------------------------------------------------------------------------------- 1 | // CREDIT: Most event code is adapted from crossterm. The main difference is that I include escape 2 | // sequences like CSI and DCS in the `Event` struct and do not make a distinction between 3 | // `InternalEvent` and `Event`. Otherwise all `KeyEvent` code is nearly identical to crossterm. 4 | 5 | use crate::{ 6 | escape::{csi::Csi, dcs::Dcs}, 7 | WindowSize, 8 | }; 9 | 10 | pub(crate) mod reader; 11 | pub(crate) mod source; 12 | #[cfg(feature = "event-stream")] 13 | pub(crate) mod stream; 14 | 15 | #[derive(Debug, Clone, PartialEq, Eq)] 16 | pub enum Event { 17 | Key(KeyEvent), 18 | Mouse(MouseEvent), 19 | /// The window was resized to the given dimensions. 20 | WindowResized(WindowSize), 21 | FocusIn, 22 | FocusOut, 23 | /// A "bracketed" paste. 24 | /// 25 | /// Normally pasting into a terminal with Ctrl+v (or Super+v) enters the pasted text as if 26 | /// you had typed the keys individually. Terminals commonly support ["bracketed 27 | /// paste"](https://en.wikipedia.org/wiki/Bracketed-paste) now however, which uses an escape 28 | /// sequence to deliver the entire pasted content. 29 | Paste(String), 30 | /// A parsed escape sequence starting with CSI (control sequence introducer). 31 | Csi(Csi), 32 | Dcs(Dcs), 33 | } 34 | 35 | impl Event { 36 | #[inline] 37 | pub fn is_escape(&self) -> bool { 38 | matches!(self, Self::Csi(_) | Self::Dcs(_)) 39 | } 40 | } 41 | 42 | // CREDIT: 43 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 44 | pub struct KeyEvent { 45 | pub code: KeyCode, 46 | pub kind: KeyEventKind, 47 | pub modifiers: Modifiers, 48 | pub state: KeyEventState, 49 | } 50 | 51 | impl KeyEvent { 52 | pub const fn new(code: KeyCode, modifiers: Modifiers) -> Self { 53 | Self { 54 | code, 55 | modifiers, 56 | kind: KeyEventKind::Press, 57 | state: KeyEventState::NONE, 58 | } 59 | } 60 | } 61 | 62 | impl From for KeyEvent { 63 | fn from(code: KeyCode) -> Self { 64 | Self { 65 | code, 66 | kind: KeyEventKind::Press, 67 | modifiers: Modifiers::NONE, 68 | state: KeyEventState::NONE, 69 | } 70 | } 71 | } 72 | 73 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 74 | pub enum KeyEventKind { 75 | Press, 76 | Release, 77 | Repeat, 78 | } 79 | 80 | bitflags::bitflags! { 81 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 82 | pub struct Modifiers: u8 { 83 | const NONE = 0; 84 | const SHIFT = 1 << 1; 85 | const ALT = 1 << 2; 86 | const CONTROL = 1 << 3; 87 | const SUPER = 1 << 4; 88 | const HYPER = 1 << 5; 89 | const META = 1 << 5; 90 | } 91 | } 92 | 93 | bitflags::bitflags! { 94 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 95 | pub struct KeyEventState: u8 { 96 | const NONE = 0; 97 | const KEYPAD = 1 << 1; 98 | const CAPS_LOCK = 1 << 2; 99 | const NUM_LOCK = 1 << 3; 100 | } 101 | } 102 | 103 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 104 | pub enum KeyCode { 105 | Char(char), 106 | Enter, 107 | Backspace, 108 | Tab, 109 | Escape, 110 | Left, 111 | Right, 112 | Up, 113 | Down, 114 | Home, 115 | End, 116 | BackTab, 117 | PageUp, 118 | PageDown, 119 | Insert, 120 | Delete, 121 | KeypadBegin, 122 | CapsLock, 123 | ScrollLock, 124 | NumLock, 125 | PrintScreen, 126 | Pause, 127 | Menu, 128 | Null, 129 | /// F1-F35 "function" keys 130 | Function(u8), 131 | Modifier(ModifierKeyCode), 132 | Media(MediaKeyCode), 133 | } 134 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 135 | pub enum ModifierKeyCode { 136 | /// Left Shift key. 137 | LeftShift, 138 | /// Left Control key. (Control on macOS, Ctrl on other platforms) 139 | LeftControl, 140 | /// Left Alt key. (Option on macOS, Alt on other platforms) 141 | LeftAlt, 142 | /// Left Super key. (Command on macOS, Windows on Windows, Super on other platforms) 143 | LeftSuper, 144 | /// Left Hyper key. 145 | LeftHyper, 146 | /// Left Meta key. 147 | LeftMeta, 148 | /// Right Shift key. 149 | RightShift, 150 | /// Right Control key. (Control on macOS, Ctrl on other platforms) 151 | RightControl, 152 | /// Right Alt key. (Option on macOS, Alt on other platforms) 153 | RightAlt, 154 | /// Right Super key. (Command on macOS, Windows on Windows, Super on other platforms) 155 | RightSuper, 156 | /// Right Hyper key. 157 | RightHyper, 158 | /// Right Meta key. 159 | RightMeta, 160 | /// Iso Level3 Shift key. 161 | IsoLevel3Shift, 162 | /// Iso Level5 Shift key. 163 | IsoLevel5Shift, 164 | } 165 | 166 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 167 | pub enum MediaKeyCode { 168 | /// Play media key. 169 | Play, 170 | /// Pause media key. 171 | Pause, 172 | /// Play/Pause media key. 173 | PlayPause, 174 | /// Reverse media key. 175 | Reverse, 176 | /// Stop media key. 177 | Stop, 178 | /// Fast-forward media key. 179 | FastForward, 180 | /// Rewind media key. 181 | Rewind, 182 | /// Next-track media key. 183 | TrackNext, 184 | /// Previous-track media key. 185 | TrackPrevious, 186 | /// Record media key. 187 | Record, 188 | /// Lower-volume media key. 189 | LowerVolume, 190 | /// Raise-volume media key. 191 | RaiseVolume, 192 | /// Mute media key. 193 | MuteVolume, 194 | } 195 | 196 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 197 | pub struct MouseEvent { 198 | /// The kind of mouse event that was caused. 199 | pub kind: MouseEventKind, 200 | /// The column that the event occurred on. 201 | pub column: u16, 202 | /// The row that the event occurred on. 203 | pub row: u16, 204 | /// The key modifiers active when the event occurred. 205 | pub modifiers: Modifiers, 206 | } 207 | 208 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 209 | pub enum MouseEventKind { 210 | /// Pressed mouse button. Contains the button that was pressed. 211 | Down(MouseButton), 212 | /// Released mouse button. Contains the button that was released. 213 | Up(MouseButton), 214 | /// Moved the mouse cursor while pressing the contained mouse button. 215 | Drag(MouseButton), 216 | /// Moved the mouse cursor while not pressing a mouse button. 217 | Moved, 218 | /// Scrolled mouse wheel downwards (towards the user). 219 | ScrollDown, 220 | /// Scrolled mouse wheel upwards (away from the user). 221 | ScrollUp, 222 | /// Scrolled mouse wheel left (mostly on a laptop touchpad). 223 | ScrollLeft, 224 | /// Scrolled mouse wheel right (mostly on a laptop touchpad). 225 | ScrollRight, 226 | } 227 | 228 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 229 | pub enum MouseButton { 230 | /// Left mouse button. 231 | Left, 232 | /// Right mouse button. 233 | Right, 234 | /// Middle mouse button. 235 | Middle, 236 | } 237 | -------------------------------------------------------------------------------- /src/event/reader.rs: -------------------------------------------------------------------------------- 1 | // CREDIT: 2 | // This module provides an `Arc>` wrapper around a type which is basically the crossterm 3 | // `InternalEventReader`. This allows it to live on the Terminal and an EventStream rather than 4 | // statically. 5 | // Instead of crossterm's `Filter` trait I have opted for a `Fn(&Event) -> bool` for simplicity. 6 | 7 | use std::{collections::VecDeque, io, sync::Arc, time::Duration}; 8 | 9 | use parking_lot::Mutex; 10 | 11 | use super::{ 12 | source::{EventSource as _, PlatformEventSource, PlatformWaker, PollTimeout}, 13 | Event, 14 | }; 15 | 16 | /// A reader of events from the terminal's input handle. 17 | /// 18 | /// Note that this type wraps an `Arc` and is cheap to clone. If the `event-stream` feature is 19 | /// enabled then this value should be passed to `EventStream::new`. 20 | #[derive(Debug, Clone)] 21 | pub struct EventReader { 22 | shared: Arc>, 23 | } 24 | 25 | impl EventReader { 26 | pub(crate) fn new(source: PlatformEventSource) -> Self { 27 | let shared = Shared { 28 | events: VecDeque::with_capacity(32), 29 | source, 30 | skipped_events: Vec::with_capacity(32), 31 | }; 32 | Self { 33 | shared: Arc::new(Mutex::new(shared)), 34 | } 35 | } 36 | 37 | pub fn waker(&self) -> PlatformWaker { 38 | let reader = self.shared.lock(); 39 | reader.source.waker() 40 | } 41 | 42 | pub fn poll(&self, timeout: Option, filter: F) -> io::Result 43 | where 44 | F: FnMut(&Event) -> bool, 45 | { 46 | let (mut reader, timeout) = if let Some(timeout) = timeout { 47 | let poll_timeout = PollTimeout::new(Some(timeout)); 48 | if let Some(reader) = self.shared.try_lock_for(timeout) { 49 | (reader, poll_timeout.leftover()) 50 | } else { 51 | return Ok(false); 52 | } 53 | } else { 54 | (self.shared.lock(), None) 55 | }; 56 | reader.poll(timeout, filter) 57 | } 58 | 59 | pub fn read(&self, filter: F) -> io::Result 60 | where 61 | F: FnMut(&Event) -> bool, 62 | { 63 | let mut reader = self.shared.lock(); 64 | reader.read(filter) 65 | } 66 | } 67 | 68 | #[derive(Debug)] 69 | struct Shared { 70 | events: VecDeque, 71 | source: PlatformEventSource, 72 | skipped_events: Vec, 73 | } 74 | 75 | impl Shared { 76 | fn poll(&mut self, timeout: Option, mut filter: F) -> io::Result 77 | where 78 | F: FnMut(&Event) -> bool, 79 | { 80 | if self.events.iter().any(&mut (filter)) { 81 | return Ok(true); 82 | } 83 | 84 | let timeout = PollTimeout::new(timeout); 85 | 86 | loop { 87 | let maybe_event = match self.source.try_read(timeout.leftover()) { 88 | Ok(None) => None, 89 | Ok(Some(event)) => { 90 | if (filter)(&event) { 91 | Some(event) 92 | } else { 93 | self.skipped_events.push(event); 94 | None 95 | } 96 | } 97 | Err(err) if err.kind() == io::ErrorKind::Interrupted => return Ok(false), 98 | Err(err) => return Err(err), 99 | }; 100 | 101 | if timeout.elapsed() || maybe_event.is_some() { 102 | self.events.extend(self.skipped_events.drain(..)); 103 | 104 | if let Some(event) = maybe_event { 105 | self.events.push_front(event); 106 | return Ok(true); 107 | } 108 | 109 | return Ok(false); 110 | } 111 | } 112 | } 113 | 114 | fn read(&mut self, mut filter: F) -> io::Result 115 | where 116 | F: FnMut(&Event) -> bool, 117 | { 118 | let mut skipped_events = VecDeque::new(); 119 | 120 | loop { 121 | while let Some(event) = self.events.pop_front() { 122 | if (filter)(&event) { 123 | self.events.extend(skipped_events.drain(..)); 124 | return Ok(event); 125 | } else { 126 | skipped_events.push_back(event); 127 | } 128 | } 129 | let _ = self.poll(None, &mut filter)?; 130 | } 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /src/event/source.rs: -------------------------------------------------------------------------------- 1 | #[cfg(unix)] 2 | mod unix; 3 | #[cfg(windows)] 4 | mod windows; 5 | 6 | use std::time::{Duration, Instant}; 7 | 8 | #[cfg(unix)] 9 | pub(crate) use unix::{UnixEventSource, UnixWaker}; 10 | #[cfg(windows)] 11 | pub(crate) use windows::{WindowsEventSource, WindowsWaker}; 12 | 13 | #[cfg(unix)] 14 | pub(crate) type PlatformEventSource = UnixEventSource; 15 | #[cfg(windows)] 16 | pub(crate) type PlatformEventSource = WindowsEventSource; 17 | 18 | #[cfg(unix)] 19 | pub(crate) type PlatformWaker = UnixWaker; 20 | #[cfg(windows)] 21 | pub(crate) type PlatformWaker = WindowsWaker; 22 | 23 | // CREDIT: 24 | pub(crate) trait EventSource: Send + Sync { 25 | fn try_read(&mut self, timeout: Option) -> std::io::Result>; 26 | 27 | fn waker(&self) -> PlatformWaker; 28 | } 29 | 30 | // CREDIT: 31 | #[derive(Debug, Clone)] 32 | pub(crate) struct PollTimeout { 33 | timeout: Option, 34 | start: Instant, 35 | } 36 | 37 | impl PollTimeout { 38 | pub fn new(timeout: Option) -> Self { 39 | Self { 40 | timeout, 41 | start: Instant::now(), 42 | } 43 | } 44 | 45 | pub fn elapsed(&self) -> bool { 46 | self.timeout 47 | .map(|timeout| self.start.elapsed() >= timeout) 48 | .unwrap_or(false) 49 | } 50 | 51 | pub fn leftover(&self) -> Option { 52 | self.timeout.map(|timeout| { 53 | let elapsed = self.start.elapsed(); 54 | 55 | if elapsed >= timeout { 56 | Duration::ZERO 57 | } else { 58 | timeout - elapsed 59 | } 60 | }) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/event/source/unix.rs: -------------------------------------------------------------------------------- 1 | // CREDIT: This is mostly a mirror of crossterm `tty` event source adjusted to use rustix 2 | // exclusively, reaching into parts of the `filedescriptor` dependency (NOTE: which is part of the 3 | // WezTerm repo) but reimplementing with rustix instead of libc. 4 | // Crossterm: 5 | // Termwiz: 6 | use std::{ 7 | io::{self, Read, Write as _}, 8 | os::{ 9 | fd::{AsFd, BorrowedFd}, 10 | unix::net::UnixStream, 11 | }, 12 | sync::Arc, 13 | time::Duration, 14 | }; 15 | 16 | use parking_lot::Mutex; 17 | use rustix::termios; 18 | 19 | use crate::{parse::Parser, terminal::FileDescriptor, Event}; 20 | 21 | use super::{EventSource, PollTimeout}; 22 | 23 | #[derive(Debug)] 24 | pub struct UnixEventSource { 25 | parser: Parser, 26 | read: FileDescriptor, 27 | write: FileDescriptor, 28 | sigwinch_id: signal_hook::SigId, 29 | sigwinch_pipe: UnixStream, 30 | wake_pipe: UnixStream, 31 | wake_pipe_write: Arc>, 32 | } 33 | 34 | #[derive(Debug, Clone)] 35 | pub struct UnixWaker { 36 | inner: Arc>, 37 | } 38 | 39 | impl UnixWaker { 40 | pub fn wake(&self) -> io::Result<()> { 41 | self.inner.lock().write_all(&[0]) 42 | } 43 | } 44 | 45 | impl UnixEventSource { 46 | pub(crate) fn new(read: FileDescriptor, write: FileDescriptor) -> io::Result { 47 | let (sigwinch_pipe, sigwinch_pipe_write) = UnixStream::pair()?; 48 | let sigwinch_id = signal_hook::low_level::pipe::register( 49 | signal_hook::consts::SIGWINCH, 50 | sigwinch_pipe_write, 51 | )?; 52 | sigwinch_pipe.set_nonblocking(true)?; 53 | let (wake_pipe, wake_pipe_write) = UnixStream::pair()?; 54 | wake_pipe.set_nonblocking(true)?; 55 | wake_pipe_write.set_nonblocking(true)?; 56 | 57 | Ok(Self { 58 | parser: Default::default(), 59 | read, 60 | write, 61 | sigwinch_id, 62 | sigwinch_pipe, 63 | wake_pipe, 64 | wake_pipe_write: Arc::new(Mutex::new(wake_pipe_write)), 65 | }) 66 | } 67 | } 68 | 69 | impl Drop for UnixEventSource { 70 | fn drop(&mut self) { 71 | signal_hook::low_level::unregister(self.sigwinch_id); 72 | } 73 | } 74 | 75 | impl EventSource for UnixEventSource { 76 | fn waker(&self) -> UnixWaker { 77 | UnixWaker { 78 | inner: self.wake_pipe_write.clone(), 79 | } 80 | } 81 | 82 | fn try_read(&mut self, timeout: Option) -> io::Result> { 83 | let timeout = PollTimeout::new(timeout); 84 | 85 | while timeout.leftover().map_or(true, |t| !t.is_zero()) { 86 | if let Some(event) = self.parser.pop() { 87 | return Ok(Some(event)); 88 | } 89 | 90 | let [read_ready, sigwinch_ready, wake_ready] = match poll( 91 | [ 92 | self.read.as_fd(), 93 | self.sigwinch_pipe.as_fd(), 94 | self.wake_pipe.as_fd(), 95 | ], 96 | timeout.leftover(), 97 | ) { 98 | Ok(ready) => ready, 99 | Err(err) if err.kind() == io::ErrorKind::Interrupted => continue, 100 | Err(err) => return Err(err), 101 | }; 102 | 103 | // The input/read pipe has data. 104 | if read_ready { 105 | let mut buffer = [0u8; 64]; 106 | let read_count = read_complete(&mut self.read, &mut buffer)?; 107 | if read_count > 0 { 108 | self.parser 109 | .parse(&buffer[..read_count], read_count == buffer.len()); 110 | } 111 | if let Some(event) = self.parser.pop() { 112 | return Ok(Some(event)); 113 | } 114 | if read_count == 0 { 115 | break; 116 | } 117 | } 118 | 119 | // SIGWINCH received. 120 | if sigwinch_ready { 121 | // Drain the pipe. 122 | while read_complete(&self.sigwinch_pipe, &mut [0; 1024])? != 0 {} 123 | 124 | let winsize = termios::tcgetwinsize(&self.write)?; 125 | let event = Event::WindowResized(winsize.into()); 126 | return Ok(Some(event)); 127 | } 128 | 129 | // Waker has awoken. 130 | if wake_ready { 131 | // Drain the pipe. 132 | while read_complete(&self.wake_pipe, &mut [0; 1024])? != 0 {} 133 | 134 | return Err(io::Error::new( 135 | io::ErrorKind::Interrupted, 136 | "Poll operation was woken up", 137 | )); 138 | } 139 | } 140 | 141 | Ok(None) 142 | } 143 | } 144 | 145 | fn read_complete(mut file: F, buf: &mut [u8]) -> io::Result { 146 | loop { 147 | match file.read(buf) { 148 | Ok(read) => return Ok(read), 149 | Err(err) => match err.kind() { 150 | io::ErrorKind::WouldBlock => return Ok(0), 151 | io::ErrorKind::Interrupted => continue, 152 | _ => return Err(err), 153 | }, 154 | } 155 | } 156 | } 157 | 158 | /// A small abstraction over platform specific polling behavior. 159 | /// 160 | /// macOS `poll(2)` doesn't work on file descriptors to `/dev/tty` so we need to use `select(2)` 161 | /// instead. This provides a function which abstracts over the parts of `poll(2)` and 162 | /// `select(2)` we want. Specifically we are looking for `POLLIN` events from `poll(2)` and we 163 | /// consider that to be "ready." 164 | /// 165 | /// This module is not meant to be generic. We consider `POLLIN` to be "ready" and do not look at 166 | /// other poll flags. For the sake of simplicity we also only allow polling exactly three FDs at 167 | /// a time - the exact amount we need for the event source. 168 | fn poll(fds: [BorrowedFd<'_>; 3], timeout: Option) -> std::io::Result<[bool; 3]> { 169 | use rustix::event::Timespec; 170 | 171 | #[cfg_attr(target_os = "macos", allow(dead_code))] 172 | fn poll2(fds: [BorrowedFd<'_>; 3], timeout: Option<&Timespec>) -> io::Result<[bool; 3]> { 173 | use rustix::event::{PollFd, PollFlags}; 174 | let mut fds = [ 175 | PollFd::new(&fds[0], PollFlags::IN), 176 | PollFd::new(&fds[1], PollFlags::IN), 177 | PollFd::new(&fds[2], PollFlags::IN), 178 | ]; 179 | 180 | rustix::event::poll(&mut fds, timeout)?; 181 | 182 | Ok([ 183 | fds[0].revents().contains(PollFlags::IN), 184 | fds[1].revents().contains(PollFlags::IN), 185 | fds[2].revents().contains(PollFlags::IN), 186 | ]) 187 | } 188 | 189 | #[cfg_attr(not(target_os = "macos"), allow(dead_code))] 190 | fn select2(fds: [BorrowedFd<'_>; 3], timeout: Option<&Timespec>) -> io::Result<[bool; 3]> { 191 | use rustix::event::{fd_set_insert, fd_set_num_elements, FdSetElement, FdSetIter}; 192 | use std::os::fd::AsRawFd; 193 | 194 | let fds = [fds[0].as_raw_fd(), fds[1].as_raw_fd(), fds[2].as_raw_fd()]; 195 | // The array is non-empty so `max()` cannot return `None`. 196 | let nfds = fds.iter().copied().max().unwrap() + 1; 197 | 198 | let mut readfds = vec![FdSetElement::default(); fd_set_num_elements(fds.len(), nfds)]; 199 | for fd in fds { 200 | fd_set_insert(&mut readfds, fd); 201 | } 202 | 203 | unsafe { rustix::event::select(nfds, Some(&mut readfds), None, None, timeout) }?; 204 | 205 | let mut ready = [false; 3]; 206 | for (fd, is_ready) in fds.iter().copied().zip(ready.iter_mut()) { 207 | if FdSetIter::new(&readfds).any(|set_fd| set_fd == fd) { 208 | *is_ready = true; 209 | } 210 | } 211 | Ok(ready) 212 | } 213 | 214 | #[cfg(not(target_os = "macos"))] 215 | use poll2 as poll_impl; 216 | #[cfg(target_os = "macos")] 217 | use select2 as poll_impl; 218 | 219 | let timespec = timeout.map(|timeout| timeout.try_into().unwrap()); 220 | poll_impl(fds, timespec.as_ref()) 221 | } 222 | -------------------------------------------------------------------------------- /src/event/source/windows.rs: -------------------------------------------------------------------------------- 1 | // CREDIT: This one is shared between crossterm and termwiz but is mostly termwiz. 2 | // Termwiz: 3 | // Crossterm: 4 | // Also see the necessary methods on the handle from the terminal module and the credit comment 5 | // there. 6 | use std::{io, os::windows::prelude::*, ptr, sync::Arc, time::Duration}; 7 | 8 | use windows_sys::Win32::System::Threading; 9 | 10 | use crate::{event::Event, parse::Parser, terminal::InputHandle}; 11 | 12 | use super::{EventSource, PollTimeout}; 13 | 14 | #[derive(Debug)] 15 | pub struct WindowsEventSource { 16 | input: InputHandle, 17 | parser: Parser, 18 | waker: Arc, 19 | } 20 | 21 | impl WindowsEventSource { 22 | pub(crate) fn new(input: InputHandle) -> io::Result { 23 | Ok(Self { 24 | input, 25 | parser: Parser::default(), 26 | waker: Arc::new(EventHandle::new()?), 27 | }) 28 | } 29 | } 30 | 31 | impl EventSource for WindowsEventSource { 32 | fn waker(&self) -> WindowsWaker { 33 | WindowsWaker { 34 | handle: self.waker.clone(), 35 | } 36 | } 37 | 38 | fn try_read(&mut self, timeout: Option) -> io::Result> { 39 | use windows_sys::Win32::Foundation::{WAIT_FAILED, WAIT_OBJECT_0}; 40 | use Threading::{WaitForMultipleObjects, INFINITE}; 41 | 42 | let timeout = PollTimeout::new(timeout); 43 | 44 | while timeout.leftover().map_or(true, |t| !t.is_zero()) { 45 | if let Some(event) = self.parser.pop() { 46 | return Ok(Some(event)); 47 | } 48 | 49 | let mut pending = self.input.get_number_of_input_events()?; 50 | 51 | if pending == 0 { 52 | let mut handles = [self.input.as_raw_handle(), self.waker.as_raw_handle()]; 53 | let wait = timeout 54 | .leftover() 55 | .map(|timeout| timeout.as_millis() as u32) 56 | .unwrap_or(INFINITE); 57 | let result = unsafe { 58 | WaitForMultipleObjects(handles.len() as u32, handles.as_mut_ptr(), 0, wait) 59 | }; 60 | 61 | if result == WAIT_OBJECT_0 { 62 | pending = self.input.get_number_of_input_events()?; 63 | } else if result == WAIT_OBJECT_0 + 1 { 64 | return Err(io::Error::new( 65 | io::ErrorKind::Interrupted, 66 | "Poll operation was woken up", 67 | )); 68 | } else if result == WAIT_FAILED { 69 | return Err(io::Error::new( 70 | io::ErrorKind::Other, 71 | format!( 72 | "failed to poll input handles: {}", 73 | io::Error::last_os_error() 74 | ), 75 | )); 76 | } else { 77 | return Ok(None); 78 | } 79 | } 80 | 81 | let records = self.input.read_console_input(pending)?; 82 | 83 | self.parser.decode_input_records(&records); 84 | } 85 | 86 | Ok(None) 87 | } 88 | } 89 | 90 | #[derive(Debug)] 91 | struct EventHandle { 92 | handle: OwnedHandle, 93 | } 94 | 95 | impl EventHandle { 96 | fn new() -> io::Result { 97 | let handle = unsafe { Threading::CreateEventW(ptr::null(), 0, 0, ptr::null()) }; 98 | if handle.is_null() { 99 | Err(io::Error::last_os_error()) 100 | } else { 101 | let handle = unsafe { OwnedHandle::from_raw_handle(handle) }; 102 | Ok(Self { handle }) 103 | } 104 | } 105 | } 106 | 107 | impl AsRawHandle for EventHandle { 108 | fn as_raw_handle(&self) -> RawHandle { 109 | self.handle.as_raw_handle() 110 | } 111 | } 112 | 113 | #[derive(Debug)] 114 | pub struct WindowsWaker { 115 | handle: Arc, 116 | } 117 | 118 | impl WindowsWaker { 119 | pub fn wake(&self) -> io::Result<()> { 120 | if unsafe { Threading::SetEvent(self.handle.as_raw_handle()) } == 0 { 121 | Err(io::Error::last_os_error()) 122 | } else { 123 | Ok(()) 124 | } 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /src/event/stream.rs: -------------------------------------------------------------------------------- 1 | // CREDIT: This is basically all crossterm. 2 | // 3 | // I added the dummy stream for integration testing in Helix. 4 | 5 | use std::{ 6 | io, 7 | pin::Pin, 8 | sync::{ 9 | atomic::{AtomicBool, Ordering}, 10 | mpsc::{self, SyncSender}, 11 | Arc, 12 | }, 13 | task::{Context, Poll}, 14 | thread, 15 | time::Duration, 16 | }; 17 | 18 | use futures_core::Stream; 19 | 20 | use super::{reader::EventReader, source::PlatformWaker, Event}; 21 | 22 | /// A stream of `termina::Event`s received from the terminal. 23 | /// 24 | /// This type is only available if the `event-stream` feature is enabled. 25 | /// 26 | /// Create an event stream for a terminal by passing the reader [crate::Terminal::event_reader] 27 | /// into [EventStream::new] with a filter. 28 | pub struct EventStream { 29 | waker: PlatformWaker, 30 | filter: Arc bool>, 31 | reader: EventReader, 32 | stream_wake_task_executed: Arc, 33 | stream_wake_task_should_shutdown: Arc, 34 | task_sender: SyncSender, 35 | } 36 | 37 | #[derive(Debug)] 38 | struct Task { 39 | stream_waker: std::task::Waker, 40 | stream_wake_task_executed: Arc, 41 | stream_wake_task_should_shutdown: Arc, 42 | } 43 | 44 | impl EventStream { 45 | pub fn new(reader: EventReader, filter: F) -> Self 46 | where 47 | F: Fn(&Event) -> bool + Send + Sync + 'static, 48 | { 49 | let filter = Arc::new(filter); 50 | let waker = reader.waker(); 51 | 52 | let (task_sender, receiver) = mpsc::sync_channel::(1); 53 | 54 | let task_reader = reader.clone(); 55 | let task_filter = filter.clone(); 56 | thread::spawn(move || { 57 | while let Ok(task) = receiver.recv() { 58 | loop { 59 | if let Ok(true) = task_reader.poll(None, &*task_filter) { 60 | break; 61 | } 62 | if task.stream_wake_task_should_shutdown.load(Ordering::SeqCst) { 63 | break; 64 | } 65 | } 66 | task.stream_wake_task_executed 67 | .store(false, Ordering::SeqCst); 68 | task.stream_waker.wake(); 69 | } 70 | }); 71 | 72 | Self { 73 | waker, 74 | filter, 75 | reader, 76 | stream_wake_task_executed: Default::default(), 77 | stream_wake_task_should_shutdown: Default::default(), 78 | task_sender, 79 | } 80 | } 81 | } 82 | 83 | impl Drop for EventStream { 84 | fn drop(&mut self) { 85 | self.stream_wake_task_should_shutdown 86 | .store(true, Ordering::SeqCst); 87 | let _ = self.waker.wake(); 88 | } 89 | } 90 | 91 | impl Stream for EventStream { 92 | type Item = io::Result; 93 | 94 | fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { 95 | match self 96 | .reader 97 | .poll(Some(Duration::from_secs(0)), &*self.filter) 98 | { 99 | Ok(true) => match self.reader.read(&*self.filter) { 100 | Ok(event) => Poll::Ready(Some(Ok(event))), 101 | Err(err) => Poll::Ready(Some(Err(err))), 102 | }, 103 | Ok(false) => { 104 | if !self 105 | .stream_wake_task_executed 106 | .compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst) 107 | .unwrap_or_else(|x| x) 108 | { 109 | self.stream_wake_task_should_shutdown 110 | .store(false, Ordering::SeqCst); 111 | let _ = self.task_sender.send(Task { 112 | stream_waker: cx.waker().clone(), 113 | stream_wake_task_executed: self.stream_wake_task_executed.clone(), 114 | stream_wake_task_should_shutdown: self 115 | .stream_wake_task_should_shutdown 116 | .clone(), 117 | }); 118 | } 119 | Poll::Pending 120 | } 121 | Err(err) => Poll::Ready(Some(Err(err))), 122 | } 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | pub(crate) mod base64; 2 | pub mod escape; 3 | pub mod event; 4 | pub(crate) mod parse; 5 | pub mod style; 6 | mod terminal; 7 | 8 | use std::{fmt, num::NonZeroU16}; 9 | 10 | pub use event::{reader::EventReader, Event}; 11 | pub use terminal::{PlatformHandle, PlatformTerminal, Terminal}; 12 | 13 | #[cfg(feature = "event-stream")] 14 | pub use event::stream::EventStream; 15 | 16 | /// A helper type which avoids tripping over Unix terminal's one-indexed conventions. 17 | /// 18 | /// Coordinates and terminal dimensions are one-based in Termina on both Unix and Windows. 19 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 20 | // CREDIT: . 21 | // This can be seen as a reimplementation on top of NonZeroU16. 22 | pub struct OneBased(NonZeroU16); 23 | 24 | impl OneBased { 25 | pub const fn new(n: u16) -> Option { 26 | match NonZeroU16::new(n) { 27 | Some(n) => Some(Self(n)), 28 | None => None, 29 | } 30 | } 31 | 32 | pub const fn from_zero_based(n: u16) -> Self { 33 | Self(unsafe { NonZeroU16::new_unchecked(n + 1) }) 34 | } 35 | 36 | pub const fn get(self) -> u16 { 37 | self.0.get() 38 | } 39 | 40 | pub const fn get_zero_based(self) -> u16 { 41 | self.get() - 1 42 | } 43 | } 44 | 45 | impl Default for OneBased { 46 | fn default() -> Self { 47 | Self(unsafe { NonZeroU16::new_unchecked(1) }) 48 | } 49 | } 50 | 51 | impl fmt::Display for OneBased { 52 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 53 | self.0.fmt(f) 54 | } 55 | } 56 | 57 | impl From for OneBased { 58 | fn from(n: NonZeroU16) -> Self { 59 | Self(n) 60 | } 61 | } 62 | 63 | /// The dimensions of a terminal screen. 64 | /// 65 | /// For both Unix and Windows, Termina returns the rows and columns. 66 | /// Pixel width and height are not supported on Windows. 67 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 68 | pub struct WindowSize { 69 | /// The width - the number of columns. 70 | #[doc(alias = "width")] 71 | pub cols: u16, 72 | /// The height - the number of rows. 73 | #[doc(alias = "height")] 74 | pub rows: u16, 75 | /// The height of the window in pixels. 76 | pub pixel_width: Option, 77 | /// The width of the window in pixels. 78 | pub pixel_height: Option, 79 | } 80 | -------------------------------------------------------------------------------- /src/parse.rs: -------------------------------------------------------------------------------- 1 | // CREDIT: This is nearly all crossterm (with modifications and additions). 2 | // 3 | // See a below credit comment about the `decode_input_records` function however. 4 | // I have extended the parsing functions from 5 | // 6 | // Crossterm comments say that the parser is a bit scary and probably in need of a refactor. I 7 | // like this approach though since it's quite easy to read and test. I'm unsure of the performance 8 | // though because of the loop in `process_bytes`: we consider the bytes as an increasing slice of 9 | // the buffer until it becomes valid or invalid. WezTerm and Alacritty have more formal parsers 10 | // (`vtparse` and `vte`, respectively) but I'm unsure of using a terminal program's parser since 11 | // it may be larger or more complex than an application needs. 12 | use std::{collections::VecDeque, num::NonZeroU16, str}; 13 | 14 | use crate::{ 15 | escape::{ 16 | self, 17 | csi::{self, Csi, KittyKeyboardFlags, ThemeMode}, 18 | dcs, 19 | }, 20 | event::{ 21 | KeyCode, KeyEvent, KeyEventKind, KeyEventState, MediaKeyCode, ModifierKeyCode, Modifiers, 22 | MouseButton, MouseEvent, MouseEventKind, 23 | }, 24 | style, Event, 25 | }; 26 | 27 | #[derive(Debug)] 28 | pub(crate) struct Parser { 29 | buffer: Vec, 30 | /// Events which have been parsed. Pop out with `Self::pop`. 31 | events: VecDeque, 32 | } 33 | 34 | impl Default for Parser { 35 | fn default() -> Self { 36 | Self { 37 | buffer: Vec::with_capacity(256), 38 | events: VecDeque::with_capacity(32), 39 | } 40 | } 41 | } 42 | 43 | impl Parser { 44 | /// Reads and removes a parsed event from the parser. 45 | pub fn pop(&mut self) -> Option { 46 | self.events.pop_front() 47 | } 48 | 49 | // NOTE: Windows is handled in the `windows` module below. 50 | #[cfg(unix)] 51 | pub fn parse(&mut self, bytes: &[u8], maybe_more: bool) { 52 | self.buffer.extend_from_slice(bytes); 53 | self.process_bytes(maybe_more); 54 | } 55 | 56 | fn process_bytes(&mut self, maybe_more: bool) { 57 | let mut start = 0; 58 | for n in 0..self.buffer.len() { 59 | let end = n + 1; 60 | match parse_event( 61 | &self.buffer[start..end], 62 | maybe_more || end < self.buffer.len(), 63 | ) { 64 | Ok(Some(event)) => { 65 | self.events.push_back(event); 66 | start = end; 67 | } 68 | Ok(None) => continue, 69 | Err(_) => start = end, 70 | } 71 | } 72 | self.advance(start); 73 | } 74 | 75 | fn advance(&mut self, len: usize) { 76 | if len == 0 { 77 | return; 78 | } 79 | let remain = self.buffer.len() - len; 80 | self.buffer.rotate_left(len); 81 | self.buffer.truncate(remain); 82 | } 83 | } 84 | 85 | // CREDIT: 86 | // I have dropped the legacy Console API handling however and switched to the `AsciiChar` part of 87 | // the key record. I suspect that Termwiz may be incorrect here as the Microsoft docs say that the 88 | // proper way to read UTF-8 is to use the `A` variant (`ReadConsoleInputA` while WezTerm uses 89 | // `ReadConsoleInputW`) to read a byte. 90 | #[cfg(windows)] 91 | mod windows { 92 | use windows_sys::Win32::System::Console; 93 | 94 | use crate::{OneBased, WindowSize}; 95 | 96 | use super::*; 97 | 98 | impl Parser { 99 | pub fn decode_input_records(&mut self, records: &[Console::INPUT_RECORD]) { 100 | for record in records { 101 | match record.EventType as u32 { 102 | Console::KEY_EVENT => { 103 | let record = unsafe { record.Event.KeyEvent }; 104 | // This skips 'down's. IIRC Termwiz skips 'down's and Crossterm skips 105 | // 'up's. If we skip 'up's we don't seem to get key events at all. 106 | if record.bKeyDown == 0 { 107 | continue; 108 | } 109 | let byte = unsafe { record.uChar.AsciiChar } as u8; 110 | // The zero byte is sent when the input record is not VT. 111 | if byte == 0 { 112 | continue; 113 | } 114 | // `read_console_input` uses `ReadConsoleInputA` so we should treat the 115 | // key code as a byte and add it to the buffer. 116 | self.buffer.push(byte); 117 | } 118 | Console::WINDOW_BUFFER_SIZE_EVENT => { 119 | // NOTE: the `WINDOW_BUFFER_SIZE_EVENT` coordinates are one-based, even 120 | // though `GetConsoleScreenBufferInfo` is zero-based. 121 | let record = unsafe { record.Event.WindowBufferSizeEvent }; 122 | let Some(rows) = OneBased::new(record.dwSize.Y as u16) else { 123 | continue; 124 | }; 125 | let Some(cols) = OneBased::new(record.dwSize.X as u16) else { 126 | continue; 127 | }; 128 | self.events.push_back(Event::WindowResized(WindowSize { 129 | rows: rows.get(), 130 | cols: cols.get(), 131 | pixel_width: None, 132 | pixel_height: None, 133 | })); 134 | } 135 | _ => (), 136 | } 137 | } 138 | self.process_bytes(false); 139 | } 140 | } 141 | } 142 | 143 | #[derive(Debug)] 144 | struct MalformedSequenceError; 145 | 146 | // This is a bit hacky but cuts down on boilerplate conversions 147 | impl From for MalformedSequenceError { 148 | fn from(_: str::Utf8Error) -> Self { 149 | Self 150 | } 151 | } 152 | 153 | type Result = std::result::Result; 154 | 155 | macro_rules! bail { 156 | () => { 157 | return Err(MalformedSequenceError) 158 | }; 159 | } 160 | 161 | fn parse_event(buffer: &[u8], maybe_more: bool) -> Result> { 162 | if buffer.is_empty() { 163 | return Ok(None); 164 | } 165 | 166 | match buffer[0] { 167 | b'\x1B' => { 168 | if buffer.len() == 1 { 169 | if maybe_more { 170 | // Possible Esc sequence 171 | Ok(None) 172 | } else { 173 | Ok(Some(Event::Key(KeyCode::Escape.into()))) 174 | } 175 | } else { 176 | match buffer[1] { 177 | b'O' => { 178 | if buffer.len() == 2 { 179 | Ok(None) 180 | } else { 181 | match buffer[2] { 182 | b'D' => Ok(Some(Event::Key(KeyCode::Left.into()))), 183 | b'C' => Ok(Some(Event::Key(KeyCode::Right.into()))), 184 | b'A' => Ok(Some(Event::Key(KeyCode::Up.into()))), 185 | b'B' => Ok(Some(Event::Key(KeyCode::Down.into()))), 186 | b'H' => Ok(Some(Event::Key(KeyCode::Home.into()))), 187 | b'F' => Ok(Some(Event::Key(KeyCode::End.into()))), 188 | // F1-F4 189 | val @ b'P'..=b'S' => { 190 | Ok(Some(Event::Key(KeyCode::Function(1 + val - b'P').into()))) 191 | } 192 | _ => bail!(), 193 | } 194 | } 195 | } 196 | b'[' => parse_csi(buffer), 197 | b'P' => parse_dcs(buffer), 198 | b'\x1B' => Ok(Some(Event::Key(KeyCode::Escape.into()))), 199 | _ => parse_event(&buffer[1..], maybe_more).map(|event_option| { 200 | event_option.map(|event| { 201 | if let Event::Key(key_event) = event { 202 | let mut alt_key_event = key_event; 203 | alt_key_event.modifiers |= Modifiers::ALT; 204 | Event::Key(alt_key_event) 205 | } else { 206 | event 207 | } 208 | }) 209 | }), 210 | } 211 | } 212 | } 213 | b'\r' => Ok(Some(Event::Key(KeyCode::Enter.into()))), 214 | b'\t' => Ok(Some(Event::Key(KeyCode::Tab.into()))), 215 | b'\x7F' => Ok(Some(Event::Key(KeyCode::Backspace.into()))), 216 | b'\0' => Ok(Some(Event::Key(KeyEvent::new( 217 | KeyCode::Char(' '), 218 | Modifiers::CONTROL, 219 | )))), 220 | c @ b'\x01'..=b'\x1A' => Ok(Some(Event::Key(KeyEvent::new( 221 | KeyCode::Char((c - 0x1 + b'a') as char), 222 | Modifiers::CONTROL, 223 | )))), 224 | c @ b'\x1C'..=b'\x1F' => Ok(Some(Event::Key(KeyEvent::new( 225 | KeyCode::Char((c - 0x1C + b'4') as char), 226 | Modifiers::CONTROL, 227 | )))), 228 | _ => parse_utf8_char(buffer).map(|maybe_char| { 229 | maybe_char.map(|ch| { 230 | let modifiers = if ch.is_uppercase() { 231 | Modifiers::SHIFT 232 | } else { 233 | Modifiers::NONE 234 | }; 235 | Event::Key(KeyEvent::new(KeyCode::Char(ch), modifiers)) 236 | }) 237 | }), 238 | } 239 | } 240 | 241 | fn parse_utf8_char(buffer: &[u8]) -> Result> { 242 | assert!(!buffer.is_empty()); 243 | match str::from_utf8(buffer) { 244 | Ok(s) => Ok(Some(s.chars().next().unwrap())), 245 | Err(_) => { 246 | // `from_utf8` failed but it could be because we don't have enough bytes to make a 247 | // valid UTF-8 codepoint. Check the validity of the bytes so far: 248 | let required_bytes = match buffer[0] { 249 | // https://en.wikipedia.org/wiki/UTF-8#Description 250 | (0x00..=0x7F) => 1, // 0xxxxxxx 251 | (0xC0..=0xDF) => 2, // 110xxxxx 10xxxxxx 252 | (0xE0..=0xEF) => 3, // 1110xxxx 10xxxxxx 10xxxxxx 253 | (0xF0..=0xF7) => 4, // 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx 254 | (0x80..=0xBF) | (0xF8..=0xFF) => bail!(), 255 | }; 256 | if required_bytes > 1 && buffer.len() > 1 { 257 | for byte in &buffer[1..] { 258 | if byte & !0b0011_1111 != 0b1000_0000 { 259 | bail!() 260 | } 261 | } 262 | } 263 | if buffer.len() < required_bytes { 264 | Ok(None) 265 | } else { 266 | bail!() 267 | } 268 | } 269 | } 270 | } 271 | 272 | fn parse_csi(buffer: &[u8]) -> Result> { 273 | assert!(buffer.starts_with(b"\x1B[")); 274 | if buffer.len() == 2 { 275 | return Ok(None); 276 | } 277 | let maybe_event = match buffer[2] { 278 | b'[' => match buffer.get(3) { 279 | None => None, 280 | Some(b @ b'A'..=b'E') => Some(Event::Key(KeyCode::Function(1 + b - b'A').into())), 281 | Some(_) => bail!(), 282 | }, 283 | b'D' => Some(Event::Key(KeyCode::Left.into())), 284 | b'C' => Some(Event::Key(KeyCode::Right.into())), 285 | b'A' => Some(Event::Key(KeyCode::Up.into())), 286 | b'B' => Some(Event::Key(KeyCode::Down.into())), 287 | b'H' => Some(Event::Key(KeyCode::Home.into())), 288 | b'F' => Some(Event::Key(KeyCode::End.into())), 289 | b'Z' => Some(Event::Key(KeyEvent { 290 | code: KeyCode::BackTab, 291 | modifiers: Modifiers::SHIFT, 292 | kind: KeyEventKind::Press, 293 | state: KeyEventState::NONE, 294 | })), 295 | b'M' => return parse_csi_normal_mouse(buffer), 296 | b'<' => return parse_csi_sgr_mouse(buffer), 297 | b'I' => Some(Event::FocusIn), 298 | b'O' => Some(Event::FocusOut), 299 | b';' => return parse_csi_modifier_key_code(buffer), 300 | // P, Q, and S for compatibility with Kitty keyboard protocol, 301 | // as the 1 in 'CSI 1 P' etc. must be omitted if there are no 302 | // modifiers pressed: 303 | // https://sw.kovidgoyal.net/kitty/keyboard-protocol/#legacy-functional-keys 304 | b'P' => Some(Event::Key(KeyCode::Function(1).into())), 305 | b'Q' => Some(Event::Key(KeyCode::Function(2).into())), 306 | b'S' => Some(Event::Key(KeyCode::Function(4).into())), 307 | b'?' => match buffer[buffer.len() - 1] { 308 | b'u' => return parse_csi_keyboard_enhancement_flags(buffer), 309 | b'c' => return parse_csi_primary_device_attributes(buffer), 310 | b'n' => return parse_csi_theme_mode(buffer), 311 | b'y' => return parse_csi_synchronized_output_mode(buffer), 312 | _ => None, 313 | }, 314 | b'0'..=b'9' => { 315 | // Numbered escape code. 316 | if buffer.len() == 3 { 317 | None 318 | } else { 319 | // The final byte of a CSI sequence can be in the range 64-126, so 320 | // let's keep reading anything else. 321 | let last_byte = buffer[buffer.len() - 1]; 322 | if !(64..=126).contains(&last_byte) { 323 | None 324 | } else { 325 | if buffer.starts_with(b"\x1B[200~") { 326 | return parse_csi_bracketed_paste(buffer); 327 | } 328 | match last_byte { 329 | b'M' => return parse_csi_rxvt_mouse(buffer), 330 | b'~' => return parse_csi_special_key_code(buffer), 331 | b'u' => return parse_csi_u_encoded_key_code(buffer), 332 | b'R' => return parse_csi_cursor_position(buffer), 333 | _ => return parse_csi_modifier_key_code(buffer), 334 | } 335 | } 336 | } 337 | } 338 | _ => bail!(), 339 | }; 340 | Ok(maybe_event) 341 | } 342 | 343 | fn next_parsed(iter: &mut dyn Iterator) -> Result 344 | where 345 | T: str::FromStr, 346 | { 347 | iter.next() 348 | .ok_or(MalformedSequenceError)? 349 | .parse::() 350 | .map_err(|_| MalformedSequenceError) 351 | } 352 | 353 | fn modifier_and_kind_parsed(iter: &mut dyn Iterator) -> Result<(u8, u8)> { 354 | let mut sub_split = iter.next().ok_or(MalformedSequenceError)?.split(':'); 355 | 356 | let modifier_mask = next_parsed::(&mut sub_split)?; 357 | 358 | if let Ok(kind_code) = next_parsed::(&mut sub_split) { 359 | Ok((modifier_mask, kind_code)) 360 | } else { 361 | Ok((modifier_mask, 1)) 362 | } 363 | } 364 | 365 | fn parse_csi_u_encoded_key_code(buffer: &[u8]) -> Result> { 366 | assert!(buffer.starts_with(b"\x1B")); // CSI 367 | assert!(buffer.ends_with(b"u")); 368 | 369 | // This function parses `CSI … u` sequences. These are sequences defined in either 370 | // the `CSI u` (a.k.a. "Fix Keyboard Input on Terminals - Please", https://www.leonerd.org.uk/hacks/fixterms/) 371 | // or Kitty Keyboard Protocol (https://sw.kovidgoyal.net/kitty/keyboard-protocol/) specifications. 372 | // This CSI sequence is a tuple of semicolon-separated numbers. 373 | let s = str::from_utf8(&buffer[2..buffer.len() - 1])?; 374 | let mut split = s.split(';'); 375 | 376 | // In `CSI u`, this is parsed as: 377 | // 378 | // CSI codepoint ; modifiers u 379 | // codepoint: ASCII Dec value 380 | // 381 | // The Kitty Keyboard Protocol extends this with optional components that can be 382 | // enabled progressively. The full sequence is parsed as: 383 | // 384 | // CSI unicode-key-code:alternate-key-codes ; modifiers:event-type ; text-as-codepoints u 385 | let mut codepoints = split.next().ok_or(MalformedSequenceError)?.split(':'); 386 | 387 | let codepoint = codepoints 388 | .next() 389 | .ok_or(MalformedSequenceError)? 390 | .parse::() 391 | .map_err(|_| MalformedSequenceError)?; 392 | 393 | let (mut modifiers, kind, state_from_modifiers) = 394 | if let Ok((modifier_mask, kind_code)) = modifier_and_kind_parsed(&mut split) { 395 | ( 396 | parse_modifiers(modifier_mask), 397 | parse_key_event_kind(kind_code), 398 | parse_modifiers_to_state(modifier_mask), 399 | ) 400 | } else { 401 | (Modifiers::NONE, KeyEventKind::Press, KeyEventState::NONE) 402 | }; 403 | 404 | let (mut code, state_from_keycode) = { 405 | if let Some((special_key_code, state)) = translate_functional_key_code(codepoint) { 406 | (special_key_code, state) 407 | } else if let Some(c) = char::from_u32(codepoint) { 408 | ( 409 | match c { 410 | '\x1B' => KeyCode::Escape, 411 | '\r' => KeyCode::Enter, 412 | /* 413 | // Issue #371: \n = 0xA, which is also the keycode for Ctrl+J. The only reason we get 414 | // newlines as input is because the terminal converts \r into \n for us. When we 415 | // enter raw mode, we disable that, so \n no longer has any meaning - it's better to 416 | // use Ctrl+J. Waiting to handle it here means it gets picked up later 417 | '\n' if !crate::terminal::sys::is_raw_mode_enabled() => KeyCode::Enter, 418 | */ 419 | '\t' => { 420 | if modifiers.contains(Modifiers::SHIFT) { 421 | KeyCode::BackTab 422 | } else { 423 | KeyCode::Tab 424 | } 425 | } 426 | '\x7F' => KeyCode::Backspace, 427 | _ => KeyCode::Char(c), 428 | }, 429 | KeyEventState::empty(), 430 | ) 431 | } else { 432 | bail!(); 433 | } 434 | }; 435 | 436 | if let KeyCode::Modifier(modifier_keycode) = code { 437 | match modifier_keycode { 438 | ModifierKeyCode::LeftAlt | ModifierKeyCode::RightAlt => { 439 | modifiers.set(Modifiers::ALT, true) 440 | } 441 | ModifierKeyCode::LeftControl | ModifierKeyCode::RightControl => { 442 | modifiers.set(Modifiers::CONTROL, true) 443 | } 444 | ModifierKeyCode::LeftShift | ModifierKeyCode::RightShift => { 445 | modifiers.set(Modifiers::SHIFT, true) 446 | } 447 | ModifierKeyCode::LeftSuper | ModifierKeyCode::RightSuper => { 448 | modifiers.set(Modifiers::SUPER, true) 449 | } 450 | ModifierKeyCode::LeftHyper | ModifierKeyCode::RightHyper => { 451 | modifiers.set(Modifiers::HYPER, true) 452 | } 453 | ModifierKeyCode::LeftMeta | ModifierKeyCode::RightMeta => { 454 | modifiers.set(Modifiers::META, true) 455 | } 456 | _ => {} 457 | } 458 | } 459 | 460 | // When the "report alternate keys" flag is enabled in the Kitty Keyboard Protocol 461 | // and the terminal sends a keyboard event containing shift, the sequence will 462 | // contain an additional codepoint separated by a ':' character which contains 463 | // the shifted character according to the keyboard layout. 464 | if modifiers.contains(Modifiers::SHIFT) { 465 | if let Some(shifted_c) = codepoints 466 | .next() 467 | .and_then(|codepoint| codepoint.parse::().ok()) 468 | .and_then(char::from_u32) 469 | { 470 | code = KeyCode::Char(shifted_c); 471 | modifiers.set(Modifiers::SHIFT, false); 472 | } 473 | } 474 | 475 | let event = Event::Key(KeyEvent { 476 | code, 477 | modifiers, 478 | kind, 479 | state: state_from_keycode | state_from_modifiers, 480 | }); 481 | 482 | Ok(Some(event)) 483 | } 484 | 485 | fn parse_modifiers(mask: u8) -> Modifiers { 486 | let modifier_mask = mask.saturating_sub(1); 487 | let mut modifiers = Modifiers::empty(); 488 | if modifier_mask & 1 != 0 { 489 | modifiers |= Modifiers::SHIFT; 490 | } 491 | if modifier_mask & 2 != 0 { 492 | modifiers |= Modifiers::ALT; 493 | } 494 | if modifier_mask & 4 != 0 { 495 | modifiers |= Modifiers::CONTROL; 496 | } 497 | if modifier_mask & 8 != 0 { 498 | modifiers |= Modifiers::SUPER; 499 | } 500 | if modifier_mask & 16 != 0 { 501 | modifiers |= Modifiers::HYPER; 502 | } 503 | if modifier_mask & 32 != 0 { 504 | modifiers |= Modifiers::META; 505 | } 506 | modifiers 507 | } 508 | 509 | fn parse_modifiers_to_state(mask: u8) -> KeyEventState { 510 | let modifier_mask = mask.saturating_sub(1); 511 | let mut state = KeyEventState::empty(); 512 | if modifier_mask & 64 != 0 { 513 | state |= KeyEventState::CAPS_LOCK; 514 | } 515 | if modifier_mask & 128 != 0 { 516 | state |= KeyEventState::NUM_LOCK; 517 | } 518 | state 519 | } 520 | 521 | fn parse_key_event_kind(kind: u8) -> KeyEventKind { 522 | match kind { 523 | 1 => KeyEventKind::Press, 524 | 2 => KeyEventKind::Repeat, 525 | 3 => KeyEventKind::Release, 526 | _ => KeyEventKind::Press, 527 | } 528 | } 529 | 530 | fn parse_csi_modifier_key_code(buffer: &[u8]) -> Result> { 531 | assert!(buffer.starts_with(b"\x1B[")); // CSI 532 | let s = str::from_utf8(&buffer[2..buffer.len() - 1])?; 533 | let mut split = s.split(';'); 534 | 535 | split.next(); 536 | 537 | let (modifiers, kind) = 538 | if let Ok((modifier_mask, kind_code)) = modifier_and_kind_parsed(&mut split) { 539 | ( 540 | parse_modifiers(modifier_mask), 541 | parse_key_event_kind(kind_code), 542 | ) 543 | } else if buffer.len() > 3 { 544 | ( 545 | parse_modifiers( 546 | (buffer[buffer.len() - 2] as char) 547 | .to_digit(10) 548 | .ok_or(MalformedSequenceError)? as u8, 549 | ), 550 | KeyEventKind::Press, 551 | ) 552 | } else { 553 | (Modifiers::NONE, KeyEventKind::Press) 554 | }; 555 | let key = buffer[buffer.len() - 1]; 556 | 557 | let code = match key { 558 | b'A' => KeyCode::Up, 559 | b'B' => KeyCode::Down, 560 | b'C' => KeyCode::Right, 561 | b'D' => KeyCode::Left, 562 | b'F' => KeyCode::End, 563 | b'H' => KeyCode::Home, 564 | b'P' => KeyCode::Function(1), 565 | b'Q' => KeyCode::Function(2), 566 | b'R' => KeyCode::Function(3), 567 | b'S' => KeyCode::Function(4), 568 | _ => bail!(), 569 | }; 570 | 571 | let event = Event::Key(KeyEvent { 572 | code, 573 | modifiers, 574 | kind, 575 | state: KeyEventState::NONE, 576 | }); 577 | 578 | Ok(Some(event)) 579 | } 580 | 581 | fn parse_csi_special_key_code(buffer: &[u8]) -> Result> { 582 | assert!(buffer.starts_with(b"\x1B[")); // CSI 583 | assert!(buffer.ends_with(b"~")); 584 | 585 | let s = str::from_utf8(&buffer[2..buffer.len() - 1])?; 586 | let mut split = s.split(';'); 587 | 588 | // This CSI sequence can be a list of semicolon-separated numbers. 589 | let first = next_parsed::(&mut split)?; 590 | 591 | let (modifiers, kind, state) = 592 | if let Ok((modifier_mask, kind_code)) = modifier_and_kind_parsed(&mut split) { 593 | ( 594 | parse_modifiers(modifier_mask), 595 | parse_key_event_kind(kind_code), 596 | parse_modifiers_to_state(modifier_mask), 597 | ) 598 | } else { 599 | (Modifiers::NONE, KeyEventKind::Press, KeyEventState::NONE) 600 | }; 601 | 602 | let code = match first { 603 | 1 | 7 => KeyCode::Home, 604 | 2 => KeyCode::Insert, 605 | 3 => KeyCode::Delete, 606 | 4 | 8 => KeyCode::End, 607 | 5 => KeyCode::PageUp, 608 | 6 => KeyCode::PageDown, 609 | v @ 11..=15 => KeyCode::Function(v - 10), 610 | v @ 17..=21 => KeyCode::Function(v - 11), 611 | v @ 23..=26 => KeyCode::Function(v - 12), 612 | v @ 28..=29 => KeyCode::Function(v - 15), 613 | v @ 31..=34 => KeyCode::Function(v - 17), 614 | _ => bail!(), 615 | }; 616 | 617 | let event = Event::Key(KeyEvent { 618 | code, 619 | modifiers, 620 | kind, 621 | state, 622 | }); 623 | 624 | Ok(Some(event)) 625 | } 626 | 627 | fn translate_functional_key_code(codepoint: u32) -> Option<(KeyCode, KeyEventState)> { 628 | if let Some(keycode) = match codepoint { 629 | 57399 => Some(KeyCode::Char('0')), 630 | 57400 => Some(KeyCode::Char('1')), 631 | 57401 => Some(KeyCode::Char('2')), 632 | 57402 => Some(KeyCode::Char('3')), 633 | 57403 => Some(KeyCode::Char('4')), 634 | 57404 => Some(KeyCode::Char('5')), 635 | 57405 => Some(KeyCode::Char('6')), 636 | 57406 => Some(KeyCode::Char('7')), 637 | 57407 => Some(KeyCode::Char('8')), 638 | 57408 => Some(KeyCode::Char('9')), 639 | 57409 => Some(KeyCode::Char('.')), 640 | 57410 => Some(KeyCode::Char('/')), 641 | 57411 => Some(KeyCode::Char('*')), 642 | 57412 => Some(KeyCode::Char('-')), 643 | 57413 => Some(KeyCode::Char('+')), 644 | 57414 => Some(KeyCode::Enter), 645 | 57415 => Some(KeyCode::Char('=')), 646 | 57416 => Some(KeyCode::Char(',')), 647 | 57417 => Some(KeyCode::Left), 648 | 57418 => Some(KeyCode::Right), 649 | 57419 => Some(KeyCode::Up), 650 | 57420 => Some(KeyCode::Down), 651 | 57421 => Some(KeyCode::PageUp), 652 | 57422 => Some(KeyCode::PageDown), 653 | 57423 => Some(KeyCode::Home), 654 | 57424 => Some(KeyCode::End), 655 | 57425 => Some(KeyCode::Insert), 656 | 57426 => Some(KeyCode::Delete), 657 | 57427 => Some(KeyCode::KeypadBegin), 658 | _ => None, 659 | } { 660 | return Some((keycode, KeyEventState::KEYPAD)); 661 | } 662 | 663 | if let Some(keycode) = match codepoint { 664 | 57358 => Some(KeyCode::CapsLock), 665 | 57359 => Some(KeyCode::ScrollLock), 666 | 57360 => Some(KeyCode::NumLock), 667 | 57361 => Some(KeyCode::PrintScreen), 668 | 57362 => Some(KeyCode::Pause), 669 | 57363 => Some(KeyCode::Menu), 670 | 57376 => Some(KeyCode::Function(13)), 671 | 57377 => Some(KeyCode::Function(14)), 672 | 57378 => Some(KeyCode::Function(15)), 673 | 57379 => Some(KeyCode::Function(16)), 674 | 57380 => Some(KeyCode::Function(17)), 675 | 57381 => Some(KeyCode::Function(18)), 676 | 57382 => Some(KeyCode::Function(19)), 677 | 57383 => Some(KeyCode::Function(20)), 678 | 57384 => Some(KeyCode::Function(21)), 679 | 57385 => Some(KeyCode::Function(22)), 680 | 57386 => Some(KeyCode::Function(23)), 681 | 57387 => Some(KeyCode::Function(24)), 682 | 57388 => Some(KeyCode::Function(25)), 683 | 57389 => Some(KeyCode::Function(26)), 684 | 57390 => Some(KeyCode::Function(27)), 685 | 57391 => Some(KeyCode::Function(28)), 686 | 57392 => Some(KeyCode::Function(29)), 687 | 57393 => Some(KeyCode::Function(30)), 688 | 57394 => Some(KeyCode::Function(31)), 689 | 57395 => Some(KeyCode::Function(32)), 690 | 57396 => Some(KeyCode::Function(33)), 691 | 57397 => Some(KeyCode::Function(34)), 692 | 57398 => Some(KeyCode::Function(35)), 693 | 57428 => Some(KeyCode::Media(MediaKeyCode::Play)), 694 | 57429 => Some(KeyCode::Media(MediaKeyCode::Pause)), 695 | 57430 => Some(KeyCode::Media(MediaKeyCode::PlayPause)), 696 | 57431 => Some(KeyCode::Media(MediaKeyCode::Reverse)), 697 | 57432 => Some(KeyCode::Media(MediaKeyCode::Stop)), 698 | 57433 => Some(KeyCode::Media(MediaKeyCode::FastForward)), 699 | 57434 => Some(KeyCode::Media(MediaKeyCode::Rewind)), 700 | 57435 => Some(KeyCode::Media(MediaKeyCode::TrackNext)), 701 | 57436 => Some(KeyCode::Media(MediaKeyCode::TrackPrevious)), 702 | 57437 => Some(KeyCode::Media(MediaKeyCode::Record)), 703 | 57438 => Some(KeyCode::Media(MediaKeyCode::LowerVolume)), 704 | 57439 => Some(KeyCode::Media(MediaKeyCode::RaiseVolume)), 705 | 57440 => Some(KeyCode::Media(MediaKeyCode::MuteVolume)), 706 | 57441 => Some(KeyCode::Modifier(ModifierKeyCode::LeftShift)), 707 | 57442 => Some(KeyCode::Modifier(ModifierKeyCode::LeftControl)), 708 | 57443 => Some(KeyCode::Modifier(ModifierKeyCode::LeftAlt)), 709 | 57444 => Some(KeyCode::Modifier(ModifierKeyCode::LeftSuper)), 710 | 57445 => Some(KeyCode::Modifier(ModifierKeyCode::LeftHyper)), 711 | 57446 => Some(KeyCode::Modifier(ModifierKeyCode::LeftMeta)), 712 | 57447 => Some(KeyCode::Modifier(ModifierKeyCode::RightShift)), 713 | 57448 => Some(KeyCode::Modifier(ModifierKeyCode::RightControl)), 714 | 57449 => Some(KeyCode::Modifier(ModifierKeyCode::RightAlt)), 715 | 57450 => Some(KeyCode::Modifier(ModifierKeyCode::RightSuper)), 716 | 57451 => Some(KeyCode::Modifier(ModifierKeyCode::RightHyper)), 717 | 57452 => Some(KeyCode::Modifier(ModifierKeyCode::RightMeta)), 718 | 57453 => Some(KeyCode::Modifier(ModifierKeyCode::IsoLevel3Shift)), 719 | 57454 => Some(KeyCode::Modifier(ModifierKeyCode::IsoLevel5Shift)), 720 | _ => None, 721 | } { 722 | return Some((keycode, KeyEventState::empty())); 723 | } 724 | 725 | None 726 | } 727 | 728 | fn parse_csi_rxvt_mouse(buffer: &[u8]) -> Result> { 729 | // rxvt mouse encoding: 730 | // CSI Cb ; Cx ; Cy ; M 731 | 732 | assert!(buffer.starts_with(b"\x1B[")); // CSI 733 | assert!(buffer.ends_with(b"M")); 734 | 735 | let s = str::from_utf8(&buffer[2..buffer.len() - 1])?; 736 | let mut split = s.split(';'); 737 | 738 | let cb = next_parsed::(&mut split)? 739 | .checked_sub(32) 740 | .ok_or(MalformedSequenceError)?; 741 | let (kind, modifiers) = parse_cb(cb)?; 742 | 743 | let cx = next_parsed::(&mut split)? - 1; 744 | let cy = next_parsed::(&mut split)? - 1; 745 | 746 | Ok(Some(Event::Mouse(MouseEvent { 747 | kind, 748 | column: cx, 749 | row: cy, 750 | modifiers, 751 | }))) 752 | } 753 | 754 | fn parse_csi_normal_mouse(buffer: &[u8]) -> Result> { 755 | // Normal mouse encoding: CSI M CB Cx Cy (6 characters only). 756 | 757 | assert!(buffer.starts_with(b"\x1B[M")); // CSI M 758 | 759 | if buffer.len() < 6 { 760 | return Ok(None); 761 | } 762 | 763 | let cb = buffer[3].checked_sub(32).ok_or(MalformedSequenceError)?; 764 | let (kind, modifiers) = parse_cb(cb)?; 765 | 766 | // See http://www.xfree86.org/current/ctlseqs.html#Mouse%20Tracking 767 | // The upper left character position on the terminal is denoted as 1,1. 768 | // Subtract 1 to keep it synced with cursor 769 | let cx = u16::from(buffer[4].saturating_sub(32)) - 1; 770 | let cy = u16::from(buffer[5].saturating_sub(32)) - 1; 771 | 772 | Ok(Some(Event::Mouse(MouseEvent { 773 | kind, 774 | column: cx, 775 | row: cy, 776 | modifiers, 777 | }))) 778 | } 779 | 780 | fn parse_csi_sgr_mouse(buffer: &[u8]) -> Result> { 781 | // CSI < Cb ; Cx ; Cy (;) (M or m) 782 | 783 | assert!(buffer.starts_with(b"\x1B[<")); // CSI < 784 | 785 | if !buffer.ends_with(b"m") && !buffer.ends_with(b"M") { 786 | return Ok(None); 787 | } 788 | 789 | let s = str::from_utf8(&buffer[3..buffer.len() - 1])?; 790 | let mut split = s.split(';'); 791 | 792 | let cb = next_parsed::(&mut split)?; 793 | let (kind, modifiers) = parse_cb(cb)?; 794 | 795 | // See http://www.xfree86.org/current/ctlseqs.html#Mouse%20Tracking 796 | // The upper left character position on the terminal is denoted as 1,1. 797 | // Subtract 1 to keep it synced with cursor 798 | let cx = next_parsed::(&mut split)? - 1; 799 | let cy = next_parsed::(&mut split)? - 1; 800 | 801 | // When button 3 in Cb is used to represent mouse release, you can't tell which button was 802 | // released. SGR mode solves this by having the sequence end with a lowercase m if it's a 803 | // button release and an uppercase M if it's a button press. 804 | // 805 | // We've already checked that the last character is a lowercase or uppercase M at the start of 806 | // this function, so we just need one if. 807 | let kind = if buffer.last() == Some(&b'm') { 808 | match kind { 809 | MouseEventKind::Down(button) => MouseEventKind::Up(button), 810 | other => other, 811 | } 812 | } else { 813 | kind 814 | }; 815 | 816 | Ok(Some(Event::Mouse(MouseEvent { 817 | kind, 818 | column: cx, 819 | row: cy, 820 | modifiers, 821 | }))) 822 | } 823 | 824 | /// Cb is the byte of a mouse input that contains the button being used, the key modifiers being 825 | /// held and whether the mouse is dragging or not. 826 | /// 827 | /// Bit layout of cb, from low to high: 828 | /// 829 | /// - button number 830 | /// - button number 831 | /// - shift 832 | /// - meta (alt) 833 | /// - control 834 | /// - mouse is dragging 835 | /// - button number 836 | /// - button number 837 | fn parse_cb(cb: u8) -> Result<(MouseEventKind, Modifiers)> { 838 | let button_number = (cb & 0b0000_0011) | ((cb & 0b1100_0000) >> 4); 839 | let dragging = cb & 0b0010_0000 == 0b0010_0000; 840 | 841 | let kind = match (button_number, dragging) { 842 | (0, false) => MouseEventKind::Down(MouseButton::Left), 843 | (1, false) => MouseEventKind::Down(MouseButton::Middle), 844 | (2, false) => MouseEventKind::Down(MouseButton::Right), 845 | (0, true) => MouseEventKind::Drag(MouseButton::Left), 846 | (1, true) => MouseEventKind::Drag(MouseButton::Middle), 847 | (2, true) => MouseEventKind::Drag(MouseButton::Right), 848 | (3, false) => MouseEventKind::Up(MouseButton::Left), 849 | (3, true) | (4, true) | (5, true) => MouseEventKind::Moved, 850 | (4, false) => MouseEventKind::ScrollUp, 851 | (5, false) => MouseEventKind::ScrollDown, 852 | (6, false) => MouseEventKind::ScrollLeft, 853 | (7, false) => MouseEventKind::ScrollRight, 854 | // We do not support other buttons. 855 | _ => bail!(), 856 | }; 857 | 858 | let mut modifiers = Modifiers::empty(); 859 | 860 | if cb & 0b0000_0100 == 0b0000_0100 { 861 | modifiers |= Modifiers::SHIFT; 862 | } 863 | if cb & 0b0000_1000 == 0b0000_1000 { 864 | modifiers |= Modifiers::ALT; 865 | } 866 | if cb & 0b0001_0000 == 0b0001_0000 { 867 | modifiers |= Modifiers::CONTROL; 868 | } 869 | 870 | Ok((kind, modifiers)) 871 | } 872 | 873 | fn parse_csi_bracketed_paste(buffer: &[u8]) -> Result> { 874 | // CSI 2 0 0 ~ pasted text CSI 2 0 1 ~ 875 | assert!(buffer.starts_with(b"\x1B[200~")); 876 | 877 | if !buffer.ends_with(b"\x1b[201~") { 878 | Ok(None) 879 | } else { 880 | let paste = String::from_utf8_lossy(&buffer[6..buffer.len() - 6]).to_string(); 881 | Ok(Some(Event::Paste(paste))) 882 | } 883 | } 884 | 885 | fn parse_csi_cursor_position(buffer: &[u8]) -> Result> { 886 | // CSI Cy ; Cx R 887 | // Cy - cursor row number (starting from 1) 888 | // Cx - cursor column number (starting from 1) 889 | assert!(buffer.starts_with(b"\x1B[")); // CSI 890 | assert!(buffer.ends_with(b"R")); 891 | 892 | let s = str::from_utf8(&buffer[2..buffer.len() - 1])?; 893 | 894 | let mut split = s.split(';'); 895 | 896 | let line = next_parsed::(&mut split)?.into(); 897 | let col = next_parsed::(&mut split)?.into(); 898 | 899 | Ok(Some(Event::Csi(Csi::Cursor( 900 | csi::Cursor::ActivePositionReport { line, col }, 901 | )))) 902 | } 903 | 904 | fn parse_csi_keyboard_enhancement_flags(buffer: &[u8]) -> Result> { 905 | // CSI ? flags u 906 | assert!(buffer.starts_with(b"\x1B[?")); // ESC [ ? 907 | assert!(buffer.ends_with(b"u")); 908 | 909 | if buffer.len() < 5 { 910 | return Ok(None); 911 | } 912 | 913 | let bits = buffer[3]; 914 | let mut flags = KittyKeyboardFlags::empty(); 915 | 916 | if bits & 1 != 0 { 917 | flags |= KittyKeyboardFlags::DISAMBIGUATE_ESCAPE_CODES; 918 | } 919 | if bits & 2 != 0 { 920 | flags |= KittyKeyboardFlags::REPORT_EVENT_TYPES; 921 | } 922 | if bits & 4 != 0 { 923 | flags |= KittyKeyboardFlags::REPORT_ALTERNATE_KEYS; 924 | } 925 | if bits & 8 != 0 { 926 | flags |= KittyKeyboardFlags::REPORT_ALL_KEYS_AS_ESCAPE_CODES; 927 | } 928 | // TODO: support this 929 | // if bits & 16 != 0 { 930 | // flags |= KeyboardEnhancementFlags::REPORT_ASSOCIATED_TEXT; 931 | // } 932 | 933 | Ok(Some(Event::Csi(Csi::Keyboard(csi::Keyboard::ReportFlags( 934 | flags, 935 | ))))) 936 | } 937 | 938 | fn parse_csi_primary_device_attributes(buffer: &[u8]) -> Result> { 939 | // CSI 64 ; attr1 ; attr2 ; ... ; attrn ; c 940 | assert!(buffer.starts_with(b"\x1B[?")); 941 | assert!(buffer.ends_with(b"c")); 942 | 943 | // This is a stub for parsing the primary device attributes. This response is not 944 | // exposed in the crossterm API so we don't need to parse the individual attributes yet. 945 | // See 946 | 947 | Ok(Some(Event::Csi(Csi::Device( 948 | csi::Device::DeviceAttributes(()), 949 | )))) 950 | } 951 | 952 | fn parse_csi_theme_mode(buffer: &[u8]) -> Result> { 953 | // dark mode: CSI ? 997 ; 1 n 954 | // light mode: CSI ? 997 ; 2 n 955 | assert!(buffer.starts_with(b"\x1B[?")); 956 | assert!(buffer.ends_with(b"n")); 957 | 958 | let s = str::from_utf8(&buffer[3..buffer.len() - 1])?; 959 | 960 | let mut split = s.split(';'); 961 | 962 | if next_parsed::(&mut split)? != 997 { 963 | bail!(); 964 | } 965 | 966 | let theme_mode = match next_parsed::(&mut split)? { 967 | 1 => ThemeMode::Dark, 968 | 2 => ThemeMode::Light, 969 | _ => bail!(), 970 | }; 971 | 972 | Ok(Some(Event::Csi(Csi::Mode(csi::Mode::ReportTheme( 973 | theme_mode, 974 | ))))) 975 | } 976 | 977 | fn parse_csi_synchronized_output_mode(buffer: &[u8]) -> Result> { 978 | // CSI ? 2026 ; 0 $ y 979 | assert!(buffer.starts_with(b"\x1B[?")); 980 | assert!(buffer.ends_with(b"y")); 981 | 982 | let s = str::from_utf8(&buffer[3..buffer.len() - 1])?; 983 | let s = match s.strip_suffix('$') { 984 | Some(s) => s, 985 | None => bail!(), 986 | }; 987 | 988 | let mut split = s.split(';'); 989 | 990 | let mode = csi::DecPrivateModeCode::SynchronizedOutput; 991 | if next_parsed::(&mut split)? != mode as u16 { 992 | bail!(); 993 | } 994 | 995 | // For synchronized output specifically, 3 is undefined and 0 and 4 are treated as "not 996 | // supported." 997 | let setting = match next_parsed::(&mut split)? { 998 | 0 | 4 => csi::DecModeSetting::NotRecognized, 999 | 1 => csi::DecModeSetting::Set, 1000 | 2 => csi::DecModeSetting::Reset, 1001 | _ => bail!(), 1002 | }; 1003 | 1004 | Ok(Some(Event::Csi(Csi::Mode( 1005 | csi::Mode::ReportDecPrivateMode { 1006 | mode: csi::DecPrivateMode::Code(mode), 1007 | setting, 1008 | }, 1009 | )))) 1010 | } 1011 | 1012 | fn parse_dcs(buffer: &[u8]) -> Result> { 1013 | assert!(buffer.starts_with(escape::DCS.as_bytes())); 1014 | if !buffer.ends_with(escape::ST.as_bytes()) { 1015 | return Ok(None); 1016 | } 1017 | match buffer[buffer.len() - 3] { 1018 | // SGR response: DCS Ps $ r SGR m ST 1019 | b'm' => { 1020 | if buffer.get(3..5) != Some(b"$r") { 1021 | bail!(); 1022 | } 1023 | // NOTE: says that '1' is a valid 1024 | // request and '0' is invalid while the vt100.net docs for DECRQSS say the opposite. 1025 | // Kitty and WezTerm both follow the ctlseqs doc. 1026 | let is_request_valid = match buffer[2] { 1027 | b'1' => true, 1028 | // TODO: don't parse attributes if the request isn't valid? 1029 | b'0' => false, 1030 | _ => bail!(), 1031 | }; 1032 | let s = str::from_utf8(&buffer[5..buffer.len() - 3])?; 1033 | let mut sgrs = Vec::new(); 1034 | // TODO: is this correct? What about terminals that use ';' for true colors? 1035 | for sgr in s.split(';') { 1036 | sgrs.push(parse_sgr(sgr)?); 1037 | } 1038 | Ok(Some(Event::Dcs(dcs::Dcs::Response { 1039 | is_request_valid, 1040 | value: dcs::DcsResponse::GraphicRendition(sgrs), 1041 | }))) 1042 | } 1043 | _ => bail!(), 1044 | } 1045 | } 1046 | 1047 | fn parse_sgr(buffer: &str) -> Result { 1048 | use csi::Sgr; 1049 | use style::*; 1050 | 1051 | let sgr = match buffer { 1052 | "0" => Sgr::Reset, 1053 | "22" => Sgr::Intensity(Intensity::Normal), 1054 | "1" => Sgr::Intensity(Intensity::Bold), 1055 | "2" => Sgr::Intensity(Intensity::Dim), 1056 | "24" => Sgr::Underline(Underline::None), 1057 | "4" => Sgr::Underline(Underline::Single), 1058 | "21" => Sgr::Underline(Underline::Double), 1059 | "4:3 " => Sgr::Underline(Underline::Curly), 1060 | "4:4" => Sgr::Underline(Underline::Dotted), 1061 | "4:5" => Sgr::Underline(Underline::Dashed), 1062 | "25" => Sgr::Blink(Blink::None), 1063 | "5" => Sgr::Blink(Blink::Slow), 1064 | "6" => Sgr::Blink(Blink::Rapid), 1065 | "3" => Sgr::Italic(true), 1066 | "23" => Sgr::Italic(false), 1067 | "7" => Sgr::Reverse(true), 1068 | "27" => Sgr::Reverse(false), 1069 | "8" => Sgr::Invisible(true), 1070 | "28" => Sgr::Invisible(false), 1071 | "9" => Sgr::StrikeThrough(true), 1072 | "29" => Sgr::StrikeThrough(false), 1073 | "53" => Sgr::Overline(true), 1074 | "55" => Sgr::Overline(false), 1075 | "10" => Sgr::Font(Font::Default), 1076 | "11" => Sgr::Font(Font::Alternate(1)), 1077 | "12" => Sgr::Font(Font::Alternate(2)), 1078 | "13" => Sgr::Font(Font::Alternate(3)), 1079 | "14" => Sgr::Font(Font::Alternate(4)), 1080 | "15" => Sgr::Font(Font::Alternate(5)), 1081 | "16" => Sgr::Font(Font::Alternate(6)), 1082 | "17" => Sgr::Font(Font::Alternate(7)), 1083 | "18" => Sgr::Font(Font::Alternate(8)), 1084 | "19" => Sgr::Font(Font::Alternate(9)), 1085 | "75" => Sgr::VerticalAlign(VerticalAlign::BaseLine), 1086 | "73" => Sgr::VerticalAlign(VerticalAlign::SuperScript), 1087 | "74" => Sgr::VerticalAlign(VerticalAlign::SubScript), 1088 | "39" => Sgr::Foreground(ColorSpec::Reset), 1089 | "30" => Sgr::Foreground(ColorSpec::BLACK), 1090 | "31" => Sgr::Foreground(ColorSpec::RED), 1091 | "32" => Sgr::Foreground(ColorSpec::GREEN), 1092 | "33" => Sgr::Foreground(ColorSpec::YELLOW), 1093 | "34" => Sgr::Foreground(ColorSpec::BLUE), 1094 | "35" => Sgr::Foreground(ColorSpec::MAGENTA), 1095 | "36" => Sgr::Foreground(ColorSpec::CYAN), 1096 | "37" => Sgr::Foreground(ColorSpec::WHITE), 1097 | "90" => Sgr::Foreground(ColorSpec::BRIGHT_BLACK), 1098 | "91" => Sgr::Foreground(ColorSpec::BRIGHT_RED), 1099 | "92" => Sgr::Foreground(ColorSpec::BRIGHT_GREEN), 1100 | "93" => Sgr::Foreground(ColorSpec::BRIGHT_YELLOW), 1101 | "94" => Sgr::Foreground(ColorSpec::BRIGHT_BLUE), 1102 | "95" => Sgr::Foreground(ColorSpec::BRIGHT_MAGENTA), 1103 | "96" => Sgr::Foreground(ColorSpec::BRIGHT_CYAN), 1104 | "97" => Sgr::Foreground(ColorSpec::BRIGHT_WHITE), 1105 | "49" => Sgr::Background(ColorSpec::Reset), 1106 | "40" => Sgr::Background(ColorSpec::BLACK), 1107 | "41" => Sgr::Background(ColorSpec::RED), 1108 | "42" => Sgr::Background(ColorSpec::GREEN), 1109 | "43" => Sgr::Background(ColorSpec::YELLOW), 1110 | "44" => Sgr::Background(ColorSpec::BLUE), 1111 | "45" => Sgr::Background(ColorSpec::MAGENTA), 1112 | "46" => Sgr::Background(ColorSpec::CYAN), 1113 | "47" => Sgr::Background(ColorSpec::WHITE), 1114 | "100" => Sgr::Background(ColorSpec::BRIGHT_BLACK), 1115 | "101" => Sgr::Background(ColorSpec::BRIGHT_RED), 1116 | "102" => Sgr::Background(ColorSpec::BRIGHT_GREEN), 1117 | "103" => Sgr::Background(ColorSpec::BRIGHT_YELLOW), 1118 | "104" => Sgr::Background(ColorSpec::BRIGHT_BLUE), 1119 | "105" => Sgr::Background(ColorSpec::BRIGHT_MAGENTA), 1120 | "106" => Sgr::Background(ColorSpec::BRIGHT_CYAN), 1121 | "107" => Sgr::Background(ColorSpec::BRIGHT_WHITE), 1122 | "59" => Sgr::UnderlineColor(ColorSpec::Reset), 1123 | _ => { 1124 | let mut split = buffer.split(':').filter(|s| !s.is_empty()); 1125 | let first = next_parsed::(&mut split)?; 1126 | let color = match next_parsed::(&mut split)? { 1127 | 2 => RgbColor { 1128 | red: next_parsed::(&mut split)?, 1129 | green: next_parsed::(&mut split)?, 1130 | blue: next_parsed::(&mut split)?, 1131 | } 1132 | .into(), 1133 | 5 => ColorSpec::PaletteIndex(next_parsed::(&mut split)?), 1134 | 6 => RgbaColor { 1135 | red: next_parsed::(&mut split)?, 1136 | green: next_parsed::(&mut split)?, 1137 | blue: next_parsed::(&mut split)?, 1138 | alpha: next_parsed::(&mut split)?, 1139 | } 1140 | .into(), 1141 | _ => bail!(), 1142 | }; 1143 | match first { 1144 | 38 => Sgr::Foreground(color), 1145 | 48 => Sgr::Background(color), 1146 | 58 => Sgr::UnderlineColor(color), 1147 | _ => bail!(), 1148 | } 1149 | } 1150 | }; 1151 | Ok(sgr) 1152 | } 1153 | 1154 | #[cfg(test)] 1155 | mod test { 1156 | use super::*; 1157 | 1158 | #[test] 1159 | fn parse_dcs_sgr_response() { 1160 | // Example from 1161 | // > If the current graphic rendition is underline, blinking, and reverse, then the 1162 | // > terminal responds with the following DECRPSS sequence: 1163 | // > DCS 0 $ r 0 ; 4 ; 5 ; 7 m ST 1164 | // NOTE: The vt100.net docs have the Ps part of this reversed. 0 is invalid and 1 is 1165 | // valid according to the xterm docs. See `parse_dcs`. 1166 | let event = parse_event(b"\x1bP0$r0;4;5;7m\x1b\\", false) 1167 | .unwrap() 1168 | .unwrap(); 1169 | assert_eq!( 1170 | event, 1171 | Event::Dcs(dcs::Dcs::Response { 1172 | is_request_valid: false, 1173 | value: dcs::DcsResponse::GraphicRendition(vec![ 1174 | csi::Sgr::Reset, 1175 | csi::Sgr::Underline(style::Underline::Single), 1176 | csi::Sgr::Blink(style::Blink::Slow), 1177 | csi::Sgr::Reverse(true), 1178 | ]) 1179 | }) 1180 | ); 1181 | } 1182 | } 1183 | -------------------------------------------------------------------------------- /src/style.rs: -------------------------------------------------------------------------------- 1 | //! Types for styling terminal cells. 2 | 3 | // CREDIT: This is shared almost fairly between crossterm and termwiz. SGR properties like 4 | // `Underline`, `CursorStyle` and `Intensity` are from termwiz. The `StyleExt` trait is similar 5 | // to a crossterm trait. 6 | 7 | use std::{ 8 | borrow::Cow, 9 | fmt::{self, Display}, 10 | sync::atomic::{AtomicBool, Ordering}, 11 | }; 12 | 13 | use crate::escape::{ 14 | self, 15 | csi::{Csi, Sgr}, 16 | }; 17 | 18 | /// Styling of a cell's underline. 19 | #[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] 20 | // 21 | pub enum Underline { 22 | /// No underline 23 | #[default] 24 | None = 0, 25 | /// Straight underline 26 | Single = 1, 27 | /// Two underlines stacked on top of one another 28 | Double = 2, 29 | /// Curly / "squiggly" / "wavy" underline 30 | Curly = 3, 31 | /// Dotted underline 32 | Dotted = 4, 33 | /// Dashed underline 34 | Dashed = 5, 35 | } 36 | 37 | #[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] 38 | pub enum CursorStyle { 39 | #[default] 40 | Default = 0, 41 | BlinkingBlock = 1, 42 | SteadyBlock = 2, 43 | BlinkingUnderline = 3, 44 | SteadyUnderline = 4, 45 | BlinkingBar = 5, 46 | SteadyBar = 6, 47 | } 48 | 49 | impl Display for CursorStyle { 50 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 51 | write!(f, "{}", *self as u8) 52 | } 53 | } 54 | 55 | /// An 8-bit "256-color". 56 | /// 57 | /// Colors 0-15 are the same as `AnsiColor`s (0-7 being normal colors and 8-15 being "bright"). 58 | /// Colors 16-231 make up a 6x6x6 "color cube." The remaining 232-255 colors define a 59 | /// dark-to-light grayscale in 24 steps. 60 | /// 61 | /// These are also known as "web-safe colors" or "X11 colors" historically, although the actual 62 | /// colors varied somewhat between historical usages. 63 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 64 | // 65 | pub struct WebColor(pub u8); 66 | 67 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 68 | pub struct RgbColor { 69 | pub red: u8, 70 | pub green: u8, 71 | pub blue: u8, 72 | } 73 | 74 | impl RgbColor { 75 | pub const fn new(red: u8, green: u8, blue: u8) -> Self { 76 | Self { red, green, blue } 77 | } 78 | 79 | /// The floats are expected to be in the range `0.0..=1.0`. 80 | pub fn new_f32(red: f32, green: f32, blue: f32) -> Self { 81 | let red = (red * 255.) as u8; 82 | let green = (green * 255.) as u8; 83 | let blue = (blue * 255.) as u8; 84 | Self { red, green, blue } 85 | } 86 | } 87 | 88 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 89 | pub struct RgbaColor { 90 | pub red: u8, 91 | pub green: u8, 92 | pub blue: u8, 93 | /// Also known as "opacity" 94 | pub alpha: u8, 95 | } 96 | 97 | impl From for RgbColor { 98 | fn from(color: RgbaColor) -> Self { 99 | Self { 100 | red: color.red, 101 | green: color.green, 102 | blue: color.blue, 103 | } 104 | } 105 | } 106 | 107 | impl From for RgbaColor { 108 | fn from(color: RgbColor) -> Self { 109 | Self { 110 | red: color.red, 111 | green: color.green, 112 | blue: color.blue, 113 | alpha: 255, 114 | } 115 | } 116 | } 117 | 118 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 119 | // 120 | pub enum AnsiColor { 121 | Black = 0, 122 | Red, 123 | Green, 124 | Yellow, 125 | Blue, 126 | Magenta, 127 | Cyan, 128 | White, 129 | /// "Bright" black (also known as "Gray") 130 | BrightBlack, 131 | BrightRed, 132 | BrightGreen, 133 | BrightYellow, 134 | BrightBlue, 135 | BrightMagenta, 136 | BrightCyan, 137 | BrightWhite, 138 | } 139 | 140 | pub type PaletteIndex = u8; 141 | 142 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 143 | pub enum ColorSpec { 144 | Reset, 145 | PaletteIndex(PaletteIndex), 146 | TrueColor(RgbaColor), 147 | } 148 | 149 | impl ColorSpec { 150 | pub const BLACK: Self = Self::PaletteIndex(AnsiColor::Black as PaletteIndex); 151 | pub const RED: Self = Self::PaletteIndex(AnsiColor::Red as PaletteIndex); 152 | pub const GREEN: Self = Self::PaletteIndex(AnsiColor::Green as PaletteIndex); 153 | pub const YELLOW: Self = Self::PaletteIndex(AnsiColor::Yellow as PaletteIndex); 154 | pub const BLUE: Self = Self::PaletteIndex(AnsiColor::Blue as PaletteIndex); 155 | pub const MAGENTA: Self = Self::PaletteIndex(AnsiColor::Magenta as PaletteIndex); 156 | pub const CYAN: Self = Self::PaletteIndex(AnsiColor::Cyan as PaletteIndex); 157 | pub const WHITE: Self = Self::PaletteIndex(AnsiColor::White as PaletteIndex); 158 | pub const BRIGHT_BLACK: Self = Self::PaletteIndex(AnsiColor::BrightBlack as PaletteIndex); 159 | pub const BRIGHT_RED: Self = Self::PaletteIndex(AnsiColor::BrightRed as PaletteIndex); 160 | pub const BRIGHT_GREEN: Self = Self::PaletteIndex(AnsiColor::BrightGreen as PaletteIndex); 161 | pub const BRIGHT_YELLOW: Self = Self::PaletteIndex(AnsiColor::BrightYellow as PaletteIndex); 162 | pub const BRIGHT_BLUE: Self = Self::PaletteIndex(AnsiColor::BrightBlue as PaletteIndex); 163 | pub const BRIGHT_MAGENTA: Self = Self::PaletteIndex(AnsiColor::BrightMagenta as PaletteIndex); 164 | pub const BRIGHT_CYAN: Self = Self::PaletteIndex(AnsiColor::BrightCyan as PaletteIndex); 165 | pub const BRIGHT_WHITE: Self = Self::PaletteIndex(AnsiColor::BrightWhite as PaletteIndex); 166 | } 167 | 168 | impl From for ColorSpec { 169 | fn from(color: AnsiColor) -> Self { 170 | Self::PaletteIndex(color as u8) 171 | } 172 | } 173 | 174 | impl From for ColorSpec { 175 | fn from(color: WebColor) -> Self { 176 | Self::PaletteIndex(color.0) 177 | } 178 | } 179 | 180 | impl From for ColorSpec { 181 | fn from(color: RgbColor) -> Self { 182 | Self::TrueColor(color.into()) 183 | } 184 | } 185 | 186 | impl From for ColorSpec { 187 | fn from(color: RgbaColor) -> Self { 188 | Self::TrueColor(color) 189 | } 190 | } 191 | 192 | #[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] 193 | pub enum Intensity { 194 | #[default] 195 | Normal, 196 | Bold, 197 | Dim, 198 | } 199 | 200 | #[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] 201 | pub enum Blink { 202 | #[default] 203 | None, 204 | Slow, 205 | Rapid, 206 | } 207 | 208 | #[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] 209 | pub enum Font { 210 | #[default] 211 | Default, 212 | /// An alternate font. Valid values are 1-9. 213 | Alternate(u8), 214 | } 215 | 216 | #[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] 217 | pub enum VerticalAlign { 218 | #[default] 219 | BaseLine = 0, 220 | SuperScript = 1, 221 | SubScript = 2, 222 | } 223 | 224 | /// A helper type for conveniently rendering styled content to the terminal. 225 | /// 226 | /// This is meant to be used instead of `PlatformTerminal` and proper CSI SGR codes when printing 227 | /// basic text, for example a CLI's help string. 228 | /// 229 | /// Instead of using this type directly, `use` the `StyleExt` trait and the helper functions 230 | /// attached to strings: 231 | /// 232 | /// ``` 233 | /// use termina::style::StyleExt as _; 234 | /// assert_eq!("green".green().to_string(), "\x1b[0;32mgreen\x1b[m"); 235 | /// ``` 236 | #[derive(Debug, Clone, PartialEq, Eq)] 237 | pub struct Stylized<'a> { 238 | pub content: Cow<'a, str>, 239 | styles: Vec, 240 | } 241 | 242 | static INITIALIZER: parking_lot::Once = parking_lot::Once::new(); 243 | static NO_COLOR: AtomicBool = AtomicBool::new(false); 244 | 245 | impl Stylized<'_> { 246 | /// Checks whether ANSI color sequences where turned off in the environment. 247 | /// 248 | /// See : if the `NO_COLOR` environment variable is present and 249 | /// non-empty, color escape sequences will be omitted when rendering this struct. This 250 | /// behavior can be overridden with [Self::force_ansi_color]. 251 | pub fn is_ansi_color_disabled() -> bool { 252 | // 253 | INITIALIZER.call_once(|| { 254 | NO_COLOR.store( 255 | std::env::var("NO_COLOR").is_ok_and(|e| !e.is_empty()), 256 | Ordering::SeqCst, 257 | ); 258 | }); 259 | NO_COLOR.load(Ordering::SeqCst) 260 | } 261 | 262 | /// Overrides detection of the `NO_COLOR` environment variable. 263 | /// 264 | /// Pass `true` to ensure that ANSI color codes are always included when displaying this type 265 | /// or `false` to ensure ANSI color codes are never included. 266 | pub fn force_ansi_color(enable_color: bool) { 267 | // Run the `Once` first so this override is not later overwritten by the `Once` fn. 268 | let _ = Self::is_ansi_color_disabled(); 269 | NO_COLOR.store(!enable_color, Ordering::SeqCst); 270 | } 271 | } 272 | 273 | impl Display for Stylized<'_> { 274 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 275 | let no_color = Self::is_ansi_color_disabled(); 276 | let mut styles = self 277 | .styles 278 | .iter() 279 | .filter(|sgr| { 280 | !(no_color 281 | && matches!( 282 | sgr, 283 | Sgr::Foreground(_) | Sgr::Background(_) | Sgr::UnderlineColor(_) 284 | )) 285 | }) 286 | .peekable(); 287 | 288 | if styles.peek().is_none() { 289 | write!(f, "{}", self.content)?; 290 | } else { 291 | write!(f, "{}0", escape::CSI)?; 292 | for sgr in styles { 293 | write!(f, ";{sgr}")?; 294 | } 295 | write!(f, "m{}{}", self.content, Csi::Sgr(Sgr::Reset))?; 296 | } 297 | Ok(()) 298 | } 299 | } 300 | 301 | pub trait StyleExt<'a>: Sized { 302 | fn stylized(self) -> Stylized<'a>; 303 | 304 | fn foreground(self, color: impl Into) -> Stylized<'a> { 305 | let mut this = self.stylized(); 306 | this.styles.push(Sgr::Foreground(color.into())); 307 | this 308 | } 309 | fn red(self) -> Stylized<'a> { 310 | self.foreground(ColorSpec::RED) 311 | } 312 | fn yellow(self) -> Stylized<'a> { 313 | self.foreground(ColorSpec::YELLOW) 314 | } 315 | fn green(self) -> Stylized<'a> { 316 | self.foreground(ColorSpec::GREEN) 317 | } 318 | fn underlined(self) -> Stylized<'a> { 319 | let mut this = self.stylized(); 320 | this.styles.push(Sgr::Underline(Underline::Single)); 321 | this 322 | } 323 | fn bold(self) -> Stylized<'a> { 324 | let mut this = self.stylized(); 325 | this.styles.push(Sgr::Intensity(Intensity::Bold)); 326 | this 327 | } 328 | } 329 | 330 | impl<'a> StyleExt<'a> for Cow<'a, str> { 331 | fn stylized(self) -> Stylized<'a> { 332 | Stylized { 333 | content: self, 334 | styles: Vec::with_capacity(2), 335 | } 336 | } 337 | } 338 | 339 | impl<'a> StyleExt<'a> for &'a str { 340 | fn stylized(self) -> Stylized<'a> { 341 | Cow::Borrowed(self).stylized() 342 | } 343 | } 344 | 345 | impl StyleExt<'static> for String { 346 | fn stylized(self) -> Stylized<'static> { 347 | Cow::::Owned(self).stylized() 348 | } 349 | } 350 | 351 | // NOTE: this allows chaining like `"hello".green().bold()`. 352 | impl<'a> StyleExt<'a> for Stylized<'a> { 353 | fn stylized(self) -> Stylized<'a> { 354 | self 355 | } 356 | } 357 | -------------------------------------------------------------------------------- /src/terminal.rs: -------------------------------------------------------------------------------- 1 | #[cfg(unix)] 2 | mod unix; 3 | 4 | #[cfg(windows)] 5 | mod windows; 6 | 7 | use std::{io, time::Duration}; 8 | 9 | #[cfg(unix)] 10 | pub use unix::*; 11 | 12 | #[cfg(windows)] 13 | pub use windows::*; 14 | 15 | use crate::{Event, EventReader, WindowSize}; 16 | 17 | /// An alias to the terminal available for the current platform. 18 | /// 19 | /// On Windows this uses the `WindowsTerminal`, otherwise `UnixTerminal`. 20 | #[cfg(unix)] 21 | pub type PlatformTerminal = UnixTerminal; 22 | #[cfg(windows)] 23 | pub type PlatformTerminal = WindowsTerminal; 24 | 25 | #[cfg(unix)] 26 | pub type PlatformHandle = FileDescriptor; 27 | #[cfg(windows)] 28 | pub type PlatformHandle = OutputHandle; 29 | 30 | // CREDIT: This is heavily based on termwiz. 31 | // 32 | // This trait is simpler, however, and the terminals themselves do not have drop glue or try 33 | // to enable features like bracketed paste: that is left to dependents of `termina`. The `poll` 34 | // and `read` functions mirror . 35 | // Also see `src/event/reader.rs`. 36 | 37 | pub trait Terminal: io::Write { 38 | /// Enters the "raw" terminal mode. 39 | /// 40 | /// While in "raw" mode a terminal will not attempt to do any helpful interpretation of input 41 | /// such as waiting for Enter key presses to pass input. This is essentially the opposite of 42 | /// "cooked" mode. To exit raw mode, use `Self::enter_cooked_mode`. 43 | fn enter_raw_mode(&mut self) -> io::Result<()>; 44 | /// Enters the "cooked" terminal mode. 45 | /// 46 | /// This is considered the normal mode for a terminal device. 47 | /// 48 | /// While in "cooked" mode a terminal will interpret the incoming data in ways that are useful 49 | /// such as waiting for an Enter key press to pass input to the application. 50 | fn enter_cooked_mode(&mut self) -> io::Result<()>; 51 | fn get_dimensions(&self) -> io::Result; 52 | fn event_reader(&self) -> EventReader; 53 | /// Checks if there is an `Event` available. 54 | /// 55 | /// Returns `Ok(true)` if an `Event` is available or `Ok(false)` if one is not available. 56 | /// If `timeout` is `None` then `poll` will block indefinitely. 57 | fn poll bool>(&self, filter: F, timeout: Option) 58 | -> io::Result; 59 | /// Reads a single `Event` from the terminal. 60 | /// 61 | /// This function blocks until an `Event` is available. Use `poll` first to guarantee that the 62 | /// read won't block. 63 | fn read bool>(&self, filter: F) -> io::Result; 64 | /// Sets a hook function to run. 65 | /// 66 | /// Depending on how your application handles panics you may wish to set a panic hook which 67 | /// eagerly resets the terminal (such as by disabling bracketed paste and entering the main 68 | /// screen). The parameter for this hook is a platform handle to `std::io::stdout` or 69 | /// equivalent which implements `std::io::Write`. When the hook function is finished running 70 | /// the handle's modes will be reset (same as `enter_cooked_mode`). 71 | fn set_panic_hook(&mut self, f: impl Fn(&mut PlatformHandle) + Send + Sync + 'static); 72 | } 73 | -------------------------------------------------------------------------------- /src/terminal/unix.rs: -------------------------------------------------------------------------------- 1 | use rustix::termios::{self, Termios}; 2 | use std::{ 3 | fs, 4 | io::{self, BufWriter, IsTerminal as _, Write as _}, 5 | os::unix::prelude::*, 6 | }; 7 | 8 | use crate::{event::source::UnixEventSource, Event, EventReader, WindowSize}; 9 | 10 | use super::Terminal; 11 | 12 | const BUF_SIZE: usize = 4096; 13 | 14 | // CREDIT: FileDescriptor stuff is mostly based on the WezTerm crate `filedescriptor` but has been 15 | // rewritten with `rustix` instead of `libc`. 16 | // 17 | 18 | #[derive(Debug)] 19 | pub enum FileDescriptor { 20 | Owned(OwnedFd), 21 | Borrowed(BorrowedFd<'static>), 22 | } 23 | 24 | impl AsFd for FileDescriptor { 25 | fn as_fd(&self) -> BorrowedFd<'_> { 26 | match self { 27 | Self::Owned(fd) => fd.as_fd(), 28 | Self::Borrowed(fd) => *fd, 29 | } 30 | } 31 | } 32 | 33 | impl FileDescriptor { 34 | pub const STDIN: Self = Self::Borrowed(rustix::stdio::stdin()); 35 | pub const STDOUT: Self = Self::Borrowed(rustix::stdio::stdout()); 36 | 37 | fn try_clone(&self) -> io::Result { 38 | let this = match self { 39 | Self::Owned(fd) => Self::Owned(fd.try_clone()?), 40 | Self::Borrowed(fd) => Self::Borrowed(*fd), 41 | }; 42 | Ok(this) 43 | } 44 | } 45 | 46 | impl io::Read for FileDescriptor { 47 | fn read(&mut self, buf: &mut [u8]) -> io::Result { 48 | let read = rustix::io::read(&self, buf)?; 49 | Ok(read) 50 | } 51 | } 52 | 53 | impl io::Write for FileDescriptor { 54 | fn write(&mut self, buf: &[u8]) -> io::Result { 55 | let written = rustix::io::write(self, buf)?; 56 | Ok(written) 57 | } 58 | 59 | fn flush(&mut self) -> io::Result<()> { 60 | Ok(()) 61 | } 62 | } 63 | 64 | fn open_pty() -> io::Result<(FileDescriptor, FileDescriptor)> { 65 | let read = if io::stdin().is_terminal() { 66 | FileDescriptor::STDIN 67 | } else { 68 | open_dev_tty()? 69 | }; 70 | let write = if io::stdout().is_terminal() { 71 | FileDescriptor::STDOUT 72 | } else { 73 | open_dev_tty()? 74 | }; 75 | 76 | // Activate non-blocking mode for the reader. 77 | // NOTE: this seems to make macOS consistently fail with io::ErrorKind::WouldBlock errors. 78 | // rustix::io::ioctl_fionbio(&read, true)?; 79 | 80 | Ok((read, write)) 81 | } 82 | 83 | fn open_dev_tty() -> io::Result { 84 | let file = fs::OpenOptions::new() 85 | .read(true) 86 | .write(true) 87 | .open("/dev/tty")?; 88 | Ok(FileDescriptor::Owned(file.into())) 89 | } 90 | 91 | impl From for WindowSize { 92 | fn from(size: termios::Winsize) -> Self { 93 | Self { 94 | cols: size.ws_col, 95 | rows: size.ws_row, 96 | pixel_width: Some(size.ws_xpixel), 97 | pixel_height: Some(size.ws_ypixel), 98 | } 99 | } 100 | } 101 | 102 | // CREDIT: 103 | // Some discussion, though: Termwiz's terminals combine the terminal interaction (reading 104 | // dimensions, reading events, writing bytes, etc.) all in one type. Crossterm splits these 105 | // concerns and I prefer that interface. As such this type is very much based on Termwiz' 106 | // `UnixTerminal` but the responsibilities are split between this file and 107 | // `src/event/source/unix.rs` - the latter being more inspired by crossterm. 108 | // Ultimately this terminal doesn't look much like Termwiz' due to the use of `rustix` and 109 | // differences in the trait and `Drop` behavior (see `super`'s CREDIT comment). 110 | 111 | #[derive(Debug)] 112 | pub struct UnixTerminal { 113 | /// Shared wrapper around the reader (stdin or `/dev/tty`) 114 | reader: EventReader, 115 | /// Buffered handle to the writer (stdout or `/dev/tty`) 116 | write: BufWriter, 117 | /// The termios of the PTY's writer detected during `Self::new`. 118 | original_termios: Termios, 119 | has_panic_hook: bool, 120 | } 121 | 122 | impl UnixTerminal { 123 | pub fn new() -> io::Result { 124 | let (read, write) = open_pty()?; 125 | let source = UnixEventSource::new(read, write.try_clone()?)?; 126 | let original_termios = termios::tcgetattr(&write)?; 127 | let reader = EventReader::new(source); 128 | 129 | Ok(Self { 130 | reader, 131 | write: BufWriter::with_capacity(BUF_SIZE, write), 132 | original_termios, 133 | has_panic_hook: false, 134 | }) 135 | } 136 | } 137 | 138 | impl Terminal for UnixTerminal { 139 | fn enter_raw_mode(&mut self) -> io::Result<()> { 140 | let mut termios = termios::tcgetattr(self.write.get_ref())?; 141 | termios.make_raw(); 142 | termios::tcsetattr( 143 | self.write.get_ref(), 144 | termios::OptionalActions::Flush, 145 | &termios, 146 | )?; 147 | 148 | Ok(()) 149 | } 150 | 151 | fn enter_cooked_mode(&mut self) -> io::Result<()> { 152 | termios::tcsetattr( 153 | self.write.get_ref(), 154 | termios::OptionalActions::Now, 155 | &self.original_termios, 156 | )?; 157 | Ok(()) 158 | } 159 | 160 | fn get_dimensions(&self) -> io::Result { 161 | let winsize = termios::tcgetwinsize(self.write.get_ref())?; 162 | Ok(winsize.into()) 163 | } 164 | 165 | fn event_reader(&self) -> EventReader { 166 | self.reader.clone() 167 | } 168 | 169 | fn poll bool>( 170 | &self, 171 | filter: F, 172 | timeout: Option, 173 | ) -> io::Result { 174 | self.reader.poll(timeout, filter) 175 | } 176 | 177 | fn read bool>(&self, filter: F) -> io::Result { 178 | self.reader.read(filter) 179 | } 180 | 181 | fn set_panic_hook(&mut self, f: impl Fn(&mut FileDescriptor) + Send + Sync + 'static) { 182 | let original_termios = self.original_termios.clone(); 183 | let hook = std::panic::take_hook(); 184 | std::panic::set_hook(Box::new(move |info| { 185 | if let Ok((_read, mut write)) = open_pty() { 186 | f(&mut write); 187 | let _ = termios::tcsetattr(write, termios::OptionalActions::Now, &original_termios); 188 | } 189 | hook(info); 190 | })); 191 | self.has_panic_hook = true; 192 | } 193 | } 194 | 195 | impl Drop for UnixTerminal { 196 | fn drop(&mut self) { 197 | if !self.has_panic_hook || !std::thread::panicking() { 198 | let _ = self.flush(); 199 | let _ = self.enter_cooked_mode(); 200 | } 201 | } 202 | } 203 | 204 | impl io::Write for UnixTerminal { 205 | fn write(&mut self, buf: &[u8]) -> io::Result { 206 | self.write.write(buf) 207 | } 208 | 209 | fn flush(&mut self) -> io::Result<()> { 210 | self.write.flush() 211 | } 212 | } 213 | -------------------------------------------------------------------------------- /src/terminal/windows.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | fs::{self, File}, 3 | io::{self, BufWriter, IsTerminal as _, Write as _}, 4 | mem, 5 | os::windows::prelude::*, 6 | ptr, 7 | }; 8 | 9 | use windows_sys::Win32::{ 10 | Storage::FileSystem::WriteFile, 11 | System::Console::{ 12 | self, GetConsoleCP, GetConsoleMode, GetConsoleOutputCP, GetConsoleScreenBufferInfo, 13 | GetNumberOfConsoleInputEvents, ReadConsoleInputA, SetConsoleCP, SetConsoleMode, 14 | SetConsoleOutputCP, CONSOLE_MODE, CONSOLE_SCREEN_BUFFER_INFO, INPUT_RECORD, 15 | }, 16 | }; 17 | 18 | use crate::{event::source::WindowsEventSource, Event, EventReader, OneBased, WindowSize}; 19 | 20 | use super::Terminal; 21 | 22 | macro_rules! bail { 23 | ($msg:literal $(,)?) => { 24 | return Err(::std::io::Error::new(::std::io::ErrorKind::Other, $msg)) 25 | }; 26 | ($fmt:expr $(,)?, $($arg:tt)*) => { 27 | return Err(::std::io::Error::new(::std::io::ErrorKind::Other, format!($fmt, $($arg)*))) 28 | }; 29 | } 30 | 31 | const BUF_SIZE: usize = 128; 32 | 33 | type CodePageID = u32; 34 | /// The code page ID for UTF-8 encoding. 35 | /// This is the same as `windows_sys::Win32::Globalization::CP_UTF8`. It is copied here rather 36 | /// than `use`d because it is the only thing we want from the globalization API. Avoiding the 37 | /// `Win32_Globalization` feature for `windows_sys` saves a fair amount of compilation time. 38 | /// And it's unimaginable that Windows would ever change a constant like this given their passion 39 | /// for backwards compatibility. 40 | const CP_UTF8: CodePageID = 65001; 41 | 42 | // CREDIT: Like the Unix terminal module this is mainly based on WezTerm code (except for the 43 | // event source parts in `src/event/source/windows.rs` which reaches into these functions). 44 | // 45 | // This crate however uses `windows-sys` instead of `winapi` and has a slightly different API for 46 | // the `InputHandle` and `OutputHandle`. 47 | 48 | #[derive(Debug)] 49 | pub enum Handle { 50 | Owned(OwnedHandle), 51 | Borrowed(BorrowedHandle<'static>), 52 | } 53 | 54 | impl AsRawHandle for Handle { 55 | fn as_raw_handle(&self) -> RawHandle { 56 | match self { 57 | Self::Owned(handle) => handle.as_raw_handle(), 58 | Self::Borrowed(handle) => handle.as_raw_handle(), 59 | } 60 | } 61 | } 62 | 63 | impl Handle { 64 | pub fn stdin() -> Self { 65 | let stdin = io::stdin().as_raw_handle(); 66 | Self::Borrowed(unsafe { BorrowedHandle::borrow_raw(stdin) }) 67 | } 68 | 69 | pub fn stdout() -> Self { 70 | let stdout = io::stdout().as_raw_handle(); 71 | Self::Borrowed(unsafe { BorrowedHandle::borrow_raw(stdout) }) 72 | } 73 | 74 | pub fn try_clone(&self) -> io::Result { 75 | let this = match self { 76 | Self::Owned(handle) => Self::Owned(handle.try_clone()?), 77 | Self::Borrowed(handle) => Self::Borrowed(*handle), 78 | }; 79 | Ok(this) 80 | } 81 | } 82 | 83 | impl From for Handle { 84 | fn from(file: File) -> Self { 85 | Self::Owned(OwnedHandle::from(file)) 86 | } 87 | } 88 | 89 | #[derive(Debug)] 90 | pub(crate) struct InputHandle { 91 | handle: Handle, 92 | } 93 | 94 | impl InputHandle { 95 | fn new(handle: Handle) -> Self { 96 | Self { handle } 97 | } 98 | 99 | fn try_clone(&self) -> io::Result { 100 | Ok(Self { 101 | handle: self.handle.try_clone()?, 102 | }) 103 | } 104 | 105 | fn get_mode(&self) -> io::Result { 106 | let mut mode = 0; 107 | if unsafe { GetConsoleMode(self.as_raw_handle(), &mut mode) } == 0 { 108 | bail!( 109 | "failed to get input console mode: {}", 110 | io::Error::last_os_error() 111 | ); 112 | } 113 | Ok(mode) 114 | } 115 | 116 | fn set_mode(&mut self, mode: CONSOLE_MODE) -> io::Result<()> { 117 | if unsafe { SetConsoleMode(self.as_raw_handle(), mode) } == 0 { 118 | bail!( 119 | "failed to set input console mode: {}", 120 | io::Error::last_os_error() 121 | ); 122 | } 123 | 124 | Ok(()) 125 | } 126 | 127 | fn get_code_page(&self) -> io::Result { 128 | let cp = unsafe { GetConsoleCP() }; 129 | if cp == 0 { 130 | bail!( 131 | "failed to get input console codepage ID: {}", 132 | io::Error::last_os_error() 133 | ); 134 | } 135 | Ok(cp) 136 | } 137 | 138 | fn set_code_page(&mut self, cp: CodePageID) -> io::Result<()> { 139 | if unsafe { SetConsoleCP(cp) } == 0 { 140 | bail!( 141 | "failed to set input console codepage ID: {}", 142 | io::Error::last_os_error() 143 | ); 144 | } 145 | Ok(()) 146 | } 147 | 148 | pub fn get_number_of_input_events(&mut self) -> io::Result { 149 | let mut num = 0; 150 | if unsafe { GetNumberOfConsoleInputEvents(self.as_raw_handle(), &mut num) } == 0 { 151 | bail!( 152 | "failed to read input console number of pending events: {}", 153 | io::Error::last_os_error() 154 | ); 155 | } 156 | Ok(num as usize) 157 | } 158 | 159 | pub fn read_console_input(&mut self, num_events: usize) -> io::Result> { 160 | let mut res = Vec::with_capacity(num_events); 161 | let zeroed: INPUT_RECORD = unsafe { mem::zeroed() }; 162 | res.resize(num_events, zeroed); 163 | let mut num = 0; 164 | // NOTE: 165 | // > UTF-8 support in the console can be utilized via the A variant of Console APIs 166 | // > against console handles after setting the codepage to 65001 or CP_UTF8 with the 167 | // > SetConsoleOutputCP and SetConsoleCP methods, as appropriate. 168 | if unsafe { 169 | ReadConsoleInputA( 170 | self.as_raw_handle(), 171 | res.as_mut_ptr(), 172 | num_events as u32, 173 | &mut num, 174 | ) 175 | } == 0 176 | { 177 | bail!( 178 | "failed to read console input events: {}", 179 | io::Error::last_os_error() 180 | ); 181 | } 182 | unsafe { res.set_len(num as usize) }; 183 | Ok(res) 184 | } 185 | } 186 | 187 | impl AsRawHandle for InputHandle { 188 | fn as_raw_handle(&self) -> RawHandle { 189 | self.handle.as_raw_handle() 190 | } 191 | } 192 | 193 | #[derive(Debug)] 194 | pub struct OutputHandle { 195 | handle: Handle, 196 | } 197 | 198 | impl OutputHandle { 199 | fn new(handle: Handle) -> Self { 200 | Self { handle } 201 | } 202 | 203 | fn get_mode(&self) -> io::Result { 204 | let mut mode = 0; 205 | if unsafe { GetConsoleMode(self.as_raw_handle(), &mut mode) } == 0 { 206 | bail!( 207 | "failed to get output console mode: {}", 208 | io::Error::last_os_error() 209 | ); 210 | } 211 | Ok(mode) 212 | } 213 | 214 | fn set_mode(&mut self, mode: CONSOLE_MODE) -> io::Result<()> { 215 | if unsafe { SetConsoleMode(self.as_raw_handle(), mode) } == 0 { 216 | bail!( 217 | "failed to set output console mode: {}", 218 | io::Error::last_os_error() 219 | ); 220 | } 221 | 222 | Ok(()) 223 | } 224 | 225 | fn get_code_page(&self) -> io::Result { 226 | let cp = unsafe { GetConsoleOutputCP() }; 227 | if cp == 0 { 228 | bail!( 229 | "failed to get output console codepage ID: {}", 230 | io::Error::last_os_error() 231 | ); 232 | } 233 | Ok(cp) 234 | } 235 | 236 | fn set_code_page(&mut self, cp: CodePageID) -> io::Result<()> { 237 | if unsafe { SetConsoleOutputCP(cp) } == 0 { 238 | bail!( 239 | "failed to set output console codepage ID: {}", 240 | io::Error::last_os_error() 241 | ); 242 | } 243 | Ok(()) 244 | } 245 | 246 | fn get_dimensions(&self) -> io::Result { 247 | let mut info: CONSOLE_SCREEN_BUFFER_INFO = unsafe { mem::zeroed() }; 248 | if unsafe { GetConsoleScreenBufferInfo(self.as_raw_handle(), &mut info) } == 0 { 249 | bail!( 250 | "failed to get console screen buffer info: {}", 251 | io::Error::last_os_error() 252 | ); 253 | } 254 | let rows = OneBased::from_zero_based((info.srWindow.Bottom - info.srWindow.Top) as u16); 255 | let cols = OneBased::from_zero_based((info.srWindow.Right - info.srWindow.Left) as u16); 256 | Ok(WindowSize { 257 | rows: rows.get(), 258 | cols: cols.get(), 259 | pixel_width: None, 260 | pixel_height: None, 261 | }) 262 | } 263 | } 264 | 265 | impl AsRawHandle for OutputHandle { 266 | fn as_raw_handle(&self) -> RawHandle { 267 | self.handle.as_raw_handle() 268 | } 269 | } 270 | 271 | impl io::Write for OutputHandle { 272 | fn write(&mut self, buf: &[u8]) -> io::Result { 273 | let mut num_written = 0; 274 | if unsafe { 275 | WriteFile( 276 | self.as_raw_handle(), 277 | buf.as_ptr(), 278 | buf.len() as u32, 279 | &mut num_written, 280 | ptr::null_mut(), 281 | ) 282 | } == 0 283 | { 284 | Err(io::Error::last_os_error()) 285 | } else { 286 | Ok(num_written as usize) 287 | } 288 | } 289 | 290 | fn flush(&mut self) -> io::Result<()> { 291 | Ok(()) 292 | } 293 | } 294 | 295 | fn open_pty() -> io::Result<(InputHandle, OutputHandle)> { 296 | let input = if io::stdin().is_terminal() { 297 | Handle::stdin() 298 | } else { 299 | open_file("CONIN$")?.into() 300 | }; 301 | let output = if io::stdout().is_terminal() { 302 | Handle::stdout() 303 | } else { 304 | open_file("CONOUT$")?.into() 305 | }; 306 | Ok((InputHandle::new(input), OutputHandle::new(output))) 307 | } 308 | 309 | fn open_file(path: &str) -> io::Result { 310 | fs::OpenOptions::new().read(true).write(true).open(path) 311 | } 312 | 313 | // CREDIT: Again, like the UnixTerminal in the unix module this is mostly based on WezTerm but 314 | // only covers the parts not related to the event source. 315 | // 316 | // Also, the legacy Console API is not implemented. 317 | 318 | #[derive(Debug)] 319 | pub struct WindowsTerminal { 320 | input: InputHandle, 321 | output: BufWriter, 322 | reader: EventReader, 323 | original_input_mode: CONSOLE_MODE, 324 | original_output_mode: CONSOLE_MODE, 325 | original_input_cp: CodePageID, 326 | original_output_cp: CodePageID, 327 | has_panic_hook: bool, 328 | } 329 | 330 | impl WindowsTerminal { 331 | pub fn new() -> io::Result { 332 | let (mut input, mut output) = open_pty()?; 333 | 334 | let original_input_mode = input.get_mode()?; 335 | let original_output_mode = output.get_mode()?; 336 | let original_input_cp = input.get_code_page()?; 337 | let original_output_cp = output.get_code_page()?; 338 | input.set_code_page(CP_UTF8)?; 339 | output.set_code_page(CP_UTF8)?; 340 | 341 | // Enable VT processing for the output handle. 342 | let desired_output_mode = original_output_mode 343 | | Console::ENABLE_VIRTUAL_TERMINAL_PROCESSING 344 | | Console::DISABLE_NEWLINE_AUTO_RETURN; 345 | if output.set_mode(desired_output_mode).is_err() { 346 | bail!("virtual terminal processing could not be enabled for the output handle"); 347 | } 348 | // And now the input handle too. 349 | let desired_input_mode = original_input_mode | Console::ENABLE_VIRTUAL_TERMINAL_INPUT; 350 | if input.set_mode(desired_input_mode).is_err() { 351 | bail!("virtual terminal processing could not be enabled for the input handle"); 352 | } 353 | 354 | let reader = EventReader::new(WindowsEventSource::new(input.try_clone()?)?); 355 | 356 | Ok(Self { 357 | input, 358 | output: BufWriter::with_capacity(BUF_SIZE, output), 359 | reader, 360 | original_input_mode, 361 | original_output_mode, 362 | original_input_cp, 363 | original_output_cp, 364 | has_panic_hook: false, 365 | }) 366 | } 367 | } 368 | 369 | impl Terminal for WindowsTerminal { 370 | fn enter_raw_mode(&mut self) -> io::Result<()> { 371 | let mode = self.output.get_mut().get_mode()?; 372 | self.output 373 | .get_mut() 374 | .set_mode(mode | Console::DISABLE_NEWLINE_AUTO_RETURN) 375 | .ok(); 376 | let mode = self.input.get_mode()?; 377 | self.input.set_mode( 378 | (mode 379 | & !(Console::ENABLE_ECHO_INPUT 380 | | Console::ENABLE_LINE_INPUT 381 | | Console::ENABLE_PROCESSED_INPUT)) 382 | | Console::ENABLE_MOUSE_INPUT 383 | | Console::ENABLE_WINDOW_INPUT, 384 | )?; 385 | 386 | Ok(()) 387 | } 388 | 389 | fn enter_cooked_mode(&mut self) -> io::Result<()> { 390 | let mode = self.output.get_mut().get_mode()?; 391 | self.output 392 | .get_mut() 393 | .set_mode(mode & !Console::DISABLE_NEWLINE_AUTO_RETURN) 394 | .ok(); 395 | 396 | let mode = self.input.get_mode()?; 397 | self.input.set_mode( 398 | (mode & !(Console::ENABLE_MOUSE_INPUT | Console::ENABLE_WINDOW_INPUT)) 399 | | Console::ENABLE_ECHO_INPUT 400 | | Console::ENABLE_LINE_INPUT 401 | | Console::ENABLE_PROCESSED_INPUT, 402 | )?; 403 | Ok(()) 404 | } 405 | 406 | fn get_dimensions(&self) -> io::Result { 407 | // NOTE: setting dimensions should be done by VT instead of `SetConsoleScreenBufferInfo`. 408 | // 409 | self.output.get_ref().get_dimensions() 410 | } 411 | 412 | fn event_reader(&self) -> EventReader { 413 | self.reader.clone() 414 | } 415 | 416 | fn poll bool>( 417 | &self, 418 | filter: F, 419 | timeout: Option, 420 | ) -> io::Result { 421 | self.reader.poll(timeout, filter) 422 | } 423 | 424 | fn read bool>(&self, filter: F) -> io::Result { 425 | self.reader.read(filter) 426 | } 427 | 428 | fn set_panic_hook(&mut self, f: impl Fn(&mut OutputHandle) + Send + Sync + 'static) { 429 | let original_input_cp = self.original_input_cp; 430 | let original_input_mode = self.original_input_mode; 431 | let original_output_cp = self.original_output_cp; 432 | let original_output_mode = self.original_output_mode; 433 | let hook = std::panic::take_hook(); 434 | std::panic::set_hook(Box::new(move |info| { 435 | if let Ok((mut input, mut output)) = open_pty() { 436 | f(&mut output); 437 | let _ = input.set_code_page(original_input_cp); 438 | let _ = input.set_mode(original_input_mode); 439 | let _ = output.set_code_page(original_output_cp); 440 | let _ = output.set_mode(original_output_mode); 441 | } 442 | hook(info); 443 | })); 444 | self.has_panic_hook = true; 445 | } 446 | } 447 | 448 | impl Drop for WindowsTerminal { 449 | fn drop(&mut self) { 450 | if !self.has_panic_hook || !std::thread::panicking() { 451 | let _ = self.flush(); 452 | let _ = self.input.set_code_page(self.original_input_cp); 453 | let _ = self.output.get_mut().set_code_page(self.original_output_cp); 454 | let _ = self.input.set_mode(self.original_input_mode); 455 | let _ = self.output.get_mut().set_mode(self.original_output_mode); 456 | } 457 | } 458 | } 459 | 460 | impl io::Write for WindowsTerminal { 461 | fn write(&mut self, buf: &[u8]) -> io::Result { 462 | self.output.write(buf) 463 | } 464 | 465 | fn flush(&mut self) -> io::Result<()> { 466 | self.output.flush() 467 | } 468 | } 469 | --------------------------------------------------------------------------------