Compare commits
No commits in common. "1b66c396b6cfe83894e94d851b2f1fef18f38230" and "fe440284bfad313998f73db663e34003d89d7ec8" have entirely different histories.
1b66c396b6
...
fe440284bf
2
.github/workflows/crossterm_test.yml
vendored
2
.github/workflows/crossterm_test.yml
vendored
@ -68,7 +68,7 @@ jobs:
|
|||||||
continue-on-error: ${{ matrix.can-fail }}
|
continue-on-error: ${{ matrix.can-fail }}
|
||||||
- name: Test no default features with use-dev-tty feature enabled
|
- name: Test no default features with use-dev-tty feature enabled
|
||||||
if: matrix.os != 'windows-2019'
|
if: matrix.os != 'windows-2019'
|
||||||
run: cargo test --no-default-features --features "use-dev-tty events bracketed-paste" -- --nocapture --test-threads 1
|
run: cargo test --no-default-features --features "use-dev-tty events" -- --nocapture --test-threads 1
|
||||||
continue-on-error: ${{ matrix.can-fail }}
|
continue-on-error: ${{ matrix.can-fail }}
|
||||||
- name: Test no default features with windows feature enabled
|
- name: Test no default features with windows feature enabled
|
||||||
if: matrix.os == 'windows-2019'
|
if: matrix.os == 'windows-2019'
|
||||||
|
33
CHANGELOG.md
33
CHANGELOG.md
@ -1,36 +1,7 @@
|
|||||||
# Unreleased
|
# Unreleased
|
||||||
|
|
||||||
# Version 0.28.1
|
- Use Rustix by default instead of libc. Libc can be re-enabled if necessary with the libc feature flag.
|
||||||
|
|
||||||
## Fixed 🐛
|
|
||||||
|
|
||||||
- Fix broken build on linux when using `use-dev-tty` with (#906)
|
|
||||||
|
|
||||||
## Breaking ⚠️
|
|
||||||
|
|
||||||
- Fix desync with mio and signalhook between repo and published crate. (upgrade to mio 1.0)
|
|
||||||
|
|
||||||
# Version 0.28
|
|
||||||
|
|
||||||
## Added ⭐
|
|
||||||
|
|
||||||
- Capture double click mouse events on windows (#826)
|
|
||||||
- (De)serialize Reset color (#824)
|
|
||||||
- Add functions to allow constructing `Attributes` in a const context (#817)
|
|
||||||
- Implement `Display` for `KeyCode` and `KeyModifiers` (#862)
|
|
||||||
|
|
||||||
## Changed ⚙️
|
|
||||||
|
|
||||||
- Use Rustix by default instead of libc. Libc can be re-enabled if necessary with the `libc` feature flag (#892)
|
|
||||||
- `FileDesc` now requires a lifetime annotation.
|
- `FileDesc` now requires a lifetime annotation.
|
||||||
- Improve available color detection (#885)
|
|
||||||
- Speed up `SetColors` by ~15-25% (#879)
|
|
||||||
- Remove unsafe and unnecessary size argument from `FileDesc::read()` (#821)
|
|
||||||
|
|
||||||
## Breaking ⚠️
|
|
||||||
|
|
||||||
- Fix duplicate bit masks for caps lock and num lock (#863).
|
|
||||||
This breaks serialization of `KeyEventState`
|
|
||||||
|
|
||||||
# Version 0.27.1
|
# Version 0.27.1
|
||||||
|
|
||||||
@ -47,7 +18,7 @@
|
|||||||
- Add `window_size` function to fetch pixel width/height of screen for more sophisticated rendering in terminals.
|
- Add `window_size` function to fetch pixel width/height of screen for more sophisticated rendering in terminals.
|
||||||
- Add support for deserializing hex color strings to `Color` e.g #fffff.
|
- Add support for deserializing hex color strings to `Color` e.g #fffff.
|
||||||
|
|
||||||
## Changed ⚙️
|
## Changes
|
||||||
|
|
||||||
- Make the events module an optional feature `events` (to make crossterm more lightweight) (#776)
|
- Make the events module an optional feature `events` (to make crossterm more lightweight) (#776)
|
||||||
|
|
||||||
|
96
Cargo.toml
96
Cargo.toml
@ -1,9 +1,10 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "minicrossterm"
|
name = "crossterm"
|
||||||
version = "0.28.1"
|
version = "0.27.0"
|
||||||
authors = ["Blasthavers", "T. Post"]
|
authors = ["T. Post"]
|
||||||
description = "A stripped back crossplatform terminal library for manipulating terminals asynchronously."
|
description = "A crossplatform terminal library for manipulating terminals."
|
||||||
repository = "https://git.blastmud.org/blasthavers/crossterm"
|
repository = "https://github.com/crossterm-rs/crossterm"
|
||||||
|
documentation = "https://docs.rs/crossterm/"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
keywords = ["event", "color", "cli", "input", "terminal"]
|
keywords = ["event", "color", "cli", "input", "terminal"]
|
||||||
exclude = ["target", "Cargo.lock"]
|
exclude = ["target", "Cargo.lock"]
|
||||||
@ -26,10 +27,21 @@ all-features = true
|
|||||||
# Features
|
# Features
|
||||||
#
|
#
|
||||||
[features]
|
[features]
|
||||||
default = ["bracketed-paste", "events"]
|
default = ["bracketed-paste", "windows", "events"]
|
||||||
|
windows = [
|
||||||
|
"dep:winapi",
|
||||||
|
"dep:crossterm_winapi",
|
||||||
|
] # Disables winapi dependencies from being included into the binary (SHOULD NOT be disabled on windows).
|
||||||
bracketed-paste = [
|
bracketed-paste = [
|
||||||
] # Enables triggering a `Event::Paste` when pasting text into the terminal.
|
] # Enables triggering a `Event::Paste` when pasting text into the terminal.
|
||||||
|
event-stream = ["dep:futures-core", "events"] # Enables async events
|
||||||
|
use-dev-tty = [
|
||||||
|
"filedescriptor",
|
||||||
|
] # Enables raw file descriptor polling / selecting instead of mio.
|
||||||
events = [
|
events = [
|
||||||
|
"dep:mio",
|
||||||
|
"dep:signal-hook",
|
||||||
|
"dep:signal-hook-mio",
|
||||||
] # Enables reading input/events from the system.
|
] # Enables reading input/events from the system.
|
||||||
serde = ["dep:serde", "bitflags/serde"] # Enables 'serde' for various types.
|
serde = ["dep:serde", "bitflags/serde"] # Enables 'serde' for various types.
|
||||||
|
|
||||||
@ -38,14 +50,84 @@ serde = ["dep:serde", "bitflags/serde"] # Enables 'serde' for various types.
|
|||||||
#
|
#
|
||||||
[dependencies]
|
[dependencies]
|
||||||
bitflags = { version = "2.3" }
|
bitflags = { version = "2.3" }
|
||||||
|
parking_lot = "0.12"
|
||||||
|
|
||||||
# optional deps only added when requested
|
# optional deps only added when requested
|
||||||
|
futures-core = { version = "0.3", optional = true, default-features = false }
|
||||||
serde = { version = "1.0", features = ["derive"], optional = true }
|
serde = { version = "1.0", features = ["derive"], optional = true }
|
||||||
|
|
||||||
|
#
|
||||||
|
# Windows dependencies
|
||||||
|
#
|
||||||
|
[target.'cfg(windows)'.dependencies.winapi]
|
||||||
|
version = "0.3.9"
|
||||||
|
features = ["winuser", "winerror"]
|
||||||
|
optional = true
|
||||||
|
|
||||||
|
[target.'cfg(windows)'.dependencies]
|
||||||
|
crossterm_winapi = { version = "0.9.1", optional = true }
|
||||||
|
|
||||||
|
#
|
||||||
|
# UNIX dependencies
|
||||||
|
#
|
||||||
|
[target.'cfg(unix)'.dependencies]
|
||||||
|
# Default to using rustix for UNIX systems, but provide an option to use libc for backwards
|
||||||
|
# compatibility.
|
||||||
|
libc = { version = "0.2", default-features = false, optional = true }
|
||||||
|
rustix = { version = "0.38.34", default-features = false, features = [
|
||||||
|
"std",
|
||||||
|
"stdio",
|
||||||
|
"termios",
|
||||||
|
] }
|
||||||
|
signal-hook = { version = "0.3.17", optional = true }
|
||||||
|
filedescriptor = { version = "0.8", optional = true }
|
||||||
|
mio = { version = "0.8", features = ["os-poll"], optional = true }
|
||||||
|
signal-hook-mio = { version = "0.2.3", features = [
|
||||||
|
"support-v0_8",
|
||||||
|
], optional = true }
|
||||||
|
|
||||||
#
|
#
|
||||||
# Dev dependencies (examples, ...)
|
# Dev dependencies (examples, ...)
|
||||||
#
|
#
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
|
tokio = { version = "1.25", features = ["full"] }
|
||||||
|
futures = "0.3"
|
||||||
|
futures-timer = "3.0"
|
||||||
|
async-std = "1.12"
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
serial_test = "2.0.0"
|
serial_test = "2.0.0"
|
||||||
temp-env = "0.3.6"
|
|
||||||
|
#
|
||||||
|
# Examples
|
||||||
|
#
|
||||||
|
[[example]]
|
||||||
|
name = "event-read"
|
||||||
|
required-features = ["bracketed-paste", "events"]
|
||||||
|
|
||||||
|
[[example]]
|
||||||
|
name = "event-match-modifiers"
|
||||||
|
required-features = ["bracketed-paste", "events"]
|
||||||
|
|
||||||
|
[[example]]
|
||||||
|
name = "event-poll-read"
|
||||||
|
required-features = ["bracketed-paste", "events"]
|
||||||
|
|
||||||
|
[[example]]
|
||||||
|
name = "event-stream-async-std"
|
||||||
|
required-features = ["event-stream", "events"]
|
||||||
|
|
||||||
|
[[example]]
|
||||||
|
name = "event-stream-tokio"
|
||||||
|
required-features = ["event-stream", "events"]
|
||||||
|
|
||||||
|
[[example]]
|
||||||
|
name = "event-read-char-line"
|
||||||
|
required-features = ["events"]
|
||||||
|
|
||||||
|
[[example]]
|
||||||
|
name = "stderr"
|
||||||
|
required-features = ["events"]
|
||||||
|
|
||||||
|
[[example]]
|
||||||
|
name = "key-display"
|
||||||
|
required-features = ["events"]
|
||||||
|
12
README.md
12
README.md
@ -1,20 +1,12 @@
|
|||||||
# Minicrossterm
|
<h1 align="center"><img width="440" src="docs/crossterm_full.png" /></h1>
|
||||||
|
|
||||||
Minicrossterm is a cut back fork of crossterm which doesn't depend on stdio, and is intended
|
[![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]
|
||||||
to only provide non-blocking functionality for applications without relying on standard IO (
|
|
||||||
for example, from webassembly).
|
|
||||||
|
|
||||||
It was created for Blasthavers Worldwideportal, and not by the original authors of crossterm.
|
|
||||||
|
|
||||||
The following information applies to crossterm - some might not be up to date for minicrossterm.
|
|
||||||
|
|
||||||
# Cross-platform Terminal Manipulation Library
|
# Cross-platform Terminal Manipulation Library
|
||||||
|
|
||||||
Crossterm is a pure-rust, terminal manipulation library that makes it possible to write cross-platform text-based interfaces (see [features](#features)). It supports all UNIX and Windows terminals down to Windows 7 (not all terminals are tested,
|
Crossterm is a pure-rust, terminal manipulation library that makes it possible to write cross-platform text-based interfaces (see [features](#features)). It supports all UNIX and Windows terminals down to Windows 7 (not all terminals are tested,
|
||||||
see [Tested Terminals](#tested-terminals) for more info).
|
see [Tested Terminals](#tested-terminals) for more info).
|
||||||
|
|
||||||
[![Donate to crossterm upstream](https://img.shields.io/badge/Donate-PayPal-green.svg)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=Z8QK6XU749JB2)]
|
|
||||||
|
|
||||||
## Table of Contents
|
## Table of Contents
|
||||||
|
|
||||||
- [Cross-platform Terminal Manipulation Library](#cross-platform-terminal-manipulation-library)
|
- [Cross-platform Terminal Manipulation Library](#cross-platform-terminal-manipulation-library)
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
# Known Problems
|
# Known Problems
|
||||||
|
|
||||||
There are some problems I discovered during development.
|
There are some problems I discovered during development.
|
||||||
And I don't think it has to do anything with crossterm but it has to do with how terminals handle ANSI or WinApi.
|
And I don't think it has to do anything with crossterm but it has to do whit how terminals handle ANSI or WinApi.
|
||||||
|
|
||||||
## WinAPI
|
## WinAPI
|
||||||
|
|
||||||
|
40
examples/README.md
Normal file
40
examples/README.md
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
![Lines of Code][s7] [![MIT][s2]][l2] [![Join us on Discord][s5]][l5]
|
||||||
|
|
||||||
|
# Crossterm Examples
|
||||||
|
|
||||||
|
The examples are compatible with the latest release.
|
||||||
|
|
||||||
|
## Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
├── examples
|
||||||
|
│ └── interactive-test
|
||||||
|
│ └── event-*
|
||||||
|
│ └── stderr
|
||||||
|
```
|
||||||
|
| File Name | Description | Topics |
|
||||||
|
|:----------------------------|:-------------------------------|:------------------------------------------|
|
||||||
|
| `examples/interactive-test` | interactive, walk through, demo | cursor, style, event |
|
||||||
|
| `event-*` | event reading demos | (async) event reading |
|
||||||
|
| `stderr` | crossterm over stderr demo | raw mode, alternate screen, custom output |
|
||||||
|
| `is_tty` | Is this instance a tty ? | tty |
|
||||||
|
|
||||||
|
## Run examples
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ cargo run --example [file name]
|
||||||
|
```
|
||||||
|
|
||||||
|
To run the interactive-demo go into the folder `examples/interactive-demo` and run `cargo run`.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
This project is licensed under the MIT License - see the [LICENSE.md](LICENSE) file for details.
|
||||||
|
|
||||||
|
[s2]: https://img.shields.io/badge/license-MIT-blue.svg
|
||||||
|
[l2]: LICENSE
|
||||||
|
|
||||||
|
[s5]: https://img.shields.io/discord/560857607196377088.svg?logo=discord
|
||||||
|
[l5]: https://discord.gg/K4nyTDB
|
||||||
|
|
||||||
|
[s7]: https://travis-ci.org/crossterm-rs/examples.svg?branch=master
|
68
examples/event-match-modifiers.rs
Normal file
68
examples/event-match-modifiers.rs
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
//! Demonstrates how to match on modifiers like: Control, alt, shift.
|
||||||
|
//!
|
||||||
|
//! cargo run --example event-match-modifiers
|
||||||
|
|
||||||
|
use crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers};
|
||||||
|
|
||||||
|
fn match_event(read_event: Event) {
|
||||||
|
match read_event {
|
||||||
|
// Match one one modifier:
|
||||||
|
Event::Key(KeyEvent {
|
||||||
|
modifiers: KeyModifiers::CONTROL,
|
||||||
|
code,
|
||||||
|
..
|
||||||
|
}) => {
|
||||||
|
println!("Control + {:?}", code);
|
||||||
|
}
|
||||||
|
Event::Key(KeyEvent {
|
||||||
|
modifiers: KeyModifiers::SHIFT,
|
||||||
|
code,
|
||||||
|
..
|
||||||
|
}) => {
|
||||||
|
println!("Shift + {:?}", code);
|
||||||
|
}
|
||||||
|
Event::Key(KeyEvent {
|
||||||
|
modifiers: KeyModifiers::ALT,
|
||||||
|
code,
|
||||||
|
..
|
||||||
|
}) => {
|
||||||
|
println!("Alt + {:?}", code);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Match on multiple modifiers:
|
||||||
|
Event::Key(KeyEvent {
|
||||||
|
code, modifiers, ..
|
||||||
|
}) => {
|
||||||
|
if modifiers == (KeyModifiers::ALT | KeyModifiers::SHIFT) {
|
||||||
|
println!("Alt + Shift {:?}", code);
|
||||||
|
} else {
|
||||||
|
println!("({:?}) with key: {:?}", modifiers, code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
match_event(Event::Key(KeyEvent::new(
|
||||||
|
KeyCode::Char('z'),
|
||||||
|
KeyModifiers::CONTROL,
|
||||||
|
)));
|
||||||
|
match_event(Event::Key(KeyEvent::new(
|
||||||
|
KeyCode::Left,
|
||||||
|
KeyModifiers::SHIFT,
|
||||||
|
)));
|
||||||
|
match_event(Event::Key(KeyEvent::new(
|
||||||
|
KeyCode::Delete,
|
||||||
|
KeyModifiers::ALT,
|
||||||
|
)));
|
||||||
|
match_event(Event::Key(KeyEvent::new(
|
||||||
|
KeyCode::Right,
|
||||||
|
KeyModifiers::ALT | KeyModifiers::SHIFT,
|
||||||
|
)));
|
||||||
|
match_event(Event::Key(KeyEvent::new(
|
||||||
|
KeyCode::Home,
|
||||||
|
KeyModifiers::ALT | KeyModifiers::CONTROL,
|
||||||
|
)));
|
||||||
|
}
|
61
examples/event-poll-read.rs
Normal file
61
examples/event-poll-read.rs
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
//! Demonstrates how to match on modifiers like: Control, alt, shift.
|
||||||
|
//!
|
||||||
|
//! cargo run --example event-poll-read
|
||||||
|
|
||||||
|
use std::{io, time::Duration};
|
||||||
|
|
||||||
|
use crossterm::{
|
||||||
|
cursor::position,
|
||||||
|
event::{poll, read, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
|
||||||
|
execute,
|
||||||
|
terminal::{disable_raw_mode, enable_raw_mode},
|
||||||
|
};
|
||||||
|
|
||||||
|
const HELP: &str = r#"Blocking poll() & non-blocking read()
|
||||||
|
- Keyboard, mouse and terminal resize events enabled
|
||||||
|
- Prints "." every second if there's no event
|
||||||
|
- Hit "c" to print current cursor position
|
||||||
|
- Use Esc to quit
|
||||||
|
"#;
|
||||||
|
|
||||||
|
fn print_events() -> io::Result<()> {
|
||||||
|
loop {
|
||||||
|
// Wait up to 1s for another event
|
||||||
|
if poll(Duration::from_millis(1_000))? {
|
||||||
|
// It's guaranteed that read() won't block if `poll` returns `Ok(true)`
|
||||||
|
let event = read()?;
|
||||||
|
|
||||||
|
println!("Event::{:?}\r", event);
|
||||||
|
|
||||||
|
if event == Event::Key(KeyCode::Char('c').into()) {
|
||||||
|
println!("Cursor position: {:?}\r", position());
|
||||||
|
}
|
||||||
|
|
||||||
|
if event == Event::Key(KeyCode::Esc.into()) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Timeout expired, no event for 1s
|
||||||
|
println!(".\r");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() -> io::Result<()> {
|
||||||
|
println!("{}", HELP);
|
||||||
|
|
||||||
|
enable_raw_mode()?;
|
||||||
|
|
||||||
|
let mut stdout = io::stdout();
|
||||||
|
execute!(stdout, EnableMouseCapture)?;
|
||||||
|
|
||||||
|
if let Err(e) = print_events() {
|
||||||
|
println!("Error: {:?}\r", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
execute!(stdout, DisableMouseCapture)?;
|
||||||
|
|
||||||
|
disable_raw_mode()
|
||||||
|
}
|
44
examples/event-read-char-line.rs
Normal file
44
examples/event-read-char-line.rs
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
//! Demonstrates how to block read characters or a full line.
|
||||||
|
//! Just note that crossterm is not required to do this and can be done with `io::stdin()`.
|
||||||
|
//!
|
||||||
|
//! cargo run --example event-read-char-line
|
||||||
|
|
||||||
|
use std::io;
|
||||||
|
|
||||||
|
use crossterm::event::{self, Event, KeyCode, KeyEvent};
|
||||||
|
|
||||||
|
pub fn read_char() -> io::Result<char> {
|
||||||
|
loop {
|
||||||
|
if let Event::Key(KeyEvent {
|
||||||
|
code: KeyCode::Char(c),
|
||||||
|
..
|
||||||
|
}) = event::read()?
|
||||||
|
{
|
||||||
|
return Ok(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn read_line() -> io::Result<String> {
|
||||||
|
let mut line = String::new();
|
||||||
|
while let Event::Key(KeyEvent { code, .. }) = event::read()? {
|
||||||
|
match code {
|
||||||
|
KeyCode::Enter => {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
KeyCode::Char(c) => {
|
||||||
|
line.push(c);
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(line)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
println!("read line:");
|
||||||
|
println!("{:?}", read_line());
|
||||||
|
println!("read char:");
|
||||||
|
println!("{:?}", read_char());
|
||||||
|
}
|
112
examples/event-read.rs
Normal file
112
examples/event-read.rs
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
//! Demonstrates how to block read events.
|
||||||
|
//!
|
||||||
|
//! cargo run --example event-read
|
||||||
|
|
||||||
|
use std::io;
|
||||||
|
|
||||||
|
use crossterm::event::{
|
||||||
|
poll, KeyboardEnhancementFlags, PopKeyboardEnhancementFlags, PushKeyboardEnhancementFlags,
|
||||||
|
};
|
||||||
|
use crossterm::{
|
||||||
|
cursor::position,
|
||||||
|
event::{
|
||||||
|
read, DisableBracketedPaste, DisableFocusChange, DisableMouseCapture, EnableBracketedPaste,
|
||||||
|
EnableFocusChange, EnableMouseCapture, Event, KeyCode,
|
||||||
|
},
|
||||||
|
execute, queue,
|
||||||
|
terminal::{disable_raw_mode, enable_raw_mode},
|
||||||
|
};
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
const HELP: &str = r#"Blocking read()
|
||||||
|
- Keyboard, mouse, focus and terminal resize events enabled
|
||||||
|
- Hit "c" to print current cursor position
|
||||||
|
- Use Esc to quit
|
||||||
|
"#;
|
||||||
|
|
||||||
|
fn print_events() -> io::Result<()> {
|
||||||
|
loop {
|
||||||
|
// Blocking read
|
||||||
|
let event = read()?;
|
||||||
|
|
||||||
|
println!("Event: {:?}\r", event);
|
||||||
|
|
||||||
|
if event == Event::Key(KeyCode::Char('c').into()) {
|
||||||
|
println!("Cursor position: {:?}\r", position());
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Event::Resize(x, y) = event {
|
||||||
|
let (original_size, new_size) = flush_resize_events((x, y));
|
||||||
|
println!("Resize from: {:?}, to: {:?}\r", original_size, new_size);
|
||||||
|
}
|
||||||
|
|
||||||
|
if event == Event::Key(KeyCode::Esc.into()) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resize events can occur in batches.
|
||||||
|
// With a simple loop they can be flushed.
|
||||||
|
// This function will keep the first and last resize event.
|
||||||
|
fn flush_resize_events(first_resize: (u16, u16)) -> ((u16, u16), (u16, u16)) {
|
||||||
|
let mut last_resize = first_resize;
|
||||||
|
while let Ok(true) = poll(Duration::from_millis(50)) {
|
||||||
|
if let Ok(Event::Resize(x, y)) = read() {
|
||||||
|
last_resize = (x, y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
(first_resize, last_resize)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() -> io::Result<()> {
|
||||||
|
println!("{}", HELP);
|
||||||
|
|
||||||
|
enable_raw_mode()?;
|
||||||
|
|
||||||
|
let mut stdout = io::stdout();
|
||||||
|
|
||||||
|
let supports_keyboard_enhancement = matches!(
|
||||||
|
crossterm::terminal::supports_keyboard_enhancement(),
|
||||||
|
Ok(true)
|
||||||
|
);
|
||||||
|
|
||||||
|
if supports_keyboard_enhancement {
|
||||||
|
queue!(
|
||||||
|
stdout,
|
||||||
|
PushKeyboardEnhancementFlags(
|
||||||
|
KeyboardEnhancementFlags::DISAMBIGUATE_ESCAPE_CODES
|
||||||
|
| KeyboardEnhancementFlags::REPORT_ALL_KEYS_AS_ESCAPE_CODES
|
||||||
|
| KeyboardEnhancementFlags::REPORT_ALTERNATE_KEYS
|
||||||
|
| KeyboardEnhancementFlags::REPORT_EVENT_TYPES
|
||||||
|
)
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
execute!(
|
||||||
|
stdout,
|
||||||
|
EnableBracketedPaste,
|
||||||
|
EnableFocusChange,
|
||||||
|
EnableMouseCapture,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
if let Err(e) = print_events() {
|
||||||
|
println!("Error: {:?}\r", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
if supports_keyboard_enhancement {
|
||||||
|
queue!(stdout, PopKeyboardEnhancementFlags)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
execute!(
|
||||||
|
stdout,
|
||||||
|
DisableBracketedPaste,
|
||||||
|
DisableFocusChange,
|
||||||
|
DisableMouseCapture
|
||||||
|
)?;
|
||||||
|
|
||||||
|
disable_raw_mode()
|
||||||
|
}
|
67
examples/event-stream-async-std.rs
Normal file
67
examples/event-stream-async-std.rs
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
//! Demonstrates how to read events asynchronously with async-std.
|
||||||
|
//!
|
||||||
|
//! cargo run --features="event-stream" --example event-stream-async-std
|
||||||
|
|
||||||
|
use std::{io::stdout, time::Duration};
|
||||||
|
|
||||||
|
use futures::{future::FutureExt, select, StreamExt};
|
||||||
|
use futures_timer::Delay;
|
||||||
|
|
||||||
|
use crossterm::{
|
||||||
|
cursor::position,
|
||||||
|
event::{DisableMouseCapture, EnableMouseCapture, Event, EventStream, KeyCode},
|
||||||
|
execute,
|
||||||
|
terminal::{disable_raw_mode, enable_raw_mode},
|
||||||
|
};
|
||||||
|
|
||||||
|
const HELP: &str = r#"EventStream based on futures_util::stream::Stream with async-std
|
||||||
|
- Keyboard, mouse and terminal resize events enabled
|
||||||
|
- Prints "." every second if there's no event
|
||||||
|
- Hit "c" to print current cursor position
|
||||||
|
- Use Esc to quit
|
||||||
|
"#;
|
||||||
|
|
||||||
|
async fn print_events() {
|
||||||
|
let mut reader = EventStream::new();
|
||||||
|
|
||||||
|
loop {
|
||||||
|
let mut delay = Delay::new(Duration::from_millis(1_000)).fuse();
|
||||||
|
let mut event = reader.next().fuse();
|
||||||
|
|
||||||
|
select! {
|
||||||
|
_ = delay => { println!(".\r"); },
|
||||||
|
maybe_event = event => {
|
||||||
|
match maybe_event {
|
||||||
|
Some(Ok(event)) => {
|
||||||
|
println!("Event::{:?}\r", event);
|
||||||
|
|
||||||
|
if event == Event::Key(KeyCode::Char('c').into()) {
|
||||||
|
println!("Cursor position: {:?}\r", position());
|
||||||
|
}
|
||||||
|
|
||||||
|
if event == Event::Key(KeyCode::Esc.into()) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(Err(e)) => println!("Error: {:?}\r", e),
|
||||||
|
None => break,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() -> std::io::Result<()> {
|
||||||
|
println!("{}", HELP);
|
||||||
|
|
||||||
|
enable_raw_mode()?;
|
||||||
|
|
||||||
|
let mut stdout = stdout();
|
||||||
|
execute!(stdout, EnableMouseCapture)?;
|
||||||
|
|
||||||
|
async_std::task::block_on(print_events());
|
||||||
|
|
||||||
|
execute!(stdout, DisableMouseCapture)?;
|
||||||
|
|
||||||
|
disable_raw_mode()
|
||||||
|
}
|
68
examples/event-stream-tokio.rs
Normal file
68
examples/event-stream-tokio.rs
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
//! Demonstrates how to read events asynchronously with tokio.
|
||||||
|
//!
|
||||||
|
//! cargo run --features="event-stream" --example event-stream-tokio
|
||||||
|
|
||||||
|
use std::{io::stdout, time::Duration};
|
||||||
|
|
||||||
|
use futures::{future::FutureExt, select, StreamExt};
|
||||||
|
use futures_timer::Delay;
|
||||||
|
|
||||||
|
use crossterm::{
|
||||||
|
cursor::position,
|
||||||
|
event::{DisableMouseCapture, EnableMouseCapture, Event, EventStream, KeyCode},
|
||||||
|
execute,
|
||||||
|
terminal::{disable_raw_mode, enable_raw_mode},
|
||||||
|
};
|
||||||
|
|
||||||
|
const HELP: &str = r#"EventStream based on futures_util::Stream with tokio
|
||||||
|
- Keyboard, mouse and terminal resize events enabled
|
||||||
|
- Prints "." every second if there's no event
|
||||||
|
- Hit "c" to print current cursor position
|
||||||
|
- Use Esc to quit
|
||||||
|
"#;
|
||||||
|
|
||||||
|
async fn print_events() {
|
||||||
|
let mut reader = EventStream::new();
|
||||||
|
|
||||||
|
loop {
|
||||||
|
let mut delay = Delay::new(Duration::from_millis(1_000)).fuse();
|
||||||
|
let mut event = reader.next().fuse();
|
||||||
|
|
||||||
|
select! {
|
||||||
|
_ = delay => { println!(".\r"); },
|
||||||
|
maybe_event = event => {
|
||||||
|
match maybe_event {
|
||||||
|
Some(Ok(event)) => {
|
||||||
|
println!("Event::{:?}\r", event);
|
||||||
|
|
||||||
|
if event == Event::Key(KeyCode::Char('c').into()) {
|
||||||
|
println!("Cursor position: {:?}\r", position());
|
||||||
|
}
|
||||||
|
|
||||||
|
if event == Event::Key(KeyCode::Esc.into()) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(Err(e)) => println!("Error: {:?}\r", e),
|
||||||
|
None => break,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() -> std::io::Result<()> {
|
||||||
|
println!("{}", HELP);
|
||||||
|
|
||||||
|
enable_raw_mode()?;
|
||||||
|
|
||||||
|
let mut stdout = stdout();
|
||||||
|
execute!(stdout, EnableMouseCapture)?;
|
||||||
|
|
||||||
|
print_events().await;
|
||||||
|
|
||||||
|
execute!(stdout, DisableMouseCapture)?;
|
||||||
|
|
||||||
|
disable_raw_mode()
|
||||||
|
}
|
13
examples/interactive-demo/Cargo.toml
Normal file
13
examples/interactive-demo/Cargo.toml
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
[package]
|
||||||
|
name = "interactive-demo"
|
||||||
|
version = "0.0.1"
|
||||||
|
authors = ["T. Post", "Robert Vojta <rvojta@me.com>"]
|
||||||
|
edition = "2018"
|
||||||
|
description = "Interactive demo for crossterm."
|
||||||
|
license = "MIT"
|
||||||
|
exclude = ["target", "Cargo.lock"]
|
||||||
|
readme = "README.md"
|
||||||
|
publish = false
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
crossterm = { path = "../../" }
|
29
examples/interactive-demo/src/macros.rs
Normal file
29
examples/interactive-demo/src/macros.rs
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
macro_rules! run_tests {
|
||||||
|
(
|
||||||
|
$dst:expr,
|
||||||
|
$(
|
||||||
|
$testfn:ident
|
||||||
|
),*
|
||||||
|
$(,)?
|
||||||
|
) => {
|
||||||
|
use crossterm::{queue, style, terminal, cursor};
|
||||||
|
$(
|
||||||
|
queue!(
|
||||||
|
$dst,
|
||||||
|
style::ResetColor,
|
||||||
|
terminal::Clear(terminal::ClearType::All),
|
||||||
|
cursor::MoveTo(1, 1),
|
||||||
|
cursor::Show,
|
||||||
|
cursor::EnableBlinking
|
||||||
|
)?;
|
||||||
|
|
||||||
|
$testfn($dst)?;
|
||||||
|
|
||||||
|
match $crate::read_char() {
|
||||||
|
Ok('q') => return Ok(()),
|
||||||
|
Err(e) => return Err(e),
|
||||||
|
_ => { },
|
||||||
|
};
|
||||||
|
)*
|
||||||
|
}
|
||||||
|
}
|
104
examples/interactive-demo/src/main.rs
Normal file
104
examples/interactive-demo/src/main.rs
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
#![allow(clippy::cognitive_complexity)]
|
||||||
|
|
||||||
|
use std::io;
|
||||||
|
|
||||||
|
use crossterm::event::KeyEventKind;
|
||||||
|
pub use crossterm::{
|
||||||
|
cursor,
|
||||||
|
event::{self, Event, KeyCode, KeyEvent},
|
||||||
|
execute, queue, style,
|
||||||
|
terminal::{self, ClearType},
|
||||||
|
Command,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[macro_use]
|
||||||
|
mod macros;
|
||||||
|
mod test;
|
||||||
|
|
||||||
|
const MENU: &str = r#"Crossterm interactive test
|
||||||
|
|
||||||
|
Controls:
|
||||||
|
|
||||||
|
- 'q' - quit interactive test (or return to this menu)
|
||||||
|
- any other key - continue with next step
|
||||||
|
|
||||||
|
Available tests:
|
||||||
|
|
||||||
|
1. cursor
|
||||||
|
2. color (foreground, background)
|
||||||
|
3. attributes (bold, italic, ...)
|
||||||
|
4. input
|
||||||
|
5. synchronized output
|
||||||
|
|
||||||
|
Select test to run ('1', '2', ...) or hit 'q' to quit.
|
||||||
|
"#;
|
||||||
|
|
||||||
|
fn run<W>(w: &mut W) -> io::Result<()>
|
||||||
|
where
|
||||||
|
W: io::Write,
|
||||||
|
{
|
||||||
|
execute!(w, terminal::EnterAlternateScreen)?;
|
||||||
|
|
||||||
|
terminal::enable_raw_mode()?;
|
||||||
|
|
||||||
|
loop {
|
||||||
|
queue!(
|
||||||
|
w,
|
||||||
|
style::ResetColor,
|
||||||
|
terminal::Clear(ClearType::All),
|
||||||
|
cursor::Hide,
|
||||||
|
cursor::MoveTo(1, 1)
|
||||||
|
)?;
|
||||||
|
|
||||||
|
for line in MENU.split('\n') {
|
||||||
|
queue!(w, style::Print(line), cursor::MoveToNextLine(1))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
w.flush()?;
|
||||||
|
|
||||||
|
match read_char()? {
|
||||||
|
'1' => test::cursor::run(w)?,
|
||||||
|
'2' => test::color::run(w)?,
|
||||||
|
'3' => test::attribute::run(w)?,
|
||||||
|
'4' => test::event::run(w)?,
|
||||||
|
'5' => test::synchronized_output::run(w)?,
|
||||||
|
'q' => {
|
||||||
|
execute!(w, cursor::SetCursorStyle::DefaultUserShape).unwrap();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
execute!(
|
||||||
|
w,
|
||||||
|
style::ResetColor,
|
||||||
|
cursor::Show,
|
||||||
|
terminal::LeaveAlternateScreen
|
||||||
|
)?;
|
||||||
|
|
||||||
|
terminal::disable_raw_mode()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn read_char() -> std::io::Result<char> {
|
||||||
|
loop {
|
||||||
|
if let Ok(Event::Key(KeyEvent {
|
||||||
|
code: KeyCode::Char(c),
|
||||||
|
kind: KeyEventKind::Press,
|
||||||
|
modifiers: _,
|
||||||
|
state: _,
|
||||||
|
})) = event::read()
|
||||||
|
{
|
||||||
|
return Ok(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn buffer_size() -> io::Result<(u16, u16)> {
|
||||||
|
terminal::size()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() -> std::io::Result<()> {
|
||||||
|
let mut stdout = io::stdout();
|
||||||
|
run(&mut stdout)
|
||||||
|
}
|
5
examples/interactive-demo/src/test.rs
Normal file
5
examples/interactive-demo/src/test.rs
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
pub mod attribute;
|
||||||
|
pub mod color;
|
||||||
|
pub mod cursor;
|
||||||
|
pub mod event;
|
||||||
|
pub mod synchronized_output;
|
58
examples/interactive-demo/src/test/attribute.rs
Normal file
58
examples/interactive-demo/src/test/attribute.rs
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
#![allow(clippy::cognitive_complexity)]
|
||||||
|
|
||||||
|
use crossterm::{cursor, queue, style};
|
||||||
|
use std::io::Write;
|
||||||
|
|
||||||
|
const ATTRIBUTES: [(style::Attribute, style::Attribute); 10] = [
|
||||||
|
(style::Attribute::Bold, style::Attribute::NormalIntensity),
|
||||||
|
(style::Attribute::Italic, style::Attribute::NoItalic),
|
||||||
|
(style::Attribute::Underlined, style::Attribute::NoUnderline),
|
||||||
|
(
|
||||||
|
style::Attribute::DoubleUnderlined,
|
||||||
|
style::Attribute::NoUnderline,
|
||||||
|
),
|
||||||
|
(style::Attribute::Undercurled, style::Attribute::NoUnderline),
|
||||||
|
(style::Attribute::Underdotted, style::Attribute::NoUnderline),
|
||||||
|
(style::Attribute::Underdashed, style::Attribute::NoUnderline),
|
||||||
|
(style::Attribute::Reverse, style::Attribute::NoReverse),
|
||||||
|
(
|
||||||
|
style::Attribute::CrossedOut,
|
||||||
|
style::Attribute::NotCrossedOut,
|
||||||
|
),
|
||||||
|
(style::Attribute::SlowBlink, style::Attribute::NoBlink),
|
||||||
|
];
|
||||||
|
|
||||||
|
fn test_set_display_attributes<W>(w: &mut W) -> std::io::Result<()>
|
||||||
|
where
|
||||||
|
W: Write,
|
||||||
|
{
|
||||||
|
queue!(
|
||||||
|
w,
|
||||||
|
style::Print("Display attributes"),
|
||||||
|
cursor::MoveToNextLine(2)
|
||||||
|
)?;
|
||||||
|
|
||||||
|
for (on, off) in &ATTRIBUTES {
|
||||||
|
queue!(
|
||||||
|
w,
|
||||||
|
style::SetAttribute(*on),
|
||||||
|
style::Print(format!("{:>width$} ", format!("{:?}", on), width = 35)),
|
||||||
|
style::SetAttribute(*off),
|
||||||
|
style::Print(format!("{:>width$}", format!("{:?}", off), width = 35)),
|
||||||
|
style::ResetColor,
|
||||||
|
cursor::MoveToNextLine(1)
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
w.flush()?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn run<W>(w: &mut W) -> std::io::Result<()>
|
||||||
|
where
|
||||||
|
W: Write,
|
||||||
|
{
|
||||||
|
run_tests!(w, test_set_display_attributes,);
|
||||||
|
Ok(())
|
||||||
|
}
|
198
examples/interactive-demo/src/test/color.rs
Normal file
198
examples/interactive-demo/src/test/color.rs
Normal file
@ -0,0 +1,198 @@
|
|||||||
|
#![allow(clippy::cognitive_complexity)]
|
||||||
|
|
||||||
|
use crossterm::{cursor, queue, style, style::Color};
|
||||||
|
use std::io::Write;
|
||||||
|
|
||||||
|
const COLORS: [Color; 21] = [
|
||||||
|
Color::Black,
|
||||||
|
Color::DarkGrey,
|
||||||
|
Color::Grey,
|
||||||
|
Color::White,
|
||||||
|
Color::DarkRed,
|
||||||
|
Color::Red,
|
||||||
|
Color::DarkGreen,
|
||||||
|
Color::Green,
|
||||||
|
Color::DarkYellow,
|
||||||
|
Color::Yellow,
|
||||||
|
Color::DarkBlue,
|
||||||
|
Color::Blue,
|
||||||
|
Color::DarkMagenta,
|
||||||
|
Color::Magenta,
|
||||||
|
Color::DarkCyan,
|
||||||
|
Color::Cyan,
|
||||||
|
Color::AnsiValue(0),
|
||||||
|
Color::AnsiValue(15),
|
||||||
|
Color::Rgb { r: 255, g: 0, b: 0 },
|
||||||
|
Color::Rgb { r: 0, g: 255, b: 0 },
|
||||||
|
Color::Rgb { r: 0, g: 0, b: 255 },
|
||||||
|
];
|
||||||
|
|
||||||
|
fn test_set_foreground_color<W>(w: &mut W) -> std::io::Result<()>
|
||||||
|
where
|
||||||
|
W: Write,
|
||||||
|
{
|
||||||
|
queue!(
|
||||||
|
w,
|
||||||
|
style::Print("Foreground colors on the black & white background"),
|
||||||
|
cursor::MoveToNextLine(2)
|
||||||
|
)?;
|
||||||
|
|
||||||
|
for color in &COLORS {
|
||||||
|
queue!(
|
||||||
|
w,
|
||||||
|
style::SetForegroundColor(*color),
|
||||||
|
style::SetBackgroundColor(Color::Black),
|
||||||
|
style::Print(format!(
|
||||||
|
"{:>width$} ",
|
||||||
|
format!("{:?} ████████████", color),
|
||||||
|
width = 40
|
||||||
|
)),
|
||||||
|
style::SetBackgroundColor(Color::White),
|
||||||
|
style::Print(format!(
|
||||||
|
"{:>width$}",
|
||||||
|
format!("{:?} ████████████", color),
|
||||||
|
width = 40
|
||||||
|
)),
|
||||||
|
cursor::MoveToNextLine(1)
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
w.flush()?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn test_set_background_color<W>(w: &mut W) -> std::io::Result<()>
|
||||||
|
where
|
||||||
|
W: Write,
|
||||||
|
{
|
||||||
|
queue!(
|
||||||
|
w,
|
||||||
|
style::Print("Background colors with black & white foreground"),
|
||||||
|
cursor::MoveToNextLine(2)
|
||||||
|
)?;
|
||||||
|
|
||||||
|
for color in &COLORS {
|
||||||
|
queue!(
|
||||||
|
w,
|
||||||
|
style::SetBackgroundColor(*color),
|
||||||
|
style::SetForegroundColor(Color::Black),
|
||||||
|
style::Print(format!(
|
||||||
|
"{:>width$} ",
|
||||||
|
format!("{:?} ▒▒▒▒▒▒▒▒▒▒▒▒", color),
|
||||||
|
width = 40
|
||||||
|
)),
|
||||||
|
style::SetForegroundColor(Color::White),
|
||||||
|
style::Print(format!(
|
||||||
|
"{:>width$}",
|
||||||
|
format!("{:?} ▒▒▒▒▒▒▒▒▒▒▒▒", color),
|
||||||
|
width = 40
|
||||||
|
)),
|
||||||
|
cursor::MoveToNextLine(1)
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
w.flush()?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn test_color_values_matrix_16x16<W, F>(w: &mut W, title: &str, color: F) -> std::io::Result<()>
|
||||||
|
where
|
||||||
|
W: Write,
|
||||||
|
F: Fn(u16, u16) -> Color,
|
||||||
|
{
|
||||||
|
queue!(w, style::Print(title))?;
|
||||||
|
|
||||||
|
for idx in 0..=15 {
|
||||||
|
queue!(
|
||||||
|
w,
|
||||||
|
cursor::MoveTo(1, idx + 4),
|
||||||
|
style::Print(format!("{:>width$}", idx, width = 2))
|
||||||
|
)?;
|
||||||
|
queue!(
|
||||||
|
w,
|
||||||
|
cursor::MoveTo(idx * 3 + 3, 3),
|
||||||
|
style::Print(format!("{:>width$}", idx, width = 3))
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
for row in 0..=15u16 {
|
||||||
|
queue!(w, cursor::MoveTo(4, row + 4))?;
|
||||||
|
for col in 0..=15u16 {
|
||||||
|
queue!(
|
||||||
|
w,
|
||||||
|
style::SetForegroundColor(color(col, row)),
|
||||||
|
style::Print("███")
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
queue!(
|
||||||
|
w,
|
||||||
|
style::SetForegroundColor(Color::White),
|
||||||
|
style::Print(format!("{:>width$} ..= ", row * 16, width = 3)),
|
||||||
|
style::Print(format!("{:>width$}", row * 16 + 15, width = 3))
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
w.flush()?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn test_color_ansi_values<W>(w: &mut W) -> std::io::Result<()>
|
||||||
|
where
|
||||||
|
W: Write,
|
||||||
|
{
|
||||||
|
test_color_values_matrix_16x16(w, "Color::Ansi values", |col, row| {
|
||||||
|
Color::AnsiValue((row * 16 + col) as u8)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn test_rgb_red_values<W>(w: &mut W) -> std::io::Result<()>
|
||||||
|
where
|
||||||
|
W: Write,
|
||||||
|
{
|
||||||
|
test_color_values_matrix_16x16(w, "Color::Rgb red values", |col, row| Color::Rgb {
|
||||||
|
r: (row * 16 + col) as u8,
|
||||||
|
g: 0_u8,
|
||||||
|
b: 0,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn test_rgb_green_values<W>(w: &mut W) -> std::io::Result<()>
|
||||||
|
where
|
||||||
|
W: Write,
|
||||||
|
{
|
||||||
|
test_color_values_matrix_16x16(w, "Color::Rgb green values", |col, row| Color::Rgb {
|
||||||
|
r: 0,
|
||||||
|
g: (row * 16 + col) as u8,
|
||||||
|
b: 0,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn test_rgb_blue_values<W>(w: &mut W) -> std::io::Result<()>
|
||||||
|
where
|
||||||
|
W: Write,
|
||||||
|
{
|
||||||
|
test_color_values_matrix_16x16(w, "Color::Rgb blue values", |col, row| Color::Rgb {
|
||||||
|
r: 0,
|
||||||
|
g: 0,
|
||||||
|
b: (row * 16 + col) as u8,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn run<W>(w: &mut W) -> std::io::Result<()>
|
||||||
|
where
|
||||||
|
W: Write,
|
||||||
|
{
|
||||||
|
run_tests!(
|
||||||
|
w,
|
||||||
|
test_set_foreground_color,
|
||||||
|
test_set_background_color,
|
||||||
|
test_color_ansi_values,
|
||||||
|
test_rgb_red_values,
|
||||||
|
test_rgb_green_values,
|
||||||
|
test_rgb_blue_values,
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
222
examples/interactive-demo/src/test/cursor.rs
Normal file
222
examples/interactive-demo/src/test/cursor.rs
Normal file
@ -0,0 +1,222 @@
|
|||||||
|
#![allow(clippy::cognitive_complexity)]
|
||||||
|
|
||||||
|
use std::io::Write;
|
||||||
|
|
||||||
|
use crossterm::{cursor, execute, queue, style, style::Stylize, Command};
|
||||||
|
use std::thread;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
fn test_move_cursor_up<W>(w: &mut W) -> std::io::Result<()>
|
||||||
|
where
|
||||||
|
W: Write,
|
||||||
|
{
|
||||||
|
draw_cursor_box(w, "Move Up (2)", |_, _| cursor::MoveUp(2))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn test_move_cursor_down<W>(w: &mut W) -> std::io::Result<()>
|
||||||
|
where
|
||||||
|
W: Write,
|
||||||
|
{
|
||||||
|
draw_cursor_box(w, "Move Down (2)", |_, _| cursor::MoveDown(2))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn test_move_cursor_left<W>(w: &mut W) -> std::io::Result<()>
|
||||||
|
where
|
||||||
|
W: Write,
|
||||||
|
{
|
||||||
|
draw_cursor_box(w, "Move Left (2)", |_, _| cursor::MoveLeft(2))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn test_move_cursor_right<W>(w: &mut W) -> std::io::Result<()>
|
||||||
|
where
|
||||||
|
W: Write,
|
||||||
|
{
|
||||||
|
draw_cursor_box(w, "Move Right (2)", |_, _| cursor::MoveRight(2))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn test_move_cursor_to_previous_line<W>(w: &mut W) -> std::io::Result<()>
|
||||||
|
where
|
||||||
|
W: Write,
|
||||||
|
{
|
||||||
|
draw_cursor_box(w, "MoveToPreviousLine (1)", |_, _| {
|
||||||
|
cursor::MoveToPreviousLine(1)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn test_move_cursor_to_next_line<W>(w: &mut W) -> std::io::Result<()>
|
||||||
|
where
|
||||||
|
W: Write,
|
||||||
|
{
|
||||||
|
draw_cursor_box(w, "MoveToNextLine (1)", |_, _| cursor::MoveToNextLine(1))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn test_move_cursor_to_column<W>(w: &mut W) -> std::io::Result<()>
|
||||||
|
where
|
||||||
|
W: Write,
|
||||||
|
{
|
||||||
|
draw_cursor_box(w, "MoveToColumn (1)", |center_x, _| {
|
||||||
|
cursor::MoveToColumn(center_x + 1)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn test_hide_cursor<W>(w: &mut W) -> std::io::Result<()>
|
||||||
|
where
|
||||||
|
W: Write,
|
||||||
|
{
|
||||||
|
execute!(w, style::Print("HideCursor"), cursor::Hide)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn test_show_cursor<W>(w: &mut W) -> std::io::Result<()>
|
||||||
|
where
|
||||||
|
W: Write,
|
||||||
|
{
|
||||||
|
execute!(w, style::Print("ShowCursor"), cursor::Show)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn test_cursor_blinking_block<W>(w: &mut W) -> std::io::Result<()>
|
||||||
|
where
|
||||||
|
W: Write,
|
||||||
|
{
|
||||||
|
execute!(
|
||||||
|
w,
|
||||||
|
style::Print("Blinking Block:"),
|
||||||
|
cursor::MoveLeft(2),
|
||||||
|
cursor::SetCursorStyle::BlinkingBlock,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn test_cursor_blinking_underscore<W>(w: &mut W) -> std::io::Result<()>
|
||||||
|
where
|
||||||
|
W: Write,
|
||||||
|
{
|
||||||
|
execute!(
|
||||||
|
w,
|
||||||
|
style::Print("Blinking Underscore:"),
|
||||||
|
cursor::MoveLeft(2),
|
||||||
|
cursor::SetCursorStyle::BlinkingUnderScore,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn test_cursor_blinking_bar<W>(w: &mut W) -> std::io::Result<()>
|
||||||
|
where
|
||||||
|
W: Write,
|
||||||
|
{
|
||||||
|
execute!(
|
||||||
|
w,
|
||||||
|
style::Print("Blinking bar:"),
|
||||||
|
cursor::MoveLeft(2),
|
||||||
|
cursor::SetCursorStyle::BlinkingBar,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn test_move_cursor_to<W>(w: &mut W) -> std::io::Result<()>
|
||||||
|
where
|
||||||
|
W: Write,
|
||||||
|
{
|
||||||
|
draw_cursor_box(
|
||||||
|
w,
|
||||||
|
"MoveTo (x: 1, y: 1) removed from center",
|
||||||
|
|center_x, center_y| cursor::MoveTo(center_x + 1, center_y + 1),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn test_save_restore_cursor_position<W>(w: &mut W) -> std::io::Result<()>
|
||||||
|
where
|
||||||
|
W: Write,
|
||||||
|
{
|
||||||
|
execute!(w,
|
||||||
|
cursor::MoveTo(0, 0),
|
||||||
|
style::Print("Save position, print character elsewhere, after three seconds restore to old position."),
|
||||||
|
cursor::MoveToNextLine(2),
|
||||||
|
style::Print("Save ->[ ]<- Position"),
|
||||||
|
cursor::MoveTo(8, 2),
|
||||||
|
cursor::SavePosition,
|
||||||
|
cursor::MoveTo(10,10),
|
||||||
|
style::Print("Move To ->[√]<- Position")
|
||||||
|
)?;
|
||||||
|
|
||||||
|
thread::sleep(Duration::from_secs(3));
|
||||||
|
|
||||||
|
execute!(w, cursor::RestorePosition, style::Print("√"))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Draws a box with an colored center, this center can be taken as a reference point after running the given cursor command.
|
||||||
|
fn draw_cursor_box<W, F, T>(w: &mut W, description: &str, cursor_command: F) -> std::io::Result<()>
|
||||||
|
where
|
||||||
|
W: Write,
|
||||||
|
F: Fn(u16, u16) -> T,
|
||||||
|
T: Command,
|
||||||
|
{
|
||||||
|
execute!(
|
||||||
|
w,
|
||||||
|
cursor::Hide,
|
||||||
|
cursor::MoveTo(0, 0),
|
||||||
|
style::SetForegroundColor(style::Color::Red),
|
||||||
|
style::Print(format!(
|
||||||
|
"Red box is the center. After the action: '{}' '√' is drawn to reflect the action from the center.",
|
||||||
|
description
|
||||||
|
))
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let start_y = 2;
|
||||||
|
let width = 21;
|
||||||
|
let height = 11 + start_y;
|
||||||
|
let center_x = width / 2;
|
||||||
|
let center_y = (height + start_y) / 2;
|
||||||
|
|
||||||
|
for row in start_y..=10 + start_y {
|
||||||
|
for column in 0..=width {
|
||||||
|
if (row == start_y || row == height - 1) || (column == 0 || column == width) {
|
||||||
|
queue!(
|
||||||
|
w,
|
||||||
|
cursor::MoveTo(column, row),
|
||||||
|
style::PrintStyledContent("▓".red()),
|
||||||
|
)?;
|
||||||
|
} else {
|
||||||
|
queue!(
|
||||||
|
w,
|
||||||
|
cursor::MoveTo(column, row),
|
||||||
|
style::PrintStyledContent("_".red().on_white())
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
queue!(
|
||||||
|
w,
|
||||||
|
cursor::MoveTo(center_x, center_y),
|
||||||
|
style::PrintStyledContent("▀".red().on_white()),
|
||||||
|
cursor::MoveTo(center_x, center_y),
|
||||||
|
)?;
|
||||||
|
queue!(
|
||||||
|
w,
|
||||||
|
cursor_command(center_x, center_y),
|
||||||
|
style::PrintStyledContent("√".magenta().on_white())
|
||||||
|
)?;
|
||||||
|
w.flush()?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn run<W>(w: &mut W) -> std::io::Result<()>
|
||||||
|
where
|
||||||
|
W: Write,
|
||||||
|
{
|
||||||
|
run_tests!(
|
||||||
|
w,
|
||||||
|
test_hide_cursor,
|
||||||
|
test_show_cursor,
|
||||||
|
test_cursor_blinking_bar,
|
||||||
|
test_cursor_blinking_block,
|
||||||
|
test_cursor_blinking_underscore,
|
||||||
|
test_move_cursor_left,
|
||||||
|
test_move_cursor_right,
|
||||||
|
test_move_cursor_up,
|
||||||
|
test_move_cursor_down,
|
||||||
|
test_move_cursor_to,
|
||||||
|
test_move_cursor_to_next_line,
|
||||||
|
test_move_cursor_to_previous_line,
|
||||||
|
test_move_cursor_to_column,
|
||||||
|
test_save_restore_cursor_position
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
42
examples/interactive-demo/src/test/event.rs
Normal file
42
examples/interactive-demo/src/test/event.rs
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
#![allow(clippy::cognitive_complexity)]
|
||||||
|
|
||||||
|
use crossterm::{
|
||||||
|
cursor::position,
|
||||||
|
event::{read, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
|
||||||
|
execute,
|
||||||
|
};
|
||||||
|
use std::io::{self, Write};
|
||||||
|
|
||||||
|
fn test_event<W>(w: &mut W) -> io::Result<()>
|
||||||
|
where
|
||||||
|
W: io::Write,
|
||||||
|
{
|
||||||
|
execute!(w, EnableMouseCapture)?;
|
||||||
|
|
||||||
|
loop {
|
||||||
|
// Blocking read
|
||||||
|
let event = read()?;
|
||||||
|
|
||||||
|
println!("Event::{:?}\r", event);
|
||||||
|
|
||||||
|
if event == Event::Key(KeyCode::Char('c').into()) {
|
||||||
|
println!("Cursor position: {:?}\r", position());
|
||||||
|
}
|
||||||
|
|
||||||
|
if event == Event::Key(KeyCode::Char('q').into()) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
execute!(w, DisableMouseCapture)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn run<W>(w: &mut W) -> std::io::Result<()>
|
||||||
|
where
|
||||||
|
W: Write,
|
||||||
|
{
|
||||||
|
run_tests!(w, test_event);
|
||||||
|
Ok(())
|
||||||
|
}
|
41
examples/interactive-demo/src/test/synchronized_output.rs
Normal file
41
examples/interactive-demo/src/test/synchronized_output.rs
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
use std::io::Write;
|
||||||
|
|
||||||
|
use crossterm::{cursor, execute, style::Print, SynchronizedUpdate};
|
||||||
|
|
||||||
|
fn render_slowly<W>(w: &mut W) -> std::io::Result<()>
|
||||||
|
where
|
||||||
|
W: Write,
|
||||||
|
{
|
||||||
|
for i in 1..10 {
|
||||||
|
execute!(w, Print(format!("{}", i)))?;
|
||||||
|
std::thread::sleep(std::time::Duration::from_millis(50));
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn test_slow_rendering<W>(w: &mut W) -> std::io::Result<()>
|
||||||
|
where
|
||||||
|
W: Write,
|
||||||
|
{
|
||||||
|
execute!(w, Print("Rendering without synchronized update:"))?;
|
||||||
|
execute!(w, cursor::MoveToNextLine(1))?;
|
||||||
|
std::thread::sleep(std::time::Duration::from_millis(50));
|
||||||
|
render_slowly(w)?;
|
||||||
|
|
||||||
|
execute!(w, cursor::MoveToNextLine(1))?;
|
||||||
|
execute!(w, Print("Rendering with synchronized update:"))?;
|
||||||
|
execute!(w, cursor::MoveToNextLine(1))?;
|
||||||
|
std::thread::sleep(std::time::Duration::from_millis(50));
|
||||||
|
w.sync_update(render_slowly)??;
|
||||||
|
|
||||||
|
execute!(w, cursor::MoveToNextLine(1))?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn run<W>(w: &mut W) -> std::io::Result<()>
|
||||||
|
where
|
||||||
|
W: Write,
|
||||||
|
{
|
||||||
|
run_tests!(w, test_slow_rendering,);
|
||||||
|
Ok(())
|
||||||
|
}
|
18
examples/is_tty.rs
Normal file
18
examples/is_tty.rs
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
use crossterm::{
|
||||||
|
execute,
|
||||||
|
terminal::{size, SetSize},
|
||||||
|
tty::IsTty,
|
||||||
|
};
|
||||||
|
use std::io::{stdin, stdout};
|
||||||
|
|
||||||
|
pub fn main() {
|
||||||
|
println!("size: {:?}", size().unwrap());
|
||||||
|
execute!(stdout(), SetSize(10, 10)).unwrap();
|
||||||
|
println!("resized: {:?}", size().unwrap());
|
||||||
|
|
||||||
|
if stdin().is_tty() {
|
||||||
|
println!("Is TTY");
|
||||||
|
} else {
|
||||||
|
println!("Is not TTY");
|
||||||
|
}
|
||||||
|
}
|
49
examples/key-display.rs
Normal file
49
examples/key-display.rs
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
//! Demonstrates the display format of key events.
|
||||||
|
//!
|
||||||
|
//! This example demonstrates the display format of key events, which is useful for displaying in
|
||||||
|
//! the help section of a terminal application.
|
||||||
|
//!
|
||||||
|
//! cargo run --example key-display
|
||||||
|
|
||||||
|
use std::io;
|
||||||
|
|
||||||
|
use crossterm::event::{KeyEventKind, KeyModifiers};
|
||||||
|
use crossterm::{
|
||||||
|
event::{read, Event, KeyCode},
|
||||||
|
terminal::{disable_raw_mode, enable_raw_mode},
|
||||||
|
};
|
||||||
|
|
||||||
|
const HELP: &str = r#"Key display
|
||||||
|
- Press any key to see its display format
|
||||||
|
- Use Esc to quit
|
||||||
|
"#;
|
||||||
|
|
||||||
|
fn main() -> io::Result<()> {
|
||||||
|
println!("{}", HELP);
|
||||||
|
enable_raw_mode()?;
|
||||||
|
if let Err(e) = print_events() {
|
||||||
|
println!("Error: {:?}\r", e);
|
||||||
|
}
|
||||||
|
disable_raw_mode()?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn print_events() -> io::Result<()> {
|
||||||
|
loop {
|
||||||
|
let event = read()?;
|
||||||
|
match event {
|
||||||
|
Event::Key(event) if event.kind == KeyEventKind::Press => {
|
||||||
|
print!("Key pressed: ");
|
||||||
|
if event.modifiers != KeyModifiers::NONE {
|
||||||
|
print!("{}+", event.modifiers);
|
||||||
|
}
|
||||||
|
println!("{}\r", event.code);
|
||||||
|
if event.code == KeyCode::Esc {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
95
examples/stderr.rs
Normal file
95
examples/stderr.rs
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
//! This shows how an application can write on stderr
|
||||||
|
//! instead of stdout, thus making it possible to
|
||||||
|
//! the command API instead of the "old style" direct
|
||||||
|
//! unbuffered API.
|
||||||
|
//!
|
||||||
|
//! This particular example is only suited to Unix
|
||||||
|
//! for now.
|
||||||
|
//!
|
||||||
|
//! cargo run --example stderr
|
||||||
|
|
||||||
|
use std::io;
|
||||||
|
|
||||||
|
use crossterm::{
|
||||||
|
cursor::{Hide, MoveTo, Show},
|
||||||
|
event,
|
||||||
|
event::{Event, KeyCode, KeyEvent},
|
||||||
|
execute, queue,
|
||||||
|
style::Print,
|
||||||
|
terminal::{self, EnterAlternateScreen, LeaveAlternateScreen},
|
||||||
|
};
|
||||||
|
|
||||||
|
const TEXT: &str = r#"
|
||||||
|
This screen is ran on stderr.
|
||||||
|
And when you hit enter, it prints on stdout.
|
||||||
|
This makes it possible to run an application and choose what will
|
||||||
|
be sent to any application calling yours.
|
||||||
|
|
||||||
|
For example, assuming you build this example with
|
||||||
|
|
||||||
|
cargo build --bin stderr
|
||||||
|
|
||||||
|
and then you run it with
|
||||||
|
|
||||||
|
cd "$(target/debug/stderr)"
|
||||||
|
|
||||||
|
what the application prints on stdout is used as argument to cd.
|
||||||
|
|
||||||
|
Try it out.
|
||||||
|
|
||||||
|
Hit any key to quit this screen:
|
||||||
|
|
||||||
|
1 will print `..`
|
||||||
|
2 will print `/`
|
||||||
|
3 will print `~`
|
||||||
|
Any other key will print this text (so that you may copy-paste)
|
||||||
|
"#;
|
||||||
|
|
||||||
|
fn run_app<W>(write: &mut W) -> io::Result<char>
|
||||||
|
where
|
||||||
|
W: io::Write,
|
||||||
|
{
|
||||||
|
queue!(
|
||||||
|
write,
|
||||||
|
EnterAlternateScreen, // enter alternate screen
|
||||||
|
Hide // hide the cursor
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let mut y = 1;
|
||||||
|
for line in TEXT.split('\n') {
|
||||||
|
queue!(write, MoveTo(1, y), Print(line.to_string()))?;
|
||||||
|
y += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
write.flush()?;
|
||||||
|
|
||||||
|
terminal::enable_raw_mode()?;
|
||||||
|
let user_char = read_char()?; // we wait for the user to hit a key
|
||||||
|
execute!(write, Show, LeaveAlternateScreen)?; // restore the cursor and leave the alternate screen
|
||||||
|
|
||||||
|
terminal::disable_raw_mode()?;
|
||||||
|
|
||||||
|
Ok(user_char)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn read_char() -> io::Result<char> {
|
||||||
|
loop {
|
||||||
|
if let Event::Key(KeyEvent {
|
||||||
|
code: KeyCode::Char(c),
|
||||||
|
..
|
||||||
|
}) = event::read()?
|
||||||
|
{
|
||||||
|
return Ok(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// cargo run --example stderr
|
||||||
|
fn main() {
|
||||||
|
match run_app(&mut io::stderr()).unwrap() {
|
||||||
|
'1' => print!(".."),
|
||||||
|
'2' => print!("/"),
|
||||||
|
'3' => print!("~"),
|
||||||
|
_ => println!("{}", TEXT),
|
||||||
|
}
|
||||||
|
}
|
@ -46,6 +46,11 @@ use std::fmt;
|
|||||||
|
|
||||||
use crate::{csi, impl_display, Command};
|
use crate::{csi, impl_display, Command};
|
||||||
|
|
||||||
|
pub(crate) mod sys;
|
||||||
|
|
||||||
|
#[cfg(feature = "events")]
|
||||||
|
pub use sys::position;
|
||||||
|
|
||||||
/// A command that moves the terminal cursor to the given position (column, row).
|
/// A command that moves the terminal cursor to the given position (column, row).
|
||||||
///
|
///
|
||||||
/// # Notes
|
/// # Notes
|
||||||
@ -365,7 +370,7 @@ impl Command for DisableBlinking {
|
|||||||
/// # Note
|
/// # Note
|
||||||
///
|
///
|
||||||
/// - Commands must be executed/queued for execution otherwise they do nothing.
|
/// - Commands must be executed/queued for execution otherwise they do nothing.
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
#[derive(Clone, Copy)]
|
||||||
pub enum SetCursorStyle {
|
pub enum SetCursorStyle {
|
||||||
/// Default cursor shape configured by the user.
|
/// Default cursor shape configured by the user.
|
||||||
DefaultUserShape,
|
DefaultUserShape,
|
||||||
|
20
src/cursor/sys.rs
Normal file
20
src/cursor/sys.rs
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
//! This module provides platform related functions.
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
#[cfg(feature = "events")]
|
||||||
|
pub use self::unix::position;
|
||||||
|
#[cfg(windows)]
|
||||||
|
#[cfg(feature = "events")]
|
||||||
|
pub use self::windows::position;
|
||||||
|
#[cfg(windows)]
|
||||||
|
pub(crate) use self::windows::{
|
||||||
|
move_down, move_left, move_right, move_to, move_to_column, move_to_next_line,
|
||||||
|
move_to_previous_line, move_to_row, move_up, restore_position, save_position, show_cursor,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
|
pub(crate) mod windows;
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
#[cfg(feature = "events")]
|
||||||
|
pub(crate) mod unix;
|
56
src/cursor/sys/unix.rs
Normal file
56
src/cursor/sys/unix.rs
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
use std::{
|
||||||
|
io::{self, Error, ErrorKind, Write},
|
||||||
|
time::Duration,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
event::{filter::CursorPositionFilter, poll_internal, read_internal, InternalEvent},
|
||||||
|
terminal::{disable_raw_mode, enable_raw_mode, sys::is_raw_mode_enabled},
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Returns the cursor position (column, row).
|
||||||
|
///
|
||||||
|
/// The top left cell is represented as `(0, 0)`.
|
||||||
|
///
|
||||||
|
/// On unix systems, this function will block and possibly time out while
|
||||||
|
/// [`crossterm::event::read`](crate::event::read) or [`crossterm::event::poll`](crate::event::poll) are being called.
|
||||||
|
pub fn position() -> io::Result<(u16, u16)> {
|
||||||
|
if is_raw_mode_enabled() {
|
||||||
|
read_position_raw()
|
||||||
|
} else {
|
||||||
|
read_position()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_position() -> io::Result<(u16, u16)> {
|
||||||
|
enable_raw_mode()?;
|
||||||
|
let pos = read_position_raw();
|
||||||
|
disable_raw_mode()?;
|
||||||
|
pos
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_position_raw() -> io::Result<(u16, u16)> {
|
||||||
|
// Use `ESC [ 6 n` to and retrieve the cursor position.
|
||||||
|
let mut stdout = io::stdout();
|
||||||
|
stdout.write_all(b"\x1B[6n")?;
|
||||||
|
stdout.flush()?;
|
||||||
|
|
||||||
|
loop {
|
||||||
|
match poll_internal(Some(Duration::from_millis(2000)), &CursorPositionFilter) {
|
||||||
|
Ok(true) => {
|
||||||
|
if let Ok(InternalEvent::CursorPosition(x, y)) =
|
||||||
|
read_internal(&CursorPositionFilter)
|
||||||
|
{
|
||||||
|
return Ok((x, y));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(false) => {
|
||||||
|
return Err(Error::new(
|
||||||
|
ErrorKind::Other,
|
||||||
|
"The cursor position could not be read within a normal duration",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Err(_) => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
341
src/cursor/sys/windows.rs
Normal file
341
src/cursor/sys/windows.rs
Normal file
@ -0,0 +1,341 @@
|
|||||||
|
//! WinAPI related logic to cursor manipulation.
|
||||||
|
|
||||||
|
use std::convert::TryFrom;
|
||||||
|
use std::io;
|
||||||
|
use std::sync::atomic::{AtomicU64, Ordering};
|
||||||
|
|
||||||
|
use crossterm_winapi::{result, Coord, Handle, HandleType, ScreenBuffer};
|
||||||
|
use winapi::{
|
||||||
|
shared::minwindef::{FALSE, TRUE},
|
||||||
|
um::wincon::{SetConsoleCursorInfo, SetConsoleCursorPosition, CONSOLE_CURSOR_INFO, COORD},
|
||||||
|
};
|
||||||
|
|
||||||
|
/// The position of the cursor, written when you save the cursor's position.
|
||||||
|
///
|
||||||
|
/// This is `u64::MAX` initially. Otherwise, it stores the cursor's x position bit-shifted left 16
|
||||||
|
/// times or-ed with the cursor's y position, where both are `i16`s.
|
||||||
|
static SAVED_CURSOR_POS: AtomicU64 = AtomicU64::new(u64::MAX);
|
||||||
|
|
||||||
|
// The 'y' position of the cursor is not relative to the window but absolute to screen buffer.
|
||||||
|
// We can calculate the relative cursor position by subtracting the top position of the terminal window from the y position.
|
||||||
|
// This results in an 1-based coord zo subtract 1 to make cursor position 0-based.
|
||||||
|
pub fn parse_relative_y(y: i16) -> std::io::Result<i16> {
|
||||||
|
let window = ScreenBuffer::current()?.info()?;
|
||||||
|
|
||||||
|
let window_size = window.terminal_window();
|
||||||
|
let screen_size = window.terminal_size();
|
||||||
|
|
||||||
|
if y <= screen_size.height {
|
||||||
|
Ok(y)
|
||||||
|
} else {
|
||||||
|
Ok(y - window_size.top)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the cursor position (column, row).
|
||||||
|
///
|
||||||
|
/// The top left cell is represented `0,0`.
|
||||||
|
pub fn position() -> io::Result<(u16, u16)> {
|
||||||
|
let cursor = ScreenBufferCursor::output()?;
|
||||||
|
let mut position = cursor.position()?;
|
||||||
|
// if position.y != 0 {
|
||||||
|
position.y = parse_relative_y(position.y)?;
|
||||||
|
// }
|
||||||
|
Ok(position.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn show_cursor(show_cursor: bool) -> std::io::Result<()> {
|
||||||
|
ScreenBufferCursor::from(Handle::current_out_handle()?).set_visibility(show_cursor)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn move_to(column: u16, row: u16) -> std::io::Result<()> {
|
||||||
|
let cursor = ScreenBufferCursor::output()?;
|
||||||
|
cursor.move_to(column as i16, row as i16)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn move_up(count: u16) -> std::io::Result<()> {
|
||||||
|
let (column, row) = position()?;
|
||||||
|
move_to(column, row - count)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn move_right(count: u16) -> std::io::Result<()> {
|
||||||
|
let (column, row) = position()?;
|
||||||
|
move_to(column + count, row)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn move_down(count: u16) -> std::io::Result<()> {
|
||||||
|
let (column, row) = position()?;
|
||||||
|
move_to(column, row + count)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn move_left(count: u16) -> std::io::Result<()> {
|
||||||
|
let (column, row) = position()?;
|
||||||
|
move_to(column - count, row)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn move_to_column(new_column: u16) -> std::io::Result<()> {
|
||||||
|
let (_, row) = position()?;
|
||||||
|
move_to(new_column, row)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn move_to_row(new_row: u16) -> std::io::Result<()> {
|
||||||
|
let (col, _) = position()?;
|
||||||
|
move_to(col, new_row)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn move_to_next_line(count: u16) -> std::io::Result<()> {
|
||||||
|
let (_, row) = position()?;
|
||||||
|
move_to(0, row + count)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn move_to_previous_line(count: u16) -> std::io::Result<()> {
|
||||||
|
let (_, row) = position()?;
|
||||||
|
move_to(0, row - count)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn save_position() -> std::io::Result<()> {
|
||||||
|
ScreenBufferCursor::output()?.save_position()?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn restore_position() -> std::io::Result<()> {
|
||||||
|
ScreenBufferCursor::output()?.restore_position()?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// WinAPI wrapper over terminal cursor behaviour.
|
||||||
|
struct ScreenBufferCursor {
|
||||||
|
screen_buffer: ScreenBuffer,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ScreenBufferCursor {
|
||||||
|
fn output() -> std::io::Result<ScreenBufferCursor> {
|
||||||
|
Ok(ScreenBufferCursor {
|
||||||
|
screen_buffer: ScreenBuffer::from(Handle::new(HandleType::CurrentOutputHandle)?),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn position(&self) -> std::io::Result<Coord> {
|
||||||
|
Ok(self.screen_buffer.info()?.cursor_pos())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn move_to(&self, x: i16, y: i16) -> std::io::Result<()> {
|
||||||
|
if x < 0 {
|
||||||
|
return Err(io::Error::new(
|
||||||
|
io::ErrorKind::Other,
|
||||||
|
format!("Argument Out of Range Exception when setting cursor position to X: {x}"),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
if y < 0 {
|
||||||
|
return Err(io::Error::new(
|
||||||
|
io::ErrorKind::Other,
|
||||||
|
format!("Argument Out of Range Exception when setting cursor position to Y: {y}"),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let position = COORD { X: x, Y: y };
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
if result(SetConsoleCursorPosition(
|
||||||
|
**self.screen_buffer.handle(),
|
||||||
|
position,
|
||||||
|
))
|
||||||
|
.is_err()
|
||||||
|
{
|
||||||
|
return Err(io::Error::last_os_error());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_visibility(&self, visible: bool) -> std::io::Result<()> {
|
||||||
|
let cursor_info = CONSOLE_CURSOR_INFO {
|
||||||
|
dwSize: 100,
|
||||||
|
bVisible: if visible { TRUE } else { FALSE },
|
||||||
|
};
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
if result(SetConsoleCursorInfo(
|
||||||
|
**self.screen_buffer.handle(),
|
||||||
|
&cursor_info,
|
||||||
|
))
|
||||||
|
.is_err()
|
||||||
|
{
|
||||||
|
return Err(io::Error::last_os_error());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn restore_position(&self) -> std::io::Result<()> {
|
||||||
|
if let Ok(val) = u32::try_from(SAVED_CURSOR_POS.load(Ordering::Relaxed)) {
|
||||||
|
let x = (val >> 16) as i16;
|
||||||
|
let y = val as i16;
|
||||||
|
self.move_to(x, y)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn save_position(&self) -> std::io::Result<()> {
|
||||||
|
let position = self.position()?;
|
||||||
|
|
||||||
|
let bits = u64::from(u32::from(position.x as u16) << 16 | u32::from(position.y as u16));
|
||||||
|
SAVED_CURSOR_POS.store(bits, Ordering::Relaxed);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Handle> for ScreenBufferCursor {
|
||||||
|
fn from(handle: Handle) -> Self {
|
||||||
|
ScreenBufferCursor {
|
||||||
|
screen_buffer: ScreenBuffer::from(handle),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::{
|
||||||
|
move_down, move_left, move_right, move_to, move_to_column, move_to_next_line,
|
||||||
|
move_to_previous_line, move_to_row, move_up, position, restore_position, save_position,
|
||||||
|
};
|
||||||
|
use crate::terminal::sys::temp_screen_buffer;
|
||||||
|
use serial_test::serial;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[serial]
|
||||||
|
fn test_move_to_winapi() {
|
||||||
|
let _test_screen = temp_screen_buffer().unwrap();
|
||||||
|
|
||||||
|
let (saved_x, saved_y) = position().unwrap();
|
||||||
|
|
||||||
|
move_to(saved_x + 1, saved_y + 1).unwrap();
|
||||||
|
assert_eq!(position().unwrap(), (saved_x + 1, saved_y + 1));
|
||||||
|
|
||||||
|
move_to(saved_x, saved_y).unwrap();
|
||||||
|
assert_eq!(position().unwrap(), (saved_x, saved_y));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[serial]
|
||||||
|
fn test_move_right_winapi() {
|
||||||
|
let _test_screen = temp_screen_buffer().unwrap();
|
||||||
|
|
||||||
|
let (saved_x, saved_y) = position().unwrap();
|
||||||
|
move_right(1).unwrap();
|
||||||
|
assert_eq!(position().unwrap(), (saved_x + 1, saved_y));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[serial]
|
||||||
|
fn test_move_left_winapi() {
|
||||||
|
let _test_screen = temp_screen_buffer().unwrap();
|
||||||
|
|
||||||
|
move_to(2, 0).unwrap();
|
||||||
|
|
||||||
|
move_left(2).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(position().unwrap(), (0, 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[serial]
|
||||||
|
fn test_move_up_winapi() {
|
||||||
|
let _test_screen = temp_screen_buffer().unwrap();
|
||||||
|
|
||||||
|
move_to(0, 2).unwrap();
|
||||||
|
|
||||||
|
move_up(2).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(position().unwrap(), (0, 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[serial]
|
||||||
|
fn test_move_to_next_line_winapi() {
|
||||||
|
let _test_screen = temp_screen_buffer().unwrap();
|
||||||
|
|
||||||
|
move_to(0, 2).unwrap();
|
||||||
|
|
||||||
|
move_to_next_line(2).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(position().unwrap(), (0, 4));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[serial]
|
||||||
|
fn test_move_to_previous_line_winapi() {
|
||||||
|
let _test_screen = temp_screen_buffer().unwrap();
|
||||||
|
|
||||||
|
move_to(0, 2).unwrap();
|
||||||
|
|
||||||
|
move_to_previous_line(2).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(position().unwrap(), (0, 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[serial]
|
||||||
|
fn test_move_to_column_winapi() {
|
||||||
|
let _test_screen = temp_screen_buffer().unwrap();
|
||||||
|
|
||||||
|
move_to(0, 2).unwrap();
|
||||||
|
|
||||||
|
move_to_column(12).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(position().unwrap(), (12, 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[serial]
|
||||||
|
fn test_move_to_row_winapi() {
|
||||||
|
let _test_screen = temp_screen_buffer().unwrap();
|
||||||
|
|
||||||
|
move_to(0, 2).unwrap();
|
||||||
|
|
||||||
|
move_to_row(5).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(position().unwrap(), (0, 5));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[serial]
|
||||||
|
fn test_move_down_winapi() {
|
||||||
|
let _test_screen = temp_screen_buffer().unwrap();
|
||||||
|
|
||||||
|
move_to(0, 0).unwrap();
|
||||||
|
|
||||||
|
move_down(2).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(position().unwrap(), (0, 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[serial]
|
||||||
|
fn test_save_restore_position_winapi() {
|
||||||
|
let _test_screen = temp_screen_buffer().unwrap();
|
||||||
|
|
||||||
|
let (saved_x, saved_y) = position().unwrap();
|
||||||
|
|
||||||
|
save_position().unwrap();
|
||||||
|
move_to(saved_x + 1, saved_y + 1).unwrap();
|
||||||
|
restore_position().unwrap();
|
||||||
|
|
||||||
|
let (x, y) = position().unwrap();
|
||||||
|
|
||||||
|
assert_eq!(x, saved_x);
|
||||||
|
assert_eq!(y, saved_y);
|
||||||
|
}
|
||||||
|
}
|
397
src/event.rs
397
src/event.rs
@ -17,33 +17,20 @@
|
|||||||
//!
|
//!
|
||||||
//! **Make sure to enable [raw mode](../terminal/index.html#raw-mode) in order for keyboard events to work properly**
|
//! **Make sure to enable [raw mode](../terminal/index.html#raw-mode) in order for keyboard events to work properly**
|
||||||
//!
|
//!
|
||||||
//! ## Mouse and Focus Events
|
//! ## Mouse Events
|
||||||
//!
|
//!
|
||||||
//! Mouse and focus events are not enabled by default. You have to enable them with the
|
//! Mouse events are not enabled by default. You have to enable them with the
|
||||||
//! [`EnableMouseCapture`](struct.EnableMouseCapture.html) / [`EnableFocusChange`](struct.EnableFocusChange.html) command.
|
//! [`EnableMouseCapture`](struct.EnableMouseCapture.html) command. See [Command API](../index.html#command-api)
|
||||||
//! See [Command API](../index.html#command-api) for more information.
|
//! for more information.
|
||||||
//!
|
//!
|
||||||
//! ## Examples
|
//! ## Examples
|
||||||
//!
|
//!
|
||||||
//! Blocking read:
|
//! Blocking read:
|
||||||
//!
|
//!
|
||||||
//! ```no_run
|
//! ```no_run
|
||||||
//! #![cfg(feature = "bracketed-paste")]
|
//! use crossterm::event::{read, Event};
|
||||||
//! use crossterm::{
|
|
||||||
//! event::{
|
|
||||||
//! read, DisableBracketedPaste, DisableFocusChange, DisableMouseCapture, EnableBracketedPaste,
|
|
||||||
//! EnableFocusChange, EnableMouseCapture, Event,
|
|
||||||
//! },
|
|
||||||
//! execute,
|
|
||||||
//! };
|
|
||||||
//!
|
//!
|
||||||
//! fn print_events() -> std::io::Result<()> {
|
//! fn print_events() -> std::io::Result<()> {
|
||||||
//! execute!(
|
|
||||||
//! std::io::stdout(),
|
|
||||||
//! EnableBracketedPaste,
|
|
||||||
//! EnableFocusChange,
|
|
||||||
//! EnableMouseCapture
|
|
||||||
//! )?;
|
|
||||||
//! loop {
|
//! loop {
|
||||||
//! // `read()` blocks until an `Event` is available
|
//! // `read()` blocks until an `Event` is available
|
||||||
//! match read()? {
|
//! match read()? {
|
||||||
@ -56,12 +43,6 @@
|
|||||||
//! Event::Resize(width, height) => println!("New size {}x{}", width, height),
|
//! Event::Resize(width, height) => println!("New size {}x{}", width, height),
|
||||||
//! }
|
//! }
|
||||||
//! }
|
//! }
|
||||||
//! execute!(
|
|
||||||
//! std::io::stdout(),
|
|
||||||
//! DisableBracketedPaste,
|
|
||||||
//! DisableFocusChange,
|
|
||||||
//! DisableMouseCapture
|
|
||||||
//! )?;
|
|
||||||
//! Ok(())
|
//! Ok(())
|
||||||
//! }
|
//! }
|
||||||
//! ```
|
//! ```
|
||||||
@ -69,24 +50,11 @@
|
|||||||
//! Non-blocking read:
|
//! Non-blocking read:
|
||||||
//!
|
//!
|
||||||
//! ```no_run
|
//! ```no_run
|
||||||
//! #![cfg(feature = "bracketed-paste")]
|
|
||||||
//! use std::{time::Duration, io};
|
//! use std::{time::Duration, io};
|
||||||
//!
|
//!
|
||||||
//! use crossterm::{
|
//! use crossterm::event::{poll, read, Event};
|
||||||
//! event::{
|
|
||||||
//! poll, read, DisableBracketedPaste, DisableFocusChange, DisableMouseCapture,
|
|
||||||
//! EnableBracketedPaste, EnableFocusChange, EnableMouseCapture, Event,
|
|
||||||
//! },
|
|
||||||
//! execute,
|
|
||||||
//! };
|
|
||||||
//!
|
//!
|
||||||
//! fn print_events() -> io::Result<()> {
|
//! fn print_events() -> io::Result<()> {
|
||||||
//! execute!(
|
|
||||||
//! std::io::stdout(),
|
|
||||||
//! EnableBracketedPaste,
|
|
||||||
//! EnableFocusChange,
|
|
||||||
//! EnableMouseCapture
|
|
||||||
//! )?;
|
|
||||||
//! loop {
|
//! loop {
|
||||||
//! // `poll()` waits for an `Event` for a given time period
|
//! // `poll()` waits for an `Event` for a given time period
|
||||||
//! if poll(Duration::from_millis(500))? {
|
//! if poll(Duration::from_millis(500))? {
|
||||||
@ -105,12 +73,6 @@
|
|||||||
//! // Timeout expired and no `Event` is available
|
//! // Timeout expired and no `Event` is available
|
||||||
//! }
|
//! }
|
||||||
//! }
|
//! }
|
||||||
//! execute!(
|
|
||||||
//! std::io::stdout(),
|
|
||||||
//! DisableBracketedPaste,
|
|
||||||
//! DisableFocusChange,
|
|
||||||
//! DisableMouseCapture
|
|
||||||
//! )?;
|
|
||||||
//! Ok(())
|
//! Ok(())
|
||||||
//! }
|
//! }
|
||||||
//! ```
|
//! ```
|
||||||
@ -119,18 +81,165 @@
|
|||||||
//! them (`event-*`).
|
//! them (`event-*`).
|
||||||
|
|
||||||
pub(crate) mod filter;
|
pub(crate) mod filter;
|
||||||
|
pub(crate) mod read;
|
||||||
|
pub(crate) mod source;
|
||||||
|
#[cfg(feature = "event-stream")]
|
||||||
|
pub(crate) mod stream;
|
||||||
pub(crate) mod sys;
|
pub(crate) mod sys;
|
||||||
|
pub(crate) mod timeout;
|
||||||
|
|
||||||
use crate::{csi, Command};
|
#[cfg(feature = "event-stream")]
|
||||||
use std::{
|
pub use stream::EventStream;
|
||||||
collections::VecDeque,
|
|
||||||
fmt::{self, Display},
|
use crate::event::{
|
||||||
|
filter::{EventFilter, Filter},
|
||||||
|
read::InternalEventReader,
|
||||||
|
timeout::PollTimeout,
|
||||||
};
|
};
|
||||||
|
use crate::{csi, Command};
|
||||||
|
use parking_lot::{MappedMutexGuard, Mutex, MutexGuard};
|
||||||
|
use std::fmt::{self, Display};
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
use bitflags::bitflags;
|
use bitflags::bitflags;
|
||||||
use std::hash::{Hash, Hasher};
|
use std::hash::{Hash, Hasher};
|
||||||
|
|
||||||
use self::sys::unix::parse::parse_event;
|
/// Static instance of `InternalEventReader`.
|
||||||
|
/// This needs to be static because there can be one event reader.
|
||||||
|
static INTERNAL_EVENT_READER: Mutex<Option<InternalEventReader>> = parking_lot::const_mutex(None);
|
||||||
|
|
||||||
|
pub(crate) fn lock_internal_event_reader() -> MappedMutexGuard<'static, InternalEventReader> {
|
||||||
|
MutexGuard::map(INTERNAL_EVENT_READER.lock(), |reader| {
|
||||||
|
reader.get_or_insert_with(InternalEventReader::default)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
fn try_lock_internal_event_reader_for(
|
||||||
|
duration: Duration,
|
||||||
|
) -> Option<MappedMutexGuard<'static, InternalEventReader>> {
|
||||||
|
Some(MutexGuard::map(
|
||||||
|
INTERNAL_EVENT_READER.try_lock_for(duration)?,
|
||||||
|
|reader| reader.get_or_insert_with(InternalEventReader::default),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Checks if there is an [`Event`](enum.Event.html) available.
|
||||||
|
///
|
||||||
|
/// Returns `Ok(true)` if an [`Event`](enum.Event.html) is available otherwise it returns `Ok(false)`.
|
||||||
|
///
|
||||||
|
/// `Ok(true)` guarantees that subsequent call to the [`read`](fn.read.html) function
|
||||||
|
/// won't block.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// * `timeout` - maximum waiting time for event availability
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
///
|
||||||
|
/// Return immediately:
|
||||||
|
///
|
||||||
|
/// ```no_run
|
||||||
|
/// use std::{time::Duration, io};
|
||||||
|
/// use crossterm::{event::poll};
|
||||||
|
///
|
||||||
|
/// fn is_event_available() -> io::Result<bool> {
|
||||||
|
/// // Zero duration says that the `poll` function must return immediately
|
||||||
|
/// // with an `Event` availability information
|
||||||
|
/// poll(Duration::from_secs(0))
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// Wait up to 100ms:
|
||||||
|
///
|
||||||
|
/// ```no_run
|
||||||
|
/// use std::{time::Duration, io};
|
||||||
|
///
|
||||||
|
/// use crossterm::event::poll;
|
||||||
|
///
|
||||||
|
/// fn is_event_available() -> io::Result<bool> {
|
||||||
|
/// // Wait for an `Event` availability for 100ms. It returns immediately
|
||||||
|
/// // if an `Event` is/becomes available.
|
||||||
|
/// poll(Duration::from_millis(100))
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
pub fn poll(timeout: Duration) -> std::io::Result<bool> {
|
||||||
|
poll_internal(Some(timeout), &EventFilter)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reads a single [`Event`](enum.Event.html).
|
||||||
|
///
|
||||||
|
/// This function blocks until an [`Event`](enum.Event.html) is available. Combine it with the
|
||||||
|
/// [`poll`](fn.poll.html) function to get non-blocking reads.
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
///
|
||||||
|
/// Blocking read:
|
||||||
|
///
|
||||||
|
/// ```no_run
|
||||||
|
/// use crossterm::event::read;
|
||||||
|
/// use std::io;
|
||||||
|
///
|
||||||
|
/// fn print_events() -> io::Result<bool> {
|
||||||
|
/// loop {
|
||||||
|
/// // Blocks until an `Event` is available
|
||||||
|
/// println!("{:?}", read()?);
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// Non-blocking read:
|
||||||
|
///
|
||||||
|
/// ```no_run
|
||||||
|
/// use std::time::Duration;
|
||||||
|
/// use std::io;
|
||||||
|
///
|
||||||
|
/// use crossterm::event::{read, poll};
|
||||||
|
///
|
||||||
|
/// fn print_events() -> io::Result<bool> {
|
||||||
|
/// loop {
|
||||||
|
/// if poll(Duration::from_millis(100))? {
|
||||||
|
/// // It's guaranteed that `read` won't block, because `poll` returned
|
||||||
|
/// // `Ok(true)`.
|
||||||
|
/// println!("{:?}", read()?);
|
||||||
|
/// } else {
|
||||||
|
/// // Timeout expired, no `Event` is available
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
pub fn read() -> std::io::Result<Event> {
|
||||||
|
match read_internal(&EventFilter)? {
|
||||||
|
InternalEvent::Event(event) => Ok(event),
|
||||||
|
#[cfg(unix)]
|
||||||
|
_ => unreachable!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Polls to check if there are any `InternalEvent`s that can be read within the given duration.
|
||||||
|
pub(crate) fn poll_internal<F>(timeout: Option<Duration>, filter: &F) -> std::io::Result<bool>
|
||||||
|
where
|
||||||
|
F: Filter,
|
||||||
|
{
|
||||||
|
let (mut reader, timeout) = if let Some(timeout) = timeout {
|
||||||
|
let poll_timeout = PollTimeout::new(Some(timeout));
|
||||||
|
if let Some(reader) = try_lock_internal_event_reader_for(timeout) {
|
||||||
|
(reader, poll_timeout.leftover())
|
||||||
|
} else {
|
||||||
|
return Ok(false);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
(lock_internal_event_reader(), None)
|
||||||
|
};
|
||||||
|
reader.poll(timeout, filter)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reads a single `InternalEvent`.
|
||||||
|
pub(crate) fn read_internal<F>(filter: &F) -> std::io::Result<InternalEvent>
|
||||||
|
where
|
||||||
|
F: Filter,
|
||||||
|
{
|
||||||
|
let mut reader = lock_internal_event_reader();
|
||||||
|
reader.read(filter)
|
||||||
|
}
|
||||||
|
|
||||||
bitflags! {
|
bitflags! {
|
||||||
/// Represents special flags that tell compatible terminals to add extra information to keyboard events.
|
/// Represents special flags that tell compatible terminals to add extra information to keyboard events.
|
||||||
@ -184,6 +293,16 @@ impl Command for EnableMouseCapture {
|
|||||||
csi!("?1006h"),
|
csi!("?1006h"),
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
|
fn execute_winapi(&self) -> std::io::Result<()> {
|
||||||
|
sys::windows::enable_mouse_capture()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
|
fn is_ansi_code_supported(&self) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A command that disables mouse event capturing.
|
/// A command that disables mouse event capturing.
|
||||||
@ -203,6 +322,16 @@ impl Command for DisableMouseCapture {
|
|||||||
csi!("?1000l"),
|
csi!("?1000l"),
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
|
fn execute_winapi(&self) -> std::io::Result<()> {
|
||||||
|
sys::windows::disable_mouse_capture()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
|
fn is_ansi_code_supported(&self) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A command that enables focus event emission.
|
/// A command that enables focus event emission.
|
||||||
@ -217,6 +346,12 @@ impl Command for EnableFocusChange {
|
|||||||
fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result {
|
fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result {
|
||||||
f.write_str(csi!("?1004h"))
|
f.write_str(csi!("?1004h"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
|
fn execute_winapi(&self) -> std::io::Result<()> {
|
||||||
|
// Focus events are always enabled on Windows
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A command that disables focus event emission.
|
/// A command that disables focus event emission.
|
||||||
@ -227,6 +362,12 @@ impl Command for DisableFocusChange {
|
|||||||
fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result {
|
fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result {
|
||||||
f.write_str(csi!("?1004l"))
|
f.write_str(csi!("?1004l"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
|
fn execute_winapi(&self) -> std::io::Result<()> {
|
||||||
|
// Focus events can't be disabled on Windows
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A command that enables [bracketed paste mode](https://en.wikipedia.org/wiki/Bracketed-paste).
|
/// A command that enables [bracketed paste mode](https://en.wikipedia.org/wiki/Bracketed-paste).
|
||||||
@ -244,6 +385,14 @@ impl Command for EnableBracketedPaste {
|
|||||||
fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result {
|
fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result {
|
||||||
f.write_str(csi!("?2004h"))
|
f.write_str(csi!("?2004h"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
|
fn execute_winapi(&self) -> std::io::Result<()> {
|
||||||
|
Err(std::io::Error::new(
|
||||||
|
std::io::ErrorKind::Unsupported,
|
||||||
|
"Bracketed paste not implemented in the legacy Windows API.",
|
||||||
|
))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A command that disables bracketed paste mode.
|
/// A command that disables bracketed paste mode.
|
||||||
@ -256,6 +405,11 @@ impl Command for DisableBracketedPaste {
|
|||||||
fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result {
|
fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result {
|
||||||
f.write_str(csi!("?2004l"))
|
f.write_str(csi!("?2004l"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
|
fn execute_winapi(&self) -> std::io::Result<()> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A command that enables the [kitty keyboard protocol](https://sw.kovidgoyal.net/kitty/keyboard-protocol/), which adds extra information to keyboard events and removes ambiguity for modifier keys.
|
/// A command that enables the [kitty keyboard protocol](https://sw.kovidgoyal.net/kitty/keyboard-protocol/), which adds extra information to keyboard events and removes ambiguity for modifier keys.
|
||||||
@ -302,6 +456,21 @@ impl Command for PushKeyboardEnhancementFlags {
|
|||||||
fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result {
|
fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result {
|
||||||
write!(f, "{}{}u", csi!(">"), self.0.bits())
|
write!(f, "{}{}u", csi!(">"), self.0.bits())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
|
fn execute_winapi(&self) -> std::io::Result<()> {
|
||||||
|
use std::io;
|
||||||
|
|
||||||
|
Err(io::Error::new(
|
||||||
|
io::ErrorKind::Unsupported,
|
||||||
|
"Keyboard progressive enhancement not implemented for the legacy Windows API.",
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
|
fn is_ansi_code_supported(&self) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A command that disables extra kinds of keyboard events.
|
/// A command that disables extra kinds of keyboard events.
|
||||||
@ -316,6 +485,21 @@ impl Command for PopKeyboardEnhancementFlags {
|
|||||||
fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result {
|
fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result {
|
||||||
f.write_str(csi!("<1u"))
|
f.write_str(csi!("<1u"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
|
fn execute_winapi(&self) -> std::io::Result<()> {
|
||||||
|
use std::io;
|
||||||
|
|
||||||
|
Err(io::Error::new(
|
||||||
|
io::ErrorKind::Unsupported,
|
||||||
|
"Keyboard progressive enhancement not implemented for the legacy Windows API.",
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
|
fn is_ansi_code_supported(&self) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Represents an event.
|
/// Represents an event.
|
||||||
@ -338,12 +522,6 @@ pub enum Event {
|
|||||||
/// An resize event with new dimensions after resize (columns, rows).
|
/// An resize event with new dimensions after resize (columns, rows).
|
||||||
/// **Note** that resize events can occur in batches.
|
/// **Note** that resize events can occur in batches.
|
||||||
Resize(u16, u16),
|
Resize(u16, u16),
|
||||||
/// A cursor position (`col`, `row`).
|
|
||||||
CursorPosition(u16, u16),
|
|
||||||
/// The progressive keyboard enhancement flags enabled by the terminal.
|
|
||||||
KeyboardEnhancementFlags(KeyboardEnhancementFlags),
|
|
||||||
/// Attributes and architectural class of the terminal.
|
|
||||||
PrimaryDeviceAttributes,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Represents a mouse event.
|
/// Represents a mouse event.
|
||||||
@ -461,8 +639,19 @@ impl Display for KeyModifiers {
|
|||||||
}
|
}
|
||||||
match modifier {
|
match modifier {
|
||||||
KeyModifiers::SHIFT => f.write_str("Shift")?,
|
KeyModifiers::SHIFT => f.write_str("Shift")?,
|
||||||
|
#[cfg(unix)]
|
||||||
KeyModifiers::CONTROL => f.write_str("Control")?,
|
KeyModifiers::CONTROL => f.write_str("Control")?,
|
||||||
|
#[cfg(windows)]
|
||||||
|
KeyModifiers::CONTROL => f.write_str("Ctrl")?,
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
KeyModifiers::ALT => f.write_str("Option")?,
|
||||||
|
#[cfg(not(target_os = "macos"))]
|
||||||
KeyModifiers::ALT => f.write_str("Alt")?,
|
KeyModifiers::ALT => f.write_str("Alt")?,
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
KeyModifiers::SUPER => f.write_str("Command")?,
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
KeyModifiers::SUPER => f.write_str("Windows")?,
|
||||||
|
#[cfg(not(any(target_os = "macos", target_os = "windows")))]
|
||||||
KeyModifiers::SUPER => f.write_str("Super")?,
|
KeyModifiers::SUPER => f.write_str("Super")?,
|
||||||
KeyModifiers::HYPER => f.write_str("Hyper")?,
|
KeyModifiers::HYPER => f.write_str("Hyper")?,
|
||||||
KeyModifiers::META => f.write_str("Meta")?,
|
KeyModifiers::META => f.write_str("Meta")?,
|
||||||
@ -496,11 +685,11 @@ bitflags! {
|
|||||||
/// Caps Lock was enabled for this key event.
|
/// Caps Lock was enabled for this key event.
|
||||||
///
|
///
|
||||||
/// **Note:** this is set for the initial press of Caps Lock itself.
|
/// **Note:** this is set for the initial press of Caps Lock itself.
|
||||||
const CAPS_LOCK = 0b0000_0010;
|
const CAPS_LOCK = 0b0000_1000;
|
||||||
/// Num Lock was enabled for this key event.
|
/// Num Lock was enabled for this key event.
|
||||||
///
|
///
|
||||||
/// **Note:** this is set for the initial press of Num Lock itself.
|
/// **Note:** this is set for the initial press of Num Lock itself.
|
||||||
const NUM_LOCK = 0b0000_0100;
|
const NUM_LOCK = 0b0000_1000;
|
||||||
const NONE = 0b0000_0000;
|
const NONE = 0b0000_0000;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -892,9 +1081,20 @@ impl Display for KeyCode {
|
|||||||
/// displayed as "Del", and the Enter key is displayed as "Enter".
|
/// displayed as "Del", and the Enter key is displayed as "Enter".
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
match self {
|
match self {
|
||||||
|
// On macOS, the Backspace key is called "Delete" and the Delete key is called "Fwd Del".
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
KeyCode::Backspace => write!(f, "Delete"),
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
KeyCode::Delete => write!(f, "Fwd Del"),
|
||||||
|
|
||||||
|
#[cfg(not(target_os = "macos"))]
|
||||||
KeyCode::Backspace => write!(f, "Backspace"),
|
KeyCode::Backspace => write!(f, "Backspace"),
|
||||||
|
#[cfg(not(target_os = "macos"))]
|
||||||
KeyCode::Delete => write!(f, "Del"),
|
KeyCode::Delete => write!(f, "Del"),
|
||||||
|
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
KeyCode::Enter => write!(f, "Return"),
|
||||||
|
#[cfg(not(target_os = "macos"))]
|
||||||
KeyCode::Enter => write!(f, "Enter"),
|
KeyCode::Enter => write!(f, "Enter"),
|
||||||
KeyCode::Left => write!(f, "Left"),
|
KeyCode::Left => write!(f, "Left"),
|
||||||
KeyCode::Right => write!(f, "Right"),
|
KeyCode::Right => write!(f, "Right"),
|
||||||
@ -928,6 +1128,25 @@ impl Display for KeyCode {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// An internal event.
|
||||||
|
///
|
||||||
|
/// Encapsulates publicly available `Event` with additional internal
|
||||||
|
/// events that shouldn't be publicly available to the crate users.
|
||||||
|
#[derive(Debug, PartialOrd, PartialEq, Hash, Clone, Eq)]
|
||||||
|
pub(crate) enum InternalEvent {
|
||||||
|
/// An event.
|
||||||
|
Event(Event),
|
||||||
|
/// A cursor position (`col`, `row`).
|
||||||
|
#[cfg(unix)]
|
||||||
|
CursorPosition(u16, u16),
|
||||||
|
/// The progressive keyboard enhancement flags enabled by the terminal.
|
||||||
|
#[cfg(unix)]
|
||||||
|
KeyboardEnhancementFlags(KeyboardEnhancementFlags),
|
||||||
|
/// Attributes and architectural class of the terminal.
|
||||||
|
#[cfg(unix)]
|
||||||
|
PrimaryDeviceAttributes,
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use std::collections::hash_map::DefaultHasher;
|
use std::collections::hash_map::DefaultHasher;
|
||||||
@ -970,9 +1189,18 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn keycode_display() {
|
fn keycode_display() {
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
{
|
||||||
|
assert_eq!(format!("{}", Backspace), "Delete");
|
||||||
|
assert_eq!(format!("{}", Delete), "Fwd Del");
|
||||||
|
assert_eq!(format!("{}", Enter), "Return");
|
||||||
|
}
|
||||||
|
#[cfg(not(target_os = "macos"))]
|
||||||
|
{
|
||||||
assert_eq!(format!("{}", Backspace), "Backspace");
|
assert_eq!(format!("{}", Backspace), "Backspace");
|
||||||
assert_eq!(format!("{}", Delete), "Del");
|
assert_eq!(format!("{}", Delete), "Del");
|
||||||
assert_eq!(format!("{}", Enter), "Enter");
|
assert_eq!(format!("{}", Enter), "Enter");
|
||||||
|
}
|
||||||
assert_eq!(format!("{}", Left), "Left");
|
assert_eq!(format!("{}", Left), "Left");
|
||||||
assert_eq!(format!("{}", Right), "Right");
|
assert_eq!(format!("{}", Right), "Right");
|
||||||
assert_eq!(format!("{}", Up), "Up");
|
assert_eq!(format!("{}", Up), "Up");
|
||||||
@ -1026,6 +1254,29 @@ mod tests {
|
|||||||
assert_eq!(format!("{}", Modifier(IsoLevel5Shift)), "Iso Level 5 Shift");
|
assert_eq!(format!("{}", Modifier(IsoLevel5Shift)), "Iso Level 5 Shift");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
#[test]
|
||||||
|
fn modifier_keycode_display_macos() {
|
||||||
|
assert_eq!(format!("{}", Modifier(LeftControl)), "Left Control");
|
||||||
|
assert_eq!(format!("{}", Modifier(LeftAlt)), "Left Option");
|
||||||
|
assert_eq!(format!("{}", Modifier(LeftSuper)), "Left Command");
|
||||||
|
assert_eq!(format!("{}", Modifier(RightControl)), "Right Control");
|
||||||
|
assert_eq!(format!("{}", Modifier(RightAlt)), "Right Option");
|
||||||
|
assert_eq!(format!("{}", Modifier(RightSuper)), "Right Command");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
#[test]
|
||||||
|
fn modifier_keycode_display_windows() {
|
||||||
|
assert_eq!(format!("{}", Modifier(LeftControl)), "Left Ctrl");
|
||||||
|
assert_eq!(format!("{}", Modifier(LeftAlt)), "Left Alt");
|
||||||
|
assert_eq!(format!("{}", Modifier(LeftSuper)), "Left Windows");
|
||||||
|
assert_eq!(format!("{}", Modifier(RightControl)), "Right Ctrl");
|
||||||
|
assert_eq!(format!("{}", Modifier(RightAlt)), "Right Alt");
|
||||||
|
assert_eq!(format!("{}", Modifier(RightSuper)), "Right Windows");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(any(target_os = "macos", target_os = "windows")))]
|
||||||
#[test]
|
#[test]
|
||||||
fn modifier_keycode_display_other() {
|
fn modifier_keycode_display_other() {
|
||||||
assert_eq!(format!("{}", Modifier(LeftControl)), "Left Ctrl");
|
assert_eq!(format!("{}", Modifier(LeftControl)), "Left Ctrl");
|
||||||
@ -1036,35 +1287,3 @@ mod tests {
|
|||||||
assert_eq!(format!("{}", Modifier(RightSuper)), "Right Super");
|
assert_eq!(format!("{}", Modifier(RightSuper)), "Right Super");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct TerminalState {
|
|
||||||
char_buffer: VecDeque<u8>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TerminalState {
|
|
||||||
pub fn new() -> Self {
|
|
||||||
Self {
|
|
||||||
char_buffer: VecDeque::<u8>::new(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn events_for_input(&mut self, input: &[u8]) -> std::io::Result<Vec<Event>> {
|
|
||||||
let mut result: Vec<Event> = Vec::new();
|
|
||||||
|
|
||||||
self.char_buffer.extend(input);
|
|
||||||
let mut buf: Vec<u8> = Vec::new();
|
|
||||||
|
|
||||||
loop {
|
|
||||||
match self.char_buffer.pop_front() {
|
|
||||||
None => return Ok(result),
|
|
||||||
Some(c) => {
|
|
||||||
buf.push(c);
|
|
||||||
match parse_event(&buf, true)? {
|
|
||||||
None => continue,
|
|
||||||
Some(e) => result.push(e),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -1,42 +1,48 @@
|
|||||||
use crate::event::Event;
|
use crate::event::InternalEvent;
|
||||||
|
|
||||||
/// Interface for filtering an `InternalEvent`.
|
/// Interface for filtering an `InternalEvent`.
|
||||||
pub(crate) trait Filter: Send + Sync + 'static {
|
pub(crate) trait Filter: Send + Sync + 'static {
|
||||||
/// Returns whether the given event fulfills the filter.
|
/// Returns whether the given event fulfills the filter.
|
||||||
fn eval(&self, event: &Event) -> bool;
|
fn eval(&self, event: &InternalEvent) -> bool;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub(crate) struct CursorPositionFilter;
|
pub(crate) struct CursorPositionFilter;
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
impl Filter for CursorPositionFilter {
|
impl Filter for CursorPositionFilter {
|
||||||
fn eval(&self, event: &Event) -> bool {
|
fn eval(&self, event: &InternalEvent) -> bool {
|
||||||
matches!(*event, Event::CursorPosition(_, _))
|
matches!(*event, InternalEvent::CursorPosition(_, _))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub(crate) struct KeyboardEnhancementFlagsFilter;
|
pub(crate) struct KeyboardEnhancementFlagsFilter;
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
impl Filter for KeyboardEnhancementFlagsFilter {
|
impl Filter for KeyboardEnhancementFlagsFilter {
|
||||||
fn eval(&self, event: &Event) -> bool {
|
fn eval(&self, event: &InternalEvent) -> bool {
|
||||||
// This filter checks for either a KeyboardEnhancementFlags response or
|
// This filter checks for either a KeyboardEnhancementFlags response or
|
||||||
// a PrimaryDeviceAttributes response. If we receive the PrimaryDeviceAttributes
|
// a PrimaryDeviceAttributes response. If we receive the PrimaryDeviceAttributes
|
||||||
// response but not KeyboardEnhancementFlags, the terminal does not support
|
// response but not KeyboardEnhancementFlags, the terminal does not support
|
||||||
// progressive keyboard enhancement.
|
// progressive keyboard enhancement.
|
||||||
matches!(
|
matches!(
|
||||||
*event,
|
*event,
|
||||||
Event::KeyboardEnhancementFlags(_) | Event::PrimaryDeviceAttributes
|
InternalEvent::KeyboardEnhancementFlags(_) | InternalEvent::PrimaryDeviceAttributes
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub(crate) struct PrimaryDeviceAttributesFilter;
|
pub(crate) struct PrimaryDeviceAttributesFilter;
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
impl Filter for PrimaryDeviceAttributesFilter {
|
impl Filter for PrimaryDeviceAttributesFilter {
|
||||||
fn eval(&self, event: &Event) -> bool {
|
fn eval(&self, event: &InternalEvent) -> bool {
|
||||||
matches!(*event, Event::PrimaryDeviceAttributes)
|
matches!(*event, InternalEvent::PrimaryDeviceAttributes)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -44,50 +50,66 @@ impl Filter for PrimaryDeviceAttributesFilter {
|
|||||||
pub(crate) struct EventFilter;
|
pub(crate) struct EventFilter;
|
||||||
|
|
||||||
impl Filter for EventFilter {
|
impl Filter for EventFilter {
|
||||||
fn eval(&self, _: &Event) -> bool {
|
#[cfg(unix)]
|
||||||
|
fn eval(&self, event: &InternalEvent) -> bool {
|
||||||
|
matches!(*event, InternalEvent::Event(_))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
|
fn eval(&self, _: &InternalEvent) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub(crate) struct InternalEventFilter;
|
||||||
|
|
||||||
|
impl Filter for InternalEventFilter {
|
||||||
|
fn eval(&self, _: &InternalEvent) -> bool {
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
#[cfg(unix)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::{
|
use super::{
|
||||||
super::Event, CursorPositionFilter, Event, EventFilter, Filter, InternalEventFilter,
|
super::Event, CursorPositionFilter, EventFilter, Filter, InternalEvent,
|
||||||
KeyboardEnhancementFlagsFilter, PrimaryDeviceAttributesFilter,
|
InternalEventFilter, KeyboardEnhancementFlagsFilter, PrimaryDeviceAttributesFilter,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_cursor_position_filter_filters_cursor_position() {
|
fn test_cursor_position_filter_filters_cursor_position() {
|
||||||
assert!(!CursorPositionFilter.eval(&Event::Event(Event::Resize(10, 10))));
|
assert!(!CursorPositionFilter.eval(&InternalEvent::Event(Event::Resize(10, 10))));
|
||||||
assert!(CursorPositionFilter.eval(&Event::CursorPosition(0, 0)));
|
assert!(CursorPositionFilter.eval(&InternalEvent::CursorPosition(0, 0)));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_keyboard_enhancement_status_filter_filters_keyboard_enhancement_status() {
|
fn test_keyboard_enhancement_status_filter_filters_keyboard_enhancement_status() {
|
||||||
assert!(!KeyboardEnhancementFlagsFilter.eval(&Event::Event(Event::Resize(10, 10))));
|
assert!(!KeyboardEnhancementFlagsFilter.eval(&InternalEvent::Event(Event::Resize(10, 10))));
|
||||||
assert!(
|
assert!(
|
||||||
KeyboardEnhancementFlagsFilter.eval(&Event::KeyboardEnhancementFlags(
|
KeyboardEnhancementFlagsFilter.eval(&InternalEvent::KeyboardEnhancementFlags(
|
||||||
crate::event::KeyboardEnhancementFlags::DISAMBIGUATE_ESCAPE_CODES
|
crate::event::KeyboardEnhancementFlags::DISAMBIGUATE_ESCAPE_CODES
|
||||||
))
|
))
|
||||||
);
|
);
|
||||||
assert!(KeyboardEnhancementFlagsFilter.eval(&Event::PrimaryDeviceAttributes));
|
assert!(KeyboardEnhancementFlagsFilter.eval(&InternalEvent::PrimaryDeviceAttributes));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_primary_device_attributes_filter_filters_primary_device_attributes() {
|
fn test_primary_device_attributes_filter_filters_primary_device_attributes() {
|
||||||
assert!(!PrimaryDeviceAttributesFilter.eval(&Event::Event(Event::Resize(10, 10))));
|
assert!(!PrimaryDeviceAttributesFilter.eval(&InternalEvent::Event(Event::Resize(10, 10))));
|
||||||
assert!(PrimaryDeviceAttributesFilter.eval(&Event::PrimaryDeviceAttributes));
|
assert!(PrimaryDeviceAttributesFilter.eval(&InternalEvent::PrimaryDeviceAttributes));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_event_filter_filters_events() {
|
fn test_event_filter_filters_events() {
|
||||||
assert!(EventFilter.eval(&Event::Event(Event::Resize(10, 10))));
|
assert!(EventFilter.eval(&InternalEvent::Event(Event::Resize(10, 10))));
|
||||||
assert!(!EventFilter.eval(&Event::CursorPosition(0, 0)));
|
assert!(!EventFilter.eval(&InternalEvent::CursorPosition(0, 0)));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_event_filter_filters_internal_events() {
|
fn test_event_filter_filters_internal_events() {
|
||||||
assert!(InternalEventFilter.eval(&Event::Event(Event::Resize(10, 10))));
|
assert!(InternalEventFilter.eval(&InternalEvent::Event(Event::Resize(10, 10))));
|
||||||
assert!(InternalEventFilter.eval(&Event::CursorPosition(0, 0)));
|
assert!(InternalEventFilter.eval(&InternalEvent::CursorPosition(0, 0)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
430
src/event/read.rs
Normal file
430
src/event/read.rs
Normal file
@ -0,0 +1,430 @@
|
|||||||
|
use std::{collections::vec_deque::VecDeque, io, time::Duration};
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
use crate::event::source::unix::UnixInternalEventSource;
|
||||||
|
#[cfg(windows)]
|
||||||
|
use crate::event::source::windows::WindowsEventSource;
|
||||||
|
#[cfg(feature = "event-stream")]
|
||||||
|
use crate::event::sys::Waker;
|
||||||
|
use crate::event::{filter::Filter, source::EventSource, timeout::PollTimeout, InternalEvent};
|
||||||
|
|
||||||
|
/// Can be used to read `InternalEvent`s.
|
||||||
|
pub(crate) struct InternalEventReader {
|
||||||
|
events: VecDeque<InternalEvent>,
|
||||||
|
source: Option<Box<dyn EventSource>>,
|
||||||
|
skipped_events: Vec<InternalEvent>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for InternalEventReader {
|
||||||
|
fn default() -> Self {
|
||||||
|
#[cfg(windows)]
|
||||||
|
let source = WindowsEventSource::new();
|
||||||
|
#[cfg(unix)]
|
||||||
|
let source = UnixInternalEventSource::new();
|
||||||
|
|
||||||
|
let source = source.ok().map(|x| Box::new(x) as Box<dyn EventSource>);
|
||||||
|
|
||||||
|
InternalEventReader {
|
||||||
|
source,
|
||||||
|
events: VecDeque::with_capacity(32),
|
||||||
|
skipped_events: Vec::with_capacity(32),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl InternalEventReader {
|
||||||
|
/// Returns a `Waker` allowing to wake/force the `poll` method to return `Ok(false)`.
|
||||||
|
#[cfg(feature = "event-stream")]
|
||||||
|
pub(crate) fn waker(&self) -> Waker {
|
||||||
|
self.source.as_ref().expect("reader source not set").waker()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn poll<F>(&mut self, timeout: Option<Duration>, filter: &F) -> io::Result<bool>
|
||||||
|
where
|
||||||
|
F: Filter,
|
||||||
|
{
|
||||||
|
for event in &self.events {
|
||||||
|
if filter.eval(event) {
|
||||||
|
return Ok(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let event_source = match self.source.as_mut() {
|
||||||
|
Some(source) => source,
|
||||||
|
None => {
|
||||||
|
return Err(std::io::Error::new(
|
||||||
|
std::io::ErrorKind::Other,
|
||||||
|
"Failed to initialize input reader",
|
||||||
|
))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let poll_timeout = PollTimeout::new(timeout);
|
||||||
|
|
||||||
|
loop {
|
||||||
|
let maybe_event = match event_source.try_read(poll_timeout.leftover()) {
|
||||||
|
Ok(None) => None,
|
||||||
|
Ok(Some(event)) => {
|
||||||
|
if filter.eval(&event) {
|
||||||
|
Some(event)
|
||||||
|
} else {
|
||||||
|
self.skipped_events.push(event);
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
if e.kind() == io::ErrorKind::Interrupted {
|
||||||
|
return Ok(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Err(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if poll_timeout.elapsed() || maybe_event.is_some() {
|
||||||
|
self.events.extend(self.skipped_events.drain(..));
|
||||||
|
|
||||||
|
if let Some(event) = maybe_event {
|
||||||
|
self.events.push_front(event);
|
||||||
|
return Ok(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ok(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn read<F>(&mut self, filter: &F) -> io::Result<InternalEvent>
|
||||||
|
where
|
||||||
|
F: Filter,
|
||||||
|
{
|
||||||
|
let mut skipped_events = VecDeque::new();
|
||||||
|
|
||||||
|
loop {
|
||||||
|
while let Some(event) = self.events.pop_front() {
|
||||||
|
if filter.eval(&event) {
|
||||||
|
while let Some(event) = skipped_events.pop_front() {
|
||||||
|
self.events.push_back(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ok(event);
|
||||||
|
} else {
|
||||||
|
// We can not directly write events back to `self.events`.
|
||||||
|
// If we did, we would put our self's into an endless loop
|
||||||
|
// that would enqueue -> dequeue -> enqueue etc.
|
||||||
|
// This happens because `poll` in this function will always return true if there are events in it's.
|
||||||
|
// And because we just put the non-fulfilling event there this is going to be the case.
|
||||||
|
// Instead we can store them into the temporary buffer,
|
||||||
|
// and then when the filter is fulfilled write all events back in order.
|
||||||
|
skipped_events.push_back(event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let _ = self.poll(None, filter)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use std::io;
|
||||||
|
use std::{collections::VecDeque, time::Duration};
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
use super::super::filter::CursorPositionFilter;
|
||||||
|
use super::{
|
||||||
|
super::{filter::InternalEventFilter, Event},
|
||||||
|
EventSource, InternalEvent, InternalEventReader,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_poll_fails_without_event_source() {
|
||||||
|
let mut reader = InternalEventReader {
|
||||||
|
events: VecDeque::new(),
|
||||||
|
source: None,
|
||||||
|
skipped_events: Vec::with_capacity(32),
|
||||||
|
};
|
||||||
|
|
||||||
|
assert!(reader.poll(None, &InternalEventFilter).is_err());
|
||||||
|
assert!(reader
|
||||||
|
.poll(Some(Duration::from_secs(0)), &InternalEventFilter)
|
||||||
|
.is_err());
|
||||||
|
assert!(reader
|
||||||
|
.poll(Some(Duration::from_secs(10)), &InternalEventFilter)
|
||||||
|
.is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_poll_returns_true_for_matching_event_in_queue_at_front() {
|
||||||
|
let mut reader = InternalEventReader {
|
||||||
|
events: vec![InternalEvent::Event(Event::Resize(10, 10))].into(),
|
||||||
|
source: None,
|
||||||
|
skipped_events: Vec::with_capacity(32),
|
||||||
|
};
|
||||||
|
|
||||||
|
assert!(reader.poll(None, &InternalEventFilter).unwrap());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[cfg(unix)]
|
||||||
|
fn test_poll_returns_true_for_matching_event_in_queue_at_back() {
|
||||||
|
let mut reader = InternalEventReader {
|
||||||
|
events: vec![
|
||||||
|
InternalEvent::Event(Event::Resize(10, 10)),
|
||||||
|
InternalEvent::CursorPosition(10, 20),
|
||||||
|
]
|
||||||
|
.into(),
|
||||||
|
source: None,
|
||||||
|
skipped_events: Vec::with_capacity(32),
|
||||||
|
};
|
||||||
|
|
||||||
|
assert!(reader.poll(None, &CursorPositionFilter).unwrap());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_read_returns_matching_event_in_queue_at_front() {
|
||||||
|
const EVENT: InternalEvent = InternalEvent::Event(Event::Resize(10, 10));
|
||||||
|
|
||||||
|
let mut reader = InternalEventReader {
|
||||||
|
events: vec![EVENT].into(),
|
||||||
|
source: None,
|
||||||
|
skipped_events: Vec::with_capacity(32),
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(reader.read(&InternalEventFilter).unwrap(), EVENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[cfg(unix)]
|
||||||
|
fn test_read_returns_matching_event_in_queue_at_back() {
|
||||||
|
const CURSOR_EVENT: InternalEvent = InternalEvent::CursorPosition(10, 20);
|
||||||
|
|
||||||
|
let mut reader = InternalEventReader {
|
||||||
|
events: vec![InternalEvent::Event(Event::Resize(10, 10)), CURSOR_EVENT].into(),
|
||||||
|
source: None,
|
||||||
|
skipped_events: Vec::with_capacity(32),
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(reader.read(&CursorPositionFilter).unwrap(), CURSOR_EVENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[cfg(unix)]
|
||||||
|
fn test_read_does_not_consume_skipped_event() {
|
||||||
|
const SKIPPED_EVENT: InternalEvent = InternalEvent::Event(Event::Resize(10, 10));
|
||||||
|
const CURSOR_EVENT: InternalEvent = InternalEvent::CursorPosition(10, 20);
|
||||||
|
|
||||||
|
let mut reader = InternalEventReader {
|
||||||
|
events: vec![SKIPPED_EVENT, CURSOR_EVENT].into(),
|
||||||
|
source: None,
|
||||||
|
skipped_events: Vec::with_capacity(32),
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(reader.read(&CursorPositionFilter).unwrap(), CURSOR_EVENT);
|
||||||
|
assert_eq!(reader.read(&InternalEventFilter).unwrap(), SKIPPED_EVENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_poll_timeouts_if_source_has_no_events() {
|
||||||
|
let source = FakeSource::default();
|
||||||
|
|
||||||
|
let mut reader = InternalEventReader {
|
||||||
|
events: VecDeque::new(),
|
||||||
|
source: Some(Box::new(source)),
|
||||||
|
skipped_events: Vec::with_capacity(32),
|
||||||
|
};
|
||||||
|
|
||||||
|
assert!(!reader
|
||||||
|
.poll(Some(Duration::from_secs(0)), &InternalEventFilter)
|
||||||
|
.unwrap());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_poll_returns_true_if_source_has_at_least_one_event() {
|
||||||
|
let source = FakeSource::with_events(&[InternalEvent::Event(Event::Resize(10, 10))]);
|
||||||
|
|
||||||
|
let mut reader = InternalEventReader {
|
||||||
|
events: VecDeque::new(),
|
||||||
|
source: Some(Box::new(source)),
|
||||||
|
skipped_events: Vec::with_capacity(32),
|
||||||
|
};
|
||||||
|
|
||||||
|
assert!(reader.poll(None, &InternalEventFilter).unwrap());
|
||||||
|
assert!(reader
|
||||||
|
.poll(Some(Duration::from_secs(0)), &InternalEventFilter)
|
||||||
|
.unwrap());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_reads_returns_event_if_source_has_at_least_one_event() {
|
||||||
|
const EVENT: InternalEvent = InternalEvent::Event(Event::Resize(10, 10));
|
||||||
|
|
||||||
|
let source = FakeSource::with_events(&[EVENT]);
|
||||||
|
|
||||||
|
let mut reader = InternalEventReader {
|
||||||
|
events: VecDeque::new(),
|
||||||
|
source: Some(Box::new(source)),
|
||||||
|
skipped_events: Vec::with_capacity(32),
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(reader.read(&InternalEventFilter).unwrap(), EVENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_read_returns_events_if_source_has_events() {
|
||||||
|
const EVENT: InternalEvent = InternalEvent::Event(Event::Resize(10, 10));
|
||||||
|
|
||||||
|
let source = FakeSource::with_events(&[EVENT, EVENT, EVENT]);
|
||||||
|
|
||||||
|
let mut reader = InternalEventReader {
|
||||||
|
events: VecDeque::new(),
|
||||||
|
source: Some(Box::new(source)),
|
||||||
|
skipped_events: Vec::with_capacity(32),
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(reader.read(&InternalEventFilter).unwrap(), EVENT);
|
||||||
|
assert_eq!(reader.read(&InternalEventFilter).unwrap(), EVENT);
|
||||||
|
assert_eq!(reader.read(&InternalEventFilter).unwrap(), EVENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_poll_returns_false_after_all_source_events_are_consumed() {
|
||||||
|
const EVENT: InternalEvent = InternalEvent::Event(Event::Resize(10, 10));
|
||||||
|
|
||||||
|
let source = FakeSource::with_events(&[EVENT, EVENT, EVENT]);
|
||||||
|
|
||||||
|
let mut reader = InternalEventReader {
|
||||||
|
events: VecDeque::new(),
|
||||||
|
source: Some(Box::new(source)),
|
||||||
|
skipped_events: Vec::with_capacity(32),
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(reader.read(&InternalEventFilter).unwrap(), EVENT);
|
||||||
|
assert_eq!(reader.read(&InternalEventFilter).unwrap(), EVENT);
|
||||||
|
assert_eq!(reader.read(&InternalEventFilter).unwrap(), EVENT);
|
||||||
|
assert!(!reader
|
||||||
|
.poll(Some(Duration::from_secs(0)), &InternalEventFilter)
|
||||||
|
.unwrap());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_poll_propagates_error() {
|
||||||
|
let mut reader = InternalEventReader {
|
||||||
|
events: VecDeque::new(),
|
||||||
|
source: Some(Box::new(FakeSource::new(&[]))),
|
||||||
|
skipped_events: Vec::with_capacity(32),
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
reader
|
||||||
|
.poll(Some(Duration::from_secs(0)), &InternalEventFilter)
|
||||||
|
.err()
|
||||||
|
.map(|e| format!("{:?}", &e.kind())),
|
||||||
|
Some(format!("{:?}", io::ErrorKind::Other))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_read_propagates_error() {
|
||||||
|
let mut reader = InternalEventReader {
|
||||||
|
events: VecDeque::new(),
|
||||||
|
source: Some(Box::new(FakeSource::new(&[]))),
|
||||||
|
skipped_events: Vec::with_capacity(32),
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
reader
|
||||||
|
.read(&InternalEventFilter)
|
||||||
|
.err()
|
||||||
|
.map(|e| format!("{:?}", &e.kind())),
|
||||||
|
Some(format!("{:?}", io::ErrorKind::Other))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_poll_continues_after_error() {
|
||||||
|
const EVENT: InternalEvent = InternalEvent::Event(Event::Resize(10, 10));
|
||||||
|
|
||||||
|
let source = FakeSource::new(&[EVENT, EVENT]);
|
||||||
|
|
||||||
|
let mut reader = InternalEventReader {
|
||||||
|
events: VecDeque::new(),
|
||||||
|
source: Some(Box::new(source)),
|
||||||
|
skipped_events: Vec::with_capacity(32),
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(reader.read(&InternalEventFilter).unwrap(), EVENT);
|
||||||
|
assert!(reader.read(&InternalEventFilter).is_err());
|
||||||
|
assert!(reader
|
||||||
|
.poll(Some(Duration::from_secs(0)), &InternalEventFilter)
|
||||||
|
.unwrap());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_read_continues_after_error() {
|
||||||
|
const EVENT: InternalEvent = InternalEvent::Event(Event::Resize(10, 10));
|
||||||
|
|
||||||
|
let source = FakeSource::new(&[EVENT, EVENT]);
|
||||||
|
|
||||||
|
let mut reader = InternalEventReader {
|
||||||
|
events: VecDeque::new(),
|
||||||
|
source: Some(Box::new(source)),
|
||||||
|
skipped_events: Vec::with_capacity(32),
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(reader.read(&InternalEventFilter).unwrap(), EVENT);
|
||||||
|
assert!(reader.read(&InternalEventFilter).is_err());
|
||||||
|
assert_eq!(reader.read(&InternalEventFilter).unwrap(), EVENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
struct FakeSource {
|
||||||
|
events: VecDeque<InternalEvent>,
|
||||||
|
error: Option<io::Error>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FakeSource {
|
||||||
|
fn new(events: &[InternalEvent]) -> FakeSource {
|
||||||
|
FakeSource {
|
||||||
|
events: events.to_vec().into(),
|
||||||
|
error: Some(io::Error::new(io::ErrorKind::Other, "")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn with_events(events: &[InternalEvent]) -> FakeSource {
|
||||||
|
FakeSource {
|
||||||
|
events: events.to_vec().into(),
|
||||||
|
error: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EventSource for FakeSource {
|
||||||
|
fn try_read(&mut self, _timeout: Option<Duration>) -> io::Result<Option<InternalEvent>> {
|
||||||
|
// Return error if set in case there's just one remaining event
|
||||||
|
if self.events.len() == 1 {
|
||||||
|
if let Some(error) = self.error.take() {
|
||||||
|
return Err(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return all events from the queue
|
||||||
|
if let Some(event) = self.events.pop_front() {
|
||||||
|
return Ok(Some(event));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return error if there're no more events
|
||||||
|
if let Some(error) = self.error.take() {
|
||||||
|
return Err(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Timeout
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "event-stream")]
|
||||||
|
fn waker(&self) -> super::super::sys::Waker {
|
||||||
|
unimplemented!();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
27
src/event/source.rs
Normal file
27
src/event/source.rs
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
use std::{io, time::Duration};
|
||||||
|
|
||||||
|
#[cfg(feature = "event-stream")]
|
||||||
|
use super::sys::Waker;
|
||||||
|
use super::InternalEvent;
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
pub(crate) mod unix;
|
||||||
|
#[cfg(windows)]
|
||||||
|
pub(crate) mod windows;
|
||||||
|
|
||||||
|
/// An interface for trying to read an `InternalEvent` within an optional `Duration`.
|
||||||
|
pub(crate) trait EventSource: Sync + Send {
|
||||||
|
/// Tries to read an `InternalEvent` within the given duration.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// * `timeout` - `None` block indefinitely until an event is available, `Some(duration)` blocks
|
||||||
|
/// for the given timeout
|
||||||
|
///
|
||||||
|
/// Returns `Ok(None)` if there's no event available and timeout expires.
|
||||||
|
fn try_read(&mut self, timeout: Option<Duration>) -> io::Result<Option<InternalEvent>>;
|
||||||
|
|
||||||
|
/// Returns a `Waker` allowing to wake/force the `try_read` method to return `Ok(None)`.
|
||||||
|
#[cfg(feature = "event-stream")]
|
||||||
|
fn waker(&self) -> Waker;
|
||||||
|
}
|
11
src/event/source/unix.rs
Normal file
11
src/event/source/unix.rs
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
#[cfg(feature = "use-dev-tty")]
|
||||||
|
pub(crate) mod tty;
|
||||||
|
|
||||||
|
#[cfg(not(feature = "use-dev-tty"))]
|
||||||
|
pub(crate) mod mio;
|
||||||
|
|
||||||
|
#[cfg(feature = "use-dev-tty")]
|
||||||
|
pub(crate) use self::tty::UnixInternalEventSource;
|
||||||
|
|
||||||
|
#[cfg(not(feature = "use-dev-tty"))]
|
||||||
|
pub(crate) use self::mio::UnixInternalEventSource;
|
229
src/event/source/unix/mio.rs
Normal file
229
src/event/source/unix/mio.rs
Normal file
@ -0,0 +1,229 @@
|
|||||||
|
use std::{collections::VecDeque, io, time::Duration};
|
||||||
|
|
||||||
|
use mio::{unix::SourceFd, Events, Interest, Poll, Token};
|
||||||
|
use signal_hook_mio::v0_8::Signals;
|
||||||
|
|
||||||
|
#[cfg(feature = "event-stream")]
|
||||||
|
use crate::event::sys::Waker;
|
||||||
|
use crate::event::{
|
||||||
|
source::EventSource, sys::unix::parse::parse_event, timeout::PollTimeout, Event, InternalEvent,
|
||||||
|
};
|
||||||
|
use crate::terminal::sys::file_descriptor::{tty_fd, FileDesc};
|
||||||
|
|
||||||
|
// Tokens to identify file descriptor
|
||||||
|
const TTY_TOKEN: Token = Token(0);
|
||||||
|
const SIGNAL_TOKEN: Token = Token(1);
|
||||||
|
#[cfg(feature = "event-stream")]
|
||||||
|
const WAKE_TOKEN: Token = Token(2);
|
||||||
|
|
||||||
|
// I (@zrzka) wasn't able to read more than 1_022 bytes when testing
|
||||||
|
// reading on macOS/Linux -> we don't need bigger buffer and 1k of bytes
|
||||||
|
// is enough.
|
||||||
|
const TTY_BUFFER_SIZE: usize = 1_024;
|
||||||
|
|
||||||
|
pub(crate) struct UnixInternalEventSource {
|
||||||
|
poll: Poll,
|
||||||
|
events: Events,
|
||||||
|
parser: Parser,
|
||||||
|
tty_buffer: [u8; TTY_BUFFER_SIZE],
|
||||||
|
tty_fd: FileDesc<'static>,
|
||||||
|
signals: Signals,
|
||||||
|
#[cfg(feature = "event-stream")]
|
||||||
|
waker: Waker,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UnixInternalEventSource {
|
||||||
|
pub fn new() -> io::Result<Self> {
|
||||||
|
UnixInternalEventSource::from_file_descriptor(tty_fd()?)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn from_file_descriptor(input_fd: FileDesc<'static>) -> io::Result<Self> {
|
||||||
|
let poll = Poll::new()?;
|
||||||
|
let registry = poll.registry();
|
||||||
|
|
||||||
|
let tty_raw_fd = input_fd.raw_fd();
|
||||||
|
let mut tty_ev = SourceFd(&tty_raw_fd);
|
||||||
|
registry.register(&mut tty_ev, TTY_TOKEN, Interest::READABLE)?;
|
||||||
|
|
||||||
|
let mut signals = Signals::new([signal_hook::consts::SIGWINCH])?;
|
||||||
|
registry.register(&mut signals, SIGNAL_TOKEN, Interest::READABLE)?;
|
||||||
|
|
||||||
|
#[cfg(feature = "event-stream")]
|
||||||
|
let waker = Waker::new(registry, WAKE_TOKEN)?;
|
||||||
|
|
||||||
|
Ok(UnixInternalEventSource {
|
||||||
|
poll,
|
||||||
|
events: Events::with_capacity(3),
|
||||||
|
parser: Parser::default(),
|
||||||
|
tty_buffer: [0u8; TTY_BUFFER_SIZE],
|
||||||
|
tty_fd: input_fd,
|
||||||
|
signals,
|
||||||
|
#[cfg(feature = "event-stream")]
|
||||||
|
waker,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EventSource for UnixInternalEventSource {
|
||||||
|
fn try_read(&mut self, timeout: Option<Duration>) -> io::Result<Option<InternalEvent>> {
|
||||||
|
if let Some(event) = self.parser.next() {
|
||||||
|
return Ok(Some(event));
|
||||||
|
}
|
||||||
|
|
||||||
|
let timeout = PollTimeout::new(timeout);
|
||||||
|
|
||||||
|
loop {
|
||||||
|
if let Err(e) = self.poll.poll(&mut self.events, timeout.leftover()) {
|
||||||
|
// Mio will throw an interrupted error in case of cursor position retrieval. We need to retry until it succeeds.
|
||||||
|
// Previous versions of Mio (< 0.7) would automatically retry the poll call if it was interrupted (if EINTR was returned).
|
||||||
|
// https://docs.rs/mio/0.7.0/mio/struct.Poll.html#notes
|
||||||
|
if e.kind() == io::ErrorKind::Interrupted {
|
||||||
|
continue;
|
||||||
|
} else {
|
||||||
|
return Err(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if self.events.is_empty() {
|
||||||
|
// No readiness events = timeout
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
|
||||||
|
for token in self.events.iter().map(|x| x.token()) {
|
||||||
|
match token {
|
||||||
|
TTY_TOKEN => {
|
||||||
|
loop {
|
||||||
|
match self.tty_fd.read(&mut self.tty_buffer) {
|
||||||
|
Ok(read_count) => {
|
||||||
|
if read_count > 0 {
|
||||||
|
self.parser.advance(
|
||||||
|
&self.tty_buffer[..read_count],
|
||||||
|
read_count == TTY_BUFFER_SIZE,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
// No more data to read at the moment. We will receive another event
|
||||||
|
if e.kind() == io::ErrorKind::WouldBlock {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// once more data is available to read.
|
||||||
|
else if e.kind() == io::ErrorKind::Interrupted {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(event) = self.parser.next() {
|
||||||
|
return Ok(Some(event));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SIGNAL_TOKEN => {
|
||||||
|
if self.signals.pending().next() == Some(signal_hook::consts::SIGWINCH) {
|
||||||
|
// TODO Should we remove tput?
|
||||||
|
//
|
||||||
|
// This can take a really long time, because terminal::size can
|
||||||
|
// launch new process (tput) and then it parses its output. It's
|
||||||
|
// not a really long time from the absolute time point of view, but
|
||||||
|
// it's a really long time from the mio, async-std/tokio executor, ...
|
||||||
|
// point of view.
|
||||||
|
let new_size = crate::terminal::size()?;
|
||||||
|
return Ok(Some(InternalEvent::Event(Event::Resize(
|
||||||
|
new_size.0, new_size.1,
|
||||||
|
))));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[cfg(feature = "event-stream")]
|
||||||
|
WAKE_TOKEN => {
|
||||||
|
return Err(std::io::Error::new(
|
||||||
|
std::io::ErrorKind::Interrupted,
|
||||||
|
"Poll operation was woken up by `Waker::wake`",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
_ => unreachable!("Synchronize Evented handle registration & token handling"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Processing above can take some time, check if timeout expired
|
||||||
|
if timeout.elapsed() {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "event-stream")]
|
||||||
|
fn waker(&self) -> Waker {
|
||||||
|
self.waker.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Following `Parser` structure exists for two reasons:
|
||||||
|
//
|
||||||
|
// * mimic anes Parser interface
|
||||||
|
// * move the advancing, parsing, ... stuff out of the `try_read` method
|
||||||
|
//
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct Parser {
|
||||||
|
buffer: Vec<u8>,
|
||||||
|
internal_events: VecDeque<InternalEvent>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Parser {
|
||||||
|
fn default() -> Self {
|
||||||
|
Parser {
|
||||||
|
// This buffer is used for -> 1 <- ANSI escape sequence. Are we
|
||||||
|
// aware of any ANSI escape sequence that is bigger? Can we make
|
||||||
|
// it smaller?
|
||||||
|
//
|
||||||
|
// Probably not worth spending more time on this as "there's a plan"
|
||||||
|
// to use the anes crate parser.
|
||||||
|
buffer: Vec::with_capacity(256),
|
||||||
|
// TTY_BUFFER_SIZE is 1_024 bytes. How many ANSI escape sequences can
|
||||||
|
// fit? What is an average sequence length? Let's guess here
|
||||||
|
// and say that the average ANSI escape sequence length is 8 bytes. Thus
|
||||||
|
// the buffer size should be 1024/8=128 to avoid additional allocations
|
||||||
|
// when processing large amounts of data.
|
||||||
|
//
|
||||||
|
// There's no need to make it bigger, because when you look at the `try_read`
|
||||||
|
// method implementation, all events are consumed before the next TTY_BUFFER
|
||||||
|
// is processed -> events pushed.
|
||||||
|
internal_events: VecDeque::with_capacity(128),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Parser {
|
||||||
|
fn advance(&mut self, buffer: &[u8], more: bool) {
|
||||||
|
for (idx, byte) in buffer.iter().enumerate() {
|
||||||
|
let more = idx + 1 < buffer.len() || more;
|
||||||
|
|
||||||
|
self.buffer.push(*byte);
|
||||||
|
|
||||||
|
match parse_event(&self.buffer, more) {
|
||||||
|
Ok(Some(ie)) => {
|
||||||
|
self.internal_events.push_back(ie);
|
||||||
|
self.buffer.clear();
|
||||||
|
}
|
||||||
|
Ok(None) => {
|
||||||
|
// Event can't be parsed, because we don't have enough bytes for
|
||||||
|
// the current sequence. Keep the buffer and process next bytes.
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
// Event can't be parsed (not enough parameters, parameter is not a number, ...).
|
||||||
|
// Clear the buffer and continue with another sequence.
|
||||||
|
self.buffer.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Iterator for Parser {
|
||||||
|
type Item = InternalEvent;
|
||||||
|
|
||||||
|
fn next(&mut self) -> Option<Self::Item> {
|
||||||
|
self.internal_events.pop_front()
|
||||||
|
}
|
||||||
|
}
|
275
src/event/source/unix/tty.rs
Normal file
275
src/event/source/unix/tty.rs
Normal file
@ -0,0 +1,275 @@
|
|||||||
|
#[cfg(feature = "libc")]
|
||||||
|
use std::os::unix::prelude::AsRawFd;
|
||||||
|
use std::{collections::VecDeque, io, os::unix::net::UnixStream, time::Duration};
|
||||||
|
|
||||||
|
#[cfg(not(feature = "libc"))]
|
||||||
|
use rustix::fd::{AsFd, AsRawFd};
|
||||||
|
|
||||||
|
use signal_hook::low_level::pipe;
|
||||||
|
|
||||||
|
use crate::event::timeout::PollTimeout;
|
||||||
|
use crate::event::Event;
|
||||||
|
use filedescriptor::{poll, pollfd, POLLIN};
|
||||||
|
|
||||||
|
#[cfg(feature = "event-stream")]
|
||||||
|
use crate::event::sys::Waker;
|
||||||
|
use crate::event::{source::EventSource, sys::unix::parse::parse_event, InternalEvent};
|
||||||
|
use crate::terminal::sys::file_descriptor::{tty_fd, FileDesc};
|
||||||
|
|
||||||
|
/// Holds a prototypical Waker and a receiver we can wait on when doing select().
|
||||||
|
#[cfg(feature = "event-stream")]
|
||||||
|
struct WakePipe {
|
||||||
|
receiver: UnixStream,
|
||||||
|
waker: Waker,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "event-stream")]
|
||||||
|
impl WakePipe {
|
||||||
|
fn new() -> io::Result<Self> {
|
||||||
|
let (receiver, sender) = nonblocking_unix_pair()?;
|
||||||
|
Ok(WakePipe {
|
||||||
|
receiver,
|
||||||
|
waker: Waker::new(sender),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// I (@zrzka) wasn't able to read more than 1_022 bytes when testing
|
||||||
|
// reading on macOS/Linux -> we don't need bigger buffer and 1k of bytes
|
||||||
|
// is enough.
|
||||||
|
const TTY_BUFFER_SIZE: usize = 1_024;
|
||||||
|
|
||||||
|
pub(crate) struct UnixInternalEventSource {
|
||||||
|
parser: Parser,
|
||||||
|
tty_buffer: [u8; TTY_BUFFER_SIZE],
|
||||||
|
tty: FileDesc<'static>,
|
||||||
|
winch_signal_receiver: UnixStream,
|
||||||
|
#[cfg(feature = "event-stream")]
|
||||||
|
wake_pipe: WakePipe,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn nonblocking_unix_pair() -> io::Result<(UnixStream, UnixStream)> {
|
||||||
|
let (receiver, sender) = UnixStream::pair()?;
|
||||||
|
receiver.set_nonblocking(true)?;
|
||||||
|
sender.set_nonblocking(true)?;
|
||||||
|
Ok((receiver, sender))
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UnixInternalEventSource {
|
||||||
|
pub fn new() -> io::Result<Self> {
|
||||||
|
UnixInternalEventSource::from_file_descriptor(tty_fd()?)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn from_file_descriptor(input_fd: FileDesc<'static>) -> io::Result<Self> {
|
||||||
|
Ok(UnixInternalEventSource {
|
||||||
|
parser: Parser::default(),
|
||||||
|
tty_buffer: [0u8; TTY_BUFFER_SIZE],
|
||||||
|
tty: input_fd,
|
||||||
|
winch_signal_receiver: {
|
||||||
|
let (receiver, sender) = nonblocking_unix_pair()?;
|
||||||
|
// Unregistering is unnecessary because EventSource is a singleton
|
||||||
|
#[cfg(feature = "libc")]
|
||||||
|
pipe::register(libc::SIGWINCH, sender)?;
|
||||||
|
#[cfg(not(feature = "libc"))]
|
||||||
|
pipe::register(rustix::process::Signal::Winch as i32, sender)?;
|
||||||
|
receiver
|
||||||
|
},
|
||||||
|
#[cfg(feature = "event-stream")]
|
||||||
|
wake_pipe: WakePipe::new()?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// read_complete reads from a non-blocking file descriptor
|
||||||
|
/// until the buffer is full or it would block.
|
||||||
|
///
|
||||||
|
/// Similar to `std::io::Read::read_to_end`, except this function
|
||||||
|
/// only fills the given buffer and does not read beyond that.
|
||||||
|
fn read_complete(fd: &FileDesc, buf: &mut [u8]) -> io::Result<usize> {
|
||||||
|
loop {
|
||||||
|
match fd.read(buf) {
|
||||||
|
Ok(x) => return Ok(x),
|
||||||
|
Err(e) => match e.kind() {
|
||||||
|
io::ErrorKind::WouldBlock => return Ok(0),
|
||||||
|
io::ErrorKind::Interrupted => continue,
|
||||||
|
_ => return Err(e),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EventSource for UnixInternalEventSource {
|
||||||
|
fn try_read(&mut self, timeout: Option<Duration>) -> io::Result<Option<InternalEvent>> {
|
||||||
|
let timeout = PollTimeout::new(timeout);
|
||||||
|
|
||||||
|
fn make_pollfd<F: AsRawFd>(fd: &F) -> pollfd {
|
||||||
|
pollfd {
|
||||||
|
fd: fd.as_raw_fd(),
|
||||||
|
events: POLLIN,
|
||||||
|
revents: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(feature = "event-stream"))]
|
||||||
|
let mut fds = [
|
||||||
|
make_pollfd(&self.tty),
|
||||||
|
make_pollfd(&self.winch_signal_receiver),
|
||||||
|
];
|
||||||
|
|
||||||
|
#[cfg(feature = "event-stream")]
|
||||||
|
let mut fds = [
|
||||||
|
make_pollfd(&self.tty),
|
||||||
|
make_pollfd(&self.winch_signal_receiver),
|
||||||
|
make_pollfd(&self.wake_pipe.receiver),
|
||||||
|
];
|
||||||
|
|
||||||
|
while timeout.leftover().map_or(true, |t| !t.is_zero()) {
|
||||||
|
// check if there are buffered events from the last read
|
||||||
|
if let Some(event) = self.parser.next() {
|
||||||
|
return Ok(Some(event));
|
||||||
|
}
|
||||||
|
match poll(&mut fds, timeout.leftover()) {
|
||||||
|
Err(filedescriptor::Error::Poll(e)) | Err(filedescriptor::Error::Io(e)) => {
|
||||||
|
match e.kind() {
|
||||||
|
// retry on EINTR
|
||||||
|
io::ErrorKind::Interrupted => continue,
|
||||||
|
_ => return Err(e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
return Err(std::io::Error::new(
|
||||||
|
std::io::ErrorKind::Other,
|
||||||
|
format!("got unexpected error while polling: {:?}", e),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
Ok(_) => (),
|
||||||
|
};
|
||||||
|
if fds[0].revents & POLLIN != 0 {
|
||||||
|
loop {
|
||||||
|
let read_count = read_complete(&self.tty, &mut self.tty_buffer)?;
|
||||||
|
if read_count > 0 {
|
||||||
|
self.parser.advance(
|
||||||
|
&self.tty_buffer[..read_count],
|
||||||
|
read_count == TTY_BUFFER_SIZE,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(event) = self.parser.next() {
|
||||||
|
return Ok(Some(event));
|
||||||
|
}
|
||||||
|
|
||||||
|
if read_count == 0 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if fds[1].revents & POLLIN != 0 {
|
||||||
|
#[cfg(feature = "libc")]
|
||||||
|
let fd = FileDesc::new(self.winch_signal_receiver.as_raw_fd(), false);
|
||||||
|
#[cfg(not(feature = "libc"))]
|
||||||
|
let fd = FileDesc::Borrowed(self.winch_signal_receiver.as_fd());
|
||||||
|
// drain the pipe
|
||||||
|
while read_complete(&fd, &mut [0; 1024])? != 0 {}
|
||||||
|
// TODO Should we remove tput?
|
||||||
|
//
|
||||||
|
// This can take a really long time, because terminal::size can
|
||||||
|
// launch new process (tput) and then it parses its output. It's
|
||||||
|
// not a really long time from the absolute time point of view, but
|
||||||
|
// it's a really long time from the mio, async-std/tokio executor, ...
|
||||||
|
// point of view.
|
||||||
|
let new_size = crate::terminal::size()?;
|
||||||
|
return Ok(Some(InternalEvent::Event(Event::Resize(
|
||||||
|
new_size.0, new_size.1,
|
||||||
|
))));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "event-stream")]
|
||||||
|
if fds[2].revents & POLLIN != 0 {
|
||||||
|
let fd = FileDesc::new(self.wake_pipe.receiver.as_raw_fd(), false);
|
||||||
|
// drain the pipe
|
||||||
|
while read_complete(&fd, &mut [0; 1024])? != 0 {}
|
||||||
|
|
||||||
|
return Err(std::io::Error::new(
|
||||||
|
std::io::ErrorKind::Interrupted,
|
||||||
|
"Poll operation was woken up by `Waker::wake`",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "event-stream")]
|
||||||
|
fn waker(&self) -> Waker {
|
||||||
|
self.wake_pipe.waker.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Following `Parser` structure exists for two reasons:
|
||||||
|
//
|
||||||
|
// * mimic anes Parser interface
|
||||||
|
// * move the advancing, parsing, ... stuff out of the `try_read` method
|
||||||
|
//
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct Parser {
|
||||||
|
buffer: Vec<u8>,
|
||||||
|
internal_events: VecDeque<InternalEvent>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Parser {
|
||||||
|
fn default() -> Self {
|
||||||
|
Parser {
|
||||||
|
// This buffer is used for -> 1 <- ANSI escape sequence. Are we
|
||||||
|
// aware of any ANSI escape sequence that is bigger? Can we make
|
||||||
|
// it smaller?
|
||||||
|
//
|
||||||
|
// Probably not worth spending more time on this as "there's a plan"
|
||||||
|
// to use the anes crate parser.
|
||||||
|
buffer: Vec::with_capacity(256),
|
||||||
|
// TTY_BUFFER_SIZE is 1_024 bytes. How many ANSI escape sequences can
|
||||||
|
// fit? What is an average sequence length? Let's guess here
|
||||||
|
// and say that the average ANSI escape sequence length is 8 bytes. Thus
|
||||||
|
// the buffer size should be 1024/8=128 to avoid additional allocations
|
||||||
|
// when processing large amounts of data.
|
||||||
|
//
|
||||||
|
// There's no need to make it bigger, because when you look at the `try_read`
|
||||||
|
// method implementation, all events are consumed before the next TTY_BUFFER
|
||||||
|
// is processed -> events pushed.
|
||||||
|
internal_events: VecDeque::with_capacity(128),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Parser {
|
||||||
|
fn advance(&mut self, buffer: &[u8], more: bool) {
|
||||||
|
for (idx, byte) in buffer.iter().enumerate() {
|
||||||
|
let more = idx + 1 < buffer.len() || more;
|
||||||
|
|
||||||
|
self.buffer.push(*byte);
|
||||||
|
|
||||||
|
match parse_event(&self.buffer, more) {
|
||||||
|
Ok(Some(ie)) => {
|
||||||
|
self.internal_events.push_back(ie);
|
||||||
|
self.buffer.clear();
|
||||||
|
}
|
||||||
|
Ok(None) => {
|
||||||
|
// Event can't be parsed, because we don't have enough bytes for
|
||||||
|
// the current sequence. Keep the buffer and process next bytes.
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
// Event can't be parsed (not enough parameters, parameter is not a number, ...).
|
||||||
|
// Clear the buffer and continue with another sequence.
|
||||||
|
self.buffer.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Iterator for Parser {
|
||||||
|
type Item = InternalEvent;
|
||||||
|
|
||||||
|
fn next(&mut self) -> Option<Self::Item> {
|
||||||
|
self.internal_events.pop_front()
|
||||||
|
}
|
||||||
|
}
|
100
src/event/source/windows.rs
Normal file
100
src/event/source/windows.rs
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use crossterm_winapi::{Console, Handle, InputRecord};
|
||||||
|
|
||||||
|
use crate::event::{
|
||||||
|
sys::windows::{parse::MouseButtonsPressed, poll::WinApiPoll},
|
||||||
|
Event,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[cfg(feature = "event-stream")]
|
||||||
|
use crate::event::sys::Waker;
|
||||||
|
use crate::event::{
|
||||||
|
source::EventSource,
|
||||||
|
sys::windows::parse::{handle_key_event, handle_mouse_event},
|
||||||
|
timeout::PollTimeout,
|
||||||
|
InternalEvent,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub(crate) struct WindowsEventSource {
|
||||||
|
console: Console,
|
||||||
|
poll: WinApiPoll,
|
||||||
|
surrogate_buffer: Option<u16>,
|
||||||
|
mouse_buttons_pressed: MouseButtonsPressed,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WindowsEventSource {
|
||||||
|
pub(crate) fn new() -> std::io::Result<WindowsEventSource> {
|
||||||
|
let console = Console::from(Handle::current_in_handle()?);
|
||||||
|
Ok(WindowsEventSource {
|
||||||
|
console,
|
||||||
|
|
||||||
|
#[cfg(not(feature = "event-stream"))]
|
||||||
|
poll: WinApiPoll::new(),
|
||||||
|
#[cfg(feature = "event-stream")]
|
||||||
|
poll: WinApiPoll::new()?,
|
||||||
|
|
||||||
|
surrogate_buffer: None,
|
||||||
|
mouse_buttons_pressed: MouseButtonsPressed::default(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EventSource for WindowsEventSource {
|
||||||
|
fn try_read(&mut self, timeout: Option<Duration>) -> std::io::Result<Option<InternalEvent>> {
|
||||||
|
let poll_timeout = PollTimeout::new(timeout);
|
||||||
|
|
||||||
|
loop {
|
||||||
|
if let Some(event_ready) = self.poll.poll(poll_timeout.leftover())? {
|
||||||
|
let number = self.console.number_of_console_input_events()?;
|
||||||
|
if event_ready && number != 0 {
|
||||||
|
let event = match self.console.read_single_input_event()? {
|
||||||
|
InputRecord::KeyEvent(record) => {
|
||||||
|
handle_key_event(record, &mut self.surrogate_buffer)
|
||||||
|
}
|
||||||
|
InputRecord::MouseEvent(record) => {
|
||||||
|
let mouse_event =
|
||||||
|
handle_mouse_event(record, &self.mouse_buttons_pressed);
|
||||||
|
self.mouse_buttons_pressed = MouseButtonsPressed {
|
||||||
|
left: record.button_state.left_button(),
|
||||||
|
right: record.button_state.right_button(),
|
||||||
|
middle: record.button_state.middle_button(),
|
||||||
|
};
|
||||||
|
|
||||||
|
mouse_event
|
||||||
|
}
|
||||||
|
InputRecord::WindowBufferSizeEvent(record) => {
|
||||||
|
// windows starts counting at 0, unix at 1, add one to replicate unix behaviour.
|
||||||
|
Some(Event::Resize(
|
||||||
|
record.size.x as u16 + 1,
|
||||||
|
record.size.y as u16 + 1,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
InputRecord::FocusEvent(record) => {
|
||||||
|
let event = if record.set_focus {
|
||||||
|
Event::FocusGained
|
||||||
|
} else {
|
||||||
|
Event::FocusLost
|
||||||
|
};
|
||||||
|
Some(event)
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(event) = event {
|
||||||
|
return Ok(Some(InternalEvent::Event(event)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if poll_timeout.elapsed() {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "event-stream")]
|
||||||
|
fn waker(&self) -> Waker {
|
||||||
|
self.poll.waker()
|
||||||
|
}
|
||||||
|
}
|
@ -1 +1,9 @@
|
|||||||
|
#[cfg(all(unix, feature = "event-stream"))]
|
||||||
|
pub(crate) use unix::waker::Waker;
|
||||||
|
#[cfg(all(windows, feature = "event-stream"))]
|
||||||
|
pub(crate) use windows::waker::Waker;
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
pub(crate) mod unix;
|
pub(crate) mod unix;
|
||||||
|
#[cfg(windows)]
|
||||||
|
pub(crate) mod windows;
|
||||||
|
@ -1,2 +1,5 @@
|
|||||||
|
#[cfg(feature = "event-stream")]
|
||||||
|
pub(crate) mod waker;
|
||||||
|
|
||||||
#[cfg(feature = "events")]
|
#[cfg(feature = "events")]
|
||||||
pub(crate) mod parse;
|
pub(crate) mod parse;
|
||||||
|
@ -5,6 +5,8 @@ use crate::event::{
|
|||||||
MediaKeyCode, ModifierKeyCode, MouseButton, MouseEvent, MouseEventKind,
|
MediaKeyCode, ModifierKeyCode, MouseButton, MouseEvent, MouseEventKind,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use super::super::super::InternalEvent;
|
||||||
|
|
||||||
// Event parsing
|
// Event parsing
|
||||||
//
|
//
|
||||||
// This code (& previous one) are kind of ugly. We have to think about this,
|
// This code (& previous one) are kind of ugly. We have to think about this,
|
||||||
@ -21,7 +23,10 @@ fn could_not_parse_event_error() -> io::Error {
|
|||||||
io::Error::new(io::ErrorKind::Other, "Could not parse an event.")
|
io::Error::new(io::ErrorKind::Other, "Could not parse an event.")
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn parse_event(buffer: &[u8], input_available: bool) -> io::Result<Option<Event>> {
|
pub(crate) fn parse_event(
|
||||||
|
buffer: &[u8],
|
||||||
|
input_available: bool,
|
||||||
|
) -> io::Result<Option<InternalEvent>> {
|
||||||
if buffer.is_empty() {
|
if buffer.is_empty() {
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
}
|
}
|
||||||
@ -33,7 +38,7 @@ pub(crate) fn parse_event(buffer: &[u8], input_available: bool) -> io::Result<Op
|
|||||||
// Possible Esc sequence
|
// Possible Esc sequence
|
||||||
Ok(None)
|
Ok(None)
|
||||||
} else {
|
} else {
|
||||||
Ok(Some(Event::Key(KeyCode::Esc.into())))
|
Ok(Some(InternalEvent::Event(Event::Key(KeyCode::Esc.into()))))
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
match buffer[1] {
|
match buffer[1] {
|
||||||
@ -42,28 +47,40 @@ pub(crate) fn parse_event(buffer: &[u8], input_available: bool) -> io::Result<Op
|
|||||||
Ok(None)
|
Ok(None)
|
||||||
} else {
|
} else {
|
||||||
match buffer[2] {
|
match buffer[2] {
|
||||||
b'D' => Ok(Some(Event::Key(KeyCode::Left.into()))),
|
b'D' => {
|
||||||
b'C' => Ok(Some(Event::Key(KeyCode::Right.into()))),
|
Ok(Some(InternalEvent::Event(Event::Key(KeyCode::Left.into()))))
|
||||||
b'A' => Ok(Some(Event::Key(KeyCode::Up.into()))),
|
|
||||||
b'B' => Ok(Some(Event::Key(KeyCode::Down.into()))),
|
|
||||||
b'H' => Ok(Some(Event::Key(KeyCode::Home.into()))),
|
|
||||||
b'F' => Ok(Some(Event::Key(KeyCode::End.into()))),
|
|
||||||
// F1-F4
|
|
||||||
val @ b'P'..=b'S' => {
|
|
||||||
Ok(Some(Event::Key(KeyCode::F(1 + val - b'P').into())))
|
|
||||||
}
|
}
|
||||||
|
b'C' => Ok(Some(InternalEvent::Event(Event::Key(
|
||||||
|
KeyCode::Right.into(),
|
||||||
|
)))),
|
||||||
|
b'A' => {
|
||||||
|
Ok(Some(InternalEvent::Event(Event::Key(KeyCode::Up.into()))))
|
||||||
|
}
|
||||||
|
b'B' => {
|
||||||
|
Ok(Some(InternalEvent::Event(Event::Key(KeyCode::Down.into()))))
|
||||||
|
}
|
||||||
|
b'H' => {
|
||||||
|
Ok(Some(InternalEvent::Event(Event::Key(KeyCode::Home.into()))))
|
||||||
|
}
|
||||||
|
b'F' => {
|
||||||
|
Ok(Some(InternalEvent::Event(Event::Key(KeyCode::End.into()))))
|
||||||
|
}
|
||||||
|
// F1-F4
|
||||||
|
val @ b'P'..=b'S' => Ok(Some(InternalEvent::Event(Event::Key(
|
||||||
|
KeyCode::F(1 + val - b'P').into(),
|
||||||
|
)))),
|
||||||
_ => Err(could_not_parse_event_error()),
|
_ => Err(could_not_parse_event_error()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
b'[' => parse_csi(buffer),
|
b'[' => parse_csi(buffer),
|
||||||
b'\x1B' => Ok(Some(Event::Key(KeyCode::Esc.into()))),
|
b'\x1B' => Ok(Some(InternalEvent::Event(Event::Key(KeyCode::Esc.into())))),
|
||||||
_ => parse_event(&buffer[1..], input_available).map(|event_option| {
|
_ => parse_event(&buffer[1..], input_available).map(|event_option| {
|
||||||
event_option.map(|event| {
|
event_option.map(|event| {
|
||||||
if let Event::Key(key_event) = event {
|
if let InternalEvent::Event(Event::Key(key_event)) = event {
|
||||||
let mut alt_key_event = key_event;
|
let mut alt_key_event = key_event;
|
||||||
alt_key_event.modifiers |= KeyModifiers::ALT;
|
alt_key_event.modifiers |= KeyModifiers::ALT;
|
||||||
Event::Key(alt_key_event)
|
InternalEvent::Event(Event::Key(alt_key_event))
|
||||||
} else {
|
} else {
|
||||||
event
|
event
|
||||||
}
|
}
|
||||||
@ -72,26 +89,38 @@ pub(crate) fn parse_event(buffer: &[u8], input_available: bool) -> io::Result<Op
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
b'\r' => Ok(Some(Event::Key(KeyCode::Enter.into()))),
|
b'\r' => Ok(Some(InternalEvent::Event(Event::Key(
|
||||||
b'\t' => Ok(Some(Event::Key(KeyCode::Tab.into()))),
|
KeyCode::Enter.into(),
|
||||||
b'\x7F' => Ok(Some(Event::Key(KeyCode::Backspace.into()))),
|
)))),
|
||||||
c @ b'\x01'..=b'\x1A' => Ok(Some(Event::Key(KeyEvent::new(
|
// Issue #371: \n = 0xA, which is also the keycode for Ctrl+J. The only reason we get
|
||||||
|
// newlines as input is because the terminal converts \r into \n for us. When we
|
||||||
|
// enter raw mode, we disable that, so \n no longer has any meaning - it's better to
|
||||||
|
// use Ctrl+J. Waiting to handle it here means it gets picked up later
|
||||||
|
b'\n' if !crate::terminal::sys::is_raw_mode_enabled() => Ok(Some(InternalEvent::Event(
|
||||||
|
Event::Key(KeyCode::Enter.into()),
|
||||||
|
))),
|
||||||
|
b'\t' => Ok(Some(InternalEvent::Event(Event::Key(KeyCode::Tab.into())))),
|
||||||
|
b'\x7F' => Ok(Some(InternalEvent::Event(Event::Key(
|
||||||
|
KeyCode::Backspace.into(),
|
||||||
|
)))),
|
||||||
|
c @ b'\x01'..=b'\x1A' => Ok(Some(InternalEvent::Event(Event::Key(KeyEvent::new(
|
||||||
KeyCode::Char((c - 0x1 + b'a') as char),
|
KeyCode::Char((c - 0x1 + b'a') as char),
|
||||||
KeyModifiers::CONTROL,
|
KeyModifiers::CONTROL,
|
||||||
)))),
|
))))),
|
||||||
c @ b'\x1C'..=b'\x1F' => Ok(Some(Event::Key(KeyEvent::new(
|
c @ b'\x1C'..=b'\x1F' => Ok(Some(InternalEvent::Event(Event::Key(KeyEvent::new(
|
||||||
KeyCode::Char((c - 0x1C + b'4') as char),
|
KeyCode::Char((c - 0x1C + b'4') as char),
|
||||||
KeyModifiers::CONTROL,
|
KeyModifiers::CONTROL,
|
||||||
)))),
|
))))),
|
||||||
b'\0' => Ok(Some(Event::Key(KeyEvent::new(
|
b'\0' => Ok(Some(InternalEvent::Event(Event::Key(KeyEvent::new(
|
||||||
KeyCode::Char(' '),
|
KeyCode::Char(' '),
|
||||||
KeyModifiers::CONTROL,
|
KeyModifiers::CONTROL,
|
||||||
)))),
|
))))),
|
||||||
_ => parse_utf8_char(buffer).map(|maybe_char| {
|
_ => parse_utf8_char(buffer).map(|maybe_char| {
|
||||||
maybe_char
|
maybe_char
|
||||||
.map(KeyCode::Char)
|
.map(KeyCode::Char)
|
||||||
.map(char_code_to_event)
|
.map(char_code_to_event)
|
||||||
.map(Event::Key)
|
.map(Event::Key)
|
||||||
|
.map(InternalEvent::Event)
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -105,7 +134,7 @@ fn char_code_to_event(code: KeyCode) -> KeyEvent {
|
|||||||
KeyEvent::new(code, modifiers)
|
KeyEvent::new(code, modifiers)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn parse_csi(buffer: &[u8]) -> io::Result<Option<Event>> {
|
pub(crate) fn parse_csi(buffer: &[u8]) -> io::Result<Option<InternalEvent>> {
|
||||||
assert!(buffer.starts_with(&[b'\x1B', b'['])); // ESC [
|
assert!(buffer.starts_with(&[b'\x1B', b'['])); // ESC [
|
||||||
|
|
||||||
if buffer.len() == 2 {
|
if buffer.len() == 2 {
|
||||||
@ -181,7 +210,7 @@ pub(crate) fn parse_csi(buffer: &[u8]) -> io::Result<Option<Event>> {
|
|||||||
_ => return Err(could_not_parse_event_error()),
|
_ => return Err(could_not_parse_event_error()),
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(input_event)
|
Ok(input_event.map(InternalEvent::Event))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn next_parsed<T>(iter: &mut dyn Iterator<Item = &str>) -> io::Result<T>
|
pub(crate) fn next_parsed<T>(iter: &mut dyn Iterator<Item = &str>) -> io::Result<T>
|
||||||
@ -209,7 +238,7 @@ fn modifier_and_kind_parsed(iter: &mut dyn Iterator<Item = &str>) -> io::Result<
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn parse_csi_cursor_position(buffer: &[u8]) -> io::Result<Option<Event>> {
|
pub(crate) fn parse_csi_cursor_position(buffer: &[u8]) -> io::Result<Option<InternalEvent>> {
|
||||||
// ESC [ Cy ; Cx R
|
// ESC [ Cy ; Cx R
|
||||||
// Cy - cursor row number (starting from 1)
|
// Cy - cursor row number (starting from 1)
|
||||||
// Cx - cursor column number (starting from 1)
|
// Cx - cursor column number (starting from 1)
|
||||||
@ -224,10 +253,10 @@ pub(crate) fn parse_csi_cursor_position(buffer: &[u8]) -> io::Result<Option<Even
|
|||||||
let y = next_parsed::<u16>(&mut split)? - 1;
|
let y = next_parsed::<u16>(&mut split)? - 1;
|
||||||
let x = next_parsed::<u16>(&mut split)? - 1;
|
let x = next_parsed::<u16>(&mut split)? - 1;
|
||||||
|
|
||||||
Ok(Some(Event::CursorPosition(x, y)))
|
Ok(Some(InternalEvent::CursorPosition(x, y)))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_csi_keyboard_enhancement_flags(buffer: &[u8]) -> io::Result<Option<Event>> {
|
fn parse_csi_keyboard_enhancement_flags(buffer: &[u8]) -> io::Result<Option<InternalEvent>> {
|
||||||
// ESC [ ? flags u
|
// ESC [ ? flags u
|
||||||
assert!(buffer.starts_with(&[b'\x1B', b'[', b'?'])); // ESC [ ?
|
assert!(buffer.starts_with(&[b'\x1B', b'[', b'?'])); // ESC [ ?
|
||||||
assert!(buffer.ends_with(&[b'u']));
|
assert!(buffer.ends_with(&[b'u']));
|
||||||
@ -256,10 +285,10 @@ fn parse_csi_keyboard_enhancement_flags(buffer: &[u8]) -> io::Result<Option<Even
|
|||||||
// flags |= KeyboardEnhancementFlags::REPORT_ASSOCIATED_TEXT;
|
// flags |= KeyboardEnhancementFlags::REPORT_ASSOCIATED_TEXT;
|
||||||
// }
|
// }
|
||||||
|
|
||||||
Ok(Some(Event::KeyboardEnhancementFlags(flags)))
|
Ok(Some(InternalEvent::KeyboardEnhancementFlags(flags)))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_csi_primary_device_attributes(buffer: &[u8]) -> io::Result<Option<Event>> {
|
fn parse_csi_primary_device_attributes(buffer: &[u8]) -> io::Result<Option<InternalEvent>> {
|
||||||
// ESC [ 64 ; attr1 ; attr2 ; ... ; attrn ; c
|
// ESC [ 64 ; attr1 ; attr2 ; ... ; attrn ; c
|
||||||
assert!(buffer.starts_with(&[b'\x1B', b'[', b'?']));
|
assert!(buffer.starts_with(&[b'\x1B', b'[', b'?']));
|
||||||
assert!(buffer.ends_with(&[b'c']));
|
assert!(buffer.ends_with(&[b'c']));
|
||||||
@ -268,7 +297,7 @@ fn parse_csi_primary_device_attributes(buffer: &[u8]) -> io::Result<Option<Event
|
|||||||
// exposed in the crossterm API so we don't need to parse the individual attributes yet.
|
// exposed in the crossterm API so we don't need to parse the individual attributes yet.
|
||||||
// See <https://vt100.net/docs/vt510-rm/DA1.html>
|
// See <https://vt100.net/docs/vt510-rm/DA1.html>
|
||||||
|
|
||||||
Ok(Some(Event::PrimaryDeviceAttributes))
|
Ok(Some(InternalEvent::PrimaryDeviceAttributes))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_modifiers(mask: u8) -> KeyModifiers {
|
fn parse_modifiers(mask: u8) -> KeyModifiers {
|
||||||
@ -316,7 +345,7 @@ fn parse_key_event_kind(kind: u8) -> KeyEventKind {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn parse_csi_modifier_key_code(buffer: &[u8]) -> io::Result<Option<Event>> {
|
pub(crate) fn parse_csi_modifier_key_code(buffer: &[u8]) -> io::Result<Option<InternalEvent>> {
|
||||||
assert!(buffer.starts_with(&[b'\x1B', b'['])); // ESC [
|
assert!(buffer.starts_with(&[b'\x1B', b'['])); // ESC [
|
||||||
//
|
//
|
||||||
let s = std::str::from_utf8(&buffer[2..buffer.len() - 1])
|
let s = std::str::from_utf8(&buffer[2..buffer.len() - 1])
|
||||||
@ -361,7 +390,7 @@ pub(crate) fn parse_csi_modifier_key_code(buffer: &[u8]) -> io::Result<Option<Ev
|
|||||||
|
|
||||||
let input_event = Event::Key(KeyEvent::new_with_kind(keycode, modifiers, kind));
|
let input_event = Event::Key(KeyEvent::new_with_kind(keycode, modifiers, kind));
|
||||||
|
|
||||||
Ok(Some(input_event))
|
Ok(Some(InternalEvent::Event(input_event)))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn translate_functional_key_code(codepoint: u32) -> Option<(KeyCode, KeyEventState)> {
|
fn translate_functional_key_code(codepoint: u32) -> Option<(KeyCode, KeyEventState)> {
|
||||||
@ -465,7 +494,7 @@ fn translate_functional_key_code(codepoint: u32) -> Option<(KeyCode, KeyEventSta
|
|||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn parse_csi_u_encoded_key_code(buffer: &[u8]) -> io::Result<Option<Event>> {
|
pub(crate) fn parse_csi_u_encoded_key_code(buffer: &[u8]) -> io::Result<Option<InternalEvent>> {
|
||||||
assert!(buffer.starts_with(&[b'\x1B', b'['])); // ESC [
|
assert!(buffer.starts_with(&[b'\x1B', b'['])); // ESC [
|
||||||
assert!(buffer.ends_with(&[b'u']));
|
assert!(buffer.ends_with(&[b'u']));
|
||||||
|
|
||||||
@ -516,6 +545,11 @@ pub(crate) fn parse_csi_u_encoded_key_code(buffer: &[u8]) -> io::Result<Option<E
|
|||||||
match c {
|
match c {
|
||||||
'\x1B' => KeyCode::Esc,
|
'\x1B' => KeyCode::Esc,
|
||||||
'\r' => KeyCode::Enter,
|
'\r' => KeyCode::Enter,
|
||||||
|
// Issue #371: \n = 0xA, which is also the keycode for Ctrl+J. The only reason we get
|
||||||
|
// newlines as input is because the terminal converts \r into \n for us. When we
|
||||||
|
// enter raw mode, we disable that, so \n no longer has any meaning - it's better to
|
||||||
|
// use Ctrl+J. Waiting to handle it here means it gets picked up later
|
||||||
|
'\n' if !crate::terminal::sys::is_raw_mode_enabled() => KeyCode::Enter,
|
||||||
'\t' => {
|
'\t' => {
|
||||||
if modifiers.contains(KeyModifiers::SHIFT) {
|
if modifiers.contains(KeyModifiers::SHIFT) {
|
||||||
KeyCode::BackTab
|
KeyCode::BackTab
|
||||||
@ -579,10 +613,10 @@ pub(crate) fn parse_csi_u_encoded_key_code(buffer: &[u8]) -> io::Result<Option<E
|
|||||||
state_from_keycode | state_from_modifiers,
|
state_from_keycode | state_from_modifiers,
|
||||||
));
|
));
|
||||||
|
|
||||||
Ok(Some(input_event))
|
Ok(Some(InternalEvent::Event(input_event)))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn parse_csi_special_key_code(buffer: &[u8]) -> io::Result<Option<Event>> {
|
pub(crate) fn parse_csi_special_key_code(buffer: &[u8]) -> io::Result<Option<InternalEvent>> {
|
||||||
assert!(buffer.starts_with(&[b'\x1B', b'['])); // ESC [
|
assert!(buffer.starts_with(&[b'\x1B', b'['])); // ESC [
|
||||||
assert!(buffer.ends_with(&[b'~']));
|
assert!(buffer.ends_with(&[b'~']));
|
||||||
|
|
||||||
@ -623,10 +657,10 @@ pub(crate) fn parse_csi_special_key_code(buffer: &[u8]) -> io::Result<Option<Eve
|
|||||||
keycode, modifiers, kind, state,
|
keycode, modifiers, kind, state,
|
||||||
));
|
));
|
||||||
|
|
||||||
Ok(Some(input_event))
|
Ok(Some(InternalEvent::Event(input_event)))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn parse_csi_rxvt_mouse(buffer: &[u8]) -> io::Result<Option<Event>> {
|
pub(crate) fn parse_csi_rxvt_mouse(buffer: &[u8]) -> io::Result<Option<InternalEvent>> {
|
||||||
// rxvt mouse encoding:
|
// rxvt mouse encoding:
|
||||||
// ESC [ Cb ; Cx ; Cy ; M
|
// ESC [ Cb ; Cx ; Cy ; M
|
||||||
|
|
||||||
@ -645,15 +679,15 @@ pub(crate) fn parse_csi_rxvt_mouse(buffer: &[u8]) -> io::Result<Option<Event>> {
|
|||||||
let cx = next_parsed::<u16>(&mut split)? - 1;
|
let cx = next_parsed::<u16>(&mut split)? - 1;
|
||||||
let cy = next_parsed::<u16>(&mut split)? - 1;
|
let cy = next_parsed::<u16>(&mut split)? - 1;
|
||||||
|
|
||||||
Ok(Some(Event::Mouse(MouseEvent {
|
Ok(Some(InternalEvent::Event(Event::Mouse(MouseEvent {
|
||||||
kind,
|
kind,
|
||||||
column: cx,
|
column: cx,
|
||||||
row: cy,
|
row: cy,
|
||||||
modifiers,
|
modifiers,
|
||||||
})))
|
}))))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn parse_csi_normal_mouse(buffer: &[u8]) -> io::Result<Option<Event>> {
|
pub(crate) fn parse_csi_normal_mouse(buffer: &[u8]) -> io::Result<Option<InternalEvent>> {
|
||||||
// Normal mouse encoding: ESC [ M CB Cx Cy (6 characters only).
|
// Normal mouse encoding: ESC [ M CB Cx Cy (6 characters only).
|
||||||
|
|
||||||
assert!(buffer.starts_with(&[b'\x1B', b'[', b'M'])); // ESC [ M
|
assert!(buffer.starts_with(&[b'\x1B', b'[', b'M'])); // ESC [ M
|
||||||
@ -673,15 +707,15 @@ pub(crate) fn parse_csi_normal_mouse(buffer: &[u8]) -> io::Result<Option<Event>>
|
|||||||
let cx = u16::from(buffer[4].saturating_sub(32)) - 1;
|
let cx = u16::from(buffer[4].saturating_sub(32)) - 1;
|
||||||
let cy = u16::from(buffer[5].saturating_sub(32)) - 1;
|
let cy = u16::from(buffer[5].saturating_sub(32)) - 1;
|
||||||
|
|
||||||
Ok(Some(Event::Mouse(MouseEvent {
|
Ok(Some(InternalEvent::Event(Event::Mouse(MouseEvent {
|
||||||
kind,
|
kind,
|
||||||
column: cx,
|
column: cx,
|
||||||
row: cy,
|
row: cy,
|
||||||
modifiers,
|
modifiers,
|
||||||
})))
|
}))))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn parse_csi_sgr_mouse(buffer: &[u8]) -> io::Result<Option<Event>> {
|
pub(crate) fn parse_csi_sgr_mouse(buffer: &[u8]) -> io::Result<Option<InternalEvent>> {
|
||||||
// ESC [ < Cb ; Cx ; Cy (;) (M or m)
|
// ESC [ < Cb ; Cx ; Cy (;) (M or m)
|
||||||
|
|
||||||
assert!(buffer.starts_with(&[b'\x1B', b'[', b'<'])); // ESC [ <
|
assert!(buffer.starts_with(&[b'\x1B', b'[', b'<'])); // ESC [ <
|
||||||
@ -718,12 +752,12 @@ pub(crate) fn parse_csi_sgr_mouse(buffer: &[u8]) -> io::Result<Option<Event>> {
|
|||||||
kind
|
kind
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(Some(Event::Mouse(MouseEvent {
|
Ok(Some(InternalEvent::Event(Event::Mouse(MouseEvent {
|
||||||
kind,
|
kind,
|
||||||
column: cx,
|
column: cx,
|
||||||
row: cy,
|
row: cy,
|
||||||
modifiers,
|
modifiers,
|
||||||
})))
|
}))))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Cb is the byte of a mouse input that contains the button being used, the key modifiers being
|
/// Cb is the byte of a mouse input that contains the button being used, the key modifiers being
|
||||||
@ -776,7 +810,7 @@ fn parse_cb(cb: u8) -> io::Result<(MouseEventKind, KeyModifiers)> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "bracketed-paste")]
|
#[cfg(feature = "bracketed-paste")]
|
||||||
pub(crate) fn parse_csi_bracketed_paste(buffer: &[u8]) -> io::Result<Option<Event>> {
|
pub(crate) fn parse_csi_bracketed_paste(buffer: &[u8]) -> io::Result<Option<InternalEvent>> {
|
||||||
// ESC [ 2 0 0 ~ pasted text ESC 2 0 1 ~
|
// ESC [ 2 0 0 ~ pasted text ESC 2 0 1 ~
|
||||||
assert!(buffer.starts_with(b"\x1B[200~"));
|
assert!(buffer.starts_with(b"\x1B[200~"));
|
||||||
|
|
||||||
@ -784,7 +818,7 @@ pub(crate) fn parse_csi_bracketed_paste(buffer: &[u8]) -> io::Result<Option<Even
|
|||||||
Ok(None)
|
Ok(None)
|
||||||
} else {
|
} else {
|
||||||
let paste = String::from_utf8_lossy(&buffer[6..buffer.len() - 6]).to_string();
|
let paste = String::from_utf8_lossy(&buffer[6..buffer.len() - 6]).to_string();
|
||||||
Ok(Some(Event::Paste(paste)))
|
Ok(Some(InternalEvent::Event(Event::Paste(paste))))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -837,7 +871,7 @@ mod tests {
|
|||||||
fn test_esc_key() {
|
fn test_esc_key() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
parse_event(b"\x1B", false).unwrap(),
|
parse_event(b"\x1B", false).unwrap(),
|
||||||
Some(Event::Key(KeyCode::Esc.into())),
|
Some(InternalEvent::Event(Event::Key(KeyCode::Esc.into()))),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -850,10 +884,10 @@ mod tests {
|
|||||||
fn test_alt_key() {
|
fn test_alt_key() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
parse_event(b"\x1Bc", false).unwrap(),
|
parse_event(b"\x1Bc", false).unwrap(),
|
||||||
Some(Event::Key(KeyEvent::new(
|
Some(InternalEvent::Event(Event::Key(KeyEvent::new(
|
||||||
KeyCode::Char('c'),
|
KeyCode::Char('c'),
|
||||||
KeyModifiers::ALT
|
KeyModifiers::ALT
|
||||||
))),
|
)))),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -861,10 +895,10 @@ mod tests {
|
|||||||
fn test_alt_shift() {
|
fn test_alt_shift() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
parse_event(b"\x1BH", false).unwrap(),
|
parse_event(b"\x1BH", false).unwrap(),
|
||||||
Some(Event::Key(KeyEvent::new(
|
Some(InternalEvent::Event(Event::Key(KeyEvent::new(
|
||||||
KeyCode::Char('H'),
|
KeyCode::Char('H'),
|
||||||
KeyModifiers::ALT | KeyModifiers::SHIFT
|
KeyModifiers::ALT | KeyModifiers::SHIFT
|
||||||
))),
|
)))),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -872,10 +906,10 @@ mod tests {
|
|||||||
fn test_alt_ctrl() {
|
fn test_alt_ctrl() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
parse_event(b"\x1B\x14", false).unwrap(),
|
parse_event(b"\x1B\x14", false).unwrap(),
|
||||||
Some(Event::Key(KeyEvent::new(
|
Some(InternalEvent::Event(Event::Key(KeyEvent::new(
|
||||||
KeyCode::Char('t'),
|
KeyCode::Char('t'),
|
||||||
KeyModifiers::ALT | KeyModifiers::CONTROL
|
KeyModifiers::ALT | KeyModifiers::CONTROL
|
||||||
))),
|
)))),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -887,77 +921,79 @@ mod tests {
|
|||||||
// parse_csi_cursor_position
|
// parse_csi_cursor_position
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
parse_event(b"\x1B[20;10R", false).unwrap(),
|
parse_event(b"\x1B[20;10R", false).unwrap(),
|
||||||
Some(Event::CursorPosition(9, 19))
|
Some(InternalEvent::CursorPosition(9, 19))
|
||||||
);
|
);
|
||||||
|
|
||||||
// parse_csi
|
// parse_csi
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
parse_event(b"\x1B[D", false).unwrap(),
|
parse_event(b"\x1B[D", false).unwrap(),
|
||||||
Some(Event::Key(KeyCode::Left.into())),
|
Some(InternalEvent::Event(Event::Key(KeyCode::Left.into()))),
|
||||||
);
|
);
|
||||||
|
|
||||||
// parse_csi_modifier_key_code
|
// parse_csi_modifier_key_code
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
parse_event(b"\x1B[2D", false).unwrap(),
|
parse_event(b"\x1B[2D", false).unwrap(),
|
||||||
Some(Event::Key(KeyEvent::new(
|
Some(InternalEvent::Event(Event::Key(KeyEvent::new(
|
||||||
KeyCode::Left,
|
KeyCode::Left,
|
||||||
KeyModifiers::SHIFT
|
KeyModifiers::SHIFT
|
||||||
)))
|
))))
|
||||||
);
|
);
|
||||||
|
|
||||||
// parse_csi_special_key_code
|
// parse_csi_special_key_code
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
parse_event(b"\x1B[3~", false).unwrap(),
|
parse_event(b"\x1B[3~", false).unwrap(),
|
||||||
Some(Event::Key(KeyCode::Delete.into())),
|
Some(InternalEvent::Event(Event::Key(KeyCode::Delete.into()))),
|
||||||
);
|
);
|
||||||
|
|
||||||
// parse_csi_bracketed_paste
|
// parse_csi_bracketed_paste
|
||||||
#[cfg(feature = "bracketed-paste")]
|
#[cfg(feature = "bracketed-paste")]
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
parse_event(b"\x1B[200~on and on and on\x1B[201~", false).unwrap(),
|
parse_event(b"\x1B[200~on and on and on\x1B[201~", false).unwrap(),
|
||||||
Some(Event::Paste("on and on and on".to_string())),
|
Some(InternalEvent::Event(Event::Paste(
|
||||||
|
"on and on and on".to_string()
|
||||||
|
))),
|
||||||
);
|
);
|
||||||
|
|
||||||
// parse_csi_rxvt_mouse
|
// parse_csi_rxvt_mouse
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
parse_event(b"\x1B[32;30;40;M", false).unwrap(),
|
parse_event(b"\x1B[32;30;40;M", false).unwrap(),
|
||||||
Some(Event::Mouse(MouseEvent {
|
Some(InternalEvent::Event(Event::Mouse(MouseEvent {
|
||||||
kind: MouseEventKind::Down(MouseButton::Left),
|
kind: MouseEventKind::Down(MouseButton::Left),
|
||||||
column: 29,
|
column: 29,
|
||||||
row: 39,
|
row: 39,
|
||||||
modifiers: KeyModifiers::empty(),
|
modifiers: KeyModifiers::empty(),
|
||||||
}))
|
})))
|
||||||
);
|
);
|
||||||
|
|
||||||
// parse_csi_normal_mouse
|
// parse_csi_normal_mouse
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
parse_event(b"\x1B[M0\x60\x70", false).unwrap(),
|
parse_event(b"\x1B[M0\x60\x70", false).unwrap(),
|
||||||
Some(Event::Mouse(MouseEvent {
|
Some(InternalEvent::Event(Event::Mouse(MouseEvent {
|
||||||
kind: MouseEventKind::Down(MouseButton::Left),
|
kind: MouseEventKind::Down(MouseButton::Left),
|
||||||
column: 63,
|
column: 63,
|
||||||
row: 79,
|
row: 79,
|
||||||
modifiers: KeyModifiers::CONTROL,
|
modifiers: KeyModifiers::CONTROL,
|
||||||
}))
|
})))
|
||||||
);
|
);
|
||||||
|
|
||||||
// parse_csi_sgr_mouse
|
// parse_csi_sgr_mouse
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
parse_event(b"\x1B[<0;20;10;M", false).unwrap(),
|
parse_event(b"\x1B[<0;20;10;M", false).unwrap(),
|
||||||
Some(Event::Mouse(MouseEvent {
|
Some(InternalEvent::Event(Event::Mouse(MouseEvent {
|
||||||
kind: MouseEventKind::Down(MouseButton::Left),
|
kind: MouseEventKind::Down(MouseButton::Left),
|
||||||
column: 19,
|
column: 19,
|
||||||
row: 9,
|
row: 9,
|
||||||
modifiers: KeyModifiers::empty(),
|
modifiers: KeyModifiers::empty(),
|
||||||
}))
|
})))
|
||||||
);
|
);
|
||||||
|
|
||||||
// parse_utf8_char
|
// parse_utf8_char
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
parse_event("Ž".as_bytes(), false).unwrap(),
|
parse_event("Ž".as_bytes(), false).unwrap(),
|
||||||
Some(Event::Key(KeyEvent::new(
|
Some(InternalEvent::Event(Event::Key(KeyEvent::new(
|
||||||
KeyCode::Char('Ž'),
|
KeyCode::Char('Ž'),
|
||||||
KeyModifiers::SHIFT
|
KeyModifiers::SHIFT
|
||||||
))),
|
)))),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -965,7 +1001,7 @@ mod tests {
|
|||||||
fn test_parse_event() {
|
fn test_parse_event() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
parse_event(b"\t", false).unwrap(),
|
parse_event(b"\t", false).unwrap(),
|
||||||
Some(Event::Key(KeyCode::Tab.into())),
|
Some(InternalEvent::Event(Event::Key(KeyCode::Tab.into()))),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -973,7 +1009,7 @@ mod tests {
|
|||||||
fn test_parse_csi_cursor_position() {
|
fn test_parse_csi_cursor_position() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
parse_csi_cursor_position(b"\x1B[20;10R").unwrap(),
|
parse_csi_cursor_position(b"\x1B[20;10R").unwrap(),
|
||||||
Some(Event::CursorPosition(9, 19))
|
Some(InternalEvent::CursorPosition(9, 19))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -981,7 +1017,7 @@ mod tests {
|
|||||||
fn test_parse_csi() {
|
fn test_parse_csi() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
parse_csi(b"\x1B[D").unwrap(),
|
parse_csi(b"\x1B[D").unwrap(),
|
||||||
Some(Event::Key(KeyCode::Left.into())),
|
Some(InternalEvent::Event(Event::Key(KeyCode::Left.into()))),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -989,10 +1025,10 @@ mod tests {
|
|||||||
fn test_parse_csi_modifier_key_code() {
|
fn test_parse_csi_modifier_key_code() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
parse_csi_modifier_key_code(b"\x1B[2D").unwrap(),
|
parse_csi_modifier_key_code(b"\x1B[2D").unwrap(),
|
||||||
Some(Event::Key(KeyEvent::new(
|
Some(InternalEvent::Event(Event::Key(KeyEvent::new(
|
||||||
KeyCode::Left,
|
KeyCode::Left,
|
||||||
KeyModifiers::SHIFT
|
KeyModifiers::SHIFT
|
||||||
))),
|
)))),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1000,7 +1036,7 @@ mod tests {
|
|||||||
fn test_parse_csi_special_key_code() {
|
fn test_parse_csi_special_key_code() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
parse_csi_special_key_code(b"\x1B[3~").unwrap(),
|
parse_csi_special_key_code(b"\x1B[3~").unwrap(),
|
||||||
Some(Event::Key(KeyCode::Delete.into())),
|
Some(InternalEvent::Event(Event::Key(KeyCode::Delete.into()))),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1008,10 +1044,10 @@ mod tests {
|
|||||||
fn test_parse_csi_special_key_code_multiple_values_not_supported() {
|
fn test_parse_csi_special_key_code_multiple_values_not_supported() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
parse_csi_special_key_code(b"\x1B[3;2~").unwrap(),
|
parse_csi_special_key_code(b"\x1B[3;2~").unwrap(),
|
||||||
Some(Event::Key(KeyEvent::new(
|
Some(InternalEvent::Event(Event::Key(KeyEvent::new(
|
||||||
KeyCode::Delete,
|
KeyCode::Delete,
|
||||||
KeyModifiers::SHIFT
|
KeyModifiers::SHIFT
|
||||||
))),
|
)))),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1031,25 +1067,28 @@ mod tests {
|
|||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
parse_event(b"\x1B[200~o\x1B[2D\x1B[201~", false).unwrap(),
|
parse_event(b"\x1B[200~o\x1B[2D\x1B[201~", false).unwrap(),
|
||||||
Some(Event::Paste("o\x1B[2D".to_string()))
|
Some(InternalEvent::Event(Event::Paste("o\x1B[2D".to_string())))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_parse_csi_focus() {
|
fn test_parse_csi_focus() {
|
||||||
assert_eq!(parse_csi(b"\x1B[O").unwrap(), Some(Event::FocusLost));
|
assert_eq!(
|
||||||
|
parse_csi(b"\x1B[O").unwrap(),
|
||||||
|
Some(InternalEvent::Event(Event::FocusLost))
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_parse_csi_rxvt_mouse() {
|
fn test_parse_csi_rxvt_mouse() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
parse_csi_rxvt_mouse(b"\x1B[32;30;40;M").unwrap(),
|
parse_csi_rxvt_mouse(b"\x1B[32;30;40;M").unwrap(),
|
||||||
Some(Event::Mouse(MouseEvent {
|
Some(InternalEvent::Event(Event::Mouse(MouseEvent {
|
||||||
kind: MouseEventKind::Down(MouseButton::Left),
|
kind: MouseEventKind::Down(MouseButton::Left),
|
||||||
column: 29,
|
column: 29,
|
||||||
row: 39,
|
row: 39,
|
||||||
modifiers: KeyModifiers::empty(),
|
modifiers: KeyModifiers::empty(),
|
||||||
}))
|
})))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1057,12 +1096,12 @@ mod tests {
|
|||||||
fn test_parse_csi_normal_mouse() {
|
fn test_parse_csi_normal_mouse() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
parse_csi_normal_mouse(b"\x1B[M0\x60\x70").unwrap(),
|
parse_csi_normal_mouse(b"\x1B[M0\x60\x70").unwrap(),
|
||||||
Some(Event::Mouse(MouseEvent {
|
Some(InternalEvent::Event(Event::Mouse(MouseEvent {
|
||||||
kind: MouseEventKind::Down(MouseButton::Left),
|
kind: MouseEventKind::Down(MouseButton::Left),
|
||||||
column: 63,
|
column: 63,
|
||||||
row: 79,
|
row: 79,
|
||||||
modifiers: KeyModifiers::CONTROL,
|
modifiers: KeyModifiers::CONTROL,
|
||||||
}))
|
})))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1070,39 +1109,39 @@ mod tests {
|
|||||||
fn test_parse_csi_sgr_mouse() {
|
fn test_parse_csi_sgr_mouse() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
parse_csi_sgr_mouse(b"\x1B[<0;20;10;M").unwrap(),
|
parse_csi_sgr_mouse(b"\x1B[<0;20;10;M").unwrap(),
|
||||||
Some(Event::Mouse(MouseEvent {
|
Some(InternalEvent::Event(Event::Mouse(MouseEvent {
|
||||||
kind: MouseEventKind::Down(MouseButton::Left),
|
kind: MouseEventKind::Down(MouseButton::Left),
|
||||||
column: 19,
|
column: 19,
|
||||||
row: 9,
|
row: 9,
|
||||||
modifiers: KeyModifiers::empty(),
|
modifiers: KeyModifiers::empty(),
|
||||||
}))
|
})))
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
parse_csi_sgr_mouse(b"\x1B[<0;20;10M").unwrap(),
|
parse_csi_sgr_mouse(b"\x1B[<0;20;10M").unwrap(),
|
||||||
Some(Event::Mouse(MouseEvent {
|
Some(InternalEvent::Event(Event::Mouse(MouseEvent {
|
||||||
kind: MouseEventKind::Down(MouseButton::Left),
|
kind: MouseEventKind::Down(MouseButton::Left),
|
||||||
column: 19,
|
column: 19,
|
||||||
row: 9,
|
row: 9,
|
||||||
modifiers: KeyModifiers::empty(),
|
modifiers: KeyModifiers::empty(),
|
||||||
}))
|
})))
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
parse_csi_sgr_mouse(b"\x1B[<0;20;10;m").unwrap(),
|
parse_csi_sgr_mouse(b"\x1B[<0;20;10;m").unwrap(),
|
||||||
Some(Event::Mouse(MouseEvent {
|
Some(InternalEvent::Event(Event::Mouse(MouseEvent {
|
||||||
kind: MouseEventKind::Up(MouseButton::Left),
|
kind: MouseEventKind::Up(MouseButton::Left),
|
||||||
column: 19,
|
column: 19,
|
||||||
row: 9,
|
row: 9,
|
||||||
modifiers: KeyModifiers::empty(),
|
modifiers: KeyModifiers::empty(),
|
||||||
}))
|
})))
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
parse_csi_sgr_mouse(b"\x1B[<0;20;10m").unwrap(),
|
parse_csi_sgr_mouse(b"\x1B[<0;20;10m").unwrap(),
|
||||||
Some(Event::Mouse(MouseEvent {
|
Some(InternalEvent::Event(Event::Mouse(MouseEvent {
|
||||||
kind: MouseEventKind::Up(MouseButton::Left),
|
kind: MouseEventKind::Up(MouseButton::Left),
|
||||||
column: 19,
|
column: 19,
|
||||||
row: 9,
|
row: 9,
|
||||||
modifiers: KeyModifiers::empty(),
|
modifiers: KeyModifiers::empty(),
|
||||||
}))
|
})))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1154,10 +1193,10 @@ mod tests {
|
|||||||
fn test_parse_char_event_lowercase() {
|
fn test_parse_char_event_lowercase() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
parse_event(b"c", false).unwrap(),
|
parse_event(b"c", false).unwrap(),
|
||||||
Some(Event::Key(KeyEvent::new(
|
Some(InternalEvent::Event(Event::Key(KeyEvent::new(
|
||||||
KeyCode::Char('c'),
|
KeyCode::Char('c'),
|
||||||
KeyModifiers::empty()
|
KeyModifiers::empty()
|
||||||
))),
|
)))),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1165,10 +1204,10 @@ mod tests {
|
|||||||
fn test_parse_char_event_uppercase() {
|
fn test_parse_char_event_uppercase() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
parse_event(b"C", false).unwrap(),
|
parse_event(b"C", false).unwrap(),
|
||||||
Some(Event::Key(KeyEvent::new(
|
Some(InternalEvent::Event(Event::Key(KeyEvent::new(
|
||||||
KeyCode::Char('C'),
|
KeyCode::Char('C'),
|
||||||
KeyModifiers::SHIFT
|
KeyModifiers::SHIFT
|
||||||
))),
|
)))),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1176,24 +1215,24 @@ mod tests {
|
|||||||
fn test_parse_basic_csi_u_encoded_key_code() {
|
fn test_parse_basic_csi_u_encoded_key_code() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
parse_csi_u_encoded_key_code(b"\x1B[97u").unwrap(),
|
parse_csi_u_encoded_key_code(b"\x1B[97u").unwrap(),
|
||||||
Some(Event::Key(KeyEvent::new(
|
Some(InternalEvent::Event(Event::Key(KeyEvent::new(
|
||||||
KeyCode::Char('a'),
|
KeyCode::Char('a'),
|
||||||
KeyModifiers::empty()
|
KeyModifiers::empty()
|
||||||
))),
|
)))),
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
parse_csi_u_encoded_key_code(b"\x1B[97;2u").unwrap(),
|
parse_csi_u_encoded_key_code(b"\x1B[97;2u").unwrap(),
|
||||||
Some(Event::Key(KeyEvent::new(
|
Some(InternalEvent::Event(Event::Key(KeyEvent::new(
|
||||||
KeyCode::Char('A'),
|
KeyCode::Char('A'),
|
||||||
KeyModifiers::SHIFT
|
KeyModifiers::SHIFT
|
||||||
))),
|
)))),
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
parse_csi_u_encoded_key_code(b"\x1B[97;7u").unwrap(),
|
parse_csi_u_encoded_key_code(b"\x1B[97;7u").unwrap(),
|
||||||
Some(Event::Key(KeyEvent::new(
|
Some(InternalEvent::Event(Event::Key(KeyEvent::new(
|
||||||
KeyCode::Char('a'),
|
KeyCode::Char('a'),
|
||||||
KeyModifiers::ALT | KeyModifiers::CONTROL
|
KeyModifiers::ALT | KeyModifiers::CONTROL
|
||||||
))),
|
)))),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1201,45 +1240,45 @@ mod tests {
|
|||||||
fn test_parse_basic_csi_u_encoded_key_code_special_keys() {
|
fn test_parse_basic_csi_u_encoded_key_code_special_keys() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
parse_csi_u_encoded_key_code(b"\x1B[13u").unwrap(),
|
parse_csi_u_encoded_key_code(b"\x1B[13u").unwrap(),
|
||||||
Some(Event::Key(KeyEvent::new(
|
Some(InternalEvent::Event(Event::Key(KeyEvent::new(
|
||||||
KeyCode::Enter,
|
KeyCode::Enter,
|
||||||
KeyModifiers::empty()
|
KeyModifiers::empty()
|
||||||
))),
|
)))),
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
parse_csi_u_encoded_key_code(b"\x1B[27u").unwrap(),
|
parse_csi_u_encoded_key_code(b"\x1B[27u").unwrap(),
|
||||||
Some(Event::Key(KeyEvent::new(
|
Some(InternalEvent::Event(Event::Key(KeyEvent::new(
|
||||||
KeyCode::Esc,
|
KeyCode::Esc,
|
||||||
KeyModifiers::empty()
|
KeyModifiers::empty()
|
||||||
))),
|
)))),
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
parse_csi_u_encoded_key_code(b"\x1B[57358u").unwrap(),
|
parse_csi_u_encoded_key_code(b"\x1B[57358u").unwrap(),
|
||||||
Some(Event::Key(KeyEvent::new(
|
Some(InternalEvent::Event(Event::Key(KeyEvent::new(
|
||||||
KeyCode::CapsLock,
|
KeyCode::CapsLock,
|
||||||
KeyModifiers::empty()
|
KeyModifiers::empty()
|
||||||
))),
|
)))),
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
parse_csi_u_encoded_key_code(b"\x1B[57376u").unwrap(),
|
parse_csi_u_encoded_key_code(b"\x1B[57376u").unwrap(),
|
||||||
Some(Event::Key(KeyEvent::new(
|
Some(InternalEvent::Event(Event::Key(KeyEvent::new(
|
||||||
KeyCode::F(13),
|
KeyCode::F(13),
|
||||||
KeyModifiers::empty()
|
KeyModifiers::empty()
|
||||||
))),
|
)))),
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
parse_csi_u_encoded_key_code(b"\x1B[57428u").unwrap(),
|
parse_csi_u_encoded_key_code(b"\x1B[57428u").unwrap(),
|
||||||
Some(Event::Key(KeyEvent::new(
|
Some(InternalEvent::Event(Event::Key(KeyEvent::new(
|
||||||
KeyCode::Media(MediaKeyCode::Play),
|
KeyCode::Media(MediaKeyCode::Play),
|
||||||
KeyModifiers::empty()
|
KeyModifiers::empty()
|
||||||
))),
|
)))),
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
parse_csi_u_encoded_key_code(b"\x1B[57441u").unwrap(),
|
parse_csi_u_encoded_key_code(b"\x1B[57441u").unwrap(),
|
||||||
Some(Event::Key(KeyEvent::new(
|
Some(InternalEvent::Event(Event::Key(KeyEvent::new(
|
||||||
KeyCode::Modifier(ModifierKeyCode::LeftShift),
|
KeyCode::Modifier(ModifierKeyCode::LeftShift),
|
||||||
KeyModifiers::SHIFT,
|
KeyModifiers::SHIFT,
|
||||||
))),
|
)))),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1247,20 +1286,24 @@ mod tests {
|
|||||||
fn test_parse_csi_u_encoded_keypad_code() {
|
fn test_parse_csi_u_encoded_keypad_code() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
parse_csi_u_encoded_key_code(b"\x1B[57399u").unwrap(),
|
parse_csi_u_encoded_key_code(b"\x1B[57399u").unwrap(),
|
||||||
Some(Event::Key(KeyEvent::new_with_kind_and_state(
|
Some(InternalEvent::Event(Event::Key(
|
||||||
|
KeyEvent::new_with_kind_and_state(
|
||||||
KeyCode::Char('0'),
|
KeyCode::Char('0'),
|
||||||
KeyModifiers::empty(),
|
KeyModifiers::empty(),
|
||||||
KeyEventKind::Press,
|
KeyEventKind::Press,
|
||||||
KeyEventState::KEYPAD,
|
KeyEventState::KEYPAD,
|
||||||
|
)
|
||||||
))),
|
))),
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
parse_csi_u_encoded_key_code(b"\x1B[57419u").unwrap(),
|
parse_csi_u_encoded_key_code(b"\x1B[57419u").unwrap(),
|
||||||
Some(Event::Key(KeyEvent::new_with_kind_and_state(
|
Some(InternalEvent::Event(Event::Key(
|
||||||
|
KeyEvent::new_with_kind_and_state(
|
||||||
KeyCode::Up,
|
KeyCode::Up,
|
||||||
KeyModifiers::empty(),
|
KeyModifiers::empty(),
|
||||||
KeyEventKind::Press,
|
KeyEventKind::Press,
|
||||||
KeyEventState::KEYPAD,
|
KeyEventState::KEYPAD,
|
||||||
|
)
|
||||||
))),
|
))),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -1269,43 +1312,43 @@ mod tests {
|
|||||||
fn test_parse_csi_u_encoded_key_code_with_types() {
|
fn test_parse_csi_u_encoded_key_code_with_types() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
parse_csi_u_encoded_key_code(b"\x1B[97;1u").unwrap(),
|
parse_csi_u_encoded_key_code(b"\x1B[97;1u").unwrap(),
|
||||||
Some(Event::Key(KeyEvent::new_with_kind(
|
Some(InternalEvent::Event(Event::Key(KeyEvent::new_with_kind(
|
||||||
KeyCode::Char('a'),
|
KeyCode::Char('a'),
|
||||||
KeyModifiers::empty(),
|
KeyModifiers::empty(),
|
||||||
KeyEventKind::Press,
|
KeyEventKind::Press,
|
||||||
))),
|
)))),
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
parse_csi_u_encoded_key_code(b"\x1B[97;1:1u").unwrap(),
|
parse_csi_u_encoded_key_code(b"\x1B[97;1:1u").unwrap(),
|
||||||
Some(Event::Key(KeyEvent::new_with_kind(
|
Some(InternalEvent::Event(Event::Key(KeyEvent::new_with_kind(
|
||||||
KeyCode::Char('a'),
|
KeyCode::Char('a'),
|
||||||
KeyModifiers::empty(),
|
KeyModifiers::empty(),
|
||||||
KeyEventKind::Press,
|
KeyEventKind::Press,
|
||||||
))),
|
)))),
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
parse_csi_u_encoded_key_code(b"\x1B[97;5:1u").unwrap(),
|
parse_csi_u_encoded_key_code(b"\x1B[97;5:1u").unwrap(),
|
||||||
Some(Event::Key(KeyEvent::new_with_kind(
|
Some(InternalEvent::Event(Event::Key(KeyEvent::new_with_kind(
|
||||||
KeyCode::Char('a'),
|
KeyCode::Char('a'),
|
||||||
KeyModifiers::CONTROL,
|
KeyModifiers::CONTROL,
|
||||||
KeyEventKind::Press,
|
KeyEventKind::Press,
|
||||||
))),
|
)))),
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
parse_csi_u_encoded_key_code(b"\x1B[97;1:2u").unwrap(),
|
parse_csi_u_encoded_key_code(b"\x1B[97;1:2u").unwrap(),
|
||||||
Some(Event::Key(KeyEvent::new_with_kind(
|
Some(InternalEvent::Event(Event::Key(KeyEvent::new_with_kind(
|
||||||
KeyCode::Char('a'),
|
KeyCode::Char('a'),
|
||||||
KeyModifiers::empty(),
|
KeyModifiers::empty(),
|
||||||
KeyEventKind::Repeat,
|
KeyEventKind::Repeat,
|
||||||
))),
|
)))),
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
parse_csi_u_encoded_key_code(b"\x1B[97;1:3u").unwrap(),
|
parse_csi_u_encoded_key_code(b"\x1B[97;1:3u").unwrap(),
|
||||||
Some(Event::Key(KeyEvent::new_with_kind(
|
Some(InternalEvent::Event(Event::Key(KeyEvent::new_with_kind(
|
||||||
KeyCode::Char('a'),
|
KeyCode::Char('a'),
|
||||||
KeyModifiers::empty(),
|
KeyModifiers::empty(),
|
||||||
KeyEventKind::Release,
|
KeyEventKind::Release,
|
||||||
))),
|
)))),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1313,40 +1356,40 @@ mod tests {
|
|||||||
fn test_parse_csi_u_encoded_key_code_has_modifier_on_modifier_press() {
|
fn test_parse_csi_u_encoded_key_code_has_modifier_on_modifier_press() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
parse_csi_u_encoded_key_code(b"\x1B[57449u").unwrap(),
|
parse_csi_u_encoded_key_code(b"\x1B[57449u").unwrap(),
|
||||||
Some(Event::Key(KeyEvent::new_with_kind(
|
Some(InternalEvent::Event(Event::Key(KeyEvent::new_with_kind(
|
||||||
KeyCode::Modifier(ModifierKeyCode::RightAlt),
|
KeyCode::Modifier(ModifierKeyCode::RightAlt),
|
||||||
KeyModifiers::ALT,
|
KeyModifiers::ALT,
|
||||||
KeyEventKind::Press,
|
KeyEventKind::Press,
|
||||||
))),
|
)))),
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
parse_csi_u_encoded_key_code(b"\x1B[57449;3:3u").unwrap(),
|
parse_csi_u_encoded_key_code(b"\x1B[57449;3:3u").unwrap(),
|
||||||
Some(Event::Key(KeyEvent::new_with_kind(
|
Some(InternalEvent::Event(Event::Key(KeyEvent::new_with_kind(
|
||||||
KeyCode::Modifier(ModifierKeyCode::RightAlt),
|
KeyCode::Modifier(ModifierKeyCode::RightAlt),
|
||||||
KeyModifiers::ALT,
|
KeyModifiers::ALT,
|
||||||
KeyEventKind::Release,
|
KeyEventKind::Release,
|
||||||
))),
|
)))),
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
parse_csi_u_encoded_key_code(b"\x1B[57450u").unwrap(),
|
parse_csi_u_encoded_key_code(b"\x1B[57450u").unwrap(),
|
||||||
Some(Event::Key(KeyEvent::new(
|
Some(InternalEvent::Event(Event::Key(KeyEvent::new(
|
||||||
KeyCode::Modifier(ModifierKeyCode::RightSuper),
|
KeyCode::Modifier(ModifierKeyCode::RightSuper),
|
||||||
KeyModifiers::SUPER,
|
KeyModifiers::SUPER,
|
||||||
))),
|
)))),
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
parse_csi_u_encoded_key_code(b"\x1B[57451u").unwrap(),
|
parse_csi_u_encoded_key_code(b"\x1B[57451u").unwrap(),
|
||||||
Some(Event::Key(KeyEvent::new(
|
Some(InternalEvent::Event(Event::Key(KeyEvent::new(
|
||||||
KeyCode::Modifier(ModifierKeyCode::RightHyper),
|
KeyCode::Modifier(ModifierKeyCode::RightHyper),
|
||||||
KeyModifiers::HYPER,
|
KeyModifiers::HYPER,
|
||||||
))),
|
)))),
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
parse_csi_u_encoded_key_code(b"\x1B[57452u").unwrap(),
|
parse_csi_u_encoded_key_code(b"\x1B[57452u").unwrap(),
|
||||||
Some(Event::Key(KeyEvent::new(
|
Some(InternalEvent::Event(Event::Key(KeyEvent::new(
|
||||||
KeyCode::Modifier(ModifierKeyCode::RightMeta),
|
KeyCode::Modifier(ModifierKeyCode::RightMeta),
|
||||||
KeyModifiers::META,
|
KeyModifiers::META,
|
||||||
))),
|
)))),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1354,24 +1397,24 @@ mod tests {
|
|||||||
fn test_parse_csi_u_encoded_key_code_with_extra_modifiers() {
|
fn test_parse_csi_u_encoded_key_code_with_extra_modifiers() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
parse_csi_u_encoded_key_code(b"\x1B[97;9u").unwrap(),
|
parse_csi_u_encoded_key_code(b"\x1B[97;9u").unwrap(),
|
||||||
Some(Event::Key(KeyEvent::new(
|
Some(InternalEvent::Event(Event::Key(KeyEvent::new(
|
||||||
KeyCode::Char('a'),
|
KeyCode::Char('a'),
|
||||||
KeyModifiers::SUPER
|
KeyModifiers::SUPER
|
||||||
))),
|
)))),
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
parse_csi_u_encoded_key_code(b"\x1B[97;17u").unwrap(),
|
parse_csi_u_encoded_key_code(b"\x1B[97;17u").unwrap(),
|
||||||
Some(Event::Key(KeyEvent::new(
|
Some(InternalEvent::Event(Event::Key(KeyEvent::new(
|
||||||
KeyCode::Char('a'),
|
KeyCode::Char('a'),
|
||||||
KeyModifiers::HYPER,
|
KeyModifiers::HYPER,
|
||||||
))),
|
)))),
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
parse_csi_u_encoded_key_code(b"\x1B[97;33u").unwrap(),
|
parse_csi_u_encoded_key_code(b"\x1B[97;33u").unwrap(),
|
||||||
Some(Event::Key(KeyEvent::new(
|
Some(InternalEvent::Event(Event::Key(KeyEvent::new(
|
||||||
KeyCode::Char('a'),
|
KeyCode::Char('a'),
|
||||||
KeyModifiers::META,
|
KeyModifiers::META,
|
||||||
))),
|
)))),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1379,20 +1422,24 @@ mod tests {
|
|||||||
fn test_parse_csi_u_encoded_key_code_with_extra_state() {
|
fn test_parse_csi_u_encoded_key_code_with_extra_state() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
parse_csi_u_encoded_key_code(b"\x1B[97;65u").unwrap(),
|
parse_csi_u_encoded_key_code(b"\x1B[97;65u").unwrap(),
|
||||||
Some(Event::Key(KeyEvent::new_with_kind_and_state(
|
Some(InternalEvent::Event(Event::Key(
|
||||||
|
KeyEvent::new_with_kind_and_state(
|
||||||
KeyCode::Char('a'),
|
KeyCode::Char('a'),
|
||||||
KeyModifiers::empty(),
|
KeyModifiers::empty(),
|
||||||
KeyEventKind::Press,
|
KeyEventKind::Press,
|
||||||
KeyEventState::CAPS_LOCK,
|
KeyEventState::CAPS_LOCK,
|
||||||
|
)
|
||||||
))),
|
))),
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
parse_csi_u_encoded_key_code(b"\x1B[49;129u").unwrap(),
|
parse_csi_u_encoded_key_code(b"\x1B[49;129u").unwrap(),
|
||||||
Some(Event::Key(KeyEvent::new_with_kind_and_state(
|
Some(InternalEvent::Event(Event::Key(
|
||||||
|
KeyEvent::new_with_kind_and_state(
|
||||||
KeyCode::Char('1'),
|
KeyCode::Char('1'),
|
||||||
KeyModifiers::empty(),
|
KeyModifiers::empty(),
|
||||||
KeyEventKind::Press,
|
KeyEventKind::Press,
|
||||||
KeyEventState::NUM_LOCK,
|
KeyEventState::NUM_LOCK,
|
||||||
|
)
|
||||||
))),
|
))),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -1402,18 +1449,18 @@ mod tests {
|
|||||||
assert_eq!(
|
assert_eq!(
|
||||||
// A-S-9 is equivalent to A-(
|
// A-S-9 is equivalent to A-(
|
||||||
parse_event(b"\x1B[57:40;4u", false).unwrap(),
|
parse_event(b"\x1B[57:40;4u", false).unwrap(),
|
||||||
Some(Event::Key(KeyEvent::new(
|
Some(InternalEvent::Event(Event::Key(KeyEvent::new(
|
||||||
KeyCode::Char('('),
|
KeyCode::Char('('),
|
||||||
KeyModifiers::ALT,
|
KeyModifiers::ALT,
|
||||||
))),
|
)))),
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
// A-S-minus is equivalent to A-_
|
// A-S-minus is equivalent to A-_
|
||||||
parse_event(b"\x1B[45:95;4u", false).unwrap(),
|
parse_event(b"\x1B[45:95;4u", false).unwrap(),
|
||||||
Some(Event::Key(KeyEvent::new(
|
Some(InternalEvent::Event(Event::Key(KeyEvent::new(
|
||||||
KeyCode::Char('_'),
|
KeyCode::Char('_'),
|
||||||
KeyModifiers::ALT,
|
KeyModifiers::ALT,
|
||||||
))),
|
)))),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1421,19 +1468,19 @@ mod tests {
|
|||||||
fn test_parse_csi_special_key_code_with_types() {
|
fn test_parse_csi_special_key_code_with_types() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
parse_event(b"\x1B[;1:3B", false).unwrap(),
|
parse_event(b"\x1B[;1:3B", false).unwrap(),
|
||||||
Some(Event::Key(KeyEvent::new_with_kind(
|
Some(InternalEvent::Event(Event::Key(KeyEvent::new_with_kind(
|
||||||
KeyCode::Down,
|
KeyCode::Down,
|
||||||
KeyModifiers::empty(),
|
KeyModifiers::empty(),
|
||||||
KeyEventKind::Release,
|
KeyEventKind::Release,
|
||||||
))),
|
)))),
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
parse_event(b"\x1B[1;1:3B", false).unwrap(),
|
parse_event(b"\x1B[1;1:3B", false).unwrap(),
|
||||||
Some(Event::Key(KeyEvent::new_with_kind(
|
Some(InternalEvent::Event(Event::Key(KeyEvent::new_with_kind(
|
||||||
KeyCode::Down,
|
KeyCode::Down,
|
||||||
KeyModifiers::empty(),
|
KeyModifiers::empty(),
|
||||||
KeyEventKind::Release,
|
KeyEventKind::Release,
|
||||||
))),
|
)))),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1441,19 +1488,19 @@ mod tests {
|
|||||||
fn test_parse_csi_numbered_escape_code_with_types() {
|
fn test_parse_csi_numbered_escape_code_with_types() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
parse_event(b"\x1B[5;1:3~", false).unwrap(),
|
parse_event(b"\x1B[5;1:3~", false).unwrap(),
|
||||||
Some(Event::Key(KeyEvent::new_with_kind(
|
Some(InternalEvent::Event(Event::Key(KeyEvent::new_with_kind(
|
||||||
KeyCode::PageUp,
|
KeyCode::PageUp,
|
||||||
KeyModifiers::empty(),
|
KeyModifiers::empty(),
|
||||||
KeyEventKind::Release,
|
KeyEventKind::Release,
|
||||||
))),
|
)))),
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
parse_event(b"\x1B[6;5:3~", false).unwrap(),
|
parse_event(b"\x1B[6;5:3~", false).unwrap(),
|
||||||
Some(Event::Key(KeyEvent::new_with_kind(
|
Some(InternalEvent::Event(Event::Key(KeyEvent::new_with_kind(
|
||||||
KeyCode::PageDown,
|
KeyCode::PageDown,
|
||||||
KeyModifiers::CONTROL,
|
KeyModifiers::CONTROL,
|
||||||
KeyEventKind::Release,
|
KeyEventKind::Release,
|
||||||
))),
|
)))),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
11
src/event/sys/unix/waker.rs
Normal file
11
src/event/sys/unix/waker.rs
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
#[cfg(feature = "use-dev-tty")]
|
||||||
|
pub(crate) mod tty;
|
||||||
|
|
||||||
|
#[cfg(not(feature = "use-dev-tty"))]
|
||||||
|
pub(crate) mod mio;
|
||||||
|
|
||||||
|
#[cfg(feature = "use-dev-tty")]
|
||||||
|
pub(crate) use self::tty::Waker;
|
||||||
|
|
||||||
|
#[cfg(not(feature = "use-dev-tty"))]
|
||||||
|
pub(crate) use self::mio::Waker;
|
34
src/event/sys/unix/waker/mio.rs
Normal file
34
src/event/sys/unix/waker/mio.rs
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
use std::sync::{Arc, Mutex};
|
||||||
|
|
||||||
|
use ::mio::{Registry, Token};
|
||||||
|
|
||||||
|
/// Allows to wake up the `mio::Poll::poll()` method.
|
||||||
|
/// This type wraps `mio::Waker`, for more information see its documentation.
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub(crate) struct Waker {
|
||||||
|
inner: Arc<Mutex<::mio::Waker>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Waker {
|
||||||
|
/// Create a new `Waker`.
|
||||||
|
pub(crate) fn new(registry: &Registry, waker_token: Token) -> std::io::Result<Self> {
|
||||||
|
Ok(Self {
|
||||||
|
inner: Arc::new(Mutex::new(mio::Waker::new(registry, waker_token)?)),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Wake up the [`Poll`] associated with this `Waker`.
|
||||||
|
///
|
||||||
|
/// Readiness is set to `Ready::readable()`.
|
||||||
|
pub(crate) fn wake(&self) -> std::io::Result<()> {
|
||||||
|
self.inner.lock().unwrap().wake()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resets the state so the same waker can be reused.
|
||||||
|
///
|
||||||
|
/// This function is not impl
|
||||||
|
#[allow(dead_code, clippy::clippy::unnecessary_wraps)]
|
||||||
|
pub(crate) fn reset(&self) -> std::io::Result<()> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
28
src/event/sys/unix/waker/tty.rs
Normal file
28
src/event/sys/unix/waker/tty.rs
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
use std::{
|
||||||
|
io::{self, Write},
|
||||||
|
os::unix::net::UnixStream,
|
||||||
|
sync::{Arc, Mutex},
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Allows to wake up the EventSource::try_read() method.
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub(crate) struct Waker {
|
||||||
|
inner: Arc<Mutex<UnixStream>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Waker {
|
||||||
|
/// Create a new `Waker`.
|
||||||
|
pub(crate) fn new(writer: UnixStream) -> Self {
|
||||||
|
Self {
|
||||||
|
inner: Arc::new(Mutex::new(writer)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Wake up the [`Poll`] associated with this `Waker`.
|
||||||
|
///
|
||||||
|
/// Readiness is set to `Ready::readable()`.
|
||||||
|
pub(crate) fn wake(&self) -> io::Result<()> {
|
||||||
|
self.inner.lock().unwrap().write(&[0])?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
48
src/event/sys/windows.rs
Normal file
48
src/event/sys/windows.rs
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
//! This is a WINDOWS specific implementation for input related action.
|
||||||
|
|
||||||
|
use std::convert::TryFrom;
|
||||||
|
use std::io;
|
||||||
|
use std::sync::atomic::{AtomicU64, Ordering};
|
||||||
|
|
||||||
|
use crossterm_winapi::{ConsoleMode, Handle};
|
||||||
|
|
||||||
|
pub(crate) mod parse;
|
||||||
|
pub(crate) mod poll;
|
||||||
|
#[cfg(feature = "event-stream")]
|
||||||
|
pub(crate) mod waker;
|
||||||
|
|
||||||
|
const ENABLE_MOUSE_MODE: u32 = 0x0010 | 0x0080 | 0x0008;
|
||||||
|
|
||||||
|
/// This is a either `u64::MAX` if it's uninitialized or a valid `u32` that stores the original
|
||||||
|
/// console mode if it's initialized.
|
||||||
|
static ORIGINAL_CONSOLE_MODE: AtomicU64 = AtomicU64::new(u64::MAX);
|
||||||
|
|
||||||
|
/// Initializes the default console color. It will will be skipped if it has already been initialized.
|
||||||
|
fn init_original_console_mode(original_mode: u32) {
|
||||||
|
let _ = ORIGINAL_CONSOLE_MODE.compare_exchange(
|
||||||
|
u64::MAX,
|
||||||
|
u64::from(original_mode),
|
||||||
|
Ordering::Relaxed,
|
||||||
|
Ordering::Relaxed,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the original console color, make sure to call `init_console_color` before calling this function. Otherwise this function will panic.
|
||||||
|
fn original_console_mode() -> std::io::Result<u32> {
|
||||||
|
u32::try_from(ORIGINAL_CONSOLE_MODE.load(Ordering::Relaxed))
|
||||||
|
.map_err(|_| io::Error::new(io::ErrorKind::Other, "Initial console modes not set"))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn enable_mouse_capture() -> std::io::Result<()> {
|
||||||
|
let mode = ConsoleMode::from(Handle::current_in_handle()?);
|
||||||
|
init_original_console_mode(mode.mode()?);
|
||||||
|
mode.set_mode(ENABLE_MOUSE_MODE)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn disable_mouse_capture() -> std::io::Result<()> {
|
||||||
|
let mode = ConsoleMode::from(Handle::current_in_handle()?);
|
||||||
|
mode.set_mode(original_console_mode()?)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
378
src/event/sys/windows/parse.rs
Normal file
378
src/event/sys/windows/parse.rs
Normal file
@ -0,0 +1,378 @@
|
|||||||
|
use crossterm_winapi::{ControlKeyState, EventFlags, KeyEventRecord, ScreenBuffer};
|
||||||
|
use winapi::um::{
|
||||||
|
wincon::{
|
||||||
|
CAPSLOCK_ON, LEFT_ALT_PRESSED, LEFT_CTRL_PRESSED, RIGHT_ALT_PRESSED, RIGHT_CTRL_PRESSED,
|
||||||
|
SHIFT_PRESSED,
|
||||||
|
},
|
||||||
|
winuser::{
|
||||||
|
GetForegroundWindow, GetKeyboardLayout, GetWindowThreadProcessId, ToUnicodeEx, VK_BACK,
|
||||||
|
VK_CONTROL, VK_DELETE, VK_DOWN, VK_END, VK_ESCAPE, VK_F1, VK_F24, VK_HOME, VK_INSERT,
|
||||||
|
VK_LEFT, VK_MENU, VK_NEXT, VK_NUMPAD0, VK_NUMPAD9, VK_PRIOR, VK_RETURN, VK_RIGHT, VK_SHIFT,
|
||||||
|
VK_TAB, VK_UP,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::event::{
|
||||||
|
Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers, MouseButton, MouseEvent, MouseEventKind,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct MouseButtonsPressed {
|
||||||
|
pub(crate) left: bool,
|
||||||
|
pub(crate) right: bool,
|
||||||
|
pub(crate) middle: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn handle_mouse_event(
|
||||||
|
mouse_event: crossterm_winapi::MouseEvent,
|
||||||
|
buttons_pressed: &MouseButtonsPressed,
|
||||||
|
) -> Option<Event> {
|
||||||
|
if let Ok(Some(event)) = parse_mouse_event_record(&mouse_event, buttons_pressed) {
|
||||||
|
return Some(Event::Mouse(event));
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
enum WindowsKeyEvent {
|
||||||
|
KeyEvent(KeyEvent),
|
||||||
|
Surrogate(u16),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn handle_key_event(
|
||||||
|
key_event: KeyEventRecord,
|
||||||
|
surrogate_buffer: &mut Option<u16>,
|
||||||
|
) -> Option<Event> {
|
||||||
|
let windows_key_event = parse_key_event_record(&key_event)?;
|
||||||
|
match windows_key_event {
|
||||||
|
WindowsKeyEvent::KeyEvent(key_event) => {
|
||||||
|
// Discard any buffered surrogate value if another valid key event comes before the
|
||||||
|
// next surrogate value.
|
||||||
|
*surrogate_buffer = None;
|
||||||
|
Some(Event::Key(key_event))
|
||||||
|
}
|
||||||
|
WindowsKeyEvent::Surrogate(new_surrogate) => {
|
||||||
|
let ch = handle_surrogate(surrogate_buffer, new_surrogate)?;
|
||||||
|
let modifiers = KeyModifiers::from(&key_event.control_key_state);
|
||||||
|
let key_event = KeyEvent::new(KeyCode::Char(ch), modifiers);
|
||||||
|
Some(Event::Key(key_event))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_surrogate(surrogate_buffer: &mut Option<u16>, new_surrogate: u16) -> Option<char> {
|
||||||
|
match *surrogate_buffer {
|
||||||
|
Some(buffered_surrogate) => {
|
||||||
|
*surrogate_buffer = None;
|
||||||
|
std::char::decode_utf16([buffered_surrogate, new_surrogate])
|
||||||
|
.next()
|
||||||
|
.unwrap()
|
||||||
|
.ok()
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
*surrogate_buffer = Some(new_surrogate);
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&ControlKeyState> for KeyModifiers {
|
||||||
|
fn from(state: &ControlKeyState) -> Self {
|
||||||
|
let shift = state.has_state(SHIFT_PRESSED);
|
||||||
|
let alt = state.has_state(LEFT_ALT_PRESSED | RIGHT_ALT_PRESSED);
|
||||||
|
let control = state.has_state(LEFT_CTRL_PRESSED | RIGHT_CTRL_PRESSED);
|
||||||
|
|
||||||
|
let mut modifier = KeyModifiers::empty();
|
||||||
|
|
||||||
|
if shift {
|
||||||
|
modifier |= KeyModifiers::SHIFT;
|
||||||
|
}
|
||||||
|
if control {
|
||||||
|
modifier |= KeyModifiers::CONTROL;
|
||||||
|
}
|
||||||
|
if alt {
|
||||||
|
modifier |= KeyModifiers::ALT;
|
||||||
|
}
|
||||||
|
|
||||||
|
modifier
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum CharCase {
|
||||||
|
LowerCase,
|
||||||
|
UpperCase,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn try_ensure_char_case(ch: char, desired_case: CharCase) -> char {
|
||||||
|
match desired_case {
|
||||||
|
CharCase::LowerCase if ch.is_uppercase() => {
|
||||||
|
let mut iter = ch.to_lowercase();
|
||||||
|
// Unwrap is safe; iterator yields one or more chars.
|
||||||
|
let ch_lower = iter.next().unwrap();
|
||||||
|
if iter.next().is_none() {
|
||||||
|
ch_lower
|
||||||
|
} else {
|
||||||
|
ch
|
||||||
|
}
|
||||||
|
}
|
||||||
|
CharCase::UpperCase if ch.is_lowercase() => {
|
||||||
|
let mut iter = ch.to_uppercase();
|
||||||
|
// Unwrap is safe; iterator yields one or more chars.
|
||||||
|
let ch_upper = iter.next().unwrap();
|
||||||
|
if iter.next().is_none() {
|
||||||
|
ch_upper
|
||||||
|
} else {
|
||||||
|
ch
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => ch,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attempts to return the character for a key event accounting for the user's keyboard layout.
|
||||||
|
// The returned character (if any) is capitalized (if applicable) based on shift and capslock state.
|
||||||
|
// Returns None if the key doesn't map to a character or if it is a dead key.
|
||||||
|
// We use the *currently* active keyboard layout (if it can be determined). This layout may not
|
||||||
|
// correspond to the keyboard layout that was active when the user typed their input, since console
|
||||||
|
// applications get their input asynchronously from the terminal. By the time a console application
|
||||||
|
// can process a key input, the user may have changed the active layout. In this case, the character
|
||||||
|
// returned might not correspond to what the user expects, but there is no way for a console
|
||||||
|
// application to know what the keyboard layout actually was for a key event, so this is our best
|
||||||
|
// effort. If a console application processes input in a timely fashion, then it is unlikely that a
|
||||||
|
// user has time to change their keyboard layout before a key event is processed.
|
||||||
|
fn get_char_for_key(key_event: &KeyEventRecord) -> Option<char> {
|
||||||
|
let virtual_key_code = key_event.virtual_key_code as u32;
|
||||||
|
let virtual_scan_code = key_event.virtual_scan_code as u32;
|
||||||
|
let key_state = [0u8; 256];
|
||||||
|
let mut utf16_buf = [0u16, 16];
|
||||||
|
let dont_change_kernel_keyboard_state = 0x4;
|
||||||
|
|
||||||
|
// Best-effort attempt at determining the currently active keyboard layout.
|
||||||
|
// At the time of writing, this works for a console application running in Windows Terminal, but
|
||||||
|
// doesn't work under a Conhost terminal. For Conhost, the window handle returned by
|
||||||
|
// GetForegroundWindow() does not appear to actually be the foreground window which has the
|
||||||
|
// keyboard layout associated with it (or perhaps it is, but also has special protection that
|
||||||
|
// doesn't allow us to query it).
|
||||||
|
// When this determination fails, the returned keyboard layout handle will be null, which is an
|
||||||
|
// acceptable input for ToUnicodeEx, as that argument is optional. In this case ToUnicodeEx
|
||||||
|
// appears to use the keyboard layout associated with the current thread, which will be the
|
||||||
|
// layout that was inherited when the console application started (or possibly when the current
|
||||||
|
// thread was spawned). This is then unfortunately not updated when the user changes their
|
||||||
|
// keyboard layout in the terminal, but it's what we get.
|
||||||
|
let active_keyboard_layout = unsafe {
|
||||||
|
let foreground_window = GetForegroundWindow();
|
||||||
|
let foreground_thread = GetWindowThreadProcessId(foreground_window, std::ptr::null_mut());
|
||||||
|
GetKeyboardLayout(foreground_thread)
|
||||||
|
};
|
||||||
|
|
||||||
|
let ret = unsafe {
|
||||||
|
ToUnicodeEx(
|
||||||
|
virtual_key_code,
|
||||||
|
virtual_scan_code,
|
||||||
|
key_state.as_ptr(),
|
||||||
|
utf16_buf.as_mut_ptr(),
|
||||||
|
utf16_buf.len() as i32,
|
||||||
|
dont_change_kernel_keyboard_state,
|
||||||
|
active_keyboard_layout,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
// -1 indicates a dead key.
|
||||||
|
// 0 indicates no character for this key.
|
||||||
|
if ret < 1 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut ch_iter = std::char::decode_utf16(utf16_buf.into_iter().take(ret as usize));
|
||||||
|
let mut ch = ch_iter.next()?.ok()?;
|
||||||
|
if ch_iter.next().is_some() {
|
||||||
|
// Key doesn't map to a single char.
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let is_shift_pressed = key_event.control_key_state.has_state(SHIFT_PRESSED);
|
||||||
|
let is_capslock_on = key_event.control_key_state.has_state(CAPSLOCK_ON);
|
||||||
|
let desired_case = if is_shift_pressed ^ is_capslock_on {
|
||||||
|
CharCase::UpperCase
|
||||||
|
} else {
|
||||||
|
CharCase::LowerCase
|
||||||
|
};
|
||||||
|
ch = try_ensure_char_case(ch, desired_case);
|
||||||
|
Some(ch)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_key_event_record(key_event: &KeyEventRecord) -> Option<WindowsKeyEvent> {
|
||||||
|
let modifiers = KeyModifiers::from(&key_event.control_key_state);
|
||||||
|
let virtual_key_code = key_event.virtual_key_code as i32;
|
||||||
|
|
||||||
|
// We normally ignore all key release events, but we will make an exception for an Alt key
|
||||||
|
// release if it carries a u_char value, as this indicates an Alt code.
|
||||||
|
let is_alt_code = virtual_key_code == VK_MENU && !key_event.key_down && key_event.u_char != 0;
|
||||||
|
if is_alt_code {
|
||||||
|
let utf16 = key_event.u_char;
|
||||||
|
match utf16 {
|
||||||
|
surrogate @ 0xD800..=0xDFFF => {
|
||||||
|
return Some(WindowsKeyEvent::Surrogate(surrogate));
|
||||||
|
}
|
||||||
|
unicode_scalar_value => {
|
||||||
|
// Unwrap is safe: We tested for surrogate values above and those are the only
|
||||||
|
// u16 values that are invalid when directly interpreted as unicode scalar
|
||||||
|
// values.
|
||||||
|
let ch = std::char::from_u32(unicode_scalar_value as u32).unwrap();
|
||||||
|
let key_code = KeyCode::Char(ch);
|
||||||
|
let kind = if key_event.key_down {
|
||||||
|
KeyEventKind::Press
|
||||||
|
} else {
|
||||||
|
KeyEventKind::Release
|
||||||
|
};
|
||||||
|
let key_event = KeyEvent::new_with_kind(key_code, modifiers, kind);
|
||||||
|
return Some(WindowsKeyEvent::KeyEvent(key_event));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't generate events for numpad key presses when they're producing Alt codes.
|
||||||
|
let is_numpad_numeric_key = (VK_NUMPAD0..=VK_NUMPAD9).contains(&virtual_key_code);
|
||||||
|
let is_only_alt_modifier = modifiers.contains(KeyModifiers::ALT)
|
||||||
|
&& !modifiers.contains(KeyModifiers::SHIFT | KeyModifiers::CONTROL);
|
||||||
|
if is_only_alt_modifier && is_numpad_numeric_key {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let parse_result = match virtual_key_code {
|
||||||
|
VK_SHIFT | VK_CONTROL | VK_MENU => None,
|
||||||
|
VK_BACK => Some(KeyCode::Backspace),
|
||||||
|
VK_ESCAPE => Some(KeyCode::Esc),
|
||||||
|
VK_RETURN => Some(KeyCode::Enter),
|
||||||
|
VK_F1..=VK_F24 => Some(KeyCode::F((key_event.virtual_key_code - 111) as u8)),
|
||||||
|
VK_LEFT => Some(KeyCode::Left),
|
||||||
|
VK_UP => Some(KeyCode::Up),
|
||||||
|
VK_RIGHT => Some(KeyCode::Right),
|
||||||
|
VK_DOWN => Some(KeyCode::Down),
|
||||||
|
VK_PRIOR => Some(KeyCode::PageUp),
|
||||||
|
VK_NEXT => Some(KeyCode::PageDown),
|
||||||
|
VK_HOME => Some(KeyCode::Home),
|
||||||
|
VK_END => Some(KeyCode::End),
|
||||||
|
VK_DELETE => Some(KeyCode::Delete),
|
||||||
|
VK_INSERT => Some(KeyCode::Insert),
|
||||||
|
VK_TAB if modifiers.contains(KeyModifiers::SHIFT) => Some(KeyCode::BackTab),
|
||||||
|
VK_TAB => Some(KeyCode::Tab),
|
||||||
|
_ => {
|
||||||
|
let utf16 = key_event.u_char;
|
||||||
|
match utf16 {
|
||||||
|
0x00..=0x1f => {
|
||||||
|
// Some key combinations generate either no u_char value or generate control
|
||||||
|
// codes. To deliver back a KeyCode::Char(...) event we want to know which
|
||||||
|
// character the key normally maps to on the user's keyboard layout.
|
||||||
|
// The keys that intentionally generate control codes (ESC, ENTER, TAB, etc.)
|
||||||
|
// are handled by their virtual key codes above.
|
||||||
|
get_char_for_key(key_event).map(KeyCode::Char)
|
||||||
|
}
|
||||||
|
surrogate @ 0xD800..=0xDFFF => {
|
||||||
|
return Some(WindowsKeyEvent::Surrogate(surrogate));
|
||||||
|
}
|
||||||
|
unicode_scalar_value => {
|
||||||
|
// Unwrap is safe: We tested for surrogate values above and those are the only
|
||||||
|
// u16 values that are invalid when directly interpreted as unicode scalar
|
||||||
|
// values.
|
||||||
|
let ch = std::char::from_u32(unicode_scalar_value as u32).unwrap();
|
||||||
|
Some(KeyCode::Char(ch))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(key_code) = parse_result {
|
||||||
|
let kind = if key_event.key_down {
|
||||||
|
KeyEventKind::Press
|
||||||
|
} else {
|
||||||
|
KeyEventKind::Release
|
||||||
|
};
|
||||||
|
let key_event = KeyEvent::new_with_kind(key_code, modifiers, kind);
|
||||||
|
return Some(WindowsKeyEvent::KeyEvent(key_event));
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
// The 'y' position of a mouse event or resize event is not relative to the window but absolute to screen buffer.
|
||||||
|
// This means that when the mouse cursor is at the top left it will be x: 0, y: 2295 (e.g. y = number of cells conting from the absolute buffer height) instead of relative x: 0, y: 0 to the window.
|
||||||
|
pub fn parse_relative_y(y: i16) -> std::io::Result<i16> {
|
||||||
|
let window_size = ScreenBuffer::current()?.info()?.terminal_window();
|
||||||
|
Ok(y - window_size.top)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_mouse_event_record(
|
||||||
|
event: &crossterm_winapi::MouseEvent,
|
||||||
|
buttons_pressed: &MouseButtonsPressed,
|
||||||
|
) -> std::io::Result<Option<MouseEvent>> {
|
||||||
|
let modifiers = KeyModifiers::from(&event.control_key_state);
|
||||||
|
|
||||||
|
let xpos = event.mouse_position.x as u16;
|
||||||
|
let ypos = parse_relative_y(event.mouse_position.y)? as u16;
|
||||||
|
|
||||||
|
let button_state = event.button_state;
|
||||||
|
|
||||||
|
let kind = match event.event_flags {
|
||||||
|
EventFlags::PressOrRelease | EventFlags::DoubleClick => {
|
||||||
|
if button_state.left_button() && !buttons_pressed.left {
|
||||||
|
Some(MouseEventKind::Down(MouseButton::Left))
|
||||||
|
} else if !button_state.left_button() && buttons_pressed.left {
|
||||||
|
Some(MouseEventKind::Up(MouseButton::Left))
|
||||||
|
} else if button_state.right_button() && !buttons_pressed.right {
|
||||||
|
Some(MouseEventKind::Down(MouseButton::Right))
|
||||||
|
} else if !button_state.right_button() && buttons_pressed.right {
|
||||||
|
Some(MouseEventKind::Up(MouseButton::Right))
|
||||||
|
} else if button_state.middle_button() && !buttons_pressed.middle {
|
||||||
|
Some(MouseEventKind::Down(MouseButton::Middle))
|
||||||
|
} else if !button_state.middle_button() && buttons_pressed.middle {
|
||||||
|
Some(MouseEventKind::Up(MouseButton::Middle))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EventFlags::MouseMoved => {
|
||||||
|
let button = if button_state.right_button() {
|
||||||
|
MouseButton::Right
|
||||||
|
} else if button_state.middle_button() {
|
||||||
|
MouseButton::Middle
|
||||||
|
} else {
|
||||||
|
MouseButton::Left
|
||||||
|
};
|
||||||
|
if button_state.release_button() {
|
||||||
|
Some(MouseEventKind::Moved)
|
||||||
|
} else {
|
||||||
|
Some(MouseEventKind::Drag(button))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EventFlags::MouseWheeled => {
|
||||||
|
// Vertical scroll
|
||||||
|
// from https://docs.microsoft.com/en-us/windows/console/mouse-event-record-str
|
||||||
|
// if `button_state` is negative then the wheel was rotated backward, toward the user.
|
||||||
|
if button_state.scroll_down() {
|
||||||
|
Some(MouseEventKind::ScrollDown)
|
||||||
|
} else if button_state.scroll_up() {
|
||||||
|
Some(MouseEventKind::ScrollUp)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EventFlags::MouseHwheeled => {
|
||||||
|
if button_state.scroll_left() {
|
||||||
|
Some(MouseEventKind::ScrollLeft)
|
||||||
|
} else if button_state.scroll_right() {
|
||||||
|
Some(MouseEventKind::ScrollRight)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(kind.map(|kind| MouseEvent {
|
||||||
|
kind,
|
||||||
|
column: xpos,
|
||||||
|
row: ypos,
|
||||||
|
modifiers,
|
||||||
|
}))
|
||||||
|
}
|
86
src/event/sys/windows/poll.rs
Normal file
86
src/event/sys/windows/poll.rs
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
use std::io;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use crossterm_winapi::Handle;
|
||||||
|
use winapi::{
|
||||||
|
shared::winerror::WAIT_TIMEOUT,
|
||||||
|
um::{
|
||||||
|
synchapi::WaitForMultipleObjects,
|
||||||
|
winbase::{INFINITE, WAIT_ABANDONED_0, WAIT_FAILED, WAIT_OBJECT_0},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[cfg(feature = "event-stream")]
|
||||||
|
pub(crate) use super::waker::Waker;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub(crate) struct WinApiPoll {
|
||||||
|
#[cfg(feature = "event-stream")]
|
||||||
|
waker: Waker,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WinApiPoll {
|
||||||
|
#[cfg(not(feature = "event-stream"))]
|
||||||
|
pub(crate) fn new() -> WinApiPoll {
|
||||||
|
WinApiPoll {}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "event-stream")]
|
||||||
|
pub(crate) fn new() -> std::io::Result<WinApiPoll> {
|
||||||
|
Ok(WinApiPoll {
|
||||||
|
waker: Waker::new()?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WinApiPoll {
|
||||||
|
pub fn poll(&mut self, timeout: Option<Duration>) -> std::io::Result<Option<bool>> {
|
||||||
|
let dw_millis = if let Some(duration) = timeout {
|
||||||
|
duration.as_millis() as u32
|
||||||
|
} else {
|
||||||
|
INFINITE
|
||||||
|
};
|
||||||
|
|
||||||
|
let console_handle = Handle::current_in_handle()?;
|
||||||
|
|
||||||
|
#[cfg(feature = "event-stream")]
|
||||||
|
let semaphore = self.waker.semaphore();
|
||||||
|
#[cfg(feature = "event-stream")]
|
||||||
|
let handles = &[*console_handle, **semaphore.handle()];
|
||||||
|
#[cfg(not(feature = "event-stream"))]
|
||||||
|
let handles = &[*console_handle];
|
||||||
|
|
||||||
|
let output =
|
||||||
|
unsafe { WaitForMultipleObjects(handles.len() as u32, handles.as_ptr(), 0, dw_millis) };
|
||||||
|
|
||||||
|
match output {
|
||||||
|
output if output == WAIT_OBJECT_0 => {
|
||||||
|
// input handle triggered
|
||||||
|
Ok(Some(true))
|
||||||
|
}
|
||||||
|
#[cfg(feature = "event-stream")]
|
||||||
|
output if output == WAIT_OBJECT_0 + 1 => {
|
||||||
|
// semaphore handle triggered
|
||||||
|
let _ = self.waker.reset();
|
||||||
|
Err(io::Error::new(
|
||||||
|
io::ErrorKind::Interrupted,
|
||||||
|
"Poll operation was woken up by `Waker::wake`",
|
||||||
|
))
|
||||||
|
}
|
||||||
|
WAIT_TIMEOUT | WAIT_ABANDONED_0 => {
|
||||||
|
// timeout elapsed
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
WAIT_FAILED => Err(io::Error::last_os_error()),
|
||||||
|
_ => Err(io::Error::new(
|
||||||
|
io::ErrorKind::Other,
|
||||||
|
"WaitForMultipleObjects returned unexpected result.",
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "event-stream")]
|
||||||
|
pub fn waker(&self) -> Waker {
|
||||||
|
self.waker.clone()
|
||||||
|
}
|
||||||
|
}
|
40
src/event/sys/windows/waker.rs
Normal file
40
src/event/sys/windows/waker.rs
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
use std::sync::{Arc, Mutex};
|
||||||
|
|
||||||
|
use crossterm_winapi::Semaphore;
|
||||||
|
|
||||||
|
/// Allows to wake up the `WinApiPoll::poll()` method.
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub(crate) struct Waker {
|
||||||
|
inner: Arc<Mutex<Semaphore>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Waker {
|
||||||
|
/// Creates a new waker.
|
||||||
|
///
|
||||||
|
/// `Waker` is based on the `Semaphore`. You have to use the semaphore
|
||||||
|
/// handle along with the `WaitForMultipleObjects`.
|
||||||
|
pub(crate) fn new() -> std::io::Result<Self> {
|
||||||
|
let inner = Semaphore::new()?;
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
inner: Arc::new(Mutex::new(inner)),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Wakes the `WaitForMultipleObjects`.
|
||||||
|
pub(crate) fn wake(&self) -> std::io::Result<()> {
|
||||||
|
self.inner.lock().unwrap().release()?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Replaces the current semaphore with a new one allowing us to reuse the same `Waker`.
|
||||||
|
pub(crate) fn reset(&self) -> std::io::Result<()> {
|
||||||
|
*self.inner.lock().unwrap() = Semaphore::new()?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the semaphore associated with the waker.
|
||||||
|
pub(crate) fn semaphore(&self) -> Semaphore {
|
||||||
|
self.inner.lock().unwrap().clone()
|
||||||
|
}
|
||||||
|
}
|
92
src/event/timeout.rs
Normal file
92
src/event/timeout.rs
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
|
/// Keeps track of the elapsed time since the moment the polling started.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct PollTimeout {
|
||||||
|
timeout: Option<Duration>,
|
||||||
|
start: Instant,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PollTimeout {
|
||||||
|
/// Constructs a new `PollTimeout` with the given optional `Duration`.
|
||||||
|
pub fn new(timeout: Option<Duration>) -> PollTimeout {
|
||||||
|
PollTimeout {
|
||||||
|
timeout,
|
||||||
|
start: Instant::now(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns whether the timeout has elapsed.
|
||||||
|
///
|
||||||
|
/// It always returns `false` if the initial timeout was set to `None`.
|
||||||
|
pub fn elapsed(&self) -> bool {
|
||||||
|
self.timeout
|
||||||
|
.map(|timeout| self.start.elapsed() >= timeout)
|
||||||
|
.unwrap_or(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the timeout leftover (initial timeout duration - elapsed duration).
|
||||||
|
pub fn leftover(&self) -> Option<Duration> {
|
||||||
|
self.timeout.map(|timeout| {
|
||||||
|
let elapsed = self.start.elapsed();
|
||||||
|
|
||||||
|
if elapsed >= timeout {
|
||||||
|
Duration::from_secs(0)
|
||||||
|
} else {
|
||||||
|
timeout - elapsed
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
|
use super::PollTimeout;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
pub fn test_timeout_without_duration_does_not_have_leftover() {
|
||||||
|
let timeout = PollTimeout::new(None);
|
||||||
|
assert_eq!(timeout.leftover(), None)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
pub fn test_timeout_without_duration_never_elapses() {
|
||||||
|
let timeout = PollTimeout::new(None);
|
||||||
|
assert!(!timeout.elapsed());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
pub fn test_timeout_elapses() {
|
||||||
|
const TIMEOUT_MILLIS: u64 = 100;
|
||||||
|
|
||||||
|
let timeout = PollTimeout {
|
||||||
|
timeout: Some(Duration::from_millis(TIMEOUT_MILLIS)),
|
||||||
|
start: Instant::now() - Duration::from_millis(2 * TIMEOUT_MILLIS),
|
||||||
|
};
|
||||||
|
|
||||||
|
assert!(timeout.elapsed());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
pub fn test_elapsed_timeout_has_zero_leftover() {
|
||||||
|
const TIMEOUT_MILLIS: u64 = 100;
|
||||||
|
|
||||||
|
let timeout = PollTimeout {
|
||||||
|
timeout: Some(Duration::from_millis(TIMEOUT_MILLIS)),
|
||||||
|
start: Instant::now() - Duration::from_millis(2 * TIMEOUT_MILLIS),
|
||||||
|
};
|
||||||
|
|
||||||
|
assert!(timeout.elapsed());
|
||||||
|
assert_eq!(timeout.leftover(), Some(Duration::from_millis(0)));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
pub fn test_not_elapsed_timeout_has_positive_leftover() {
|
||||||
|
let timeout = PollTimeout::new(Some(Duration::from_secs(60)));
|
||||||
|
|
||||||
|
assert!(!timeout.elapsed());
|
||||||
|
assert!(timeout.leftover().unwrap() > Duration::from_secs(0));
|
||||||
|
}
|
||||||
|
}
|
15
src/lib.rs
15
src/lib.rs
@ -241,5 +241,20 @@ pub mod style;
|
|||||||
/// A module to work with the terminal.
|
/// A module to work with the terminal.
|
||||||
pub mod terminal;
|
pub mod terminal;
|
||||||
|
|
||||||
|
/// A module to query if the current instance is a tty.
|
||||||
|
pub mod tty;
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
|
/// A module that exposes one function to check if the current terminal supports ANSI sequences.
|
||||||
|
pub mod ansi_support;
|
||||||
mod command;
|
mod command;
|
||||||
pub(crate) mod macros;
|
pub(crate) mod macros;
|
||||||
|
|
||||||
|
#[cfg(all(windows, not(feature = "windows")))]
|
||||||
|
compile_error!("Compiling on Windows with \"windows\" feature disabled. Feature \"windows\" should only be disabled when project will never be compiled on Windows.");
|
||||||
|
|
||||||
|
#[cfg(all(winapi, not(feature = "winapi")))]
|
||||||
|
compile_error!("Compiling on Windows with \"winapi\" feature disabled. Feature \"winapi\" should only be disabled when project will never be compiled on Windows.");
|
||||||
|
|
||||||
|
#[cfg(all(crossterm_winapi, not(feature = "crossterm_winapi")))]
|
||||||
|
compile_error!("Compiling on Windows with \"crossterm_winapi\" feature disabled. Feature \"crossterm_winapi\" should only be disabled when project will never be compiled on Windows.");
|
||||||
|
116
src/style.rs
116
src/style.rs
@ -161,23 +161,21 @@ pub fn style<D: Display>(val: D) -> StyledContent<D> {
|
|||||||
///
|
///
|
||||||
/// This does not always provide a good result.
|
/// This does not always provide a good result.
|
||||||
pub fn available_color_count() -> u16 {
|
pub fn available_color_count() -> u16 {
|
||||||
#[cfg(windows)]
|
env::var("TERM")
|
||||||
{
|
.map(|x| if x.contains("256color") { 256 } else { 8 })
|
||||||
// Check if we're running in a pseudo TTY, which supports true color.
|
.unwrap_or(8)
|
||||||
// Fall back to env vars otherwise for other terminals on Windows.
|
}
|
||||||
if crate::ansi_support::supports_ansi() {
|
|
||||||
return u16::MAX;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const DEFAULT: u16 = 8;
|
/// Forces colored output on or off globally, overriding NO_COLOR.
|
||||||
env::var("COLORTERM")
|
///
|
||||||
.or_else(|_| env::var("TERM"))
|
/// # Notes
|
||||||
.map_or(DEFAULT, |x| match x {
|
///
|
||||||
_ if x.contains("24bit") || x.contains("truecolor") => u16::MAX,
|
/// crossterm supports NO_COLOR (https://no-color.org/) to disabled colored output.
|
||||||
_ if x.contains("256") => 256,
|
///
|
||||||
_ => DEFAULT,
|
/// This API allows applications to override that behavior and force colorized output
|
||||||
})
|
/// even if NO_COLOR is set.
|
||||||
|
pub fn force_color_output(enabled: bool) {
|
||||||
|
Colored::set_ansi_color_disabled(!enabled)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A command that sets the the foreground color.
|
/// A command that sets the the foreground color.
|
||||||
@ -521,89 +519,3 @@ impl_display!(for ResetColor);
|
|||||||
fn parse_next_u8<'a>(iter: &mut impl Iterator<Item = &'a str>) -> Option<u8> {
|
fn parse_next_u8<'a>(iter: &mut impl Iterator<Item = &'a str>) -> Option<u8> {
|
||||||
iter.next().and_then(|s| s.parse().ok())
|
iter.next().and_then(|s| s.parse().ok())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
// On Windows many env var tests will fail so we need to conditionally check for ANSI support.
|
|
||||||
// This allows other terminals on Windows to still assert env var support.
|
|
||||||
macro_rules! skip_windows_ansi_supported {
|
|
||||||
() => {
|
|
||||||
#[cfg(windows)]
|
|
||||||
{
|
|
||||||
if crate::ansi_support::supports_ansi() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg_attr(windows, test)]
|
|
||||||
#[cfg(windows)]
|
|
||||||
fn windows_always_truecolor() {
|
|
||||||
// This should always be true on supported Windows 10+,
|
|
||||||
// but downlevel Windows clients and other terminals may fail `cargo test` otherwise.
|
|
||||||
if crate::ansi_support::supports_ansi() {
|
|
||||||
assert_eq!(u16::MAX, available_color_count());
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn colorterm_overrides_term() {
|
|
||||||
skip_windows_ansi_supported!();
|
|
||||||
temp_env::with_vars(
|
|
||||||
[
|
|
||||||
("COLORTERM", Some("truecolor")),
|
|
||||||
("TERM", Some("xterm-256color")),
|
|
||||||
],
|
|
||||||
|| {
|
|
||||||
assert_eq!(u16::MAX, available_color_count());
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn term_24bits() {
|
|
||||||
skip_windows_ansi_supported!();
|
|
||||||
temp_env::with_vars(
|
|
||||||
[("COLORTERM", None), ("TERM", Some("xterm-24bits"))],
|
|
||||||
|| {
|
|
||||||
assert_eq!(u16::MAX, available_color_count());
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn term_256color() {
|
|
||||||
skip_windows_ansi_supported!();
|
|
||||||
temp_env::with_vars(
|
|
||||||
[("COLORTERM", None), ("TERM", Some("xterm-256color"))],
|
|
||||||
|| {
|
|
||||||
assert_eq!(256u16, available_color_count());
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn default_color_count() {
|
|
||||||
skip_windows_ansi_supported!();
|
|
||||||
temp_env::with_vars([("COLORTERM", None::<&str>), ("TERM", None)], || {
|
|
||||||
assert_eq!(8, available_color_count());
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn unsupported_term_colorterm_values() {
|
|
||||||
skip_windows_ansi_supported!();
|
|
||||||
temp_env::with_vars(
|
|
||||||
[
|
|
||||||
("COLORTERM", Some("gibberish")),
|
|
||||||
("TERM", Some("gibberish")),
|
|
||||||
],
|
|
||||||
|| {
|
|
||||||
assert_eq!(8u16, available_color_count());
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
|
use parking_lot::Once;
|
||||||
use std::fmt::{self, Formatter};
|
use std::fmt::{self, Formatter};
|
||||||
|
use std::sync::atomic::{AtomicBool, Ordering};
|
||||||
|
|
||||||
#[cfg(feature = "serde")]
|
#[cfg(feature = "serde")]
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
@ -17,10 +19,13 @@ pub enum Colored {
|
|||||||
/// A background color.
|
/// A background color.
|
||||||
BackgroundColor(Color),
|
BackgroundColor(Color),
|
||||||
/// An underline color.
|
/// An underline color.
|
||||||
/// Important: doesn't work on windows 10 or lower.
|
/// Imporant: doesnt work on windows 10 or lower.
|
||||||
UnderlineColor(Color),
|
UnderlineColor(Color),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static ANSI_COLOR_DISABLED: AtomicBool = AtomicBool::new(false);
|
||||||
|
static INITIALIZER: Once = Once::new();
|
||||||
|
|
||||||
impl Colored {
|
impl Colored {
|
||||||
/// Parse an ANSI foreground or background color.
|
/// Parse an ANSI foreground or background color.
|
||||||
/// This is the string that would appear within an `ESC [ <str> m` escape sequence, as found in
|
/// This is the string that would appear within an `ESC [ <str> m` escape sequence, as found in
|
||||||
@ -64,12 +69,38 @@ impl Colored {
|
|||||||
|
|
||||||
Some(output)
|
Some(output)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Checks whether ansi color sequences are disabled by setting of NO_COLOR
|
||||||
|
/// in environment as per https://no-color.org/
|
||||||
|
pub fn ansi_color_disabled() -> bool {
|
||||||
|
!std::env::var("NO_COLOR")
|
||||||
|
.unwrap_or("".to_string())
|
||||||
|
.is_empty()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn ansi_color_disabled_memoized() -> bool {
|
||||||
|
INITIALIZER.call_once(|| {
|
||||||
|
ANSI_COLOR_DISABLED.store(Self::ansi_color_disabled(), Ordering::SeqCst);
|
||||||
|
});
|
||||||
|
|
||||||
|
ANSI_COLOR_DISABLED.load(Ordering::SeqCst)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_ansi_color_disabled(val: bool) {
|
||||||
|
// Force the one-time initializer to run.
|
||||||
|
_ = Self::ansi_color_disabled_memoized();
|
||||||
|
ANSI_COLOR_DISABLED.store(val, Ordering::SeqCst);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Display for Colored {
|
impl fmt::Display for Colored {
|
||||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||||
let color;
|
let color;
|
||||||
|
|
||||||
|
if Self::ansi_color_disabled_memoized() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
match *self {
|
match *self {
|
||||||
Colored::ForegroundColor(new_color) => {
|
Colored::ForegroundColor(new_color) => {
|
||||||
if new_color == Color::Reset {
|
if new_color == Color::Reset {
|
||||||
|
103
src/terminal.rs
103
src/terminal.rs
@ -83,15 +83,60 @@
|
|||||||
//!
|
//!
|
||||||
//! For manual execution control check out [crossterm::queue](../macro.queue.html).
|
//! For manual execution control check out [crossterm::queue](../macro.queue.html).
|
||||||
|
|
||||||
use std::fmt;
|
use std::{fmt, io};
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
|
use crossterm_winapi::{ConsoleMode, Handle, ScreenBuffer};
|
||||||
#[cfg(feature = "serde")]
|
#[cfg(feature = "serde")]
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
#[cfg(windows)]
|
||||||
|
use winapi::um::wincon::ENABLE_WRAP_AT_EOL_OUTPUT;
|
||||||
|
|
||||||
#[doc(no_inline)]
|
#[doc(no_inline)]
|
||||||
use crate::Command;
|
use crate::Command;
|
||||||
use crate::{csi, impl_display};
|
use crate::{csi, impl_display};
|
||||||
|
|
||||||
|
pub(crate) mod sys;
|
||||||
|
|
||||||
|
#[cfg(feature = "events")]
|
||||||
|
pub use sys::supports_keyboard_enhancement;
|
||||||
|
|
||||||
|
/// Tells whether the raw mode is enabled.
|
||||||
|
///
|
||||||
|
/// Please have a look at the [raw mode](./index.html#raw-mode) section.
|
||||||
|
pub fn is_raw_mode_enabled() -> io::Result<bool> {
|
||||||
|
#[cfg(unix)]
|
||||||
|
{
|
||||||
|
Ok(sys::is_raw_mode_enabled())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
|
{
|
||||||
|
sys::is_raw_mode_enabled()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Enables raw mode.
|
||||||
|
///
|
||||||
|
/// Please have a look at the [raw mode](./index.html#raw-mode) section.
|
||||||
|
pub fn enable_raw_mode() -> io::Result<()> {
|
||||||
|
sys::enable_raw_mode()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Disables raw mode.
|
||||||
|
///
|
||||||
|
/// Please have a look at the [raw mode](./index.html#raw-mode) section.
|
||||||
|
pub fn disable_raw_mode() -> io::Result<()> {
|
||||||
|
sys::disable_raw_mode()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the terminal size `(columns, rows)`.
|
||||||
|
///
|
||||||
|
/// The top left cell is represented `(1, 1)`.
|
||||||
|
pub fn size() -> io::Result<(u16, u16)> {
|
||||||
|
sys::size()
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct WindowSize {
|
pub struct WindowSize {
|
||||||
pub rows: u16,
|
pub rows: u16,
|
||||||
@ -100,6 +145,15 @@ pub struct WindowSize {
|
|||||||
pub height: u16,
|
pub height: u16,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns the terminal size `[WindowSize]`.
|
||||||
|
///
|
||||||
|
/// The width and height in pixels may not be reliably implemented or default to 0.
|
||||||
|
/// For unix, https://man7.org/linux/man-pages/man4/tty_ioctl.4.html documents them as "unused".
|
||||||
|
/// For windows it is not implemented.
|
||||||
|
pub fn window_size() -> io::Result<WindowSize> {
|
||||||
|
sys::window_size()
|
||||||
|
}
|
||||||
|
|
||||||
/// Disables line wrapping.
|
/// Disables line wrapping.
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
pub struct DisableLineWrap;
|
pub struct DisableLineWrap;
|
||||||
@ -247,6 +301,11 @@ impl Command for ScrollUp {
|
|||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
|
fn execute_winapi(&self) -> io::Result<()> {
|
||||||
|
sys::scroll_up(self.0)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A command that scrolls the terminal screen a given number of rows down.
|
/// A command that scrolls the terminal screen a given number of rows down.
|
||||||
@ -264,6 +323,11 @@ impl Command for ScrollDown {
|
|||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
|
fn execute_winapi(&self) -> io::Result<()> {
|
||||||
|
sys::scroll_down(self.0)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A command that clears the terminal screen buffer.
|
/// A command that clears the terminal screen buffer.
|
||||||
@ -287,6 +351,11 @@ impl Command for Clear {
|
|||||||
ClearType::UntilNewLine => csi!("K"),
|
ClearType::UntilNewLine => csi!("K"),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
|
fn execute_winapi(&self) -> io::Result<()> {
|
||||||
|
sys::clear(self.0)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A command that sets the terminal buffer size `(columns, rows)`.
|
/// A command that sets the terminal buffer size `(columns, rows)`.
|
||||||
@ -301,6 +370,11 @@ impl Command for SetSize {
|
|||||||
fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result {
|
fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result {
|
||||||
write!(f, csi!("8;{};{}t"), self.1, self.0)
|
write!(f, csi!("8;{};{}t"), self.1, self.0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
|
fn execute_winapi(&self) -> io::Result<()> {
|
||||||
|
sys::set_size(self.0, self.1)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A command that sets the terminal title
|
/// A command that sets the terminal title
|
||||||
@ -315,6 +389,11 @@ impl<T: fmt::Display> Command for SetTitle<T> {
|
|||||||
fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result {
|
fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result {
|
||||||
write!(f, "\x1B]0;{}\x07", &self.0)
|
write!(f, "\x1B]0;{}\x07", &self.0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
|
fn execute_winapi(&self) -> io::Result<()> {
|
||||||
|
sys::set_window_title(&self.0)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A command that instructs the terminal emulator to begin a synchronized frame.
|
/// A command that instructs the terminal emulator to begin a synchronized frame.
|
||||||
@ -357,6 +436,17 @@ impl Command for BeginSynchronizedUpdate {
|
|||||||
fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result {
|
fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result {
|
||||||
f.write_str(csi!("?2026h"))
|
f.write_str(csi!("?2026h"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
|
fn execute_winapi(&self) -> io::Result<()> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
|
#[inline]
|
||||||
|
fn is_ansi_code_supported(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A command that instructs the terminal to end a synchronized frame.
|
/// A command that instructs the terminal to end a synchronized frame.
|
||||||
@ -399,6 +489,17 @@ impl Command for EndSynchronizedUpdate {
|
|||||||
fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result {
|
fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result {
|
||||||
f.write_str(csi!("?2026l"))
|
f.write_str(csi!("?2026l"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
|
fn execute_winapi(&self) -> io::Result<()> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
|
#[inline]
|
||||||
|
fn is_ansi_code_supported(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl_display!(for ScrollUp);
|
impl_display!(for ScrollUp);
|
||||||
|
27
src/terminal/sys.rs
Normal file
27
src/terminal/sys.rs
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
//! This module provides platform related functions.
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
#[cfg(feature = "events")]
|
||||||
|
pub use self::unix::supports_keyboard_enhancement;
|
||||||
|
#[cfg(unix)]
|
||||||
|
pub(crate) use self::unix::{
|
||||||
|
disable_raw_mode, enable_raw_mode, is_raw_mode_enabled, size, window_size,
|
||||||
|
};
|
||||||
|
#[cfg(windows)]
|
||||||
|
#[cfg(feature = "events")]
|
||||||
|
pub use self::windows::supports_keyboard_enhancement;
|
||||||
|
#[cfg(all(windows, test))]
|
||||||
|
pub(crate) use self::windows::temp_screen_buffer;
|
||||||
|
#[cfg(windows)]
|
||||||
|
pub(crate) use self::windows::{
|
||||||
|
clear, disable_raw_mode, enable_raw_mode, is_raw_mode_enabled, scroll_down, scroll_up,
|
||||||
|
set_size, set_window_title, size, window_size,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
|
mod windows;
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
pub mod file_descriptor;
|
||||||
|
#[cfg(unix)]
|
||||||
|
mod unix;
|
154
src/terminal/sys/file_descriptor.rs
Normal file
154
src/terminal/sys/file_descriptor.rs
Normal file
@ -0,0 +1,154 @@
|
|||||||
|
use std::io;
|
||||||
|
|
||||||
|
#[cfg(feature = "libc")]
|
||||||
|
use libc::size_t;
|
||||||
|
#[cfg(not(feature = "libc"))]
|
||||||
|
use rustix::fd::{AsFd, AsRawFd, BorrowedFd, OwnedFd, RawFd};
|
||||||
|
#[cfg(feature = "libc")]
|
||||||
|
use std::{
|
||||||
|
fs,
|
||||||
|
marker::PhantomData,
|
||||||
|
os::unix::{
|
||||||
|
io::{IntoRawFd, RawFd},
|
||||||
|
prelude::AsRawFd,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/// A file descriptor wrapper.
|
||||||
|
///
|
||||||
|
/// It allows to retrieve raw file descriptor, write to the file descriptor and
|
||||||
|
/// mainly it closes the file descriptor once dropped.
|
||||||
|
#[derive(Debug)]
|
||||||
|
#[cfg(feature = "libc")]
|
||||||
|
pub struct FileDesc<'a> {
|
||||||
|
fd: RawFd,
|
||||||
|
close_on_drop: bool,
|
||||||
|
phantom: PhantomData<&'a ()>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(feature = "libc"))]
|
||||||
|
pub enum FileDesc<'a> {
|
||||||
|
Owned(OwnedFd),
|
||||||
|
Borrowed(BorrowedFd<'a>),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "libc")]
|
||||||
|
impl FileDesc<'_> {
|
||||||
|
/// Constructs a new `FileDesc` with the given `RawFd`.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// * `fd` - raw file descriptor
|
||||||
|
/// * `close_on_drop` - specify if the raw file descriptor should be closed once the `FileDesc` is dropped
|
||||||
|
pub fn new(fd: RawFd, close_on_drop: bool) -> FileDesc<'static> {
|
||||||
|
FileDesc {
|
||||||
|
fd,
|
||||||
|
close_on_drop,
|
||||||
|
phantom: PhantomData,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn read(&self, buffer: &mut [u8]) -> io::Result<usize> {
|
||||||
|
let result = unsafe {
|
||||||
|
libc::read(
|
||||||
|
self.fd,
|
||||||
|
buffer.as_mut_ptr() as *mut libc::c_void,
|
||||||
|
buffer.len() as size_t,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
if result < 0 {
|
||||||
|
Err(io::Error::last_os_error())
|
||||||
|
} else {
|
||||||
|
Ok(result as usize)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the underlying file descriptor.
|
||||||
|
pub fn raw_fd(&self) -> RawFd {
|
||||||
|
self.fd
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(feature = "libc"))]
|
||||||
|
impl FileDesc<'_> {
|
||||||
|
pub fn read(&self, buffer: &mut [u8]) -> io::Result<usize> {
|
||||||
|
let fd = match self {
|
||||||
|
FileDesc::Owned(fd) => fd.as_fd(),
|
||||||
|
FileDesc::Borrowed(fd) => fd.as_fd(),
|
||||||
|
};
|
||||||
|
let result = rustix::io::read(fd, buffer)?;
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn raw_fd(&self) -> RawFd {
|
||||||
|
match self {
|
||||||
|
FileDesc::Owned(fd) => fd.as_raw_fd(),
|
||||||
|
FileDesc::Borrowed(fd) => fd.as_raw_fd(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "libc")]
|
||||||
|
impl Drop for FileDesc<'_> {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
if self.close_on_drop {
|
||||||
|
// Note that errors are ignored when closing a file descriptor. The
|
||||||
|
// reason for this is that if an error occurs we don't actually know if
|
||||||
|
// the file descriptor was closed or not, and if we retried (for
|
||||||
|
// something like EINTR), we might close another valid file descriptor
|
||||||
|
// opened after we closed ours.
|
||||||
|
let _ = unsafe { libc::close(self.fd) };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AsRawFd for FileDesc<'_> {
|
||||||
|
fn as_raw_fd(&self) -> RawFd {
|
||||||
|
self.raw_fd()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(feature = "libc"))]
|
||||||
|
impl AsFd for FileDesc<'_> {
|
||||||
|
fn as_fd(&self) -> BorrowedFd<'_> {
|
||||||
|
match self {
|
||||||
|
FileDesc::Owned(fd) => fd.as_fd(),
|
||||||
|
FileDesc::Borrowed(fd) => fd.as_fd(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "libc")]
|
||||||
|
/// Creates a file descriptor pointing to the standard input or `/dev/tty`.
|
||||||
|
pub fn tty_fd() -> io::Result<FileDesc<'static>> {
|
||||||
|
let (fd, close_on_drop) = if unsafe { libc::isatty(libc::STDIN_FILENO) == 1 } {
|
||||||
|
(libc::STDIN_FILENO, false)
|
||||||
|
} else {
|
||||||
|
(
|
||||||
|
fs::OpenOptions::new()
|
||||||
|
.read(true)
|
||||||
|
.write(true)
|
||||||
|
.open("/dev/tty")?
|
||||||
|
.into_raw_fd(),
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(FileDesc::new(fd, close_on_drop))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(feature = "libc"))]
|
||||||
|
/// Creates a file descriptor pointing to the standard input or `/dev/tty`.
|
||||||
|
pub fn tty_fd() -> io::Result<FileDesc<'static>> {
|
||||||
|
use std::fs::File;
|
||||||
|
|
||||||
|
let stdin = rustix::stdio::stdin();
|
||||||
|
let fd = if rustix::termios::isatty(stdin) {
|
||||||
|
FileDesc::Borrowed(stdin)
|
||||||
|
} else {
|
||||||
|
let dev_tty = File::options().read(true).write(true).open("/dev/tty")?;
|
||||||
|
FileDesc::Owned(dev_tty.into())
|
||||||
|
};
|
||||||
|
Ok(fd)
|
||||||
|
}
|
315
src/terminal/sys/unix.rs
Normal file
315
src/terminal/sys/unix.rs
Normal file
@ -0,0 +1,315 @@
|
|||||||
|
//! UNIX related logic for terminal manipulation.
|
||||||
|
|
||||||
|
use crate::terminal::{
|
||||||
|
sys::file_descriptor::{tty_fd, FileDesc},
|
||||||
|
WindowSize,
|
||||||
|
};
|
||||||
|
#[cfg(feature = "libc")]
|
||||||
|
use libc::{
|
||||||
|
cfmakeraw, ioctl, tcgetattr, tcsetattr, termios as Termios, winsize, STDOUT_FILENO, TCSANOW,
|
||||||
|
TIOCGWINSZ,
|
||||||
|
};
|
||||||
|
use parking_lot::Mutex;
|
||||||
|
#[cfg(not(feature = "libc"))]
|
||||||
|
use rustix::{
|
||||||
|
fd::AsFd,
|
||||||
|
termios::{Termios, Winsize},
|
||||||
|
};
|
||||||
|
|
||||||
|
use std::{fs::File, io, process};
|
||||||
|
#[cfg(feature = "libc")]
|
||||||
|
use std::{
|
||||||
|
mem,
|
||||||
|
os::unix::io::{IntoRawFd, RawFd},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Some(Termios) -> we're in the raw mode and this is the previous mode
|
||||||
|
// None -> we're not in the raw mode
|
||||||
|
static TERMINAL_MODE_PRIOR_RAW_MODE: Mutex<Option<Termios>> = parking_lot::const_mutex(None);
|
||||||
|
|
||||||
|
pub(crate) fn is_raw_mode_enabled() -> bool {
|
||||||
|
TERMINAL_MODE_PRIOR_RAW_MODE.lock().is_some()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "libc")]
|
||||||
|
impl From<winsize> for WindowSize {
|
||||||
|
fn from(size: winsize) -> WindowSize {
|
||||||
|
WindowSize {
|
||||||
|
columns: size.ws_col,
|
||||||
|
rows: size.ws_row,
|
||||||
|
width: size.ws_xpixel,
|
||||||
|
height: size.ws_ypixel,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[cfg(not(feature = "libc"))]
|
||||||
|
impl From<Winsize> for WindowSize {
|
||||||
|
fn from(size: Winsize) -> WindowSize {
|
||||||
|
WindowSize {
|
||||||
|
columns: size.ws_col,
|
||||||
|
rows: size.ws_row,
|
||||||
|
width: size.ws_xpixel,
|
||||||
|
height: size.ws_ypixel,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::useless_conversion)]
|
||||||
|
#[cfg(feature = "libc")]
|
||||||
|
pub(crate) fn window_size() -> io::Result<WindowSize> {
|
||||||
|
// http://rosettacode.org/wiki/Terminal_control/Dimensions#Library:_BSD_libc
|
||||||
|
let mut size = winsize {
|
||||||
|
ws_row: 0,
|
||||||
|
ws_col: 0,
|
||||||
|
ws_xpixel: 0,
|
||||||
|
ws_ypixel: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
let file = File::open("/dev/tty").map(|file| (FileDesc::new(file.into_raw_fd(), true)));
|
||||||
|
let fd = if let Ok(file) = &file {
|
||||||
|
file.raw_fd()
|
||||||
|
} else {
|
||||||
|
// Fallback to libc::STDOUT_FILENO if /dev/tty is missing
|
||||||
|
STDOUT_FILENO
|
||||||
|
};
|
||||||
|
|
||||||
|
if wrap_with_result(unsafe { ioctl(fd, TIOCGWINSZ.into(), &mut size) }).is_ok() {
|
||||||
|
return Ok(size.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
Err(std::io::Error::last_os_error().into())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(feature = "libc"))]
|
||||||
|
pub(crate) fn window_size() -> io::Result<WindowSize> {
|
||||||
|
let file = File::open("/dev/tty").map(|file| (FileDesc::Owned(file.into())));
|
||||||
|
let fd = if let Ok(file) = &file {
|
||||||
|
file.as_fd()
|
||||||
|
} else {
|
||||||
|
// Fallback to libc::STDOUT_FILENO if /dev/tty is missing
|
||||||
|
rustix::stdio::stdout()
|
||||||
|
};
|
||||||
|
let size = rustix::termios::tcgetwinsize(fd)?;
|
||||||
|
Ok(size.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::useless_conversion)]
|
||||||
|
pub(crate) fn size() -> io::Result<(u16, u16)> {
|
||||||
|
if let Ok(window_size) = window_size() {
|
||||||
|
return Ok((window_size.columns, window_size.rows));
|
||||||
|
}
|
||||||
|
|
||||||
|
tput_size().ok_or_else(|| std::io::Error::last_os_error().into())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "libc")]
|
||||||
|
pub(crate) fn enable_raw_mode() -> io::Result<()> {
|
||||||
|
let mut original_mode = TERMINAL_MODE_PRIOR_RAW_MODE.lock();
|
||||||
|
if original_mode.is_some() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let tty = tty_fd()?;
|
||||||
|
let fd = tty.raw_fd();
|
||||||
|
let mut ios = get_terminal_attr(fd)?;
|
||||||
|
let original_mode_ios = ios;
|
||||||
|
raw_terminal_attr(&mut ios);
|
||||||
|
set_terminal_attr(fd, &ios)?;
|
||||||
|
// Keep it last - set the original mode only if we were able to switch to the raw mode
|
||||||
|
*original_mode = Some(original_mode_ios);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(feature = "libc"))]
|
||||||
|
pub(crate) fn enable_raw_mode() -> io::Result<()> {
|
||||||
|
let mut original_mode = TERMINAL_MODE_PRIOR_RAW_MODE.lock();
|
||||||
|
if original_mode.is_some() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let tty = tty_fd()?;
|
||||||
|
let mut ios = get_terminal_attr(&tty)?;
|
||||||
|
let original_mode_ios = ios.clone();
|
||||||
|
ios.make_raw();
|
||||||
|
set_terminal_attr(&tty, &ios)?;
|
||||||
|
// Keep it last - set the original mode only if we were able to switch to the raw mode
|
||||||
|
*original_mode = Some(original_mode_ios);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reset the raw mode.
|
||||||
|
///
|
||||||
|
/// More precisely, reset the whole termios mode to what it was before the first call
|
||||||
|
/// to [enable_raw_mode]. If you don't mess with termios outside of crossterm, it's
|
||||||
|
/// effectively disabling the raw mode and doing nothing else.
|
||||||
|
#[cfg(feature = "libc")]
|
||||||
|
pub(crate) fn disable_raw_mode() -> io::Result<()> {
|
||||||
|
let mut original_mode = TERMINAL_MODE_PRIOR_RAW_MODE.lock();
|
||||||
|
if let Some(original_mode_ios) = original_mode.as_ref() {
|
||||||
|
let tty = tty_fd()?;
|
||||||
|
set_terminal_attr(tty.raw_fd(), original_mode_ios)?;
|
||||||
|
// Keep it last - remove the original mode only if we were able to switch back
|
||||||
|
*original_mode = None;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(feature = "libc"))]
|
||||||
|
pub(crate) fn disable_raw_mode() -> io::Result<()> {
|
||||||
|
let mut original_mode = TERMINAL_MODE_PRIOR_RAW_MODE.lock();
|
||||||
|
if let Some(original_mode_ios) = original_mode.as_ref() {
|
||||||
|
let tty = tty_fd()?;
|
||||||
|
set_terminal_attr(&tty, original_mode_ios)?;
|
||||||
|
// Keep it last - remove the original mode only if we were able to switch back
|
||||||
|
*original_mode = None;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(feature = "libc"))]
|
||||||
|
fn get_terminal_attr(fd: impl AsFd) -> io::Result<Termios> {
|
||||||
|
let result = rustix::termios::tcgetattr(fd)?;
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(feature = "libc"))]
|
||||||
|
fn set_terminal_attr(fd: impl AsFd, termios: &Termios) -> io::Result<()> {
|
||||||
|
rustix::termios::tcsetattr(fd, rustix::termios::OptionalActions::Now, termios)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Queries the terminal's support for progressive keyboard enhancement.
|
||||||
|
///
|
||||||
|
/// On unix systems, this function will block and possibly time out while
|
||||||
|
/// [`crossterm::event::read`](crate::event::read) or [`crossterm::event::poll`](crate::event::poll) are being called.
|
||||||
|
#[cfg(feature = "events")]
|
||||||
|
pub fn supports_keyboard_enhancement() -> io::Result<bool> {
|
||||||
|
if is_raw_mode_enabled() {
|
||||||
|
read_supports_keyboard_enhancement_raw()
|
||||||
|
} else {
|
||||||
|
read_supports_keyboard_enhancement_flags()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "events")]
|
||||||
|
fn read_supports_keyboard_enhancement_flags() -> io::Result<bool> {
|
||||||
|
enable_raw_mode()?;
|
||||||
|
let flags = read_supports_keyboard_enhancement_raw();
|
||||||
|
disable_raw_mode()?;
|
||||||
|
flags
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "events")]
|
||||||
|
fn read_supports_keyboard_enhancement_raw() -> io::Result<bool> {
|
||||||
|
use crate::event::{
|
||||||
|
filter::{KeyboardEnhancementFlagsFilter, PrimaryDeviceAttributesFilter},
|
||||||
|
poll_internal, read_internal, InternalEvent,
|
||||||
|
};
|
||||||
|
use std::io::Write;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
// This is the recommended method for testing support for the keyboard enhancement protocol.
|
||||||
|
// We send a query for the flags supported by the terminal and then the primary device attributes
|
||||||
|
// query. If we receive the primary device attributes response but not the keyboard enhancement
|
||||||
|
// flags, none of the flags are supported.
|
||||||
|
//
|
||||||
|
// See <https://sw.kovidgoyal.net/kitty/keyboard-protocol/#detection-of-support-for-this-protocol>
|
||||||
|
|
||||||
|
// ESC [ ? u Query progressive keyboard enhancement flags (kitty protocol).
|
||||||
|
// ESC [ c Query primary device attributes.
|
||||||
|
const QUERY: &[u8] = b"\x1B[?u\x1B[c";
|
||||||
|
|
||||||
|
let result = File::open("/dev/tty").and_then(|mut file| {
|
||||||
|
file.write_all(QUERY)?;
|
||||||
|
file.flush()
|
||||||
|
});
|
||||||
|
if result.is_err() {
|
||||||
|
let mut stdout = io::stdout();
|
||||||
|
stdout.write_all(QUERY)?;
|
||||||
|
stdout.flush()?;
|
||||||
|
}
|
||||||
|
|
||||||
|
loop {
|
||||||
|
match poll_internal(
|
||||||
|
Some(Duration::from_millis(2000)),
|
||||||
|
&KeyboardEnhancementFlagsFilter,
|
||||||
|
) {
|
||||||
|
Ok(true) => {
|
||||||
|
match read_internal(&KeyboardEnhancementFlagsFilter) {
|
||||||
|
Ok(InternalEvent::KeyboardEnhancementFlags(_current_flags)) => {
|
||||||
|
// Flush the PrimaryDeviceAttributes out of the event queue.
|
||||||
|
read_internal(&PrimaryDeviceAttributesFilter).ok();
|
||||||
|
return Ok(true);
|
||||||
|
}
|
||||||
|
_ => return Ok(false),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(false) => {
|
||||||
|
return Err(io::Error::new(
|
||||||
|
io::ErrorKind::Other,
|
||||||
|
"The keyboard enhancement status could not be read within a normal duration",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Err(_) => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// execute tput with the given argument and parse
|
||||||
|
/// the output as a u16.
|
||||||
|
///
|
||||||
|
/// The arg should be "cols" or "lines"
|
||||||
|
fn tput_value(arg: &str) -> Option<u16> {
|
||||||
|
let output = process::Command::new("tput").arg(arg).output().ok()?;
|
||||||
|
let value = output
|
||||||
|
.stdout
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|b| char::from(b).to_digit(10))
|
||||||
|
.fold(0, |v, n| v * 10 + n as u16);
|
||||||
|
|
||||||
|
if value > 0 {
|
||||||
|
Some(value)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the size of the screen as determined by tput.
|
||||||
|
///
|
||||||
|
/// This alternate way of computing the size is useful
|
||||||
|
/// when in a subshell.
|
||||||
|
fn tput_size() -> Option<(u16, u16)> {
|
||||||
|
match (tput_value("cols"), tput_value("lines")) {
|
||||||
|
(Some(w), Some(h)) => Some((w, h)),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "libc")]
|
||||||
|
// Transform the given mode into an raw mode (non-canonical) mode.
|
||||||
|
fn raw_terminal_attr(termios: &mut Termios) {
|
||||||
|
unsafe { cfmakeraw(termios) }
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "libc")]
|
||||||
|
fn get_terminal_attr(fd: RawFd) -> io::Result<Termios> {
|
||||||
|
unsafe {
|
||||||
|
let mut termios = mem::zeroed();
|
||||||
|
wrap_with_result(tcgetattr(fd, &mut termios))?;
|
||||||
|
Ok(termios)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "libc")]
|
||||||
|
fn set_terminal_attr(fd: RawFd, termios: &Termios) -> io::Result<()> {
|
||||||
|
wrap_with_result(unsafe { tcsetattr(fd, TCSANOW, termios) })
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "libc")]
|
||||||
|
fn wrap_with_result(result: i32) -> io::Result<()> {
|
||||||
|
if result == -1 {
|
||||||
|
Err(io::Error::last_os_error())
|
||||||
|
} else {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
471
src/terminal/sys/windows.rs
Normal file
471
src/terminal/sys/windows.rs
Normal file
@ -0,0 +1,471 @@
|
|||||||
|
//! WinAPI related logic for terminal manipulation.
|
||||||
|
|
||||||
|
use std::fmt::{self, Write};
|
||||||
|
use std::io::{self};
|
||||||
|
|
||||||
|
use crossterm_winapi::{Console, ConsoleMode, Coord, Handle, ScreenBuffer, Size};
|
||||||
|
use winapi::{
|
||||||
|
shared::minwindef::DWORD,
|
||||||
|
um::wincon::{SetConsoleTitleW, ENABLE_ECHO_INPUT, ENABLE_LINE_INPUT, ENABLE_PROCESSED_INPUT},
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
cursor,
|
||||||
|
terminal::{ClearType, WindowSize},
|
||||||
|
};
|
||||||
|
|
||||||
|
/// bits which can't be set in raw mode
|
||||||
|
const NOT_RAW_MODE_MASK: DWORD = ENABLE_LINE_INPUT | ENABLE_ECHO_INPUT | ENABLE_PROCESSED_INPUT;
|
||||||
|
|
||||||
|
pub(crate) fn is_raw_mode_enabled() -> std::io::Result<bool> {
|
||||||
|
let console_mode = ConsoleMode::from(Handle::current_in_handle()?);
|
||||||
|
|
||||||
|
let dw_mode = console_mode.mode()?;
|
||||||
|
|
||||||
|
Ok(
|
||||||
|
// check none of the "not raw" bits is set
|
||||||
|
dw_mode & NOT_RAW_MODE_MASK == 0,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn enable_raw_mode() -> std::io::Result<()> {
|
||||||
|
let console_mode = ConsoleMode::from(Handle::current_in_handle()?);
|
||||||
|
|
||||||
|
let dw_mode = console_mode.mode()?;
|
||||||
|
|
||||||
|
let new_mode = dw_mode & !NOT_RAW_MODE_MASK;
|
||||||
|
|
||||||
|
console_mode.set_mode(new_mode)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn disable_raw_mode() -> std::io::Result<()> {
|
||||||
|
let console_mode = ConsoleMode::from(Handle::current_in_handle()?);
|
||||||
|
|
||||||
|
let dw_mode = console_mode.mode()?;
|
||||||
|
|
||||||
|
let new_mode = dw_mode | NOT_RAW_MODE_MASK;
|
||||||
|
|
||||||
|
console_mode.set_mode(new_mode)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn size() -> io::Result<(u16, u16)> {
|
||||||
|
let terminal_size = ScreenBuffer::current()?.info()?.terminal_size();
|
||||||
|
// windows starts counting at 0, unix at 1, add one to replicated unix behaviour.
|
||||||
|
Ok((
|
||||||
|
(terminal_size.width + 1) as u16,
|
||||||
|
(terminal_size.height + 1) as u16,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn window_size() -> io::Result<WindowSize> {
|
||||||
|
Err(io::Error::new(
|
||||||
|
io::ErrorKind::Unsupported,
|
||||||
|
"Window pixel size not implemented for the Windows API.",
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Queries the terminal's support for progressive keyboard enhancement.
|
||||||
|
///
|
||||||
|
/// This always returns `Ok(false)` on Windows.
|
||||||
|
#[cfg(feature = "events")]
|
||||||
|
pub fn supports_keyboard_enhancement() -> std::io::Result<bool> {
|
||||||
|
Ok(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn clear(clear_type: ClearType) -> std::io::Result<()> {
|
||||||
|
let screen_buffer = ScreenBuffer::current()?;
|
||||||
|
let csbi = screen_buffer.info()?;
|
||||||
|
|
||||||
|
let pos = csbi.cursor_pos();
|
||||||
|
let buffer_size = csbi.buffer_size();
|
||||||
|
let current_attribute = csbi.attributes();
|
||||||
|
|
||||||
|
match clear_type {
|
||||||
|
ClearType::All => {
|
||||||
|
clear_entire_screen(buffer_size, current_attribute)?;
|
||||||
|
}
|
||||||
|
ClearType::FromCursorDown => clear_after_cursor(pos, buffer_size, current_attribute)?,
|
||||||
|
ClearType::FromCursorUp => clear_before_cursor(pos, buffer_size, current_attribute)?,
|
||||||
|
ClearType::CurrentLine => clear_current_line(pos, buffer_size, current_attribute)?,
|
||||||
|
ClearType::UntilNewLine => clear_until_line(pos, buffer_size, current_attribute)?,
|
||||||
|
_ => {
|
||||||
|
clear_entire_screen(buffer_size, current_attribute)?;
|
||||||
|
} //TODO: make purge flush the entire screen buffer not just the visible window.
|
||||||
|
};
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn scroll_up(row_count: u16) -> std::io::Result<()> {
|
||||||
|
let csbi = ScreenBuffer::current()?;
|
||||||
|
let mut window = csbi.info()?.terminal_window();
|
||||||
|
|
||||||
|
// check whether the window is too close to the screen buffer top
|
||||||
|
let count = row_count as i16;
|
||||||
|
if window.top >= count {
|
||||||
|
window.top -= count; // move top down
|
||||||
|
window.bottom -= count; // move bottom down
|
||||||
|
|
||||||
|
Console::output()?.set_console_info(true, window)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn scroll_down(row_count: u16) -> std::io::Result<()> {
|
||||||
|
let screen_buffer = ScreenBuffer::current()?;
|
||||||
|
let csbi = screen_buffer.info()?;
|
||||||
|
let mut window = csbi.terminal_window();
|
||||||
|
let buffer_size = csbi.buffer_size();
|
||||||
|
|
||||||
|
// check whether the window is too close to the screen buffer top
|
||||||
|
let count = row_count as i16;
|
||||||
|
if window.bottom < buffer_size.height - count {
|
||||||
|
window.top += count; // move top down
|
||||||
|
window.bottom += count; // move bottom down
|
||||||
|
|
||||||
|
Console::output()?.set_console_info(true, window)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn set_size(width: u16, height: u16) -> std::io::Result<()> {
|
||||||
|
if width <= 1 {
|
||||||
|
return Err(io::Error::new(
|
||||||
|
io::ErrorKind::InvalidInput,
|
||||||
|
"terminal width must be at least 1",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
if height <= 1 {
|
||||||
|
return Err(io::Error::new(
|
||||||
|
io::ErrorKind::InvalidInput,
|
||||||
|
"terminal height must be at least 1",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
// get the position of the current console window
|
||||||
|
let screen_buffer = ScreenBuffer::current()?;
|
||||||
|
let console = Console::from(screen_buffer.handle().clone());
|
||||||
|
let csbi = screen_buffer.info()?;
|
||||||
|
|
||||||
|
let current_size = csbi.buffer_size();
|
||||||
|
let window = csbi.terminal_window();
|
||||||
|
|
||||||
|
let mut new_size = Size::new(current_size.width, current_size.height);
|
||||||
|
|
||||||
|
// If the buffer is smaller than this new window size, resize the
|
||||||
|
// buffer to be large enough. Include window position.
|
||||||
|
let mut resize_buffer = false;
|
||||||
|
|
||||||
|
let width = width as i16;
|
||||||
|
if current_size.width < window.left + width {
|
||||||
|
if window.left >= i16::max_value() - width {
|
||||||
|
return Err(io::Error::new(
|
||||||
|
io::ErrorKind::InvalidInput,
|
||||||
|
"terminal width too large",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
new_size.width = window.left + width;
|
||||||
|
resize_buffer = true;
|
||||||
|
}
|
||||||
|
let height = height as i16;
|
||||||
|
if current_size.height < window.top + height {
|
||||||
|
if window.top >= i16::max_value() - height {
|
||||||
|
return Err(io::Error::new(
|
||||||
|
io::ErrorKind::InvalidInput,
|
||||||
|
"terminal height too large",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
new_size.height = window.top + height;
|
||||||
|
resize_buffer = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if resize_buffer {
|
||||||
|
screen_buffer.set_size(new_size.width - 1, new_size.height - 1)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut window = window;
|
||||||
|
|
||||||
|
// preserve the position, but change the size.
|
||||||
|
window.bottom = window.top + height - 1;
|
||||||
|
window.right = window.left + width - 1;
|
||||||
|
console.set_console_info(true, window)?;
|
||||||
|
|
||||||
|
// if we resized the buffer, un-resize it.
|
||||||
|
if resize_buffer {
|
||||||
|
screen_buffer.set_size(current_size.width - 1, current_size.height - 1)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let bounds = console.largest_window_size()?;
|
||||||
|
|
||||||
|
if width > bounds.x {
|
||||||
|
return Err(io::Error::new(
|
||||||
|
io::ErrorKind::InvalidInput,
|
||||||
|
format!("terminal width {width} too large"),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if height > bounds.y {
|
||||||
|
return Err(io::Error::new(
|
||||||
|
io::ErrorKind::InvalidInput,
|
||||||
|
format!("terminal height {height} too large"),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn set_window_title(title: impl fmt::Display) -> std::io::Result<()> {
|
||||||
|
struct Utf16Encoder(Vec<u16>);
|
||||||
|
impl Write for Utf16Encoder {
|
||||||
|
fn write_str(&mut self, s: &str) -> fmt::Result {
|
||||||
|
self.0.extend(s.encode_utf16());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut title_utf16 = Utf16Encoder(Vec::new());
|
||||||
|
write!(title_utf16, "{title}").expect("formatting failed");
|
||||||
|
title_utf16.0.push(0);
|
||||||
|
let title = title_utf16.0;
|
||||||
|
|
||||||
|
let result = unsafe { SetConsoleTitleW(title.as_ptr()) };
|
||||||
|
if result != 0 {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(io::Error::last_os_error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn clear_after_cursor(
|
||||||
|
location: Coord,
|
||||||
|
buffer_size: Size,
|
||||||
|
current_attribute: u16,
|
||||||
|
) -> std::io::Result<()> {
|
||||||
|
let (mut x, mut y) = (location.x, location.y);
|
||||||
|
|
||||||
|
// if cursor position is at the outer right position
|
||||||
|
if x > buffer_size.width {
|
||||||
|
y += 1;
|
||||||
|
x = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// location where to start clearing
|
||||||
|
let start_location = Coord::new(x, y);
|
||||||
|
|
||||||
|
// get sum cells before cursor
|
||||||
|
let cells_to_write = buffer_size.width as u32 * buffer_size.height as u32;
|
||||||
|
|
||||||
|
clear_winapi(start_location, cells_to_write, current_attribute)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn clear_before_cursor(
|
||||||
|
location: Coord,
|
||||||
|
buffer_size: Size,
|
||||||
|
current_attribute: u16,
|
||||||
|
) -> std::io::Result<()> {
|
||||||
|
let (xpos, ypos) = (location.x, location.y);
|
||||||
|
|
||||||
|
// one cell after cursor position
|
||||||
|
let x = 0;
|
||||||
|
// one at row of cursor position
|
||||||
|
let y = 0;
|
||||||
|
|
||||||
|
// location where to start clearing
|
||||||
|
let start_location = Coord::new(x, y);
|
||||||
|
|
||||||
|
// get sum cells before cursor
|
||||||
|
let cells_to_write = (buffer_size.width as u32 * ypos as u32) + (xpos as u32 + 1);
|
||||||
|
|
||||||
|
// clear everything before cursor position
|
||||||
|
clear_winapi(start_location, cells_to_write, current_attribute)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn clear_entire_screen(buffer_size: Size, current_attribute: u16) -> std::io::Result<()> {
|
||||||
|
// get sum cells before cursor
|
||||||
|
let cells_to_write = buffer_size.width as u32 * buffer_size.height as u32;
|
||||||
|
|
||||||
|
// location where to start clearing
|
||||||
|
let start_location = Coord::new(0, 0);
|
||||||
|
|
||||||
|
// clear the entire screen
|
||||||
|
clear_winapi(start_location, cells_to_write, current_attribute)?;
|
||||||
|
|
||||||
|
// put the cursor back at cell 0,0
|
||||||
|
cursor::sys::move_to(0, 0)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn clear_current_line(
|
||||||
|
location: Coord,
|
||||||
|
buffer_size: Size,
|
||||||
|
current_attribute: u16,
|
||||||
|
) -> std::io::Result<()> {
|
||||||
|
// location where to start clearing
|
||||||
|
let start_location = Coord::new(0, location.y);
|
||||||
|
|
||||||
|
// get sum cells before cursor
|
||||||
|
let cells_to_write = buffer_size.width as u32;
|
||||||
|
|
||||||
|
// clear the whole current line
|
||||||
|
clear_winapi(start_location, cells_to_write, current_attribute)?;
|
||||||
|
|
||||||
|
// put the cursor back at cell 1 on current row
|
||||||
|
cursor::sys::move_to(0, location.y as u16)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn clear_until_line(
|
||||||
|
location: Coord,
|
||||||
|
buffer_size: Size,
|
||||||
|
current_attribute: u16,
|
||||||
|
) -> std::io::Result<()> {
|
||||||
|
let (x, y) = (location.x, location.y);
|
||||||
|
|
||||||
|
// location where to start clearing
|
||||||
|
let start_location = Coord::new(x, y);
|
||||||
|
|
||||||
|
// get sum cells before cursor
|
||||||
|
let cells_to_write = (buffer_size.width - x) as u32;
|
||||||
|
|
||||||
|
// clear until the current line
|
||||||
|
clear_winapi(start_location, cells_to_write, current_attribute)?;
|
||||||
|
|
||||||
|
// put the cursor back at original cursor position before we did the clearing
|
||||||
|
cursor::sys::move_to(x as u16, y as u16)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn clear_winapi(
|
||||||
|
start_location: Coord,
|
||||||
|
cells_to_write: u32,
|
||||||
|
current_attribute: u16,
|
||||||
|
) -> std::io::Result<()> {
|
||||||
|
let console = Console::from(Handle::current_out_handle()?);
|
||||||
|
console.fill_whit_character(start_location, cells_to_write, ' ')?;
|
||||||
|
console.fill_whit_attribute(start_location, cells_to_write, current_attribute)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
// Create a new screen buffer to avoid changing the terminal the test
|
||||||
|
// is running within.
|
||||||
|
pub fn temp_screen_buffer() -> std::io::Result<ScreenBuffer> {
|
||||||
|
let alternate_screen = ScreenBuffer::create()?;
|
||||||
|
alternate_screen.show().unwrap();
|
||||||
|
Ok(alternate_screen)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use std::{ffi::OsString, os::windows::ffi::OsStringExt};
|
||||||
|
|
||||||
|
use crossterm_winapi::ScreenBuffer;
|
||||||
|
use serial_test::serial;
|
||||||
|
use winapi::um::wincon::GetConsoleTitleW;
|
||||||
|
|
||||||
|
use super::{scroll_down, scroll_up, set_size, set_window_title, size, temp_screen_buffer};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[serial]
|
||||||
|
fn test_resize_winapi_20_21() {
|
||||||
|
let _test_screen = temp_screen_buffer().unwrap();
|
||||||
|
|
||||||
|
let (width, height) = size().unwrap();
|
||||||
|
|
||||||
|
// The values 20 and 21 are arbitrary and different from each other
|
||||||
|
// just to see they're not crossed over.
|
||||||
|
set_size(20, 21).unwrap();
|
||||||
|
assert_eq!((20, 21), size().unwrap());
|
||||||
|
|
||||||
|
// reset to previous size
|
||||||
|
set_size(width, height).unwrap();
|
||||||
|
assert_eq!((width, height), size().unwrap());
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is similar to test_resize_winapi_20_21() above. This verifies that
|
||||||
|
// another test of similar functionality runs independently (that a testing
|
||||||
|
// race condition has been addressed).
|
||||||
|
#[test]
|
||||||
|
#[serial]
|
||||||
|
#[ignore]
|
||||||
|
fn test_resize_winapi_30_31() {
|
||||||
|
let _test_screen = temp_screen_buffer().unwrap();
|
||||||
|
|
||||||
|
let (width, height) = size().unwrap();
|
||||||
|
|
||||||
|
set_size(30, 31).unwrap();
|
||||||
|
assert_eq!((30, 31), size().unwrap());
|
||||||
|
|
||||||
|
// reset to previous size
|
||||||
|
set_size(width, height).unwrap();
|
||||||
|
assert_eq!((width, height), size().unwrap());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test is disabled, because it's failing on Travis CI
|
||||||
|
#[test]
|
||||||
|
#[ignore]
|
||||||
|
fn test_scroll_down_winapi() {
|
||||||
|
let current_window = ScreenBuffer::current()
|
||||||
|
.unwrap()
|
||||||
|
.info()
|
||||||
|
.unwrap()
|
||||||
|
.terminal_window();
|
||||||
|
|
||||||
|
scroll_down(2).unwrap();
|
||||||
|
|
||||||
|
let new_window = ScreenBuffer::current()
|
||||||
|
.unwrap()
|
||||||
|
.info()
|
||||||
|
.unwrap()
|
||||||
|
.terminal_window();
|
||||||
|
|
||||||
|
assert_eq!(new_window.top, current_window.top + 2);
|
||||||
|
assert_eq!(new_window.bottom, current_window.bottom + 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test is disabled, because it's failing on Travis CI
|
||||||
|
#[test]
|
||||||
|
#[ignore]
|
||||||
|
fn test_scroll_up_winapi() {
|
||||||
|
// move the terminal buffer down before moving it up
|
||||||
|
test_scroll_down_winapi();
|
||||||
|
|
||||||
|
let current_window = ScreenBuffer::current()
|
||||||
|
.unwrap()
|
||||||
|
.info()
|
||||||
|
.unwrap()
|
||||||
|
.terminal_window();
|
||||||
|
|
||||||
|
scroll_up(2).unwrap();
|
||||||
|
|
||||||
|
let new_window = ScreenBuffer::current()
|
||||||
|
.unwrap()
|
||||||
|
.info()
|
||||||
|
.unwrap()
|
||||||
|
.terminal_window();
|
||||||
|
|
||||||
|
assert_eq!(new_window.top, current_window.top - 2);
|
||||||
|
assert_eq!(new_window.bottom, current_window.bottom - 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[serial]
|
||||||
|
fn test_set_title_winapi() {
|
||||||
|
let _test_screen = temp_screen_buffer().unwrap();
|
||||||
|
|
||||||
|
let test_title = "this is a crossterm test title";
|
||||||
|
set_window_title(test_title).unwrap();
|
||||||
|
|
||||||
|
let mut raw = [0_u16; 128];
|
||||||
|
let length = unsafe { GetConsoleTitleW(raw.as_mut_ptr(), raw.len() as u32) } as usize;
|
||||||
|
assert_ne!(0, length);
|
||||||
|
|
||||||
|
let console_title = OsString::from_wide(&raw[..length]).into_string().unwrap();
|
||||||
|
assert_eq!(test_title, &console_title[..]);
|
||||||
|
}
|
||||||
|
}
|
54
src/tty.rs
Normal file
54
src/tty.rs
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
//! Making it a little more convenient and safe to query whether
|
||||||
|
//! something is a terminal teletype or not.
|
||||||
|
//! This module defines the IsTty trait and the is_tty method to
|
||||||
|
//! return true if the item represents a terminal.
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
use std::os::unix::io::AsRawFd;
|
||||||
|
#[cfg(windows)]
|
||||||
|
use std::os::windows::io::AsRawHandle;
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
|
use winapi::um::consoleapi::GetConsoleMode;
|
||||||
|
|
||||||
|
/// Adds the `is_tty` method to types that might represent a terminal
|
||||||
|
///
|
||||||
|
/// ```rust
|
||||||
|
/// use std::io::stdout;
|
||||||
|
/// use crossterm::tty::IsTty;
|
||||||
|
///
|
||||||
|
/// let is_tty: bool = stdout().is_tty();
|
||||||
|
/// ```
|
||||||
|
pub trait IsTty {
|
||||||
|
/// Returns true when an instance is a terminal teletype, otherwise false.
|
||||||
|
fn is_tty(&self) -> bool;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// On UNIX, the `isatty()` function returns true if a file
|
||||||
|
/// descriptor is a terminal.
|
||||||
|
#[cfg(all(unix, feature = "libc"))]
|
||||||
|
impl<S: AsRawFd> IsTty for S {
|
||||||
|
fn is_tty(&self) -> bool {
|
||||||
|
let fd = self.as_raw_fd();
|
||||||
|
unsafe { libc::isatty(fd) == 1 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(all(unix, not(feature = "libc")))]
|
||||||
|
impl<S: AsRawFd> IsTty for S {
|
||||||
|
fn is_tty(&self) -> bool {
|
||||||
|
let fd = self.as_raw_fd();
|
||||||
|
rustix::termios::isatty(unsafe { std::os::unix::io::BorrowedFd::borrow_raw(fd) })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// On windows, `GetConsoleMode` will return true if we are in a terminal.
|
||||||
|
/// Otherwise false.
|
||||||
|
#[cfg(windows)]
|
||||||
|
impl<S: AsRawHandle> IsTty for S {
|
||||||
|
fn is_tty(&self) -> bool {
|
||||||
|
let mut mode = 0;
|
||||||
|
let ok = unsafe { GetConsoleMode(self.as_raw_handle() as *mut _, &mut mode) };
|
||||||
|
ok == 1
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user