Input module Rewrite (#284)

This commit is contained in:
Timon 2019-11-18 21:50:57 +01:00 committed by GitHub
parent bf51821238
commit f597cfd232
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
46 changed files with 2975 additions and 3030 deletions

View File

@ -50,20 +50,23 @@ jobs:
- name: Test all features
run: cargo test --all-features --no-default-features -- --nocapture --test-threads 1
continue-on-error: ${{ matrix.can-fail }}
- name: Test cursor Feature
- name: Test cursor feature
run: cargo test --features cursor --no-default-features --lib -- --nocapture --test-threads 1
continue-on-error: ${{ matrix.can-fail }}
- name: Test style Feature
- name: Test style feature
run: cargo test --features style --no-default-features --lib -- --nocapture --test-threads 1
continue-on-error: ${{ matrix.can-fail }}
- name: Test terminal Feature
- name: Test terminal feature
run: cargo test --features terminal --no-default-features --lib -- --nocapture --test-threads 1
continue-on-error: ${{ matrix.can-fail }}
- name: Test screen Feature
- name: Test screen feature
run: cargo test --features screen --no-default-features --lib -- --nocapture --test-threads 1
continue-on-error: ${{ matrix.can-fail }}
- name: Test input Feature
run: cargo test --features input --no-default-features --lib -- --nocapture --test-threads 1
- name: Test event feature
run: cargo test --features event --no-default-features --lib -- --nocapture --test-threads 1
continue-on-error: ${{ matrix.can-fail }}
- name: Test async-event feature
run: cargo test --features event-stream --no-default-features --lib -- --nocapture --test-threads 1
continue-on-error: ${{ matrix.can-fail }}
- name: Test Packaging
if: matrix.rust == 'stable'

View File

@ -38,4 +38,5 @@ script:
- cargo test --features style --no-default-features --lib -- --nocapture --test-threads 1
- cargo test --features terminal --no-default-features --lib -- --nocapture --test-threads 1
- cargo test --features screen --no-default-features --lib -- --nocapture --test-threads 1
- cargo test --features input --no-default-features --lib -- --nocapture --test-threads 1
- cargo test --features event --no-default-features --lib -- --nocapture --test-threads 1
- cargo test --features event-stream --no-default-features --lib -- --nocapture --test-threads 1

View File

@ -1,7 +1,13 @@
# Version Master
- `queue!` & `execute!` macros allow trailing comma
- Replace the `input` module with brand new `event` module
- It's **highly recommended** to read the
[Upgrade from 0.13 to 0.14](https://github.com/crossterm-rs/crossterm/wiki/Upgrade-from-0.13-to-0.14)
documentation
* Replace `docs/UPGRADE.md` with the [Upgrade Paths](https://github.com/crossterm-rs/crossterm/wiki#upgrade-paths)
documentation
# Version 0.13.3
- Remove thread from AsyncReader on Windows.

View File

@ -12,26 +12,40 @@ readme = "README.md"
edition = "2018"
categories = ["command-line-interface", "command-line-utilities"]
[lib]
name = "crossterm"
path = "src/lib.rs"
[package.metadata.docs.rs]
all-features = true
[features]
default = ["cursor", "style", "terminal", "screen", "input"]
cursor = ["lazy_static", "input", "winapi/wincon", "winapi/winnt", "winapi/minwindef"]
default = ["cursor", "style", "terminal", "screen", "event"]
cursor = ["lazy_static", "event", "winapi/wincon", "winapi/winnt", "winapi/minwindef"]
style = ["lazy_static", "winapi/wincon"]
terminal = ["cursor"]
screen = ["lazy_static", "winapi/wincon", "winapi/minwindef"]
input = ["mio", "lazy_static", "screen", "winapi/winnt", "winapi/winuser"]
event = ["bitflags", "mio", "lazy_static", "parking_lot", "screen", "terminal", "winapi/winnt", "winapi/winuser", "winapi/wincon", "winapi/synchapi", "winapi/namedpipeapi", "winapi/impl-default"]
event-stream = ["event", "futures"]
[dependencies]
bitflags = { version = "1.2", optional = true }
lazy_static = { version = "1.4", optional = true }
parking_lot = { version = "0.9", optional = true }
serde = { version = "1.0.0", features = ["derive"], optional = true }
futures = {version = "0.3", optional = true }
[target.'cfg(windows)'.dependencies]
winapi = "0.3.8"
crossterm_winapi = "0.4.0"
crossterm_winapi = { git = "https://github.com/crossterm-rs/crossterm-winapi", branch = "input_rewrite_changes" }
[target.'cfg(unix)'.dependencies]
libc = "0.2.51"
mio = { version = "0.6.19", optional = true }
signal-hook = { version = "0.1.11", features=["mio-support"]}
[lib]
name = "crossterm"
path = "src/lib.rs"
[dev-dependencies]
tokio = "0.2.0-alpha.6"
futures = "0.3"
futures-timer = "2"
async-std = "1"

View File

@ -16,14 +16,22 @@ see [Tested Terminals](#tested-terminals) for more info).
## Note on Migration
You may have noticed that Crossterm has been [changing](https://github.com/crossterm-rs/crossterm/blob/master/CHANGELOG.md) very quickly with the latest versions.
We have done a lot of API-breaking changes by renaming functions, commands, changing the exports, improving the encapsulation, etc..
However, all of this happens to improve the library and make it better and ready for a possible [1.0 release](#287).
We want to stick to the [Command API](https://docs.rs/crossterm/#command-api) and remove all other ways to use crossterm.
You may have noticed that Crossterm has been
[changing](https://github.com/crossterm-rs/crossterm/blob/master/CHANGELOG.md) very quickly with the latest versions.
We have done a lot of API-breaking changes by renaming functions, commands, changing the exports,
improving the encapsulation, etc. However, all of this happens to improve the library and make it better
and ready for a possible [1.0 release](#287). We want to stick to the
[Command API](https://docs.rs/crossterm/#command-api) and remove all other ways to use crossterm.
Try to use this API and change your code accordingly.
This way you will survive or overcome major migration problems ;).
We hope you can understand this, feel free to ask around in [discord](https://discord.gg/K4nyTDB) if you have questions. For up-to-date examples, have a look at the [examples](https://github.com/crossterm-rs/examples/tree/master) repository. Sorry for the inconvenience.
We hope you can understand this, feel free to ask around in [discord](https://discord.gg/K4nyTDB) if you have
questions. For up-to-date examples, have a look at the [examples](https://github.com/crossterm-rs/examples/tree/master)
repository. Sorry for the inconvenience.
> It's highly recommended to read the
> [Upgrade from 0.13 to 0.14](https://github.com/crossterm-rs/crossterm/wiki/Upgrade-from-0.13-to-0.14)
> documentation, which explains everything you need to know.
## Table of Contents

View File

@ -1,383 +0,0 @@
## Upgrade crossterm to 0.9.0
This release is all about moving to a stabilized API for 1.0. It has a lot of changes to the API however it has become much better.
### Removed functions
First you don't have to pass any screens or output's to the crossterm API. This makes the API much more easier to use.
_**old**_
_"Use this function when you want your terminal to operate with a specific output.
This could be useful when you have a screen which is in 'alternate mode', and you want your actions from the TerminalCursor, created by this function, to operate on the 'alternate screen'."_
Because crosstrem does not have to keep track of the output and you don't have to pass an `TerminalOutput` anymore those functions are removed.
```rust
use crossterm::{Screen, Terminal, TerminalCursor, TerminalColor, TerminalInput, Crossterm};
let screen = Screen::new(false);
Terminal::from_output(&screen.stdout);
TerminalCursor::from_output(&screen.stdout);
TerminalColor::from_output(&screen.stdout);
TerminalInput::from_output(&screen.stdout);
Crossterm::from_screen(&screen.stdout);
```
_**new**_
```rust
Terminal::new();
TerminalCursor::new();
TerminalColor::new();
TerminalInput::new();
Crossterm::new();
```
_"This could be used to paint the styled object onto the given screen. You have to pass a reference to the screen whereon you want to perform the painting"_
Because crossterm does not have to keep track of the output anymore those functions are removed.
_**old**_
```rust
use crossterm::{Crossterm, Screen, style};
let screen = Screen::new(false);
style("Some colored text")
.with(Color::Blue)
.on(Color::Black)
.paint(&screen);
let crossterm = Crossterm::new();
crossterm.style("Some colored text")
.with(Color::Blue)
.on(Color::Black)
.paint(&screen);
```
_**new**_
```rust
print!("{}", "Some colored text".blue().on_black());
```
### Removed Types
`Screen` was removed because it hadn't any purpose anymore.
_**old**_
use crossterm::Screen;
```rust
// create raw screen
let screen = Screen::new(true);
// create alternate raw screen
let screen = Screen::new(true);
let alternate = screen.enable_raw_modes(true);
```
_**new**_
```rust
use crossterm::{AlternateScreen, RawScreen, IntoRawModes};
let raw_screen = RawScreen::into_raw_mode();
let raw_screen = stdout().into_raw_mode();
let alternate = AlternateScreen::to_alternate(true);
```
### Renamed Functions
```rust
RawScreen::disable_raw_modes => RawScreen::disable_raw_mode
AlternateScreen::to_alternate_screen => Alternate::to_alternate
AlternateScreen::to_main_screen => Alternate::to_main
```
## Upgrade crossterm to 0.8.0
This update will cause problems with `read_async`. `read_async` first returned a type implementing `Read` it returns an `Iterator` of input events now.
See the examples for details on how this works.
## Upgrade crossterm to 0.7.0
Upgrade to `crossterm_style 0.2` caused some API changes.
- Introduced more `Attributes` and renamed some.
- Removed `ColorType` since it was unnecessary.
## Upgrade crossterm to 0.6.0
#### Namespace refactor
Some namespaces have been changed. All types of could be used directly by `use crossterm::*;` instead of having to go to a specific module for importing a type.
_**old**_
```rust
crossterm::input::{TerminalInput, ...};
crossterm::style::style;
crossterm::cursor::*;
crossterm::teriminal::{Terminal, ...};
```
_**new**_
```rust
crossterm::{TerminalInput, style, TerminalColor, StyledObject, Terminal, ...}
```
#### Removed unclear methods
```rust
let screen = Screen::new();
// the below methods are not available anymore
cursor::from_screen(&screen);
input::from_screen(&screen);
terminal::from_screen(&screen);
color::from_screen(&screen);
```
Instead of this you should make use of `Crossterm` type
```rust
let screen = Screen::new();
let crossterm = Crossterm::from_screen(screen);
let cursor = crossterm.cursor();
....
```
Or you can use the `from_output()`; this takes in the output stored in `Screen`
```rust
let screen = Screen::new();
let cursor = TerminalCursor::from_output(&screen.stdout);
let input = TerminalInput::from_output(&screen.stdout);
```
#### Wait until takes in a self now.
_**old**_
```rust
TerminalInput::wait_until(KeyEvent::OnKeyPress(b'x'));
```
_**new**_
```rust
let terminal_input = TerminalInput::new();
terminal_input.wait_until(KeyEvent::OnKeyPress(b'x'));
```
## Upgrade crossterm to 0.5.0
***WARNING***
I workded on making the user API more convenient therefore I had to make some changes to the user API. The problem with `0.4` is that you need to pass a `Screen` to the modules: `cursor(), color(), terminal()`.
In the new situation you only have to do this when working with raw or alternate screen. When you just want to perform actions like styling on the main screen you don't have to to pass in the `Screen` any more. This will look like the following:
#### 1. Remove `Screen` from the function calls: `cursor(), color(), terminal(), input()`
_**old**_
```
let screen = Screen::default();
let color = color(&screen);
let cursor = cursor(&screen);
let input = input(&screen);
let terminal = terminal(&screen);
let crossterm = Crossterm::new(&screen);
let terminal = Terminal::new(&screen.stdout);
let cursor = TerminalCursor::new(&screen.stdout);
let color = TerminalColor::new(&screen.stdout);
let input = TerminalInput::new(&screen.stdout);
```
_**new**_
```
let color = color();
let cursor = cursor();
let input = input();
let terminal = terminal();
let crossterm = Crossterm::new();
let terminal = Terminal::new();
let cursor = TerminalCursor::new();
let color = TerminalColor::new();
let input = TerminalInput::new();
```
#### 2. When working with alternate or raw screen.
When working with alternate and or raw screen you still have to provide a `Screen` instance since information of the alternate and raw screen is stored in it. When doing this, the actions of the module will be perfomed on the alternate screen. If you don't do this your actions will executed at the main screen.
```
use crossterm::cursor;
use crossterm::color;
use crossterm::input;
use crossterm::terminal;
let screen = Screen::default();
if let Ok(alternate) = screen.enable_alternate_modes(false) {
let screen = alternate.screen;
let color = color::from_screen(&screen);
let cursor = cursor::from_screen(&screen);
let input = input::from_screen(&screen);
let terminal = terminal::from_screen(&screen);
let crossterm = Crossterm::from_screen(&screen);
let terminal = Terminal::from_output(&screen.stdout);
let cursor = TerminalCursor::from_output(&screen.stdout);
let color = TerminalColor::from_output(&screen.stdout);
let input = TerminalInput::from_output(&screen.stdout);
}
```
## Upgrade crossterm to 0.4.0
***WARNING***
This new version contains some cool features but to get those features working I needed to add some user API breaking changes.
I really did not want to do this but it had to be done for some reasons.
#### 1. You need to pass a reference to a `Screen` to the modules: `cursor()`, `color()`, `terminal()`
_**old**_
```
use crossterm::terminal::terminal;
use crossterm::cursor::cursor;
use crossterm::style::color;
use crossterm::Context;
let context: Rc<Context> = Context::new();
let cursor = cursor(&context);
let terminal = terminal(&context);
let color = color(&context);
```
_**new**_
```
use crossterm::Screen;
let screen: Screen = Screen::default();
let cursor = cursor(&screen);
let terminal = terminal(&screen);
let color = color(&screen);
```
#### 2. The `::crossterm::Crossterm::paint()` function does not exits anymore like before:
Instead you could do it like the following:
```
use crossterm::Crossterm;
use crossterm::style::{Color, input, style};
// 1: use the `Crossterm` type
let crossterm = Crossterm::new();
let styled_object = crossterm.style("Red text on Black background").with(Color::Red).on(Color::Black);
styled_object.paint(&screen);
// 2: use the `Terminal` type
let styled_object = style("Red text on Black background").with(Color::Red).on(Color::Black);
styled_object.paint(&screen);
```
#### 3. Alternate Screen and Raw Screen
Also I have changed how the alternate and raw screen are working.
```
// could not be used any more
::crossterm::AlternateScreen::from();
// cannot put any Write into raw mode.
::std::io::Write::into_raw_mode()
```
This now should be done with the `Screen` type like:
```
use crossterm::Screen;
use crossterm::cursor::cursor;
// this will create a default screen.
let screen = Screen::default();
// this will create a new screen with raw modes enabled.
let screen = Screen::new(true);
// `false` specifies whether the alternate screen should be in raw modes.
if let Ok(alternate) = screen.enable_alternate_modes(false)
{
let cursor = cursor(&alternate.screen);
}
```
#### Other
- ::crossterm::Crossterm::write() is gone.
- ::crossterm::Crossterm::flush() is gone.
- Context type is removed
- StateManager is removed
- ScreenManager type is renamed to Stdout.
## Upgrade crossterm 0.2.1 to 0.3.0
***WARNING***
This new version contains some cool features but to get those features working I needed to add some user API braking changes.
I really did not want to do this but it had to be done for some reasons. Check `LINK (updates crossterm version)` for more info about why.
First thing that has changed is that you need to pass a reference to an `Rc<Context>` to the modules: `cursor(), color(), terminal()`
_**old**_
```
use crossterm::terminal::terminal;
use crossterm::cursor::cursor;
use crossterm::style::color;
/// Old situation
let cursor = cursor();
let terminal = terminal();
let color = color();
```
_**new**_
```
use crossterm::Context;
let context: Rc<Context> = Context::new();
let cursor = cursor(&context);
let terminal = terminal(&context);
let color = color(&context);
```
Also the `::crossterm::style::paint()` function does not exits anymore like before:
Instead you could do it like the following:
```
use crossterm::Crossterm;
use crossterm::style::Color;
use crossterm::terminal::terminal;
// 1: use the `Crossterm` type
let crossterm = Crossterm::new();
let mut color = crossterm.paint("Red on Blue").with(Color::Red).on(Color::Blue);
// 2: use the `Terminal` type
let context: Rc<Context> = Context::new();
let terminal = terminal(&context).paint("Red on Blue").with(Color::Red).on(Color::Blue);
```
And you do not need `mut` for a lot of function calls anymore.
## Upgrade crossterm 0.2 to 0.2.1
Namespaces:
I have changed the namespaces. I found the namsespaces to long so I have shortened them like the following:
```
Old: crossterm::crossterm_style
New: crossterm::style
Old: crossterm::crossterm_terminal
New: crossterm::terminal
Old: crossterm::crossterm_cursor
New: crossterm::cursor
```
Method names that changed [Issue 4](https://github.com/crossterm-rs/crossterm/issues/4):
```
Old: ::crossterm::crossterm_cursor::get();
New: ::crossterm::cursor::cursor();
Old: ::crossterm::crossterm_terminal::get();
New: ::crossterm::terminal::terminal();
Old: ::crossterm::crossterm_style::color::get();
New: ::crossterm::style::color::color();
```

View File

@ -0,0 +1,61 @@
//
// cargo run --example event-poll-read
//
use std::io::{stdout, Write};
use std::time::Duration;
use crossterm::{
cursor::position,
event::{poll, read, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
execute,
screen::RawScreen,
Result,
};
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() -> Result<()> {
loop {
// Wait up to 1s for another event
if poll(Duration::from_millis(1_000))? {
// It's guaranteed that read() wont 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() -> Result<()> {
println!("{}", HELP);
let _r = RawScreen::into_raw_mode()?;
let mut stdout = stdout();
execute!(stdout, EnableMouseCapture)?;
if let Err(e) = print_events() {
println!("Error: {:?}\r", e);
}
execute!(stdout, DisableMouseCapture)?;
Ok(())
}

53
examples/event-read.rs Normal file
View File

@ -0,0 +1,53 @@
//
// cargo run --example event-read
//
use std::io::{stdout, Write};
use crossterm::{
cursor::position,
event::{read, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
execute,
screen::RawScreen,
Result,
};
const HELP: &str = r#"Blocking read()
- Keyboard, mouse and terminal resize events enabled
- Hit "c" to print current cursor position
- Use Esc to quit
"#;
fn print_events() -> 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 event == Event::Key(KeyCode::Esc.into()) {
break;
}
}
Ok(())
}
fn main() -> Result<()> {
println!("{}", HELP);
let _r = RawScreen::into_raw_mode()?;
let mut stdout = stdout();
execute!(stdout, EnableMouseCapture)?;
if let Err(e) = print_events() {
println!("Error: {:?}\r", e);
}
execute!(stdout, DisableMouseCapture)?;
Ok(())
}

View File

@ -0,0 +1,67 @@
//
// cargo run --features event-stream --example event-stream-async-std
//
use std::io::{stdout, Write};
use std::time::Duration;
use futures::{future::FutureExt, select, StreamExt};
use futures_timer::Delay;
use crossterm::{
cursor::position,
event::{DisableMouseCapture, EnableMouseCapture, Event, EventStream, KeyCode},
execute,
screen::RawScreen,
Result,
};
const HELP: &str = r#"EventStream based on futures::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() -> Result<()> {
println!("{}", HELP);
let _r = RawScreen::into_raw_mode()?;
let mut stdout = stdout();
execute!(stdout, EnableMouseCapture)?;
async_std::task::block_on(print_events());
execute!(stdout, DisableMouseCapture)?;
Ok(())
}

View File

@ -0,0 +1,68 @@
//
// cargo run --features event-stream --example event-stream-tokio
//
use std::io::{stdout, Write};
use std::time::Duration;
use futures::{future::FutureExt, select, StreamExt};
use futures_timer::Delay;
use crossterm::{
cursor::position,
event::{DisableMouseCapture, EnableMouseCapture, Event, EventStream, KeyCode},
execute,
screen::RawScreen,
Result,
};
const HELP: &str = r#"EventStream based on futures::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() -> Result<()> {
println!("{}", HELP);
let _r = RawScreen::into_raw_mode()?;
let mut stdout = stdout();
execute!(stdout, EnableMouseCapture)?;
print_events().await;
execute!(stdout, DisableMouseCapture)?;
Ok(())
}

View File

@ -44,10 +44,9 @@
pub use sys::position;
use crate::impl_display;
use crate::utils::Command;
#[cfg(windows)]
use crate::utils::Result;
use crate::{impl_display, utils::Command};
mod ansi;
pub(crate) mod sys;

View File

@ -1,11 +1,14 @@
//! UNIX related logic to cursor manipulation.
use std::{
io::{self, Error, ErrorKind, Write},
time::Duration,
};
use std::io::{self, Write};
use crate::input::{InputEvent, TerminalInput};
use crate::utils::{
sys::unix::{disable_raw_mode, enable_raw_mode, is_raw_mode_enabled},
Result,
use crate::{
event::{filter::CursorPositionFilter, poll_internal, read_internal, InternalEvent},
utils::{
sys::unix::{disable_raw_mode, enable_raw_mode, is_raw_mode_enabled},
Result,
},
};
/// Returns the cursor position (column, row).
@ -29,15 +32,26 @@ fn read_position() -> Result<(u16, u16)> {
fn read_position_raw() -> 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()?;
let mut reader = TerminalInput::new().read_sync();
loop {
if let Some(InputEvent::CursorPosition(x, y)) = reader.next() {
return Ok((x, y));
match poll_internal(Some(Duration::from_millis(2000)), &CursorPositionFilter) {
Ok(true) => {
match read_internal(&CursorPositionFilter) {
Ok(InternalEvent::CursorPosition(x, y)) => {
return Ok((x, y));
}
_ => { /* unreachable */ }
};
}
Ok(false) => {
return Err(Error::new(
ErrorKind::Other,
"The cursor position could not be read within a normal duration",
))?;
}
Err(_) => {}
}
}
}

View File

@ -1,7 +1,6 @@
//! WinApi related logic to cursor manipulation.
use std::io;
use std::sync::Mutex;
use std::{io, sync::Mutex};
use crossterm_winapi::{is_true, Coord, Handle, HandleType, ScreenBuffer};
use winapi::{

413
src/event.rs Normal file
View File

@ -0,0 +1,413 @@
//! # Event
//!
//! The `event` module provides the functionality to read keyboard, mouse and terminal resize events.
//!
//! * The [`read`](fn.read.html) function returns an [`Event`](enum.Event.html) immediately
//! (if available) or blocks until an [`Event`](enum.Event.html) is available.
//!
//! * The [`poll`](fn.poll.html) function allows you to check if there is or isn't an [`Event`](enum.Event.html) available
//! within the given period of time. In other words - if subsequent call to the [`read`](fn.read.html)
//! function will block or not.
//!
//! It's **not allowed** to call these functions from different threads or combine them with the
//! [`EventStream`](struct.EventStream.html). You're allowed to either:
//!
//! * use the [`read`](fn.read.html) & [`poll`](fn.poll.html) functions on any, but same, thread
//! * or the [`EventStream`](struct.EventStream.html).
//!
//! ## Mouse Events
//!
//! Mouse events are not enabled by default. You have to enable them with the
//! [`EnableMouseCapture`](struct.EnableMouseCapture.html) command. See [Command API](../index.html#command-api)
//! for more information.
//!
//! ## Examples
//!
//! Blocking read:
//!
//! ```no_run
//! use crossterm::event::{read, Event};
//!
//! fn print_events() -> crossterm::Result<()> {
//! loop {
//! // `read()` blocks until an `Event` is available
//! match read()? {
//! Event::Key(event) => println!("{:?}", event),
//! Event::Mouse(event) => println!("{:?}", event),
//! Event::Resize(width, height) => println!("New size {}x{}", width, height),
//! }
//! }
//! Ok(())
//! }
//! ```
//!
//! Non-blocking read:
//!
//! ```no_run
//! use std::time::Duration;
//!
//! use crossterm::event::{poll, read, Event};
//!
//! fn print_events() -> crossterm::Result<()> {
//! loop {
//! // `poll()` waits for an `Event` for a given time period
//! if poll(Duration::from_millis(500))? {
//! // It's guaranteed that the `read()` won't block when the `poll()`
//! // function returns `true`
//! match read()? {
//! Event::Key(event) => println!("{:?}", event),
//! Event::Mouse(event) => println!("{:?}", event),
//! Event::Resize(width, height) => println!("New size {}x{}", width, height),
//! }
//! } else {
//! // Timeout expired and no `Event` is available
//! }
//! }
//! Ok(())
//! }
//! ```
//!
//! Check the [examples](https://github.com/crossterm-rs/crossterm/tree/master/examples) folder for more of
//! them (`event-*`).
use std::time::Duration;
use parking_lot::RwLock;
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
use bitflags::bitflags;
use filter::{EventFilter, Filter};
use lazy_static::lazy_static;
#[cfg(feature = "event-stream")]
pub use stream::EventStream;
use timeout::PollTimeout;
use crate::{Command, Result};
mod ansi;
pub(crate) mod filter;
mod read;
mod source;
#[cfg(feature = "event-stream")]
mod stream;
mod sys;
mod timeout;
lazy_static! {
/// Static instance of `InternalEventReader`.
/// This needs to be static because there can be one event reader.
static ref INTERNAL_EVENT_READER: RwLock<read::InternalEventReader> = { RwLock::new(read::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
/// wont block.
///
/// # Arguments
///
/// * `timeout` - maximum waiting time for event availability
///
/// # Examples
///
/// Return immediately:
///
/// ```no_run
/// use std::time::Duration;
///
/// use crossterm::{event::poll, Result};
///
/// fn is_event_available() -> 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;
///
/// use crossterm::{event::poll, Result};
///
/// fn is_event_available() -> 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) -> 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, Result};
///
/// fn print_events() -> Result<bool> {
/// loop {
/// // Blocks until an `Event` is available
/// println!("{:?}", read()?);
/// }
/// }
/// ```
///
/// Non-blocking read:
///
/// ```no_run
/// use std::time::Duration;
///
/// use crossterm::{event::{read, poll}, Result};
///
/// fn print_events() -> Result<bool> {
/// loop {
/// if poll(Duration::from_millis(100))? {
/// // It's guaranteed that `read` wont block, because `poll` returned
/// // `Ok(true)`.
/// println!("{:?}", read()?);
/// } else {
/// // Timeout expired, no `Event` is available
/// }
/// }
/// }
/// ```
pub fn read() -> 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 withing the given duration.
pub(crate) fn poll_internal<F>(timeout: Option<Duration>, filter: &F) -> 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) = INTERNAL_EVENT_READER.try_write_for(timeout) {
(reader, poll_timeout.leftover())
} else {
return Ok(false);
}
} else {
(INTERNAL_EVENT_READER.write(), None)
};
reader.poll(timeout, filter)
}
/// Reads a single `InternalEvent`.
pub(crate) fn read_internal<F>(filter: &F) -> Result<InternalEvent>
where
F: Filter,
{
let mut reader = INTERNAL_EVENT_READER.write();
reader.read(filter)
}
/// A command that enables mouse event capturing.
///
/// Mouse events can be captured with [read](./fn.read.html)/[poll](./fn.poll.html).
pub struct EnableMouseCapture;
impl Command for EnableMouseCapture {
type AnsiType = &'static str;
fn ansi_code(&self) -> Self::AnsiType {
ansi::ENABLE_MOUSE_MODE_CSI_SEQUENCE
}
#[cfg(windows)]
fn execute_winapi(&self) -> Result<()> {
sys::windows::enable_mouse_capture()
}
}
/// A command that disables mouse event capturing.
///
/// Mouse events can be captured with [read](./fn.read.html)/[poll](./fn.poll.html).
pub struct DisableMouseCapture;
impl Command for DisableMouseCapture {
type AnsiType = &'static str;
fn ansi_code(&self) -> Self::AnsiType {
ansi::DISABLE_MOUSE_MODE_CSI_SEQUENCE
}
#[cfg(windows)]
fn execute_winapi(&self) -> Result<()> {
sys::windows::disable_mouse_capture()
}
}
/// Represents an event.
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, PartialOrd, PartialEq, Eq, Clone, Copy, Hash)]
pub enum Event {
/// A single key event with additional pressed modifiers.
Key(KeyEvent),
/// A singe mouse event with additional pressed modifiers.
Mouse(MouseEvent),
/// An resize event with new dimensions after resize (columns, rows).
Resize(u16, u16),
}
/// Represents a mouse event.
///
/// # Platform-specific Notes
///
/// ## Mouse Buttons
///
/// Some platforms/terminals do not report mouse button for the
/// `MouseEvent::Up` and `MouseEvent::Drag` events. `MouseButton::Left`
/// is returned if we don't know which button was used.
///
/// ## Key Modifiers
///
/// Some platforms/terminals does not report all key modifiers
/// combinations for all mouse event types. For example - macOS reports
/// `Ctrl` + left mouse button click as a right mouse button click.
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, PartialOrd, PartialEq, Eq, Clone, Copy, Hash)]
pub enum MouseEvent {
/// Pressed mouse button.
///
/// Contains mouse button, pressed pointer location (column, row), and additional key modifiers.
Down(MouseButton, u16, u16, KeyModifiers),
/// Released mouse button.
///
/// Contains mouse button, released pointer location (column, row), and additional key modifiers.
Up(MouseButton, u16, u16, KeyModifiers),
/// Moved mouse pointer while pressing a mouse button.
///
/// Contains the pressed mouse button, released pointer location (column, row), and additional key modifiers.
Drag(MouseButton, u16, u16, KeyModifiers),
/// Scrolled mouse wheel downwards (towards the user).
///
/// Contains the scroll location (column, row), and additional key modifiers.
ScrollDown(u16, u16, KeyModifiers),
/// Scrolled mouse wheel upwards (away from the user).
///
/// Contains the scroll location (column, row), and additional key modifiers.
ScrollUp(u16, u16, KeyModifiers),
}
/// Represents a mouse button.
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, PartialOrd, PartialEq, Eq, Clone, Copy, Hash)]
pub enum MouseButton {
/// Left mouse button.
Left,
/// Right mouse button.
Right,
/// Middle mouse button.
Middle,
}
bitflags! {
/// Represents key modifiers (shift, control, alt).
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct KeyModifiers: u8 {
const SHIFT = 0b0000_0001;
const CONTROL = 0b0000_0010;
const ALT = 0b0000_0100;
}
}
/// Represents a key event.
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, PartialOrd, PartialEq, Eq, Clone, Copy, Hash)]
pub struct KeyEvent {
/// The key itself.
pub code: KeyCode,
/// Additional key modifiers.
pub modifiers: KeyModifiers,
}
impl KeyEvent {
pub fn new(code: KeyCode, modifiers: KeyModifiers) -> KeyEvent {
KeyEvent { code, modifiers }
}
}
impl From<KeyCode> for KeyEvent {
fn from(code: KeyCode) -> Self {
KeyEvent {
code,
modifiers: KeyModifiers::empty(),
}
}
}
/// Represents a key.
#[derive(Debug, PartialOrd, PartialEq, Eq, Clone, Copy, Hash)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub enum KeyCode {
/// Backspace key.
Backspace,
/// Enter key.
Enter,
/// Left arrow key.
Left,
/// Right arrow key.
Right,
/// Up arrow key.
Up,
/// Down arrow key.
Down,
/// Home key.
Home,
/// End key.
End,
/// Page up key.
PageUp,
/// Page dow key.
PageDown,
/// Tab key.
Tab,
/// Shift + Tab key.
BackTab,
/// Delete key.
Delete,
/// Insert key.
Insert,
/// F key.
///
/// `KeyEvent::F(1)` represents F1 key, etc.
F(u8),
/// A character.
///
/// `KeyEvent::Char('c')` represents `c` character, etc.
Char(char),
/// Null.
Null,
/// Escape key.
Esc,
}
/// 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)]
pub(crate) enum InternalEvent {
/// An event.
Event(Event),
/// A cursor position (`col`, `row`).
#[cfg(unix)]
CursorPosition(u16, u16),
}

17
src/event/ansi.rs Normal file
View File

@ -0,0 +1,17 @@
//! This module provides input related ANSI escape codes.
use crate::csi;
pub(crate) const ENABLE_MOUSE_MODE_CSI_SEQUENCE: &str = concat!(
csi!("?1000h"),
csi!("?1002h"),
csi!("?1015h"),
csi!("?1006h")
);
pub(crate) const DISABLE_MOUSE_MODE_CSI_SEQUENCE: &str = concat!(
csi!("?1006l"),
csi!("?1015l"),
csi!("?1002l"),
csi!("?1000l")
);

73
src/event/filter.rs Normal file
View File

@ -0,0 +1,73 @@
use crate::event::InternalEvent;
/// Interface for filtering an `InternalEvent`.
pub(crate) trait Filter: Send + Sync + 'static {
/// Returns whether the given event fulfills the filter.
fn eval(&self, event: &InternalEvent) -> bool;
}
#[cfg(unix)]
pub(crate) struct CursorPositionFilter;
#[cfg(unix)]
impl Filter for CursorPositionFilter {
fn eval(&self, event: &InternalEvent) -> bool {
if let InternalEvent::CursorPosition(_, _) = *event {
true
} else {
false
}
}
}
pub(crate) struct EventFilter;
impl Filter for EventFilter {
#[cfg(unix)]
fn eval(&self, event: &InternalEvent) -> bool {
if let InternalEvent::Event(_) = *event {
true
} else {
false
}
}
#[cfg(windows)]
fn eval(&self, _: &InternalEvent) -> bool {
true
}
}
pub(crate) struct InternalEventFilter;
impl Filter for InternalEventFilter {
fn eval(&self, _: &InternalEvent) -> bool {
true
}
}
#[cfg(test)]
#[cfg(unix)]
mod tests {
use super::{
super::Event, CursorPositionFilter, EventFilter, Filter, InternalEvent, InternalEventFilter,
};
#[test]
fn test_cursor_position_filter_filters_cursor_position() {
assert!(!CursorPositionFilter.eval(&InternalEvent::Event(Event::Resize(10, 10))));
assert!(CursorPositionFilter.eval(&InternalEvent::CursorPosition(0, 0)));
}
#[test]
fn test_event_filter_filters_events() {
assert!(EventFilter.eval(&InternalEvent::Event(Event::Resize(10, 10))));
assert!(!EventFilter.eval(&InternalEvent::CursorPosition(0, 0)));
}
#[test]
fn test_event_filter_filters_internal_events() {
assert!(InternalEventFilter.eval(&InternalEvent::Event(Event::Resize(10, 10))));
assert!(InternalEventFilter.eval(&InternalEvent::CursorPosition(0, 0)));
}
}

435
src/event/read.rs Normal file
View File

@ -0,0 +1,435 @@
use std::{collections::vec_deque::VecDeque, time::Duration};
use super::filter::Filter;
#[cfg(unix)]
use super::source::unix::UnixInternalEventSource;
#[cfg(windows)]
use super::source::windows::WindowsEventSource;
use super::{source::EventSource, timeout::PollTimeout, InternalEvent, Result};
/// Can be used to read `InternalEvent`s.
pub(crate) struct InternalEventReader {
events: VecDeque<InternalEvent>,
source: Option<Box<dyn EventSource>>,
}
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::new(),
}
}
}
impl InternalEventReader {
#[cfg(feature = "event-stream")]
pub(crate) fn wake(&self) {
if let Some(source) = self.source.as_ref() {
source.wake();
}
}
pub(crate) fn poll<F>(&mut self, timeout: Option<Duration>, filter: &F) -> 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",
)
.into())
}
};
let poll_timeout = PollTimeout::new(timeout);
let mut skipped_events = VecDeque::new();
loop {
let maybe_event = match event_source.try_read(timeout)? {
None => None,
Some(event) => {
if filter.eval(&event) {
Some(event)
} else {
skipped_events.push_back(event);
None
}
}
};
if poll_timeout.elapsed() || maybe_event.is_some() {
while let Some(event) = skipped_events.pop_front() {
self.events.push_back(event);
}
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) -> 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::collections::VecDeque;
use std::time::Duration;
use crate::ErrorKind;
#[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,
};
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,
};
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,
};
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,
};
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,
};
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,
};
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)),
};
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)),
};
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)),
};
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)),
};
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)),
};
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 source = FakeSource::with_error(ErrorKind::ResizingTerminalFailure("Foo".to_string()));
let mut reader = InternalEventReader {
events: VecDeque::new(),
source: Some(Box::new(source)),
};
assert_eq!(
reader
.poll(Some(Duration::from_secs(0)), &InternalEventFilter)
.err()
.map(|e| format!("{:?}", &e)),
Some(format!(
"{:?}",
ErrorKind::ResizingTerminalFailure("Foo".to_string())
))
);
}
#[test]
fn test_read_propagates_error() {
let source = FakeSource::with_error(ErrorKind::ResizingTerminalFailure("Foo".to_string()));
let mut reader = InternalEventReader {
events: VecDeque::new(),
source: Some(Box::new(source)),
};
assert_eq!(
reader
.read(&InternalEventFilter)
.err()
.map(|e| format!("{:?}", &e)),
Some(format!(
"{:?}",
ErrorKind::ResizingTerminalFailure("Foo".to_string())
))
);
}
#[test]
fn test_poll_continues_after_error() {
const EVENT: InternalEvent = InternalEvent::Event(Event::Resize(10, 10));
let source = FakeSource::new(
&[EVENT, EVENT],
ErrorKind::ResizingTerminalFailure("Foo".to_string()),
);
let mut reader = InternalEventReader {
events: VecDeque::new(),
source: Some(Box::new(source)),
};
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],
ErrorKind::ResizingTerminalFailure("Foo".to_string()),
);
let mut reader = InternalEventReader {
events: VecDeque::new(),
source: Some(Box::new(source)),
};
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<ErrorKind>,
}
impl FakeSource {
fn new(events: &[InternalEvent], error: ErrorKind) -> FakeSource {
FakeSource {
events: events.to_vec().into(),
error: Some(error),
}
}
fn with_events(events: &[InternalEvent]) -> FakeSource {
FakeSource {
events: events.to_vec().into(),
error: None,
}
}
fn with_error(error: ErrorKind) -> FakeSource {
FakeSource {
events: VecDeque::new(),
error: Some(error),
}
}
}
impl EventSource for FakeSource {
fn try_read(
&mut self,
_timeout: Option<Duration>,
) -> Result<Option<InternalEvent>, ErrorKind> {
// 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)
}
fn wake(&self) {}
}
}

25
src/event/source.rs Normal file
View File

@ -0,0 +1,25 @@
use std::time::Duration;
use super::InternalEvent;
#[cfg(unix)]
pub mod unix;
#[cfg(windows)]
pub 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.
///
/// This function takes in an optional duration.
/// * `None`: blocks indefinitely until an event is able to be read.
/// * `Some(duration)`: blocks for the given duration.
///
/// Returns:
/// `Ok(Some(event))`: in case an event is ready.
/// `Ok(None)`: in case an event is not ready.
fn try_read(&mut self, timeout: Option<Duration>) -> crate::Result<Option<InternalEvent>>;
/// Forces the `try_read` method to return `Ok(None)` immediately.
fn wake(&self);
}

186
src/event/source/unix.rs Normal file
View File

@ -0,0 +1,186 @@
use std::io;
use std::time::Duration;
use mio::{unix::EventedFd, Events, Poll, PollOpt, Ready, Token};
use signal_hook::iterator::Signals;
use crate::Result;
use super::super::{
source::EventSource,
sys::unix::{parse_event, tty_fd, FileDesc},
timeout::PollTimeout,
Event, InternalEvent,
};
// Tokens to identify file descriptor
const TTY_TOKEN: Token = Token(0);
const SIGNAL_TOKEN: Token = Token(1);
const WAKE_TOKEN: Token = Token(2);
/// Creates a new pipe and returns `(read, write)` file descriptors.
fn pipe() -> Result<(FileDesc, FileDesc)> {
let (read_fd, write_fd) = unsafe {
let mut pipe_fds: [libc::c_int; 2] = [0; 2];
if libc::pipe(pipe_fds.as_mut_ptr()) == -1 {
return Err(io::Error::last_os_error().into());
}
(pipe_fds[0], pipe_fds[1])
};
let read_fd = FileDesc::new(read_fd, true);
let write_fd = FileDesc::new(write_fd, true);
Ok((read_fd, write_fd))
}
pub(crate) struct UnixInternalEventSource {
poll: Poll,
events: Events,
tty_buffer: Vec<u8>,
tty_fd: FileDesc,
signals: Signals,
wake_read_fd: FileDesc,
wake_write_fd: FileDesc,
}
impl UnixInternalEventSource {
pub fn new() -> Result<Self> {
Ok(UnixInternalEventSource::from_file_descriptor(tty_fd()?)?)
}
pub(crate) fn from_file_descriptor(input_fd: FileDesc) -> Result<Self> {
let poll = Poll::new()?;
// PollOpt::level vs PollOpt::edge mio documentation:
//
// > With edge-triggered events, operations must be performed on the Evented type until
// > WouldBlock is returned.
//
// TL;DR - DO NOT use PollOpt::edge.
//
// Because of the `try_read` nature (loop with returns) we can't use `PollOpt::edge`. All
// `Evented` handles MUST be registered with the `PollOpt::level`.
//
// If you have to use `PollOpt::edge` and there's no way how to do it with the `PollOpt::level`,
// be aware that the whole `TtyInternalEventSource` have to be rewritten
// (read everything from each `Evented`, process without returns, store all InternalEvent events
// into a buffer and then return first InternalEvent, etc.). Even these changes wont be
// enough, because `Poll::poll` wont fire again until additional `Evented` event happens and
// we can still have a buffer filled with InternalEvent events.
let tty_raw_fd = input_fd.raw_fd();
let tty_ev = EventedFd(&tty_raw_fd);
poll.register(&tty_ev, TTY_TOKEN, Ready::readable(), PollOpt::level())?;
let signals = Signals::new(&[signal_hook::SIGWINCH])?;
poll.register(&signals, SIGNAL_TOKEN, Ready::readable(), PollOpt::level())?;
let (wake_read_fd, wake_write_fd) = pipe()?;
let wake_read_raw_fd = wake_read_fd.raw_fd();
let wake_read_ev = EventedFd(&wake_read_raw_fd);
poll.register(
&wake_read_ev,
WAKE_TOKEN,
Ready::readable(),
PollOpt::level(),
)?;
Ok(UnixInternalEventSource {
poll,
events: Events::with_capacity(3),
tty_buffer: Vec::new(),
tty_fd: input_fd,
signals,
wake_read_fd,
wake_write_fd,
})
}
}
impl EventSource for UnixInternalEventSource {
fn try_read(&mut self, timeout: Option<Duration>) -> Result<Option<InternalEvent>> {
let timeout = PollTimeout::new(timeout);
loop {
let event_count = self.poll.poll(&mut self.events, timeout.leftover())?;
match event_count {
event_count if event_count > 0 => {
let events_count = self
.events
.iter()
.map(|x| x.token())
.collect::<Vec<Token>>();
for event in events_count {
match event {
TTY_TOKEN => {
self.tty_buffer.push(self.tty_fd.read_byte()?);
let input_available = self
.poll
.poll(&mut self.events, Some(Duration::from_secs(0)))
.map(|x| x > 0)?;
match parse_event(&self.tty_buffer, input_available) {
Ok(None) => {
// Not enough bytes to construct an InternalEvent
}
Ok(Some(ie)) => {
self.tty_buffer.clear();
return Ok(Some(ie));
}
Err(_) => {
// Can't parse an event, clear buffer and start over
self.tty_buffer.clear();
}
};
}
SIGNAL_TOKEN => {
for signal in &self.signals {
match signal as libc::c_int {
signal_hook::SIGWINCH => {
// TODO Should we remove tput?
//
// This can take a really long time, because terminal::size can
// launch new process (tput) and then it parses its output. It's
// not a really long time from the absolute time point of view, but
// it's a really long time from the mio, async-std/tokio executor, ...
// point of view.
let new_size = crate::terminal::size()?;
return Ok(Some(InternalEvent::Event(Event::Resize(
new_size.0, new_size.1,
))));
}
_ => unreachable!(),
};
}
}
WAKE_TOKEN => {
// Something happened on the self pipe. Try to read single byte
// (see wake() fn) and ignore result. If we can't read the byte,
// mio Poll::poll will fire another event with WAKE_TOKEN.
let _ = self.wake_read_fd.read_byte();
return Ok(None);
}
_ => {}
}
}
}
_ => return Ok(None),
};
if timeout.elapsed() {
return Ok(None);
}
}
}
fn wake(&self) {
// DO NOT write more than 1 byte. See try_read & WAKE_TOKEN
// handling - it reads just 1 byte. If you write more than
// 1 byte, lets say N, then the try_read will be woken up
// N times.
let _ = self.wake_write_fd.write(&[0x57]);
}
}

View File

@ -0,0 +1,71 @@
use std::time::Duration;
use crossterm_winapi::{Console, Handle, InputEventType, KeyEventRecord, MouseEvent};
use crate::event::sys::windows::WinApiPoll;
use crate::event::Event;
use super::super::{
source::EventSource,
sys::windows::{handle_key_event, handle_mouse_event},
timeout::PollTimeout,
InternalEvent, Result,
};
pub(crate) struct WindowsEventSource {
console: Console,
poll: WinApiPoll,
}
impl WindowsEventSource {
pub(crate) fn new() -> Result<WindowsEventSource> {
let console = Console::from(Handle::current_in_handle()?);
Ok(WindowsEventSource {
console,
poll: WinApiPoll::new()?,
})
}
}
impl EventSource for WindowsEventSource {
fn try_read(&mut self, timeout: Option<Duration>) -> Result<Option<InternalEvent>> {
let poll_timeout = PollTimeout::new(timeout);
loop {
if let Some(event_ready) = self.poll.poll(timeout)? {
if event_ready && self.console.number_of_console_input_events()? != 0 {
let input = self.console.read_single_input_event()?;
let event = match input.event_type {
InputEventType::KeyEvent => handle_key_event(unsafe {
KeyEventRecord::from(*input.event.KeyEvent())
})?,
InputEventType::MouseEvent => handle_mouse_event(unsafe {
MouseEvent::from(*input.event.MouseEvent())
})?,
InputEventType::WindowBufferSizeEvent => {
let new_size = crate::terminal::size()?;
Some(Event::Resize(new_size.0, new_size.1))
}
InputEventType::FocusEvent | InputEventType::MenuEvent => None,
};
return Ok(match event {
None => None,
Some(event) => Some(InternalEvent::Event(event)),
});
}
} else {
return Ok(None);
}
if poll_timeout.elapsed() {
return Ok(None);
}
}
}
fn wake(&self) {
let _ = self.poll.cancel();
}
}

98
src/event/stream.rs Normal file
View File

@ -0,0 +1,98 @@
use futures::{
task::{Context, Poll},
Stream,
};
use std::{
pin::Pin,
sync::{
atomic::{AtomicBool, Ordering},
Arc,
},
thread,
time::Duration,
};
use crate::Result;
use super::{
filter::EventFilter, poll_internal, read_internal, Event, InternalEvent, INTERNAL_EVENT_READER,
};
/// A stream of `Result<Event>`.
///
/// **This type is not available by default. You have to use the `event-stream` feature flag
/// to make it available.**
///
/// It implements the [`futures::stream::Stream`](https://docs.rs/futures/0.3.1/futures/stream/trait.Stream.html)
/// trait and allows you to receive `Event`s with [`async-std`](https://crates.io/crates/async-std)
/// or [`tokio`](https://crates.io/crates/tokio) crates.
///
/// Check the [examples](https://github.com/crossterm-rs/crossterm/tree/master/examples) folder to see how to use
/// it (`event-stream-*`).
#[derive(Default)]
pub struct EventStream {
wake_thread_spawned: Arc<AtomicBool>,
wake_thread_should_shutdown: Arc<AtomicBool>,
}
impl EventStream {
/// Constructs a new instance of `EventStream`.
pub fn new() -> EventStream {
EventStream {
wake_thread_spawned: Arc::new(AtomicBool::new(false)),
wake_thread_should_shutdown: Arc::new(AtomicBool::new(false)),
}
}
}
impl Stream for EventStream {
type Item = Result<Event>;
fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
let result = match poll_internal(Some(Duration::from_secs(0)), &EventFilter) {
Ok(true) => match read_internal(&EventFilter) {
Ok(InternalEvent::Event(event)) => Poll::Ready(Some(Ok(event))),
Err(e) => Poll::Ready(Some(Err(e))),
#[cfg(unix)]
_ => unreachable!(),
},
Ok(false) => {
if !self
.wake_thread_spawned
.compare_and_swap(false, true, Ordering::SeqCst)
{
let waker = cx.waker().clone();
let wake_thread_spawned = self.wake_thread_spawned.clone();
let wake_thread_should_shutdown = self.wake_thread_should_shutdown.clone();
wake_thread_should_shutdown.store(false, Ordering::SeqCst);
thread::spawn(move || {
loop {
if let Ok(true) = poll_internal(None, &EventFilter) {
break;
}
if wake_thread_should_shutdown.load(Ordering::SeqCst) {
break;
}
}
wake_thread_spawned.store(false, Ordering::SeqCst);
waker.wake();
});
}
Poll::Pending
}
Err(e) => Poll::Ready(Some(Err(e))),
};
result
}
}
impl Drop for EventStream {
fn drop(&mut self) {
self.wake_thread_should_shutdown
.store(true, Ordering::SeqCst);
INTERNAL_EVENT_READER.read().wake();
}
}

4
src/event/sys.rs Normal file
View File

@ -0,0 +1,4 @@
#[cfg(unix)]
pub mod unix;
#[cfg(windows)]
pub mod windows;

816
src/event/sys/unix.rs Normal file
View File

@ -0,0 +1,816 @@
use std::{
fs, io,
os::unix::io::{IntoRawFd, RawFd},
};
use libc::{c_int, c_void, size_t, ssize_t};
use crate::{
event::{Event, KeyCode, KeyEvent, KeyModifiers, MouseButton, MouseEvent},
utils::sys::unix::wrap_with_result,
ErrorKind, Result,
};
use super::super::InternalEvent;
// libstd::sys::unix::fd.rs
fn max_len() -> usize {
// The maximum read limit on most posix-like systems is `SSIZE_MAX`,
// with the man page quoting that if the count of bytes to read is
// greater than `SSIZE_MAX` the result is "unspecified".
//
// On macOS, however, apparently the 64-bit libc is either buggy or
// intentionally showing odd behavior by rejecting any read with a size
// larger than or equal to INT_MAX. To handle both of these the read
// size is capped on both platforms.
if cfg!(target_os = "macos") {
<c_int>::max_value() as usize - 1
} else {
<ssize_t>::max_value() as usize
}
}
/// 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.
pub struct FileDesc {
fd: RawFd,
close_on_drop: bool,
}
impl FileDesc {
/// Constructs a new `FileDesc` with the given `RawFd`.
///
/// # Arguments
///
/// * `fd` - raw file descriptor
/// * `close_on_drop` - specify if the raw file descriptor should be closed once the `FileDesc` is dropped
pub fn new(fd: RawFd, close_on_drop: bool) -> FileDesc {
FileDesc { fd, close_on_drop }
}
/// Reads a single byte from the file descriptor.
pub fn read_byte(&self) -> Result<u8> {
let mut buf: [u8; 1] = [0];
wrap_with_result(unsafe {
libc::read(self.fd, buf.as_mut_ptr() as *mut libc::c_void, 1) as c_int
})?;
Ok(buf[0])
}
pub fn write(&self, buf: &[u8]) -> io::Result<usize> {
// libstd::sys::unix::fd.rs
let ret = unsafe {
libc::write(
self.fd,
buf.as_ptr() as *const c_void,
std::cmp::min(buf.len(), max_len()) as size_t,
) as c_int
};
if ret == -1 {
return Err(io::Error::last_os_error());
}
Ok(ret as usize)
}
/// Returns the underlying file descriptor.
pub fn raw_fd(&self) -> RawFd {
self.fd
}
}
impl Drop for FileDesc {
fn drop(&mut self) {
if self.close_on_drop {
// Note that errors are ignored when closing a file descriptor. The
// reason for this is that if an error occurs we don't actually know if
// the file descriptor was closed or not, and if we retried (for
// something like EINTR), we might close another valid file descriptor
// opened after we closed ours.
let _ = unsafe { libc::close(self.fd) };
}
}
}
/// Creates a file descriptor pointing to the standard input or `/dev/tty`.
pub fn tty_fd() -> Result<FileDesc> {
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))
}
//
// Event parsing
//
// This code (& previous one) are kind of ugly. We have to think about this,
// because it's really not maintainable, no tests, etc.
//
// Every fn returns Result<Option<InputEvent>>
//
// Ok(None) -> wait for more bytes
// Err(_) -> failed to parse event, clear the buffer
// Ok(Some(event)) -> we have event, clear the buffer
//
fn could_not_parse_event_error() -> ErrorKind {
ErrorKind::IoError(io::Error::new(
io::ErrorKind::Other,
"Could not parse an event.",
))
}
pub(crate) fn parse_event(buffer: &[u8], input_available: bool) -> Result<Option<InternalEvent>> {
if buffer.is_empty() {
return Ok(None);
}
match buffer[0] {
b'\x1B' => {
if buffer.len() == 1 {
if input_available {
// Possible Esc sequence
Ok(None)
} else {
Ok(Some(InternalEvent::Event(Event::Key(KeyCode::Esc.into()))))
}
} else {
match buffer[1] {
b'O' => {
if buffer.len() == 2 {
Ok(None)
} else {
match buffer[2] {
// F1-F4
val @ b'P'..=b'S' => Ok(Some(InternalEvent::Event(Event::Key(
KeyCode::F(1 + val - b'P').into(),
)))),
_ => Err(could_not_parse_event_error()),
}
}
}
b'[' => parse_csi(buffer),
b'\x1B' => Ok(Some(InternalEvent::Event(Event::Key(KeyCode::Esc.into())))),
_ => parse_utf8_char(&buffer[1..]).map(|maybe_char| {
maybe_char
.map(KeyCode::Char)
.map(|code| KeyEvent::new(code, KeyModifiers::ALT))
.map(Event::Key)
.map(InternalEvent::Event)
}),
}
}
}
b'\r' | b'\n' => 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 as u8 - 0x1 + b'a') as char),
KeyModifiers::CONTROL,
))))),
c @ b'\x1C'..=b'\x1F' => Ok(Some(InternalEvent::Event(Event::Key(KeyEvent::new(
KeyCode::Char((c as u8 - 0x1C + b'4') as char),
KeyModifiers::CONTROL,
))))),
b'\0' => Ok(Some(InternalEvent::Event(Event::Key(KeyCode::Null.into())))),
_ => parse_utf8_char(buffer).map(|maybe_char| {
maybe_char
.map(KeyCode::Char)
.map(Into::into)
.map(Event::Key)
.map(InternalEvent::Event)
}),
}
}
pub(crate) fn parse_csi(buffer: &[u8]) -> Result<Option<InternalEvent>> {
assert!(buffer.starts_with(&[b'\x1B', b'['])); // ESC [
if buffer.len() == 2 {
return Ok(None);
}
let input_event = match buffer[2] {
b'[' => {
if buffer.len() == 3 {
None
} else {
match buffer[3] {
// NOTE (@imdaveho): cannot find when this occurs;
// having another '[' after ESC[ not a likely scenario
val @ b'A'..=b'E' => Some(Event::Key(KeyCode::F(1 + val - b'A').into())),
_ => return Err(could_not_parse_event_error()),
}
}
}
b'D' => Some(Event::Key(KeyCode::Left.into())),
b'C' => Some(Event::Key(KeyCode::Right.into())),
b'A' => Some(Event::Key(KeyCode::Up.into())),
b'B' => Some(Event::Key(KeyCode::Down.into())),
b'H' => Some(Event::Key(KeyCode::Home.into())),
b'F' => Some(Event::Key(KeyCode::End.into())),
b'Z' => Some(Event::Key(KeyCode::BackTab.into())),
b'M' => return parse_csi_x10_mouse(buffer),
b'<' => return parse_csi_xterm_mouse(buffer),
b'0'..=b'9' => {
// Numbered escape code.
if buffer.len() == 3 {
None
} else {
// The final byte of a CSI sequence can be in the range 64-126, so
// let's keep reading anything else.
let last_byte = *buffer.last().unwrap();
if last_byte < 64 || last_byte > 126 {
None
} else {
match buffer[buffer.len() - 1] {
b'M' => return parse_csi_rxvt_mouse(buffer),
b'~' => return parse_csi_special_key_code(buffer),
b'R' => return parse_csi_cursor_position(buffer),
_ => return parse_csi_modifier_key_code(buffer),
}
}
}
}
_ => return Err(could_not_parse_event_error()),
};
Ok(input_event.map(InternalEvent::Event))
}
pub(crate) fn next_parsed<T>(iter: &mut dyn Iterator<Item = &str>) -> Result<T>
where
T: std::str::FromStr,
{
iter.next()
.ok_or_else(could_not_parse_event_error)?
.parse::<T>()
.map_err(|_| could_not_parse_event_error())
}
pub(crate) fn parse_csi_cursor_position(buffer: &[u8]) -> Result<Option<InternalEvent>> {
// ESC [ Cy ; Cx R
// Cy - cursor row number (starting from 1)
// Cx - cursor column number (starting from 1)
assert!(buffer.starts_with(&[b'\x1B', b'['])); // ESC [
assert!(buffer.ends_with(&[b'R']));
let s = std::str::from_utf8(&buffer[2..buffer.len() - 1])
.map_err(|_| could_not_parse_event_error())?;
let mut split = s.split(';');
let y = next_parsed::<u16>(&mut split)? - 1;
let x = next_parsed::<u16>(&mut split)? - 1;
Ok(Some(InternalEvent::CursorPosition(x, y)))
}
pub(crate) fn parse_csi_modifier_key_code(buffer: &[u8]) -> Result<Option<InternalEvent>> {
assert!(buffer.starts_with(&[b'\x1B', b'['])); // ESC [
let modifier = buffer[buffer.len() - 2];
let key = buffer[buffer.len() - 1];
let input_event = match (modifier, key) {
(53, 65) => Event::Key(KeyEvent::new(KeyCode::Up, KeyModifiers::CONTROL)),
(53, 66) => Event::Key(KeyEvent::new(KeyCode::Down, KeyModifiers::CONTROL)),
(53, 67) => Event::Key(KeyEvent::new(KeyCode::Right, KeyModifiers::CONTROL)),
(53, 68) => Event::Key(KeyEvent::new(KeyCode::Left, KeyModifiers::CONTROL)),
(50, 65) => Event::Key(KeyEvent::new(KeyCode::Up, KeyModifiers::SHIFT)),
(50, 66) => Event::Key(KeyEvent::new(KeyCode::Down, KeyModifiers::SHIFT)),
(50, 67) => Event::Key(KeyEvent::new(KeyCode::Right, KeyModifiers::SHIFT)),
(50, 68) => Event::Key(KeyEvent::new(KeyCode::Left, KeyModifiers::SHIFT)),
_ => return Err(could_not_parse_event_error()),
};
Ok(Some(InternalEvent::Event(input_event)))
}
pub(crate) fn parse_csi_special_key_code(buffer: &[u8]) -> Result<Option<InternalEvent>> {
assert!(buffer.starts_with(&[b'\x1B', b'['])); // ESC [
assert!(buffer.ends_with(&[b'~']));
let s = std::str::from_utf8(&buffer[2..buffer.len() - 1])
.map_err(|_| could_not_parse_event_error())?;
let mut split = s.split(';');
// This CSI sequence can be a list of semicolon-separated numbers.
let first = next_parsed::<u8>(&mut split)?;
if next_parsed::<u8>(&mut split).is_ok() {
// TODO: handle multiple values for key modifiers (ex: values [3, 2] means Shift+Delete)
return Err(could_not_parse_event_error());
}
let input_event = match first {
1 | 7 => Event::Key(KeyCode::Home.into()),
2 => Event::Key(KeyCode::Insert.into()),
3 => Event::Key(KeyCode::Delete.into()),
4 | 8 => Event::Key(KeyCode::End.into()),
5 => Event::Key(KeyCode::PageUp.into()),
6 => Event::Key(KeyCode::PageDown.into()),
v @ 11..=15 => Event::Key(KeyCode::F(v - 10).into()),
v @ 17..=21 => Event::Key(KeyCode::F(v - 11).into()),
v @ 23..=24 => Event::Key(KeyCode::F(v - 12).into()),
_ => return Err(could_not_parse_event_error()),
};
Ok(Some(InternalEvent::Event(input_event)))
}
pub(crate) fn parse_csi_rxvt_mouse(buffer: &[u8]) -> Result<Option<InternalEvent>> {
// rxvt mouse encoding:
// ESC [ Cb ; Cx ; Cy ; M
assert!(buffer.starts_with(&[b'\x1B', b'['])); // ESC [
assert!(buffer.ends_with(&[b'M']));
let s = std::str::from_utf8(&buffer[2..buffer.len() - 1])
.map_err(|_| could_not_parse_event_error())?;
let mut split = s.split(';');
let cb = next_parsed::<u16>(&mut split)?;
let cx = next_parsed::<u16>(&mut split)? - 1;
let cy = next_parsed::<u16>(&mut split)? - 1;
let mut modifiers = KeyModifiers::empty();
if cb & 0b0000_0100 == 0b0000_0100 {
modifiers |= KeyModifiers::SHIFT;
}
if cb & 0b0000_1000 == 0b0000_1000 {
modifiers |= KeyModifiers::ALT;
}
if cb & 0b0001_0000 == 0b0001_0000 {
modifiers |= KeyModifiers::CONTROL;
}
let event = if cb & 0b0110_0000 == 0b0110_0000 {
if cb & 0b0000_0001 == 0b0000_0001 {
MouseEvent::ScrollDown(cx, cy, modifiers)
} else {
MouseEvent::ScrollUp(cx, cy, modifiers)
}
} else {
let drag = cb & 0b0100_0000 == 0b0100_0000;
match (cb & 0b0000_0011, drag) {
(0b0000_0000, false) => MouseEvent::Down(MouseButton::Left, cx, cy, modifiers),
(0b0000_0010, false) => MouseEvent::Down(MouseButton::Right, cx, cy, modifiers),
(0b0000_0001, false) => MouseEvent::Down(MouseButton::Middle, cx, cy, modifiers),
(0b0000_0000, true) => MouseEvent::Drag(MouseButton::Left, cx, cy, modifiers),
(0b0000_0010, true) => MouseEvent::Drag(MouseButton::Right, cx, cy, modifiers),
(0b0000_0001, true) => MouseEvent::Drag(MouseButton::Middle, cx, cy, modifiers),
(0b0000_0011, false) => MouseEvent::Up(MouseButton::Left, cx, cy, modifiers),
_ => return Err(could_not_parse_event_error()),
}
};
Ok(Some(InternalEvent::Event(Event::Mouse(event))))
}
pub(crate) fn parse_csi_x10_mouse(buffer: &[u8]) -> Result<Option<InternalEvent>> {
// X10 emulation mouse encoding: ESC [ M CB Cx Cy (6 characters only).
// NOTE (@imdaveho): cannot find documentation on this
assert!(buffer.starts_with(&[b'\x1B', b'[', b'M'])); // ESC [ M
if buffer.len() < 6 {
return Ok(None);
}
let cb = buffer[3] - 0x30;
// See http://www.xfree86.org/current/ctlseqs.html#Mouse%20Tracking
// The upper left character position on the terminal is denoted as 1,1.
// Subtract 1 to keep it synced with cursor
let cx = u16::from(buffer[4].saturating_sub(32)) - 1;
let cy = u16::from(buffer[5].saturating_sub(32)) - 1;
let mut modifiers = KeyModifiers::empty();
if cb & 0b0000_0100 == 0b0000_0100 {
modifiers |= KeyModifiers::SHIFT;
}
if cb & 0b0000_1000 == 0b0000_1000 {
modifiers |= KeyModifiers::ALT;
}
if cb & 0b0001_0000 == 0b0001_0000 {
modifiers |= KeyModifiers::CONTROL;
}
let mouse_input_event = match cb & 0b0000_0011 {
0 => {
if cb & 0b0100_0000 == 0b0100_0000 {
MouseEvent::ScrollUp(cx, cy, modifiers)
} else {
MouseEvent::Down(MouseButton::Left, cx, cy, modifiers)
}
}
1 => {
if cb & 0b0100_0000 == 0b0100_0000 {
MouseEvent::ScrollDown(cx, cy, modifiers)
} else {
MouseEvent::Down(MouseButton::Middle, cx, cy, modifiers)
}
}
2 => MouseEvent::Down(MouseButton::Right, cx, cy, modifiers),
3 => MouseEvent::Up(MouseButton::Left, cx, cy, modifiers),
_ => return Err(could_not_parse_event_error()),
};
Ok(Some(InternalEvent::Event(Event::Mouse(mouse_input_event))))
}
pub(crate) fn parse_csi_xterm_mouse(buffer: &[u8]) -> Result<Option<InternalEvent>> {
// ESC [ < Cb ; Cx ; Cy (;) (M or m)
assert!(buffer.starts_with(&[b'\x1B', b'[', b'<'])); // ESC [ <
if !buffer.ends_with(&[b'm']) && !buffer.ends_with(&[b'M']) {
return Ok(None);
}
let s = std::str::from_utf8(&buffer[3..buffer.len() - 1])
.map_err(|_| could_not_parse_event_error())?;
let mut split = s.split(';');
let cb = next_parsed::<u16>(&mut split)?;
// See http://www.xfree86.org/current/ctlseqs.html#Mouse%20Tracking
// The upper left character position on the terminal is denoted as 1,1.
// Subtract 1 to keep it synced with cursor
let cx = next_parsed::<u16>(&mut split)? - 1;
let cy = next_parsed::<u16>(&mut split)? - 1;
let mut modifiers = KeyModifiers::empty();
if cb & 0b0000_0100 == 0b0000_0100 {
modifiers |= KeyModifiers::SHIFT;
}
if cb & 0b0000_1000 == 0b0000_1000 {
modifiers |= KeyModifiers::ALT;
}
if cb & 0b0001_0000 == 0b0001_0000 {
modifiers |= KeyModifiers::CONTROL;
}
let event = if cb & 0b0100_0000 == 0b0100_0000 {
if cb & 0b0000_0001 == 0b0000_0001 {
MouseEvent::ScrollDown(cx, cy, modifiers)
} else {
MouseEvent::ScrollUp(cx, cy, modifiers)
}
} else {
let up = match buffer.last().unwrap() {
b'm' => true,
b'M' => false,
_ => return Err(could_not_parse_event_error()),
};
let drag = cb & 0b0010_0000 == 0b0010_0000;
match (cb & 0b111, up, drag) {
(0, true, _) => MouseEvent::Up(MouseButton::Left, cx, cy, modifiers),
(0, false, false) => MouseEvent::Down(MouseButton::Left, cx, cy, modifiers),
(0, false, true) => MouseEvent::Drag(MouseButton::Left, cx, cy, modifiers),
(1, true, _) => MouseEvent::Up(MouseButton::Middle, cx, cy, modifiers),
(1, false, false) => MouseEvent::Down(MouseButton::Middle, cx, cy, modifiers),
(1, false, true) => MouseEvent::Drag(MouseButton::Middle, cx, cy, modifiers),
(2, true, _) => MouseEvent::Up(MouseButton::Right, cx, cy, modifiers),
(2, false, false) => MouseEvent::Down(MouseButton::Right, cx, cy, modifiers),
(2, false, true) => MouseEvent::Drag(MouseButton::Right, cx, cy, modifiers),
_ => return Err(could_not_parse_event_error()),
}
};
Ok(Some(InternalEvent::Event(Event::Mouse(event))))
}
pub(crate) fn parse_utf8_char(buffer: &[u8]) -> Result<Option<char>> {
match std::str::from_utf8(buffer) {
Ok(s) => {
let ch = s.chars().next().ok_or_else(could_not_parse_event_error)?;
Ok(Some(ch))
}
Err(_) => {
// from_utf8 failed, but we have to check if we need more bytes for code point
// and if all the bytes we have no are valid
let required_bytes = match buffer[0] {
// https://en.wikipedia.org/wiki/UTF-8#Description
(0x00..=0x7F) => 1, // 0xxxxxxx
(0xC0..=0xDF) => 2, // 110xxxxx 10xxxxxx
(0xE0..=0xEF) => 3, // 1110xxxx 10xxxxxx 10xxxxxx
(0xF0..=0xF7) => 4, // 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
(0x80..=0xBF) | (0xF8..=0xFF) => return Err(could_not_parse_event_error()),
};
// More than 1 byte, check them for 10xxxxxx pattern
if required_bytes > 1 && buffer.len() > 1 {
for byte in &buffer[1..] {
if byte & !0b0011_1111 != 0b1000_0000 {
return Err(could_not_parse_event_error());
}
}
}
if buffer.len() < required_bytes {
// All bytes looks good so far, but we need more of them
Ok(None)
} else {
Err(could_not_parse_event_error())
}
}
}
}
#[cfg(test)]
mod tests {
use crate::event::{KeyModifiers, MouseButton, MouseEvent};
use super::*;
#[test]
fn test_esc_key() {
assert_eq!(
parse_event("\x1B".as_bytes(), false).unwrap(),
Some(InternalEvent::Event(Event::Key(KeyCode::Esc.into()))),
);
}
#[test]
fn test_possible_esc_sequence() {
assert_eq!(parse_event("\x1B".as_bytes(), true).unwrap(), None,);
}
#[test]
fn test_alt_key() {
assert_eq!(
parse_event("\x1Bc".as_bytes(), false).unwrap(),
Some(InternalEvent::Event(Event::Key(KeyEvent::new(
KeyCode::Char('c'),
KeyModifiers::ALT
)))),
);
}
#[test]
fn test_parse_event_subsequent_calls() {
// The main purpose of this test is to check if we're passing
// correct slice to other parse_ functions.
// parse_csi_cursor_position
assert_eq!(
parse_event("\x1B[20;10R".as_bytes(), false).unwrap(),
Some(InternalEvent::CursorPosition(9, 19))
);
// parse_csi
assert_eq!(
parse_event("\x1B[D".as_bytes(), false).unwrap(),
Some(InternalEvent::Event(Event::Key(KeyCode::Left.into()))),
);
// parse_csi_modifier_key_code
assert_eq!(
parse_event("\x1B[2D".as_bytes(), false).unwrap(),
Some(InternalEvent::Event(Event::Key(KeyEvent::new(
KeyCode::Left,
KeyModifiers::SHIFT
))))
);
// parse_csi_special_key_code
assert_eq!(
parse_event("\x1B[3~".as_bytes(), false).unwrap(),
Some(InternalEvent::Event(Event::Key(KeyCode::Delete.into()))),
);
// parse_csi_rxvt_mouse
assert_eq!(
parse_event("\x1B[32;30;40;M".as_bytes(), false).unwrap(),
Some(InternalEvent::Event(Event::Mouse(MouseEvent::Down(
MouseButton::Left,
29,
39,
KeyModifiers::empty(),
))))
);
// parse_csi_x10_mouse
assert_eq!(
parse_event("\x1B[M0\x60\x70".as_bytes(), false).unwrap(),
Some(InternalEvent::Event(Event::Mouse(MouseEvent::Down(
MouseButton::Left,
63,
79,
KeyModifiers::empty(),
))))
);
// parse_csi_xterm_mouse
assert_eq!(
parse_event("\x1B[<0;20;10;M".as_bytes(), false).unwrap(),
Some(InternalEvent::Event(Event::Mouse(MouseEvent::Down(
MouseButton::Left,
19,
9,
KeyModifiers::empty(),
))))
);
// parse_utf8_char
assert_eq!(
parse_event("Ž".as_bytes(), false).unwrap(),
Some(InternalEvent::Event(Event::Key(KeyCode::Char('Ž').into()))),
);
}
#[test]
fn test_parse_event() {
assert_eq!(
parse_event("\t".as_bytes(), false).unwrap(),
Some(InternalEvent::Event(Event::Key(KeyCode::Tab.into()))),
);
}
#[test]
fn test_parse_csi_cursor_position() {
assert_eq!(
parse_csi_cursor_position("\x1B[20;10R".as_bytes()).unwrap(),
Some(InternalEvent::CursorPosition(9, 19))
);
}
#[test]
fn test_parse_csi() {
assert_eq!(
parse_csi("\x1B[D".as_bytes()).unwrap(),
Some(InternalEvent::Event(Event::Key(KeyCode::Left.into()))),
);
}
#[test]
fn test_parse_csi_modifier_key_code() {
assert_eq!(
parse_csi_modifier_key_code("\x1B[2D".as_bytes()).unwrap(),
Some(InternalEvent::Event(Event::Key(KeyEvent::new(
KeyCode::Left,
KeyModifiers::SHIFT
)))),
);
}
#[test]
fn test_parse_csi_special_key_code() {
assert_eq!(
parse_csi_special_key_code("\x1B[3~".as_bytes()).unwrap(),
Some(InternalEvent::Event(Event::Key(KeyCode::Delete.into()))),
);
}
#[test]
fn test_parse_csi_special_key_code_multiple_values_not_supported() {
assert!(parse_csi_special_key_code("\x1B[3;2~".as_bytes()).is_err());
}
#[test]
fn test_parse_csi_rxvt_mouse() {
assert_eq!(
parse_csi_rxvt_mouse("\x1B[32;30;40;M".as_bytes()).unwrap(),
Some(InternalEvent::Event(Event::Mouse(MouseEvent::Down(
MouseButton::Left,
29,
39,
KeyModifiers::empty(),
))))
);
}
#[test]
fn test_parse_csi_x10_mouse() {
assert_eq!(
parse_csi_x10_mouse("\x1B[M0\x60\x70".as_bytes()).unwrap(),
Some(InternalEvent::Event(Event::Mouse(MouseEvent::Down(
MouseButton::Left,
63,
79,
KeyModifiers::empty(),
))))
);
}
#[test]
fn test_parse_csi_xterm_mouse() {
assert_eq!(
parse_csi_xterm_mouse("\x1B[<0;20;10;M".as_bytes()).unwrap(),
Some(InternalEvent::Event(Event::Mouse(MouseEvent::Down(
MouseButton::Left,
19,
9,
KeyModifiers::empty(),
))))
);
assert_eq!(
parse_csi_xterm_mouse("\x1B[<0;20;10M".as_bytes()).unwrap(),
Some(InternalEvent::Event(Event::Mouse(MouseEvent::Down(
MouseButton::Left,
19,
9,
KeyModifiers::empty(),
))))
);
assert_eq!(
parse_csi_xterm_mouse("\x1B[<0;20;10;m".as_bytes()).unwrap(),
Some(InternalEvent::Event(Event::Mouse(MouseEvent::Up(
MouseButton::Left,
19,
9,
KeyModifiers::empty(),
))))
);
assert_eq!(
parse_csi_xterm_mouse("\x1B[<0;20;10m".as_bytes()).unwrap(),
Some(InternalEvent::Event(Event::Mouse(MouseEvent::Up(
MouseButton::Left,
19,
9,
KeyModifiers::empty(),
))))
);
}
#[test]
fn test_utf8() {
// https://www.php.net/manual/en/reference.pcre.pattern.modifiers.php#54805
// 'Valid ASCII' => "a",
assert_eq!(parse_utf8_char("a".as_bytes()).unwrap(), Some('a'),);
// 'Valid 2 Octet Sequence' => "\xc3\xb1",
assert_eq!(parse_utf8_char(&[0xC3, 0xB1]).unwrap(), Some('ñ'),);
// 'Invalid 2 Octet Sequence' => "\xc3\x28",
assert!(parse_utf8_char(&[0xC3, 0x28]).is_err());
// 'Invalid Sequence Identifier' => "\xa0\xa1",
assert!(parse_utf8_char(&[0xA0, 0xA1]).is_err());
// 'Valid 3 Octet Sequence' => "\xe2\x82\xa1",
assert_eq!(
parse_utf8_char(&[0xE2, 0x81, 0xA1]).unwrap(),
Some('\u{2061}'),
);
// 'Invalid 3 Octet Sequence (in 2nd Octet)' => "\xe2\x28\xa1",
assert!(parse_utf8_char(&[0xE2, 0x28, 0xA1]).is_err());
// 'Invalid 3 Octet Sequence (in 3rd Octet)' => "\xe2\x82\x28",
assert!(parse_utf8_char(&[0xE2, 0x82, 0x28]).is_err());
// 'Valid 4 Octet Sequence' => "\xf0\x90\x8c\xbc",
assert_eq!(
parse_utf8_char(&[0xF0, 0x90, 0x8C, 0xBC]).unwrap(),
Some('𐌼'),
);
// 'Invalid 4 Octet Sequence (in 2nd Octet)' => "\xf0\x28\x8c\xbc",
assert!(parse_utf8_char(&[0xF0, 0x28, 0x8C, 0xBC]).is_err());
// 'Invalid 4 Octet Sequence (in 3rd Octet)' => "\xf0\x90\x28\xbc",
assert!(parse_utf8_char(&[0xF0, 0x90, 0x28, 0xBC]).is_err());
// 'Invalid 4 Octet Sequence (in 4th Octet)' => "\xf0\x28\x8c\x28",
assert!(parse_utf8_char(&[0xF0, 0x28, 0x8C, 0x28]).is_err());
}
}

307
src/event/sys/windows.rs Normal file
View File

@ -0,0 +1,307 @@
//! This is a WINDOWS specific implementation for input related action.
use std::io;
use std::io::ErrorKind;
use std::sync::Mutex;
use std::time::Duration;
use crossterm_winapi::{
ConsoleMode, ControlKeyState, EventFlags, Handle, KeyEventRecord, MouseEvent, ScreenBuffer,
Semaphore,
};
use winapi::shared::winerror::WAIT_TIMEOUT;
use winapi::um::{
synchapi::WaitForMultipleObjects,
winbase::{INFINITE, WAIT_ABANDONED_0, WAIT_FAILED, WAIT_OBJECT_0},
};
use winapi::um::{
wincon::{
LEFT_ALT_PRESSED, LEFT_CTRL_PRESSED, RIGHT_ALT_PRESSED, RIGHT_CTRL_PRESSED, SHIFT_PRESSED,
},
winuser::{
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_PRIOR, VK_RETURN, VK_RIGHT, VK_SHIFT, VK_UP,
},
};
use lazy_static::lazy_static;
use crate::{
event::{Event, KeyCode, KeyEvent, KeyModifiers, MouseButton},
Result,
};
const ENABLE_MOUSE_MODE: u32 = 0x0010 | 0x0080 | 0x0008;
lazy_static! {
static ref ORIGINAL_CONSOLE_MODE: Mutex<Option<u32>> = Mutex::new(None);
}
/// 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 mut lock = ORIGINAL_CONSOLE_MODE.lock().unwrap();
if lock.is_none() {
*lock = Some(original_mode);
}
}
/// 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() -> u32 {
// safe unwrap, initial console color was set with `init_console_color` in `WinApiColor::new()`
ORIGINAL_CONSOLE_MODE
.lock()
.unwrap()
.expect("Original console mode not set")
}
pub(crate) fn enable_mouse_capture() -> 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() -> Result<()> {
let mode = ConsoleMode::from(Handle::current_in_handle()?);
mode.set_mode(original_console_mode())?;
Ok(())
}
pub(crate) fn handle_mouse_event(mouse_event: MouseEvent) -> Result<Option<Event>> {
if let Ok(Some(event)) = parse_mouse_event_record(&mouse_event) {
return Ok(Some(Event::Mouse(event)));
}
Ok(None)
}
pub(crate) fn handle_key_event(key_event: KeyEventRecord) -> Result<Option<Event>> {
if key_event.key_down {
if let Some(event) = parse_key_event_record(&key_event) {
return Ok(Some(Event::Key(event)));
}
}
Ok(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
}
}
fn parse_key_event_record(key_event: &KeyEventRecord) -> Option<KeyEvent> {
let modifiers = KeyModifiers::from(key_event.control_key_state);
let key_code = key_event.virtual_key_code as i32;
let parse_result = match 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),
_ => {
// Modifier Keys (Ctrl, Alt, Shift) Support
let character_raw = { (unsafe { *key_event.u_char.UnicodeChar() } as u16) };
if character_raw < 255 {
let mut character = character_raw as u8 as char;
if modifiers.contains(KeyModifiers::ALT) {
// If the ALT key is held down, pressing the A key produces ALT+A, which the system does not treat as a character at all, but rather as a system command.
// The pressed command is stored in `virtual_key_code`.
let command = key_event.virtual_key_code as u8 as char;
if command.is_alphabetic() {
character = command;
} else {
return None;
}
} else if modifiers.contains(KeyModifiers::CONTROL) {
// we need to do some parsing
character = match character_raw as u8 {
c @ b'\x01'..=b'\x1A' => (c as u8 - 0x1 + b'a') as char,
c @ b'\x1C'..=b'\x1F' => (c as u8 - 0x1C + b'4') as char,
_ => return None,
}
}
if modifiers.contains(KeyModifiers::SHIFT) && character == '\t' {
Some(KeyCode::BackTab)
} else if character == '\t' {
Some(KeyCode::Tab)
} else {
Some(KeyCode::Char(character))
}
} else {
None
}
}
};
if let Some(key_code) = parse_result {
return Some(KeyEvent::new(key_code, modifiers));
}
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) -> Result<i16> {
let window_size = ScreenBuffer::current()?.info()?.terminal_window();
Ok(y - window_size.top)
}
fn parse_mouse_event_record(event: &MouseEvent) -> Result<Option<crate::event::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 mut button = MouseButton::Left;
if button_state.right_button() {
button = MouseButton::Right;
}
if button_state.middle_button() {
button = MouseButton::Middle;
}
Ok(match event.event_flags {
EventFlags::PressOrRelease => {
if button_state.release_button() {
// in order to read the up button type, we have to check the last down input record.
Some(crate::event::MouseEvent::Up(
MouseButton::Left,
xpos,
ypos,
modifiers,
))
} else {
Some(crate::event::MouseEvent::Down(
button, xpos, ypos, modifiers,
))
}
}
EventFlags::MouseMoved => {
// Click + Move
// Only register when mouse is not released
// because unix systems share this behaviour.
if !button_state.release_button() {
Some(crate::event::MouseEvent::Drag(
button, xpos, ypos, modifiers,
))
} else {
None
}
}
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(crate::event::MouseEvent::ScrollDown(xpos, ypos, modifiers))
} else if button_state.scroll_up() {
Some(crate::event::MouseEvent::ScrollUp(xpos, ypos, modifiers))
} else {
None
}
}
EventFlags::DoubleClick => None, // double click not supported by unix terminals
EventFlags::MouseHwheeled => None, // horizontal scroll not supported by unix terminals
})
}
pub(crate) struct WinApiPoll {
semaphore: Option<Semaphore>,
}
impl WinApiPoll {
pub(crate) fn new() -> Result<WinApiPoll> {
Ok(WinApiPoll { semaphore: None })
}
}
impl WinApiPoll {
pub fn poll(&mut self, timeout: Option<Duration>) -> Result<Option<bool>> {
let dw_millis = if let Some(duration) = timeout {
duration.as_millis() as u32
} else {
INFINITE
};
let semaphore = Semaphore::new()?;
let console_handle = Handle::current_in_handle()?;
let handles = &[*console_handle, semaphore.handle()];
self.semaphore = Some(semaphore);
let output =
unsafe { WaitForMultipleObjects(handles.len() as u32, handles.as_ptr(), 0, dw_millis) };
let result = match output {
output if output == WAIT_OBJECT_0 + 0 => {
// input handle triggered
Ok(Some(true))
}
output if output == WAIT_OBJECT_0 + 1 => {
// semaphore handle triggered
Ok(None)
}
WAIT_TIMEOUT | WAIT_ABANDONED_0 => {
// timeout elapsed
Ok(None)
}
WAIT_FAILED => return Err(io::Error::last_os_error().into()),
_ => Err(io::Error::new(
ErrorKind::Other,
"WaitForMultipleObjects returned unexpected result.",
)
.into()),
};
self.semaphore = None;
result
}
pub fn cancel(&self) -> Result<()> {
if let Some(semaphore) = &self.semaphore {
semaphore.release()?
}
Ok(())
}
}

91
src/event/timeout.rs Normal file
View File

@ -0,0 +1,91 @@
use std::time::{Duration, Instant};
/// Keeps track of the elapsed time since the moment the polling started.
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));
}
}

View File

@ -1,480 +0,0 @@
//! # Input
//!
//! The `input` module provides a functionality to read the input events.
//!
//! This documentation does not contain a lot of examples. The reason is that it's fairly
//! obvious how to use this crate. Although, we do provide
//! [examples](https://github.com/crossterm-rs/examples) repository
//! to demonstrate the capabilities.
//!
//! ## Synchronous vs Asynchronous
//!
//! ### Synchronous Reading
//!
//! Read the input synchronously from the user, the reads performed will be blocking calls.
//! Using synchronous over asynchronous reading has the benefit that it is using fewer resources than
//! the asynchronous because background thread and queues are left away.
//!
//! See the [`SyncReader`](struct.SyncReader.html) documentation for more details.
//!
//! ### Asynchronous Reading
//!
//! Read the input asynchronously, input events are gathered in the background and queued for you to read.
//! Using asynchronous reading has the benefit that input events are queued until you read them. You can poll
//! for occurred events, and the reads won't block your program.
//!
//! See the [`AsyncReader`](struct.AsyncReader.html) documentation for more details.
//!
//! ### Technical details
//!
//! On UNIX systems crossterm reads from the TTY, on Windows, it uses `ReadConsoleInputW`.
//! For asynchronous reading, a background thread will be fired up to read input events,
//! occurred events will be queued on an MPSC-channel, and the user can iterate over those events.
//!
//! The terminal has to be in the raw mode, raw mode prevents the input of the user to be displayed
//! on the terminal screen. See the
//! [`crossterm_screen`](https://docs.rs/crossterm_screen/) crate documentation to learn more.
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
use crate::utils::{Command, Result};
#[cfg(unix)]
use self::input::unix::UnixInput;
#[cfg(windows)]
use self::input::windows::WindowsInput;
use self::input::Input;
pub use self::input::{AsyncReader, SyncReader};
mod ansi;
mod input;
mod sys;
/// Represents an input event.
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, PartialOrd, PartialEq, Hash, Clone)]
pub enum InputEvent {
/// A single key or a combination of keys.
Keyboard(KeyEvent),
/// A mouse event.
Mouse(MouseEvent),
/// An unsupported event.
///
/// You can ignore this type of event, because it isn't used.
Unsupported(Vec<u8>), // TODO Not used, should be removed.
/// An unknown event.
Unknown,
/// Internal cursor position event. Don't use it, it will be removed in the
/// `crossterm` 1.0.
#[doc(hidden)]
#[cfg(unix)]
CursorPosition(u16, u16), // TODO 1.0: Remove
}
/// Represents a mouse event.
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, PartialOrd, PartialEq, Hash, Clone, Copy)]
pub enum MouseEvent {
/// Pressed mouse button at the location (column, row).
Press(MouseButton, u16, u16),
/// Released mouse button at the location (column, row).
Release(u16, u16),
/// Mouse moved with a pressed left button to the new location (column, row).
Hold(u16, u16),
/// An unknown mouse event.
Unknown,
}
/// Represents a mouse button/wheel.
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, PartialOrd, PartialEq, Hash, Clone, Copy)]
pub enum MouseButton {
/// Left mouse button.
Left,
/// Right mouse button.
Right,
/// Middle mouse button.
Middle,
/// Wheel scrolled up.
WheelUp,
/// Wheel scrolled down.
WheelDown,
}
/// Represents a key or a combination of keys.
#[derive(Debug, PartialOrd, PartialEq, Eq, Clone, Copy, Hash)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub enum KeyEvent {
/// Backspace key.
Backspace,
/// Enter key.
Enter,
/// Left arrow key.
Left,
/// Right arrow key.
Right,
/// Up arrow key.
Up,
/// Down arrow key.
Down,
/// Home key.
Home,
/// End key.
End,
/// Page up key.
PageUp,
/// Page dow key.
PageDown,
/// Tab key.
Tab,
/// Shift + Tab key.
BackTab,
/// Delete key.
Delete,
/// Insert key.
Insert,
/// F key.
///
/// `KeyEvent::F(1)` represents F1 key, etc.
F(u8),
/// A character.
///
/// `KeyEvent::Char('c')` represents `c` character, etc.
Char(char),
/// Alt key + character.
///
/// `KeyEvent::Alt('c')` represents `Alt + c`, etc.
Alt(char),
/// Ctrl key + character.
///
/// `KeyEvent::Ctrl('c') ` represents `Ctrl + c`, etc.
Ctrl(char),
/// Null.
Null,
/// Escape key.
Esc,
/// Ctrl + up arrow key.
CtrlUp,
/// Ctrl + down arrow key.
CtrlDown,
/// Ctrl + right arrow key.
CtrlRight,
/// Ctrl + left arrow key.
CtrlLeft,
/// Shift + up arrow key.
ShiftUp,
/// Shift + down arrow key.
ShiftDown,
/// Shift + right arrow key.
ShiftRight,
/// Shift + left arrow key.
ShiftLeft,
}
/// An internal event.
///
/// Encapsulates publicly available `InputEvent` with additional internal
/// events that shouldn't be publicly available to the crate users.
#[cfg(unix)]
#[derive(Debug, PartialOrd, PartialEq, Hash, Clone)]
pub(crate) enum InternalEvent {
/// An input event.
Input(InputEvent),
/// A cursor position (`x`, `y`).
CursorPosition(u16, u16),
}
/// Converts an `InternalEvent` into a possible `InputEvent`.
#[cfg(unix)]
impl From<InternalEvent> for Option<InputEvent> {
fn from(ie: InternalEvent) -> Self {
match ie {
InternalEvent::Input(input_event) => Some(input_event),
// TODO 1.0: Swallow `CursorPosition` and return `None`.
// `cursor::pos_raw()` will be able to use this module `internal_event_receiver()`
InternalEvent::CursorPosition(x, y) => Some(InputEvent::CursorPosition(x, y)),
}
}
}
/// A terminal input.
///
/// # Examples
///
/// ```no_run
/// // You can replace the following line with `use crossterm::...;`
/// // if you're using the `crossterm` crate with the `input` feature enabled.
/// use crossterm::{Result, input::{TerminalInput}, screen::RawScreen};
///
/// fn main() -> Result<()> {
/// let input = TerminalInput::new();
/// // Read a single character
/// let char = input.read_char()?;
/// // Read a single line
/// let line = input.read_line()?;
///
/// // Make sure to enable raw screen when reading input events
/// let screen = RawScreen::into_raw_mode();
///
/// // Create async reader
/// let mut async_stdin = input.read_async();
///
/// // Create sync reader
/// let mut sync_stdin = input.read_sync();
///
/// // Enable mouse input events
/// input.enable_mouse_mode()?;
/// // Disable mouse input events
/// input.disable_mouse_mode()
/// }
/// ```
pub struct TerminalInput {
#[cfg(windows)]
input: WindowsInput,
#[cfg(unix)]
input: UnixInput,
}
impl TerminalInput {
/// Creates a new `TerminalInput`.
pub fn new() -> TerminalInput {
#[cfg(windows)]
let input = WindowsInput::new();
#[cfg(unix)]
let input = UnixInput::new();
TerminalInput { input }
}
/// Reads one line from the user input and strips the new line character(s).
///
/// This function **does not work** when the raw mode is enabled (see the
/// [`crossterm_screen`](https://docs.rs/crossterm_screen/) crate documentation
/// to learn more). You should use the
/// [`read_async`](struct.TerminalInput.html#method.read_async),
/// [`read_until_async`](struct.TerminalInput.html#method.read_until_async)
/// or [`read_sync`](struct.TerminalInput.html#method.read_sync) method if the
/// raw mode is enabled.
///
/// # Examples
///
/// ```no_run
/// let input = crossterm::input::input();
/// match input.read_line() {
/// Ok(s) => println!("string typed: {}", s),
/// Err(e) => println!("error: {}", e),
/// }
/// ```
pub fn read_line(&self) -> Result<String> {
self.input.read_line()
}
/// Reads one character from the user input.
///
/// # Examples
///
/// ```no_run
/// let input = crossterm::input::input();
/// match input.read_char() {
/// Ok(c) => println!("character pressed: {}", c),
/// Err(e) => println!("error: {}", e),
/// }
/// ```
pub fn read_char(&self) -> Result<char> {
self.input.read_char()
}
/// Creates a new `AsyncReader` allowing to read the input asynchronously (not blocking).
///
/// If you want a blocking, or less resource consuming read, see the
/// [`read_sync`](struct.TerminalInput.html#method.read_sync) method.
///
/// # Notes
///
/// * It requires enabled raw mode (see the
/// [`crossterm_screen`](https://docs.rs/crossterm_screen/) crate documentation to learn more).
/// * A thread is spawned to read the input.
/// * The reading thread is cleaned up when you drop the [`AsyncReader`](struct.AsyncReader.html).
///
/// # Examples
///
/// ```no_run
/// use std::{thread, time::Duration};
/// use crossterm::input::input;
///
/// let mut async_stdin = input().read_async();
///
/// loop {
/// if let Some(key_event) = async_stdin.next() {
/// /* Check which event occurred here */
/// }
///
/// thread::sleep(Duration::from_millis(50));
/// }
/// ```
pub fn read_async(&self) -> AsyncReader {
self.input.read_async()
}
/// Creates a new `AsyncReader` allowing to read the input asynchronously (not blocking) until the
/// given `delimiter`.
///
/// It behaves in the same way as the [`read_async`](struct.TerminalInput.html#method.read_async)
/// method, but it stops reading when the `delimiter` is hit.
///
/// # Notes
///
/// * It requires enabled raw mode (see the
/// [`crossterm_screen`](https://docs.rs/crossterm_screen/) crate documentation to learn more).
/// * A thread is spawned to read the input.
/// * The reading thread is cleaned up when you drop the [`AsyncReader`](struct.AsyncReader.html).
///
/// # Examples
///
/// ```no_run
/// use std::{thread, time::Duration};
/// use crossterm::input::input;
///
/// let mut async_stdin = input().read_until_async(b'x');
///
/// loop {
/// if let Some(key_event) = async_stdin.next() {
/// /* Check which event occurred here */
/// }
///
/// thread::sleep(Duration::from_millis(50));
/// }
/// ```
pub fn read_until_async(&self, delimiter: u8) -> AsyncReader {
self.input.read_until_async(delimiter)
}
/// Creates a new `SyncReader` allowing to read the input synchronously (blocking).
///
/// It's less resource hungry when compared to the
/// [`read_async`](struct.TerminalInput.html#method.read_async) method, because it doesn't
/// spawn any reading threads.
///
/// # Examples
///
/// ```no_run
/// use std::{thread, time::Duration};
/// use crossterm::input::input;
///
/// let mut sync_stdin = input().read_sync();
///
/// loop {
/// if let Some(key_event) = sync_stdin.next() {
/// /* Check which event occurred here */
/// }
/// }
/// ```
pub fn read_sync(&self) -> SyncReader {
self.input.read_sync()
}
/// Enables mouse events.
///
/// Mouse events will be produced by the
/// [`AsyncReader`](struct.AsyncReader.html)/[`SyncReader`](struct.SyncReader.html).
pub fn enable_mouse_mode(&self) -> Result<()> {
self.input.enable_mouse_mode()
}
/// Disables mouse events.
///
/// Mouse events wont be produced by the
/// [`AsyncReader`](struct.AsyncReader.html)/[`SyncReader`](struct.SyncReader.html).
pub fn disable_mouse_mode(&self) -> Result<()> {
self.input.disable_mouse_mode()
}
}
/// Creates a new `TerminalInput`.
///
/// # Examples
///
/// ```no_run
/// // You can replace the following line with `use crossterm::...;`
/// // if you're using the `crossterm` crate with the `input` feature enabled.
/// use crossterm::{input::input, screen::RawScreen, Result};
///
/// fn main() -> Result<()> {
/// let input = input();
/// // Read a single character
/// let char = input.read_char()?;
/// // Read a single line
/// let line = input.read_line()?;
///
/// // Make sure to enable raw screen when reading input events
/// let screen = RawScreen::into_raw_mode();
///
/// // Create async reader
/// let mut async_stdin = input.read_async();
///
/// // Create sync reader
/// let mut sync_stdin = input.read_sync();
///
/// // Enable mouse input events
/// input.enable_mouse_mode()?;
/// // Disable mouse input events
/// input.disable_mouse_mode()
/// }
/// ```
pub fn input() -> TerminalInput {
TerminalInput::new()
}
/// A command that enables mouse mode
///
pub struct EnableMouseCapture;
impl Command for EnableMouseCapture {
type AnsiType = String;
fn ansi_code(&self) -> Self::AnsiType {
ansi::enable_mouse_mode_csi_sequence()
}
#[cfg(windows)]
fn execute_winapi(&self) -> Result<()> {
input().enable_mouse_mode()
}
}
/// A command that disables mouse event monitoring.
///
/// Mouse events will be produced by the
/// [`AsyncReader`](struct.AsyncReader.html)/[`SyncReader`](struct.SyncReader.html).
///
pub struct DisableMouseCapture;
impl Command for DisableMouseCapture {
type AnsiType = String;
fn ansi_code(&self) -> Self::AnsiType {
ansi::disable_mouse_mode_csi_sequence()
}
#[cfg(windows)]
fn execute_winapi(&self) -> Result<()> {
input().disable_mouse_mode()
}
}
/// Stops the reading thread.
///
/// # Notes
///
/// This function is a no-op on the Windows platform.
///
/// When you call this function on the UNIX platform, all event channel senders
/// are dropped and as a consequence you have to drop all `SyncReader`/`AsyncReader` readers.
pub fn stop_reading_thread() {
#[cfg(unix)]
{
sys::unix::stop_reading_thread();
}
}

View File

@ -1,23 +0,0 @@
//! This module provides input related ANSI escape codes.
use crate::csi;
pub(crate) fn enable_mouse_mode_csi_sequence() -> String {
format!(
"{}h{}h{}h{}h",
csi!("?1000"),
csi!("?1002"),
csi!("?1015"),
csi!("?1006")
)
}
pub(crate) fn disable_mouse_mode_csi_sequence() -> String {
format!(
"{}l{}l{}l{}l",
csi!("?1006"),
csi!("?1015"),
csi!("?1002"),
csi!("?1000")
)
}

View File

@ -1,68 +0,0 @@
//! A module that contains all the actions related to reading input from the terminal.
//! Like reading a line, reading a character and reading asynchronously.
use std::io;
use crate::utils::Result;
// TODO Create a new common AsyncReader structure (like TerminalCursor, TerminalInput, ...).
// To avoid copy & pasting of the documentation, to sync the code organization, ...
#[cfg(unix)]
pub use self::unix::{AsyncReader, SyncReader};
#[cfg(windows)]
pub use self::windows::{AsyncReader, SyncReader};
#[cfg(unix)]
pub(crate) mod unix;
#[cfg(windows)]
pub(crate) mod windows;
/// This trait defines the actions that can be performed with the terminal input.
/// This trait can be implemented so that a concrete implementation of the ITerminalInput can fulfill
/// the wishes to work on a specific platform.
///
/// ## For example:
///
/// This trait is implemented for Windows and UNIX systems.
/// Unix is using the 'TTY' and windows is using 'libc' C functions to read the input.
pub(crate) trait Input {
/// Reads one line from the user input and strips the new line character(s).
///
/// This function **does not work** when the raw mode is enabled (see the
/// [`crossterm_screen`](https://docs.rs/crossterm_screen/) crate documentation
/// to learn more). You should use the
/// [`read_async`](struct.TerminalInput.html#method.read_async),
/// [`read_until_async`](struct.TerminalInput.html#method.read_until_async)
/// or [`read_sync`](struct.TerminalInput.html#method.read_sync) method if the
/// raw mode is enabled.
///
/// # Examples
///
/// ```no_run
/// let input = crossterm::input::input();
/// match input.read_line() {
/// Ok(s) => println!("string typed: {}", s),
/// Err(e) => println!("error: {}", e),
/// }
/// ```
fn read_line(&self) -> Result<String> {
let mut rv = String::new();
io::stdin().read_line(&mut rv)?;
let len = rv.trim_end_matches(&['\r', '\n'][..]).len();
rv.truncate(len);
Ok(rv)
}
/// Read one character from the user input
fn read_char(&self) -> Result<char>;
/// Read the input asynchronously from the user.
fn read_async(&self) -> AsyncReader;
/// Read the input asynchronously until a certain character is hit.
fn read_until_async(&self, delimiter: u8) -> AsyncReader;
/// Read the input synchronously from the user.
fn read_sync(&self) -> SyncReader;
/// Start monitoring mouse events.
fn enable_mouse_mode(&self) -> Result<()>;
/// Stop monitoring mouse events.
fn disable_mouse_mode(&self) -> Result<()>;
}

View File

@ -1,291 +0,0 @@
//! This is a UNIX specific implementation for input related action.
use std::sync::mpsc::Receiver;
use std::{char, sync::mpsc};
use crate::utils::Result;
use crate::write_cout;
use super::{
super::{
ansi::{disable_mouse_mode_csi_sequence, enable_mouse_mode_csi_sequence},
sys::unix::internal_event_receiver,
InputEvent, InternalEvent, KeyEvent,
},
Input,
};
pub(crate) struct UnixInput;
impl UnixInput {
pub fn new() -> UnixInput {
UnixInput {}
}
}
impl Input for UnixInput {
fn read_char(&self) -> Result<char> {
let mut reader = self.read_sync();
loop {
if let Some(InputEvent::Keyboard(KeyEvent::Char(ch))) = reader.next() {
return Ok(ch);
}
}
}
fn read_async(&self) -> AsyncReader {
AsyncReader::new(None)
}
fn read_until_async(&self, delimiter: u8) -> AsyncReader {
let sentinel = match delimiter {
b'\n' | b'\r' => Some(KeyEvent::Enter),
b'\x1B' => Some(KeyEvent::Esc),
c if c.is_ascii() => Some(KeyEvent::Char(c as char)),
_ => None,
}
.map(InputEvent::Keyboard);
AsyncReader::new(sentinel)
}
fn read_sync(&self) -> SyncReader {
SyncReader::new()
}
fn enable_mouse_mode(&self) -> Result<()> {
write_cout!(enable_mouse_mode_csi_sequence())?;
Ok(())
}
fn disable_mouse_mode(&self) -> Result<()> {
write_cout!(disable_mouse_mode_csi_sequence())?;
Ok(())
}
}
/// An asynchronous input reader (not blocking).
///
/// `AsyncReader` implements the [`Iterator`](https://doc.rust-lang.org/std/iter/index.html#iterator)
/// trait. Documentation says:
///
/// > An iterator has a method, `next`, which when called, returns an `Option<Item>`. `next` will return
/// > `Some(Item)` as long as there are elements, and once they've all been exhausted, will return `None`
/// > to indicate that iteration is finished. Individual iterators may choose to resume iteration, and
/// > so calling `next` again may or may not eventually start returning `Some(Item)` again at some point.
///
/// `AsyncReader` is an individual iterator and it doesn't use `None` to indicate that the iteration is
/// finished. You can expect additional `Some(InputEvent)` after calling `next` even if you have already
/// received `None`.
///
/// # Notes
///
/// * It requires enabled raw mode (see the
/// [`crossterm_screen`](https://docs.rs/crossterm_screen/) crate documentation to learn more).
/// * A thread is spawned/reused to read the input.
/// * The reading thread is cleaned up when you drop the `AsyncReader`.
/// * See the [`SyncReader`](struct.SyncReader.html) if you want a blocking,
/// or a less resource hungry reader.
///
/// # Examples
///
/// ```no_run
/// use std::{thread, time::Duration};
///
/// use crossterm::{screen::RawScreen, input::{input, InputEvent, KeyEvent}};
///
/// fn main() {
/// println!("Press 'ESC' to quit.");
///
/// // Enable raw mode and keep the `_raw` around otherwise the raw mode will be disabled
/// let _raw = RawScreen::into_raw_mode();
///
/// // Create an input from our screen
/// let input = input();
///
/// // Create an async reader
/// let mut reader = input.read_async();
///
/// loop {
/// if let Some(event) = reader.next() { // Not a blocking call
/// match event {
/// InputEvent::Keyboard(KeyEvent::Esc) => {
/// println!("Program closing ...");
/// break;
/// }
/// InputEvent::Mouse(event) => { /* Mouse event */ }
/// _ => { /* Other events */ }
/// }
/// }
/// thread::sleep(Duration::from_millis(50));
/// }
/// } // `reader` dropped <- thread cleaned up, `_raw` dropped <- raw mode disabled
/// ```
pub struct AsyncReader {
rx: Option<Receiver<InternalEvent>>,
stop_event: Option<InputEvent>,
}
impl AsyncReader {
/// Creates a new `AsyncReader`.
///
/// # Arguments
///
/// * `stop_event` - if set, no more events will be produced if this exact event is reached.
///
/// # Notes
///
/// * A thread is spawned/reused to read the input.
/// * The reading thread is cleaned up when you drop the `AsyncReader`.
fn new(stop_event: Option<InputEvent>) -> AsyncReader {
// TODO 1.0: Following expect is here to keep the API compatible (no Result)
AsyncReader {
rx: Some(internal_event_receiver().expect("Unable to get event receiver")),
stop_event,
}
}
// TODO If we we keep the Drop semantics, do we really need this in the public API? It's useless as
// there's no `start`, etc.
/// Stops the input reader.
///
/// # Notes
///
/// * You don't need to call this method, because it will be automatically called when the
/// `AsyncReader` is dropped.
pub fn stop(&mut self) {
self.rx = None;
}
}
impl Iterator for AsyncReader {
type Item = InputEvent;
/// Tries to read the next input event (not blocking).
///
/// `None` doesn't mean that the iteration is finished. See the
/// [`AsyncReader`](struct.AsyncReader.html) documentation for more information.
fn next(&mut self) -> Option<Self::Item> {
// TODO 1.0: This whole `InternalEvent` -> `InputEvent` mapping should be shared
// between UNIX & Windows implementations
let ref mut rx = match self.rx.as_ref() {
Some(rx) => rx,
None => return None,
};
match rx.try_recv() {
Ok(internal_event) => {
let input_event = internal_event.into();
if self.stop_event.is_some() && input_event == self.stop_event {
// Drop the receiver, stop event received
self.rx = None;
}
input_event
}
Err(mpsc::TryRecvError::Empty) => None,
Err(mpsc::TryRecvError::Disconnected) => {
// Sender dropped, drop the receiver
self.rx = None;
None
}
}
}
}
/// A synchronous input reader (blocking).
///
/// `SyncReader` implements the [`Iterator`](https://doc.rust-lang.org/std/iter/index.html#iterator)
/// trait. Documentation says:
///
/// > An iterator has a method, `next`, which when called, returns an `Option<Item>`. `next` will return
/// > `Some(Item)` as long as there are elements, and once they've all been exhausted, will return `None`
/// > to indicate that iteration is finished. Individual iterators may choose to resume iteration, and
/// > so calling `next` again may or may not eventually start returning `Some(Item)` again at some point.
///
/// `SyncReader` is an individual iterator and it doesn't use `None` to indicate that the iteration is
/// finished. You can expect additional `Some(InputEvent)` after calling `next` even if you have already
/// received `None`. Unfortunately, `None` means that an error occurred, but you're free to call `next`
/// again. This behavior will be changed in the future to avoid errors consumption.
///
/// # Notes
///
/// * It requires enabled raw mode (see the
/// [`crossterm_screen`](https://docs.rs/crossterm_screen/) crate documentation to learn more).
/// * See the [`AsyncReader`](struct.AsyncReader.html) if you want a non blocking reader.
///
/// # Examples
///
/// ```no_run
/// use std::{thread, time::Duration};
///
/// use crossterm::{screen::RawScreen, input::{input, InputEvent, KeyEvent}};
///
/// fn main() {
/// println!("Press 'ESC' to quit.");
///
/// // Enable raw mode and keep the `_raw` around otherwise the raw mode will be disabled
/// let _raw = RawScreen::into_raw_mode();
///
/// // Create an input from our screen
/// let input = input();
///
/// // Create a sync reader
/// let mut reader = input.read_sync();
///
/// loop {
/// if let Some(event) = reader.next() { // Blocking call
/// match event {
/// InputEvent::Keyboard(KeyEvent::Esc) => {
/// println!("Program closing ...");
/// break;
/// }
/// InputEvent::Mouse(event) => { /* Mouse event */ }
/// _ => { /* Other events */ }
/// }
/// }
/// thread::sleep(Duration::from_millis(50));
/// }
/// } // `_raw` dropped <- raw mode disabled
/// ```
pub struct SyncReader {
rx: Option<Receiver<InternalEvent>>,
}
impl SyncReader {
fn new() -> SyncReader {
// TODO 1.0: Following expect is here to keep the API compatible (no Result)
SyncReader {
rx: Some(internal_event_receiver().expect("Unable to get event receiver")),
}
}
}
impl Iterator for SyncReader {
type Item = InputEvent;
/// Tries to read the next input event (blocking).
///
/// `None` doesn't mean that the iteration is finished. See the
/// [`SyncReader`](struct.SyncReader.html) documentation for more information.
fn next(&mut self) -> Option<Self::Item> {
// TODO 1.0: This whole `InternalEvent` -> `InputEvent` mapping should be shared
// between UNIX & Windows implementations
let ref mut rx = match self.rx.as_ref() {
Some(rx) => rx,
None => return None,
};
match rx.recv() {
Ok(internal_event) => internal_event.into(),
Err(mpsc::RecvError) => {
// Sender is dropped, drop the receiver
self.rx = None;
None
}
}
}
}

View File

@ -1,576 +0,0 @@
//! This is a WINDOWS specific implementation for input related action.
use std::{char, collections::VecDeque, io, sync::Mutex};
use crossterm_winapi::{
ButtonState, Console, ConsoleMode, EventFlags, Handle, InputEventType, KeyEventRecord,
MouseEvent, ScreenBuffer,
};
use winapi::um::{
wincon::{
LEFT_ALT_PRESSED, LEFT_CTRL_PRESSED, RIGHT_ALT_PRESSED, RIGHT_CTRL_PRESSED, SHIFT_PRESSED,
},
winnt::INT,
winuser::{
VK_BACK, VK_CONTROL, VK_DELETE, VK_DOWN, VK_END, VK_ESCAPE, VK_F1, VK_F10, VK_F11, VK_F12,
VK_F2, VK_F3, VK_F4, VK_F5, VK_F6, VK_F7, VK_F8, VK_F9, VK_HOME, VK_INSERT, VK_LEFT,
VK_MENU, VK_NEXT, VK_PRIOR, VK_RETURN, VK_RIGHT, VK_SHIFT, VK_UP,
},
};
use lazy_static::lazy_static;
use crate::input::{input::Input, InputEvent, KeyEvent, MouseButton};
use crate::utils::Result;
const ENABLE_MOUSE_MODE: u32 = 0x0010 | 0x0080 | 0x0008;
lazy_static! {
static ref ORIGINAL_CONSOLE_MODE: Mutex<Option<u32>> = Mutex::new(None);
}
/// 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 mut lock = ORIGINAL_CONSOLE_MODE.lock().unwrap();
if lock.is_none() {
*lock = Some(original_mode);
}
}
/// 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() -> u32 {
// safe unwrap, initial console color was set with `init_console_color` in `WinApiColor::new()`
ORIGINAL_CONSOLE_MODE
.lock()
.unwrap()
.expect("Original console mode not set")
}
pub(crate) struct WindowsInput;
impl WindowsInput {
pub fn new() -> WindowsInput {
WindowsInput
}
}
impl Input for WindowsInput {
fn read_char(&self) -> Result<char> {
// _getwch is without echo and _getwche is with echo
let pressed_char = unsafe { _getwche() };
// we could return error but maybe option to keep listening until valid character is inputted.
if pressed_char == 0 || pressed_char == 0xe0 {
Err(io::Error::new(
io::ErrorKind::Other,
"Given input char is not a valid char, mostly occurs when pressing special keys",
))?;
}
let ch = char::from_u32(pressed_char as u32).ok_or_else(|| {
io::Error::new(io::ErrorKind::Other, "Could not parse given input to char")
})?;
Ok(ch)
}
fn read_async(&self) -> AsyncReader {
let handle = Handle::current_in_handle().expect("failed to create console input handle");
let console = Console::from(handle);
AsyncReader::new(console, None)
}
fn read_until_async(&self, delimiter: u8) -> AsyncReader {
let handle = Handle::current_in_handle().expect("failed to create console input handle");
let console = Console::from(handle);
AsyncReader::new(console, Some(delimiter))
}
fn read_sync(&self) -> SyncReader {
SyncReader
}
fn enable_mouse_mode(&self) -> Result<()> {
let mode = ConsoleMode::from(Handle::current_in_handle()?);
init_original_console_mode(mode.mode()?);
mode.set_mode(ENABLE_MOUSE_MODE)?;
Ok(())
}
fn disable_mouse_mode(&self) -> Result<()> {
let mode = ConsoleMode::from(Handle::current_in_handle()?);
mode.set_mode(original_console_mode())?;
Ok(())
}
}
/// A synchronous input reader (blocking).
///
/// `SyncReader` implements the [`Iterator`](https://doc.rust-lang.org/std/iter/index.html#iterator)
/// trait. Documentation says:
///
/// > An iterator has a method, `next`, which when called, returns an `Option<Item>`. `next` will return
/// > `Some(Item)` as long as there are elements, and once they've all been exhausted, will return `None`
/// > to indicate that iteration is finished. Individual iterators may choose to resume iteration, and
/// > so calling `next` again may or may not eventually start returning `Some(Item)` again at some point.
///
/// `SyncReader` is an individual iterator and it doesn't use `None` to indicate that the iteration is
/// finished. You can expect additional `Some(InputEvent)` after calling `next` even if you have already
/// received `None`. Unfortunately, `None` means that an error occurred, but you're free to call `next`
/// again. This behavior will be changed in the future to avoid errors consumption.
///
/// # Notes
///
/// * It requires enabled raw mode (see the
/// [`crossterm_screen`](https://docs.rs/crossterm_screen/) crate documentation to learn more).
/// * See the [`AsyncReader`](struct.AsyncReader.html) if you want a non blocking reader.
///
/// # Examples
///
/// ```no_run
/// use std::{thread, time::Duration};
///
/// use crossterm::{screen::RawScreen, input::{input, InputEvent, KeyEvent}};
///
/// fn main() {
/// println!("Press 'ESC' to quit.");
///
/// // Enable raw mode and keep the `_raw` around otherwise the raw mode will be disabled
/// let _raw = RawScreen::into_raw_mode();
///
/// // Create an input from our screen
/// let input = input();
///
/// // Create a sync reader
/// let mut reader = input.read_sync();
///
/// loop {
/// if let Some(event) = reader.next() { // Blocking call
/// match event {
/// InputEvent::Keyboard(KeyEvent::Esc) => {
/// println!("Program closing ...");
/// break;
/// }
/// InputEvent::Mouse(event) => { /* Mouse event */ }
/// _ => { /* Other events */ }
/// }
/// }
/// thread::sleep(Duration::from_millis(50));
/// }
/// } // `_raw` dropped <- raw mode disabled
/// ```
pub struct SyncReader;
impl Iterator for SyncReader {
type Item = InputEvent;
/// Tries to read the next input event (blocking).
///
/// `None` doesn't mean that the iteration is finished. See the
/// [`SyncReader`](struct.SyncReader.html) documentation for more information.
fn next(&mut self) -> Option<Self::Item> {
// This synces the behaviour with the unix::SyncReader (& documentation) where
// None is returned in case of error.
read_single_event().unwrap_or(None)
}
}
/// An asynchronous input reader (not blocking).
///
/// `AsyncReader` implements the [`Iterator`](https://doc.rust-lang.org/std/iter/index.html#iterator)
/// trait. Documentation says:
///
/// > An iterator has a method, `next`, which when called, returns an `Option<Item>`. `next` will return
/// > `Some(Item)` as long as there are elements, and once they've all been exhausted, will return `None`
/// > to indicate that iteration is finished. Individual iterators may choose to resume iteration, and
/// > so calling `next` again may or may not eventually start returning `Some(Item)` again at some point.
///
/// `AsyncReader` is an individual iterator and it doesn't use `None` to indicate that the iteration is
/// finished. You can expect additional `Some(InputEvent)` after calling `next` even if you have already
/// received `None`.
///
/// # Notes
///
/// * It requires enabled raw mode (see the
/// [`crossterm_screen`](https://docs.rs/crossterm_screen/) crate documentation to learn more).
/// * A thread is spawned to read the input.
/// * The reading thread is cleaned up when you drop the `AsyncReader`.
/// * See the [`SyncReader`](struct.SyncReader.html) if you want a blocking,
/// or a less resource hungry reader.
///
/// # Examples
///
/// ```no_run
/// use std::{thread, time::Duration};
///
/// use crossterm::{screen::RawScreen, input::{input, InputEvent, KeyEvent}};
///
/// fn main() {
/// println!("Press 'ESC' to quit.");
///
/// // Enable raw mode and keep the `_raw` around otherwise the raw mode will be disabled
/// let _raw = RawScreen::into_raw_mode();
///
/// // Create an input from our screen
/// let input = input();
///
/// // Create an async reader
/// let mut reader = input.read_async();
///
/// loop {
/// if let Some(event) = reader.next() { // Not a blocking call
/// match event {
/// InputEvent::Keyboard(KeyEvent::Esc) => {
/// println!("Program closing ...");
/// break;
/// }
/// InputEvent::Mouse(event) => { /* Mouse event */ }
/// _ => { /* Other events */ }
/// }
/// }
/// thread::sleep(Duration::from_millis(50));
/// }
/// } // `reader` dropped <- thread cleaned up, `_raw` dropped <- raw mode disabled
/// ```
pub struct AsyncReader {
console: Console,
buffer: VecDeque<InputEvent>,
delimiter: Option<u8>,
}
impl AsyncReader {
// TODO Should the new() really be public?
/// Creates a new `AsyncReader`.
///
/// # Notes
///
/// * A thread is spawned to read the input.
/// * The reading thread is cleaned up when you drop the `AsyncReader`.
pub fn new(console: Console, delimiter: Option<u8>) -> AsyncReader {
AsyncReader {
console,
buffer: VecDeque::new(),
delimiter,
}
}
pub fn stop(&mut self) {}
}
impl Iterator for AsyncReader {
type Item = InputEvent;
/// Tries to read the next input event (not blocking).
///
/// `None` doesn't mean that the iteration is finished. See the
/// [`AsyncReader`](struct.AsyncReader.html) documentation for more information.
fn next(&mut self) -> Option<Self::Item> {
loop {
if self.buffer.is_empty() {
let (_, events) = read_input_events(&self.console).expect("read failed");
if events.is_empty() {
return None;
}
self.buffer.extend(events);
}
if let Some(delimiter) = self.delimiter {
while let Some(e) = self.buffer.pop_front() {
if let InputEvent::Keyboard(KeyEvent::Char(key)) = e {
if (key as u8) == delimiter {
return Some(e);
}
}
}
continue;
}
return self.buffer.pop_front();
}
}
}
extern "C" {
fn _getwche() -> INT;
}
fn read_single_event() -> Result<Option<InputEvent>> {
let console = Console::from(Handle::current_in_handle()?);
let input = console.read_single_input_event()?;
match input.event_type {
InputEventType::KeyEvent => {
handle_key_event(unsafe { KeyEventRecord::from(*input.event.KeyEvent()) })
}
InputEventType::MouseEvent => {
handle_mouse_event(unsafe { MouseEvent::from(*input.event.MouseEvent()) })
}
// NOTE (@imdaveho): ignore below
InputEventType::WindowBufferSizeEvent => return Ok(None), // TODO implement terminal resize event
InputEventType::FocusEvent => Ok(None),
InputEventType::MenuEvent => Ok(None),
}
}
/// partially inspired by: https://github.com/retep998/wio-rs/blob/master/src/console.rs#L130
fn read_input_events(console: &Console) -> Result<(u32, Vec<InputEvent>)> {
let result = console.read_console_input()?;
let mut input_events = Vec::with_capacity(result.0 as usize);
for input in result.1 {
match input.event_type {
InputEventType::KeyEvent => {
if let Ok(Some(event)) =
handle_key_event(unsafe { KeyEventRecord::from(*input.event.KeyEvent()) })
{
input_events.push(event)
}
}
InputEventType::MouseEvent => {
if let Ok(Some(event)) =
handle_mouse_event(unsafe { MouseEvent::from(*input.event.MouseEvent()) })
{
input_events.push(event)
}
}
// NOTE (@imdaveho): ignore below
InputEventType::WindowBufferSizeEvent => (), // TODO implement terminal resize event
InputEventType::FocusEvent => (),
InputEventType::MenuEvent => (),
}
}
return Ok((result.0, input_events));
}
fn handle_mouse_event(mouse_event: MouseEvent) -> Result<Option<InputEvent>> {
if let Ok(Some(event)) = parse_mouse_event_record(&mouse_event) {
return Ok(Some(InputEvent::Mouse(event)));
}
Ok(None)
}
fn handle_key_event(key_event: KeyEventRecord) -> Result<Option<InputEvent>> {
if key_event.key_down {
if let Some(event) = parse_key_event_record(&key_event) {
return Ok(Some(InputEvent::Keyboard(event)));
}
}
return Ok(None);
}
fn parse_key_event_record(key_event: &KeyEventRecord) -> Option<KeyEvent> {
let key_code = key_event.virtual_key_code as i32;
match key_code {
VK_SHIFT | VK_CONTROL | VK_MENU => None,
VK_BACK => Some(KeyEvent::Backspace),
VK_ESCAPE => Some(KeyEvent::Esc),
VK_RETURN => Some(KeyEvent::Enter),
VK_F1 | VK_F2 | VK_F3 | VK_F4 | VK_F5 | VK_F6 | VK_F7 | VK_F8 | VK_F9 | VK_F10 | VK_F11
| VK_F12 => Some(KeyEvent::F((key_event.virtual_key_code - 111) as u8)),
VK_LEFT | VK_UP | VK_RIGHT | VK_DOWN => {
// Modifier Keys (Ctrl, Shift) Support
let key_state = &key_event.control_key_state;
let ctrl_pressed = key_state.has_state(RIGHT_CTRL_PRESSED | LEFT_CTRL_PRESSED);
let shift_pressed = key_state.has_state(SHIFT_PRESSED);
let event = match key_code {
VK_LEFT => {
if ctrl_pressed {
Some(KeyEvent::CtrlLeft)
} else if shift_pressed {
Some(KeyEvent::ShiftLeft)
} else {
Some(KeyEvent::Left)
}
}
VK_UP => {
if ctrl_pressed {
Some(KeyEvent::CtrlUp)
} else if shift_pressed {
Some(KeyEvent::ShiftUp)
} else {
Some(KeyEvent::Up)
}
}
VK_RIGHT => {
if ctrl_pressed {
Some(KeyEvent::CtrlRight)
} else if shift_pressed {
Some(KeyEvent::ShiftRight)
} else {
Some(KeyEvent::Right)
}
}
VK_DOWN => {
if ctrl_pressed {
Some(KeyEvent::CtrlDown)
} else if shift_pressed {
Some(KeyEvent::ShiftDown)
} else {
Some(KeyEvent::Down)
}
}
_ => None,
};
event
}
VK_PRIOR | VK_NEXT => {
if key_code == VK_PRIOR {
Some(KeyEvent::PageUp)
} else if key_code == VK_NEXT {
Some(KeyEvent::PageDown)
} else {
None
}
}
VK_END | VK_HOME => {
if key_code == VK_HOME {
Some(KeyEvent::Home)
} else if key_code == VK_END {
Some(KeyEvent::End)
} else {
None
}
}
VK_DELETE => Some(KeyEvent::Delete),
VK_INSERT => Some(KeyEvent::Insert),
_ => {
// Modifier Keys (Ctrl, Alt, Shift) Support
let character_raw = { (unsafe { *key_event.u_char.UnicodeChar() } as u16) };
if character_raw < 255 {
let character = character_raw as u8 as char;
let key_state = &key_event.control_key_state;
if key_state.has_state(LEFT_ALT_PRESSED | RIGHT_ALT_PRESSED) {
// If the ALT key is held down, pressing the A key produces ALT+A, which the system does not treat as a character at all, but rather as a system command.
// The pressed command is stored in `virtual_key_code`.
let command = key_event.virtual_key_code as u8 as char;
if (command).is_alphabetic() {
Some(KeyEvent::Alt(command))
} else {
None
}
} else if key_state.has_state(LEFT_CTRL_PRESSED | RIGHT_CTRL_PRESSED) {
match character_raw as u8 {
c @ b'\x01'..=b'\x1A' => {
Some(KeyEvent::Ctrl((c as u8 - 0x1 + b'a') as char))
}
c @ b'\x1C'..=b'\x1F' => {
Some(KeyEvent::Ctrl((c as u8 - 0x1C + b'4') as char))
}
_ => None,
}
} else if key_state.has_state(SHIFT_PRESSED) && character == '\t' {
Some(KeyEvent::BackTab)
} else {
if character == '\t' {
Some(KeyEvent::Tab)
} else {
// Shift + key press, essentially the same as single key press
// Separating to be explicit about the Shift press.
Some(KeyEvent::Char(character))
}
}
} else {
None
}
}
}
}
fn parse_mouse_event_record(event: &MouseEvent) -> Result<Option<crate::input::MouseEvent>> {
// NOTE (@imdaveho): xterm emulation takes the digits of the coords and passes them
// individually as bytes into a buffer; the below cxbs and cybs replicates that and
// mimicks the behavior; additionally, in xterm, mouse move is only handled when a
// mouse button is held down (ie. mouse drag)
let window_size = ScreenBuffer::current()?.info()?.terminal_window();
let xpos = event.mouse_position.x;
let mut ypos = event.mouse_position.y;
// The 'y' position of a mouse 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 counting from the absolute buffer height) instead of relative x: 0, y: 0 to the window.
ypos = ypos - window_size.top;
Ok(match event.event_flags {
EventFlags::PressOrRelease => {
// Single click
match event.button_state {
ButtonState::Release => {
Some(crate::input::MouseEvent::Release(xpos as u16, ypos as u16))
}
ButtonState::FromLeft1stButtonPressed => {
// left click
Some(crate::input::MouseEvent::Press(
MouseButton::Left,
xpos as u16,
ypos as u16,
))
}
ButtonState::RightmostButtonPressed => {
// right click
Some(crate::input::MouseEvent::Press(
MouseButton::Right,
xpos as u16,
ypos as u16,
))
}
ButtonState::FromLeft2ndButtonPressed => {
// middle click
Some(crate::input::MouseEvent::Press(
MouseButton::Middle,
xpos as u16,
ypos as u16,
))
}
_ => None,
}
}
EventFlags::MouseMoved => {
// Click + Move
// NOTE (@imdaveho) only register when mouse is not released
if event.button_state != ButtonState::Release {
Some(crate::input::MouseEvent::Hold(xpos as u16, ypos as u16))
} else {
None
}
}
EventFlags::MouseWheeled => {
// Vertical scroll
// NOTE (@imdaveho) 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 event.button_state != ButtonState::Negative {
Some(crate::input::MouseEvent::Press(
MouseButton::WheelUp,
xpos as u16,
ypos as u16,
))
} else {
Some(crate::input::MouseEvent::Press(
MouseButton::WheelDown,
xpos as u16,
ypos as u16,
))
}
}
EventFlags::DoubleClick => None, // NOTE (@imdaveho): double click not supported by unix terminals
EventFlags::MouseHwheeled => None, // NOTE (@imdaveho): horizontal scroll not supported by unix terminals
// TODO: Handle Ctrl + Mouse, Alt + Mouse, etc.
})
}

View File

@ -1,2 +0,0 @@
#[cfg(unix)]
pub(crate) mod unix;

File diff suppressed because it is too large Load Diff

View File

@ -3,7 +3,7 @@
//! # Crossterm
//!
//! Have you ever been disappointed when a terminal library for rust was only written for UNIX systems?
//! Crossterm provides clearing, input handling, styling, cursor movement, and terminal actions for both
//! Crossterm provides clearing, event (input) handling, styling, cursor movement, and terminal actions for both
//! Windows and UNIX systems.
//!
//! Crossterm aims to be simple and easy to call in code. Through the simplicity of Crossterm, you do not
@ -38,46 +38,45 @@
//!
//! ### Supported Commands
//!
//!| *Command Name* | *Description* |
//!| :------------------------------ | :---------------------------- |
//!| **crossterm::cursor module** | |
//!| `cursor::DisableBlinking` | disables blinking of the terminal cursor. |
//!| `cursor::EnableBlinking` | enables blinking of the terminal cursor. |
//!| `cursor::Hide` | hides the terminal cursor. |
//!| `cursor::MoveDown` | moves the terminal cursor a given number of rows down. |
//!| `cursor::MoveLeft` | moves the terminal cursor a given number of columns to the left. |
//!| `cursor::MoveRight` | moves the terminal cursor a given number of columns to the right. |
//!| `cursor::MoveTo` | moves the terminal cursor to the given position (column, row). |
//!| `cursor::MoveUp` | moves the terminal cursor a given number of rows up. |
//!| `cursor::RestorePosition` | restores the saved terminal cursor position. |
//!| `cursor::SavePosition` | saves the current terminal cursor position. |
//!| `cursor::Show` | shows the terminal cursor. |
//!| **crossterm::input module** | |
//!| `input::DisableMouseCapture` | disables mouse event monitoring. |
//!| `input::EnableMouseCapture` | enables mouse mode |
//!| | |
//!| `screen::EnterAlternateScreen` | switches to the alternate screen. |
//!| `screen::LeaveAlternateScreen` | switches back to the main screen. |
//!| **crossterm::style module** | |
//!| `style::PrintStyledContent` | prints styled content. |
//!| `style::ResetColor` | resets the colors back to default. |
//!| `style::SetAttribute` | sets an attribute. |
//!| `style::SetBackgroundColor` | sets the the background color. |
//!| `style::SetForegroundColor` | sets the the foreground color. |
//!| **crossterm::terminal module** | |
//!| `terminal::Clear` | clears the terminal screen buffer. |
//!| `terminal::ScrollDown` | scrolls the terminal screen a given number of rows down. |
//!| `terminal::ScrollUp` | scrolls the terminal screen a given number of rows up. |
//!| `terminal::SetSize` | sets the terminal size (columns, rows). |
//! - Module `cursor`
//! - Visibility - [`Show`](cursor/struct.Show.html), [`Hide`](cursor/struct.Show.html)
//! - Appearance - [`EnableBlinking`](cursor/struct.EnableBlinking.html),
//! [`DisableBlinking`](cursor/struct.DisableBlinking.html)
//! - Position -
//! [`SavePosition`](cursor/struct.SavePosition.html), [`RestorePosition`](cursor/struct.RestorePosition.html),
//! [`MoveUp`](cursor/struct.MoveUp.html), [`MoveDown`](cursor/struct.MoveDown.html),
//! [`MoveLeft`](cursor/struct.MoveLeft.html), [`MoveRight`](cursor/struct.MoveRight.html),
//! [`MoveTo`](cursor/struct.MoveTo.html)
//! - Module `event`
//! - Mouse events - [`EnableMouseCapture`](event/struct.EnableMouseCapture.html),
//! [`DisableMouseCapture`](event/struct.DisableMouseCapture.html)
//! - Module `screen`
//! - Alternate screen - [`EnterAlternateScreen`](screen/struct.EnterAlternateScreen.html),
//! [`LeaveAlternateScreen`](screen/struct.LeaveAlternateScreen.html)
//! - Module `style`
//! - Colors - [`SetForegroundColor`](style/struct.SetForegroundColor.html),
//! [`SetBackgroundColor`](style/struct.SetBackgroundColor.html),
//! [`ResetColor`](style/struct.ResetColor.html)
//! - Attributes - [`SetAttribute`](style/struct.SetAttribute.html),
//! [`PrintStyledContent`](style/struct.PrintStyledContent.html)
//! - Module `terminal`
//! - Scrolling - [`ScrollUp`](terminal/struct.ScrollUp.html),
//! [`ScrollDown`](terminal/struct.ScrollDown.html)
//! - Miscellaneous - [`Clear`](terminal/struct.Clear.html),
//! [`SetSize`](terminal/struct.SetSize.html)
//!
//! ### Command Execution
//!
//! There are two different way's to execute commands:
//!
//! There are two different way's to execute commands.
//! * [Lazy Execution](#lazy-execution)
//! * [Direct Execution](#direct-execution)
//!
//! ## Lazy Execution
//! #### Lazy Execution
//!
//! Flushing bytes to the terminal buffer is a heavy system call. If we perform a lot of actions with the terminal,
//! we want to do this periodically - like with a TUI editor - so that we can flush more data to the terminal buffer at the same time.
//! we want to do this periodically - like with a TUI editor - so that we can flush more data to the terminal buffer
//! at the same time.
//!
//! Crossterm offers the possibility to do this with `queue`.
//! With `queue` you can queue commands, and when you call [Write::flush][flush] these commands will be executed.
@ -86,10 +85,11 @@
//! The commands will be executed on that buffer.
//! The most common buffer is [std::io::stdout][stdout] however, [std::io::stderr][stderr] is used sometimes as well.
//!
//! ### Examples
//! ##### Examples
//!
//! A simple demonstration that shows the command API in action with cursor commands.
//!
//! **Functions**
//! Functions:
//!
//! ```no_run
//! use std::io::{Write, stdout};
@ -103,10 +103,10 @@
//! stdout.flush();
//! ```
//!
//! The [queue](./trait.QueueableCommand.html) function returns itself, therefore you can use this to queue another command. Like
//! `stdout.queue(Goto(5,5)).queue(Clear(ClearType::All))`.
//! The [queue](./trait.QueueableCommand.html) function returns itself, therefore you can use this to queue another
//! command. Like `stdout.queue(Goto(5,5)).queue(Clear(ClearType::All))`.
//!
//! **Macros**
//! Macros:
//!
//! ```no_run
//! use std::io::{Write, stdout};
@ -121,10 +121,11 @@
//! stdout.flush();
//! ```
//!
//! You can pass more than one command into the [queue](./macro.queue.html) macro like `queue!(stdout, MoveTo(5, 5), Clear(ClearType::All))` and
//! You can pass more than one command into the [queue](./macro.queue.html) macro like
//! `queue!(stdout, MoveTo(5, 5), Clear(ClearType::All))` and
//! they will be executed in the given order from left to right.
//!
//! ## Direct Execution
//! #### Direct Execution
//!
//! For many applications it is not at all important to be efficient with 'flush' operations.
//! For this use case there is the `execute` operation.
@ -134,9 +135,9 @@
//! The commands will be executed on that buffer.
//! The most common buffer is [std::io::stdout][stdout] however, [std::io::stderr][stderr] is used sometimes as well.
//!
//! ### Examples
//! ##### Examples
//!
//! **Functions**
//! Functions:
//!
//! ```no_run
//! use std::io::{Write, stdout};
@ -145,10 +146,10 @@
//! let mut stdout = stdout();
//! stdout.execute(cursor::MoveTo(5,5));
//! ```
//! The [execute](./trait.ExecutableCommand.html) function returns itself, therefore you can use this to queue another command. Like
//! `stdout.queue(Goto(5,5)).queue(Clear(ClearType::All))`.
//! The [execute](./trait.ExecutableCommand.html) function returns itself, therefore you can use this to queue
//! another command. Like `stdout.queue(Goto(5,5)).queue(Clear(ClearType::All))`.
//!
//! **Macros**
//! Macros:
//!
//! ```no_run
//! use std::io::{Write, stdout};
@ -157,14 +158,15 @@
//! let mut stdout = stdout();
//! execute!(stdout, cursor::MoveTo(5, 5));
//! ```
//! You can pass more than one command into the [execute](./macro.execute.html) macro like `execute!(stdout, MoveTo(5, 5), Clear(ClearType::All))` and
//! they will be executed in the given order from left to right.
//! You can pass more than one command into the [execute](./macro.execute.html) macro like
//! `execute!(stdout, MoveTo(5, 5), Clear(ClearType::All))` and they will be executed in the given order from
//! left to right.
//!
//! ## Examples
//!
//! Print a rectangle colored with magenta and use both direct execution and lazy execution.
//!
//! **Functions**
//! Functions:
//!
//! ```no_run
//! use std::io::{stdout, Write};
@ -193,7 +195,7 @@
//! }
//! ```
//!
//! **Macros:**
//! Macros:
//!
//! ```no_run
//! use std::io::{stdout, Write};
@ -225,17 +227,16 @@
//! [stderr]: https://doc.rust-lang.org/std/io/fn.stderr.html
//! [flush]: https://doc.rust-lang.org/std/io/trait.Write.html#tymethod.flush
pub use utils::{Command, ErrorKind, ExecutableCommand, Output, QueueableCommand, Result};
#[cfg(windows)]
pub use utils::functions::supports_ansi;
pub use utils::{Command, ErrorKind, ExecutableCommand, Output, QueueableCommand, Result};
/// A module to work with the terminal cursor
#[cfg(feature = "cursor")]
pub mod cursor;
/// A module to read the input events.
#[cfg(feature = "input")]
pub mod input;
/// A module to read events.
#[cfg(feature = "event")]
pub mod event;
/// A module to work with the terminal screen.
#[cfg(feature = "screen")]
pub mod screen;

View File

@ -36,7 +36,7 @@
//! to remove letters. Sometimes it can be useful to disable these modes because this is undesirable.
//! This may be undesirable if your application wants to read the input without it being shown on the screen.
//! Raw modes are the modes to create this possibility.
//
//!
//! Those modes will be set when enabling raw modes:
//!
//! - Input will not be forwarded to screen

View File

@ -1,5 +1,4 @@
use crate::utils::Result;
use crate::{csi, write_cout};
use crate::{csi, utils::Result, write_cout};
use super::AlternateScreen;

View File

@ -1,6 +1,5 @@
use crossterm_winapi::{ConsoleMode, Handle};
use winapi::shared::minwindef::DWORD;
use winapi::um::wincon;
use winapi::{shared::minwindef::DWORD, um::wincon};
use crate::utils::Result;

View File

@ -16,12 +16,14 @@
//! * [Attribute](enum.Attribute.html#platform-specific-notes)
//!
//! ## Examples
//!
//! A few examples of how to use the style module.
//!
//! ### Colors
//!
//! How to change the terminal text color.
//!
//! **Command API**
//! Command API:
//!
//! Using the Command API to color text.
//!
@ -46,7 +48,7 @@
//! }
//! ```
//!
//! **Functions**
//! Functions:
//!
//! Using functions from [`Colorize`](trait.Colorize.html) on a `String` or `&'static str` to color it.
//!
@ -57,9 +59,10 @@
//! ```
//!
//! ### Attributes
//!
//! How to appy terminal attributes to text.
//!
//! **Command API**
//! Command API:
//!
//! Using the Command API to set attributes.
//!
@ -81,7 +84,7 @@
//! }
//! ```
//!
//! **Functions**:
//! Functions:
//!
//! Using [`Styler`](trait.Styler.html) functions on a `String` or `&'static str` to set attributes to it.
//!
@ -93,7 +96,7 @@
//! println!("{}", "Negative".negative());
//! ```
//!
//! **Displayable**
//! Displayable:
//!
//! [`Attribute`](enum.Attribute.html) implements [Display](https://doc.rust-lang.org/beta/std/fmt/trait.Display.html) and therefore it can be formatted like:
//!
@ -107,19 +110,19 @@
//! );
//! ```
use std::env;
use std::fmt::Display;
use std::{env, fmt::Display};
use crate::impl_display;
use crate::utils::Command;
#[cfg(windows)]
use crate::Result;
use crate::{impl_display, utils::Command};
pub use self::content_style::ContentStyle;
pub(crate) use self::enums::Colored;
pub use self::enums::{Attribute, Color};
pub use self::styled_content::StyledContent;
pub use self::traits::{Colorize, Styler};
pub use self::{
content_style::ContentStyle,
enums::{Attribute, Color},
styled_content::StyledContent,
traits::{Colorize, Styler},
};
#[macro_use]
mod macros;

View File

@ -1,8 +1,10 @@
//! This is a ANSI specific implementation for styling related action.
//! This module is used for Windows 10 terminals and Unix terminals by default.
use crate::csi;
use crate::style::{Attribute, Color, Colored};
use crate::{
csi,
style::{Attribute, Color, Colored},
};
pub(crate) fn set_fg_csi_sequence(fg_color: Color) -> String {
format!(

View File

@ -1,5 +1,4 @@
use std::convert::AsRef;
use std::str::FromStr;
use std::{convert::AsRef, str::FromStr};
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};

View File

@ -1,12 +1,16 @@
//! This module contains the logic to style some content.
use std::fmt::{self, Display, Formatter};
use std::result;
use std::{
fmt::{self, Display, Formatter},
result,
};
use crate::queue;
use crate::style::{
Attribute, Color, Colorize, ContentStyle, ResetColor, SetAttribute, SetBackgroundColor,
SetForegroundColor, Styler,
use crate::{
queue,
style::{
Attribute, Color, Colorize, ContentStyle, ResetColor, SetAttribute, SetBackgroundColor,
SetForegroundColor, Styler,
},
};
/// The style with the content to be styled.

View File

@ -30,14 +30,14 @@
//! Ok(())
//! }
//! ```
//!
//! For manual execution control check out [crossterm::queue](../macro.queue.html).
pub use sys::exit;
pub use sys::size;
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
pub use sys::{exit, size};
use crate::impl_display;
#[doc(no_inline)]
use crate::utils::Command;

View File

@ -1,9 +1,9 @@
//! UNIX related logic for terminal manipulation.
use libc::{ioctl, winsize, STDOUT_FILENO, TIOCGWINSZ};
use std::process;
use crate::utils::sys::unix::wrap_with_result;
use crate::utils::Result;
use libc::{ioctl, winsize, STDOUT_FILENO, TIOCGWINSZ};
use crate::utils::{sys::unix::wrap_with_result, Result};
/// Exits the current application.
pub fn exit() {
@ -55,7 +55,8 @@ pub fn size() -> Result<(u16, u16)> {
ws_ypixel: 0,
};
if let Ok(()) = wrap_with_result(unsafe { ioctl(STDOUT_FILENO, TIOCGWINSZ.into(), &mut size) })
if let Ok(true) =
wrap_with_result(unsafe { ioctl(STDOUT_FILENO, TIOCGWINSZ.into(), &mut size) })
{
return Ok((size.ws_col, size.ws_row));
} else {

View File

@ -2,9 +2,7 @@
use crossterm_winapi::{Console, Coord, Handle, ScreenBuffer, Size};
use crate::terminal::ClearType;
use crate::utils::Result;
use crate::{cursor, ErrorKind};
use crate::{cursor, terminal::ClearType, utils::Result, ErrorKind};
/// Exits the current application.
pub fn exit() {

View File

@ -1,7 +1,9 @@
//! # Utils
pub use self::command::{Command, ExecutableCommand, Output, QueueableCommand};
pub use self::error::{ErrorKind, Result};
pub use self::{
command::{Command, ExecutableCommand, Output, QueueableCommand},
error::{ErrorKind, Result},
};
mod command;
mod error;

View File

@ -1,5 +1,4 @@
use std::fmt::Display;
use std::io::Write;
use std::{fmt::Display, io::Write};
use crate::{execute, queue, write_cout};

View File

@ -1,7 +1,6 @@
//! This module contains all `unix` specific terminal related logic.
use std::sync::Mutex;
use std::{io, mem};
use std::{io, mem, sync::Mutex};
pub use libc::termios as Termios;
use libc::{cfmakeraw, tcgetattr, tcsetattr, STDIN_FILENO, TCSANOW};
@ -20,11 +19,11 @@ pub fn is_raw_mode_enabled() -> bool {
TERMINAL_MODE_PRIOR_RAW_MODE.lock().unwrap().is_some()
}
pub fn wrap_with_result(t: i32) -> Result<()> {
if t == -1 {
pub fn wrap_with_result(result: i32) -> Result<bool> {
if result == -1 {
Err(ErrorKind::IoError(io::Error::last_os_error()))
} else {
Ok(())
Ok(true)
}
}
@ -41,7 +40,7 @@ pub fn get_terminal_attr() -> Result<Termios> {
}
}
pub fn set_terminal_attr(termios: &Termios) -> Result<()> {
pub fn set_terminal_attr(termios: &Termios) -> Result<bool> {
wrap_with_result(unsafe { tcsetattr(STDIN_FILENO, TCSANOW, termios) })
}