Compare commits
10 Commits
fe440284bf
...
1b66c396b6
Author | SHA1 | Date | |
---|---|---|---|
1b66c396b6 | |||
|
b056370038 | ||
|
492b764f9f | ||
|
4712ff5619 | ||
|
8d1f0cc603 | ||
|
5d50d8da62 | ||
|
25e14782e3 | ||
|
33b4e37223 | ||
|
61ff5ae1ce | ||
|
080f06494a |
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" -- --nocapture --test-threads 1
|
run: cargo test --no-default-features --features "use-dev-tty events bracketed-paste" -- --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,7 +1,36 @@
|
|||||||
# Unreleased
|
# Unreleased
|
||||||
|
|
||||||
- Use Rustix by default instead of libc. Libc can be re-enabled if necessary with the libc feature flag.
|
# Version 0.28.1
|
||||||
|
|
||||||
|
## 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
|
||||||
|
|
||||||
@ -18,7 +47,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.
|
||||||
|
|
||||||
## Changes
|
## Changed ⚙️
|
||||||
|
|
||||||
- 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,10 +1,9 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "crossterm"
|
name = "minicrossterm"
|
||||||
version = "0.27.0"
|
version = "0.28.1"
|
||||||
authors = ["T. Post"]
|
authors = ["Blasthavers", "T. Post"]
|
||||||
description = "A crossplatform terminal library for manipulating terminals."
|
description = "A stripped back crossplatform terminal library for manipulating terminals asynchronously."
|
||||||
repository = "https://github.com/crossterm-rs/crossterm"
|
repository = "https://git.blastmud.org/blasthavers/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"]
|
||||||
@ -27,21 +26,10 @@ all-features = true
|
|||||||
# Features
|
# Features
|
||||||
#
|
#
|
||||||
[features]
|
[features]
|
||||||
default = ["bracketed-paste", "windows", "events"]
|
default = ["bracketed-paste", "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.
|
||||||
|
|
||||||
@ -50,84 +38,14 @@ 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,12 +1,20 @@
|
|||||||
<h1 align="center"><img width="440" src="docs/crossterm_full.png" /></h1>
|
# Minicrossterm
|
||||||
|
|
||||||
[![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]
|
Minicrossterm is a cut back fork of crossterm which doesn't depend on stdio, and is intended
|
||||||
|
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 whit how terminals handle ANSI or WinApi.
|
And I don't think it has to do anything with crossterm but it has to do with how terminals handle ANSI or WinApi.
|
||||||
|
|
||||||
## WinAPI
|
## WinAPI
|
||||||
|
|
||||||
|
@ -1,40 +0,0 @@
|
|||||||
![Lines of Code][s7] [![MIT][s2]][l2] [![Join us on Discord][s5]][l5]
|
|
||||||
|
|
||||||
# Crossterm Examples
|
|
||||||
|
|
||||||
The examples are compatible with the latest release.
|
|
||||||
|
|
||||||
## Structure
|
|
||||||
|
|
||||||
```
|
|
||||||
├── examples
|
|
||||||
│ └── interactive-test
|
|
||||||
│ └── event-*
|
|
||||||
│ └── stderr
|
|
||||||
```
|
|
||||||
| File Name | Description | Topics |
|
|
||||||
|:----------------------------|:-------------------------------|:------------------------------------------|
|
|
||||||
| `examples/interactive-test` | interactive, walk through, demo | cursor, style, event |
|
|
||||||
| `event-*` | event reading demos | (async) event reading |
|
|
||||||
| `stderr` | crossterm over stderr demo | raw mode, alternate screen, custom output |
|
|
||||||
| `is_tty` | Is this instance a tty ? | tty |
|
|
||||||
|
|
||||||
## Run examples
|
|
||||||
|
|
||||||
```bash
|
|
||||||
$ cargo run --example [file name]
|
|
||||||
```
|
|
||||||
|
|
||||||
To run the interactive-demo go into the folder `examples/interactive-demo` and run `cargo run`.
|
|
||||||
|
|
||||||
## License
|
|
||||||
|
|
||||||
This project is licensed under the MIT License - see the [LICENSE.md](LICENSE) file for details.
|
|
||||||
|
|
||||||
[s2]: https://img.shields.io/badge/license-MIT-blue.svg
|
|
||||||
[l2]: LICENSE
|
|
||||||
|
|
||||||
[s5]: https://img.shields.io/discord/560857607196377088.svg?logo=discord
|
|
||||||
[l5]: https://discord.gg/K4nyTDB
|
|
||||||
|
|
||||||
[s7]: https://travis-ci.org/crossterm-rs/examples.svg?branch=master
|
|
@ -1,68 +0,0 @@
|
|||||||
//! 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,
|
|
||||||
)));
|
|
||||||
}
|
|
@ -1,61 +0,0 @@
|
|||||||
//! Demonstrates how to match on modifiers like: Control, alt, shift.
|
|
||||||
//!
|
|
||||||
//! cargo run --example event-poll-read
|
|
||||||
|
|
||||||
use std::{io, time::Duration};
|
|
||||||
|
|
||||||
use 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()
|
|
||||||
}
|
|
@ -1,44 +0,0 @@
|
|||||||
//! Demonstrates how to block read characters or a full line.
|
|
||||||
//! Just note that crossterm is not required to do this and can be done with `io::stdin()`.
|
|
||||||
//!
|
|
||||||
//! cargo run --example event-read-char-line
|
|
||||||
|
|
||||||
use std::io;
|
|
||||||
|
|
||||||
use 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());
|
|
||||||
}
|
|
@ -1,112 +0,0 @@
|
|||||||
//! 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()
|
|
||||||
}
|
|
@ -1,67 +0,0 @@
|
|||||||
//! Demonstrates how to read events asynchronously with async-std.
|
|
||||||
//!
|
|
||||||
//! cargo run --features="event-stream" --example event-stream-async-std
|
|
||||||
|
|
||||||
use std::{io::stdout, time::Duration};
|
|
||||||
|
|
||||||
use futures::{future::FutureExt, select, StreamExt};
|
|
||||||
use futures_timer::Delay;
|
|
||||||
|
|
||||||
use 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()
|
|
||||||
}
|
|
@ -1,68 +0,0 @@
|
|||||||
//! Demonstrates how to read events asynchronously with tokio.
|
|
||||||
//!
|
|
||||||
//! cargo run --features="event-stream" --example event-stream-tokio
|
|
||||||
|
|
||||||
use std::{io::stdout, time::Duration};
|
|
||||||
|
|
||||||
use futures::{future::FutureExt, select, StreamExt};
|
|
||||||
use futures_timer::Delay;
|
|
||||||
|
|
||||||
use 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()
|
|
||||||
}
|
|
@ -1,13 +0,0 @@
|
|||||||
[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 = "../../" }
|
|
@ -1,29 +0,0 @@
|
|||||||
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),
|
|
||||||
_ => { },
|
|
||||||
};
|
|
||||||
)*
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,104 +0,0 @@
|
|||||||
#![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)
|
|
||||||
}
|
|
@ -1,5 +0,0 @@
|
|||||||
pub mod attribute;
|
|
||||||
pub mod color;
|
|
||||||
pub mod cursor;
|
|
||||||
pub mod event;
|
|
||||||
pub mod synchronized_output;
|
|
@ -1,58 +0,0 @@
|
|||||||
#![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(())
|
|
||||||
}
|
|
@ -1,198 +0,0 @@
|
|||||||
#![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(())
|
|
||||||
}
|
|
@ -1,222 +0,0 @@
|
|||||||
#![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(())
|
|
||||||
}
|
|
@ -1,42 +0,0 @@
|
|||||||
#![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(())
|
|
||||||
}
|
|
@ -1,41 +0,0 @@
|
|||||||
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(())
|
|
||||||
}
|
|
@ -1,18 +0,0 @@
|
|||||||
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");
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,49 +0,0 @@
|
|||||||
//! 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(())
|
|
||||||
}
|
|
@ -1,95 +0,0 @@
|
|||||||
//! This shows how an application can write on stderr
|
|
||||||
//! instead of stdout, thus making it possible to
|
|
||||||
//! the command API instead of the "old style" direct
|
|
||||||
//! unbuffered API.
|
|
||||||
//!
|
|
||||||
//! This particular example is only suited to Unix
|
|
||||||
//! for now.
|
|
||||||
//!
|
|
||||||
//! cargo run --example stderr
|
|
||||||
|
|
||||||
use std::io;
|
|
||||||
|
|
||||||
use 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,11 +46,6 @@ 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
|
||||||
@ -370,7 +365,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(Clone, Copy)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||||
pub enum SetCursorStyle {
|
pub enum SetCursorStyle {
|
||||||
/// Default cursor shape configured by the user.
|
/// Default cursor shape configured by the user.
|
||||||
DefaultUserShape,
|
DefaultUserShape,
|
||||||
|
@ -1,20 +0,0 @@
|
|||||||
//! 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;
|
|
@ -1,56 +0,0 @@
|
|||||||
use std::{
|
|
||||||
io::{self, Error, ErrorKind, Write},
|
|
||||||
time::Duration,
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
event::{filter::CursorPositionFilter, poll_internal, read_internal, InternalEvent},
|
|
||||||
terminal::{disable_raw_mode, enable_raw_mode, sys::is_raw_mode_enabled},
|
|
||||||
};
|
|
||||||
|
|
||||||
/// Returns the cursor position (column, row).
|
|
||||||
///
|
|
||||||
/// The top left cell is represented as `(0, 0)`.
|
|
||||||
///
|
|
||||||
/// On unix systems, this function will block and possibly time out while
|
|
||||||
/// [`crossterm::event::read`](crate::event::read) or [`crossterm::event::poll`](crate::event::poll) are being called.
|
|
||||||
pub fn position() -> io::Result<(u16, u16)> {
|
|
||||||
if is_raw_mode_enabled() {
|
|
||||||
read_position_raw()
|
|
||||||
} else {
|
|
||||||
read_position()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn read_position() -> io::Result<(u16, u16)> {
|
|
||||||
enable_raw_mode()?;
|
|
||||||
let pos = read_position_raw();
|
|
||||||
disable_raw_mode()?;
|
|
||||||
pos
|
|
||||||
}
|
|
||||||
|
|
||||||
fn read_position_raw() -> io::Result<(u16, u16)> {
|
|
||||||
// Use `ESC [ 6 n` to and retrieve the cursor position.
|
|
||||||
let mut stdout = io::stdout();
|
|
||||||
stdout.write_all(b"\x1B[6n")?;
|
|
||||||
stdout.flush()?;
|
|
||||||
|
|
||||||
loop {
|
|
||||||
match poll_internal(Some(Duration::from_millis(2000)), &CursorPositionFilter) {
|
|
||||||
Ok(true) => {
|
|
||||||
if let Ok(InternalEvent::CursorPosition(x, y)) =
|
|
||||||
read_internal(&CursorPositionFilter)
|
|
||||||
{
|
|
||||||
return Ok((x, y));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(false) => {
|
|
||||||
return Err(Error::new(
|
|
||||||
ErrorKind::Other,
|
|
||||||
"The cursor position could not be read within a normal duration",
|
|
||||||
));
|
|
||||||
}
|
|
||||||
Err(_) => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,341 +0,0 @@
|
|||||||
//! WinAPI related logic to cursor manipulation.
|
|
||||||
|
|
||||||
use std::convert::TryFrom;
|
|
||||||
use std::io;
|
|
||||||
use std::sync::atomic::{AtomicU64, Ordering};
|
|
||||||
|
|
||||||
use crossterm_winapi::{result, Coord, Handle, HandleType, ScreenBuffer};
|
|
||||||
use winapi::{
|
|
||||||
shared::minwindef::{FALSE, TRUE},
|
|
||||||
um::wincon::{SetConsoleCursorInfo, SetConsoleCursorPosition, CONSOLE_CURSOR_INFO, COORD},
|
|
||||||
};
|
|
||||||
|
|
||||||
/// The position of the cursor, written when you save the cursor's position.
|
|
||||||
///
|
|
||||||
/// This is `u64::MAX` initially. Otherwise, it stores the cursor's x position bit-shifted left 16
|
|
||||||
/// times or-ed with the cursor's y position, where both are `i16`s.
|
|
||||||
static SAVED_CURSOR_POS: AtomicU64 = AtomicU64::new(u64::MAX);
|
|
||||||
|
|
||||||
// The 'y' position of the cursor is not relative to the window but absolute to screen buffer.
|
|
||||||
// We can calculate the relative cursor position by subtracting the top position of the terminal window from the y position.
|
|
||||||
// This results in an 1-based coord zo subtract 1 to make cursor position 0-based.
|
|
||||||
pub fn parse_relative_y(y: i16) -> std::io::Result<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);
|
|
||||||
}
|
|
||||||
}
|
|
403
src/event.rs
403
src/event.rs
@ -17,20 +17,33 @@
|
|||||||
//!
|
//!
|
||||||
//! **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 Events
|
//! ## Mouse and Focus Events
|
||||||
//!
|
//!
|
||||||
//! Mouse events are not enabled by default. You have to enable them with the
|
//! Mouse and focus events are not enabled by default. You have to enable them with the
|
||||||
//! [`EnableMouseCapture`](struct.EnableMouseCapture.html) command. See [Command API](../index.html#command-api)
|
//! [`EnableMouseCapture`](struct.EnableMouseCapture.html) / [`EnableFocusChange`](struct.EnableFocusChange.html) command.
|
||||||
//! for more information.
|
//! See [Command API](../index.html#command-api) for more information.
|
||||||
//!
|
//!
|
||||||
//! ## Examples
|
//! ## Examples
|
||||||
//!
|
//!
|
||||||
//! Blocking read:
|
//! Blocking read:
|
||||||
//!
|
//!
|
||||||
//! ```no_run
|
//! ```no_run
|
||||||
//! use crossterm::event::{read, Event};
|
//! #![cfg(feature = "bracketed-paste")]
|
||||||
|
//! 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()? {
|
||||||
@ -43,6 +56,12 @@
|
|||||||
//! 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(())
|
||||||
//! }
|
//! }
|
||||||
//! ```
|
//! ```
|
||||||
@ -50,11 +69,24 @@
|
|||||||
//! 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::event::{poll, read, Event};
|
//! use crossterm::{
|
||||||
|
//! 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))? {
|
||||||
@ -73,6 +105,12 @@
|
|||||||
//! // Timeout expired and no `Event` is available
|
//! // Timeout expired and no `Event` is available
|
||||||
//! }
|
//! }
|
||||||
//! }
|
//! }
|
||||||
|
//! execute!(
|
||||||
|
//! std::io::stdout(),
|
||||||
|
//! DisableBracketedPaste,
|
||||||
|
//! DisableFocusChange,
|
||||||
|
//! DisableMouseCapture
|
||||||
|
//! )?;
|
||||||
//! Ok(())
|
//! Ok(())
|
||||||
//! }
|
//! }
|
||||||
//! ```
|
//! ```
|
||||||
@ -81,165 +119,18 @@
|
|||||||
//! 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;
|
|
||||||
|
|
||||||
#[cfg(feature = "event-stream")]
|
|
||||||
pub use stream::EventStream;
|
|
||||||
|
|
||||||
use crate::event::{
|
|
||||||
filter::{EventFilter, Filter},
|
|
||||||
read::InternalEventReader,
|
|
||||||
timeout::PollTimeout,
|
|
||||||
};
|
|
||||||
use crate::{csi, Command};
|
use crate::{csi, Command};
|
||||||
use parking_lot::{MappedMutexGuard, Mutex, MutexGuard};
|
use std::{
|
||||||
use std::fmt::{self, Display};
|
collections::VecDeque,
|
||||||
use std::time::Duration;
|
fmt::{self, Display},
|
||||||
|
};
|
||||||
|
|
||||||
use bitflags::bitflags;
|
use bitflags::bitflags;
|
||||||
use std::hash::{Hash, Hasher};
|
use std::hash::{Hash, Hasher};
|
||||||
|
|
||||||
/// Static instance of `InternalEventReader`.
|
use self::sys::unix::parse::parse_event;
|
||||||
/// 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.
|
||||||
@ -293,16 +184,6 @@ 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.
|
||||||
@ -322,16 +203,6 @@ 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.
|
||||||
@ -346,12 +217,6 @@ 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.
|
||||||
@ -362,12 +227,6 @@ 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).
|
||||||
@ -385,14 +244,6 @@ 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.
|
||||||
@ -405,11 +256,6 @@ 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.
|
||||||
@ -456,21 +302,6 @@ 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.
|
||||||
@ -485,21 +316,6 @@ 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.
|
||||||
@ -522,6 +338,12 @@ 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.
|
||||||
@ -639,19 +461,8 @@ 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")?,
|
||||||
@ -685,11 +496,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_1000;
|
const CAPS_LOCK = 0b0000_0010;
|
||||||
/// 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_1000;
|
const NUM_LOCK = 0b0000_0100;
|
||||||
const NONE = 0b0000_0000;
|
const NONE = 0b0000_0000;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1081,20 +892,9 @@ 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"),
|
||||||
@ -1128,25 +928,6 @@ 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;
|
||||||
@ -1189,18 +970,9 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn keycode_display() {
|
fn keycode_display() {
|
||||||
#[cfg(target_os = "macos")]
|
assert_eq!(format!("{}", Backspace), "Backspace");
|
||||||
{
|
assert_eq!(format!("{}", Delete), "Del");
|
||||||
assert_eq!(format!("{}", Backspace), "Delete");
|
assert_eq!(format!("{}", Enter), "Enter");
|
||||||
assert_eq!(format!("{}", Delete), "Fwd Del");
|
|
||||||
assert_eq!(format!("{}", Enter), "Return");
|
|
||||||
}
|
|
||||||
#[cfg(not(target_os = "macos"))]
|
|
||||||
{
|
|
||||||
assert_eq!(format!("{}", Backspace), "Backspace");
|
|
||||||
assert_eq!(format!("{}", Delete), "Del");
|
|
||||||
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");
|
||||||
@ -1254,29 +1026,6 @@ 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");
|
||||||
@ -1287,3 +1036,35 @@ 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,48 +1,42 @@
|
|||||||
use crate::event::InternalEvent;
|
use crate::event::Event;
|
||||||
|
|
||||||
/// 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: &InternalEvent) -> bool;
|
fn eval(&self, event: &Event) -> 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: &InternalEvent) -> bool {
|
fn eval(&self, event: &Event) -> bool {
|
||||||
matches!(*event, InternalEvent::CursorPosition(_, _))
|
matches!(*event, Event::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: &InternalEvent) -> bool {
|
fn eval(&self, event: &Event) -> 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,
|
||||||
InternalEvent::KeyboardEnhancementFlags(_) | InternalEvent::PrimaryDeviceAttributes
|
Event::KeyboardEnhancementFlags(_) | Event::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: &InternalEvent) -> bool {
|
fn eval(&self, event: &Event) -> bool {
|
||||||
matches!(*event, InternalEvent::PrimaryDeviceAttributes)
|
matches!(*event, Event::PrimaryDeviceAttributes)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -50,66 +44,50 @@ impl Filter for PrimaryDeviceAttributesFilter {
|
|||||||
pub(crate) struct EventFilter;
|
pub(crate) struct EventFilter;
|
||||||
|
|
||||||
impl Filter for EventFilter {
|
impl Filter for EventFilter {
|
||||||
#[cfg(unix)]
|
fn eval(&self, _: &Event) -> bool {
|
||||||
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, EventFilter, Filter, InternalEvent,
|
super::Event, CursorPositionFilter, Event, EventFilter, Filter, InternalEventFilter,
|
||||||
InternalEventFilter, KeyboardEnhancementFlagsFilter, PrimaryDeviceAttributesFilter,
|
KeyboardEnhancementFlagsFilter, PrimaryDeviceAttributesFilter,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_cursor_position_filter_filters_cursor_position() {
|
fn test_cursor_position_filter_filters_cursor_position() {
|
||||||
assert!(!CursorPositionFilter.eval(&InternalEvent::Event(Event::Resize(10, 10))));
|
assert!(!CursorPositionFilter.eval(&Event::Event(Event::Resize(10, 10))));
|
||||||
assert!(CursorPositionFilter.eval(&InternalEvent::CursorPosition(0, 0)));
|
assert!(CursorPositionFilter.eval(&Event::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(&InternalEvent::Event(Event::Resize(10, 10))));
|
assert!(!KeyboardEnhancementFlagsFilter.eval(&Event::Event(Event::Resize(10, 10))));
|
||||||
assert!(
|
assert!(
|
||||||
KeyboardEnhancementFlagsFilter.eval(&InternalEvent::KeyboardEnhancementFlags(
|
KeyboardEnhancementFlagsFilter.eval(&Event::KeyboardEnhancementFlags(
|
||||||
crate::event::KeyboardEnhancementFlags::DISAMBIGUATE_ESCAPE_CODES
|
crate::event::KeyboardEnhancementFlags::DISAMBIGUATE_ESCAPE_CODES
|
||||||
))
|
))
|
||||||
);
|
);
|
||||||
assert!(KeyboardEnhancementFlagsFilter.eval(&InternalEvent::PrimaryDeviceAttributes));
|
assert!(KeyboardEnhancementFlagsFilter.eval(&Event::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(&InternalEvent::Event(Event::Resize(10, 10))));
|
assert!(!PrimaryDeviceAttributesFilter.eval(&Event::Event(Event::Resize(10, 10))));
|
||||||
assert!(PrimaryDeviceAttributesFilter.eval(&InternalEvent::PrimaryDeviceAttributes));
|
assert!(PrimaryDeviceAttributesFilter.eval(&Event::PrimaryDeviceAttributes));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_event_filter_filters_events() {
|
fn test_event_filter_filters_events() {
|
||||||
assert!(EventFilter.eval(&InternalEvent::Event(Event::Resize(10, 10))));
|
assert!(EventFilter.eval(&Event::Event(Event::Resize(10, 10))));
|
||||||
assert!(!EventFilter.eval(&InternalEvent::CursorPosition(0, 0)));
|
assert!(!EventFilter.eval(&Event::CursorPosition(0, 0)));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_event_filter_filters_internal_events() {
|
fn test_event_filter_filters_internal_events() {
|
||||||
assert!(InternalEventFilter.eval(&InternalEvent::Event(Event::Resize(10, 10))));
|
assert!(InternalEventFilter.eval(&Event::Event(Event::Resize(10, 10))));
|
||||||
assert!(InternalEventFilter.eval(&InternalEvent::CursorPosition(0, 0)));
|
assert!(InternalEventFilter.eval(&Event::CursorPosition(0, 0)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,430 +0,0 @@
|
|||||||
use std::{collections::vec_deque::VecDeque, io, time::Duration};
|
|
||||||
|
|
||||||
#[cfg(unix)]
|
|
||||||
use crate::event::source::unix::UnixInternalEventSource;
|
|
||||||
#[cfg(windows)]
|
|
||||||
use crate::event::source::windows::WindowsEventSource;
|
|
||||||
#[cfg(feature = "event-stream")]
|
|
||||||
use crate::event::sys::Waker;
|
|
||||||
use crate::event::{filter::Filter, source::EventSource, timeout::PollTimeout, InternalEvent};
|
|
||||||
|
|
||||||
/// Can be used to read `InternalEvent`s.
|
|
||||||
pub(crate) struct InternalEventReader {
|
|
||||||
events: VecDeque<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!();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,27 +0,0 @@
|
|||||||
use std::{io, time::Duration};
|
|
||||||
|
|
||||||
#[cfg(feature = "event-stream")]
|
|
||||||
use super::sys::Waker;
|
|
||||||
use super::InternalEvent;
|
|
||||||
|
|
||||||
#[cfg(unix)]
|
|
||||||
pub(crate) mod unix;
|
|
||||||
#[cfg(windows)]
|
|
||||||
pub(crate) mod windows;
|
|
||||||
|
|
||||||
/// An interface for trying to read an `InternalEvent` within an optional `Duration`.
|
|
||||||
pub(crate) trait EventSource: Sync + Send {
|
|
||||||
/// Tries to read an `InternalEvent` within the given duration.
|
|
||||||
///
|
|
||||||
/// # Arguments
|
|
||||||
///
|
|
||||||
/// * `timeout` - `None` block indefinitely until an event is available, `Some(duration)` blocks
|
|
||||||
/// for the given timeout
|
|
||||||
///
|
|
||||||
/// Returns `Ok(None)` if there's no event available and timeout expires.
|
|
||||||
fn try_read(&mut self, timeout: Option<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;
|
|
||||||
}
|
|
@ -1,11 +0,0 @@
|
|||||||
#[cfg(feature = "use-dev-tty")]
|
|
||||||
pub(crate) mod tty;
|
|
||||||
|
|
||||||
#[cfg(not(feature = "use-dev-tty"))]
|
|
||||||
pub(crate) mod mio;
|
|
||||||
|
|
||||||
#[cfg(feature = "use-dev-tty")]
|
|
||||||
pub(crate) use self::tty::UnixInternalEventSource;
|
|
||||||
|
|
||||||
#[cfg(not(feature = "use-dev-tty"))]
|
|
||||||
pub(crate) use self::mio::UnixInternalEventSource;
|
|
@ -1,229 +0,0 @@
|
|||||||
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()
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,275 +0,0 @@
|
|||||||
#[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()
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,100 +0,0 @@
|
|||||||
use std::time::Duration;
|
|
||||||
|
|
||||||
use crossterm_winapi::{Console, Handle, InputRecord};
|
|
||||||
|
|
||||||
use crate::event::{
|
|
||||||
sys::windows::{parse::MouseButtonsPressed, poll::WinApiPoll},
|
|
||||||
Event,
|
|
||||||
};
|
|
||||||
|
|
||||||
#[cfg(feature = "event-stream")]
|
|
||||||
use crate::event::sys::Waker;
|
|
||||||
use crate::event::{
|
|
||||||
source::EventSource,
|
|
||||||
sys::windows::parse::{handle_key_event, handle_mouse_event},
|
|
||||||
timeout::PollTimeout,
|
|
||||||
InternalEvent,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub(crate) struct WindowsEventSource {
|
|
||||||
console: Console,
|
|
||||||
poll: WinApiPoll,
|
|
||||||
surrogate_buffer: Option<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,9 +1 @@
|
|||||||
#[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,5 +1,2 @@
|
|||||||
#[cfg(feature = "event-stream")]
|
|
||||||
pub(crate) mod waker;
|
|
||||||
|
|
||||||
#[cfg(feature = "events")]
|
#[cfg(feature = "events")]
|
||||||
pub(crate) mod parse;
|
pub(crate) mod parse;
|
||||||
|
@ -5,8 +5,6 @@ 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,
|
||||||
@ -23,10 +21,7 @@ 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(
|
pub(crate) fn parse_event(buffer: &[u8], input_available: bool) -> io::Result<Option<Event>> {
|
||||||
buffer: &[u8],
|
|
||||||
input_available: bool,
|
|
||||||
) -> io::Result<Option<InternalEvent>> {
|
|
||||||
if buffer.is_empty() {
|
if buffer.is_empty() {
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
}
|
}
|
||||||
@ -38,7 +33,7 @@ pub(crate) fn parse_event(
|
|||||||
// Possible Esc sequence
|
// Possible Esc sequence
|
||||||
Ok(None)
|
Ok(None)
|
||||||
} else {
|
} else {
|
||||||
Ok(Some(InternalEvent::Event(Event::Key(KeyCode::Esc.into()))))
|
Ok(Some(Event::Key(KeyCode::Esc.into())))
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
match buffer[1] {
|
match buffer[1] {
|
||||||
@ -47,40 +42,28 @@ pub(crate) fn parse_event(
|
|||||||
Ok(None)
|
Ok(None)
|
||||||
} else {
|
} else {
|
||||||
match buffer[2] {
|
match buffer[2] {
|
||||||
b'D' => {
|
b'D' => Ok(Some(Event::Key(KeyCode::Left.into()))),
|
||||||
Ok(Some(InternalEvent::Event(Event::Key(KeyCode::Left.into()))))
|
b'C' => Ok(Some(Event::Key(KeyCode::Right.into()))),
|
||||||
}
|
b'A' => Ok(Some(Event::Key(KeyCode::Up.into()))),
|
||||||
b'C' => Ok(Some(InternalEvent::Event(Event::Key(
|
b'B' => Ok(Some(Event::Key(KeyCode::Down.into()))),
|
||||||
KeyCode::Right.into(),
|
b'H' => Ok(Some(Event::Key(KeyCode::Home.into()))),
|
||||||
)))),
|
b'F' => Ok(Some(Event::Key(KeyCode::End.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
|
// F1-F4
|
||||||
val @ b'P'..=b'S' => Ok(Some(InternalEvent::Event(Event::Key(
|
val @ b'P'..=b'S' => {
|
||||||
KeyCode::F(1 + val - b'P').into(),
|
Ok(Some(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(InternalEvent::Event(Event::Key(KeyCode::Esc.into())))),
|
b'\x1B' => Ok(Some(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 InternalEvent::Event(Event::Key(key_event)) = event {
|
if let 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;
|
||||||
InternalEvent::Event(Event::Key(alt_key_event))
|
Event::Key(alt_key_event)
|
||||||
} else {
|
} else {
|
||||||
event
|
event
|
||||||
}
|
}
|
||||||
@ -89,38 +72,26 @@ pub(crate) fn parse_event(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
b'\r' => Ok(Some(InternalEvent::Event(Event::Key(
|
b'\r' => Ok(Some(Event::Key(KeyCode::Enter.into()))),
|
||||||
KeyCode::Enter.into(),
|
b'\t' => Ok(Some(Event::Key(KeyCode::Tab.into()))),
|
||||||
)))),
|
b'\x7F' => Ok(Some(Event::Key(KeyCode::Backspace.into()))),
|
||||||
// Issue #371: \n = 0xA, which is also the keycode for Ctrl+J. The only reason we get
|
c @ b'\x01'..=b'\x1A' => Ok(Some(Event::Key(KeyEvent::new(
|
||||||
// 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(InternalEvent::Event(Event::Key(KeyEvent::new(
|
c @ b'\x1C'..=b'\x1F' => Ok(Some(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(InternalEvent::Event(Event::Key(KeyEvent::new(
|
b'\0' => Ok(Some(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)
|
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -134,7 +105,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<InternalEvent>> {
|
pub(crate) fn parse_csi(buffer: &[u8]) -> io::Result<Option<Event>> {
|
||||||
assert!(buffer.starts_with(&[b'\x1B', b'['])); // ESC [
|
assert!(buffer.starts_with(&[b'\x1B', b'['])); // ESC [
|
||||||
|
|
||||||
if buffer.len() == 2 {
|
if buffer.len() == 2 {
|
||||||
@ -210,7 +181,7 @@ pub(crate) fn parse_csi(buffer: &[u8]) -> io::Result<Option<InternalEvent>> {
|
|||||||
_ => return Err(could_not_parse_event_error()),
|
_ => return Err(could_not_parse_event_error()),
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(input_event.map(InternalEvent::Event))
|
Ok(input_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>
|
||||||
@ -238,7 +209,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<InternalEvent>> {
|
pub(crate) fn parse_csi_cursor_position(buffer: &[u8]) -> io::Result<Option<Event>> {
|
||||||
// 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)
|
||||||
@ -253,10 +224,10 @@ pub(crate) fn parse_csi_cursor_position(buffer: &[u8]) -> io::Result<Option<Inte
|
|||||||
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(InternalEvent::CursorPosition(x, y)))
|
Ok(Some(Event::CursorPosition(x, y)))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_csi_keyboard_enhancement_flags(buffer: &[u8]) -> io::Result<Option<InternalEvent>> {
|
fn parse_csi_keyboard_enhancement_flags(buffer: &[u8]) -> io::Result<Option<Event>> {
|
||||||
// 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']));
|
||||||
@ -285,10 +256,10 @@ fn parse_csi_keyboard_enhancement_flags(buffer: &[u8]) -> io::Result<Option<Inte
|
|||||||
// flags |= KeyboardEnhancementFlags::REPORT_ASSOCIATED_TEXT;
|
// flags |= KeyboardEnhancementFlags::REPORT_ASSOCIATED_TEXT;
|
||||||
// }
|
// }
|
||||||
|
|
||||||
Ok(Some(InternalEvent::KeyboardEnhancementFlags(flags)))
|
Ok(Some(Event::KeyboardEnhancementFlags(flags)))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_csi_primary_device_attributes(buffer: &[u8]) -> io::Result<Option<InternalEvent>> {
|
fn parse_csi_primary_device_attributes(buffer: &[u8]) -> io::Result<Option<Event>> {
|
||||||
// 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']));
|
||||||
@ -297,7 +268,7 @@ fn parse_csi_primary_device_attributes(buffer: &[u8]) -> io::Result<Option<Inter
|
|||||||
// 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(InternalEvent::PrimaryDeviceAttributes))
|
Ok(Some(Event::PrimaryDeviceAttributes))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_modifiers(mask: u8) -> KeyModifiers {
|
fn parse_modifiers(mask: u8) -> KeyModifiers {
|
||||||
@ -345,7 +316,7 @@ fn parse_key_event_kind(kind: u8) -> KeyEventKind {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn parse_csi_modifier_key_code(buffer: &[u8]) -> io::Result<Option<InternalEvent>> {
|
pub(crate) fn parse_csi_modifier_key_code(buffer: &[u8]) -> io::Result<Option<Event>> {
|
||||||
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])
|
||||||
@ -390,7 +361,7 @@ pub(crate) fn parse_csi_modifier_key_code(buffer: &[u8]) -> io::Result<Option<In
|
|||||||
|
|
||||||
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(InternalEvent::Event(input_event)))
|
Ok(Some(input_event))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn translate_functional_key_code(codepoint: u32) -> Option<(KeyCode, KeyEventState)> {
|
fn translate_functional_key_code(codepoint: u32) -> Option<(KeyCode, KeyEventState)> {
|
||||||
@ -494,7 +465,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<InternalEvent>> {
|
pub(crate) fn parse_csi_u_encoded_key_code(buffer: &[u8]) -> io::Result<Option<Event>> {
|
||||||
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']));
|
||||||
|
|
||||||
@ -545,11 +516,6 @@ pub(crate) fn parse_csi_u_encoded_key_code(buffer: &[u8]) -> io::Result<Option<I
|
|||||||
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
|
||||||
@ -613,10 +579,10 @@ pub(crate) fn parse_csi_u_encoded_key_code(buffer: &[u8]) -> io::Result<Option<I
|
|||||||
state_from_keycode | state_from_modifiers,
|
state_from_keycode | state_from_modifiers,
|
||||||
));
|
));
|
||||||
|
|
||||||
Ok(Some(InternalEvent::Event(input_event)))
|
Ok(Some(input_event))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn parse_csi_special_key_code(buffer: &[u8]) -> io::Result<Option<InternalEvent>> {
|
pub(crate) fn parse_csi_special_key_code(buffer: &[u8]) -> io::Result<Option<Event>> {
|
||||||
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'~']));
|
||||||
|
|
||||||
@ -657,10 +623,10 @@ pub(crate) fn parse_csi_special_key_code(buffer: &[u8]) -> io::Result<Option<Int
|
|||||||
keycode, modifiers, kind, state,
|
keycode, modifiers, kind, state,
|
||||||
));
|
));
|
||||||
|
|
||||||
Ok(Some(InternalEvent::Event(input_event)))
|
Ok(Some(input_event))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn parse_csi_rxvt_mouse(buffer: &[u8]) -> io::Result<Option<InternalEvent>> {
|
pub(crate) fn parse_csi_rxvt_mouse(buffer: &[u8]) -> io::Result<Option<Event>> {
|
||||||
// rxvt mouse encoding:
|
// rxvt mouse encoding:
|
||||||
// ESC [ Cb ; Cx ; Cy ; M
|
// ESC [ Cb ; Cx ; Cy ; M
|
||||||
|
|
||||||
@ -679,15 +645,15 @@ pub(crate) fn parse_csi_rxvt_mouse(buffer: &[u8]) -> io::Result<Option<InternalE
|
|||||||
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(InternalEvent::Event(Event::Mouse(MouseEvent {
|
Ok(Some(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<InternalEvent>> {
|
pub(crate) fn parse_csi_normal_mouse(buffer: &[u8]) -> io::Result<Option<Event>> {
|
||||||
// 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
|
||||||
@ -707,15 +673,15 @@ pub(crate) fn parse_csi_normal_mouse(buffer: &[u8]) -> io::Result<Option<Interna
|
|||||||
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(InternalEvent::Event(Event::Mouse(MouseEvent {
|
Ok(Some(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<InternalEvent>> {
|
pub(crate) fn parse_csi_sgr_mouse(buffer: &[u8]) -> io::Result<Option<Event>> {
|
||||||
// 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 [ <
|
||||||
@ -752,12 +718,12 @@ pub(crate) fn parse_csi_sgr_mouse(buffer: &[u8]) -> io::Result<Option<InternalEv
|
|||||||
kind
|
kind
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(Some(InternalEvent::Event(Event::Mouse(MouseEvent {
|
Ok(Some(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
|
||||||
@ -810,7 +776,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<InternalEvent>> {
|
pub(crate) fn parse_csi_bracketed_paste(buffer: &[u8]) -> io::Result<Option<Event>> {
|
||||||
// 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~"));
|
||||||
|
|
||||||
@ -818,7 +784,7 @@ pub(crate) fn parse_csi_bracketed_paste(buffer: &[u8]) -> io::Result<Option<Inte
|
|||||||
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(InternalEvent::Event(Event::Paste(paste))))
|
Ok(Some(Event::Paste(paste)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -871,7 +837,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(InternalEvent::Event(Event::Key(KeyCode::Esc.into()))),
|
Some(Event::Key(KeyCode::Esc.into())),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -884,10 +850,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(InternalEvent::Event(Event::Key(KeyEvent::new(
|
Some(Event::Key(KeyEvent::new(
|
||||||
KeyCode::Char('c'),
|
KeyCode::Char('c'),
|
||||||
KeyModifiers::ALT
|
KeyModifiers::ALT
|
||||||
)))),
|
))),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -895,10 +861,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(InternalEvent::Event(Event::Key(KeyEvent::new(
|
Some(Event::Key(KeyEvent::new(
|
||||||
KeyCode::Char('H'),
|
KeyCode::Char('H'),
|
||||||
KeyModifiers::ALT | KeyModifiers::SHIFT
|
KeyModifiers::ALT | KeyModifiers::SHIFT
|
||||||
)))),
|
))),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -906,10 +872,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(InternalEvent::Event(Event::Key(KeyEvent::new(
|
Some(Event::Key(KeyEvent::new(
|
||||||
KeyCode::Char('t'),
|
KeyCode::Char('t'),
|
||||||
KeyModifiers::ALT | KeyModifiers::CONTROL
|
KeyModifiers::ALT | KeyModifiers::CONTROL
|
||||||
)))),
|
))),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -921,79 +887,77 @@ 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(InternalEvent::CursorPosition(9, 19))
|
Some(Event::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(InternalEvent::Event(Event::Key(KeyCode::Left.into()))),
|
Some(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(InternalEvent::Event(Event::Key(KeyEvent::new(
|
Some(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(InternalEvent::Event(Event::Key(KeyCode::Delete.into()))),
|
Some(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(InternalEvent::Event(Event::Paste(
|
Some(Event::Paste("on and on and on".to_string())),
|
||||||
"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(InternalEvent::Event(Event::Mouse(MouseEvent {
|
Some(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(InternalEvent::Event(Event::Mouse(MouseEvent {
|
Some(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(InternalEvent::Event(Event::Mouse(MouseEvent {
|
Some(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(InternalEvent::Event(Event::Key(KeyEvent::new(
|
Some(Event::Key(KeyEvent::new(
|
||||||
KeyCode::Char('Ž'),
|
KeyCode::Char('Ž'),
|
||||||
KeyModifiers::SHIFT
|
KeyModifiers::SHIFT
|
||||||
)))),
|
))),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1001,7 +965,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(InternalEvent::Event(Event::Key(KeyCode::Tab.into()))),
|
Some(Event::Key(KeyCode::Tab.into())),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1009,7 +973,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(InternalEvent::CursorPosition(9, 19))
|
Some(Event::CursorPosition(9, 19))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1017,7 +981,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(InternalEvent::Event(Event::Key(KeyCode::Left.into()))),
|
Some(Event::Key(KeyCode::Left.into())),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1025,10 +989,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(InternalEvent::Event(Event::Key(KeyEvent::new(
|
Some(Event::Key(KeyEvent::new(
|
||||||
KeyCode::Left,
|
KeyCode::Left,
|
||||||
KeyModifiers::SHIFT
|
KeyModifiers::SHIFT
|
||||||
)))),
|
))),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1036,7 +1000,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(InternalEvent::Event(Event::Key(KeyCode::Delete.into()))),
|
Some(Event::Key(KeyCode::Delete.into())),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1044,10 +1008,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(InternalEvent::Event(Event::Key(KeyEvent::new(
|
Some(Event::Key(KeyEvent::new(
|
||||||
KeyCode::Delete,
|
KeyCode::Delete,
|
||||||
KeyModifiers::SHIFT
|
KeyModifiers::SHIFT
|
||||||
)))),
|
))),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1067,28 +1031,25 @@ 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(InternalEvent::Event(Event::Paste("o\x1B[2D".to_string())))
|
Some(Event::Paste("o\x1B[2D".to_string()))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_parse_csi_focus() {
|
fn test_parse_csi_focus() {
|
||||||
assert_eq!(
|
assert_eq!(parse_csi(b"\x1B[O").unwrap(), Some(Event::FocusLost));
|
||||||
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(InternalEvent::Event(Event::Mouse(MouseEvent {
|
Some(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(),
|
||||||
})))
|
}))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1096,12 +1057,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(InternalEvent::Event(Event::Mouse(MouseEvent {
|
Some(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,
|
||||||
})))
|
}))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1109,39 +1070,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(InternalEvent::Event(Event::Mouse(MouseEvent {
|
Some(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(InternalEvent::Event(Event::Mouse(MouseEvent {
|
Some(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(InternalEvent::Event(Event::Mouse(MouseEvent {
|
Some(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(InternalEvent::Event(Event::Mouse(MouseEvent {
|
Some(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(),
|
||||||
})))
|
}))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1193,10 +1154,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(InternalEvent::Event(Event::Key(KeyEvent::new(
|
Some(Event::Key(KeyEvent::new(
|
||||||
KeyCode::Char('c'),
|
KeyCode::Char('c'),
|
||||||
KeyModifiers::empty()
|
KeyModifiers::empty()
|
||||||
)))),
|
))),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1204,10 +1165,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(InternalEvent::Event(Event::Key(KeyEvent::new(
|
Some(Event::Key(KeyEvent::new(
|
||||||
KeyCode::Char('C'),
|
KeyCode::Char('C'),
|
||||||
KeyModifiers::SHIFT
|
KeyModifiers::SHIFT
|
||||||
)))),
|
))),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1215,24 +1176,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(InternalEvent::Event(Event::Key(KeyEvent::new(
|
Some(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(InternalEvent::Event(Event::Key(KeyEvent::new(
|
Some(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(InternalEvent::Event(Event::Key(KeyEvent::new(
|
Some(Event::Key(KeyEvent::new(
|
||||||
KeyCode::Char('a'),
|
KeyCode::Char('a'),
|
||||||
KeyModifiers::ALT | KeyModifiers::CONTROL
|
KeyModifiers::ALT | KeyModifiers::CONTROL
|
||||||
)))),
|
))),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1240,45 +1201,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(InternalEvent::Event(Event::Key(KeyEvent::new(
|
Some(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(InternalEvent::Event(Event::Key(KeyEvent::new(
|
Some(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(InternalEvent::Event(Event::Key(KeyEvent::new(
|
Some(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(InternalEvent::Event(Event::Key(KeyEvent::new(
|
Some(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(InternalEvent::Event(Event::Key(KeyEvent::new(
|
Some(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(InternalEvent::Event(Event::Key(KeyEvent::new(
|
Some(Event::Key(KeyEvent::new(
|
||||||
KeyCode::Modifier(ModifierKeyCode::LeftShift),
|
KeyCode::Modifier(ModifierKeyCode::LeftShift),
|
||||||
KeyModifiers::SHIFT,
|
KeyModifiers::SHIFT,
|
||||||
)))),
|
))),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1286,24 +1247,20 @@ 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(InternalEvent::Event(Event::Key(
|
Some(Event::Key(KeyEvent::new_with_kind_and_state(
|
||||||
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(InternalEvent::Event(Event::Key(
|
Some(Event::Key(KeyEvent::new_with_kind_and_state(
|
||||||
KeyEvent::new_with_kind_and_state(
|
KeyCode::Up,
|
||||||
KeyCode::Up,
|
KeyModifiers::empty(),
|
||||||
KeyModifiers::empty(),
|
KeyEventKind::Press,
|
||||||
KeyEventKind::Press,
|
KeyEventState::KEYPAD,
|
||||||
KeyEventState::KEYPAD,
|
|
||||||
)
|
|
||||||
))),
|
))),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -1312,43 +1269,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(InternalEvent::Event(Event::Key(KeyEvent::new_with_kind(
|
Some(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(InternalEvent::Event(Event::Key(KeyEvent::new_with_kind(
|
Some(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(InternalEvent::Event(Event::Key(KeyEvent::new_with_kind(
|
Some(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(InternalEvent::Event(Event::Key(KeyEvent::new_with_kind(
|
Some(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(InternalEvent::Event(Event::Key(KeyEvent::new_with_kind(
|
Some(Event::Key(KeyEvent::new_with_kind(
|
||||||
KeyCode::Char('a'),
|
KeyCode::Char('a'),
|
||||||
KeyModifiers::empty(),
|
KeyModifiers::empty(),
|
||||||
KeyEventKind::Release,
|
KeyEventKind::Release,
|
||||||
)))),
|
))),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1356,40 +1313,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(InternalEvent::Event(Event::Key(KeyEvent::new_with_kind(
|
Some(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(InternalEvent::Event(Event::Key(KeyEvent::new_with_kind(
|
Some(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(InternalEvent::Event(Event::Key(KeyEvent::new(
|
Some(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(InternalEvent::Event(Event::Key(KeyEvent::new(
|
Some(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(InternalEvent::Event(Event::Key(KeyEvent::new(
|
Some(Event::Key(KeyEvent::new(
|
||||||
KeyCode::Modifier(ModifierKeyCode::RightMeta),
|
KeyCode::Modifier(ModifierKeyCode::RightMeta),
|
||||||
KeyModifiers::META,
|
KeyModifiers::META,
|
||||||
)))),
|
))),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1397,24 +1354,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(InternalEvent::Event(Event::Key(KeyEvent::new(
|
Some(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(InternalEvent::Event(Event::Key(KeyEvent::new(
|
Some(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(InternalEvent::Event(Event::Key(KeyEvent::new(
|
Some(Event::Key(KeyEvent::new(
|
||||||
KeyCode::Char('a'),
|
KeyCode::Char('a'),
|
||||||
KeyModifiers::META,
|
KeyModifiers::META,
|
||||||
)))),
|
))),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1422,24 +1379,20 @@ 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(InternalEvent::Event(Event::Key(
|
Some(Event::Key(KeyEvent::new_with_kind_and_state(
|
||||||
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(InternalEvent::Event(Event::Key(
|
Some(Event::Key(KeyEvent::new_with_kind_and_state(
|
||||||
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,
|
|
||||||
)
|
|
||||||
))),
|
))),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -1449,18 +1402,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(InternalEvent::Event(Event::Key(KeyEvent::new(
|
Some(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(InternalEvent::Event(Event::Key(KeyEvent::new(
|
Some(Event::Key(KeyEvent::new(
|
||||||
KeyCode::Char('_'),
|
KeyCode::Char('_'),
|
||||||
KeyModifiers::ALT,
|
KeyModifiers::ALT,
|
||||||
)))),
|
))),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1468,19 +1421,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(InternalEvent::Event(Event::Key(KeyEvent::new_with_kind(
|
Some(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(InternalEvent::Event(Event::Key(KeyEvent::new_with_kind(
|
Some(Event::Key(KeyEvent::new_with_kind(
|
||||||
KeyCode::Down,
|
KeyCode::Down,
|
||||||
KeyModifiers::empty(),
|
KeyModifiers::empty(),
|
||||||
KeyEventKind::Release,
|
KeyEventKind::Release,
|
||||||
)))),
|
))),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1488,19 +1441,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(InternalEvent::Event(Event::Key(KeyEvent::new_with_kind(
|
Some(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(InternalEvent::Event(Event::Key(KeyEvent::new_with_kind(
|
Some(Event::Key(KeyEvent::new_with_kind(
|
||||||
KeyCode::PageDown,
|
KeyCode::PageDown,
|
||||||
KeyModifiers::CONTROL,
|
KeyModifiers::CONTROL,
|
||||||
KeyEventKind::Release,
|
KeyEventKind::Release,
|
||||||
)))),
|
))),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,11 +0,0 @@
|
|||||||
#[cfg(feature = "use-dev-tty")]
|
|
||||||
pub(crate) mod tty;
|
|
||||||
|
|
||||||
#[cfg(not(feature = "use-dev-tty"))]
|
|
||||||
pub(crate) mod mio;
|
|
||||||
|
|
||||||
#[cfg(feature = "use-dev-tty")]
|
|
||||||
pub(crate) use self::tty::Waker;
|
|
||||||
|
|
||||||
#[cfg(not(feature = "use-dev-tty"))]
|
|
||||||
pub(crate) use self::mio::Waker;
|
|
@ -1,34 +0,0 @@
|
|||||||
use std::sync::{Arc, Mutex};
|
|
||||||
|
|
||||||
use ::mio::{Registry, Token};
|
|
||||||
|
|
||||||
/// Allows to wake up the `mio::Poll::poll()` method.
|
|
||||||
/// This type wraps `mio::Waker`, for more information see its documentation.
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
pub(crate) struct Waker {
|
|
||||||
inner: Arc<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(())
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,28 +0,0 @@
|
|||||||
use std::{
|
|
||||||
io::{self, Write},
|
|
||||||
os::unix::net::UnixStream,
|
|
||||||
sync::{Arc, Mutex},
|
|
||||||
};
|
|
||||||
|
|
||||||
/// Allows to wake up the EventSource::try_read() method.
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
pub(crate) struct Waker {
|
|
||||||
inner: Arc<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(())
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,48 +0,0 @@
|
|||||||
//! This is a WINDOWS specific implementation for input related action.
|
|
||||||
|
|
||||||
use std::convert::TryFrom;
|
|
||||||
use std::io;
|
|
||||||
use std::sync::atomic::{AtomicU64, Ordering};
|
|
||||||
|
|
||||||
use crossterm_winapi::{ConsoleMode, Handle};
|
|
||||||
|
|
||||||
pub(crate) mod parse;
|
|
||||||
pub(crate) mod poll;
|
|
||||||
#[cfg(feature = "event-stream")]
|
|
||||||
pub(crate) mod waker;
|
|
||||||
|
|
||||||
const ENABLE_MOUSE_MODE: u32 = 0x0010 | 0x0080 | 0x0008;
|
|
||||||
|
|
||||||
/// This is a either `u64::MAX` if it's uninitialized or a valid `u32` that stores the original
|
|
||||||
/// console mode if it's initialized.
|
|
||||||
static ORIGINAL_CONSOLE_MODE: AtomicU64 = AtomicU64::new(u64::MAX);
|
|
||||||
|
|
||||||
/// Initializes the default console color. It will will be skipped if it has already been initialized.
|
|
||||||
fn init_original_console_mode(original_mode: u32) {
|
|
||||||
let _ = ORIGINAL_CONSOLE_MODE.compare_exchange(
|
|
||||||
u64::MAX,
|
|
||||||
u64::from(original_mode),
|
|
||||||
Ordering::Relaxed,
|
|
||||||
Ordering::Relaxed,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the original console color, make sure to call `init_console_color` before calling this function. Otherwise this function will panic.
|
|
||||||
fn original_console_mode() -> std::io::Result<u32> {
|
|
||||||
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(())
|
|
||||||
}
|
|
@ -1,378 +0,0 @@
|
|||||||
use crossterm_winapi::{ControlKeyState, EventFlags, KeyEventRecord, ScreenBuffer};
|
|
||||||
use winapi::um::{
|
|
||||||
wincon::{
|
|
||||||
CAPSLOCK_ON, LEFT_ALT_PRESSED, LEFT_CTRL_PRESSED, RIGHT_ALT_PRESSED, RIGHT_CTRL_PRESSED,
|
|
||||||
SHIFT_PRESSED,
|
|
||||||
},
|
|
||||||
winuser::{
|
|
||||||
GetForegroundWindow, GetKeyboardLayout, GetWindowThreadProcessId, ToUnicodeEx, VK_BACK,
|
|
||||||
VK_CONTROL, VK_DELETE, VK_DOWN, VK_END, VK_ESCAPE, VK_F1, VK_F24, VK_HOME, VK_INSERT,
|
|
||||||
VK_LEFT, VK_MENU, VK_NEXT, VK_NUMPAD0, VK_NUMPAD9, VK_PRIOR, VK_RETURN, VK_RIGHT, VK_SHIFT,
|
|
||||||
VK_TAB, VK_UP,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::event::{
|
|
||||||
Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers, MouseButton, MouseEvent, MouseEventKind,
|
|
||||||
};
|
|
||||||
|
|
||||||
#[derive(Default)]
|
|
||||||
pub struct MouseButtonsPressed {
|
|
||||||
pub(crate) left: bool,
|
|
||||||
pub(crate) right: bool,
|
|
||||||
pub(crate) middle: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn handle_mouse_event(
|
|
||||||
mouse_event: crossterm_winapi::MouseEvent,
|
|
||||||
buttons_pressed: &MouseButtonsPressed,
|
|
||||||
) -> Option<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,
|
|
||||||
}))
|
|
||||||
}
|
|
@ -1,86 +0,0 @@
|
|||||||
use std::io;
|
|
||||||
use std::time::Duration;
|
|
||||||
|
|
||||||
use crossterm_winapi::Handle;
|
|
||||||
use winapi::{
|
|
||||||
shared::winerror::WAIT_TIMEOUT,
|
|
||||||
um::{
|
|
||||||
synchapi::WaitForMultipleObjects,
|
|
||||||
winbase::{INFINITE, WAIT_ABANDONED_0, WAIT_FAILED, WAIT_OBJECT_0},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
#[cfg(feature = "event-stream")]
|
|
||||||
pub(crate) use super::waker::Waker;
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub(crate) struct WinApiPoll {
|
|
||||||
#[cfg(feature = "event-stream")]
|
|
||||||
waker: Waker,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl WinApiPoll {
|
|
||||||
#[cfg(not(feature = "event-stream"))]
|
|
||||||
pub(crate) fn new() -> WinApiPoll {
|
|
||||||
WinApiPoll {}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(feature = "event-stream")]
|
|
||||||
pub(crate) fn new() -> std::io::Result<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()
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,40 +0,0 @@
|
|||||||
use std::sync::{Arc, Mutex};
|
|
||||||
|
|
||||||
use crossterm_winapi::Semaphore;
|
|
||||||
|
|
||||||
/// Allows to wake up the `WinApiPoll::poll()` method.
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
pub(crate) struct Waker {
|
|
||||||
inner: Arc<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()
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,92 +0,0 @@
|
|||||||
use std::time::{Duration, Instant};
|
|
||||||
|
|
||||||
/// Keeps track of the elapsed time since the moment the polling started.
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct PollTimeout {
|
|
||||||
timeout: Option<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,20 +241,5 @@ 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,21 +161,23 @@ 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 {
|
||||||
env::var("TERM")
|
#[cfg(windows)]
|
||||||
.map(|x| if x.contains("256color") { 256 } else { 8 })
|
{
|
||||||
.unwrap_or(8)
|
// Check if we're running in a pseudo TTY, which supports true color.
|
||||||
}
|
// Fall back to env vars otherwise for other terminals on Windows.
|
||||||
|
if crate::ansi_support::supports_ansi() {
|
||||||
|
return u16::MAX;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Forces colored output on or off globally, overriding NO_COLOR.
|
const DEFAULT: u16 = 8;
|
||||||
///
|
env::var("COLORTERM")
|
||||||
/// # Notes
|
.or_else(|_| env::var("TERM"))
|
||||||
///
|
.map_or(DEFAULT, |x| match x {
|
||||||
/// crossterm supports NO_COLOR (https://no-color.org/) to disabled colored output.
|
_ if x.contains("24bit") || x.contains("truecolor") => u16::MAX,
|
||||||
///
|
_ if x.contains("256") => 256,
|
||||||
/// This API allows applications to override that behavior and force colorized output
|
_ => DEFAULT,
|
||||||
/// 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.
|
||||||
@ -519,3 +521,89 @@ 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,6 +1,4 @@
|
|||||||
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};
|
||||||
@ -19,13 +17,10 @@ pub enum Colored {
|
|||||||
/// A background color.
|
/// A background color.
|
||||||
BackgroundColor(Color),
|
BackgroundColor(Color),
|
||||||
/// An underline color.
|
/// An underline color.
|
||||||
/// Imporant: doesnt work on windows 10 or lower.
|
/// Important: doesn't 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
|
||||||
@ -69,38 +64,12 @@ 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,60 +83,15 @@
|
|||||||
//!
|
//!
|
||||||
//! 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, io};
|
use std::fmt;
|
||||||
|
|
||||||
#[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,
|
||||||
@ -145,15 +100,6 @@ 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;
|
||||||
@ -301,11 +247,6 @@ 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.
|
||||||
@ -323,11 +264,6 @@ 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.
|
||||||
@ -351,11 +287,6 @@ 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)`.
|
||||||
@ -370,11 +301,6 @@ 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
|
||||||
@ -389,11 +315,6 @@ 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.
|
||||||
@ -436,17 +357,6 @@ 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.
|
||||||
@ -489,17 +399,6 @@ 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);
|
||||||
|
@ -1,27 +0,0 @@
|
|||||||
//! This module provides platform related functions.
|
|
||||||
|
|
||||||
#[cfg(unix)]
|
|
||||||
#[cfg(feature = "events")]
|
|
||||||
pub use self::unix::supports_keyboard_enhancement;
|
|
||||||
#[cfg(unix)]
|
|
||||||
pub(crate) use self::unix::{
|
|
||||||
disable_raw_mode, enable_raw_mode, 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;
|
|
@ -1,154 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
@ -1,315 +0,0 @@
|
|||||||
//! 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(())
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,471 +0,0 @@
|
|||||||
//! WinAPI related logic for terminal manipulation.
|
|
||||||
|
|
||||||
use std::fmt::{self, Write};
|
|
||||||
use std::io::{self};
|
|
||||||
|
|
||||||
use crossterm_winapi::{Console, ConsoleMode, Coord, Handle, ScreenBuffer, Size};
|
|
||||||
use winapi::{
|
|
||||||
shared::minwindef::DWORD,
|
|
||||||
um::wincon::{SetConsoleTitleW, ENABLE_ECHO_INPUT, ENABLE_LINE_INPUT, ENABLE_PROCESSED_INPUT},
|
|
||||||
};
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
cursor,
|
|
||||||
terminal::{ClearType, WindowSize},
|
|
||||||
};
|
|
||||||
|
|
||||||
/// bits which can't be set in raw mode
|
|
||||||
const NOT_RAW_MODE_MASK: DWORD = ENABLE_LINE_INPUT | ENABLE_ECHO_INPUT | ENABLE_PROCESSED_INPUT;
|
|
||||||
|
|
||||||
pub(crate) fn is_raw_mode_enabled() -> std::io::Result<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
54
src/tty.rs
@ -1,54 +0,0 @@
|
|||||||
//! Making it a little more convenient and safe to query whether
|
|
||||||
//! something is a terminal teletype or not.
|
|
||||||
//! This module defines the IsTty trait and the is_tty method to
|
|
||||||
//! return true if the item represents a terminal.
|
|
||||||
|
|
||||||
#[cfg(unix)]
|
|
||||||
use std::os::unix::io::AsRawFd;
|
|
||||||
#[cfg(windows)]
|
|
||||||
use std::os::windows::io::AsRawHandle;
|
|
||||||
|
|
||||||
#[cfg(windows)]
|
|
||||||
use winapi::um::consoleapi::GetConsoleMode;
|
|
||||||
|
|
||||||
/// Adds the `is_tty` method to types that might represent a terminal
|
|
||||||
///
|
|
||||||
/// ```rust
|
|
||||||
/// use std::io::stdout;
|
|
||||||
/// use 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