├── .github ├── CODEOWNERS ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ └── crossterm_test.yml ├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── Cargo.toml ├── LICENSE ├── README.md ├── docs ├── .gitignore ├── CONTRIBUTING.md ├── crossterm_c.png ├── crossterm_full.png ├── crossterm_full.svg └── know-problems.md ├── examples ├── README.md ├── copy-to-clipboard.rs ├── event-match-modifiers.rs ├── event-poll-read.rs ├── event-read-char-line.rs ├── event-read.rs ├── event-stream-async-std.rs ├── event-stream-tokio.rs ├── interactive-demo │ ├── Cargo.toml │ └── src │ │ ├── macros.rs │ │ ├── main.rs │ │ ├── test.rs │ │ └── test │ │ ├── attribute.rs │ │ ├── color.rs │ │ ├── cursor.rs │ │ ├── event.rs │ │ └── synchronized_output.rs ├── is_tty.rs ├── key-display.rs └── stderr.rs └── src ├── ansi_support.rs ├── clipboard.rs ├── command.rs ├── cursor.rs ├── cursor ├── sys.rs └── sys │ ├── unix.rs │ └── windows.rs ├── event.rs ├── event ├── filter.rs ├── read.rs ├── source.rs ├── source │ ├── unix.rs │ ├── unix │ │ ├── mio.rs │ │ └── tty.rs │ └── windows.rs ├── stream.rs ├── sys.rs ├── sys │ ├── unix.rs │ ├── unix │ │ ├── parse.rs │ │ ├── waker.rs │ │ └── waker │ │ │ ├── mio.rs │ │ │ └── tty.rs │ ├── windows.rs │ └── windows │ │ ├── parse.rs │ │ ├── poll.rs │ │ └── waker.rs └── timeout.rs ├── lib.rs ├── macros.rs ├── style.rs ├── style ├── attributes.rs ├── content_style.rs ├── styled_content.rs ├── stylize.rs ├── sys.rs ├── sys │ └── windows.rs ├── types.rs └── types │ ├── attribute.rs │ ├── color.rs │ ├── colored.rs │ └── colors.rs ├── terminal.rs ├── terminal ├── sys.rs └── sys │ ├── file_descriptor.rs │ ├── unix.rs │ └── windows.rs └── tty.rs /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @TimonPost 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **OS** 24 | - e.g. MacOs, Windows, Linux, (version) etc... 25 | 26 | **Terminal/Console** 27 | - e.g. ConHost/xterm/iterm2/Windows Terminal etc. 28 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered if any** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Anything else? 21 | -------------------------------------------------------------------------------- /.github/workflows/crossterm_test.yml: -------------------------------------------------------------------------------- 1 | name: Crossterm Test 2 | 3 | on: 4 | # Build master branch only 5 | push: 6 | branches: 7 | - master 8 | # Build pull requests targeting master branch only 9 | pull_request: 10 | branches: 11 | - master 12 | 13 | jobs: 14 | test: 15 | name: ${{matrix.rust}} on ${{ matrix.os }} 16 | runs-on: ${{ matrix.os }} 17 | strategy: 18 | matrix: 19 | os: [ubuntu-latest, windows-2019, macOS-latest] 20 | rust: [stable, nightly] 21 | # Allow failures on nightly, it's just informative 22 | include: 23 | - rust: stable 24 | can-fail: false 25 | - rust: nightly 26 | can-fail: true 27 | steps: 28 | - name: Checkout Repository 29 | uses: actions/checkout@v1 30 | with: 31 | fetch-depth: 1 32 | - name: Install Rust 33 | uses: hecrj/setup-rust-action@master 34 | with: 35 | rust-version: ${{ matrix.rust }} 36 | components: rustfmt,clippy 37 | - name: Toolchain Information 38 | run: | 39 | rustc --version 40 | rustfmt --version 41 | rustup --version 42 | cargo --version 43 | - name: Check Formatting 44 | if: matrix.rust == 'stable' 45 | run: cargo fmt --all -- --check 46 | continue-on-error: ${{ matrix.can-fail }} 47 | - name: Clippy 48 | run: cargo clippy --all-features -- -D clippy::all 49 | continue-on-error: ${{ matrix.can-fail }} 50 | - name: Test Build 51 | run: cargo build 52 | continue-on-error: ${{ matrix.can-fail }} 53 | - name: Test default features 54 | run: cargo test --lib -- --nocapture --test-threads 1 55 | continue-on-error: ${{ matrix.can-fail }} 56 | - name: Test serde feature 57 | run: cargo test --lib --features serde -- --nocapture --test-threads 1 58 | continue-on-error: ${{ matrix.can-fail }} 59 | - name: Test event-stream feature 60 | run: cargo test --lib --features "event-stream,events" -- --nocapture --test-threads 1 61 | continue-on-error: ${{ matrix.can-fail }} 62 | - name: Test all features 63 | run: cargo test --all-features -- --nocapture --test-threads 1 64 | continue-on-error: ${{ matrix.can-fail }} 65 | - name: Test no default features 66 | if: matrix.os != 'windows-2019' 67 | run: cargo test --no-default-features -- --nocapture --test-threads 1 68 | continue-on-error: ${{ matrix.can-fail }} 69 | - name: Test no default features with use-dev-tty feature enabled 70 | if: matrix.os != 'windows-2019' 71 | run: cargo test --no-default-features --features "use-dev-tty events event-stream bracketed-paste" -- --nocapture --test-threads 1 72 | continue-on-error: ${{ matrix.can-fail }} 73 | - name: Test no default features with windows feature enabled 74 | if: matrix.os == 'windows-2019' 75 | run: cargo test --no-default-features --features "windows" -- --nocapture --test-threads 1 76 | continue-on-error: ${{ matrix.can-fail }} 77 | - name: Test Packaging 78 | if: matrix.rust == 'stable' 79 | run: cargo package 80 | continue-on-error: ${{ matrix.can-fail }} 81 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | **/target/ 2 | **/.idea/ 3 | **/.vscode/ 4 | **/*.rs.bk 5 | **/Cargo.lock 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # Build only pushed (merged) master or any pull request. This avoids the 2 | # pull request to be build twice. 3 | branches: 4 | only: 5 | - master 6 | 7 | language: rust 8 | 9 | rust: 10 | - stable 11 | - nightly 12 | 13 | os: 14 | - linux 15 | - windows 16 | - osx 17 | 18 | git: 19 | depth: 1 20 | quiet: true 21 | 22 | matrix: 23 | allow_failures: 24 | - rust: nightly 25 | 26 | before_script: 27 | - export PATH=$PATH:/home/travis/.cargo/bin 28 | - rustup component add rustfmt 29 | - rustup component add clippy 30 | 31 | script: 32 | - cargo fmt --version 33 | - rustup --version 34 | - rustc --version 35 | - if [ "$TRAVIS_RUST_VERSION" = "stable" ]; then cargo fmt --all -- --check; fi 36 | - cargo clippy -- -D clippy::all 37 | - cargo build 38 | - cargo test --lib -- --nocapture --test-threads 1 39 | - cargo test --lib --features serde -- --nocapture --test-threads 1 40 | - cargo test --lib --features event-stream -- --nocapture --test-threads 1 41 | - cargo test --all-features -- --nocapture --test-threads 1 42 | - if [ "$TRAVIS_RUST_VERSION" = "stable" ]; then cargo package; fi 43 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "crossterm" 3 | version = "0.29.0" 4 | authors = ["T. Post"] 5 | description = "A crossplatform terminal library for manipulating terminals." 6 | repository = "https://github.com/crossterm-rs/crossterm" 7 | documentation = "https://docs.rs/crossterm/" 8 | license = "MIT" 9 | keywords = ["event", "color", "cli", "input", "terminal"] 10 | exclude = ["target", "Cargo.lock"] 11 | readme = "README.md" 12 | edition = "2021" 13 | rust-version = "1.63.0" 14 | categories = ["command-line-interface", "command-line-utilities"] 15 | 16 | [lib] 17 | name = "crossterm" 18 | path = "src/lib.rs" 19 | 20 | [package.metadata.docs.rs] 21 | all-features = true 22 | 23 | [features] 24 | default = ["bracketed-paste", "derive-more", "events", "windows"] 25 | 26 | #! ### Default features 27 | ## Enables triggering [`Event::Paste`](event::Event::Paste) when pasting text into the terminal. 28 | bracketed-paste = [] 29 | 30 | ## Enables reading input/events from the system using the [`event`] module. 31 | events = ["dep:mio", "dep:signal-hook", "dep:signal-hook-mio"] 32 | 33 | ## Enables windows specific crates. 34 | windows = ["dep:winapi", "dep:crossterm_winapi"] 35 | 36 | ## Enables `is_*` helper functions for event enums. 37 | derive-more = ["dep:derive_more"] 38 | 39 | #! ### Optional Features 40 | 41 | ## Enables documentation for crate features. 42 | document-features = ["dep:document-features"] 43 | 44 | ## Enables the [EventStream](event::EventStream) struct for async event reading. 45 | event-stream = ["dep:futures-core", "events"] 46 | 47 | ## Enables [`serde`] for various types. 48 | serde = ["dep:serde", "bitflags/serde"] 49 | 50 | ## Enables raw file descriptor polling / selecting instead of mio. 51 | use-dev-tty = ["filedescriptor", "rustix/process"] 52 | 53 | ## Enables interacting with a host clipboard via [`clipboard`](clipboard/index.html) 54 | osc52 = ["dep:base64"] 55 | 56 | [dependencies] 57 | base64 = { version = "0.22", optional = true } 58 | bitflags = { version = "2.9" } 59 | derive_more = { version = "2.0.0", features = ["is_variant"], optional = true } 60 | document-features = { version = "0.2.11", optional = true } 61 | futures-core = { version = "0.3", optional = true, default-features = false } 62 | parking_lot = "0.12" 63 | serde = { version = "1.0", features = ["derive"], optional = true } 64 | 65 | # Windows dependencies 66 | [target.'cfg(windows)'.dependencies] 67 | crossterm_winapi = { version = "0.9.1", optional = true } 68 | winapi = { version = "0.3.9", optional = true, features = ["winuser", "winerror"] } 69 | 70 | # UNIX dependencies 71 | [target.'cfg(unix)'.dependencies] 72 | filedescriptor = { version = "0.8", optional = true } 73 | # Default to using rustix for UNIX systems, but provide an option to use libc for backwards 74 | # compatibility. 75 | libc = { version = "0.2", default-features = false, optional = true } 76 | mio = { version = "1.0", features = ["os-poll"], optional = true } 77 | rustix = { version = "1", default-features = false, features = ["std", "stdio", "termios"] } 78 | signal-hook = { version = "0.3.17", optional = true } 79 | signal-hook-mio = { version = "0.2.4", features = ["support-v1_0"], optional = true } 80 | 81 | [dev-dependencies] 82 | async-std = "1.13" 83 | futures = "0.3" 84 | futures-timer = "3.0" 85 | serde_json = "1.0" 86 | serial_test = "3.0.0" 87 | temp-env = "0.3.6" 88 | tokio = { version = "1.44", features = ["full"] } 89 | 90 | # Examples 91 | [[example]] 92 | name = "event-read" 93 | required-features = ["bracketed-paste", "events"] 94 | 95 | [[example]] 96 | name = "event-match-modifiers" 97 | required-features = ["bracketed-paste", "events"] 98 | 99 | [[example]] 100 | name = "event-poll-read" 101 | required-features = ["bracketed-paste", "events"] 102 | 103 | [[example]] 104 | name = "event-stream-async-std" 105 | required-features = ["event-stream", "events"] 106 | 107 | [[example]] 108 | name = "event-stream-tokio" 109 | required-features = ["event-stream", "events"] 110 | 111 | [[example]] 112 | name = "event-read-char-line" 113 | required-features = ["events"] 114 | 115 | [[example]] 116 | name = "stderr" 117 | required-features = ["events"] 118 | 119 | [[example]] 120 | name = "key-display" 121 | required-features = ["events"] 122 | 123 | [[example]] 124 | name = "copy-to-clipboard" 125 | required-features = ["osc52"] 126 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Timon 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | [![Donate](https://img.shields.io/badge/Donate-PayPal-green.svg)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=Z8QK6XU749JB2) ![Travis][s7] [![Latest Version][s1]][l1] [![MIT][s2]][l2] [![docs][s3]][l3] ![Lines of Code][s6] [![Join us on Discord][s5]][l5] 4 | 5 | # Cross-platform Terminal Manipulation Library 6 | 7 | Crossterm is a pure-rust, terminal manipulation library that makes it possible to write cross-platform text-based interfaces (see [features](#features)). It supports all UNIX and Windows terminals down to Windows 7 (not all terminals are tested, 8 | see [Tested Terminals](#tested-terminals) for more info). 9 | 10 | ## Table of Contents 11 | 12 | - [Cross-platform Terminal Manipulation Library](#cross-platform-terminal-manipulation-library) 13 | - [Table of Contents](#table-of-contents) 14 | - [Features](#features) 15 | - [Tested Terminals](#tested-terminals) 16 | - [Getting Started](#getting-started) 17 | - [Feature Flags](#feature-flags) 18 | - [Dependency Justification](#dependency-justification) 19 | - [Other Resources](#other-resources) 20 | - [Used By](#used-by) 21 | - [Contributing](#contributing) 22 | - [Authors](#authors) 23 | - [License](#license) 24 | 25 | ## Features 26 | 27 | - Cross-platform 28 | - Multi-threaded (send, sync) 29 | - Detailed documentation 30 | - Few dependencies 31 | - Full control over writing and flushing output buffer 32 | - Is tty 33 | - Cursor 34 | - Move the cursor N times (up, down, left, right) 35 | - Move to previous / next line 36 | - Move to column 37 | - Set/get the cursor position 38 | - Store the cursor position and restore to it later 39 | - Hide/show the cursor 40 | - Enable/disable cursor blinking (not all terminals do support this feature) 41 | - Styled output 42 | - Foreground color (16 base colors) 43 | - Background color (16 base colors) 44 | - 256 (ANSI) color support (Windows 10 and UNIX only) 45 | - RGB color support (Windows 10 and UNIX only) 46 | - Text attributes like bold, italic, underscore, crossed, etc 47 | - Terminal 48 | - Clear (all lines, current line, from cursor down and up, until new line) 49 | - Scroll up, down 50 | - Set/get the terminal size 51 | - Exit current process 52 | - Alternate screen 53 | - Raw screen 54 | - Set terminal title 55 | - Enable/disable line wrapping 56 | - Event 57 | - Input Events 58 | - Mouse Events (press, release, position, button, drag) 59 | - Terminal Resize Events 60 | - Advanced modifier (SHIFT | ALT | CTRL) support for both mouse and key events and 61 | - futures Stream (feature 'event-stream') 62 | - Poll/read API 63 | 64 | 67 | 68 | ### Tested Terminals 69 | 70 | - Console Host 71 | - Windows 10 (Pro) 72 | - Windows 8.1 (N) 73 | - Windows Terminal 74 | - Windows 10 x86_64 (Enterprise) 75 | - Windows 11 arm64 (Enterprise) 76 | - Ubuntu Desktop Terminal 77 | - Ubuntu 23.04 64-bit 78 | - Ubuntu 17.10 79 | - Pop!_OS ( Ubuntu ) 20.04 80 | - (Arch, Manjaro) KDE Konsole 81 | - (Arch, NixOS) Kitty 82 | - Linux Mint 83 | - (OpenSuse) Alacritty 84 | - (Chrome OS) Crostini 85 | - Apple 86 | - macOS Monterey 12.7.1 (Intel-Chip) 87 | - macOS Sonama 14.4 (M1 Max, Apple Silicon-Chip) 88 | 89 | This crate supports all UNIX terminals and Windows terminals down to Windows 7; however, not all of the 90 | terminals have been tested. If you have used this library for a terminal other than the above list without 91 | issues, then feel free to add it to the above list - I really would appreciate it! 92 | 93 | ## Getting Started 94 | _see the [examples directory](examples/) and [documentation](https://docs.rs/crossterm/) for more advanced examples._ 95 | 96 |
97 | 98 | Click to show Cargo.toml. 99 | 100 | 101 | ```toml 102 | [dependencies] 103 | crossterm = "0.27" 104 | ``` 105 | 106 |
107 |

108 | 109 | ```rust 110 | use std::io::{stdout, Write}; 111 | 112 | use crossterm::{ 113 | execute, 114 | style::{Color, Print, ResetColor, SetBackgroundColor, SetForegroundColor}, 115 | ExecutableCommand, 116 | event, 117 | }; 118 | 119 | fn main() -> std::io::Result<()> { 120 | // using the macro 121 | execute!( 122 | stdout(), 123 | SetForegroundColor(Color::Blue), 124 | SetBackgroundColor(Color::Red), 125 | Print("Styled text here."), 126 | ResetColor 127 | )?; 128 | 129 | // or using functions 130 | stdout() 131 | .execute(SetForegroundColor(Color::Blue))? 132 | .execute(SetBackgroundColor(Color::Red))? 133 | .execute(Print("Styled text here."))? 134 | .execute(ResetColor)?; 135 | 136 | Ok(()) 137 | } 138 | ``` 139 | 140 | Checkout this [list](https://docs.rs/crossterm/latest/crossterm/index.html#supported-commands) with all possible commands. 141 | 142 | ### Feature Flags 143 | 144 | ```toml 145 | [dependencies.crossterm] 146 | version = "0.27" 147 | features = ["event-stream"] 148 | ``` 149 | 150 | | Feature | Description | 151 | |:---------------|:---------------------------------------------| 152 | | `event-stream` | `futures::Stream` producing `Result`. | 153 | | `serde` | (De)serializing of events. | 154 | | `events` | Reading input/system events (enabled by default) | 155 | | `filedescriptor` | Use raw filedescriptor for all events rather then mio dependency | 156 | | `derive-more` | Adds `is_*` helper functions for event types | 157 | | `osc52` | Enables crossterm::clipboard | 158 | 159 | 160 | To use crossterm as a very thin layer you can disable the `events` feature or use `filedescriptor` feature. 161 | This can disable `mio` / `signal-hook` / `signal-hook-mio` dependencies. 162 | 163 | ### Dependency Justification 164 | 165 | | Dependency | Used for | Included | 166 | |:---------------|:---------------------------------------------------------------------------------|:--------------------------------------| 167 | | `bitflags` | `KeyModifiers`, those are differ based on input. | always | 168 | | `parking_lot` | locking `RwLock`s with a timeout, const mutexes. | always | 169 | | `libc` | UNIX terminal_size/raw modes/set_title and several other low level functionality. | optional (`events` feature), UNIX only | 170 | | `Mio` | event readiness polling, waking up poller | optional (`events` feature), UNIX only | 171 | | `signal-hook` | signal-hook is used to handle terminal resize SIGNAL with Mio. | optional (`events` feature),UNIX only | 172 | | `winapi` | Used for low-level windows system calls which ANSI codes can't replace | windows only | 173 | | `futures-core` | For async stream of events | only with `event-stream` feature flag | 174 | | `serde` | ***ser***ializing and ***de***serializing of events | only with `serde` feature flag | 175 | | `derive_more` | Adds `is_*` helper functions for event types | optional (`derive-more` feature), included by default | 176 | | `base64` | Encoding clipboard data for OSC52 sequences in crossterm::clipboard | only with `osc52` feature flag | 177 | 178 | ### Other Resources 179 | 180 | - [API documentation](https://docs.rs/crossterm/) 181 | - [Deprecated examples repository](https://github.com/crossterm-rs/examples) 182 | 183 | ## Used By 184 | 185 | - [Broot](https://dystroy.org/broot/) 186 | - [Cursive](https://github.com/gyscos/Cursive) 187 | - [Ratatui](https://github.com/ratatui/ratatui) 188 | - [Rust-sloth](https://github.com/ecumene/rust-sloth) 189 | - [Rusty-rain](https://github.com/cowboy8625/rusty-rain) 190 | 191 | ## Contributing 192 | 193 | We highly appreciate when anyone contributes to this crate. Before you do, please, 194 | read the [Contributing](docs/CONTRIBUTING.md) guidelines. 195 | 196 | ## Authors 197 | 198 | * **Timon Post** - *Project Owner & creator* 199 | 200 | ## License 201 | 202 | This project, `crossterm` and all its sub-crates: `crossterm_screen`, `crossterm_cursor`, `crossterm_style`, 203 | `crossterm_input`, `crossterm_terminal`, `crossterm_winapi`, `crossterm_utils` are licensed under the MIT 204 | License - see the [LICENSE](https://github.com/crossterm-rs/crossterm/blob/master/LICENSE) file for details. 205 | 206 | [s1]: https://img.shields.io/crates/v/crossterm.svg 207 | [l1]: https://crates.io/crates/crossterm 208 | 209 | [s2]: https://img.shields.io/badge/license-MIT-blue.svg 210 | [l2]: ./LICENSE 211 | 212 | [s3]: https://docs.rs/crossterm/badge.svg 213 | [l3]: https://docs.rs/crossterm/ 214 | 215 | [s3]: https://docs.rs/crossterm/badge.svg 216 | [l3]: https://docs.rs/crossterm/ 217 | 218 | [s5]: https://img.shields.io/discord/560857607196377088.svg?logo=discord 219 | [l5]: https://discord.gg/K4nyTDB 220 | 221 | [s6]: https://tokei.rs/b1/github/crossterm-rs/crossterm?category=code 222 | [s7]: https://travis-ci.org/crossterm-rs/crossterm.svg?branch=master 223 | -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | book 2 | -------------------------------------------------------------------------------- /docs/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | I would appreciate any contributions to this crate. However, some things are handy to know. 4 | 5 | ## Code Style 6 | 7 | ### Import Order 8 | 9 | All imports are semantically grouped and ordered. The order is: 10 | 11 | - standard library (`use std::...`) 12 | - external crates (`use rand::...`) 13 | - current crate (`use crate::...`) 14 | - parent module (`use super::..`) 15 | - current module (`use self::...`) 16 | - module declaration (`mod ...`) 17 | 18 | There must be an empty line between groups. An example: 19 | 20 | ```rust 21 | use crossterm_utils::{csi, write_cout, Result}; 22 | 23 | use crate::sys::{get_cursor_position, show_cursor}; 24 | 25 | use super::Cursor; 26 | ``` 27 | 28 | #### CLion Tips 29 | 30 | The CLion IDE does this for you (_Menu_ -> _Code_ -> _Optimize Imports_). Be aware that the CLion sorts 31 | imports in a group in a different way when compared to the `rustfmt`. It's effectively two steps operation 32 | to get proper grouping & sorting: 33 | 34 | * _Menu_ -> _Code_ -> _Optimize Imports_ - group & semantically order imports 35 | * `cargo fmt` - fix ordering within the group 36 | 37 | Second step can be automated via _CLion_ -> _Preferences_ -> 38 | _Languages & Frameworks_ -> _Rust_ -> _Rustfmt_ -> _Run rustfmt on save_. 39 | 40 | ### Max Line Length 41 | 42 | | Type | Max line length | 43 | |:---------------------|----------------:| 44 | | Code | 100 | 45 | | Comments in the code | 120 | 46 | | Documentation | 120 | 47 | 48 | 100 is the [`max_width`](https://github.com/rust-lang/rustfmt/blob/master/Configurations.md#max_width) 49 | default value. 50 | 51 | 120 is because of the GitHub. The editor & viewer width there is +- 123 characters. 52 | 53 | ### Warnings 54 | 55 | The code must be warning free. It's quite hard to find an error if the build logs are polluted with warnings. 56 | If you decide to silent a warning with (`#[allow(...)]`), please add a comment why it's required. 57 | 58 | Always consult the [Travis CI](https://travis-ci.org/crossterm-rs/crossterm/pull_requests) build logs. 59 | 60 | ### Forbidden Warnings 61 | 62 | Search for `#![deny(...)]` in the code: 63 | 64 | * `unused_must_use` 65 | * `unused_imports` 66 | -------------------------------------------------------------------------------- /docs/crossterm_c.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crossterm-rs/crossterm/17192310472778bf94566b027ac1be0abdd05a7c/docs/crossterm_c.png -------------------------------------------------------------------------------- /docs/crossterm_full.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crossterm-rs/crossterm/17192310472778bf94566b027ac1be0abdd05a7c/docs/crossterm_full.png -------------------------------------------------------------------------------- /docs/crossterm_full.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | Created by potrace 1.15, written by Peter Selinger 2001-2017 9 | 10 | 12 | 56 | 61 | 66 | 71 | 77 | 84 | 88 | 93 | 102 | 103 | 104 | -------------------------------------------------------------------------------- /docs/know-problems.md: -------------------------------------------------------------------------------- 1 | # Known Problems 2 | 3 | There are some problems I discovered during development. 4 | And I don't think it has to do anything with crossterm but it has to do with how terminals handle ANSI or WinApi. 5 | 6 | ## WinAPI 7 | 8 | - Power shell does not interpreter 'DarkYellow' and is instead using gray instead, cmd is working perfectly fine. 9 | - Power shell inserts an '\n' (enter) when the program starts, this enter is the one you pressed when running the command. 10 | - After the program ran, power shell will reset the background and foreground colors. 11 | 12 | ## UNIX-terminals 13 | 14 | The Arc and Manjaro KDE Konsole's are not seeming to resize the terminal instead they are resizing the buffer. 15 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | ![Lines of Code][s7] [![MIT][s2]][l2] [![Join us on Discord][s5]][l5] 2 | 3 | # Crossterm Examples 4 | 5 | The examples are compatible with the latest release. 6 | 7 | ## Structure 8 | 9 | ``` 10 | ├── examples 11 | │   └── interactive-test 12 | │   └── event-* 13 | │   └── stderr 14 | ``` 15 | | File Name | Description | Topics | 16 | |:----------------------------|:-------------------------------|:------------------------------------------| 17 | | `examples/interactive-test` | interactive, walk through, demo | cursor, style, event | 18 | | `event-*` | event reading demos | (async) event reading | 19 | | `stderr` | crossterm over stderr demo | raw mode, alternate screen, custom output | 20 | | `is_tty` | Is this instance a tty ? | tty | 21 | 22 | ## Run examples 23 | 24 | ```bash 25 | $ cargo run --example [file name] 26 | ``` 27 | 28 | To run the interactive-demo go into the folder `examples/interactive-demo` and run `cargo run`. 29 | 30 | ## License 31 | 32 | This project is licensed under the MIT License - see the [LICENSE.md](LICENSE) file for details. 33 | 34 | [s2]: https://img.shields.io/badge/license-MIT-blue.svg 35 | [l2]: LICENSE 36 | 37 | [s5]: https://img.shields.io/discord/560857607196377088.svg?logo=discord 38 | [l5]: https://discord.gg/K4nyTDB 39 | 40 | [s7]: https://travis-ci.org/crossterm-rs/examples.svg?branch=master 41 | -------------------------------------------------------------------------------- /examples/copy-to-clipboard.rs: -------------------------------------------------------------------------------- 1 | //! Demonstrates copying a string to clipboard 2 | //! 3 | //! This example uses OSC control sequence `Pr = 5 2` (See 4 | //! 5 | //! to copy data to the terminal host clipboard. 6 | //! 7 | //! This only works if it is enabled on the respective terminal emulator. If a terminal multiplexer 8 | //! is used, the multiplexer will likely need to support it, too. 9 | //! 10 | //! ```no_run 11 | //! cargo run --example copy-to-clipboard -- --clipboard "Some String" 12 | //! cargo run --example copy-to-clipboard -- --primary "Some String" 13 | //! cargo run --example copy-to-clipboard -- "Some String" 14 | //! ``` 15 | 16 | use std::io; 17 | 18 | use crossterm::clipboard; 19 | use crossterm::execute; 20 | 21 | fn main() -> io::Result<()> { 22 | let mut stdout = io::stdout(); 23 | let mut args = std::env::args(); 24 | args.next(); // Skip to first argument 25 | 26 | let default_text = String::from("Example text"); 27 | let (text, dest) = match args.next().as_deref() { 28 | Some("--clipboard") => ( 29 | args.next().unwrap_or(default_text), 30 | clipboard::ClipboardType::Clipboard, 31 | ), 32 | Some("--primary") => ( 33 | args.next().unwrap_or(default_text), 34 | clipboard::ClipboardType::Primary, 35 | ), 36 | Some(text) => (text.to_owned(), clipboard::ClipboardType::Clipboard), 37 | None => (default_text, clipboard::ClipboardType::Clipboard), 38 | }; 39 | execute!( 40 | stdout, 41 | clipboard::CopyToClipboard { 42 | content: text, 43 | destination: clipboard::ClipboardSelection(vec![dest]) 44 | } 45 | ) 46 | } 47 | -------------------------------------------------------------------------------- /examples/event-match-modifiers.rs: -------------------------------------------------------------------------------- 1 | //! Demonstrates how to match on modifiers like: Control, alt, shift. 2 | //! 3 | //! cargo run --example event-match-modifiers 4 | 5 | use crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers}; 6 | 7 | fn match_event(event: Event) { 8 | if let Some(key) = event.as_key_press_event() { 9 | match key { 10 | KeyEvent { 11 | modifiers: KeyModifiers::CONTROL, 12 | code, 13 | .. 14 | } => { 15 | println!("Control + {:?}", code); 16 | } 17 | KeyEvent { 18 | modifiers: KeyModifiers::SHIFT, 19 | code, 20 | .. 21 | } => { 22 | println!("Shift + {:?}", code); 23 | } 24 | KeyEvent { 25 | modifiers: KeyModifiers::ALT, 26 | code, 27 | .. 28 | } => { 29 | println!("Alt + {:?}", code); 30 | } 31 | 32 | // Match on multiple modifiers: 33 | KeyEvent { 34 | code, modifiers, .. 35 | } => { 36 | if modifiers == (KeyModifiers::ALT | KeyModifiers::SHIFT) { 37 | println!("Alt + Shift {:?}", code); 38 | } else { 39 | println!("({:?}) with key: {:?}", modifiers, code) 40 | } 41 | } 42 | } 43 | } 44 | } 45 | 46 | fn main() { 47 | match_event(Event::Key(KeyEvent::new( 48 | KeyCode::Char('z'), 49 | KeyModifiers::CONTROL, 50 | ))); 51 | match_event(Event::Key(KeyEvent::new( 52 | KeyCode::Left, 53 | KeyModifiers::SHIFT, 54 | ))); 55 | match_event(Event::Key(KeyEvent::new( 56 | KeyCode::Delete, 57 | KeyModifiers::ALT, 58 | ))); 59 | match_event(Event::Key(KeyEvent::new( 60 | KeyCode::Right, 61 | KeyModifiers::ALT | KeyModifiers::SHIFT, 62 | ))); 63 | match_event(Event::Key(KeyEvent::new( 64 | KeyCode::Home, 65 | KeyModifiers::ALT | KeyModifiers::CONTROL, 66 | ))); 67 | } 68 | -------------------------------------------------------------------------------- /examples/event-poll-read.rs: -------------------------------------------------------------------------------- 1 | //! Demonstrates how to match on modifiers like: Control, alt, shift. 2 | //! 3 | //! cargo run --example event-poll-read 4 | 5 | use std::{io, time::Duration}; 6 | 7 | use crossterm::{ 8 | cursor::position, 9 | event::{poll, read, DisableMouseCapture, EnableMouseCapture, Event, KeyCode}, 10 | execute, 11 | terminal::{disable_raw_mode, enable_raw_mode}, 12 | }; 13 | 14 | const HELP: &str = r#"Blocking poll() & non-blocking read() 15 | - Keyboard, mouse and terminal resize events enabled 16 | - Prints "." every second if there's no event 17 | - Hit "c" to print current cursor position 18 | - Use Esc to quit 19 | "#; 20 | 21 | fn print_events() -> io::Result<()> { 22 | loop { 23 | // Wait up to 1s for another event 24 | if poll(Duration::from_millis(1_000))? { 25 | // It's guaranteed that read() won't block if `poll` returns `Ok(true)` 26 | let event = read()?; 27 | 28 | println!("Event::{:?}\r", event); 29 | 30 | if event == Event::Key(KeyCode::Char('c').into()) { 31 | println!("Cursor position: {:?}\r", position()); 32 | } 33 | 34 | if event == Event::Key(KeyCode::Esc.into()) { 35 | break; 36 | } 37 | } else { 38 | // Timeout expired, no event for 1s 39 | println!(".\r"); 40 | } 41 | } 42 | 43 | Ok(()) 44 | } 45 | 46 | fn main() -> io::Result<()> { 47 | println!("{}", HELP); 48 | 49 | enable_raw_mode()?; 50 | 51 | let mut stdout = io::stdout(); 52 | execute!(stdout, EnableMouseCapture)?; 53 | 54 | if let Err(e) = print_events() { 55 | println!("Error: {:?}\r", e); 56 | } 57 | 58 | execute!(stdout, DisableMouseCapture)?; 59 | 60 | disable_raw_mode() 61 | } 62 | -------------------------------------------------------------------------------- /examples/event-read-char-line.rs: -------------------------------------------------------------------------------- 1 | //! Demonstrates how to block read characters or a full line. 2 | //! Just note that crossterm is not required to do this and can be done with `io::stdin()`. 3 | //! 4 | //! cargo run --example event-read-char-line 5 | 6 | use std::io; 7 | 8 | use crossterm::{ 9 | event::{self, Event, KeyCode, KeyEvent, KeyEventKind}, 10 | terminal, 11 | }; 12 | 13 | pub fn read_char() -> io::Result { 14 | loop { 15 | if let Event::Key(KeyEvent { 16 | code: KeyCode::Char(c), 17 | kind: KeyEventKind::Press, 18 | .. 19 | }) = event::read()? 20 | { 21 | return Ok(c); 22 | } 23 | } 24 | } 25 | 26 | pub fn read_line() -> io::Result { 27 | let mut line = String::new(); 28 | loop { 29 | if let Event::Key(KeyEvent { 30 | code, 31 | kind: KeyEventKind::Press, 32 | .. 33 | }) = event::read()? 34 | { 35 | match code { 36 | KeyCode::Enter => { 37 | break; 38 | } 39 | KeyCode::Char(c) => { 40 | line.push(c); 41 | } 42 | _ => {} 43 | } 44 | } 45 | } 46 | 47 | Ok(line) 48 | } 49 | 50 | fn main() -> io::Result<()> { 51 | terminal::enable_raw_mode()?; 52 | 53 | println!("read line:\r"); 54 | println!("{:?}\r", read_line()); 55 | println!("read char:\r"); 56 | println!("{:?}\r", read_char()); 57 | println!("read char again:\r"); 58 | println!("{:?}\r", read_char()); 59 | 60 | terminal::disable_raw_mode() 61 | } 62 | -------------------------------------------------------------------------------- /examples/event-read.rs: -------------------------------------------------------------------------------- 1 | //! Demonstrates how to block read events. 2 | //! 3 | //! cargo run --example event-read 4 | 5 | use std::io; 6 | 7 | use crossterm::event::{ 8 | poll, KeyboardEnhancementFlags, PopKeyboardEnhancementFlags, PushKeyboardEnhancementFlags, 9 | }; 10 | use crossterm::{ 11 | cursor::position, 12 | event::{ 13 | read, DisableBracketedPaste, DisableFocusChange, DisableMouseCapture, EnableBracketedPaste, 14 | EnableFocusChange, EnableMouseCapture, Event, KeyCode, 15 | }, 16 | execute, queue, 17 | terminal::{disable_raw_mode, enable_raw_mode}, 18 | }; 19 | use std::time::Duration; 20 | 21 | const HELP: &str = r#"Blocking read() 22 | - Keyboard, mouse, focus and terminal resize events enabled 23 | - Hit "c" to print current cursor position 24 | - Use Esc to quit 25 | "#; 26 | 27 | fn print_events() -> io::Result<()> { 28 | loop { 29 | // Blocking read 30 | let event = read()?; 31 | 32 | println!("Event: {:?}\r", event); 33 | 34 | if event == Event::Key(KeyCode::Char('c').into()) { 35 | println!("Cursor position: {:?}\r", position()); 36 | } 37 | 38 | if let Event::Resize(x, y) = event { 39 | let (original_size, new_size) = flush_resize_events((x, y)); 40 | println!("Resize from: {:?}, to: {:?}\r", original_size, new_size); 41 | } 42 | 43 | if event == Event::Key(KeyCode::Esc.into()) { 44 | break; 45 | } 46 | } 47 | 48 | Ok(()) 49 | } 50 | 51 | // Resize events can occur in batches. 52 | // With a simple loop they can be flushed. 53 | // This function will keep the first and last resize event. 54 | fn flush_resize_events(first_resize: (u16, u16)) -> ((u16, u16), (u16, u16)) { 55 | let mut last_resize = first_resize; 56 | while let Ok(true) = poll(Duration::from_millis(50)) { 57 | if let Ok(Event::Resize(x, y)) = read() { 58 | last_resize = (x, y); 59 | } 60 | } 61 | 62 | (first_resize, last_resize) 63 | } 64 | 65 | fn main() -> io::Result<()> { 66 | println!("{}", HELP); 67 | 68 | enable_raw_mode()?; 69 | 70 | let mut stdout = io::stdout(); 71 | 72 | let supports_keyboard_enhancement = matches!( 73 | crossterm::terminal::supports_keyboard_enhancement(), 74 | Ok(true) 75 | ); 76 | 77 | if supports_keyboard_enhancement { 78 | queue!( 79 | stdout, 80 | PushKeyboardEnhancementFlags( 81 | KeyboardEnhancementFlags::DISAMBIGUATE_ESCAPE_CODES 82 | | KeyboardEnhancementFlags::REPORT_ALL_KEYS_AS_ESCAPE_CODES 83 | | KeyboardEnhancementFlags::REPORT_ALTERNATE_KEYS 84 | | KeyboardEnhancementFlags::REPORT_EVENT_TYPES 85 | ) 86 | )?; 87 | } 88 | 89 | execute!( 90 | stdout, 91 | EnableBracketedPaste, 92 | EnableFocusChange, 93 | EnableMouseCapture, 94 | )?; 95 | 96 | if let Err(e) = print_events() { 97 | println!("Error: {:?}\r", e); 98 | } 99 | 100 | if supports_keyboard_enhancement { 101 | queue!(stdout, PopKeyboardEnhancementFlags)?; 102 | } 103 | 104 | execute!( 105 | stdout, 106 | DisableBracketedPaste, 107 | DisableFocusChange, 108 | DisableMouseCapture 109 | )?; 110 | 111 | disable_raw_mode() 112 | } 113 | -------------------------------------------------------------------------------- /examples/event-stream-async-std.rs: -------------------------------------------------------------------------------- 1 | //! Demonstrates how to read events asynchronously with async-std. 2 | //! 3 | //! cargo run --features="event-stream" --example event-stream-async-std 4 | 5 | use std::{io::stdout, time::Duration}; 6 | 7 | use futures::{future::FutureExt, select, StreamExt}; 8 | use futures_timer::Delay; 9 | 10 | use crossterm::{ 11 | cursor::position, 12 | event::{DisableMouseCapture, EnableMouseCapture, Event, EventStream, KeyCode}, 13 | execute, 14 | terminal::{disable_raw_mode, enable_raw_mode}, 15 | }; 16 | 17 | const HELP: &str = r#"EventStream based on futures_util::stream::Stream with async-std 18 | - Keyboard, mouse and terminal resize events enabled 19 | - Prints "." every second if there's no event 20 | - Hit "c" to print current cursor position 21 | - Use Esc to quit 22 | "#; 23 | 24 | async fn print_events() { 25 | let mut reader = EventStream::new(); 26 | 27 | loop { 28 | let mut delay = Delay::new(Duration::from_millis(1_000)).fuse(); 29 | let mut event = reader.next().fuse(); 30 | 31 | select! { 32 | _ = delay => { println!(".\r"); }, 33 | maybe_event = event => { 34 | match maybe_event { 35 | Some(Ok(event)) => { 36 | println!("Event::{:?}\r", event); 37 | 38 | if event == Event::Key(KeyCode::Char('c').into()) { 39 | println!("Cursor position: {:?}\r", position()); 40 | } 41 | 42 | if event == Event::Key(KeyCode::Esc.into()) { 43 | break; 44 | } 45 | } 46 | Some(Err(e)) => println!("Error: {:?}\r", e), 47 | None => break, 48 | } 49 | } 50 | }; 51 | } 52 | } 53 | 54 | fn main() -> std::io::Result<()> { 55 | println!("{}", HELP); 56 | 57 | enable_raw_mode()?; 58 | 59 | let mut stdout = stdout(); 60 | execute!(stdout, EnableMouseCapture)?; 61 | 62 | async_std::task::block_on(print_events()); 63 | 64 | execute!(stdout, DisableMouseCapture)?; 65 | 66 | disable_raw_mode() 67 | } 68 | -------------------------------------------------------------------------------- /examples/event-stream-tokio.rs: -------------------------------------------------------------------------------- 1 | //! Demonstrates how to read events asynchronously with tokio. 2 | //! 3 | //! cargo run --features="event-stream" --example event-stream-tokio 4 | 5 | use std::{io::stdout, time::Duration}; 6 | 7 | use futures::{future::FutureExt, select, StreamExt}; 8 | use futures_timer::Delay; 9 | 10 | use crossterm::{ 11 | cursor::position, 12 | event::{DisableMouseCapture, EnableMouseCapture, Event, EventStream, KeyCode}, 13 | execute, 14 | terminal::{disable_raw_mode, enable_raw_mode}, 15 | }; 16 | 17 | const HELP: &str = r#"EventStream based on futures_util::Stream with tokio 18 | - Keyboard, mouse and terminal resize events enabled 19 | - Prints "." every second if there's no event 20 | - Hit "c" to print current cursor position 21 | - Use Esc to quit 22 | "#; 23 | 24 | async fn print_events() { 25 | let mut reader = EventStream::new(); 26 | 27 | loop { 28 | let mut delay = Delay::new(Duration::from_millis(1_000)).fuse(); 29 | let mut event = reader.next().fuse(); 30 | 31 | select! { 32 | _ = delay => { println!(".\r"); }, 33 | maybe_event = event => { 34 | match maybe_event { 35 | Some(Ok(event)) => { 36 | println!("Event::{:?}\r", event); 37 | 38 | if event == Event::Key(KeyCode::Char('c').into()) { 39 | println!("Cursor position: {:?}\r", position()); 40 | } 41 | 42 | if event == Event::Key(KeyCode::Esc.into()) { 43 | break; 44 | } 45 | } 46 | Some(Err(e)) => println!("Error: {:?}\r", e), 47 | None => break, 48 | } 49 | } 50 | }; 51 | } 52 | } 53 | 54 | #[tokio::main] 55 | async fn main() -> std::io::Result<()> { 56 | println!("{}", HELP); 57 | 58 | enable_raw_mode()?; 59 | 60 | let mut stdout = stdout(); 61 | execute!(stdout, EnableMouseCapture)?; 62 | 63 | print_events().await; 64 | 65 | execute!(stdout, DisableMouseCapture)?; 66 | 67 | disable_raw_mode() 68 | } 69 | -------------------------------------------------------------------------------- /examples/interactive-demo/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "interactive-demo" 3 | version = "0.0.1" 4 | authors = ["T. Post", "Robert Vojta "] 5 | edition = "2018" 6 | description = "Interactive demo for crossterm." 7 | license = "MIT" 8 | exclude = ["target", "Cargo.lock"] 9 | readme = "README.md" 10 | publish = false 11 | 12 | [dependencies] 13 | crossterm = { path = "../../" } -------------------------------------------------------------------------------- /examples/interactive-demo/src/macros.rs: -------------------------------------------------------------------------------- 1 | macro_rules! run_tests { 2 | ( 3 | $dst:expr, 4 | $( 5 | $testfn:ident 6 | ),* 7 | $(,)? 8 | ) => { 9 | use crossterm::{queue, style, terminal, cursor}; 10 | $( 11 | queue!( 12 | $dst, 13 | style::ResetColor, 14 | terminal::Clear(terminal::ClearType::All), 15 | cursor::MoveTo(1, 1), 16 | cursor::Show, 17 | cursor::EnableBlinking 18 | )?; 19 | 20 | $testfn($dst)?; 21 | 22 | match $crate::read_char() { 23 | Ok('q') => return Ok(()), 24 | Err(e) => return Err(e), 25 | _ => { }, 26 | }; 27 | )* 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /examples/interactive-demo/src/main.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::cognitive_complexity)] 2 | 3 | use std::io; 4 | 5 | use crossterm::event::KeyEventKind; 6 | pub use crossterm::{ 7 | cursor, 8 | event::{self, Event, KeyCode, KeyEvent}, 9 | execute, queue, style, 10 | terminal::{self, ClearType}, 11 | Command, 12 | }; 13 | 14 | #[macro_use] 15 | mod macros; 16 | mod test; 17 | 18 | const MENU: &str = r#"Crossterm interactive test 19 | 20 | Controls: 21 | 22 | - 'q' - quit interactive test (or return to this menu) 23 | - any other key - continue with next step 24 | 25 | Available tests: 26 | 27 | 1. cursor 28 | 2. color (foreground, background) 29 | 3. attributes (bold, italic, ...) 30 | 4. input 31 | 5. synchronized output 32 | 33 | Select test to run ('1', '2', ...) or hit 'q' to quit. 34 | "#; 35 | 36 | fn run(w: &mut W) -> io::Result<()> 37 | where 38 | W: io::Write, 39 | { 40 | execute!(w, terminal::EnterAlternateScreen)?; 41 | 42 | terminal::enable_raw_mode()?; 43 | 44 | loop { 45 | queue!( 46 | w, 47 | style::ResetColor, 48 | terminal::Clear(ClearType::All), 49 | cursor::Hide, 50 | cursor::MoveTo(1, 1) 51 | )?; 52 | 53 | for line in MENU.split('\n') { 54 | queue!(w, style::Print(line), cursor::MoveToNextLine(1))?; 55 | } 56 | 57 | w.flush()?; 58 | 59 | match read_char()? { 60 | '1' => test::cursor::run(w)?, 61 | '2' => test::color::run(w)?, 62 | '3' => test::attribute::run(w)?, 63 | '4' => test::event::run(w)?, 64 | '5' => test::synchronized_output::run(w)?, 65 | 'q' => { 66 | execute!(w, cursor::SetCursorStyle::DefaultUserShape).unwrap(); 67 | break; 68 | } 69 | _ => {} 70 | }; 71 | } 72 | 73 | execute!( 74 | w, 75 | style::ResetColor, 76 | cursor::Show, 77 | terminal::LeaveAlternateScreen 78 | )?; 79 | 80 | terminal::disable_raw_mode() 81 | } 82 | 83 | pub fn read_char() -> std::io::Result { 84 | loop { 85 | if let Ok(Event::Key(KeyEvent { 86 | code: KeyCode::Char(c), 87 | kind: KeyEventKind::Press, 88 | modifiers: _, 89 | state: _, 90 | })) = event::read() 91 | { 92 | return Ok(c); 93 | } 94 | } 95 | } 96 | 97 | pub fn buffer_size() -> io::Result<(u16, u16)> { 98 | terminal::size() 99 | } 100 | 101 | fn main() -> std::io::Result<()> { 102 | let mut stdout = io::stdout(); 103 | run(&mut stdout) 104 | } 105 | -------------------------------------------------------------------------------- /examples/interactive-demo/src/test.rs: -------------------------------------------------------------------------------- 1 | pub mod attribute; 2 | pub mod color; 3 | pub mod cursor; 4 | pub mod event; 5 | pub mod synchronized_output; 6 | -------------------------------------------------------------------------------- /examples/interactive-demo/src/test/attribute.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::cognitive_complexity)] 2 | 3 | use crossterm::{cursor, queue, style}; 4 | use std::io::Write; 5 | 6 | const ATTRIBUTES: [(style::Attribute, style::Attribute); 10] = [ 7 | (style::Attribute::Bold, style::Attribute::NormalIntensity), 8 | (style::Attribute::Italic, style::Attribute::NoItalic), 9 | (style::Attribute::Underlined, style::Attribute::NoUnderline), 10 | ( 11 | style::Attribute::DoubleUnderlined, 12 | style::Attribute::NoUnderline, 13 | ), 14 | (style::Attribute::Undercurled, style::Attribute::NoUnderline), 15 | (style::Attribute::Underdotted, style::Attribute::NoUnderline), 16 | (style::Attribute::Underdashed, style::Attribute::NoUnderline), 17 | (style::Attribute::Reverse, style::Attribute::NoReverse), 18 | ( 19 | style::Attribute::CrossedOut, 20 | style::Attribute::NotCrossedOut, 21 | ), 22 | (style::Attribute::SlowBlink, style::Attribute::NoBlink), 23 | ]; 24 | 25 | fn test_set_display_attributes(w: &mut W) -> std::io::Result<()> 26 | where 27 | W: Write, 28 | { 29 | queue!( 30 | w, 31 | style::Print("Display attributes"), 32 | cursor::MoveToNextLine(2) 33 | )?; 34 | 35 | for (on, off) in &ATTRIBUTES { 36 | queue!( 37 | w, 38 | style::SetAttribute(*on), 39 | style::Print(format!("{:>width$} ", format!("{:?}", on), width = 35)), 40 | style::SetAttribute(*off), 41 | style::Print(format!("{:>width$}", format!("{:?}", off), width = 35)), 42 | style::ResetColor, 43 | cursor::MoveToNextLine(1) 44 | )?; 45 | } 46 | 47 | w.flush()?; 48 | 49 | Ok(()) 50 | } 51 | 52 | pub fn run(w: &mut W) -> std::io::Result<()> 53 | where 54 | W: Write, 55 | { 56 | run_tests!(w, test_set_display_attributes,); 57 | Ok(()) 58 | } 59 | -------------------------------------------------------------------------------- /examples/interactive-demo/src/test/color.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::cognitive_complexity)] 2 | 3 | use crossterm::{cursor, queue, style, style::Color}; 4 | use std::io::Write; 5 | 6 | const COLORS: [Color; 21] = [ 7 | Color::Black, 8 | Color::DarkGrey, 9 | Color::Grey, 10 | Color::White, 11 | Color::DarkRed, 12 | Color::Red, 13 | Color::DarkGreen, 14 | Color::Green, 15 | Color::DarkYellow, 16 | Color::Yellow, 17 | Color::DarkBlue, 18 | Color::Blue, 19 | Color::DarkMagenta, 20 | Color::Magenta, 21 | Color::DarkCyan, 22 | Color::Cyan, 23 | Color::AnsiValue(0), 24 | Color::AnsiValue(15), 25 | Color::Rgb { r: 255, g: 0, b: 0 }, 26 | Color::Rgb { r: 0, g: 255, b: 0 }, 27 | Color::Rgb { r: 0, g: 0, b: 255 }, 28 | ]; 29 | 30 | fn test_set_foreground_color(w: &mut W) -> std::io::Result<()> 31 | where 32 | W: Write, 33 | { 34 | queue!( 35 | w, 36 | style::Print("Foreground colors on the black & white background"), 37 | cursor::MoveToNextLine(2) 38 | )?; 39 | 40 | for color in &COLORS { 41 | queue!( 42 | w, 43 | style::SetForegroundColor(*color), 44 | style::SetBackgroundColor(Color::Black), 45 | style::Print(format!( 46 | "{:>width$} ", 47 | format!("{:?} ████████████", color), 48 | width = 40 49 | )), 50 | style::SetBackgroundColor(Color::White), 51 | style::Print(format!( 52 | "{:>width$}", 53 | format!("{:?} ████████████", color), 54 | width = 40 55 | )), 56 | cursor::MoveToNextLine(1) 57 | )?; 58 | } 59 | 60 | w.flush()?; 61 | 62 | Ok(()) 63 | } 64 | 65 | fn test_set_background_color(w: &mut W) -> std::io::Result<()> 66 | where 67 | W: Write, 68 | { 69 | queue!( 70 | w, 71 | style::Print("Background colors with black & white foreground"), 72 | cursor::MoveToNextLine(2) 73 | )?; 74 | 75 | for color in &COLORS { 76 | queue!( 77 | w, 78 | style::SetBackgroundColor(*color), 79 | style::SetForegroundColor(Color::Black), 80 | style::Print(format!( 81 | "{:>width$} ", 82 | format!("{:?} ▒▒▒▒▒▒▒▒▒▒▒▒", color), 83 | width = 40 84 | )), 85 | style::SetForegroundColor(Color::White), 86 | style::Print(format!( 87 | "{:>width$}", 88 | format!("{:?} ▒▒▒▒▒▒▒▒▒▒▒▒", color), 89 | width = 40 90 | )), 91 | cursor::MoveToNextLine(1) 92 | )?; 93 | } 94 | 95 | w.flush()?; 96 | 97 | Ok(()) 98 | } 99 | 100 | fn test_color_values_matrix_16x16(w: &mut W, title: &str, color: F) -> std::io::Result<()> 101 | where 102 | W: Write, 103 | F: Fn(u16, u16) -> Color, 104 | { 105 | queue!(w, style::Print(title))?; 106 | 107 | for idx in 0..=15 { 108 | queue!( 109 | w, 110 | cursor::MoveTo(1, idx + 4), 111 | style::Print(format!("{:>width$}", idx, width = 2)) 112 | )?; 113 | queue!( 114 | w, 115 | cursor::MoveTo(idx * 3 + 3, 3), 116 | style::Print(format!("{:>width$}", idx, width = 3)) 117 | )?; 118 | } 119 | 120 | for row in 0..=15u16 { 121 | queue!(w, cursor::MoveTo(4, row + 4))?; 122 | for col in 0..=15u16 { 123 | queue!( 124 | w, 125 | style::SetForegroundColor(color(col, row)), 126 | style::Print("███") 127 | )?; 128 | } 129 | queue!( 130 | w, 131 | style::SetForegroundColor(Color::White), 132 | style::Print(format!("{:>width$} ..= ", row * 16, width = 3)), 133 | style::Print(format!("{:>width$}", row * 16 + 15, width = 3)) 134 | )?; 135 | } 136 | 137 | w.flush()?; 138 | 139 | Ok(()) 140 | } 141 | 142 | fn test_color_ansi_values(w: &mut W) -> std::io::Result<()> 143 | where 144 | W: Write, 145 | { 146 | test_color_values_matrix_16x16(w, "Color::Ansi values", |col, row| { 147 | Color::AnsiValue((row * 16 + col) as u8) 148 | }) 149 | } 150 | 151 | fn test_rgb_red_values(w: &mut W) -> std::io::Result<()> 152 | where 153 | W: Write, 154 | { 155 | test_color_values_matrix_16x16(w, "Color::Rgb red values", |col, row| Color::Rgb { 156 | r: (row * 16 + col) as u8, 157 | g: 0_u8, 158 | b: 0, 159 | }) 160 | } 161 | 162 | fn test_rgb_green_values(w: &mut W) -> std::io::Result<()> 163 | where 164 | W: Write, 165 | { 166 | test_color_values_matrix_16x16(w, "Color::Rgb green values", |col, row| Color::Rgb { 167 | r: 0, 168 | g: (row * 16 + col) as u8, 169 | b: 0, 170 | }) 171 | } 172 | 173 | fn test_rgb_blue_values(w: &mut W) -> std::io::Result<()> 174 | where 175 | W: Write, 176 | { 177 | test_color_values_matrix_16x16(w, "Color::Rgb blue values", |col, row| Color::Rgb { 178 | r: 0, 179 | g: 0, 180 | b: (row * 16 + col) as u8, 181 | }) 182 | } 183 | 184 | pub fn run(w: &mut W) -> std::io::Result<()> 185 | where 186 | W: Write, 187 | { 188 | run_tests!( 189 | w, 190 | test_set_foreground_color, 191 | test_set_background_color, 192 | test_color_ansi_values, 193 | test_rgb_red_values, 194 | test_rgb_green_values, 195 | test_rgb_blue_values, 196 | ); 197 | Ok(()) 198 | } 199 | -------------------------------------------------------------------------------- /examples/interactive-demo/src/test/cursor.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::cognitive_complexity)] 2 | 3 | use std::io::Write; 4 | 5 | use crossterm::{cursor, execute, queue, style, style::Stylize, Command}; 6 | use std::thread; 7 | use std::time::Duration; 8 | 9 | fn test_move_cursor_up(w: &mut W) -> std::io::Result<()> 10 | where 11 | W: Write, 12 | { 13 | draw_cursor_box(w, "Move Up (2)", |_, _| cursor::MoveUp(2)) 14 | } 15 | 16 | fn test_move_cursor_down(w: &mut W) -> std::io::Result<()> 17 | where 18 | W: Write, 19 | { 20 | draw_cursor_box(w, "Move Down (2)", |_, _| cursor::MoveDown(2)) 21 | } 22 | 23 | fn test_move_cursor_left(w: &mut W) -> std::io::Result<()> 24 | where 25 | W: Write, 26 | { 27 | draw_cursor_box(w, "Move Left (2)", |_, _| cursor::MoveLeft(2)) 28 | } 29 | 30 | fn test_move_cursor_right(w: &mut W) -> std::io::Result<()> 31 | where 32 | W: Write, 33 | { 34 | draw_cursor_box(w, "Move Right (2)", |_, _| cursor::MoveRight(2)) 35 | } 36 | 37 | fn test_move_cursor_to_previous_line(w: &mut W) -> std::io::Result<()> 38 | where 39 | W: Write, 40 | { 41 | draw_cursor_box(w, "MoveToPreviousLine (1)", |_, _| { 42 | cursor::MoveToPreviousLine(1) 43 | }) 44 | } 45 | 46 | fn test_move_cursor_to_next_line(w: &mut W) -> std::io::Result<()> 47 | where 48 | W: Write, 49 | { 50 | draw_cursor_box(w, "MoveToNextLine (1)", |_, _| cursor::MoveToNextLine(1)) 51 | } 52 | 53 | fn test_move_cursor_to_column(w: &mut W) -> std::io::Result<()> 54 | where 55 | W: Write, 56 | { 57 | draw_cursor_box(w, "MoveToColumn (1)", |center_x, _| { 58 | cursor::MoveToColumn(center_x + 1) 59 | }) 60 | } 61 | 62 | fn test_hide_cursor(w: &mut W) -> std::io::Result<()> 63 | where 64 | W: Write, 65 | { 66 | execute!(w, style::Print("HideCursor"), cursor::Hide) 67 | } 68 | 69 | fn test_show_cursor(w: &mut W) -> std::io::Result<()> 70 | where 71 | W: Write, 72 | { 73 | execute!(w, style::Print("ShowCursor"), cursor::Show) 74 | } 75 | 76 | fn test_cursor_blinking_block(w: &mut W) -> std::io::Result<()> 77 | where 78 | W: Write, 79 | { 80 | execute!( 81 | w, 82 | style::Print("Blinking Block:"), 83 | cursor::MoveLeft(2), 84 | cursor::SetCursorStyle::BlinkingBlock, 85 | ) 86 | } 87 | 88 | fn test_cursor_blinking_underscore(w: &mut W) -> std::io::Result<()> 89 | where 90 | W: Write, 91 | { 92 | execute!( 93 | w, 94 | style::Print("Blinking Underscore:"), 95 | cursor::MoveLeft(2), 96 | cursor::SetCursorStyle::BlinkingUnderScore, 97 | ) 98 | } 99 | 100 | fn test_cursor_blinking_bar(w: &mut W) -> std::io::Result<()> 101 | where 102 | W: Write, 103 | { 104 | execute!( 105 | w, 106 | style::Print("Blinking bar:"), 107 | cursor::MoveLeft(2), 108 | cursor::SetCursorStyle::BlinkingBar, 109 | ) 110 | } 111 | 112 | fn test_move_cursor_to(w: &mut W) -> std::io::Result<()> 113 | where 114 | W: Write, 115 | { 116 | draw_cursor_box( 117 | w, 118 | "MoveTo (x: 1, y: 1) removed from center", 119 | |center_x, center_y| cursor::MoveTo(center_x + 1, center_y + 1), 120 | ) 121 | } 122 | 123 | fn test_save_restore_cursor_position(w: &mut W) -> std::io::Result<()> 124 | where 125 | W: Write, 126 | { 127 | execute!(w, 128 | cursor::MoveTo(0, 0), 129 | style::Print("Save position, print character elsewhere, after three seconds restore to old position."), 130 | cursor::MoveToNextLine(2), 131 | style::Print("Save ->[ ]<- Position"), 132 | cursor::MoveTo(8, 2), 133 | cursor::SavePosition, 134 | cursor::MoveTo(10,10), 135 | style::Print("Move To ->[√]<- Position") 136 | )?; 137 | 138 | thread::sleep(Duration::from_secs(3)); 139 | 140 | execute!(w, cursor::RestorePosition, style::Print("√")) 141 | } 142 | 143 | /// Draws a box with an colored center, this center can be taken as a reference point after running the given cursor command. 144 | fn draw_cursor_box(w: &mut W, description: &str, cursor_command: F) -> std::io::Result<()> 145 | where 146 | W: Write, 147 | F: Fn(u16, u16) -> T, 148 | T: Command, 149 | { 150 | execute!( 151 | w, 152 | cursor::Hide, 153 | cursor::MoveTo(0, 0), 154 | style::SetForegroundColor(style::Color::Red), 155 | style::Print(format!( 156 | "Red box is the center. After the action: '{}' '√' is drawn to reflect the action from the center.", 157 | description 158 | )) 159 | )?; 160 | 161 | let start_y = 2; 162 | let width = 21; 163 | let height = 11 + start_y; 164 | let center_x = width / 2; 165 | let center_y = (height + start_y) / 2; 166 | 167 | for row in start_y..=10 + start_y { 168 | for column in 0..=width { 169 | if (row == start_y || row == height - 1) || (column == 0 || column == width) { 170 | queue!( 171 | w, 172 | cursor::MoveTo(column, row), 173 | style::PrintStyledContent("▓".red()), 174 | )?; 175 | } else { 176 | queue!( 177 | w, 178 | cursor::MoveTo(column, row), 179 | style::PrintStyledContent("_".red().on_white()) 180 | )?; 181 | } 182 | } 183 | } 184 | 185 | queue!( 186 | w, 187 | cursor::MoveTo(center_x, center_y), 188 | style::PrintStyledContent("▀".red().on_white()), 189 | cursor::MoveTo(center_x, center_y), 190 | )?; 191 | queue!( 192 | w, 193 | cursor_command(center_x, center_y), 194 | style::PrintStyledContent("√".magenta().on_white()) 195 | )?; 196 | w.flush()?; 197 | Ok(()) 198 | } 199 | 200 | pub fn run(w: &mut W) -> std::io::Result<()> 201 | where 202 | W: Write, 203 | { 204 | run_tests!( 205 | w, 206 | test_hide_cursor, 207 | test_show_cursor, 208 | test_cursor_blinking_bar, 209 | test_cursor_blinking_block, 210 | test_cursor_blinking_underscore, 211 | test_move_cursor_left, 212 | test_move_cursor_right, 213 | test_move_cursor_up, 214 | test_move_cursor_down, 215 | test_move_cursor_to, 216 | test_move_cursor_to_next_line, 217 | test_move_cursor_to_previous_line, 218 | test_move_cursor_to_column, 219 | test_save_restore_cursor_position 220 | ); 221 | Ok(()) 222 | } 223 | -------------------------------------------------------------------------------- /examples/interactive-demo/src/test/event.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::cognitive_complexity)] 2 | 3 | use crossterm::{ 4 | cursor::position, 5 | event::{read, DisableMouseCapture, EnableMouseCapture, Event, KeyCode}, 6 | execute, 7 | }; 8 | use std::io::{self, Write}; 9 | 10 | fn test_event(w: &mut W) -> io::Result<()> 11 | where 12 | W: io::Write, 13 | { 14 | execute!(w, EnableMouseCapture)?; 15 | 16 | loop { 17 | // Blocking read 18 | let event = read()?; 19 | 20 | println!("Event::{:?}\r", event); 21 | 22 | if event == Event::Key(KeyCode::Char('c').into()) { 23 | println!("Cursor position: {:?}\r", position()); 24 | } 25 | 26 | if event == Event::Key(KeyCode::Char('q').into()) { 27 | break; 28 | } 29 | } 30 | 31 | execute!(w, DisableMouseCapture)?; 32 | 33 | Ok(()) 34 | } 35 | 36 | pub fn run(w: &mut W) -> std::io::Result<()> 37 | where 38 | W: Write, 39 | { 40 | run_tests!(w, test_event); 41 | Ok(()) 42 | } 43 | -------------------------------------------------------------------------------- /examples/interactive-demo/src/test/synchronized_output.rs: -------------------------------------------------------------------------------- 1 | use std::io::Write; 2 | 3 | use crossterm::{cursor, execute, style::Print, SynchronizedUpdate}; 4 | 5 | fn render_slowly(w: &mut W) -> std::io::Result<()> 6 | where 7 | W: Write, 8 | { 9 | for i in 1..10 { 10 | execute!(w, Print(format!("{}", i)))?; 11 | std::thread::sleep(std::time::Duration::from_millis(50)); 12 | } 13 | Ok(()) 14 | } 15 | 16 | fn test_slow_rendering(w: &mut W) -> std::io::Result<()> 17 | where 18 | W: Write, 19 | { 20 | execute!(w, Print("Rendering without synchronized update:"))?; 21 | execute!(w, cursor::MoveToNextLine(1))?; 22 | std::thread::sleep(std::time::Duration::from_millis(50)); 23 | render_slowly(w)?; 24 | 25 | execute!(w, cursor::MoveToNextLine(1))?; 26 | execute!(w, Print("Rendering with synchronized update:"))?; 27 | execute!(w, cursor::MoveToNextLine(1))?; 28 | std::thread::sleep(std::time::Duration::from_millis(50)); 29 | w.sync_update(render_slowly)??; 30 | 31 | execute!(w, cursor::MoveToNextLine(1))?; 32 | Ok(()) 33 | } 34 | 35 | pub fn run(w: &mut W) -> std::io::Result<()> 36 | where 37 | W: Write, 38 | { 39 | run_tests!(w, test_slow_rendering,); 40 | Ok(()) 41 | } 42 | -------------------------------------------------------------------------------- /examples/is_tty.rs: -------------------------------------------------------------------------------- 1 | use crossterm::{ 2 | execute, 3 | terminal::{size, SetSize}, 4 | tty::IsTty, 5 | }; 6 | use std::io::{stdin, stdout}; 7 | 8 | pub fn main() { 9 | println!("size: {:?}", size().unwrap()); 10 | execute!(stdout(), SetSize(10, 10)).unwrap(); 11 | println!("resized: {:?}", size().unwrap()); 12 | 13 | if stdin().is_tty() { 14 | println!("Is TTY"); 15 | } else { 16 | println!("Is not TTY"); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /examples/key-display.rs: -------------------------------------------------------------------------------- 1 | //! Demonstrates the display format of key events. 2 | //! 3 | //! This example demonstrates the display format of key events, which is useful for displaying in 4 | //! the help section of a terminal application. 5 | //! 6 | //! cargo run --example key-display 7 | 8 | use std::io; 9 | 10 | use crossterm::event::KeyModifiers; 11 | use crossterm::{ 12 | event::{read, KeyCode}, 13 | terminal::{disable_raw_mode, enable_raw_mode}, 14 | }; 15 | 16 | const HELP: &str = r#"Key display 17 | - Press any key to see its display format 18 | - Use Esc to quit 19 | "#; 20 | 21 | fn main() -> io::Result<()> { 22 | println!("{}", HELP); 23 | enable_raw_mode()?; 24 | if let Err(e) = print_events() { 25 | println!("Error: {:?}\r", e); 26 | } 27 | disable_raw_mode()?; 28 | Ok(()) 29 | } 30 | 31 | fn print_events() -> io::Result<()> { 32 | while let Ok(event) = read() { 33 | let Some(event) = event.as_key_press_event() else { 34 | continue; 35 | }; 36 | let modifier = match event.modifiers { 37 | KeyModifiers::NONE => "".to_string(), 38 | _ => format!("{:}+", event.modifiers), 39 | }; 40 | println!("Key pressed: {modifier}{code}\r", code = event.code); 41 | if event.code == KeyCode::Esc { 42 | break; 43 | } 44 | } 45 | Ok(()) 46 | } 47 | -------------------------------------------------------------------------------- /examples/stderr.rs: -------------------------------------------------------------------------------- 1 | //! This shows how an application can write on stderr 2 | //! instead of stdout, thus making it possible to 3 | //! the command API instead of the "old style" direct 4 | //! unbuffered API. 5 | //! 6 | //! This particular example is only suited to Unix 7 | //! for now. 8 | //! 9 | //! cargo run --example stderr 10 | 11 | use std::io; 12 | 13 | use crossterm::{ 14 | cursor::{Hide, MoveTo, Show}, 15 | event, 16 | event::{Event, KeyCode, KeyEvent}, 17 | execute, queue, 18 | style::Print, 19 | terminal::{self, EnterAlternateScreen, LeaveAlternateScreen}, 20 | }; 21 | 22 | const TEXT: &str = r#" 23 | This screen is ran on stderr. 24 | And when you hit enter, it prints on stdout. 25 | This makes it possible to run an application and choose what will 26 | be sent to any application calling yours. 27 | 28 | For example, assuming you build this example with 29 | 30 | cargo build --bin stderr 31 | 32 | and then you run it with 33 | 34 | cd "$(target/debug/stderr)" 35 | 36 | what the application prints on stdout is used as argument to cd. 37 | 38 | Try it out. 39 | 40 | Hit any key to quit this screen: 41 | 42 | 1 will print `..` 43 | 2 will print `/` 44 | 3 will print `~` 45 | Any other key will print this text (so that you may copy-paste) 46 | "#; 47 | 48 | fn run_app(write: &mut W) -> io::Result 49 | where 50 | W: io::Write, 51 | { 52 | queue!( 53 | write, 54 | EnterAlternateScreen, // enter alternate screen 55 | Hide // hide the cursor 56 | )?; 57 | 58 | let mut y = 1; 59 | for line in TEXT.split('\n') { 60 | queue!(write, MoveTo(1, y), Print(line.to_string()))?; 61 | y += 1; 62 | } 63 | 64 | write.flush()?; 65 | 66 | terminal::enable_raw_mode()?; 67 | let user_char = read_char()?; // we wait for the user to hit a key 68 | execute!(write, Show, LeaveAlternateScreen)?; // restore the cursor and leave the alternate screen 69 | 70 | terminal::disable_raw_mode()?; 71 | 72 | Ok(user_char) 73 | } 74 | 75 | pub fn read_char() -> io::Result { 76 | loop { 77 | if let Event::Key(KeyEvent { 78 | code: KeyCode::Char(c), 79 | .. 80 | }) = event::read()? 81 | { 82 | return Ok(c); 83 | } 84 | } 85 | } 86 | 87 | // cargo run --example stderr 88 | fn main() { 89 | match run_app(&mut io::stderr()).unwrap() { 90 | '1' => print!(".."), 91 | '2' => print!("/"), 92 | '3' => print!("~"), 93 | _ => println!("{}", TEXT), 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/ansi_support.rs: -------------------------------------------------------------------------------- 1 | use std::sync::atomic::{AtomicBool, Ordering}; 2 | 3 | use crossterm_winapi::{ConsoleMode, Handle}; 4 | use parking_lot::Once; 5 | use winapi::um::wincon::ENABLE_VIRTUAL_TERMINAL_PROCESSING; 6 | 7 | /// Enable virtual terminal processing. 8 | /// 9 | /// This method attempts to enable virtual terminal processing for this 10 | /// console. If there was a problem enabling it, then an error returned. 11 | /// On success, the caller may assume that enabling it was successful. 12 | /// 13 | /// When virtual terminal processing is enabled, characters emitted to the 14 | /// console are parsed for VT100 and similar control character sequences 15 | /// that control color and other similar operations. 16 | fn enable_vt_processing() -> std::io::Result<()> { 17 | let mask = ENABLE_VIRTUAL_TERMINAL_PROCESSING; 18 | 19 | let console_mode = ConsoleMode::from(Handle::current_out_handle()?); 20 | let old_mode = console_mode.mode()?; 21 | 22 | if old_mode & mask == 0 { 23 | console_mode.set_mode(old_mode | mask)?; 24 | } 25 | 26 | Ok(()) 27 | } 28 | 29 | static SUPPORTS_ANSI_ESCAPE_CODES: AtomicBool = AtomicBool::new(false); 30 | static INITIALIZER: Once = Once::new(); 31 | 32 | /// Checks if the current terminal supports ANSI escape sequences 33 | pub fn supports_ansi() -> bool { 34 | INITIALIZER.call_once(|| { 35 | // Some terminals on Windows like GitBash can't use WinAPI calls directly 36 | // so when we try to enable the ANSI-flag for Windows this won't work. 37 | // Because of that we should check first if the TERM-variable is set 38 | // and see if the current terminal is a terminal who does support ANSI. 39 | let supported = enable_vt_processing().is_ok() 40 | || std::env::var("TERM").map_or(false, |term| term != "dumb"); 41 | 42 | SUPPORTS_ANSI_ESCAPE_CODES.store(supported, Ordering::SeqCst); 43 | }); 44 | 45 | SUPPORTS_ANSI_ESCAPE_CODES.load(Ordering::SeqCst) 46 | } 47 | -------------------------------------------------------------------------------- /src/clipboard.rs: -------------------------------------------------------------------------------- 1 | //! # Clipboard 2 | //! 3 | //! The `clipboard` module provides functionality to work with a host clipboard. 4 | //! 5 | //! ## Implemented operations: 6 | //! 7 | //! - Copy: [`CopyToClipboard`](struct.CopyToClipboard.html) 8 | use base64::prelude::{Engine, BASE64_STANDARD}; 9 | 10 | use std::fmt; 11 | use std::str::FromStr; 12 | 13 | use crate::{osc, Command}; 14 | 15 | /// Different clipboard types 16 | /// 17 | /// Some operating systems and desktop environments support multiple buffers 18 | /// for copy/cut/paste. Their details differ between operating systems. 19 | /// See 20 | /// for a detailed survey of supported types based on the X window system. 21 | #[derive(Debug, Clone, PartialEq, Eq)] 22 | pub enum ClipboardType { 23 | /// Default clipboard when using Ctrl+C or Ctrl+V 24 | Clipboard, 25 | 26 | /// Clipboard on Linux/X/Wayland when using selection and middle mouse button 27 | Primary, 28 | 29 | /// Other clipboard type not explicitly supported by crossterm 30 | /// 31 | /// See 32 | /// [XTerm Control Sequences](https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Operating-System-Commands) 33 | /// for potential values. 34 | /// 35 | /// Note that support for these in terminal emulators is very limited. 36 | Other(char), 37 | } 38 | 39 | impl From<&ClipboardType> for char { 40 | fn from(val: &ClipboardType) -> Self { 41 | match val { 42 | ClipboardType::Clipboard => 'c', 43 | ClipboardType::Primary => 'p', 44 | ClipboardType::Other(other) => *other, 45 | } 46 | } 47 | } 48 | 49 | impl From for ClipboardType { 50 | fn from(value: char) -> Self { 51 | match value { 52 | 'c' => ClipboardType::Clipboard, 53 | 'p' => ClipboardType::Primary, 54 | other => ClipboardType::Other(other), 55 | } 56 | } 57 | } 58 | 59 | /// A sequence of clipboard types 60 | #[derive(Debug, Clone, PartialEq, Eq)] 61 | pub struct ClipboardSelection( 62 | /// An ordered list of clipboards which will be the destination for the copied selection. 63 | /// 64 | /// Order matters due to implementations deviating from the 65 | /// [XTerm Control Sequences](https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Operating-System-Commands) 66 | /// reference. Some terminal emulators may only interpret the first character of this 67 | /// parameter. For differences, see 68 | /// [`CopyToClipboard` (Terminal Support)](struct.CopyToClipboard.html#terminal-support). 69 | pub Vec, 70 | ); 71 | 72 | impl ClipboardSelection { 73 | /// Returns a String corresponsing to the "Pc" parameter of the OSC52 74 | /// sequence. 75 | fn to_osc52_pc(&self) -> String { 76 | self.0.iter().map(Into::::into).collect() 77 | } 78 | } 79 | 80 | impl fmt::Display for ClipboardSelection { 81 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 82 | f.write_str(&self.to_osc52_pc()) 83 | } 84 | } 85 | 86 | impl FromStr for ClipboardSelection { 87 | type Err = (); 88 | fn from_str(s: &str) -> Result { 89 | Ok(ClipboardSelection( 90 | s.chars().map(From::::from).collect(), 91 | )) 92 | } 93 | } 94 | 95 | /// A command that copies to clipboard 96 | /// 97 | /// This command uses OSC control sequence `Pr = 5 2` (See 98 | /// [XTerm Control Sequences](https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h3-Operating-System-Commands) ) 99 | /// to copy data to the terminal host clipboard. 100 | /// 101 | /// This only works if it is enabled on the user's terminal emulator. If a terminal multiplexer 102 | /// is used, the multiplexer must support it, too. 103 | /// 104 | /// Commands must be executed/queued for execution otherwise they do nothing. 105 | /// 106 | /// # Examples 107 | /// 108 | /// ```no_run 109 | /// use crossterm::execute; 110 | /// use crossterm::clipboard::CopyToClipboard; 111 | /// // Copy foo to clipboard 112 | /// execute!(std::io::stdout(), CopyToClipboard::to_clipboard_from("foo")); 113 | /// // Copy bar to primary 114 | /// execute!(std::io::stdout(), CopyToClipboard::to_primary_from("bar")); 115 | /// ``` 116 | /// 117 | /// See also examples/copy-to-clipboard.rs. 118 | /// 119 | /// # Terminal Support 120 | /// 121 | /// The following table shows what destinations are filled by different terminal emulators when 122 | /// asked to copy to different destination sequences. 123 | /// 124 | /// | Terminal (Version) | dest '' | dest 'c' | dest 'p' | dest 'cp' | dest'pc' | 125 | /// | --------------------- | --------- | --------- | -------- | ------------- | ------------- | 126 | /// | xterm (397) *3 | primary | clipboard | primary | clipb., prim. | clipb., prim. | 127 | /// | Alacritty (0.15.1) *3 | clipboard | clipboard | primary | clipb. | prim. | 128 | /// | Wezterm (*1) *3 | clipboard | clipboard | primary | clipb. | clipb. | 129 | /// | Konsole (24.12.3) *3 | clipboard | clipboard | primary | clipb., prim. | clipb., prim. | 130 | /// | Kitty (0.40.0) *3 | clipboard | clipboard | primary | clipb. | clipb. | 131 | /// | foot (1.20.2) *3 | clipboard | clipboard | primary | clipb., prim. | clipb., prim. | 132 | /// | tmux (3.5a) *2 *3 | primary | clipboard | primary | clipb., prim. | clipb., prim. | 133 | /// 134 | /// Asterisks: 135 | /// 1. 20240203-110809-5046fc22 136 | /// 2. set-clipboard set to [external](https://github.com/tmux/tmux/wiki/Clipboard#how-it-works), 137 | /// i.e. this is OSC52 pass-through. 138 | /// 3. This was tested on wayland with the 139 | /// [primary selection protocol](https://wayland.app/protocols/primary-selection-unstable-v1) 140 | /// enabled. 141 | #[derive(Debug, Clone, PartialEq, Eq)] 142 | pub struct CopyToClipboard { 143 | /// Content to be copied 144 | pub content: T, 145 | /// Sequence of copy destinations 146 | /// 147 | /// Not all sequences are equally supported by terminal emulators. See 148 | /// [`CopyToClipboard` (Terminal Support)](struct.CopyToClipboard.html#terminal-support). 149 | pub destination: ClipboardSelection, 150 | } 151 | 152 | impl> Command for CopyToClipboard { 153 | fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result { 154 | write!( 155 | f, 156 | osc!("52;{destination};{encoded_text}"), 157 | destination = self.destination.to_osc52_pc(), 158 | encoded_text = BASE64_STANDARD.encode(&self.content) 159 | ) 160 | } 161 | 162 | #[cfg(windows)] 163 | fn execute_winapi(&self) -> std::io::Result<()> { 164 | use std::io; 165 | 166 | Err(io::Error::new( 167 | io::ErrorKind::Unsupported, 168 | "Copying is not implemented for the Windows API.", 169 | )) 170 | } 171 | } 172 | 173 | impl> CopyToClipboard { 174 | /// Construct a [`CopyToClipboard`] that writes content into the 175 | /// "clipboard" (or 'c') clipboard selection. 176 | /// 177 | /// # Example 178 | /// 179 | /// ```no_run 180 | /// use crossterm::{execute, Command}; 181 | /// use crossterm::clipboard::CopyToClipboard; 182 | /// execute!(std::io::stdout(), CopyToClipboard::to_clipboard_from("foo")); 183 | /// ``` 184 | pub fn to_clipboard_from(content: T) -> CopyToClipboard { 185 | CopyToClipboard { 186 | content, 187 | destination: ClipboardSelection(vec![ClipboardType::Clipboard]), 188 | } 189 | } 190 | 191 | /// Construct a [`CopyToClipboard`] that writes content into the "primary" 192 | /// (or 'p') clipboard selection. 193 | /// 194 | /// # Example 195 | /// 196 | /// ```no_run 197 | /// use crossterm::execute; 198 | /// use crossterm::clipboard::CopyToClipboard; 199 | /// execute!(std::io::stdout(), CopyToClipboard::to_primary_from("foo")); 200 | /// ``` 201 | pub fn to_primary_from(content: T) -> CopyToClipboard { 202 | CopyToClipboard { 203 | content, 204 | destination: ClipboardSelection(vec![ClipboardType::Primary]), 205 | } 206 | } 207 | } 208 | 209 | #[cfg(test)] 210 | mod tests { 211 | use super::*; 212 | 213 | #[test] 214 | fn test_clipboard_string_to_selection() { 215 | assert_eq!( 216 | ClipboardSelection::from_str("p").unwrap(), 217 | ClipboardSelection(vec![ClipboardType::Primary]) 218 | ); 219 | assert_eq!( 220 | ClipboardSelection::from_str("").unwrap(), 221 | ClipboardSelection(vec![]) 222 | ); 223 | assert_eq!( 224 | ClipboardSelection::from_str("cp").unwrap(), 225 | ClipboardSelection(vec![ClipboardType::Clipboard, ClipboardType::Primary]) 226 | ); 227 | } 228 | #[test] 229 | fn test_clipboard_selection_to_osc52_pc() { 230 | assert_eq!(ClipboardSelection(vec![]).to_osc52_pc(), ""); 231 | assert_eq!( 232 | ClipboardSelection(vec![ClipboardType::Clipboard]).to_osc52_pc(), 233 | "c" 234 | ); 235 | assert_eq!( 236 | ClipboardSelection(vec![ClipboardType::Primary]).to_osc52_pc(), 237 | "p" 238 | ); 239 | assert_eq!( 240 | ClipboardSelection(vec![ClipboardType::Primary, ClipboardType::Clipboard]) 241 | .to_osc52_pc(), 242 | "pc" 243 | ); 244 | assert_eq!( 245 | ClipboardSelection(vec![ClipboardType::Clipboard, ClipboardType::Primary]) 246 | .to_osc52_pc(), 247 | "cp" 248 | ); 249 | assert_eq!( 250 | ClipboardSelection(vec![ClipboardType::Other('s')]).to_osc52_pc(), 251 | "s" 252 | ); 253 | } 254 | 255 | #[test] 256 | fn test_clipboard_copy_string_osc52() { 257 | let mut buffer = String::new(); 258 | super::CopyToClipboard { 259 | content: "foo", 260 | destination: ClipboardSelection(vec![ClipboardType::Clipboard]), 261 | } 262 | .write_ansi(&mut buffer) 263 | .unwrap(); 264 | assert_eq!(buffer, "\x1b]52;c;Zm9v\x1b\\"); 265 | 266 | buffer.clear(); 267 | super::CopyToClipboard { 268 | content: "foo", 269 | destination: ClipboardSelection(vec![ClipboardType::Primary]), 270 | } 271 | .write_ansi(&mut buffer) 272 | .unwrap(); 273 | assert_eq!(buffer, "\x1b]52;p;Zm9v\x1b\\"); 274 | 275 | buffer.clear(); 276 | super::CopyToClipboard { 277 | content: "foo", 278 | destination: ClipboardSelection(vec![ClipboardType::Primary, ClipboardType::Clipboard]), 279 | } 280 | .write_ansi(&mut buffer) 281 | .unwrap(); 282 | assert_eq!(buffer, "\x1b]52;pc;Zm9v\x1b\\"); 283 | 284 | buffer.clear(); 285 | super::CopyToClipboard { 286 | content: "foo", 287 | destination: ClipboardSelection(vec![]), 288 | } 289 | .write_ansi(&mut buffer) 290 | .unwrap(); 291 | assert_eq!(buffer, "\x1b]52;;Zm9v\x1b\\"); 292 | } 293 | 294 | #[test] 295 | fn test_clipboard_copy_string_osc52_constructor() { 296 | let mut buffer = String::new(); 297 | super::CopyToClipboard::to_clipboard_from("foo") 298 | .write_ansi(&mut buffer) 299 | .unwrap(); 300 | assert_eq!(buffer, "\x1b]52;c;Zm9v\x1b\\"); 301 | 302 | let mut buffer = String::new(); 303 | super::CopyToClipboard::to_primary_from("foo") 304 | .write_ansi(&mut buffer) 305 | .unwrap(); 306 | assert_eq!(buffer, "\x1b]52;p;Zm9v\x1b\\"); 307 | } 308 | } 309 | -------------------------------------------------------------------------------- /src/cursor/sys.rs: -------------------------------------------------------------------------------- 1 | //! This module provides platform related functions. 2 | 3 | #[cfg(unix)] 4 | #[cfg(feature = "events")] 5 | pub use self::unix::position; 6 | #[cfg(windows)] 7 | #[cfg(feature = "events")] 8 | pub use self::windows::position; 9 | #[cfg(windows)] 10 | pub(crate) use self::windows::{ 11 | move_down, move_left, move_right, move_to, move_to_column, move_to_next_line, 12 | move_to_previous_line, move_to_row, move_up, restore_position, save_position, show_cursor, 13 | }; 14 | 15 | #[cfg(windows)] 16 | pub(crate) mod windows; 17 | 18 | #[cfg(unix)] 19 | #[cfg(feature = "events")] 20 | pub(crate) mod unix; 21 | -------------------------------------------------------------------------------- /src/cursor/sys/unix.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | io::{self, Error, ErrorKind, Write}, 3 | time::Duration, 4 | }; 5 | 6 | use crate::{ 7 | event::{filter::CursorPositionFilter, poll_internal, read_internal, InternalEvent}, 8 | terminal::{disable_raw_mode, enable_raw_mode, sys::is_raw_mode_enabled}, 9 | }; 10 | 11 | /// Returns the cursor position (column, row). 12 | /// 13 | /// The top left cell is represented as `(0, 0)`. 14 | /// 15 | /// On unix systems, this function will block and possibly time out while 16 | /// [`crossterm::event::read`](crate::event::read) or [`crossterm::event::poll`](crate::event::poll) are being called. 17 | pub fn position() -> io::Result<(u16, u16)> { 18 | if is_raw_mode_enabled() { 19 | read_position_raw() 20 | } else { 21 | read_position() 22 | } 23 | } 24 | 25 | fn read_position() -> io::Result<(u16, u16)> { 26 | enable_raw_mode()?; 27 | let pos = read_position_raw(); 28 | disable_raw_mode()?; 29 | pos 30 | } 31 | 32 | fn read_position_raw() -> io::Result<(u16, u16)> { 33 | // Use `ESC [ 6 n` to and retrieve the cursor position. 34 | let mut stdout = io::stdout(); 35 | stdout.write_all(b"\x1B[6n")?; 36 | stdout.flush()?; 37 | 38 | loop { 39 | match poll_internal(Some(Duration::from_millis(2000)), &CursorPositionFilter) { 40 | Ok(true) => { 41 | if let Ok(InternalEvent::CursorPosition(x, y)) = 42 | read_internal(&CursorPositionFilter) 43 | { 44 | return Ok((x, y)); 45 | } 46 | } 47 | Ok(false) => { 48 | return Err(Error::new( 49 | ErrorKind::Other, 50 | "The cursor position could not be read within a normal duration", 51 | )); 52 | } 53 | Err(_) => {} 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/cursor/sys/windows.rs: -------------------------------------------------------------------------------- 1 | //! WinAPI related logic to cursor manipulation. 2 | 3 | use std::convert::TryFrom; 4 | use std::io; 5 | use std::sync::atomic::{AtomicU64, Ordering}; 6 | 7 | use crossterm_winapi::{result, Coord, Handle, HandleType, ScreenBuffer}; 8 | use winapi::{ 9 | shared::minwindef::{FALSE, TRUE}, 10 | um::wincon::{SetConsoleCursorInfo, SetConsoleCursorPosition, CONSOLE_CURSOR_INFO, COORD}, 11 | }; 12 | 13 | /// The position of the cursor, written when you save the cursor's position. 14 | /// 15 | /// This is `u64::MAX` initially. Otherwise, it stores the cursor's x position bit-shifted left 16 16 | /// times or-ed with the cursor's y position, where both are `i16`s. 17 | static SAVED_CURSOR_POS: AtomicU64 = AtomicU64::new(u64::MAX); 18 | 19 | // The 'y' position of the cursor is not relative to the window but absolute to screen buffer. 20 | // We can calculate the relative cursor position by subtracting the top position of the terminal window from the y position. 21 | // This results in an 1-based coord zo subtract 1 to make cursor position 0-based. 22 | pub fn parse_relative_y(y: i16) -> std::io::Result { 23 | let window = ScreenBuffer::current()?.info()?; 24 | 25 | let window_size = window.terminal_window(); 26 | let screen_size = window.terminal_size(); 27 | 28 | if y <= screen_size.height { 29 | Ok(y) 30 | } else { 31 | Ok(y - window_size.top) 32 | } 33 | } 34 | 35 | /// Returns the cursor position (column, row). 36 | /// 37 | /// The top left cell is represented `0,0`. 38 | pub fn position() -> io::Result<(u16, u16)> { 39 | let cursor = ScreenBufferCursor::output()?; 40 | let mut position = cursor.position()?; 41 | // if position.y != 0 { 42 | position.y = parse_relative_y(position.y)?; 43 | // } 44 | Ok(position.into()) 45 | } 46 | 47 | pub(crate) fn show_cursor(show_cursor: bool) -> std::io::Result<()> { 48 | ScreenBufferCursor::from(Handle::current_out_handle()?).set_visibility(show_cursor) 49 | } 50 | 51 | pub(crate) fn move_to(column: u16, row: u16) -> std::io::Result<()> { 52 | let cursor = ScreenBufferCursor::output()?; 53 | cursor.move_to(column as i16, row as i16)?; 54 | Ok(()) 55 | } 56 | 57 | pub(crate) fn move_up(count: u16) -> std::io::Result<()> { 58 | let (column, row) = position()?; 59 | move_to(column, row - count)?; 60 | Ok(()) 61 | } 62 | 63 | pub(crate) fn move_right(count: u16) -> std::io::Result<()> { 64 | let (column, row) = position()?; 65 | move_to(column + count, row)?; 66 | Ok(()) 67 | } 68 | 69 | pub(crate) fn move_down(count: u16) -> std::io::Result<()> { 70 | let (column, row) = position()?; 71 | move_to(column, row + count)?; 72 | Ok(()) 73 | } 74 | 75 | pub(crate) fn move_left(count: u16) -> std::io::Result<()> { 76 | let (column, row) = position()?; 77 | move_to(column - count, row)?; 78 | Ok(()) 79 | } 80 | 81 | pub(crate) fn move_to_column(new_column: u16) -> std::io::Result<()> { 82 | let (_, row) = position()?; 83 | move_to(new_column, row)?; 84 | Ok(()) 85 | } 86 | 87 | pub(crate) fn move_to_row(new_row: u16) -> std::io::Result<()> { 88 | let (col, _) = position()?; 89 | move_to(col, new_row)?; 90 | Ok(()) 91 | } 92 | 93 | pub(crate) fn move_to_next_line(count: u16) -> std::io::Result<()> { 94 | let (_, row) = position()?; 95 | move_to(0, row + count)?; 96 | Ok(()) 97 | } 98 | 99 | pub(crate) fn move_to_previous_line(count: u16) -> std::io::Result<()> { 100 | let (_, row) = position()?; 101 | move_to(0, row - count)?; 102 | Ok(()) 103 | } 104 | 105 | pub(crate) fn save_position() -> std::io::Result<()> { 106 | ScreenBufferCursor::output()?.save_position()?; 107 | Ok(()) 108 | } 109 | 110 | pub(crate) fn restore_position() -> std::io::Result<()> { 111 | ScreenBufferCursor::output()?.restore_position()?; 112 | Ok(()) 113 | } 114 | 115 | /// WinAPI wrapper over terminal cursor behaviour. 116 | struct ScreenBufferCursor { 117 | screen_buffer: ScreenBuffer, 118 | } 119 | 120 | impl ScreenBufferCursor { 121 | fn output() -> std::io::Result { 122 | Ok(ScreenBufferCursor { 123 | screen_buffer: ScreenBuffer::from(Handle::new(HandleType::CurrentOutputHandle)?), 124 | }) 125 | } 126 | 127 | fn position(&self) -> std::io::Result { 128 | Ok(self.screen_buffer.info()?.cursor_pos()) 129 | } 130 | 131 | fn move_to(&self, x: i16, y: i16) -> std::io::Result<()> { 132 | if x < 0 { 133 | return Err(io::Error::new( 134 | io::ErrorKind::Other, 135 | format!("Argument Out of Range Exception when setting cursor position to X: {x}"), 136 | )); 137 | } 138 | 139 | if y < 0 { 140 | return Err(io::Error::new( 141 | io::ErrorKind::Other, 142 | format!("Argument Out of Range Exception when setting cursor position to Y: {y}"), 143 | )); 144 | } 145 | 146 | let position = COORD { X: x, Y: y }; 147 | 148 | unsafe { 149 | if result(SetConsoleCursorPosition( 150 | **self.screen_buffer.handle(), 151 | position, 152 | )) 153 | .is_err() 154 | { 155 | return Err(io::Error::last_os_error()); 156 | } 157 | } 158 | Ok(()) 159 | } 160 | 161 | fn set_visibility(&self, visible: bool) -> std::io::Result<()> { 162 | let cursor_info = CONSOLE_CURSOR_INFO { 163 | dwSize: 100, 164 | bVisible: if visible { TRUE } else { FALSE }, 165 | }; 166 | 167 | unsafe { 168 | if result(SetConsoleCursorInfo( 169 | **self.screen_buffer.handle(), 170 | &cursor_info, 171 | )) 172 | .is_err() 173 | { 174 | return Err(io::Error::last_os_error()); 175 | } 176 | } 177 | Ok(()) 178 | } 179 | 180 | fn restore_position(&self) -> std::io::Result<()> { 181 | if let Ok(val) = u32::try_from(SAVED_CURSOR_POS.load(Ordering::Relaxed)) { 182 | let x = (val >> 16) as i16; 183 | let y = val as i16; 184 | self.move_to(x, y)?; 185 | } 186 | 187 | Ok(()) 188 | } 189 | 190 | fn save_position(&self) -> std::io::Result<()> { 191 | let position = self.position()?; 192 | 193 | let upper = u32::from(position.x as u16) << 16; 194 | let lower = u32::from(position.y as u16); 195 | let bits = u64::from(upper | lower); 196 | SAVED_CURSOR_POS.store(bits, Ordering::Relaxed); 197 | 198 | Ok(()) 199 | } 200 | } 201 | 202 | impl From for ScreenBufferCursor { 203 | fn from(handle: Handle) -> Self { 204 | ScreenBufferCursor { 205 | screen_buffer: ScreenBuffer::from(handle), 206 | } 207 | } 208 | } 209 | 210 | #[cfg(test)] 211 | mod tests { 212 | use super::{ 213 | move_down, move_left, move_right, move_to, move_to_column, move_to_next_line, 214 | move_to_previous_line, move_to_row, move_up, position, restore_position, save_position, 215 | }; 216 | use crate::terminal::sys::temp_screen_buffer; 217 | use serial_test::serial; 218 | 219 | #[test] 220 | #[serial] 221 | fn test_move_to_winapi() { 222 | let _test_screen = temp_screen_buffer().unwrap(); 223 | 224 | let (saved_x, saved_y) = position().unwrap(); 225 | 226 | move_to(saved_x + 1, saved_y + 1).unwrap(); 227 | assert_eq!(position().unwrap(), (saved_x + 1, saved_y + 1)); 228 | 229 | move_to(saved_x, saved_y).unwrap(); 230 | assert_eq!(position().unwrap(), (saved_x, saved_y)); 231 | } 232 | 233 | #[test] 234 | #[serial] 235 | fn test_move_right_winapi() { 236 | let _test_screen = temp_screen_buffer().unwrap(); 237 | 238 | let (saved_x, saved_y) = position().unwrap(); 239 | move_right(1).unwrap(); 240 | assert_eq!(position().unwrap(), (saved_x + 1, saved_y)); 241 | } 242 | 243 | #[test] 244 | #[serial] 245 | fn test_move_left_winapi() { 246 | let _test_screen = temp_screen_buffer().unwrap(); 247 | 248 | move_to(2, 0).unwrap(); 249 | 250 | move_left(2).unwrap(); 251 | 252 | assert_eq!(position().unwrap(), (0, 0)); 253 | } 254 | 255 | #[test] 256 | #[serial] 257 | fn test_move_up_winapi() { 258 | let _test_screen = temp_screen_buffer().unwrap(); 259 | 260 | move_to(0, 2).unwrap(); 261 | 262 | move_up(2).unwrap(); 263 | 264 | assert_eq!(position().unwrap(), (0, 0)); 265 | } 266 | 267 | #[test] 268 | #[serial] 269 | fn test_move_to_next_line_winapi() { 270 | let _test_screen = temp_screen_buffer().unwrap(); 271 | 272 | move_to(0, 2).unwrap(); 273 | 274 | move_to_next_line(2).unwrap(); 275 | 276 | assert_eq!(position().unwrap(), (0, 4)); 277 | } 278 | 279 | #[test] 280 | #[serial] 281 | fn test_move_to_previous_line_winapi() { 282 | let _test_screen = temp_screen_buffer().unwrap(); 283 | 284 | move_to(0, 2).unwrap(); 285 | 286 | move_to_previous_line(2).unwrap(); 287 | 288 | assert_eq!(position().unwrap(), (0, 0)); 289 | } 290 | 291 | #[test] 292 | #[serial] 293 | fn test_move_to_column_winapi() { 294 | let _test_screen = temp_screen_buffer().unwrap(); 295 | 296 | move_to(0, 2).unwrap(); 297 | 298 | move_to_column(12).unwrap(); 299 | 300 | assert_eq!(position().unwrap(), (12, 2)); 301 | } 302 | 303 | #[test] 304 | #[serial] 305 | fn test_move_to_row_winapi() { 306 | let _test_screen = temp_screen_buffer().unwrap(); 307 | 308 | move_to(0, 2).unwrap(); 309 | 310 | move_to_row(5).unwrap(); 311 | 312 | assert_eq!(position().unwrap(), (0, 5)); 313 | } 314 | 315 | #[test] 316 | #[serial] 317 | fn test_move_down_winapi() { 318 | let _test_screen = temp_screen_buffer().unwrap(); 319 | 320 | move_to(0, 0).unwrap(); 321 | 322 | move_down(2).unwrap(); 323 | 324 | assert_eq!(position().unwrap(), (0, 2)); 325 | } 326 | 327 | #[test] 328 | #[serial] 329 | fn test_save_restore_position_winapi() { 330 | let _test_screen = temp_screen_buffer().unwrap(); 331 | 332 | let (saved_x, saved_y) = position().unwrap(); 333 | 334 | save_position().unwrap(); 335 | move_to(saved_x + 1, saved_y + 1).unwrap(); 336 | restore_position().unwrap(); 337 | 338 | let (x, y) = position().unwrap(); 339 | 340 | assert_eq!(x, saved_x); 341 | assert_eq!(y, saved_y); 342 | } 343 | } 344 | -------------------------------------------------------------------------------- /src/event/filter.rs: -------------------------------------------------------------------------------- 1 | use crate::event::InternalEvent; 2 | 3 | /// Interface for filtering an `InternalEvent`. 4 | pub(crate) trait Filter: Send + Sync + 'static { 5 | /// Returns whether the given event fulfills the filter. 6 | fn eval(&self, event: &InternalEvent) -> bool; 7 | } 8 | 9 | #[cfg(unix)] 10 | #[derive(Debug, Clone)] 11 | pub(crate) struct CursorPositionFilter; 12 | 13 | #[cfg(unix)] 14 | impl Filter for CursorPositionFilter { 15 | fn eval(&self, event: &InternalEvent) -> bool { 16 | matches!(*event, InternalEvent::CursorPosition(_, _)) 17 | } 18 | } 19 | 20 | #[cfg(unix)] 21 | #[derive(Debug, Clone)] 22 | pub(crate) struct KeyboardEnhancementFlagsFilter; 23 | 24 | #[cfg(unix)] 25 | impl Filter for KeyboardEnhancementFlagsFilter { 26 | fn eval(&self, event: &InternalEvent) -> bool { 27 | // This filter checks for either a KeyboardEnhancementFlags response or 28 | // a PrimaryDeviceAttributes response. If we receive the PrimaryDeviceAttributes 29 | // response but not KeyboardEnhancementFlags, the terminal does not support 30 | // progressive keyboard enhancement. 31 | matches!( 32 | *event, 33 | InternalEvent::KeyboardEnhancementFlags(_) | InternalEvent::PrimaryDeviceAttributes 34 | ) 35 | } 36 | } 37 | 38 | #[cfg(unix)] 39 | #[derive(Debug, Clone)] 40 | pub(crate) struct PrimaryDeviceAttributesFilter; 41 | 42 | #[cfg(unix)] 43 | impl Filter for PrimaryDeviceAttributesFilter { 44 | fn eval(&self, event: &InternalEvent) -> bool { 45 | matches!(*event, InternalEvent::PrimaryDeviceAttributes) 46 | } 47 | } 48 | 49 | #[derive(Debug, Clone)] 50 | pub(crate) struct EventFilter; 51 | 52 | impl Filter for EventFilter { 53 | #[cfg(unix)] 54 | fn eval(&self, event: &InternalEvent) -> bool { 55 | matches!(*event, InternalEvent::Event(_)) 56 | } 57 | 58 | #[cfg(windows)] 59 | fn eval(&self, _: &InternalEvent) -> bool { 60 | true 61 | } 62 | } 63 | 64 | #[cfg(test)] 65 | #[cfg(unix)] 66 | mod tests { 67 | use super::{ 68 | super::Event, CursorPositionFilter, EventFilter, Filter, InternalEvent, 69 | KeyboardEnhancementFlagsFilter, PrimaryDeviceAttributesFilter, 70 | }; 71 | 72 | #[derive(Debug, Clone)] 73 | pub(crate) struct InternalEventFilter; 74 | 75 | impl Filter for InternalEventFilter { 76 | fn eval(&self, _: &InternalEvent) -> bool { 77 | true 78 | } 79 | } 80 | 81 | #[test] 82 | fn test_cursor_position_filter_filters_cursor_position() { 83 | assert!(!CursorPositionFilter.eval(&InternalEvent::Event(Event::Resize(10, 10)))); 84 | assert!(CursorPositionFilter.eval(&InternalEvent::CursorPosition(0, 0))); 85 | } 86 | 87 | #[test] 88 | fn test_keyboard_enhancement_status_filter_filters_keyboard_enhancement_status() { 89 | assert!(!KeyboardEnhancementFlagsFilter.eval(&InternalEvent::Event(Event::Resize(10, 10)))); 90 | assert!( 91 | KeyboardEnhancementFlagsFilter.eval(&InternalEvent::KeyboardEnhancementFlags( 92 | crate::event::KeyboardEnhancementFlags::DISAMBIGUATE_ESCAPE_CODES 93 | )) 94 | ); 95 | assert!(KeyboardEnhancementFlagsFilter.eval(&InternalEvent::PrimaryDeviceAttributes)); 96 | } 97 | 98 | #[test] 99 | fn test_primary_device_attributes_filter_filters_primary_device_attributes() { 100 | assert!(!PrimaryDeviceAttributesFilter.eval(&InternalEvent::Event(Event::Resize(10, 10)))); 101 | assert!(PrimaryDeviceAttributesFilter.eval(&InternalEvent::PrimaryDeviceAttributes)); 102 | } 103 | 104 | #[test] 105 | fn test_event_filter_filters_events() { 106 | assert!(EventFilter.eval(&InternalEvent::Event(Event::Resize(10, 10)))); 107 | assert!(!EventFilter.eval(&InternalEvent::CursorPosition(0, 0))); 108 | } 109 | 110 | #[test] 111 | fn test_event_filter_filters_internal_events() { 112 | assert!(InternalEventFilter.eval(&InternalEvent::Event(Event::Resize(10, 10)))); 113 | assert!(InternalEventFilter.eval(&InternalEvent::CursorPosition(0, 0))); 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/event/source.rs: -------------------------------------------------------------------------------- 1 | use std::{io, time::Duration}; 2 | 3 | #[cfg(feature = "event-stream")] 4 | use super::sys::Waker; 5 | use super::InternalEvent; 6 | 7 | #[cfg(unix)] 8 | pub(crate) mod unix; 9 | #[cfg(windows)] 10 | pub(crate) mod windows; 11 | 12 | /// An interface for trying to read an `InternalEvent` within an optional `Duration`. 13 | pub(crate) trait EventSource: Sync + Send { 14 | /// Tries to read an `InternalEvent` within the given duration. 15 | /// 16 | /// # Arguments 17 | /// 18 | /// * `timeout` - `None` block indefinitely until an event is available, `Some(duration)` blocks 19 | /// for the given timeout 20 | /// 21 | /// Returns `Ok(None)` if there's no event available and timeout expires. 22 | fn try_read(&mut self, timeout: Option) -> io::Result>; 23 | 24 | /// Returns a `Waker` allowing to wake/force the `try_read` method to return `Ok(None)`. 25 | #[cfg(feature = "event-stream")] 26 | fn waker(&self) -> Waker; 27 | } 28 | -------------------------------------------------------------------------------- /src/event/source/unix.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "use-dev-tty")] 2 | pub(crate) mod tty; 3 | 4 | #[cfg(not(feature = "use-dev-tty"))] 5 | pub(crate) mod mio; 6 | 7 | #[cfg(feature = "use-dev-tty")] 8 | pub(crate) use self::tty::UnixInternalEventSource; 9 | 10 | #[cfg(not(feature = "use-dev-tty"))] 11 | pub(crate) use self::mio::UnixInternalEventSource; 12 | -------------------------------------------------------------------------------- /src/event/source/unix/mio.rs: -------------------------------------------------------------------------------- 1 | use std::{collections::VecDeque, io, time::Duration}; 2 | 3 | use mio::{unix::SourceFd, Events, Interest, Poll, Token}; 4 | use signal_hook_mio::v1_0::Signals; 5 | 6 | #[cfg(feature = "event-stream")] 7 | use crate::event::sys::Waker; 8 | use crate::event::{ 9 | source::EventSource, sys::unix::parse::parse_event, timeout::PollTimeout, Event, InternalEvent, 10 | }; 11 | use crate::terminal::sys::file_descriptor::{tty_fd, FileDesc}; 12 | 13 | // Tokens to identify file descriptor 14 | const TTY_TOKEN: Token = Token(0); 15 | const SIGNAL_TOKEN: Token = Token(1); 16 | #[cfg(feature = "event-stream")] 17 | const WAKE_TOKEN: Token = Token(2); 18 | 19 | // I (@zrzka) wasn't able to read more than 1_022 bytes when testing 20 | // reading on macOS/Linux -> we don't need bigger buffer and 1k of bytes 21 | // is enough. 22 | const TTY_BUFFER_SIZE: usize = 1_024; 23 | 24 | pub(crate) struct UnixInternalEventSource { 25 | poll: Poll, 26 | events: Events, 27 | parser: Parser, 28 | tty_buffer: [u8; TTY_BUFFER_SIZE], 29 | tty_fd: FileDesc<'static>, 30 | signals: Signals, 31 | #[cfg(feature = "event-stream")] 32 | waker: Waker, 33 | } 34 | 35 | impl UnixInternalEventSource { 36 | pub fn new() -> io::Result { 37 | UnixInternalEventSource::from_file_descriptor(tty_fd()?) 38 | } 39 | 40 | pub(crate) fn from_file_descriptor(input_fd: FileDesc<'static>) -> io::Result { 41 | let poll = Poll::new()?; 42 | let registry = poll.registry(); 43 | 44 | let tty_raw_fd = input_fd.raw_fd(); 45 | let mut tty_ev = SourceFd(&tty_raw_fd); 46 | registry.register(&mut tty_ev, TTY_TOKEN, Interest::READABLE)?; 47 | 48 | let mut signals = Signals::new([signal_hook::consts::SIGWINCH])?; 49 | registry.register(&mut signals, SIGNAL_TOKEN, Interest::READABLE)?; 50 | 51 | #[cfg(feature = "event-stream")] 52 | let waker = Waker::new(registry, WAKE_TOKEN)?; 53 | 54 | Ok(UnixInternalEventSource { 55 | poll, 56 | events: Events::with_capacity(3), 57 | parser: Parser::default(), 58 | tty_buffer: [0u8; TTY_BUFFER_SIZE], 59 | tty_fd: input_fd, 60 | signals, 61 | #[cfg(feature = "event-stream")] 62 | waker, 63 | }) 64 | } 65 | } 66 | 67 | impl EventSource for UnixInternalEventSource { 68 | fn try_read(&mut self, timeout: Option) -> io::Result> { 69 | if let Some(event) = self.parser.next() { 70 | return Ok(Some(event)); 71 | } 72 | 73 | let timeout = PollTimeout::new(timeout); 74 | 75 | loop { 76 | if let Err(e) = self.poll.poll(&mut self.events, timeout.leftover()) { 77 | // Mio will throw an interrupted error in case of cursor position retrieval. We need to retry until it succeeds. 78 | // Previous versions of Mio (< 0.7) would automatically retry the poll call if it was interrupted (if EINTR was returned). 79 | // https://docs.rs/mio/0.7.0/mio/struct.Poll.html#notes 80 | if e.kind() == io::ErrorKind::Interrupted { 81 | continue; 82 | } else { 83 | return Err(e); 84 | } 85 | }; 86 | 87 | if self.events.is_empty() { 88 | // No readiness events = timeout 89 | return Ok(None); 90 | } 91 | 92 | for token in self.events.iter().map(|x| x.token()) { 93 | match token { 94 | TTY_TOKEN => { 95 | loop { 96 | match self.tty_fd.read(&mut self.tty_buffer) { 97 | Ok(read_count) => { 98 | if read_count > 0 { 99 | self.parser.advance( 100 | &self.tty_buffer[..read_count], 101 | read_count == TTY_BUFFER_SIZE, 102 | ); 103 | } 104 | } 105 | Err(e) => { 106 | // No more data to read at the moment. We will receive another event 107 | if e.kind() == io::ErrorKind::WouldBlock { 108 | break; 109 | } 110 | // once more data is available to read. 111 | else if e.kind() == io::ErrorKind::Interrupted { 112 | continue; 113 | } 114 | } 115 | }; 116 | 117 | if let Some(event) = self.parser.next() { 118 | return Ok(Some(event)); 119 | } 120 | } 121 | } 122 | SIGNAL_TOKEN => { 123 | if self.signals.pending().next() == Some(signal_hook::consts::SIGWINCH) { 124 | // TODO Should we remove tput? 125 | // 126 | // This can take a really long time, because terminal::size can 127 | // launch new process (tput) and then it parses its output. It's 128 | // not a really long time from the absolute time point of view, but 129 | // it's a really long time from the mio, async-std/tokio executor, ... 130 | // point of view. 131 | let new_size = crate::terminal::size()?; 132 | return Ok(Some(InternalEvent::Event(Event::Resize( 133 | new_size.0, new_size.1, 134 | )))); 135 | } 136 | } 137 | #[cfg(feature = "event-stream")] 138 | WAKE_TOKEN => { 139 | return Err(std::io::Error::new( 140 | std::io::ErrorKind::Interrupted, 141 | "Poll operation was woken up by `Waker::wake`", 142 | )); 143 | } 144 | _ => unreachable!("Synchronize Evented handle registration & token handling"), 145 | } 146 | } 147 | 148 | // Processing above can take some time, check if timeout expired 149 | if timeout.elapsed() { 150 | return Ok(None); 151 | } 152 | } 153 | } 154 | 155 | #[cfg(feature = "event-stream")] 156 | fn waker(&self) -> Waker { 157 | self.waker.clone() 158 | } 159 | } 160 | 161 | // 162 | // Following `Parser` structure exists for two reasons: 163 | // 164 | // * mimic anes Parser interface 165 | // * move the advancing, parsing, ... stuff out of the `try_read` method 166 | // 167 | #[derive(Debug)] 168 | struct Parser { 169 | buffer: Vec, 170 | internal_events: VecDeque, 171 | } 172 | 173 | impl Default for Parser { 174 | fn default() -> Self { 175 | Parser { 176 | // This buffer is used for -> 1 <- ANSI escape sequence. Are we 177 | // aware of any ANSI escape sequence that is bigger? Can we make 178 | // it smaller? 179 | // 180 | // Probably not worth spending more time on this as "there's a plan" 181 | // to use the anes crate parser. 182 | buffer: Vec::with_capacity(256), 183 | // TTY_BUFFER_SIZE is 1_024 bytes. How many ANSI escape sequences can 184 | // fit? What is an average sequence length? Let's guess here 185 | // and say that the average ANSI escape sequence length is 8 bytes. Thus 186 | // the buffer size should be 1024/8=128 to avoid additional allocations 187 | // when processing large amounts of data. 188 | // 189 | // There's no need to make it bigger, because when you look at the `try_read` 190 | // method implementation, all events are consumed before the next TTY_BUFFER 191 | // is processed -> events pushed. 192 | internal_events: VecDeque::with_capacity(128), 193 | } 194 | } 195 | } 196 | 197 | impl Parser { 198 | fn advance(&mut self, buffer: &[u8], more: bool) { 199 | for (idx, byte) in buffer.iter().enumerate() { 200 | let more = idx + 1 < buffer.len() || more; 201 | 202 | self.buffer.push(*byte); 203 | 204 | match parse_event(&self.buffer, more) { 205 | Ok(Some(ie)) => { 206 | self.internal_events.push_back(ie); 207 | self.buffer.clear(); 208 | } 209 | Ok(None) => { 210 | // Event can't be parsed, because we don't have enough bytes for 211 | // the current sequence. Keep the buffer and process next bytes. 212 | } 213 | Err(_) => { 214 | // Event can't be parsed (not enough parameters, parameter is not a number, ...). 215 | // Clear the buffer and continue with another sequence. 216 | self.buffer.clear(); 217 | } 218 | } 219 | } 220 | } 221 | } 222 | 223 | impl Iterator for Parser { 224 | type Item = InternalEvent; 225 | 226 | fn next(&mut self) -> Option { 227 | self.internal_events.pop_front() 228 | } 229 | } 230 | -------------------------------------------------------------------------------- /src/event/source/unix/tty.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "libc")] 2 | use std::os::unix::prelude::AsRawFd; 3 | use std::{collections::VecDeque, io, os::unix::net::UnixStream, time::Duration}; 4 | 5 | #[cfg(not(feature = "libc"))] 6 | use rustix::fd::{AsFd, AsRawFd}; 7 | 8 | use signal_hook::low_level::pipe; 9 | 10 | use crate::event::timeout::PollTimeout; 11 | use crate::event::Event; 12 | use filedescriptor::{poll, pollfd, POLLIN}; 13 | 14 | #[cfg(feature = "event-stream")] 15 | use crate::event::sys::Waker; 16 | use crate::event::{source::EventSource, sys::unix::parse::parse_event, InternalEvent}; 17 | use crate::terminal::sys::file_descriptor::{tty_fd, FileDesc}; 18 | 19 | /// Holds a prototypical Waker and a receiver we can wait on when doing select(). 20 | #[cfg(feature = "event-stream")] 21 | struct WakePipe { 22 | receiver: UnixStream, 23 | waker: Waker, 24 | } 25 | 26 | #[cfg(feature = "event-stream")] 27 | impl WakePipe { 28 | fn new() -> io::Result { 29 | let (receiver, sender) = nonblocking_unix_pair()?; 30 | Ok(WakePipe { 31 | receiver, 32 | waker: Waker::new(sender), 33 | }) 34 | } 35 | } 36 | 37 | // I (@zrzka) wasn't able to read more than 1_022 bytes when testing 38 | // reading on macOS/Linux -> we don't need bigger buffer and 1k of bytes 39 | // is enough. 40 | const TTY_BUFFER_SIZE: usize = 1_024; 41 | 42 | pub(crate) struct UnixInternalEventSource { 43 | parser: Parser, 44 | tty_buffer: [u8; TTY_BUFFER_SIZE], 45 | tty: FileDesc<'static>, 46 | winch_signal_receiver: UnixStream, 47 | #[cfg(feature = "event-stream")] 48 | wake_pipe: WakePipe, 49 | } 50 | 51 | fn nonblocking_unix_pair() -> io::Result<(UnixStream, UnixStream)> { 52 | let (receiver, sender) = UnixStream::pair()?; 53 | receiver.set_nonblocking(true)?; 54 | sender.set_nonblocking(true)?; 55 | Ok((receiver, sender)) 56 | } 57 | 58 | impl UnixInternalEventSource { 59 | pub fn new() -> io::Result { 60 | UnixInternalEventSource::from_file_descriptor(tty_fd()?) 61 | } 62 | 63 | pub(crate) fn from_file_descriptor(input_fd: FileDesc<'static>) -> io::Result { 64 | Ok(UnixInternalEventSource { 65 | parser: Parser::default(), 66 | tty_buffer: [0u8; TTY_BUFFER_SIZE], 67 | tty: input_fd, 68 | winch_signal_receiver: { 69 | let (receiver, sender) = nonblocking_unix_pair()?; 70 | // Unregistering is unnecessary because EventSource is a singleton 71 | #[cfg(feature = "libc")] 72 | pipe::register(libc::SIGWINCH, sender)?; 73 | #[cfg(not(feature = "libc"))] 74 | pipe::register(rustix::process::Signal::WINCH.as_raw(), sender)?; 75 | receiver 76 | }, 77 | #[cfg(feature = "event-stream")] 78 | wake_pipe: WakePipe::new()?, 79 | }) 80 | } 81 | } 82 | 83 | /// read_complete reads from a non-blocking file descriptor 84 | /// until the buffer is full or it would block. 85 | /// 86 | /// Similar to `std::io::Read::read_to_end`, except this function 87 | /// only fills the given buffer and does not read beyond that. 88 | fn read_complete(fd: &FileDesc, buf: &mut [u8]) -> io::Result { 89 | loop { 90 | match fd.read(buf) { 91 | Ok(x) => return Ok(x), 92 | Err(e) => match e.kind() { 93 | io::ErrorKind::WouldBlock => return Ok(0), 94 | io::ErrorKind::Interrupted => continue, 95 | _ => return Err(e), 96 | }, 97 | } 98 | } 99 | } 100 | 101 | impl EventSource for UnixInternalEventSource { 102 | fn try_read(&mut self, timeout: Option) -> io::Result> { 103 | let timeout = PollTimeout::new(timeout); 104 | 105 | fn make_pollfd(fd: &F) -> pollfd { 106 | pollfd { 107 | fd: fd.as_raw_fd(), 108 | events: POLLIN, 109 | revents: 0, 110 | } 111 | } 112 | 113 | #[cfg(not(feature = "event-stream"))] 114 | let mut fds = [ 115 | make_pollfd(&self.tty), 116 | make_pollfd(&self.winch_signal_receiver), 117 | ]; 118 | 119 | #[cfg(feature = "event-stream")] 120 | let mut fds = [ 121 | make_pollfd(&self.tty), 122 | make_pollfd(&self.winch_signal_receiver), 123 | make_pollfd(&self.wake_pipe.receiver), 124 | ]; 125 | 126 | while timeout.leftover().map_or(true, |t| !t.is_zero()) { 127 | // check if there are buffered events from the last read 128 | if let Some(event) = self.parser.next() { 129 | return Ok(Some(event)); 130 | } 131 | match poll(&mut fds, timeout.leftover()) { 132 | Err(filedescriptor::Error::Poll(e)) | Err(filedescriptor::Error::Io(e)) => { 133 | match e.kind() { 134 | // retry on EINTR 135 | io::ErrorKind::Interrupted => continue, 136 | _ => return Err(e), 137 | } 138 | } 139 | Err(e) => { 140 | return Err(std::io::Error::new( 141 | std::io::ErrorKind::Other, 142 | format!("got unexpected error while polling: {:?}", e), 143 | )) 144 | } 145 | Ok(_) => (), 146 | }; 147 | if fds[0].revents & POLLIN != 0 { 148 | loop { 149 | let read_count = read_complete(&self.tty, &mut self.tty_buffer)?; 150 | if read_count > 0 { 151 | self.parser.advance( 152 | &self.tty_buffer[..read_count], 153 | read_count == TTY_BUFFER_SIZE, 154 | ); 155 | } 156 | 157 | if let Some(event) = self.parser.next() { 158 | return Ok(Some(event)); 159 | } 160 | 161 | if read_count == 0 { 162 | break; 163 | } 164 | } 165 | } 166 | if fds[1].revents & POLLIN != 0 { 167 | #[cfg(feature = "libc")] 168 | let fd = FileDesc::new(self.winch_signal_receiver.as_raw_fd(), false); 169 | #[cfg(not(feature = "libc"))] 170 | let fd = FileDesc::Borrowed(self.winch_signal_receiver.as_fd()); 171 | // drain the pipe 172 | while read_complete(&fd, &mut [0; 1024])? != 0 {} 173 | // TODO Should we remove tput? 174 | // 175 | // This can take a really long time, because terminal::size can 176 | // launch new process (tput) and then it parses its output. It's 177 | // not a really long time from the absolute time point of view, but 178 | // it's a really long time from the mio, async-std/tokio executor, ... 179 | // point of view. 180 | let new_size = crate::terminal::size()?; 181 | return Ok(Some(InternalEvent::Event(Event::Resize( 182 | new_size.0, new_size.1, 183 | )))); 184 | } 185 | 186 | #[cfg(feature = "event-stream")] 187 | if fds[2].revents & POLLIN != 0 { 188 | #[cfg(feature = "libc")] 189 | let fd = FileDesc::new(self.wake_pipe.receiver.as_raw_fd(), false); 190 | #[cfg(not(feature = "libc"))] 191 | let fd = FileDesc::Borrowed(self.wake_pipe.receiver.as_fd()); 192 | // drain the pipe 193 | while read_complete(&fd, &mut [0; 1024])? != 0 {} 194 | 195 | return Err(std::io::Error::new( 196 | std::io::ErrorKind::Interrupted, 197 | "Poll operation was woken up by `Waker::wake`", 198 | )); 199 | } 200 | } 201 | Ok(None) 202 | } 203 | 204 | #[cfg(feature = "event-stream")] 205 | fn waker(&self) -> Waker { 206 | self.wake_pipe.waker.clone() 207 | } 208 | } 209 | 210 | // 211 | // Following `Parser` structure exists for two reasons: 212 | // 213 | // * mimic anes Parser interface 214 | // * move the advancing, parsing, ... stuff out of the `try_read` method 215 | // 216 | #[derive(Debug)] 217 | struct Parser { 218 | buffer: Vec, 219 | internal_events: VecDeque, 220 | } 221 | 222 | impl Default for Parser { 223 | fn default() -> Self { 224 | Parser { 225 | // This buffer is used for -> 1 <- ANSI escape sequence. Are we 226 | // aware of any ANSI escape sequence that is bigger? Can we make 227 | // it smaller? 228 | // 229 | // Probably not worth spending more time on this as "there's a plan" 230 | // to use the anes crate parser. 231 | buffer: Vec::with_capacity(256), 232 | // TTY_BUFFER_SIZE is 1_024 bytes. How many ANSI escape sequences can 233 | // fit? What is an average sequence length? Let's guess here 234 | // and say that the average ANSI escape sequence length is 8 bytes. Thus 235 | // the buffer size should be 1024/8=128 to avoid additional allocations 236 | // when processing large amounts of data. 237 | // 238 | // There's no need to make it bigger, because when you look at the `try_read` 239 | // method implementation, all events are consumed before the next TTY_BUFFER 240 | // is processed -> events pushed. 241 | internal_events: VecDeque::with_capacity(128), 242 | } 243 | } 244 | } 245 | 246 | impl Parser { 247 | fn advance(&mut self, buffer: &[u8], more: bool) { 248 | for (idx, byte) in buffer.iter().enumerate() { 249 | let more = idx + 1 < buffer.len() || more; 250 | 251 | self.buffer.push(*byte); 252 | 253 | match parse_event(&self.buffer, more) { 254 | Ok(Some(ie)) => { 255 | self.internal_events.push_back(ie); 256 | self.buffer.clear(); 257 | } 258 | Ok(None) => { 259 | // Event can't be parsed, because we don't have enough bytes for 260 | // the current sequence. Keep the buffer and process next bytes. 261 | } 262 | Err(_) => { 263 | // Event can't be parsed (not enough parameters, parameter is not a number, ...). 264 | // Clear the buffer and continue with another sequence. 265 | self.buffer.clear(); 266 | } 267 | } 268 | } 269 | } 270 | } 271 | 272 | impl Iterator for Parser { 273 | type Item = InternalEvent; 274 | 275 | fn next(&mut self) -> Option { 276 | self.internal_events.pop_front() 277 | } 278 | } 279 | -------------------------------------------------------------------------------- /src/event/source/windows.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | use crossterm_winapi::{Console, Handle, InputRecord}; 4 | 5 | use crate::event::{ 6 | sys::windows::{parse::MouseButtonsPressed, poll::WinApiPoll}, 7 | Event, 8 | }; 9 | 10 | #[cfg(feature = "event-stream")] 11 | use crate::event::sys::Waker; 12 | use crate::event::{ 13 | source::EventSource, 14 | sys::windows::parse::{handle_key_event, handle_mouse_event}, 15 | timeout::PollTimeout, 16 | InternalEvent, 17 | }; 18 | 19 | pub(crate) struct WindowsEventSource { 20 | console: Console, 21 | poll: WinApiPoll, 22 | surrogate_buffer: Option, 23 | mouse_buttons_pressed: MouseButtonsPressed, 24 | } 25 | 26 | impl WindowsEventSource { 27 | pub(crate) fn new() -> std::io::Result { 28 | let console = Console::from(Handle::current_in_handle()?); 29 | Ok(WindowsEventSource { 30 | console, 31 | 32 | #[cfg(not(feature = "event-stream"))] 33 | poll: WinApiPoll::new(), 34 | #[cfg(feature = "event-stream")] 35 | poll: WinApiPoll::new()?, 36 | 37 | surrogate_buffer: None, 38 | mouse_buttons_pressed: MouseButtonsPressed::default(), 39 | }) 40 | } 41 | } 42 | 43 | impl EventSource for WindowsEventSource { 44 | fn try_read(&mut self, timeout: Option) -> std::io::Result> { 45 | let poll_timeout = PollTimeout::new(timeout); 46 | 47 | loop { 48 | if let Some(event_ready) = self.poll.poll(poll_timeout.leftover())? { 49 | let number = self.console.number_of_console_input_events()?; 50 | if event_ready && number != 0 { 51 | let event = match self.console.read_single_input_event()? { 52 | InputRecord::KeyEvent(record) => { 53 | handle_key_event(record, &mut self.surrogate_buffer) 54 | } 55 | InputRecord::MouseEvent(record) => { 56 | let mouse_event = 57 | handle_mouse_event(record, &self.mouse_buttons_pressed); 58 | self.mouse_buttons_pressed = MouseButtonsPressed { 59 | left: record.button_state.left_button(), 60 | right: record.button_state.right_button(), 61 | middle: record.button_state.middle_button(), 62 | }; 63 | 64 | mouse_event 65 | } 66 | InputRecord::WindowBufferSizeEvent(record) => { 67 | // windows starts counting at 0, unix at 1, add one to replicate unix behaviour. 68 | Some(Event::Resize( 69 | (record.size.x as i32 + 1) as u16, 70 | (record.size.y as i32 + 1) as u16, 71 | )) 72 | } 73 | InputRecord::FocusEvent(record) => { 74 | let event = if record.set_focus { 75 | Event::FocusGained 76 | } else { 77 | Event::FocusLost 78 | }; 79 | Some(event) 80 | } 81 | _ => None, 82 | }; 83 | 84 | if let Some(event) = event { 85 | return Ok(Some(InternalEvent::Event(event))); 86 | } 87 | } 88 | } 89 | 90 | if poll_timeout.elapsed() { 91 | return Ok(None); 92 | } 93 | } 94 | } 95 | 96 | #[cfg(feature = "event-stream")] 97 | fn waker(&self) -> Waker { 98 | self.poll.waker() 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/event/stream.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | io, 3 | pin::Pin, 4 | sync::{ 5 | atomic::{AtomicBool, Ordering}, 6 | mpsc::{self, SyncSender}, 7 | Arc, 8 | }, 9 | task::{Context, Poll}, 10 | thread, 11 | time::Duration, 12 | }; 13 | 14 | use futures_core::stream::Stream; 15 | 16 | use crate::event::{ 17 | filter::EventFilter, lock_internal_event_reader, poll_internal, read_internal, sys::Waker, 18 | Event, InternalEvent, 19 | }; 20 | 21 | /// A stream of `Result`. 22 | /// 23 | /// **This type is not available by default. You have to use the `event-stream` feature flag 24 | /// to make it available.** 25 | /// 26 | /// It implements the [Stream](futures_core::stream::Stream) 27 | /// trait and allows you to receive [`Event`]s with [`async-std`](https://crates.io/crates/async-std) 28 | /// or [`tokio`](https://crates.io/crates/tokio) crates. 29 | /// 30 | /// Check the [examples](https://github.com/crossterm-rs/crossterm/tree/master/examples) folder to see how to use 31 | /// it (`event-stream-*`). 32 | #[derive(Debug)] 33 | pub struct EventStream { 34 | poll_internal_waker: Waker, 35 | stream_wake_task_executed: Arc, 36 | stream_wake_task_should_shutdown: Arc, 37 | task_sender: SyncSender, 38 | } 39 | 40 | impl Default for EventStream { 41 | fn default() -> Self { 42 | let (task_sender, receiver) = mpsc::sync_channel::(1); 43 | 44 | thread::spawn(move || { 45 | while let Ok(task) = receiver.recv() { 46 | loop { 47 | if let Ok(true) = poll_internal(None, &EventFilter) { 48 | break; 49 | } 50 | 51 | if task.stream_wake_task_should_shutdown.load(Ordering::SeqCst) { 52 | break; 53 | } 54 | } 55 | task.stream_wake_task_executed 56 | .store(false, Ordering::SeqCst); 57 | task.stream_waker.wake(); 58 | } 59 | }); 60 | 61 | EventStream { 62 | poll_internal_waker: lock_internal_event_reader().waker(), 63 | stream_wake_task_executed: Arc::new(AtomicBool::new(false)), 64 | stream_wake_task_should_shutdown: Arc::new(AtomicBool::new(false)), 65 | task_sender, 66 | } 67 | } 68 | } 69 | 70 | impl EventStream { 71 | /// Constructs a new instance of `EventStream`. 72 | pub fn new() -> EventStream { 73 | EventStream::default() 74 | } 75 | } 76 | 77 | struct Task { 78 | stream_waker: std::task::Waker, 79 | stream_wake_task_executed: Arc, 80 | stream_wake_task_should_shutdown: Arc, 81 | } 82 | 83 | // Note to future me 84 | // 85 | // We need two wakers in order to implement EventStream correctly. 86 | // 87 | // 1. futures::Stream waker 88 | // 89 | // Stream::poll_next can return Poll::Pending which means that there's no 90 | // event available. We are going to spawn a thread with the 91 | // poll_internal(None, &EventFilter) call. This call blocks until an 92 | // event is available and then we have to wake up the executor with notification 93 | // that the task can be resumed. 94 | // 95 | // 2. poll_internal waker 96 | // 97 | // There's no event available, Poll::Pending was returned, stream waker thread 98 | // is up and sitting in the poll_internal. User wants to drop the EventStream. 99 | // We have to wake up the poll_internal (force it to return Ok(false)) and quit 100 | // the thread before we drop. 101 | impl Stream for EventStream { 102 | type Item = io::Result; 103 | 104 | fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { 105 | let result = match poll_internal(Some(Duration::from_secs(0)), &EventFilter) { 106 | Ok(true) => match read_internal(&EventFilter) { 107 | Ok(InternalEvent::Event(event)) => Poll::Ready(Some(Ok(event))), 108 | Err(e) => Poll::Ready(Some(Err(e))), 109 | #[cfg(unix)] 110 | _ => unreachable!(), 111 | }, 112 | Ok(false) => { 113 | if !self 114 | .stream_wake_task_executed 115 | .compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst) 116 | // https://github.com/rust-lang/rust/issues/80486#issuecomment-752244166 117 | .unwrap_or_else(|x| x) 118 | { 119 | let stream_waker = cx.waker().clone(); 120 | let stream_wake_task_executed = self.stream_wake_task_executed.clone(); 121 | let stream_wake_task_should_shutdown = 122 | self.stream_wake_task_should_shutdown.clone(); 123 | 124 | stream_wake_task_should_shutdown.store(false, Ordering::SeqCst); 125 | 126 | let _ = self.task_sender.send(Task { 127 | stream_waker, 128 | stream_wake_task_executed, 129 | stream_wake_task_should_shutdown, 130 | }); 131 | } 132 | Poll::Pending 133 | } 134 | Err(e) => Poll::Ready(Some(Err(e))), 135 | }; 136 | result 137 | } 138 | } 139 | 140 | impl Drop for EventStream { 141 | fn drop(&mut self) { 142 | self.stream_wake_task_should_shutdown 143 | .store(true, Ordering::SeqCst); 144 | let _ = self.poll_internal_waker.wake(); 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /src/event/sys.rs: -------------------------------------------------------------------------------- 1 | #[cfg(all(unix, feature = "event-stream"))] 2 | pub(crate) use unix::waker::Waker; 3 | #[cfg(all(windows, feature = "event-stream"))] 4 | pub(crate) use windows::waker::Waker; 5 | 6 | #[cfg(unix)] 7 | pub(crate) mod unix; 8 | #[cfg(windows)] 9 | pub(crate) mod windows; 10 | -------------------------------------------------------------------------------- /src/event/sys/unix.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "event-stream")] 2 | pub(crate) mod waker; 3 | 4 | #[cfg(feature = "events")] 5 | pub(crate) mod parse; 6 | -------------------------------------------------------------------------------- /src/event/sys/unix/waker.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "use-dev-tty")] 2 | pub(crate) mod tty; 3 | 4 | #[cfg(not(feature = "use-dev-tty"))] 5 | pub(crate) mod mio; 6 | 7 | #[cfg(feature = "use-dev-tty")] 8 | pub(crate) use self::tty::Waker; 9 | 10 | #[cfg(not(feature = "use-dev-tty"))] 11 | pub(crate) use self::mio::Waker; 12 | -------------------------------------------------------------------------------- /src/event/sys/unix/waker/mio.rs: -------------------------------------------------------------------------------- 1 | use std::sync::{Arc, Mutex}; 2 | 3 | use ::mio::{Registry, Token}; 4 | 5 | /// Allows to wake up the `mio::Poll::poll()` method. 6 | /// This type wraps `mio::Waker`, for more information see its documentation. 7 | #[derive(Clone, Debug)] 8 | pub(crate) struct Waker { 9 | inner: Arc>, 10 | } 11 | 12 | impl Waker { 13 | /// Create a new `Waker`. 14 | pub(crate) fn new(registry: &Registry, waker_token: Token) -> std::io::Result { 15 | Ok(Self { 16 | inner: Arc::new(Mutex::new(mio::Waker::new(registry, waker_token)?)), 17 | }) 18 | } 19 | 20 | /// Wake up the [`Poll`] associated with this `Waker`. 21 | /// 22 | /// Readiness is set to `Ready::readable()`. 23 | pub(crate) fn wake(&self) -> std::io::Result<()> { 24 | self.inner.lock().unwrap().wake() 25 | } 26 | 27 | /// Resets the state so the same waker can be reused. 28 | /// 29 | /// This function is not impl 30 | #[allow(dead_code, clippy::clippy::unnecessary_wraps)] 31 | pub(crate) fn reset(&self) -> std::io::Result<()> { 32 | Ok(()) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/event/sys/unix/waker/tty.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | io::{self, Write}, 3 | os::unix::net::UnixStream, 4 | sync::{Arc, Mutex}, 5 | }; 6 | 7 | /// Allows to wake up the EventSource::try_read() method. 8 | #[derive(Clone, Debug)] 9 | pub(crate) struct Waker { 10 | inner: Arc>, 11 | } 12 | 13 | impl Waker { 14 | /// Create a new `Waker`. 15 | pub(crate) fn new(writer: UnixStream) -> Self { 16 | Self { 17 | inner: Arc::new(Mutex::new(writer)), 18 | } 19 | } 20 | 21 | /// Wake up the [`Poll`] associated with this `Waker`. 22 | /// 23 | /// Readiness is set to `Ready::readable()`. 24 | pub(crate) fn wake(&self) -> io::Result<()> { 25 | self.inner.lock().unwrap().write_all(&[0])?; 26 | Ok(()) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/event/sys/windows.rs: -------------------------------------------------------------------------------- 1 | //! This is a WINDOWS specific implementation for input related action. 2 | 3 | use std::convert::TryFrom; 4 | use std::io; 5 | use std::sync::atomic::{AtomicU64, Ordering}; 6 | 7 | use crossterm_winapi::{ConsoleMode, Handle}; 8 | 9 | pub(crate) mod parse; 10 | pub(crate) mod poll; 11 | #[cfg(feature = "event-stream")] 12 | pub(crate) mod waker; 13 | 14 | const ENABLE_MOUSE_MODE: u32 = 0x0010 | 0x0080 | 0x0008; 15 | 16 | /// This is a either `u64::MAX` if it's uninitialized or a valid `u32` that stores the original 17 | /// console mode if it's initialized. 18 | static ORIGINAL_CONSOLE_MODE: AtomicU64 = AtomicU64::new(u64::MAX); 19 | 20 | /// Initializes the default console color. It will will be skipped if it has already been initialized. 21 | fn init_original_console_mode(original_mode: u32) { 22 | let _ = ORIGINAL_CONSOLE_MODE.compare_exchange( 23 | u64::MAX, 24 | u64::from(original_mode), 25 | Ordering::Relaxed, 26 | Ordering::Relaxed, 27 | ); 28 | } 29 | 30 | /// Returns the original console color, make sure to call `init_console_color` before calling this function. Otherwise this function will panic. 31 | fn original_console_mode() -> std::io::Result { 32 | u32::try_from(ORIGINAL_CONSOLE_MODE.load(Ordering::Relaxed)) 33 | .map_err(|_| io::Error::new(io::ErrorKind::Other, "Initial console modes not set")) 34 | } 35 | 36 | pub(crate) fn enable_mouse_capture() -> std::io::Result<()> { 37 | let mode = ConsoleMode::from(Handle::current_in_handle()?); 38 | init_original_console_mode(mode.mode()?); 39 | mode.set_mode(ENABLE_MOUSE_MODE)?; 40 | 41 | Ok(()) 42 | } 43 | 44 | pub(crate) fn disable_mouse_capture() -> std::io::Result<()> { 45 | let mode = ConsoleMode::from(Handle::current_in_handle()?); 46 | mode.set_mode(original_console_mode()?)?; 47 | Ok(()) 48 | } 49 | -------------------------------------------------------------------------------- /src/event/sys/windows/poll.rs: -------------------------------------------------------------------------------- 1 | use std::io; 2 | use std::time::Duration; 3 | 4 | use crossterm_winapi::Handle; 5 | use winapi::{ 6 | shared::winerror::WAIT_TIMEOUT, 7 | um::{ 8 | synchapi::WaitForMultipleObjects, 9 | winbase::{INFINITE, WAIT_ABANDONED_0, WAIT_FAILED, WAIT_OBJECT_0}, 10 | }, 11 | }; 12 | 13 | #[cfg(feature = "event-stream")] 14 | pub(crate) use super::waker::Waker; 15 | 16 | #[derive(Debug)] 17 | pub(crate) struct WinApiPoll { 18 | #[cfg(feature = "event-stream")] 19 | waker: Waker, 20 | } 21 | 22 | impl WinApiPoll { 23 | #[cfg(not(feature = "event-stream"))] 24 | pub(crate) fn new() -> WinApiPoll { 25 | WinApiPoll {} 26 | } 27 | 28 | #[cfg(feature = "event-stream")] 29 | pub(crate) fn new() -> std::io::Result { 30 | Ok(WinApiPoll { 31 | waker: Waker::new()?, 32 | }) 33 | } 34 | } 35 | 36 | impl WinApiPoll { 37 | pub fn poll(&mut self, timeout: Option) -> std::io::Result> { 38 | let dw_millis = if let Some(duration) = timeout { 39 | duration.as_millis() as u32 40 | } else { 41 | INFINITE 42 | }; 43 | 44 | let console_handle = Handle::current_in_handle()?; 45 | 46 | #[cfg(feature = "event-stream")] 47 | let semaphore = self.waker.semaphore(); 48 | #[cfg(feature = "event-stream")] 49 | let handles = &[*console_handle, **semaphore.handle()]; 50 | #[cfg(not(feature = "event-stream"))] 51 | let handles = &[*console_handle]; 52 | 53 | let output = 54 | unsafe { WaitForMultipleObjects(handles.len() as u32, handles.as_ptr(), 0, dw_millis) }; 55 | 56 | match output { 57 | output if output == WAIT_OBJECT_0 => { 58 | // input handle triggered 59 | Ok(Some(true)) 60 | } 61 | #[cfg(feature = "event-stream")] 62 | output if output == WAIT_OBJECT_0 + 1 => { 63 | // semaphore handle triggered 64 | let _ = self.waker.reset(); 65 | Err(io::Error::new( 66 | io::ErrorKind::Interrupted, 67 | "Poll operation was woken up by `Waker::wake`", 68 | )) 69 | } 70 | WAIT_TIMEOUT | WAIT_ABANDONED_0 => { 71 | // timeout elapsed 72 | Ok(None) 73 | } 74 | WAIT_FAILED => Err(io::Error::last_os_error()), 75 | _ => Err(io::Error::new( 76 | io::ErrorKind::Other, 77 | "WaitForMultipleObjects returned unexpected result.", 78 | )), 79 | } 80 | } 81 | 82 | #[cfg(feature = "event-stream")] 83 | pub fn waker(&self) -> Waker { 84 | self.waker.clone() 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/event/sys/windows/waker.rs: -------------------------------------------------------------------------------- 1 | use std::sync::{Arc, Mutex}; 2 | 3 | use crossterm_winapi::Semaphore; 4 | 5 | /// Allows to wake up the `WinApiPoll::poll()` method. 6 | #[derive(Clone, Debug)] 7 | pub(crate) struct Waker { 8 | inner: Arc>, 9 | } 10 | 11 | impl Waker { 12 | /// Creates a new waker. 13 | /// 14 | /// `Waker` is based on the `Semaphore`. You have to use the semaphore 15 | /// handle along with the `WaitForMultipleObjects`. 16 | pub(crate) fn new() -> std::io::Result { 17 | let inner = Semaphore::new()?; 18 | 19 | Ok(Self { 20 | inner: Arc::new(Mutex::new(inner)), 21 | }) 22 | } 23 | 24 | /// Wakes the `WaitForMultipleObjects`. 25 | pub(crate) fn wake(&self) -> std::io::Result<()> { 26 | self.inner.lock().unwrap().release()?; 27 | Ok(()) 28 | } 29 | 30 | /// Replaces the current semaphore with a new one allowing us to reuse the same `Waker`. 31 | pub(crate) fn reset(&self) -> std::io::Result<()> { 32 | *self.inner.lock().unwrap() = Semaphore::new()?; 33 | Ok(()) 34 | } 35 | 36 | /// Returns the semaphore associated with the waker. 37 | pub(crate) fn semaphore(&self) -> Semaphore { 38 | self.inner.lock().unwrap().clone() 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/event/timeout.rs: -------------------------------------------------------------------------------- 1 | use std::time::{Duration, Instant}; 2 | 3 | /// Keeps track of the elapsed time since the moment the polling started. 4 | #[derive(Debug, Clone)] 5 | pub struct PollTimeout { 6 | timeout: Option, 7 | start: Instant, 8 | } 9 | 10 | impl PollTimeout { 11 | /// Constructs a new `PollTimeout` with the given optional `Duration`. 12 | pub fn new(timeout: Option) -> PollTimeout { 13 | PollTimeout { 14 | timeout, 15 | start: Instant::now(), 16 | } 17 | } 18 | 19 | /// Returns whether the timeout has elapsed. 20 | /// 21 | /// It always returns `false` if the initial timeout was set to `None`. 22 | pub fn elapsed(&self) -> bool { 23 | self.timeout 24 | .map(|timeout| self.start.elapsed() >= timeout) 25 | .unwrap_or(false) 26 | } 27 | 28 | /// Returns the timeout leftover (initial timeout duration - elapsed duration). 29 | pub fn leftover(&self) -> Option { 30 | self.timeout.map(|timeout| { 31 | let elapsed = self.start.elapsed(); 32 | 33 | if elapsed >= timeout { 34 | Duration::from_secs(0) 35 | } else { 36 | timeout - elapsed 37 | } 38 | }) 39 | } 40 | } 41 | 42 | #[cfg(test)] 43 | mod tests { 44 | use std::time::{Duration, Instant}; 45 | 46 | use super::PollTimeout; 47 | 48 | #[test] 49 | pub fn test_timeout_without_duration_does_not_have_leftover() { 50 | let timeout = PollTimeout::new(None); 51 | assert_eq!(timeout.leftover(), None) 52 | } 53 | 54 | #[test] 55 | pub fn test_timeout_without_duration_never_elapses() { 56 | let timeout = PollTimeout::new(None); 57 | assert!(!timeout.elapsed()); 58 | } 59 | 60 | #[test] 61 | pub fn test_timeout_elapses() { 62 | const TIMEOUT_MILLIS: u64 = 100; 63 | 64 | let timeout = PollTimeout { 65 | timeout: Some(Duration::from_millis(TIMEOUT_MILLIS)), 66 | start: Instant::now() - Duration::from_millis(2 * TIMEOUT_MILLIS), 67 | }; 68 | 69 | assert!(timeout.elapsed()); 70 | } 71 | 72 | #[test] 73 | pub fn test_elapsed_timeout_has_zero_leftover() { 74 | const TIMEOUT_MILLIS: u64 = 100; 75 | 76 | let timeout = PollTimeout { 77 | timeout: Some(Duration::from_millis(TIMEOUT_MILLIS)), 78 | start: Instant::now() - Duration::from_millis(2 * TIMEOUT_MILLIS), 79 | }; 80 | 81 | assert!(timeout.elapsed()); 82 | assert_eq!(timeout.leftover(), Some(Duration::from_millis(0))); 83 | } 84 | 85 | #[test] 86 | pub fn test_not_elapsed_timeout_has_positive_leftover() { 87 | let timeout = PollTimeout::new(Some(Duration::from_secs(60))); 88 | 89 | assert!(!timeout.elapsed()); 90 | assert!(timeout.leftover().unwrap() > Duration::from_secs(0)); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![deny(unused_imports, unused_must_use)] 2 | 3 | //! # Cross-platform Terminal Manipulation Library 4 | //! 5 | //! Crossterm is a pure-rust, terminal manipulation library that makes it possible to write cross-platform text-based interfaces. 6 | //! 7 | //! This crate supports all UNIX and Windows terminals down to Windows 7 (not all terminals are tested 8 | //! see [Tested Terminals](https://github.com/crossterm-rs/crossterm#tested-terminals) 9 | //! for more info). 10 | //! 11 | //! ## Command API 12 | //! 13 | //! The command API makes the use of `crossterm` much easier and offers more control over when and how a 14 | //! command is executed. A command is just an action you can perform on the terminal e.g. cursor movement. 15 | //! 16 | //! The command API offers: 17 | //! 18 | //! * Better Performance. 19 | //! * Complete control over when to flush. 20 | //! * Complete control over where the ANSI escape commands are executed to. 21 | //! * Way easier and nicer API. 22 | //! 23 | //! There are two ways to use the API command: 24 | //! 25 | //! * Functions can execute commands on types that implement Write. Functions are easier to use and debug. 26 | //! There is a disadvantage, and that is that there is a boilerplate code involved. 27 | //! * Macros are generally seen as more difficult and aren't always well supported by editors but offer an API with less boilerplate code. If you are 28 | //! not afraid of macros, this is a recommendation. 29 | //! 30 | //! Linux and Windows 10 systems support ANSI escape codes. Those ANSI escape codes are strings or rather a 31 | //! byte sequence. When we `write` and `flush` those to the terminal we can perform some action. 32 | //! For older windows systems a WinAPI call is made. 33 | //! 34 | //! ### Supported Commands 35 | //! 36 | //! - Module [`cursor`](cursor/index.html) 37 | //! - Visibility - [`Show`](cursor/struct.Show.html), [`Hide`](cursor/struct.Hide.html) 38 | //! - Appearance - [`EnableBlinking`](cursor/struct.EnableBlinking.html), 39 | //! [`DisableBlinking`](cursor/struct.DisableBlinking.html), 40 | //! [`SetCursorStyle`](cursor/enum.SetCursorStyle.html) 41 | //! - Position - 42 | //! [`SavePosition`](cursor/struct.SavePosition.html), [`RestorePosition`](cursor/struct.RestorePosition.html), 43 | //! [`MoveUp`](cursor/struct.MoveUp.html), [`MoveDown`](cursor/struct.MoveDown.html), 44 | //! [`MoveLeft`](cursor/struct.MoveLeft.html), [`MoveRight`](cursor/struct.MoveRight.html), 45 | //! [`MoveTo`](cursor/struct.MoveTo.html), [`MoveToColumn`](cursor/struct.MoveToColumn.html),[`MoveToRow`](cursor/struct.MoveToRow.html), 46 | //! [`MoveToNextLine`](cursor/struct.MoveToNextLine.html), [`MoveToPreviousLine`](cursor/struct.MoveToPreviousLine.html) 47 | //! - Module [`event`](event/index.html) 48 | //! - Keyboard events - 49 | //! [`PushKeyboardEnhancementFlags`](event/struct.PushKeyboardEnhancementFlags.html), 50 | //! [`PopKeyboardEnhancementFlags`](event/struct.PopKeyboardEnhancementFlags.html) 51 | //! - Mouse events - [`EnableMouseCapture`](event/struct.EnableMouseCapture.html), 52 | //! [`DisableMouseCapture`](event/struct.DisableMouseCapture.html) 53 | //! - Module [`style`](style/index.html) 54 | //! - Colors - [`SetForegroundColor`](style/struct.SetForegroundColor.html), 55 | //! [`SetBackgroundColor`](style/struct.SetBackgroundColor.html), 56 | //! [`ResetColor`](style/struct.ResetColor.html), [`SetColors`](style/struct.SetColors.html) 57 | //! - Attributes - [`SetAttribute`](style/struct.SetAttribute.html), [`SetAttributes`](style/struct.SetAttributes.html), 58 | //! [`PrintStyledContent`](style/struct.PrintStyledContent.html) 59 | //! - Module [`terminal`](terminal/index.html) 60 | //! - Scrolling - [`ScrollUp`](terminal/struct.ScrollUp.html), 61 | //! [`ScrollDown`](terminal/struct.ScrollDown.html) 62 | //! - Miscellaneous - [`Clear`](terminal/struct.Clear.html), 63 | //! [`SetSize`](terminal/struct.SetSize.html), 64 | //! [`SetTitle`](terminal/struct.SetTitle.html), 65 | //! [`DisableLineWrap`](terminal/struct.DisableLineWrap.html), 66 | //! [`EnableLineWrap`](terminal/struct.EnableLineWrap.html) 67 | //! - Alternate screen - [`EnterAlternateScreen`](terminal/struct.EnterAlternateScreen.html), 68 | //! [`LeaveAlternateScreen`](terminal/struct.LeaveAlternateScreen.html) 69 | //! - Module [`clipboard`](clipboard/index.html) (requires 70 | //! [`feature = "osc52"`](#optional-features)) 71 | //! - Clipboard - [`CopyToClipboard`](clipboard/struct.CopyToClipboard.html) 72 | //! 73 | //! ### Command Execution 74 | //! 75 | //! There are two different ways to execute commands: 76 | //! 77 | //! * [Lazy Execution](#lazy-execution) 78 | //! * [Direct Execution](#direct-execution) 79 | //! 80 | //! #### Lazy Execution 81 | //! 82 | //! Flushing bytes to the terminal buffer is a heavy system call. If we perform a lot of actions with the terminal, 83 | //! we want to do this periodically - like with a TUI editor - so that we can flush more data to the terminal buffer 84 | //! at the same time. 85 | //! 86 | //! Crossterm offers the possibility to do this with `queue`. 87 | //! With `queue` you can queue commands, and when you call [Write::flush][flush] these commands will be executed. 88 | //! 89 | //! You can pass a custom buffer implementing [std::io::Write][write] to this `queue` operation. 90 | //! The commands will be executed on that buffer. 91 | //! The most common buffer is [std::io::stdout][stdout] however, [std::io::stderr][stderr] is used sometimes as well. 92 | //! 93 | //! ##### Examples 94 | //! 95 | //! A simple demonstration that shows the command API in action with cursor commands. 96 | //! 97 | //! Functions: 98 | //! 99 | //! ```no_run 100 | //! use std::io::{Write, stdout}; 101 | //! use crossterm::{QueueableCommand, cursor}; 102 | //! 103 | //! let mut stdout = stdout(); 104 | //! stdout.queue(cursor::MoveTo(5,5)); 105 | //! 106 | //! // some other code ... 107 | //! 108 | //! stdout.flush(); 109 | //! ``` 110 | //! 111 | //! The [queue](./trait.QueueableCommand.html) function returns itself, therefore you can use this to queue another 112 | //! command. Like `stdout.queue(Goto(5,5)).queue(Clear(ClearType::All))`. 113 | //! 114 | //! Macros: 115 | //! 116 | //! ```no_run 117 | //! use std::io::{Write, stdout}; 118 | //! use crossterm::{queue, QueueableCommand, cursor}; 119 | //! 120 | //! let mut stdout = stdout(); 121 | //! queue!(stdout, cursor::MoveTo(5, 5)); 122 | //! 123 | //! // some other code ... 124 | //! 125 | //! // move operation is performed only if we flush the buffer. 126 | //! stdout.flush(); 127 | //! ``` 128 | //! 129 | //! You can pass more than one command into the [queue](./macro.queue.html) macro like 130 | //! `queue!(stdout, MoveTo(5, 5), Clear(ClearType::All))` and 131 | //! they will be executed in the given order from left to right. 132 | //! 133 | //! #### Direct Execution 134 | //! 135 | //! For many applications it is not at all important to be efficient with 'flush' operations. 136 | //! For this use case there is the `execute` operation. 137 | //! This operation executes the command immediately, and calls the `flush` under water. 138 | //! 139 | //! You can pass a custom buffer implementing [std::io::Write][write] to this `execute` operation. 140 | //! The commands will be executed on that buffer. 141 | //! The most common buffer is [std::io::stdout][stdout] however, [std::io::stderr][stderr] is used sometimes as well. 142 | //! 143 | //! ##### Examples 144 | //! 145 | //! Functions: 146 | //! 147 | //! ```no_run 148 | //! use std::io::{Write, stdout}; 149 | //! use crossterm::{ExecutableCommand, cursor}; 150 | //! 151 | //! let mut stdout = stdout(); 152 | //! stdout.execute(cursor::MoveTo(5,5)); 153 | //! ``` 154 | //! The [execute](./trait.ExecutableCommand.html) function returns itself, therefore you can use this to queue 155 | //! another command. Like `stdout.execute(Goto(5,5))?.execute(Clear(ClearType::All))`. 156 | //! 157 | //! Macros: 158 | //! 159 | //! ```no_run 160 | //! use std::io::{stdout, Write}; 161 | //! use crossterm::{execute, ExecutableCommand, cursor}; 162 | //! 163 | //! let mut stdout = stdout(); 164 | //! execute!(stdout, cursor::MoveTo(5, 5)); 165 | //! ``` 166 | //! You can pass more than one command into the [execute](./macro.execute.html) macro like 167 | //! `execute!(stdout, MoveTo(5, 5), Clear(ClearType::All))` and they will be executed in the given order from 168 | //! left to right. 169 | //! 170 | //! ## Examples 171 | //! 172 | //! Print a rectangle colored with magenta and use both direct execution and lazy execution. 173 | //! 174 | //! Functions: 175 | //! 176 | //! ```no_run 177 | //! use std::io::{self, Write}; 178 | //! use crossterm::{ 179 | //! ExecutableCommand, QueueableCommand, 180 | //! terminal, cursor, style::{self, Stylize} 181 | //! }; 182 | //! 183 | //! fn main() -> io::Result<()> { 184 | //! let mut stdout = io::stdout(); 185 | //! 186 | //! stdout.execute(terminal::Clear(terminal::ClearType::All))?; 187 | //! 188 | //! for y in 0..40 { 189 | //! for x in 0..150 { 190 | //! if (y == 0 || y == 40 - 1) || (x == 0 || x == 150 - 1) { 191 | //! // in this loop we are more efficient by not flushing the buffer. 192 | //! stdout 193 | //! .queue(cursor::MoveTo(x,y))? 194 | //! .queue(style::PrintStyledContent( "█".magenta()))?; 195 | //! } 196 | //! } 197 | //! } 198 | //! stdout.flush()?; 199 | //! Ok(()) 200 | //! } 201 | //! ``` 202 | //! 203 | //! Macros: 204 | //! 205 | //! ```no_run 206 | //! use std::io::{self, Write}; 207 | //! use crossterm::{ 208 | //! execute, queue, 209 | //! style::{self, Stylize}, cursor, terminal 210 | //! }; 211 | //! 212 | //! fn main() -> io::Result<()> { 213 | //! let mut stdout = io::stdout(); 214 | //! 215 | //! execute!(stdout, terminal::Clear(terminal::ClearType::All))?; 216 | //! 217 | //! for y in 0..40 { 218 | //! for x in 0..150 { 219 | //! if (y == 0 || y == 40 - 1) || (x == 0 || x == 150 - 1) { 220 | //! // in this loop we are more efficient by not flushing the buffer. 221 | //! queue!(stdout, cursor::MoveTo(x,y), style::PrintStyledContent( "█".magenta()))?; 222 | //! } 223 | //! } 224 | //! } 225 | //! stdout.flush()?; 226 | //! Ok(()) 227 | //! } 228 | //!``` 229 | //! 230 | #![cfg_attr(feature = "document-features", doc = "## Feature Flags")] 231 | #![cfg_attr(feature = "document-features", doc = document_features::document_features!())] 232 | //! 233 | //! [write]: https://doc.rust-lang.org/std/io/trait.Write.html 234 | //! [stdout]: https://doc.rust-lang.org/std/io/fn.stdout.html 235 | //! [stderr]: https://doc.rust-lang.org/std/io/fn.stderr.html 236 | //! [flush]: https://doc.rust-lang.org/std/io/trait.Write.html#tymethod.flush 237 | 238 | pub use crate::command::{Command, ExecutableCommand, QueueableCommand, SynchronizedUpdate}; 239 | 240 | /// A module to work with the terminal cursor 241 | pub mod cursor; 242 | /// A module to read events. 243 | #[cfg(feature = "events")] 244 | pub mod event; 245 | /// A module to apply attributes and colors on your text. 246 | pub mod style; 247 | /// A module to work with the terminal. 248 | pub mod terminal; 249 | 250 | /// A module to query if the current instance is a tty. 251 | pub mod tty; 252 | 253 | /// A module for clipboard interaction 254 | #[cfg(feature = "osc52")] 255 | pub mod clipboard; 256 | 257 | #[cfg(windows)] 258 | /// A module that exposes one function to check if the current terminal supports ANSI sequences. 259 | pub mod ansi_support; 260 | mod command; 261 | pub(crate) mod macros; 262 | 263 | #[cfg(all(windows, not(feature = "windows")))] 264 | compile_error!("Compiling on Windows with \"windows\" feature disabled. Feature \"windows\" should only be disabled when project will never be compiled on Windows."); 265 | -------------------------------------------------------------------------------- /src/style/attributes.rs: -------------------------------------------------------------------------------- 1 | use std::ops::{BitAnd, BitOr, BitXor}; 2 | 3 | use crate::style::Attribute; 4 | 5 | /// a bitset for all possible attributes 6 | #[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] 7 | pub struct Attributes(u32); 8 | 9 | impl From for Attributes { 10 | fn from(attribute: Attribute) -> Self { 11 | Self(attribute.bytes()) 12 | } 13 | } 14 | 15 | impl From<&[Attribute]> for Attributes { 16 | fn from(arr: &[Attribute]) -> Self { 17 | let mut attributes = Attributes::default(); 18 | for &attr in arr { 19 | attributes.set(attr); 20 | } 21 | attributes 22 | } 23 | } 24 | 25 | impl BitAnd for Attributes { 26 | type Output = Self; 27 | fn bitand(self, rhs: Attribute) -> Self { 28 | Self(self.0 & rhs.bytes()) 29 | } 30 | } 31 | impl BitAnd for Attributes { 32 | type Output = Self; 33 | fn bitand(self, rhs: Self) -> Self { 34 | Self(self.0 & rhs.0) 35 | } 36 | } 37 | 38 | impl BitOr for Attributes { 39 | type Output = Self; 40 | fn bitor(self, rhs: Attribute) -> Self { 41 | Self(self.0 | rhs.bytes()) 42 | } 43 | } 44 | impl BitOr for Attributes { 45 | type Output = Self; 46 | fn bitor(self, rhs: Self) -> Self { 47 | Self(self.0 | rhs.0) 48 | } 49 | } 50 | 51 | impl BitXor for Attributes { 52 | type Output = Self; 53 | fn bitxor(self, rhs: Attribute) -> Self { 54 | Self(self.0 ^ rhs.bytes()) 55 | } 56 | } 57 | impl BitXor for Attributes { 58 | type Output = Self; 59 | fn bitxor(self, rhs: Self) -> Self { 60 | Self(self.0 ^ rhs.0) 61 | } 62 | } 63 | 64 | impl Attributes { 65 | /// Returns the empty bitset. 66 | #[inline(always)] 67 | pub const fn none() -> Self { 68 | Self(0) 69 | } 70 | 71 | /// Returns a copy of the bitset with the given attribute set. 72 | /// If it's already set, this returns the bitset unmodified. 73 | #[inline(always)] 74 | pub const fn with(self, attribute: Attribute) -> Self { 75 | Self(self.0 | attribute.bytes()) 76 | } 77 | 78 | /// Returns a copy of the bitset with the given attribute unset. 79 | /// If it's not set, this returns the bitset unmodified. 80 | #[inline(always)] 81 | pub const fn without(self, attribute: Attribute) -> Self { 82 | Self(self.0 & !attribute.bytes()) 83 | } 84 | 85 | /// Sets the attribute. 86 | /// If it's already set, this does nothing. 87 | #[inline(always)] 88 | pub fn set(&mut self, attribute: Attribute) { 89 | self.0 |= attribute.bytes(); 90 | } 91 | 92 | /// Unsets the attribute. 93 | /// If it's not set, this changes nothing. 94 | #[inline(always)] 95 | pub fn unset(&mut self, attribute: Attribute) { 96 | self.0 &= !attribute.bytes(); 97 | } 98 | 99 | /// Sets the attribute if it's unset, unset it 100 | /// if it is set. 101 | #[inline(always)] 102 | pub fn toggle(&mut self, attribute: Attribute) { 103 | self.0 ^= attribute.bytes(); 104 | } 105 | 106 | /// Returns whether the attribute is set. 107 | #[inline(always)] 108 | pub const fn has(self, attribute: Attribute) -> bool { 109 | self.0 & attribute.bytes() != 0 110 | } 111 | 112 | /// Sets all the passed attributes. Removes none. 113 | #[inline(always)] 114 | pub fn extend(&mut self, attributes: Attributes) { 115 | self.0 |= attributes.0; 116 | } 117 | 118 | /// Returns whether there is no attribute set. 119 | #[inline(always)] 120 | pub const fn is_empty(self) -> bool { 121 | self.0 == 0 122 | } 123 | } 124 | 125 | #[cfg(test)] 126 | mod tests { 127 | use super::{Attribute, Attributes}; 128 | 129 | #[test] 130 | fn test_attributes() { 131 | let mut attributes: Attributes = Attribute::Bold.into(); 132 | assert!(attributes.has(Attribute::Bold)); 133 | attributes.set(Attribute::Italic); 134 | assert!(attributes.has(Attribute::Italic)); 135 | attributes.unset(Attribute::Italic); 136 | assert!(!attributes.has(Attribute::Italic)); 137 | attributes.toggle(Attribute::Bold); 138 | assert!(attributes.is_empty()); 139 | } 140 | 141 | #[test] 142 | fn test_attributes_const() { 143 | const ATTRIBUTES: Attributes = Attributes::none() 144 | .with(Attribute::Bold) 145 | .with(Attribute::Italic) 146 | .without(Attribute::Bold); 147 | assert!(!ATTRIBUTES.has(Attribute::Bold)); 148 | assert!(ATTRIBUTES.has(Attribute::Italic)); 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /src/style/content_style.rs: -------------------------------------------------------------------------------- 1 | //! This module contains the `content style` that can be applied to an `styled content`. 2 | 3 | use std::fmt::Display; 4 | 5 | use crate::style::{Attributes, Color, StyledContent}; 6 | 7 | /// The style that can be put on content. 8 | #[derive(Debug, Copy, Clone, Default, PartialEq, Eq)] 9 | pub struct ContentStyle { 10 | /// The foreground color. 11 | pub foreground_color: Option, 12 | /// The background color. 13 | pub background_color: Option, 14 | /// The underline color. 15 | pub underline_color: Option, 16 | /// List of attributes. 17 | pub attributes: Attributes, 18 | } 19 | 20 | impl ContentStyle { 21 | /// Creates a `StyledContent` by applying the style to the given `val`. 22 | #[inline] 23 | pub fn apply(self, val: D) -> StyledContent { 24 | StyledContent::new(self, val) 25 | } 26 | 27 | /// Creates a new `ContentStyle`. 28 | #[inline] 29 | pub fn new() -> ContentStyle { 30 | ContentStyle::default() 31 | } 32 | } 33 | 34 | impl AsRef for ContentStyle { 35 | fn as_ref(&self) -> &Self { 36 | self 37 | } 38 | } 39 | impl AsMut for ContentStyle { 40 | fn as_mut(&mut self) -> &mut Self { 41 | self 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/style/styled_content.rs: -------------------------------------------------------------------------------- 1 | //! This module contains the logic to style some content. 2 | 3 | use std::fmt::{self, Display, Formatter}; 4 | 5 | use super::{ContentStyle, PrintStyledContent}; 6 | 7 | /// The style with the content to be styled. 8 | /// 9 | /// # Examples 10 | /// 11 | /// ```rust 12 | /// use crossterm::style::{style, Color, Attribute, Stylize}; 13 | /// 14 | /// let styled = "Hello there" 15 | /// .with(Color::Yellow) 16 | /// .on(Color::Blue) 17 | /// .attribute(Attribute::Bold); 18 | /// 19 | /// println!("{}", styled); 20 | /// ``` 21 | #[derive(Copy, Clone, Debug, Eq, PartialEq)] 22 | pub struct StyledContent { 23 | /// The style (colors, content attributes). 24 | style: ContentStyle, 25 | /// A content to apply the style on. 26 | content: D, 27 | } 28 | 29 | impl StyledContent { 30 | /// Creates a new `StyledContent`. 31 | #[inline] 32 | pub fn new(style: ContentStyle, content: D) -> StyledContent { 33 | StyledContent { style, content } 34 | } 35 | 36 | /// Returns the content. 37 | #[inline] 38 | pub fn content(&self) -> &D { 39 | &self.content 40 | } 41 | 42 | /// Returns the style. 43 | #[inline] 44 | pub fn style(&self) -> &ContentStyle { 45 | &self.style 46 | } 47 | 48 | /// Returns a mutable reference to the style, so that it can be further 49 | /// manipulated 50 | #[inline] 51 | pub fn style_mut(&mut self) -> &mut ContentStyle { 52 | &mut self.style 53 | } 54 | } 55 | 56 | impl AsRef for StyledContent { 57 | fn as_ref(&self) -> &ContentStyle { 58 | &self.style 59 | } 60 | } 61 | impl AsMut for StyledContent { 62 | fn as_mut(&mut self) -> &mut ContentStyle { 63 | &mut self.style 64 | } 65 | } 66 | 67 | impl Display for StyledContent { 68 | fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { 69 | crate::command::execute_fmt( 70 | f, 71 | PrintStyledContent(StyledContent { 72 | style: self.style, 73 | content: &self.content, 74 | }), 75 | ) 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/style/stylize.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::Display; 2 | 3 | use super::{style, Attribute, Color, ContentStyle, StyledContent}; 4 | 5 | macro_rules! stylize_method { 6 | ($method_name:ident Attribute::$attribute:ident) => { 7 | calculated_docs! { 8 | #[doc = concat!( 9 | "Applies the [`", 10 | stringify!($attribute), 11 | "`](Attribute::", 12 | stringify!($attribute), 13 | ") attribute to the text.", 14 | )] 15 | fn $method_name(self) -> Self::Styled { 16 | self.attribute(Attribute::$attribute) 17 | } 18 | } 19 | }; 20 | ($method_name_fg:ident, $method_name_bg:ident, $method_name_ul:ident Color::$color:ident) => { 21 | calculated_docs! { 22 | #[doc = concat!( 23 | "Sets the foreground color to [`", 24 | stringify!($color), 25 | "`](Color::", 26 | stringify!($color), 27 | ")." 28 | )] 29 | fn $method_name_fg(self) -> Self::Styled { 30 | self.with(Color::$color) 31 | } 32 | 33 | #[doc = concat!( 34 | "Sets the background color to [`", 35 | stringify!($color), 36 | "`](Color::", 37 | stringify!($color), 38 | ")." 39 | )] 40 | fn $method_name_bg(self) -> Self::Styled { 41 | self.on(Color::$color) 42 | } 43 | 44 | #[doc = concat!( 45 | "Sets the underline color to [`", 46 | stringify!($color), 47 | "`](Color::", 48 | stringify!($color), 49 | ")." 50 | )] 51 | fn $method_name_ul(self) -> Self::Styled { 52 | self.underline(Color::$color) 53 | } 54 | } 55 | }; 56 | } 57 | 58 | /// Provides a set of methods to set attributes and colors. 59 | /// 60 | /// # Examples 61 | /// 62 | /// ```no_run 63 | /// use crossterm::style::Stylize; 64 | /// 65 | /// println!("{}", "Bold text".bold()); 66 | /// println!("{}", "Underlined text".underlined()); 67 | /// println!("{}", "Negative text".negative()); 68 | /// println!("{}", "Red on blue".red().on_blue()); 69 | /// ``` 70 | pub trait Stylize: Sized { 71 | /// This type with styles applied. 72 | type Styled: AsRef + AsMut; 73 | 74 | /// Styles this type. 75 | fn stylize(self) -> Self::Styled; 76 | 77 | /// Sets the foreground color. 78 | fn with(self, color: Color) -> Self::Styled { 79 | let mut styled = self.stylize(); 80 | styled.as_mut().foreground_color = Some(color); 81 | styled 82 | } 83 | 84 | /// Sets the background color. 85 | fn on(self, color: Color) -> Self::Styled { 86 | let mut styled = self.stylize(); 87 | styled.as_mut().background_color = Some(color); 88 | styled 89 | } 90 | 91 | /// Sets the underline color. 92 | fn underline(self, color: Color) -> Self::Styled { 93 | let mut styled = self.stylize(); 94 | styled.as_mut().underline_color = Some(color); 95 | styled 96 | } 97 | 98 | /// Styles the content with the attribute. 99 | fn attribute(self, attr: Attribute) -> Self::Styled { 100 | let mut styled = self.stylize(); 101 | styled.as_mut().attributes.set(attr); 102 | styled 103 | } 104 | 105 | stylize_method!(reset Attribute::Reset); 106 | stylize_method!(bold Attribute::Bold); 107 | stylize_method!(underlined Attribute::Underlined); 108 | stylize_method!(reverse Attribute::Reverse); 109 | stylize_method!(dim Attribute::Dim); 110 | stylize_method!(italic Attribute::Italic); 111 | stylize_method!(negative Attribute::Reverse); 112 | stylize_method!(slow_blink Attribute::SlowBlink); 113 | stylize_method!(rapid_blink Attribute::RapidBlink); 114 | stylize_method!(hidden Attribute::Hidden); 115 | stylize_method!(crossed_out Attribute::CrossedOut); 116 | 117 | stylize_method!(black, on_black, underline_black Color::Black); 118 | stylize_method!(dark_grey, on_dark_grey, underline_dark_grey Color::DarkGrey); 119 | stylize_method!(red, on_red, underline_red Color::Red); 120 | stylize_method!(dark_red, on_dark_red, underline_dark_red Color::DarkRed); 121 | stylize_method!(green, on_green, underline_green Color::Green); 122 | stylize_method!(dark_green, on_dark_green, underline_dark_green Color::DarkGreen); 123 | stylize_method!(yellow, on_yellow, underline_yellow Color::Yellow); 124 | stylize_method!(dark_yellow, on_dark_yellow, underline_dark_yellow Color::DarkYellow); 125 | stylize_method!(blue, on_blue, underline_blue Color::Blue); 126 | stylize_method!(dark_blue, on_dark_blue, underline_dark_blue Color::DarkBlue); 127 | stylize_method!(magenta, on_magenta, underline_magenta Color::Magenta); 128 | stylize_method!(dark_magenta, on_dark_magenta, underline_dark_magenta Color::DarkMagenta); 129 | stylize_method!(cyan, on_cyan, underline_cyan Color::Cyan); 130 | stylize_method!(dark_cyan, on_dark_cyan, underline_dark_cyan Color::DarkCyan); 131 | stylize_method!(white, on_white, underline_white Color::White); 132 | stylize_method!(grey, on_grey, underline_grey Color::Grey); 133 | } 134 | 135 | macro_rules! impl_stylize_for_display { 136 | ($($t:ty),*) => { $( 137 | impl Stylize for $t { 138 | type Styled = StyledContent; 139 | #[inline] 140 | fn stylize(self) -> Self::Styled { 141 | style(self) 142 | } 143 | } 144 | )* } 145 | } 146 | impl_stylize_for_display!(String, char, &str); 147 | 148 | impl Stylize for ContentStyle { 149 | type Styled = Self; 150 | #[inline] 151 | fn stylize(self) -> Self::Styled { 152 | self 153 | } 154 | } 155 | impl Stylize for StyledContent { 156 | type Styled = StyledContent; 157 | fn stylize(self) -> Self::Styled { 158 | self 159 | } 160 | } 161 | 162 | // Workaround for https://github.com/rust-lang/rust/issues/78835 163 | macro_rules! calculated_docs { 164 | ($(#[doc = $doc:expr] $item:item)*) => { $(#[doc = $doc] $item)* }; 165 | } 166 | // Remove once https://github.com/rust-lang/rust-clippy/issues/7106 stabilizes. 167 | #[allow(clippy::single_component_path_imports)] 168 | #[allow(clippy::useless_attribute)] 169 | use calculated_docs; 170 | 171 | #[cfg(test)] 172 | mod tests { 173 | use super::super::{Attribute, Color, ContentStyle, Stylize}; 174 | 175 | #[test] 176 | fn set_fg_bg_add_attr() { 177 | let style = ContentStyle::new() 178 | .with(Color::Blue) 179 | .on(Color::Red) 180 | .attribute(Attribute::Bold); 181 | 182 | assert_eq!(style.foreground_color, Some(Color::Blue)); 183 | assert_eq!(style.background_color, Some(Color::Red)); 184 | assert!(style.attributes.has(Attribute::Bold)); 185 | 186 | let mut styled_content = style.apply("test"); 187 | 188 | styled_content = styled_content 189 | .with(Color::Green) 190 | .on(Color::Magenta) 191 | .attribute(Attribute::NoItalic); 192 | 193 | let style = styled_content.style(); 194 | 195 | assert_eq!(style.foreground_color, Some(Color::Green)); 196 | assert_eq!(style.background_color, Some(Color::Magenta)); 197 | assert!(style.attributes.has(Attribute::Bold)); 198 | assert!(style.attributes.has(Attribute::NoItalic)); 199 | } 200 | } 201 | -------------------------------------------------------------------------------- /src/style/sys.rs: -------------------------------------------------------------------------------- 1 | #[cfg(windows)] 2 | pub(crate) mod windows; 3 | -------------------------------------------------------------------------------- /src/style/sys/windows.rs: -------------------------------------------------------------------------------- 1 | use std::convert::TryFrom; 2 | use std::sync::atomic::{AtomicU32, Ordering}; 3 | 4 | use crossterm_winapi::{Console, Handle, HandleType, ScreenBuffer}; 5 | use winapi::um::wincon; 6 | 7 | use super::super::{Color, Colored}; 8 | 9 | const FG_GREEN: u16 = wincon::FOREGROUND_GREEN; 10 | const FG_RED: u16 = wincon::FOREGROUND_RED; 11 | const FG_BLUE: u16 = wincon::FOREGROUND_BLUE; 12 | const FG_INTENSITY: u16 = wincon::FOREGROUND_INTENSITY; 13 | 14 | const BG_GREEN: u16 = wincon::BACKGROUND_GREEN; 15 | const BG_RED: u16 = wincon::BACKGROUND_RED; 16 | const BG_BLUE: u16 = wincon::BACKGROUND_BLUE; 17 | const BG_INTENSITY: u16 = wincon::BACKGROUND_INTENSITY; 18 | 19 | pub(crate) fn set_foreground_color(fg_color: Color) -> std::io::Result<()> { 20 | init_console_color()?; 21 | 22 | let color_value: u16 = Colored::ForegroundColor(fg_color).into(); 23 | 24 | let screen_buffer = ScreenBuffer::current()?; 25 | let csbi = screen_buffer.info()?; 26 | 27 | // Notice that the color values are stored in wAttribute. 28 | // So we need to use bitwise operators to check if the values exists or to get current console colors. 29 | let attrs = csbi.attributes(); 30 | let bg_color = attrs & 0x0070; 31 | let mut color = color_value | bg_color; 32 | 33 | // background intensity is a separate value in attrs, 34 | // we need to check if this was applied to the current bg color. 35 | if (attrs & wincon::BACKGROUND_INTENSITY) != 0 { 36 | color |= wincon::BACKGROUND_INTENSITY; 37 | } 38 | 39 | Console::from(screen_buffer.handle().clone()).set_text_attribute(color)?; 40 | Ok(()) 41 | } 42 | 43 | pub(crate) fn set_background_color(bg_color: Color) -> std::io::Result<()> { 44 | init_console_color()?; 45 | 46 | let color_value: u16 = Colored::BackgroundColor(bg_color).into(); 47 | 48 | let screen_buffer = ScreenBuffer::current()?; 49 | let csbi = screen_buffer.info()?; 50 | 51 | // Notice that the color values are stored in wAttribute. 52 | // So we need to use bitwise operators to check if the values exists or to get current console colors. 53 | let attrs = csbi.attributes(); 54 | let fg_color = attrs & 0x0007; 55 | let mut color = fg_color | color_value; 56 | 57 | // Foreground intensity is a separate value in attrs, 58 | // So we need to check if this was applied to the current fg color. 59 | if (attrs & wincon::FOREGROUND_INTENSITY) != 0 { 60 | color |= wincon::FOREGROUND_INTENSITY; 61 | } 62 | 63 | Console::from(screen_buffer.handle().clone()).set_text_attribute(color)?; 64 | Ok(()) 65 | } 66 | 67 | pub(crate) fn reset() -> std::io::Result<()> { 68 | if let Ok(original_color) = u16::try_from(ORIGINAL_CONSOLE_COLOR.load(Ordering::Relaxed)) { 69 | Console::from(Handle::new(HandleType::CurrentOutputHandle)?) 70 | .set_text_attribute(original_color)?; 71 | } 72 | 73 | Ok(()) 74 | } 75 | 76 | /// Initializes the default console color. It will will be skipped if it has already been initialized. 77 | pub(crate) fn init_console_color() -> std::io::Result<()> { 78 | if ORIGINAL_CONSOLE_COLOR.load(Ordering::Relaxed) == u32::MAX { 79 | let screen_buffer = ScreenBuffer::current()?; 80 | let attr = screen_buffer.info()?.attributes(); 81 | ORIGINAL_CONSOLE_COLOR.store(u32::from(attr), Ordering::Relaxed); 82 | } 83 | 84 | Ok(()) 85 | } 86 | 87 | /// Returns the original console color, make sure to call `init_console_color` before calling this function. Otherwise this function will panic. 88 | pub(crate) fn original_console_color() -> u16 { 89 | u16::try_from(ORIGINAL_CONSOLE_COLOR.load(Ordering::Relaxed)) 90 | // safe unwrap, initial console color was set with `init_console_color` in `WinApiColor::new()` 91 | .expect("Initial console color not set") 92 | } 93 | 94 | // This is either a valid u16 in which case it stores the original console color or it is u32::MAX 95 | // in which case it is uninitialized. 96 | static ORIGINAL_CONSOLE_COLOR: AtomicU32 = AtomicU32::new(u32::MAX); 97 | 98 | impl From for u16 { 99 | /// Returns the WinAPI color value (u16) from the `Colored` struct. 100 | fn from(colored: Colored) -> Self { 101 | match colored { 102 | Colored::ForegroundColor(color) => { 103 | match color { 104 | Color::Black => 0, 105 | Color::DarkGrey => FG_INTENSITY, 106 | Color::Red => FG_INTENSITY | FG_RED, 107 | Color::DarkRed => FG_RED, 108 | Color::Green => FG_INTENSITY | FG_GREEN, 109 | Color::DarkGreen => FG_GREEN, 110 | Color::Yellow => FG_INTENSITY | FG_GREEN | FG_RED, 111 | Color::DarkYellow => FG_GREEN | FG_RED, 112 | Color::Blue => FG_INTENSITY | FG_BLUE, 113 | Color::DarkBlue => FG_BLUE, 114 | Color::Magenta => FG_INTENSITY | FG_RED | FG_BLUE, 115 | Color::DarkMagenta => FG_RED | FG_BLUE, 116 | Color::Cyan => FG_INTENSITY | FG_GREEN | FG_BLUE, 117 | Color::DarkCyan => FG_GREEN | FG_BLUE, 118 | Color::White => FG_INTENSITY | FG_RED | FG_GREEN | FG_BLUE, 119 | Color::Grey => FG_RED | FG_GREEN | FG_BLUE, 120 | 121 | Color::Reset => { 122 | // safe unwrap, initial console color was set with `init_console_color`. 123 | let original_color = original_console_color(); 124 | 125 | const REMOVE_BG_MASK: u16 = BG_INTENSITY | BG_RED | BG_GREEN | BG_BLUE; 126 | // remove all background values from the original color, we don't want to reset those. 127 | 128 | original_color & !REMOVE_BG_MASK 129 | } 130 | 131 | /* WinAPI will be used for systems that do not support ANSI, those are windows version less then 10. RGB and 255 (AnsiBValue) colors are not supported in that case.*/ 132 | Color::Rgb { .. } => 0, 133 | Color::AnsiValue(_val) => 0, 134 | } 135 | } 136 | Colored::BackgroundColor(color) => { 137 | match color { 138 | Color::Black => 0, 139 | Color::DarkGrey => BG_INTENSITY, 140 | Color::Red => BG_INTENSITY | BG_RED, 141 | Color::DarkRed => BG_RED, 142 | Color::Green => BG_INTENSITY | BG_GREEN, 143 | Color::DarkGreen => BG_GREEN, 144 | Color::Yellow => BG_INTENSITY | BG_GREEN | BG_RED, 145 | Color::DarkYellow => BG_GREEN | BG_RED, 146 | Color::Blue => BG_INTENSITY | BG_BLUE, 147 | Color::DarkBlue => BG_BLUE, 148 | Color::Magenta => BG_INTENSITY | BG_RED | BG_BLUE, 149 | Color::DarkMagenta => BG_RED | BG_BLUE, 150 | Color::Cyan => BG_INTENSITY | BG_GREEN | BG_BLUE, 151 | Color::DarkCyan => BG_GREEN | BG_BLUE, 152 | Color::White => BG_INTENSITY | BG_RED | BG_GREEN | BG_BLUE, 153 | Color::Grey => BG_RED | BG_GREEN | BG_BLUE, 154 | 155 | Color::Reset => { 156 | let original_color = original_console_color(); 157 | 158 | const REMOVE_FG_MASK: u16 = FG_INTENSITY | FG_RED | FG_GREEN | FG_BLUE; 159 | // remove all foreground values from the original color, we don't want to reset those. 160 | 161 | original_color & !REMOVE_FG_MASK 162 | } 163 | /* WinAPI will be used for systems that do not support ANSI, those are windows version less then 10. RGB and 255 (AnsiBValue) colors are not supported in that case.*/ 164 | Color::Rgb { .. } => 0, 165 | Color::AnsiValue(_val) => 0, 166 | } 167 | } 168 | Colored::UnderlineColor(_) => 0, 169 | } 170 | } 171 | } 172 | 173 | #[cfg(test)] 174 | mod tests { 175 | use std::sync::atomic::Ordering; 176 | 177 | use crate::style::sys::windows::set_foreground_color; 178 | 179 | use super::{ 180 | Color, Colored, BG_INTENSITY, BG_RED, FG_INTENSITY, FG_RED, ORIGINAL_CONSOLE_COLOR, 181 | }; 182 | 183 | #[test] 184 | fn test_parse_fg_color() { 185 | let colored = Colored::ForegroundColor(Color::Red); 186 | assert_eq!(Into::::into(colored), FG_INTENSITY | FG_RED); 187 | } 188 | 189 | #[test] 190 | fn test_parse_bg_color() { 191 | let colored = Colored::BackgroundColor(Color::Red); 192 | assert_eq!(Into::::into(colored), BG_INTENSITY | BG_RED); 193 | } 194 | 195 | #[test] 196 | fn test_original_console_color_is_set() { 197 | assert_eq!(ORIGINAL_CONSOLE_COLOR.load(Ordering::Relaxed), u32::MAX); 198 | 199 | // will call `init_console_color` 200 | set_foreground_color(Color::Blue).unwrap(); 201 | 202 | assert_ne!(ORIGINAL_CONSOLE_COLOR.load(Ordering::Relaxed), u32::MAX); 203 | } 204 | } 205 | -------------------------------------------------------------------------------- /src/style/types.rs: -------------------------------------------------------------------------------- 1 | pub use self::{attribute::Attribute, color::Color, colored::Colored, colors::Colors}; 2 | 3 | mod attribute; 4 | mod color; 5 | mod colored; 6 | mod colors; 7 | -------------------------------------------------------------------------------- /src/style/types/attribute.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::Display; 2 | 3 | #[cfg(feature = "serde")] 4 | use serde::{Deserialize, Serialize}; 5 | 6 | use super::super::SetAttribute; 7 | 8 | // This macro generates the Attribute enum, its iterator 9 | // function, and the static array containing the sgr code 10 | // of each attribute 11 | macro_rules! Attribute { 12 | ( 13 | $( 14 | $(#[$inner:ident $($args:tt)*])* 15 | $name:ident = $sgr:expr, 16 | )* 17 | ) => { 18 | /// Represents an attribute. 19 | /// 20 | /// # Platform-specific Notes 21 | /// 22 | /// * Only UNIX and Windows 10 terminals do support text attributes. 23 | /// * Keep in mind that not all terminals support all attributes. 24 | /// * Crossterm implements almost all attributes listed in the 25 | /// [SGR parameters](https://en.wikipedia.org/wiki/ANSI_escape_code#Select_Graphic_Rendition_parameters). 26 | /// 27 | /// | Attribute | Windows | UNIX | Notes | 28 | /// | :-- | :--: | :--: | :-- | 29 | /// | `Reset` | ✓ | ✓ | | 30 | /// | `Bold` | ✓ | ✓ | | 31 | /// | `Dim` | ✓ | ✓ | | 32 | /// | `Italic` | ? | ? | Not widely supported, sometimes treated as inverse. | 33 | /// | `Underlined` | ✓ | ✓ | | 34 | /// | `SlowBlink` | ? | ? | Not widely supported, sometimes treated as inverse. | 35 | /// | `RapidBlink` | ? | ? | Not widely supported. MS-DOS ANSI.SYS; 150+ per minute. | 36 | /// | `Reverse` | ✓ | ✓ | | 37 | /// | `Hidden` | ✓ | ✓ | Also known as Conceal. | 38 | /// | `Fraktur` | ✗ | ✓ | Legible characters, but marked for deletion. | 39 | /// | `DefaultForegroundColor` | ? | ? | Implementation specific (according to standard). | 40 | /// | `DefaultBackgroundColor` | ? | ? | Implementation specific (according to standard). | 41 | /// | `Framed` | ? | ? | Not widely supported. | 42 | /// | `Encircled` | ? | ? | This should turn on the encircled attribute. | 43 | /// | `OverLined` | ? | ? | This should draw a line at the top of the text. | 44 | /// 45 | /// # Examples 46 | /// 47 | /// Basic usage: 48 | /// 49 | /// ```no_run 50 | /// use crossterm::style::Attribute; 51 | /// 52 | /// println!( 53 | /// "{} Underlined {} No Underline", 54 | /// Attribute::Underlined, 55 | /// Attribute::NoUnderline 56 | /// ); 57 | /// ``` 58 | /// 59 | /// Style existing text: 60 | /// 61 | /// ```no_run 62 | /// use crossterm::style::Stylize; 63 | /// 64 | /// println!("{}", "Bold text".bold()); 65 | /// println!("{}", "Underlined text".underlined()); 66 | /// println!("{}", "Negative text".negative()); 67 | /// ``` 68 | #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] 69 | #[derive(Copy, Clone, Debug, PartialEq, Eq, Ord, PartialOrd, Hash)] 70 | #[non_exhaustive] 71 | pub enum Attribute { 72 | $( 73 | $(#[$inner $($args)*])* 74 | $name, 75 | )* 76 | } 77 | 78 | pub static SGR: &'static[i16] = &[ 79 | $($sgr,)* 80 | ]; 81 | 82 | impl Attribute { 83 | /// Iterates over all the variants of the Attribute enum. 84 | pub fn iterator() -> impl Iterator { 85 | use self::Attribute::*; 86 | [ $($name,)* ].iter().copied() 87 | } 88 | } 89 | } 90 | } 91 | 92 | Attribute! { 93 | /// Resets all the attributes. 94 | Reset = 0, 95 | /// Increases the text intensity. 96 | Bold = 1, 97 | /// Decreases the text intensity. 98 | Dim = 2, 99 | /// Emphasises the text. 100 | Italic = 3, 101 | /// Underlines the text. 102 | Underlined = 4, 103 | 104 | // Other types of underlining 105 | /// Double underlines the text. 106 | DoubleUnderlined = 2, 107 | /// Undercurls the text. 108 | Undercurled = 3, 109 | /// Underdots the text. 110 | Underdotted = 4, 111 | /// Underdashes the text. 112 | Underdashed = 5, 113 | 114 | /// Makes the text blinking (< 150 per minute). 115 | SlowBlink = 5, 116 | /// Makes the text blinking (>= 150 per minute). 117 | RapidBlink = 6, 118 | /// Swaps foreground and background colors. 119 | Reverse = 7, 120 | /// Hides the text (also known as Conceal). 121 | Hidden = 8, 122 | /// Crosses the text. 123 | CrossedOut = 9, 124 | /// Sets the [Fraktur](https://en.wikipedia.org/wiki/Fraktur) typeface. 125 | /// 126 | /// Mostly used for [mathematical alphanumeric symbols](https://en.wikipedia.org/wiki/Mathematical_Alphanumeric_Symbols). 127 | Fraktur = 20, 128 | /// Turns off the `Bold` attribute. - Inconsistent - Prefer to use NormalIntensity 129 | NoBold = 21, 130 | /// Switches the text back to normal intensity (no bold, italic). 131 | NormalIntensity = 22, 132 | /// Turns off the `Italic` attribute. 133 | NoItalic = 23, 134 | /// Turns off the `Underlined` attribute. 135 | NoUnderline = 24, 136 | /// Turns off the text blinking (`SlowBlink` or `RapidBlink`). 137 | NoBlink = 25, 138 | /// Turns off the `Reverse` attribute. 139 | NoReverse = 27, 140 | /// Turns off the `Hidden` attribute. 141 | NoHidden = 28, 142 | /// Turns off the `CrossedOut` attribute. 143 | NotCrossedOut = 29, 144 | /// Makes the text framed. 145 | Framed = 51, 146 | /// Makes the text encircled. 147 | Encircled = 52, 148 | /// Draws a line at the top of the text. 149 | OverLined = 53, 150 | /// Turns off the `Frame` and `Encircled` attributes. 151 | NotFramedOrEncircled = 54, 152 | /// Turns off the `OverLined` attribute. 153 | NotOverLined = 55, 154 | } 155 | 156 | impl Display for Attribute { 157 | fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> std::fmt::Result { 158 | write!(f, "{}", SetAttribute(*self))?; 159 | Ok(()) 160 | } 161 | } 162 | 163 | impl Attribute { 164 | /// Returns a u32 with one bit set, which is the 165 | /// signature of this attribute in the Attributes 166 | /// bitset. 167 | /// 168 | /// The +1 enables storing Reset (whose index is 0) 169 | /// in the bitset Attributes. 170 | #[inline(always)] 171 | pub const fn bytes(self) -> u32 { 172 | 1 << ((self as u32) + 1) 173 | } 174 | /// Returns the SGR attribute value. 175 | /// 176 | /// See 177 | pub fn sgr(self) -> String { 178 | if (self as usize) > 4 && (self as usize) < 9 { 179 | return "4:".to_string() + SGR[self as usize].to_string().as_str(); 180 | } 181 | SGR[self as usize].to_string() 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /src/style/types/colored.rs: -------------------------------------------------------------------------------- 1 | use parking_lot::Once; 2 | use std::fmt::{self, Formatter}; 3 | use std::sync::atomic::{AtomicBool, Ordering}; 4 | 5 | #[cfg(feature = "serde")] 6 | use serde::{Deserialize, Serialize}; 7 | 8 | use crate::style::{parse_next_u8, Color}; 9 | 10 | /// Represents a foreground or background color. 11 | /// 12 | /// This can be converted to a [Colors](struct.Colors.html) by calling `into()` and applied 13 | /// using the [SetColors](struct.SetColors.html) command. 14 | #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] 15 | #[derive(Copy, Clone, Debug, PartialEq, Eq, Ord, PartialOrd, Hash)] 16 | pub enum Colored { 17 | /// A foreground color. 18 | ForegroundColor(Color), 19 | /// A background color. 20 | BackgroundColor(Color), 21 | /// An underline color. 22 | /// Important: doesn't work on windows 10 or lower. 23 | UnderlineColor(Color), 24 | } 25 | 26 | static ANSI_COLOR_DISABLED: AtomicBool = AtomicBool::new(false); 27 | static INITIALIZER: Once = Once::new(); 28 | 29 | impl Colored { 30 | /// Parse an ANSI foreground or background color. 31 | /// This is the string that would appear within an `ESC [ m` escape sequence, as found in 32 | /// various configuration files. 33 | /// 34 | /// # Examples 35 | /// 36 | /// ``` 37 | /// use crossterm::style::{Colored::{self, ForegroundColor, BackgroundColor}, Color}; 38 | /// 39 | /// assert_eq!(Colored::parse_ansi("38;5;0"), Some(ForegroundColor(Color::Black))); 40 | /// assert_eq!(Colored::parse_ansi("38;5;26"), Some(ForegroundColor(Color::AnsiValue(26)))); 41 | /// assert_eq!(Colored::parse_ansi("48;2;50;60;70"), Some(BackgroundColor(Color::Rgb { r: 50, g: 60, b: 70 }))); 42 | /// assert_eq!(Colored::parse_ansi("49"), Some(BackgroundColor(Color::Reset))); 43 | /// assert_eq!(Colored::parse_ansi("invalid color"), None); 44 | /// ``` 45 | /// 46 | /// Currently, 3/4 bit color values aren't supported so return `None`. 47 | /// 48 | /// See also: [`Color::parse_ansi`]. 49 | pub fn parse_ansi(ansi: &str) -> Option { 50 | use Colored::{BackgroundColor, ForegroundColor, UnderlineColor}; 51 | 52 | let values = &mut ansi.split(';'); 53 | 54 | let output = match parse_next_u8(values)? { 55 | 38 => return Color::parse_ansi_iter(values).map(ForegroundColor), 56 | 48 => return Color::parse_ansi_iter(values).map(BackgroundColor), 57 | 58 => return Color::parse_ansi_iter(values).map(UnderlineColor), 58 | 59 | 39 => ForegroundColor(Color::Reset), 60 | 49 => BackgroundColor(Color::Reset), 61 | 59 => UnderlineColor(Color::Reset), 62 | 63 | _ => return None, 64 | }; 65 | 66 | if values.next().is_some() { 67 | return None; 68 | } 69 | 70 | Some(output) 71 | } 72 | 73 | /// Checks whether ansi color sequences are disabled by setting of NO_COLOR 74 | /// in environment as per 75 | pub fn ansi_color_disabled() -> bool { 76 | !std::env::var("NO_COLOR") 77 | .unwrap_or("".to_string()) 78 | .is_empty() 79 | } 80 | 81 | pub fn ansi_color_disabled_memoized() -> bool { 82 | INITIALIZER.call_once(|| { 83 | ANSI_COLOR_DISABLED.store(Self::ansi_color_disabled(), Ordering::SeqCst); 84 | }); 85 | 86 | ANSI_COLOR_DISABLED.load(Ordering::SeqCst) 87 | } 88 | 89 | pub fn set_ansi_color_disabled(val: bool) { 90 | // Force the one-time initializer to run. 91 | _ = Self::ansi_color_disabled_memoized(); 92 | ANSI_COLOR_DISABLED.store(val, Ordering::SeqCst); 93 | } 94 | } 95 | 96 | impl fmt::Display for Colored { 97 | fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { 98 | let color; 99 | 100 | if Self::ansi_color_disabled_memoized() { 101 | return Ok(()); 102 | } 103 | 104 | match *self { 105 | Colored::ForegroundColor(new_color) => { 106 | if new_color == Color::Reset { 107 | return f.write_str("39"); 108 | } else { 109 | f.write_str("38;")?; 110 | color = new_color; 111 | } 112 | } 113 | Colored::BackgroundColor(new_color) => { 114 | if new_color == Color::Reset { 115 | return f.write_str("49"); 116 | } else { 117 | f.write_str("48;")?; 118 | color = new_color; 119 | } 120 | } 121 | Colored::UnderlineColor(new_color) => { 122 | if new_color == Color::Reset { 123 | return f.write_str("59"); 124 | } else { 125 | f.write_str("58;")?; 126 | color = new_color; 127 | } 128 | } 129 | } 130 | 131 | match color { 132 | Color::Black => f.write_str("5;0"), 133 | Color::DarkGrey => f.write_str("5;8"), 134 | Color::Red => f.write_str("5;9"), 135 | Color::DarkRed => f.write_str("5;1"), 136 | Color::Green => f.write_str("5;10"), 137 | Color::DarkGreen => f.write_str("5;2"), 138 | Color::Yellow => f.write_str("5;11"), 139 | Color::DarkYellow => f.write_str("5;3"), 140 | Color::Blue => f.write_str("5;12"), 141 | Color::DarkBlue => f.write_str("5;4"), 142 | Color::Magenta => f.write_str("5;13"), 143 | Color::DarkMagenta => f.write_str("5;5"), 144 | Color::Cyan => f.write_str("5;14"), 145 | Color::DarkCyan => f.write_str("5;6"), 146 | Color::White => f.write_str("5;15"), 147 | Color::Grey => f.write_str("5;7"), 148 | Color::Rgb { r, g, b } => write!(f, "2;{r};{g};{b}"), 149 | Color::AnsiValue(val) => write!(f, "5;{val}"), 150 | _ => Ok(()), 151 | } 152 | } 153 | } 154 | 155 | #[cfg(test)] 156 | mod tests { 157 | use crate::style::{Color, Colored}; 158 | 159 | fn check_format_color(colored: Colored, expected: &str) { 160 | Colored::set_ansi_color_disabled(true); 161 | assert_eq!(colored.to_string(), ""); 162 | Colored::set_ansi_color_disabled(false); 163 | assert_eq!(colored.to_string(), expected); 164 | } 165 | 166 | #[test] 167 | fn test_format_fg_color() { 168 | let colored = Colored::ForegroundColor(Color::Red); 169 | check_format_color(colored, "38;5;9"); 170 | } 171 | 172 | #[test] 173 | fn test_format_bg_color() { 174 | let colored = Colored::BackgroundColor(Color::Red); 175 | check_format_color(colored, "48;5;9"); 176 | } 177 | 178 | #[test] 179 | fn test_format_reset_fg_color() { 180 | let colored = Colored::ForegroundColor(Color::Reset); 181 | check_format_color(colored, "39"); 182 | } 183 | 184 | #[test] 185 | fn test_format_reset_bg_color() { 186 | let colored = Colored::BackgroundColor(Color::Reset); 187 | check_format_color(colored, "49"); 188 | } 189 | 190 | #[test] 191 | fn test_format_fg_rgb_color() { 192 | let colored = Colored::BackgroundColor(Color::Rgb { r: 1, g: 2, b: 3 }); 193 | check_format_color(colored, "48;2;1;2;3"); 194 | } 195 | 196 | #[test] 197 | fn test_format_fg_ansi_color() { 198 | let colored = Colored::ForegroundColor(Color::AnsiValue(255)); 199 | check_format_color(colored, "38;5;255"); 200 | } 201 | 202 | #[test] 203 | fn test_parse_ansi_fg() { 204 | test_parse_ansi(Colored::ForegroundColor) 205 | } 206 | 207 | #[test] 208 | fn test_parse_ansi_bg() { 209 | test_parse_ansi(Colored::ForegroundColor) 210 | } 211 | 212 | /// Used for test_parse_ansi_fg and test_parse_ansi_bg 213 | fn test_parse_ansi(bg_or_fg: impl Fn(Color) -> Colored) { 214 | /// Formats a re-parses `color` to check the result. 215 | macro_rules! test { 216 | ($color:expr) => { 217 | let colored = bg_or_fg($color); 218 | assert_eq!(Colored::parse_ansi(&format!("{}", colored)), Some(colored)); 219 | }; 220 | } 221 | 222 | use Color::*; 223 | 224 | test!(Reset); 225 | test!(Black); 226 | test!(DarkGrey); 227 | test!(Red); 228 | test!(DarkRed); 229 | test!(Green); 230 | test!(DarkGreen); 231 | test!(Yellow); 232 | test!(DarkYellow); 233 | test!(Blue); 234 | test!(DarkBlue); 235 | test!(Magenta); 236 | test!(DarkMagenta); 237 | test!(Cyan); 238 | test!(DarkCyan); 239 | test!(White); 240 | test!(Grey); 241 | 242 | // n in 0..=15 will give us the color values above back. 243 | for n in 16..=255 { 244 | test!(AnsiValue(n)); 245 | } 246 | 247 | for r in 0..=255 { 248 | for g in [0, 2, 18, 19, 60, 100, 200, 250, 254, 255].iter().copied() { 249 | for b in [0, 12, 16, 99, 100, 161, 200, 255].iter().copied() { 250 | test!(Rgb { r, g, b }); 251 | } 252 | } 253 | } 254 | } 255 | 256 | #[test] 257 | fn test_parse_invalid_ansi_color() { 258 | /// Checks that trying to parse `s` yields None. 259 | fn test(s: &str) { 260 | assert_eq!(Colored::parse_ansi(s), None); 261 | } 262 | test(""); 263 | test(";"); 264 | test(";;"); 265 | test(";;"); 266 | test("0"); 267 | test("1"); 268 | test("12"); 269 | test("100"); 270 | test("100048949345"); 271 | test("39;"); 272 | test("49;"); 273 | test("39;2"); 274 | test("49;2"); 275 | test("38"); 276 | test("38;"); 277 | test("38;0"); 278 | test("38;5"); 279 | test("38;5;0;"); 280 | test("38;5;0;2"); 281 | test("38;5;80;"); 282 | test("38;5;80;2"); 283 | test("38;5;257"); 284 | test("38;2"); 285 | test("38;2;"); 286 | test("38;2;0"); 287 | test("38;2;0;2"); 288 | test("38;2;0;2;257"); 289 | test("38;2;0;2;25;"); 290 | test("38;2;0;2;25;3"); 291 | test("48"); 292 | test("48;"); 293 | test("48;0"); 294 | test("48;5"); 295 | test("48;5;0;"); 296 | test("48;5;0;2"); 297 | test("48;5;80;"); 298 | test("48;5;80;2"); 299 | test("48;5;257"); 300 | test("48;2"); 301 | test("48;2;"); 302 | test("48;2;0"); 303 | test("48;2;0;2"); 304 | test("48;2;0;2;257"); 305 | test("48;2;0;2;25;"); 306 | test("48;2;0;2;25;3"); 307 | } 308 | 309 | #[test] 310 | fn test_no_color() { 311 | std::env::set_var("NO_COLOR", "1"); 312 | assert!(Colored::ansi_color_disabled()); 313 | std::env::set_var("NO_COLOR", "XXX"); 314 | assert!(Colored::ansi_color_disabled()); 315 | std::env::set_var("NO_COLOR", ""); 316 | assert!(!Colored::ansi_color_disabled()); 317 | std::env::remove_var("NO_COLOR"); 318 | assert!(!Colored::ansi_color_disabled()); 319 | } 320 | } 321 | -------------------------------------------------------------------------------- /src/style/types/colors.rs: -------------------------------------------------------------------------------- 1 | use crate::style::{Color, Colored}; 2 | 3 | /// Represents, optionally, a foreground and/or a background color. 4 | /// 5 | /// It can be applied using the `SetColors` command. 6 | /// 7 | /// It can also be created from a [Colored](enum.Colored.html) value or a tuple of 8 | /// `(Color, Color)` in the order `(foreground, background)`. 9 | /// 10 | /// The [then](#method.then) method can be used to combine `Colors` values. 11 | /// 12 | /// For example: 13 | /// ```no_run 14 | /// use crossterm::style::{Color, Colors, Colored}; 15 | /// 16 | /// // An example color, loaded from a config, file in ANSI format. 17 | /// let config_color = "38;2;23;147;209"; 18 | /// 19 | /// // Default to green text on a black background. 20 | /// let default_colors = Colors::new(Color::Green, Color::Black); 21 | /// // Load a colored value from a config and override the default colors 22 | /// let colors = match Colored::parse_ansi(config_color) { 23 | /// Some(colored) => default_colors.then(&colored.into()), 24 | /// None => default_colors, 25 | /// }; 26 | /// ``` 27 | /// 28 | /// See [Color](enum.Color.html). 29 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 30 | pub struct Colors { 31 | pub foreground: Option, 32 | pub background: Option, 33 | } 34 | 35 | impl Colors { 36 | /// Returns a new `Color` which, when applied, has the same effect as applying `self` and *then* 37 | /// `other`. 38 | pub fn then(&self, other: &Colors) -> Colors { 39 | Colors { 40 | foreground: other.foreground.or(self.foreground), 41 | background: other.background.or(self.background), 42 | } 43 | } 44 | } 45 | 46 | impl Colors { 47 | pub fn new(foreground: Color, background: Color) -> Colors { 48 | Colors { 49 | foreground: Some(foreground), 50 | background: Some(background), 51 | } 52 | } 53 | } 54 | 55 | impl From for Colors { 56 | fn from(colored: Colored) -> Colors { 57 | match colored { 58 | Colored::ForegroundColor(color) => Colors { 59 | foreground: Some(color), 60 | background: None, 61 | }, 62 | Colored::BackgroundColor(color) => Colors { 63 | foreground: None, 64 | background: Some(color), 65 | }, 66 | Colored::UnderlineColor(color) => Colors { 67 | foreground: None, 68 | background: Some(color), 69 | }, 70 | } 71 | } 72 | } 73 | 74 | #[cfg(test)] 75 | mod tests { 76 | use crate::style::{Color, Colors}; 77 | 78 | #[test] 79 | fn test_colors_then() { 80 | use Color::*; 81 | 82 | assert_eq!( 83 | Colors { 84 | foreground: None, 85 | background: None, 86 | } 87 | .then(&Colors { 88 | foreground: None, 89 | background: None, 90 | }), 91 | Colors { 92 | foreground: None, 93 | background: None, 94 | } 95 | ); 96 | 97 | assert_eq!( 98 | Colors { 99 | foreground: None, 100 | background: None, 101 | } 102 | .then(&Colors { 103 | foreground: Some(Black), 104 | background: None, 105 | }), 106 | Colors { 107 | foreground: Some(Black), 108 | background: None, 109 | } 110 | ); 111 | 112 | assert_eq!( 113 | Colors { 114 | foreground: None, 115 | background: None, 116 | } 117 | .then(&Colors { 118 | foreground: None, 119 | background: Some(Grey), 120 | }), 121 | Colors { 122 | foreground: None, 123 | background: Some(Grey), 124 | } 125 | ); 126 | 127 | assert_eq!( 128 | Colors { 129 | foreground: None, 130 | background: None, 131 | } 132 | .then(&Colors::new(White, Grey)), 133 | Colors::new(White, Grey), 134 | ); 135 | 136 | assert_eq!( 137 | Colors { 138 | foreground: None, 139 | background: Some(Blue), 140 | } 141 | .then(&Colors::new(White, Grey)), 142 | Colors::new(White, Grey), 143 | ); 144 | 145 | assert_eq!( 146 | Colors { 147 | foreground: Some(Blue), 148 | background: None, 149 | } 150 | .then(&Colors::new(White, Grey)), 151 | Colors::new(White, Grey), 152 | ); 153 | 154 | assert_eq!( 155 | Colors::new(Blue, Green).then(&Colors::new(White, Grey)), 156 | Colors::new(White, Grey), 157 | ); 158 | 159 | assert_eq!( 160 | Colors { 161 | foreground: Some(Blue), 162 | background: Some(Green), 163 | } 164 | .then(&Colors { 165 | foreground: None, 166 | background: Some(Grey), 167 | }), 168 | Colors { 169 | foreground: Some(Blue), 170 | background: Some(Grey), 171 | } 172 | ); 173 | 174 | assert_eq!( 175 | Colors { 176 | foreground: Some(Blue), 177 | background: Some(Green), 178 | } 179 | .then(&Colors { 180 | foreground: Some(White), 181 | background: None, 182 | }), 183 | Colors { 184 | foreground: Some(White), 185 | background: Some(Green), 186 | } 187 | ); 188 | 189 | assert_eq!( 190 | Colors { 191 | foreground: Some(Blue), 192 | background: Some(Green), 193 | } 194 | .then(&Colors { 195 | foreground: None, 196 | background: None, 197 | }), 198 | Colors { 199 | foreground: Some(Blue), 200 | background: Some(Green), 201 | } 202 | ); 203 | 204 | assert_eq!( 205 | Colors { 206 | foreground: None, 207 | background: Some(Green), 208 | } 209 | .then(&Colors { 210 | foreground: None, 211 | background: None, 212 | }), 213 | Colors { 214 | foreground: None, 215 | background: Some(Green), 216 | } 217 | ); 218 | 219 | assert_eq!( 220 | Colors { 221 | foreground: Some(Blue), 222 | background: None, 223 | } 224 | .then(&Colors { 225 | foreground: None, 226 | background: None, 227 | }), 228 | Colors { 229 | foreground: Some(Blue), 230 | background: None, 231 | } 232 | ); 233 | } 234 | } 235 | -------------------------------------------------------------------------------- /src/terminal/sys.rs: -------------------------------------------------------------------------------- 1 | //! This module provides platform related functions. 2 | 3 | #[cfg(unix)] 4 | #[cfg(feature = "events")] 5 | pub use self::unix::supports_keyboard_enhancement; 6 | #[cfg(unix)] 7 | pub(crate) use self::unix::{ 8 | disable_raw_mode, enable_raw_mode, is_raw_mode_enabled, size, window_size, 9 | }; 10 | #[cfg(windows)] 11 | #[cfg(feature = "events")] 12 | pub use self::windows::supports_keyboard_enhancement; 13 | #[cfg(all(windows, test))] 14 | pub(crate) use self::windows::temp_screen_buffer; 15 | #[cfg(windows)] 16 | pub(crate) use self::windows::{ 17 | clear, disable_raw_mode, enable_raw_mode, is_raw_mode_enabled, scroll_down, scroll_up, 18 | set_size, set_window_title, size, window_size, 19 | }; 20 | 21 | #[cfg(windows)] 22 | mod windows; 23 | 24 | #[cfg(unix)] 25 | pub mod file_descriptor; 26 | #[cfg(unix)] 27 | mod unix; 28 | -------------------------------------------------------------------------------- /src/terminal/sys/file_descriptor.rs: -------------------------------------------------------------------------------- 1 | use std::io; 2 | 3 | #[cfg(feature = "libc")] 4 | use libc::size_t; 5 | #[cfg(not(feature = "libc"))] 6 | use rustix::fd::{AsFd, AsRawFd, BorrowedFd, OwnedFd, RawFd}; 7 | #[cfg(feature = "libc")] 8 | use std::{ 9 | fs, 10 | marker::PhantomData, 11 | os::unix::{ 12 | io::{IntoRawFd, RawFd}, 13 | prelude::AsRawFd, 14 | }, 15 | }; 16 | 17 | /// A file descriptor wrapper. 18 | /// 19 | /// It allows to retrieve raw file descriptor, write to the file descriptor and 20 | /// mainly it closes the file descriptor once dropped. 21 | #[derive(Debug)] 22 | #[cfg(feature = "libc")] 23 | pub struct FileDesc<'a> { 24 | fd: RawFd, 25 | close_on_drop: bool, 26 | phantom: PhantomData<&'a ()>, 27 | } 28 | 29 | #[cfg(not(feature = "libc"))] 30 | pub enum FileDesc<'a> { 31 | Owned(OwnedFd), 32 | Borrowed(BorrowedFd<'a>), 33 | } 34 | 35 | #[cfg(feature = "libc")] 36 | impl FileDesc<'_> { 37 | /// Constructs a new `FileDesc` with the given `RawFd`. 38 | /// 39 | /// # Arguments 40 | /// 41 | /// * `fd` - raw file descriptor 42 | /// * `close_on_drop` - specify if the raw file descriptor should be closed once the `FileDesc` is dropped 43 | pub fn new(fd: RawFd, close_on_drop: bool) -> FileDesc<'static> { 44 | FileDesc { 45 | fd, 46 | close_on_drop, 47 | phantom: PhantomData, 48 | } 49 | } 50 | 51 | pub fn read(&self, buffer: &mut [u8]) -> io::Result { 52 | let result = unsafe { 53 | libc::read( 54 | self.fd, 55 | buffer.as_mut_ptr() as *mut libc::c_void, 56 | buffer.len() as size_t, 57 | ) 58 | }; 59 | 60 | if result < 0 { 61 | Err(io::Error::last_os_error()) 62 | } else { 63 | Ok(result as usize) 64 | } 65 | } 66 | 67 | /// Returns the underlying file descriptor. 68 | pub fn raw_fd(&self) -> RawFd { 69 | self.fd 70 | } 71 | } 72 | 73 | #[cfg(not(feature = "libc"))] 74 | impl FileDesc<'_> { 75 | pub fn read(&self, buffer: &mut [u8]) -> io::Result { 76 | let fd = match self { 77 | FileDesc::Owned(fd) => fd.as_fd(), 78 | FileDesc::Borrowed(fd) => fd.as_fd(), 79 | }; 80 | let result = rustix::io::read(fd, buffer)?; 81 | Ok(result) 82 | } 83 | 84 | pub fn raw_fd(&self) -> RawFd { 85 | match self { 86 | FileDesc::Owned(fd) => fd.as_raw_fd(), 87 | FileDesc::Borrowed(fd) => fd.as_raw_fd(), 88 | } 89 | } 90 | } 91 | 92 | #[cfg(feature = "libc")] 93 | impl Drop for FileDesc<'_> { 94 | fn drop(&mut self) { 95 | if self.close_on_drop { 96 | // Note that errors are ignored when closing a file descriptor. The 97 | // reason for this is that if an error occurs we don't actually know if 98 | // the file descriptor was closed or not, and if we retried (for 99 | // something like EINTR), we might close another valid file descriptor 100 | // opened after we closed ours. 101 | let _ = unsafe { libc::close(self.fd) }; 102 | } 103 | } 104 | } 105 | 106 | impl AsRawFd for FileDesc<'_> { 107 | fn as_raw_fd(&self) -> RawFd { 108 | self.raw_fd() 109 | } 110 | } 111 | 112 | #[cfg(not(feature = "libc"))] 113 | impl AsFd for FileDesc<'_> { 114 | fn as_fd(&self) -> BorrowedFd<'_> { 115 | match self { 116 | FileDesc::Owned(fd) => fd.as_fd(), 117 | FileDesc::Borrowed(fd) => fd.as_fd(), 118 | } 119 | } 120 | } 121 | 122 | #[cfg(feature = "libc")] 123 | /// Creates a file descriptor pointing to the standard input or `/dev/tty`. 124 | pub fn tty_fd() -> io::Result> { 125 | let (fd, close_on_drop) = if unsafe { libc::isatty(libc::STDIN_FILENO) == 1 } { 126 | (libc::STDIN_FILENO, false) 127 | } else { 128 | ( 129 | fs::OpenOptions::new() 130 | .read(true) 131 | .write(true) 132 | .open("/dev/tty")? 133 | .into_raw_fd(), 134 | true, 135 | ) 136 | }; 137 | 138 | Ok(FileDesc::new(fd, close_on_drop)) 139 | } 140 | 141 | #[cfg(not(feature = "libc"))] 142 | /// Creates a file descriptor pointing to the standard input or `/dev/tty`. 143 | pub fn tty_fd() -> io::Result> { 144 | use std::fs::File; 145 | 146 | let stdin = rustix::stdio::stdin(); 147 | let fd = if rustix::termios::isatty(stdin) { 148 | FileDesc::Borrowed(stdin) 149 | } else { 150 | let dev_tty = File::options().read(true).write(true).open("/dev/tty")?; 151 | FileDesc::Owned(dev_tty.into()) 152 | }; 153 | Ok(fd) 154 | } 155 | -------------------------------------------------------------------------------- /src/tty.rs: -------------------------------------------------------------------------------- 1 | //! Making it a little more convenient and safe to query whether 2 | //! something is a terminal teletype or not. 3 | //! This module defines the IsTty trait and the is_tty method to 4 | //! return true if the item represents a terminal. 5 | 6 | #[cfg(unix)] 7 | use std::os::unix::io::AsRawFd; 8 | #[cfg(windows)] 9 | use std::os::windows::io::AsRawHandle; 10 | 11 | #[cfg(windows)] 12 | use winapi::um::consoleapi::GetConsoleMode; 13 | 14 | /// Adds the `is_tty` method to types that might represent a terminal 15 | /// 16 | /// ```rust 17 | /// use std::io::stdout; 18 | /// use crossterm::tty::IsTty; 19 | /// 20 | /// let is_tty: bool = stdout().is_tty(); 21 | /// ``` 22 | pub trait IsTty { 23 | /// Returns true when an instance is a terminal teletype, otherwise false. 24 | fn is_tty(&self) -> bool; 25 | } 26 | 27 | /// On UNIX, the `isatty()` function returns true if a file 28 | /// descriptor is a terminal. 29 | #[cfg(all(unix, feature = "libc"))] 30 | impl IsTty for S { 31 | fn is_tty(&self) -> bool { 32 | let fd = self.as_raw_fd(); 33 | unsafe { libc::isatty(fd) == 1 } 34 | } 35 | } 36 | 37 | #[cfg(all(unix, not(feature = "libc")))] 38 | impl IsTty for S { 39 | fn is_tty(&self) -> bool { 40 | let fd = self.as_raw_fd(); 41 | rustix::termios::isatty(unsafe { std::os::unix::io::BorrowedFd::borrow_raw(fd) }) 42 | } 43 | } 44 | 45 | /// On windows, `GetConsoleMode` will return true if we are in a terminal. 46 | /// Otherwise false. 47 | #[cfg(windows)] 48 | impl IsTty for S { 49 | fn is_tty(&self) -> bool { 50 | let mut mode = 0; 51 | let ok = unsafe { GetConsoleMode(self.as_raw_handle() as *mut _, &mut mode) }; 52 | ok == 1 53 | } 54 | } 55 | --------------------------------------------------------------------------------