Remove and simplify for webassembly use case
This commit is contained in:
parent
b056370038
commit
1b66c396b6
94
Cargo.toml
94
Cargo.toml
@ -1,10 +1,9 @@
|
||||
[package]
|
||||
name = "crossterm"
|
||||
name = "minicrossterm"
|
||||
version = "0.28.1"
|
||||
authors = ["T. Post"]
|
||||
description = "A crossplatform terminal library for manipulating terminals."
|
||||
repository = "https://github.com/crossterm-rs/crossterm"
|
||||
documentation = "https://docs.rs/crossterm/"
|
||||
authors = ["Blasthavers", "T. Post"]
|
||||
description = "A stripped back crossplatform terminal library for manipulating terminals asynchronously."
|
||||
repository = "https://git.blastmud.org/blasthavers/crossterm"
|
||||
license = "MIT"
|
||||
keywords = ["event", "color", "cli", "input", "terminal"]
|
||||
exclude = ["target", "Cargo.lock"]
|
||||
@ -27,22 +26,10 @@ all-features = true
|
||||
# Features
|
||||
#
|
||||
[features]
|
||||
default = ["bracketed-paste", "windows", "events"]
|
||||
windows = [
|
||||
"dep:winapi",
|
||||
"dep:crossterm_winapi",
|
||||
] # Disables winapi dependencies from being included into the binary (SHOULD NOT be disabled on windows).
|
||||
default = ["bracketed-paste", "events"]
|
||||
bracketed-paste = [
|
||||
] # Enables triggering a `Event::Paste` when pasting text into the terminal.
|
||||
event-stream = ["dep:futures-core", "events"] # Enables async events
|
||||
use-dev-tty = [
|
||||
"filedescriptor",
|
||||
"rustix/process",
|
||||
] # Enables raw file descriptor polling / selecting instead of mio.
|
||||
events = [
|
||||
"dep:mio",
|
||||
"dep:signal-hook",
|
||||
"dep:signal-hook-mio",
|
||||
] # Enables reading input/events from the system.
|
||||
serde = ["dep:serde", "bitflags/serde"] # Enables 'serde' for various types.
|
||||
|
||||
@ -51,85 +38,14 @@ serde = ["dep:serde", "bitflags/serde"] # Enables 'serde' for various types.
|
||||
#
|
||||
[dependencies]
|
||||
bitflags = { version = "2.3" }
|
||||
parking_lot = "0.12"
|
||||
|
||||
# optional deps only added when requested
|
||||
futures-core = { version = "0.3", optional = true, default-features = false }
|
||||
serde = { version = "1.0", features = ["derive"], optional = true }
|
||||
|
||||
#
|
||||
# Windows dependencies
|
||||
#
|
||||
[target.'cfg(windows)'.dependencies.winapi]
|
||||
version = "0.3.9"
|
||||
features = ["winuser", "winerror"]
|
||||
optional = true
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
crossterm_winapi = { version = "0.9.1", optional = true }
|
||||
|
||||
#
|
||||
# UNIX dependencies
|
||||
#
|
||||
[target.'cfg(unix)'.dependencies]
|
||||
# Default to using rustix for UNIX systems, but provide an option to use libc for backwards
|
||||
# compatibility.
|
||||
libc = { version = "0.2", default-features = false, optional = true }
|
||||
rustix = { version = "0.38.34", default-features = false, features = [
|
||||
"std",
|
||||
"stdio",
|
||||
"termios",
|
||||
] }
|
||||
signal-hook = { version = "0.3.17", optional = true }
|
||||
filedescriptor = { version = "0.8", optional = true }
|
||||
mio = { version = "1.0", features = ["os-poll"], optional = true }
|
||||
signal-hook-mio = { version = "0.2.4", features = [
|
||||
"support-v1_0",
|
||||
], optional = true }
|
||||
|
||||
#
|
||||
# Dev dependencies (examples, ...)
|
||||
#
|
||||
[dev-dependencies]
|
||||
tokio = { version = "1.25", features = ["full"] }
|
||||
futures = "0.3"
|
||||
futures-timer = "3.0"
|
||||
async-std = "1.12"
|
||||
serde_json = "1.0"
|
||||
serial_test = "2.0.0"
|
||||
temp-env = "0.3.6"
|
||||
|
||||
#
|
||||
# Examples
|
||||
#
|
||||
[[example]]
|
||||
name = "event-read"
|
||||
required-features = ["bracketed-paste", "events"]
|
||||
|
||||
[[example]]
|
||||
name = "event-match-modifiers"
|
||||
required-features = ["bracketed-paste", "events"]
|
||||
|
||||
[[example]]
|
||||
name = "event-poll-read"
|
||||
required-features = ["bracketed-paste", "events"]
|
||||
|
||||
[[example]]
|
||||
name = "event-stream-async-std"
|
||||
required-features = ["event-stream", "events"]
|
||||
|
||||
[[example]]
|
||||
name = "event-stream-tokio"
|
||||
required-features = ["event-stream", "events"]
|
||||
|
||||
[[example]]
|
||||
name = "event-read-char-line"
|
||||
required-features = ["events"]
|
||||
|
||||
[[example]]
|
||||
name = "stderr"
|
||||
required-features = ["events"]
|
||||
|
||||
[[example]]
|
||||
name = "key-display"
|
||||
required-features = ["events"]
|
||||
|
12
README.md
12
README.md
@ -1,12 +1,20 @@
|
||||
<h1 align="center"><img width="440" src="docs/crossterm_full.png" /></h1>
|
||||
# Minicrossterm
|
||||
|
||||
[![Donate](https://img.shields.io/badge/Donate-PayPal-green.svg)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=Z8QK6XU749JB2) ![Travis][s7] [![Latest Version][s1]][l1] [![MIT][s2]][l2] [![docs][s3]][l3] ![Lines of Code][s6] [![Join us on Discord][s5]][l5]
|
||||
Minicrossterm is a cut back fork of crossterm which doesn't depend on stdio, and is intended
|
||||
to only provide non-blocking functionality for applications without relying on standard IO (
|
||||
for example, from webassembly).
|
||||
|
||||
It was created for Blasthavers Worldwideportal, and not by the original authors of crossterm.
|
||||
|
||||
The following information applies to crossterm - some might not be up to date for minicrossterm.
|
||||
|
||||
# Cross-platform Terminal Manipulation Library
|
||||
|
||||
Crossterm is a pure-rust, terminal manipulation library that makes it possible to write cross-platform text-based interfaces (see [features](#features)). It supports all UNIX and Windows terminals down to Windows 7 (not all terminals are tested,
|
||||
see [Tested Terminals](#tested-terminals) for more info).
|
||||
|
||||
[![Donate to crossterm upstream](https://img.shields.io/badge/Donate-PayPal-green.svg)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=Z8QK6XU749JB2)]
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Cross-platform Terminal Manipulation Library](#cross-platform-terminal-manipulation-library)
|
||||
|
@ -1,40 +0,0 @@
|
||||
![Lines of Code][s7] [![MIT][s2]][l2] [![Join us on Discord][s5]][l5]
|
||||
|
||||
# Crossterm Examples
|
||||
|
||||
The examples are compatible with the latest release.
|
||||
|
||||
## Structure
|
||||
|
||||
```
|
||||
├── examples
|
||||
│ └── interactive-test
|
||||
│ └── event-*
|
||||
│ └── stderr
|
||||
```
|
||||
| File Name | Description | Topics |
|
||||
|:----------------------------|:-------------------------------|:------------------------------------------|
|
||||
| `examples/interactive-test` | interactive, walk through, demo | cursor, style, event |
|
||||
| `event-*` | event reading demos | (async) event reading |
|
||||
| `stderr` | crossterm over stderr demo | raw mode, alternate screen, custom output |
|
||||
| `is_tty` | Is this instance a tty ? | tty |
|
||||
|
||||
## Run examples
|
||||
|
||||
```bash
|
||||
$ cargo run --example [file name]
|
||||
```
|
||||
|
||||
To run the interactive-demo go into the folder `examples/interactive-demo` and run `cargo run`.
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the MIT License - see the [LICENSE.md](LICENSE) file for details.
|
||||
|
||||
[s2]: https://img.shields.io/badge/license-MIT-blue.svg
|
||||
[l2]: LICENSE
|
||||
|
||||
[s5]: https://img.shields.io/discord/560857607196377088.svg?logo=discord
|
||||
[l5]: https://discord.gg/K4nyTDB
|
||||
|
||||
[s7]: https://travis-ci.org/crossterm-rs/examples.svg?branch=master
|
@ -1,68 +0,0 @@
|
||||
//! Demonstrates how to match on modifiers like: Control, alt, shift.
|
||||
//!
|
||||
//! cargo run --example event-match-modifiers
|
||||
|
||||
use crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers};
|
||||
|
||||
fn match_event(read_event: Event) {
|
||||
match read_event {
|
||||
// Match one one modifier:
|
||||
Event::Key(KeyEvent {
|
||||
modifiers: KeyModifiers::CONTROL,
|
||||
code,
|
||||
..
|
||||
}) => {
|
||||
println!("Control + {:?}", code);
|
||||
}
|
||||
Event::Key(KeyEvent {
|
||||
modifiers: KeyModifiers::SHIFT,
|
||||
code,
|
||||
..
|
||||
}) => {
|
||||
println!("Shift + {:?}", code);
|
||||
}
|
||||
Event::Key(KeyEvent {
|
||||
modifiers: KeyModifiers::ALT,
|
||||
code,
|
||||
..
|
||||
}) => {
|
||||
println!("Alt + {:?}", code);
|
||||
}
|
||||
|
||||
// Match on multiple modifiers:
|
||||
Event::Key(KeyEvent {
|
||||
code, modifiers, ..
|
||||
}) => {
|
||||
if modifiers == (KeyModifiers::ALT | KeyModifiers::SHIFT) {
|
||||
println!("Alt + Shift {:?}", code);
|
||||
} else {
|
||||
println!("({:?}) with key: {:?}", modifiers, code)
|
||||
}
|
||||
}
|
||||
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
match_event(Event::Key(KeyEvent::new(
|
||||
KeyCode::Char('z'),
|
||||
KeyModifiers::CONTROL,
|
||||
)));
|
||||
match_event(Event::Key(KeyEvent::new(
|
||||
KeyCode::Left,
|
||||
KeyModifiers::SHIFT,
|
||||
)));
|
||||
match_event(Event::Key(KeyEvent::new(
|
||||
KeyCode::Delete,
|
||||
KeyModifiers::ALT,
|
||||
)));
|
||||
match_event(Event::Key(KeyEvent::new(
|
||||
KeyCode::Right,
|
||||
KeyModifiers::ALT | KeyModifiers::SHIFT,
|
||||
)));
|
||||
match_event(Event::Key(KeyEvent::new(
|
||||
KeyCode::Home,
|
||||
KeyModifiers::ALT | KeyModifiers::CONTROL,
|
||||
)));
|
||||
}
|
@ -1,61 +0,0 @@
|
||||
//! Demonstrates how to match on modifiers like: Control, alt, shift.
|
||||
//!
|
||||
//! cargo run --example event-poll-read
|
||||
|
||||
use std::{io, time::Duration};
|
||||
|
||||
use crossterm::{
|
||||
cursor::position,
|
||||
event::{poll, read, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode},
|
||||
};
|
||||
|
||||
const HELP: &str = r#"Blocking poll() & non-blocking read()
|
||||
- Keyboard, mouse and terminal resize events enabled
|
||||
- Prints "." every second if there's no event
|
||||
- Hit "c" to print current cursor position
|
||||
- Use Esc to quit
|
||||
"#;
|
||||
|
||||
fn print_events() -> io::Result<()> {
|
||||
loop {
|
||||
// Wait up to 1s for another event
|
||||
if poll(Duration::from_millis(1_000))? {
|
||||
// It's guaranteed that read() won't block if `poll` returns `Ok(true)`
|
||||
let event = read()?;
|
||||
|
||||
println!("Event::{:?}\r", event);
|
||||
|
||||
if event == Event::Key(KeyCode::Char('c').into()) {
|
||||
println!("Cursor position: {:?}\r", position());
|
||||
}
|
||||
|
||||
if event == Event::Key(KeyCode::Esc.into()) {
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
// Timeout expired, no event for 1s
|
||||
println!(".\r");
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn main() -> io::Result<()> {
|
||||
println!("{}", HELP);
|
||||
|
||||
enable_raw_mode()?;
|
||||
|
||||
let mut stdout = io::stdout();
|
||||
execute!(stdout, EnableMouseCapture)?;
|
||||
|
||||
if let Err(e) = print_events() {
|
||||
println!("Error: {:?}\r", e);
|
||||
}
|
||||
|
||||
execute!(stdout, DisableMouseCapture)?;
|
||||
|
||||
disable_raw_mode()
|
||||
}
|
@ -1,44 +0,0 @@
|
||||
//! Demonstrates how to block read characters or a full line.
|
||||
//! Just note that crossterm is not required to do this and can be done with `io::stdin()`.
|
||||
//!
|
||||
//! cargo run --example event-read-char-line
|
||||
|
||||
use std::io;
|
||||
|
||||
use crossterm::event::{self, Event, KeyCode, KeyEvent};
|
||||
|
||||
pub fn read_char() -> io::Result<char> {
|
||||
loop {
|
||||
if let Event::Key(KeyEvent {
|
||||
code: KeyCode::Char(c),
|
||||
..
|
||||
}) = event::read()?
|
||||
{
|
||||
return Ok(c);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn read_line() -> io::Result<String> {
|
||||
let mut line = String::new();
|
||||
while let Event::Key(KeyEvent { code, .. }) = event::read()? {
|
||||
match code {
|
||||
KeyCode::Enter => {
|
||||
break;
|
||||
}
|
||||
KeyCode::Char(c) => {
|
||||
line.push(c);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(line)
|
||||
}
|
||||
|
||||
fn main() {
|
||||
println!("read line:");
|
||||
println!("{:?}", read_line());
|
||||
println!("read char:");
|
||||
println!("{:?}", read_char());
|
||||
}
|
@ -1,112 +0,0 @@
|
||||
//! Demonstrates how to block read events.
|
||||
//!
|
||||
//! cargo run --example event-read
|
||||
|
||||
use std::io;
|
||||
|
||||
use crossterm::event::{
|
||||
poll, KeyboardEnhancementFlags, PopKeyboardEnhancementFlags, PushKeyboardEnhancementFlags,
|
||||
};
|
||||
use crossterm::{
|
||||
cursor::position,
|
||||
event::{
|
||||
read, DisableBracketedPaste, DisableFocusChange, DisableMouseCapture, EnableBracketedPaste,
|
||||
EnableFocusChange, EnableMouseCapture, Event, KeyCode,
|
||||
},
|
||||
execute, queue,
|
||||
terminal::{disable_raw_mode, enable_raw_mode},
|
||||
};
|
||||
use std::time::Duration;
|
||||
|
||||
const HELP: &str = r#"Blocking read()
|
||||
- Keyboard, mouse, focus and terminal resize events enabled
|
||||
- Hit "c" to print current cursor position
|
||||
- Use Esc to quit
|
||||
"#;
|
||||
|
||||
fn print_events() -> io::Result<()> {
|
||||
loop {
|
||||
// Blocking read
|
||||
let event = read()?;
|
||||
|
||||
println!("Event: {:?}\r", event);
|
||||
|
||||
if event == Event::Key(KeyCode::Char('c').into()) {
|
||||
println!("Cursor position: {:?}\r", position());
|
||||
}
|
||||
|
||||
if let Event::Resize(x, y) = event {
|
||||
let (original_size, new_size) = flush_resize_events((x, y));
|
||||
println!("Resize from: {:?}, to: {:?}\r", original_size, new_size);
|
||||
}
|
||||
|
||||
if event == Event::Key(KeyCode::Esc.into()) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Resize events can occur in batches.
|
||||
// With a simple loop they can be flushed.
|
||||
// This function will keep the first and last resize event.
|
||||
fn flush_resize_events(first_resize: (u16, u16)) -> ((u16, u16), (u16, u16)) {
|
||||
let mut last_resize = first_resize;
|
||||
while let Ok(true) = poll(Duration::from_millis(50)) {
|
||||
if let Ok(Event::Resize(x, y)) = read() {
|
||||
last_resize = (x, y);
|
||||
}
|
||||
}
|
||||
|
||||
(first_resize, last_resize)
|
||||
}
|
||||
|
||||
fn main() -> io::Result<()> {
|
||||
println!("{}", HELP);
|
||||
|
||||
enable_raw_mode()?;
|
||||
|
||||
let mut stdout = io::stdout();
|
||||
|
||||
let supports_keyboard_enhancement = matches!(
|
||||
crossterm::terminal::supports_keyboard_enhancement(),
|
||||
Ok(true)
|
||||
);
|
||||
|
||||
if supports_keyboard_enhancement {
|
||||
queue!(
|
||||
stdout,
|
||||
PushKeyboardEnhancementFlags(
|
||||
KeyboardEnhancementFlags::DISAMBIGUATE_ESCAPE_CODES
|
||||
| KeyboardEnhancementFlags::REPORT_ALL_KEYS_AS_ESCAPE_CODES
|
||||
| KeyboardEnhancementFlags::REPORT_ALTERNATE_KEYS
|
||||
| KeyboardEnhancementFlags::REPORT_EVENT_TYPES
|
||||
)
|
||||
)?;
|
||||
}
|
||||
|
||||
execute!(
|
||||
stdout,
|
||||
EnableBracketedPaste,
|
||||
EnableFocusChange,
|
||||
EnableMouseCapture,
|
||||
)?;
|
||||
|
||||
if let Err(e) = print_events() {
|
||||
println!("Error: {:?}\r", e);
|
||||
}
|
||||
|
||||
if supports_keyboard_enhancement {
|
||||
queue!(stdout, PopKeyboardEnhancementFlags)?;
|
||||
}
|
||||
|
||||
execute!(
|
||||
stdout,
|
||||
DisableBracketedPaste,
|
||||
DisableFocusChange,
|
||||
DisableMouseCapture
|
||||
)?;
|
||||
|
||||
disable_raw_mode()
|
||||
}
|
@ -1,67 +0,0 @@
|
||||
//! Demonstrates how to read events asynchronously with async-std.
|
||||
//!
|
||||
//! cargo run --features="event-stream" --example event-stream-async-std
|
||||
|
||||
use std::{io::stdout, time::Duration};
|
||||
|
||||
use futures::{future::FutureExt, select, StreamExt};
|
||||
use futures_timer::Delay;
|
||||
|
||||
use crossterm::{
|
||||
cursor::position,
|
||||
event::{DisableMouseCapture, EnableMouseCapture, Event, EventStream, KeyCode},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode},
|
||||
};
|
||||
|
||||
const HELP: &str = r#"EventStream based on futures_util::stream::Stream with async-std
|
||||
- Keyboard, mouse and terminal resize events enabled
|
||||
- Prints "." every second if there's no event
|
||||
- Hit "c" to print current cursor position
|
||||
- Use Esc to quit
|
||||
"#;
|
||||
|
||||
async fn print_events() {
|
||||
let mut reader = EventStream::new();
|
||||
|
||||
loop {
|
||||
let mut delay = Delay::new(Duration::from_millis(1_000)).fuse();
|
||||
let mut event = reader.next().fuse();
|
||||
|
||||
select! {
|
||||
_ = delay => { println!(".\r"); },
|
||||
maybe_event = event => {
|
||||
match maybe_event {
|
||||
Some(Ok(event)) => {
|
||||
println!("Event::{:?}\r", event);
|
||||
|
||||
if event == Event::Key(KeyCode::Char('c').into()) {
|
||||
println!("Cursor position: {:?}\r", position());
|
||||
}
|
||||
|
||||
if event == Event::Key(KeyCode::Esc.into()) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Some(Err(e)) => println!("Error: {:?}\r", e),
|
||||
None => break,
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
fn main() -> std::io::Result<()> {
|
||||
println!("{}", HELP);
|
||||
|
||||
enable_raw_mode()?;
|
||||
|
||||
let mut stdout = stdout();
|
||||
execute!(stdout, EnableMouseCapture)?;
|
||||
|
||||
async_std::task::block_on(print_events());
|
||||
|
||||
execute!(stdout, DisableMouseCapture)?;
|
||||
|
||||
disable_raw_mode()
|
||||
}
|
@ -1,68 +0,0 @@
|
||||
//! Demonstrates how to read events asynchronously with tokio.
|
||||
//!
|
||||
//! cargo run --features="event-stream" --example event-stream-tokio
|
||||
|
||||
use std::{io::stdout, time::Duration};
|
||||
|
||||
use futures::{future::FutureExt, select, StreamExt};
|
||||
use futures_timer::Delay;
|
||||
|
||||
use crossterm::{
|
||||
cursor::position,
|
||||
event::{DisableMouseCapture, EnableMouseCapture, Event, EventStream, KeyCode},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode},
|
||||
};
|
||||
|
||||
const HELP: &str = r#"EventStream based on futures_util::Stream with tokio
|
||||
- Keyboard, mouse and terminal resize events enabled
|
||||
- Prints "." every second if there's no event
|
||||
- Hit "c" to print current cursor position
|
||||
- Use Esc to quit
|
||||
"#;
|
||||
|
||||
async fn print_events() {
|
||||
let mut reader = EventStream::new();
|
||||
|
||||
loop {
|
||||
let mut delay = Delay::new(Duration::from_millis(1_000)).fuse();
|
||||
let mut event = reader.next().fuse();
|
||||
|
||||
select! {
|
||||
_ = delay => { println!(".\r"); },
|
||||
maybe_event = event => {
|
||||
match maybe_event {
|
||||
Some(Ok(event)) => {
|
||||
println!("Event::{:?}\r", event);
|
||||
|
||||
if event == Event::Key(KeyCode::Char('c').into()) {
|
||||
println!("Cursor position: {:?}\r", position());
|
||||
}
|
||||
|
||||
if event == Event::Key(KeyCode::Esc.into()) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Some(Err(e)) => println!("Error: {:?}\r", e),
|
||||
None => break,
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> std::io::Result<()> {
|
||||
println!("{}", HELP);
|
||||
|
||||
enable_raw_mode()?;
|
||||
|
||||
let mut stdout = stdout();
|
||||
execute!(stdout, EnableMouseCapture)?;
|
||||
|
||||
print_events().await;
|
||||
|
||||
execute!(stdout, DisableMouseCapture)?;
|
||||
|
||||
disable_raw_mode()
|
||||
}
|
@ -1,13 +0,0 @@
|
||||
[package]
|
||||
name = "interactive-demo"
|
||||
version = "0.0.1"
|
||||
authors = ["T. Post", "Robert Vojta <rvojta@me.com>"]
|
||||
edition = "2018"
|
||||
description = "Interactive demo for crossterm."
|
||||
license = "MIT"
|
||||
exclude = ["target", "Cargo.lock"]
|
||||
readme = "README.md"
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
crossterm = { path = "../../" }
|
@ -1,29 +0,0 @@
|
||||
macro_rules! run_tests {
|
||||
(
|
||||
$dst:expr,
|
||||
$(
|
||||
$testfn:ident
|
||||
),*
|
||||
$(,)?
|
||||
) => {
|
||||
use crossterm::{queue, style, terminal, cursor};
|
||||
$(
|
||||
queue!(
|
||||
$dst,
|
||||
style::ResetColor,
|
||||
terminal::Clear(terminal::ClearType::All),
|
||||
cursor::MoveTo(1, 1),
|
||||
cursor::Show,
|
||||
cursor::EnableBlinking
|
||||
)?;
|
||||
|
||||
$testfn($dst)?;
|
||||
|
||||
match $crate::read_char() {
|
||||
Ok('q') => return Ok(()),
|
||||
Err(e) => return Err(e),
|
||||
_ => { },
|
||||
};
|
||||
)*
|
||||
}
|
||||
}
|
@ -1,104 +0,0 @@
|
||||
#![allow(clippy::cognitive_complexity)]
|
||||
|
||||
use std::io;
|
||||
|
||||
use crossterm::event::KeyEventKind;
|
||||
pub use crossterm::{
|
||||
cursor,
|
||||
event::{self, Event, KeyCode, KeyEvent},
|
||||
execute, queue, style,
|
||||
terminal::{self, ClearType},
|
||||
Command,
|
||||
};
|
||||
|
||||
#[macro_use]
|
||||
mod macros;
|
||||
mod test;
|
||||
|
||||
const MENU: &str = r#"Crossterm interactive test
|
||||
|
||||
Controls:
|
||||
|
||||
- 'q' - quit interactive test (or return to this menu)
|
||||
- any other key - continue with next step
|
||||
|
||||
Available tests:
|
||||
|
||||
1. cursor
|
||||
2. color (foreground, background)
|
||||
3. attributes (bold, italic, ...)
|
||||
4. input
|
||||
5. synchronized output
|
||||
|
||||
Select test to run ('1', '2', ...) or hit 'q' to quit.
|
||||
"#;
|
||||
|
||||
fn run<W>(w: &mut W) -> io::Result<()>
|
||||
where
|
||||
W: io::Write,
|
||||
{
|
||||
execute!(w, terminal::EnterAlternateScreen)?;
|
||||
|
||||
terminal::enable_raw_mode()?;
|
||||
|
||||
loop {
|
||||
queue!(
|
||||
w,
|
||||
style::ResetColor,
|
||||
terminal::Clear(ClearType::All),
|
||||
cursor::Hide,
|
||||
cursor::MoveTo(1, 1)
|
||||
)?;
|
||||
|
||||
for line in MENU.split('\n') {
|
||||
queue!(w, style::Print(line), cursor::MoveToNextLine(1))?;
|
||||
}
|
||||
|
||||
w.flush()?;
|
||||
|
||||
match read_char()? {
|
||||
'1' => test::cursor::run(w)?,
|
||||
'2' => test::color::run(w)?,
|
||||
'3' => test::attribute::run(w)?,
|
||||
'4' => test::event::run(w)?,
|
||||
'5' => test::synchronized_output::run(w)?,
|
||||
'q' => {
|
||||
execute!(w, cursor::SetCursorStyle::DefaultUserShape).unwrap();
|
||||
break;
|
||||
}
|
||||
_ => {}
|
||||
};
|
||||
}
|
||||
|
||||
execute!(
|
||||
w,
|
||||
style::ResetColor,
|
||||
cursor::Show,
|
||||
terminal::LeaveAlternateScreen
|
||||
)?;
|
||||
|
||||
terminal::disable_raw_mode()
|
||||
}
|
||||
|
||||
pub fn read_char() -> std::io::Result<char> {
|
||||
loop {
|
||||
if let Ok(Event::Key(KeyEvent {
|
||||
code: KeyCode::Char(c),
|
||||
kind: KeyEventKind::Press,
|
||||
modifiers: _,
|
||||
state: _,
|
||||
})) = event::read()
|
||||
{
|
||||
return Ok(c);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn buffer_size() -> io::Result<(u16, u16)> {
|
||||
terminal::size()
|
||||
}
|
||||
|
||||
fn main() -> std::io::Result<()> {
|
||||
let mut stdout = io::stdout();
|
||||
run(&mut stdout)
|
||||
}
|
@ -1,5 +0,0 @@
|
||||
pub mod attribute;
|
||||
pub mod color;
|
||||
pub mod cursor;
|
||||
pub mod event;
|
||||
pub mod synchronized_output;
|
@ -1,58 +0,0 @@
|
||||
#![allow(clippy::cognitive_complexity)]
|
||||
|
||||
use crossterm::{cursor, queue, style};
|
||||
use std::io::Write;
|
||||
|
||||
const ATTRIBUTES: [(style::Attribute, style::Attribute); 10] = [
|
||||
(style::Attribute::Bold, style::Attribute::NormalIntensity),
|
||||
(style::Attribute::Italic, style::Attribute::NoItalic),
|
||||
(style::Attribute::Underlined, style::Attribute::NoUnderline),
|
||||
(
|
||||
style::Attribute::DoubleUnderlined,
|
||||
style::Attribute::NoUnderline,
|
||||
),
|
||||
(style::Attribute::Undercurled, style::Attribute::NoUnderline),
|
||||
(style::Attribute::Underdotted, style::Attribute::NoUnderline),
|
||||
(style::Attribute::Underdashed, style::Attribute::NoUnderline),
|
||||
(style::Attribute::Reverse, style::Attribute::NoReverse),
|
||||
(
|
||||
style::Attribute::CrossedOut,
|
||||
style::Attribute::NotCrossedOut,
|
||||
),
|
||||
(style::Attribute::SlowBlink, style::Attribute::NoBlink),
|
||||
];
|
||||
|
||||
fn test_set_display_attributes<W>(w: &mut W) -> std::io::Result<()>
|
||||
where
|
||||
W: Write,
|
||||
{
|
||||
queue!(
|
||||
w,
|
||||
style::Print("Display attributes"),
|
||||
cursor::MoveToNextLine(2)
|
||||
)?;
|
||||
|
||||
for (on, off) in &ATTRIBUTES {
|
||||
queue!(
|
||||
w,
|
||||
style::SetAttribute(*on),
|
||||
style::Print(format!("{:>width$} ", format!("{:?}", on), width = 35)),
|
||||
style::SetAttribute(*off),
|
||||
style::Print(format!("{:>width$}", format!("{:?}", off), width = 35)),
|
||||
style::ResetColor,
|
||||
cursor::MoveToNextLine(1)
|
||||
)?;
|
||||
}
|
||||
|
||||
w.flush()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn run<W>(w: &mut W) -> std::io::Result<()>
|
||||
where
|
||||
W: Write,
|
||||
{
|
||||
run_tests!(w, test_set_display_attributes,);
|
||||
Ok(())
|
||||
}
|
@ -1,198 +0,0 @@
|
||||
#![allow(clippy::cognitive_complexity)]
|
||||
|
||||
use crossterm::{cursor, queue, style, style::Color};
|
||||
use std::io::Write;
|
||||
|
||||
const COLORS: [Color; 21] = [
|
||||
Color::Black,
|
||||
Color::DarkGrey,
|
||||
Color::Grey,
|
||||
Color::White,
|
||||
Color::DarkRed,
|
||||
Color::Red,
|
||||
Color::DarkGreen,
|
||||
Color::Green,
|
||||
Color::DarkYellow,
|
||||
Color::Yellow,
|
||||
Color::DarkBlue,
|
||||
Color::Blue,
|
||||
Color::DarkMagenta,
|
||||
Color::Magenta,
|
||||
Color::DarkCyan,
|
||||
Color::Cyan,
|
||||
Color::AnsiValue(0),
|
||||
Color::AnsiValue(15),
|
||||
Color::Rgb { r: 255, g: 0, b: 0 },
|
||||
Color::Rgb { r: 0, g: 255, b: 0 },
|
||||
Color::Rgb { r: 0, g: 0, b: 255 },
|
||||
];
|
||||
|
||||
fn test_set_foreground_color<W>(w: &mut W) -> std::io::Result<()>
|
||||
where
|
||||
W: Write,
|
||||
{
|
||||
queue!(
|
||||
w,
|
||||
style::Print("Foreground colors on the black & white background"),
|
||||
cursor::MoveToNextLine(2)
|
||||
)?;
|
||||
|
||||
for color in &COLORS {
|
||||
queue!(
|
||||
w,
|
||||
style::SetForegroundColor(*color),
|
||||
style::SetBackgroundColor(Color::Black),
|
||||
style::Print(format!(
|
||||
"{:>width$} ",
|
||||
format!("{:?} ████████████", color),
|
||||
width = 40
|
||||
)),
|
||||
style::SetBackgroundColor(Color::White),
|
||||
style::Print(format!(
|
||||
"{:>width$}",
|
||||
format!("{:?} ████████████", color),
|
||||
width = 40
|
||||
)),
|
||||
cursor::MoveToNextLine(1)
|
||||
)?;
|
||||
}
|
||||
|
||||
w.flush()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn test_set_background_color<W>(w: &mut W) -> std::io::Result<()>
|
||||
where
|
||||
W: Write,
|
||||
{
|
||||
queue!(
|
||||
w,
|
||||
style::Print("Background colors with black & white foreground"),
|
||||
cursor::MoveToNextLine(2)
|
||||
)?;
|
||||
|
||||
for color in &COLORS {
|
||||
queue!(
|
||||
w,
|
||||
style::SetBackgroundColor(*color),
|
||||
style::SetForegroundColor(Color::Black),
|
||||
style::Print(format!(
|
||||
"{:>width$} ",
|
||||
format!("{:?} ▒▒▒▒▒▒▒▒▒▒▒▒", color),
|
||||
width = 40
|
||||
)),
|
||||
style::SetForegroundColor(Color::White),
|
||||
style::Print(format!(
|
||||
"{:>width$}",
|
||||
format!("{:?} ▒▒▒▒▒▒▒▒▒▒▒▒", color),
|
||||
width = 40
|
||||
)),
|
||||
cursor::MoveToNextLine(1)
|
||||
)?;
|
||||
}
|
||||
|
||||
w.flush()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn test_color_values_matrix_16x16<W, F>(w: &mut W, title: &str, color: F) -> std::io::Result<()>
|
||||
where
|
||||
W: Write,
|
||||
F: Fn(u16, u16) -> Color,
|
||||
{
|
||||
queue!(w, style::Print(title))?;
|
||||
|
||||
for idx in 0..=15 {
|
||||
queue!(
|
||||
w,
|
||||
cursor::MoveTo(1, idx + 4),
|
||||
style::Print(format!("{:>width$}", idx, width = 2))
|
||||
)?;
|
||||
queue!(
|
||||
w,
|
||||
cursor::MoveTo(idx * 3 + 3, 3),
|
||||
style::Print(format!("{:>width$}", idx, width = 3))
|
||||
)?;
|
||||
}
|
||||
|
||||
for row in 0..=15u16 {
|
||||
queue!(w, cursor::MoveTo(4, row + 4))?;
|
||||
for col in 0..=15u16 {
|
||||
queue!(
|
||||
w,
|
||||
style::SetForegroundColor(color(col, row)),
|
||||
style::Print("███")
|
||||
)?;
|
||||
}
|
||||
queue!(
|
||||
w,
|
||||
style::SetForegroundColor(Color::White),
|
||||
style::Print(format!("{:>width$} ..= ", row * 16, width = 3)),
|
||||
style::Print(format!("{:>width$}", row * 16 + 15, width = 3))
|
||||
)?;
|
||||
}
|
||||
|
||||
w.flush()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn test_color_ansi_values<W>(w: &mut W) -> std::io::Result<()>
|
||||
where
|
||||
W: Write,
|
||||
{
|
||||
test_color_values_matrix_16x16(w, "Color::Ansi values", |col, row| {
|
||||
Color::AnsiValue((row * 16 + col) as u8)
|
||||
})
|
||||
}
|
||||
|
||||
fn test_rgb_red_values<W>(w: &mut W) -> std::io::Result<()>
|
||||
where
|
||||
W: Write,
|
||||
{
|
||||
test_color_values_matrix_16x16(w, "Color::Rgb red values", |col, row| Color::Rgb {
|
||||
r: (row * 16 + col) as u8,
|
||||
g: 0_u8,
|
||||
b: 0,
|
||||
})
|
||||
}
|
||||
|
||||
fn test_rgb_green_values<W>(w: &mut W) -> std::io::Result<()>
|
||||
where
|
||||
W: Write,
|
||||
{
|
||||
test_color_values_matrix_16x16(w, "Color::Rgb green values", |col, row| Color::Rgb {
|
||||
r: 0,
|
||||
g: (row * 16 + col) as u8,
|
||||
b: 0,
|
||||
})
|
||||
}
|
||||
|
||||
fn test_rgb_blue_values<W>(w: &mut W) -> std::io::Result<()>
|
||||
where
|
||||
W: Write,
|
||||
{
|
||||
test_color_values_matrix_16x16(w, "Color::Rgb blue values", |col, row| Color::Rgb {
|
||||
r: 0,
|
||||
g: 0,
|
||||
b: (row * 16 + col) as u8,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn run<W>(w: &mut W) -> std::io::Result<()>
|
||||
where
|
||||
W: Write,
|
||||
{
|
||||
run_tests!(
|
||||
w,
|
||||
test_set_foreground_color,
|
||||
test_set_background_color,
|
||||
test_color_ansi_values,
|
||||
test_rgb_red_values,
|
||||
test_rgb_green_values,
|
||||
test_rgb_blue_values,
|
||||
);
|
||||
Ok(())
|
||||
}
|
@ -1,222 +0,0 @@
|
||||
#![allow(clippy::cognitive_complexity)]
|
||||
|
||||
use std::io::Write;
|
||||
|
||||
use crossterm::{cursor, execute, queue, style, style::Stylize, Command};
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
|
||||
fn test_move_cursor_up<W>(w: &mut W) -> std::io::Result<()>
|
||||
where
|
||||
W: Write,
|
||||
{
|
||||
draw_cursor_box(w, "Move Up (2)", |_, _| cursor::MoveUp(2))
|
||||
}
|
||||
|
||||
fn test_move_cursor_down<W>(w: &mut W) -> std::io::Result<()>
|
||||
where
|
||||
W: Write,
|
||||
{
|
||||
draw_cursor_box(w, "Move Down (2)", |_, _| cursor::MoveDown(2))
|
||||
}
|
||||
|
||||
fn test_move_cursor_left<W>(w: &mut W) -> std::io::Result<()>
|
||||
where
|
||||
W: Write,
|
||||
{
|
||||
draw_cursor_box(w, "Move Left (2)", |_, _| cursor::MoveLeft(2))
|
||||
}
|
||||
|
||||
fn test_move_cursor_right<W>(w: &mut W) -> std::io::Result<()>
|
||||
where
|
||||
W: Write,
|
||||
{
|
||||
draw_cursor_box(w, "Move Right (2)", |_, _| cursor::MoveRight(2))
|
||||
}
|
||||
|
||||
fn test_move_cursor_to_previous_line<W>(w: &mut W) -> std::io::Result<()>
|
||||
where
|
||||
W: Write,
|
||||
{
|
||||
draw_cursor_box(w, "MoveToPreviousLine (1)", |_, _| {
|
||||
cursor::MoveToPreviousLine(1)
|
||||
})
|
||||
}
|
||||
|
||||
fn test_move_cursor_to_next_line<W>(w: &mut W) -> std::io::Result<()>
|
||||
where
|
||||
W: Write,
|
||||
{
|
||||
draw_cursor_box(w, "MoveToNextLine (1)", |_, _| cursor::MoveToNextLine(1))
|
||||
}
|
||||
|
||||
fn test_move_cursor_to_column<W>(w: &mut W) -> std::io::Result<()>
|
||||
where
|
||||
W: Write,
|
||||
{
|
||||
draw_cursor_box(w, "MoveToColumn (1)", |center_x, _| {
|
||||
cursor::MoveToColumn(center_x + 1)
|
||||
})
|
||||
}
|
||||
|
||||
fn test_hide_cursor<W>(w: &mut W) -> std::io::Result<()>
|
||||
where
|
||||
W: Write,
|
||||
{
|
||||
execute!(w, style::Print("HideCursor"), cursor::Hide)
|
||||
}
|
||||
|
||||
fn test_show_cursor<W>(w: &mut W) -> std::io::Result<()>
|
||||
where
|
||||
W: Write,
|
||||
{
|
||||
execute!(w, style::Print("ShowCursor"), cursor::Show)
|
||||
}
|
||||
|
||||
fn test_cursor_blinking_block<W>(w: &mut W) -> std::io::Result<()>
|
||||
where
|
||||
W: Write,
|
||||
{
|
||||
execute!(
|
||||
w,
|
||||
style::Print("Blinking Block:"),
|
||||
cursor::MoveLeft(2),
|
||||
cursor::SetCursorStyle::BlinkingBlock,
|
||||
)
|
||||
}
|
||||
|
||||
fn test_cursor_blinking_underscore<W>(w: &mut W) -> std::io::Result<()>
|
||||
where
|
||||
W: Write,
|
||||
{
|
||||
execute!(
|
||||
w,
|
||||
style::Print("Blinking Underscore:"),
|
||||
cursor::MoveLeft(2),
|
||||
cursor::SetCursorStyle::BlinkingUnderScore,
|
||||
)
|
||||
}
|
||||
|
||||
fn test_cursor_blinking_bar<W>(w: &mut W) -> std::io::Result<()>
|
||||
where
|
||||
W: Write,
|
||||
{
|
||||
execute!(
|
||||
w,
|
||||
style::Print("Blinking bar:"),
|
||||
cursor::MoveLeft(2),
|
||||
cursor::SetCursorStyle::BlinkingBar,
|
||||
)
|
||||
}
|
||||
|
||||
fn test_move_cursor_to<W>(w: &mut W) -> std::io::Result<()>
|
||||
where
|
||||
W: Write,
|
||||
{
|
||||
draw_cursor_box(
|
||||
w,
|
||||
"MoveTo (x: 1, y: 1) removed from center",
|
||||
|center_x, center_y| cursor::MoveTo(center_x + 1, center_y + 1),
|
||||
)
|
||||
}
|
||||
|
||||
fn test_save_restore_cursor_position<W>(w: &mut W) -> std::io::Result<()>
|
||||
where
|
||||
W: Write,
|
||||
{
|
||||
execute!(w,
|
||||
cursor::MoveTo(0, 0),
|
||||
style::Print("Save position, print character elsewhere, after three seconds restore to old position."),
|
||||
cursor::MoveToNextLine(2),
|
||||
style::Print("Save ->[ ]<- Position"),
|
||||
cursor::MoveTo(8, 2),
|
||||
cursor::SavePosition,
|
||||
cursor::MoveTo(10,10),
|
||||
style::Print("Move To ->[√]<- Position")
|
||||
)?;
|
||||
|
||||
thread::sleep(Duration::from_secs(3));
|
||||
|
||||
execute!(w, cursor::RestorePosition, style::Print("√"))
|
||||
}
|
||||
|
||||
/// Draws a box with an colored center, this center can be taken as a reference point after running the given cursor command.
|
||||
fn draw_cursor_box<W, F, T>(w: &mut W, description: &str, cursor_command: F) -> std::io::Result<()>
|
||||
where
|
||||
W: Write,
|
||||
F: Fn(u16, u16) -> T,
|
||||
T: Command,
|
||||
{
|
||||
execute!(
|
||||
w,
|
||||
cursor::Hide,
|
||||
cursor::MoveTo(0, 0),
|
||||
style::SetForegroundColor(style::Color::Red),
|
||||
style::Print(format!(
|
||||
"Red box is the center. After the action: '{}' '√' is drawn to reflect the action from the center.",
|
||||
description
|
||||
))
|
||||
)?;
|
||||
|
||||
let start_y = 2;
|
||||
let width = 21;
|
||||
let height = 11 + start_y;
|
||||
let center_x = width / 2;
|
||||
let center_y = (height + start_y) / 2;
|
||||
|
||||
for row in start_y..=10 + start_y {
|
||||
for column in 0..=width {
|
||||
if (row == start_y || row == height - 1) || (column == 0 || column == width) {
|
||||
queue!(
|
||||
w,
|
||||
cursor::MoveTo(column, row),
|
||||
style::PrintStyledContent("▓".red()),
|
||||
)?;
|
||||
} else {
|
||||
queue!(
|
||||
w,
|
||||
cursor::MoveTo(column, row),
|
||||
style::PrintStyledContent("_".red().on_white())
|
||||
)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
queue!(
|
||||
w,
|
||||
cursor::MoveTo(center_x, center_y),
|
||||
style::PrintStyledContent("▀".red().on_white()),
|
||||
cursor::MoveTo(center_x, center_y),
|
||||
)?;
|
||||
queue!(
|
||||
w,
|
||||
cursor_command(center_x, center_y),
|
||||
style::PrintStyledContent("√".magenta().on_white())
|
||||
)?;
|
||||
w.flush()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn run<W>(w: &mut W) -> std::io::Result<()>
|
||||
where
|
||||
W: Write,
|
||||
{
|
||||
run_tests!(
|
||||
w,
|
||||
test_hide_cursor,
|
||||
test_show_cursor,
|
||||
test_cursor_blinking_bar,
|
||||
test_cursor_blinking_block,
|
||||
test_cursor_blinking_underscore,
|
||||
test_move_cursor_left,
|
||||
test_move_cursor_right,
|
||||
test_move_cursor_up,
|
||||
test_move_cursor_down,
|
||||
test_move_cursor_to,
|
||||
test_move_cursor_to_next_line,
|
||||
test_move_cursor_to_previous_line,
|
||||
test_move_cursor_to_column,
|
||||
test_save_restore_cursor_position
|
||||
);
|
||||
Ok(())
|
||||
}
|
@ -1,42 +0,0 @@
|
||||
#![allow(clippy::cognitive_complexity)]
|
||||
|
||||
use crossterm::{
|
||||
cursor::position,
|
||||
event::{read, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
|
||||
execute,
|
||||
};
|
||||
use std::io::{self, Write};
|
||||
|
||||
fn test_event<W>(w: &mut W) -> io::Result<()>
|
||||
where
|
||||
W: io::Write,
|
||||
{
|
||||
execute!(w, EnableMouseCapture)?;
|
||||
|
||||
loop {
|
||||
// Blocking read
|
||||
let event = read()?;
|
||||
|
||||
println!("Event::{:?}\r", event);
|
||||
|
||||
if event == Event::Key(KeyCode::Char('c').into()) {
|
||||
println!("Cursor position: {:?}\r", position());
|
||||
}
|
||||
|
||||
if event == Event::Key(KeyCode::Char('q').into()) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
execute!(w, DisableMouseCapture)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn run<W>(w: &mut W) -> std::io::Result<()>
|
||||
where
|
||||
W: Write,
|
||||
{
|
||||
run_tests!(w, test_event);
|
||||
Ok(())
|
||||
}
|
@ -1,41 +0,0 @@
|
||||
use std::io::Write;
|
||||
|
||||
use crossterm::{cursor, execute, style::Print, SynchronizedUpdate};
|
||||
|
||||
fn render_slowly<W>(w: &mut W) -> std::io::Result<()>
|
||||
where
|
||||
W: Write,
|
||||
{
|
||||
for i in 1..10 {
|
||||
execute!(w, Print(format!("{}", i)))?;
|
||||
std::thread::sleep(std::time::Duration::from_millis(50));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn test_slow_rendering<W>(w: &mut W) -> std::io::Result<()>
|
||||
where
|
||||
W: Write,
|
||||
{
|
||||
execute!(w, Print("Rendering without synchronized update:"))?;
|
||||
execute!(w, cursor::MoveToNextLine(1))?;
|
||||
std::thread::sleep(std::time::Duration::from_millis(50));
|
||||
render_slowly(w)?;
|
||||
|
||||
execute!(w, cursor::MoveToNextLine(1))?;
|
||||
execute!(w, Print("Rendering with synchronized update:"))?;
|
||||
execute!(w, cursor::MoveToNextLine(1))?;
|
||||
std::thread::sleep(std::time::Duration::from_millis(50));
|
||||
w.sync_update(render_slowly)??;
|
||||
|
||||
execute!(w, cursor::MoveToNextLine(1))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn run<W>(w: &mut W) -> std::io::Result<()>
|
||||
where
|
||||
W: Write,
|
||||
{
|
||||
run_tests!(w, test_slow_rendering,);
|
||||
Ok(())
|
||||
}
|
@ -1,18 +0,0 @@
|
||||
use crossterm::{
|
||||
execute,
|
||||
terminal::{size, SetSize},
|
||||
tty::IsTty,
|
||||
};
|
||||
use std::io::{stdin, stdout};
|
||||
|
||||
pub fn main() {
|
||||
println!("size: {:?}", size().unwrap());
|
||||
execute!(stdout(), SetSize(10, 10)).unwrap();
|
||||
println!("resized: {:?}", size().unwrap());
|
||||
|
||||
if stdin().is_tty() {
|
||||
println!("Is TTY");
|
||||
} else {
|
||||
println!("Is not TTY");
|
||||
}
|
||||
}
|
@ -1,49 +0,0 @@
|
||||
//! Demonstrates the display format of key events.
|
||||
//!
|
||||
//! This example demonstrates the display format of key events, which is useful for displaying in
|
||||
//! the help section of a terminal application.
|
||||
//!
|
||||
//! cargo run --example key-display
|
||||
|
||||
use std::io;
|
||||
|
||||
use crossterm::event::{KeyEventKind, KeyModifiers};
|
||||
use crossterm::{
|
||||
event::{read, Event, KeyCode},
|
||||
terminal::{disable_raw_mode, enable_raw_mode},
|
||||
};
|
||||
|
||||
const HELP: &str = r#"Key display
|
||||
- Press any key to see its display format
|
||||
- Use Esc to quit
|
||||
"#;
|
||||
|
||||
fn main() -> io::Result<()> {
|
||||
println!("{}", HELP);
|
||||
enable_raw_mode()?;
|
||||
if let Err(e) = print_events() {
|
||||
println!("Error: {:?}\r", e);
|
||||
}
|
||||
disable_raw_mode()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn print_events() -> io::Result<()> {
|
||||
loop {
|
||||
let event = read()?;
|
||||
match event {
|
||||
Event::Key(event) if event.kind == KeyEventKind::Press => {
|
||||
print!("Key pressed: ");
|
||||
if event.modifiers != KeyModifiers::NONE {
|
||||
print!("{}+", event.modifiers);
|
||||
}
|
||||
println!("{}\r", event.code);
|
||||
if event.code == KeyCode::Esc {
|
||||
break;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
@ -1,95 +0,0 @@
|
||||
//! This shows how an application can write on stderr
|
||||
//! instead of stdout, thus making it possible to
|
||||
//! the command API instead of the "old style" direct
|
||||
//! unbuffered API.
|
||||
//!
|
||||
//! This particular example is only suited to Unix
|
||||
//! for now.
|
||||
//!
|
||||
//! cargo run --example stderr
|
||||
|
||||
use std::io;
|
||||
|
||||
use crossterm::{
|
||||
cursor::{Hide, MoveTo, Show},
|
||||
event,
|
||||
event::{Event, KeyCode, KeyEvent},
|
||||
execute, queue,
|
||||
style::Print,
|
||||
terminal::{self, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
|
||||
const TEXT: &str = r#"
|
||||
This screen is ran on stderr.
|
||||
And when you hit enter, it prints on stdout.
|
||||
This makes it possible to run an application and choose what will
|
||||
be sent to any application calling yours.
|
||||
|
||||
For example, assuming you build this example with
|
||||
|
||||
cargo build --bin stderr
|
||||
|
||||
and then you run it with
|
||||
|
||||
cd "$(target/debug/stderr)"
|
||||
|
||||
what the application prints on stdout is used as argument to cd.
|
||||
|
||||
Try it out.
|
||||
|
||||
Hit any key to quit this screen:
|
||||
|
||||
1 will print `..`
|
||||
2 will print `/`
|
||||
3 will print `~`
|
||||
Any other key will print this text (so that you may copy-paste)
|
||||
"#;
|
||||
|
||||
fn run_app<W>(write: &mut W) -> io::Result<char>
|
||||
where
|
||||
W: io::Write,
|
||||
{
|
||||
queue!(
|
||||
write,
|
||||
EnterAlternateScreen, // enter alternate screen
|
||||
Hide // hide the cursor
|
||||
)?;
|
||||
|
||||
let mut y = 1;
|
||||
for line in TEXT.split('\n') {
|
||||
queue!(write, MoveTo(1, y), Print(line.to_string()))?;
|
||||
y += 1;
|
||||
}
|
||||
|
||||
write.flush()?;
|
||||
|
||||
terminal::enable_raw_mode()?;
|
||||
let user_char = read_char()?; // we wait for the user to hit a key
|
||||
execute!(write, Show, LeaveAlternateScreen)?; // restore the cursor and leave the alternate screen
|
||||
|
||||
terminal::disable_raw_mode()?;
|
||||
|
||||
Ok(user_char)
|
||||
}
|
||||
|
||||
pub fn read_char() -> io::Result<char> {
|
||||
loop {
|
||||
if let Event::Key(KeyEvent {
|
||||
code: KeyCode::Char(c),
|
||||
..
|
||||
}) = event::read()?
|
||||
{
|
||||
return Ok(c);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// cargo run --example stderr
|
||||
fn main() {
|
||||
match run_app(&mut io::stderr()).unwrap() {
|
||||
'1' => print!(".."),
|
||||
'2' => print!("/"),
|
||||
'3' => print!("~"),
|
||||
_ => println!("{}", TEXT),
|
||||
}
|
||||
}
|
@ -46,11 +46,6 @@ use std::fmt;
|
||||
|
||||
use crate::{csi, impl_display, Command};
|
||||
|
||||
pub(crate) mod sys;
|
||||
|
||||
#[cfg(feature = "events")]
|
||||
pub use sys::position;
|
||||
|
||||
/// A command that moves the terminal cursor to the given position (column, row).
|
||||
///
|
||||
/// # Notes
|
||||
|
@ -1,20 +0,0 @@
|
||||
//! This module provides platform related functions.
|
||||
|
||||
#[cfg(unix)]
|
||||
#[cfg(feature = "events")]
|
||||
pub use self::unix::position;
|
||||
#[cfg(windows)]
|
||||
#[cfg(feature = "events")]
|
||||
pub use self::windows::position;
|
||||
#[cfg(windows)]
|
||||
pub(crate) use self::windows::{
|
||||
move_down, move_left, move_right, move_to, move_to_column, move_to_next_line,
|
||||
move_to_previous_line, move_to_row, move_up, restore_position, save_position, show_cursor,
|
||||
};
|
||||
|
||||
#[cfg(windows)]
|
||||
pub(crate) mod windows;
|
||||
|
||||
#[cfg(unix)]
|
||||
#[cfg(feature = "events")]
|
||||
pub(crate) mod unix;
|
@ -1,56 +0,0 @@
|
||||
use std::{
|
||||
io::{self, Error, ErrorKind, Write},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
event::{filter::CursorPositionFilter, poll_internal, read_internal, InternalEvent},
|
||||
terminal::{disable_raw_mode, enable_raw_mode, sys::is_raw_mode_enabled},
|
||||
};
|
||||
|
||||
/// Returns the cursor position (column, row).
|
||||
///
|
||||
/// The top left cell is represented as `(0, 0)`.
|
||||
///
|
||||
/// On unix systems, this function will block and possibly time out while
|
||||
/// [`crossterm::event::read`](crate::event::read) or [`crossterm::event::poll`](crate::event::poll) are being called.
|
||||
pub fn position() -> io::Result<(u16, u16)> {
|
||||
if is_raw_mode_enabled() {
|
||||
read_position_raw()
|
||||
} else {
|
||||
read_position()
|
||||
}
|
||||
}
|
||||
|
||||
fn read_position() -> io::Result<(u16, u16)> {
|
||||
enable_raw_mode()?;
|
||||
let pos = read_position_raw();
|
||||
disable_raw_mode()?;
|
||||
pos
|
||||
}
|
||||
|
||||
fn read_position_raw() -> io::Result<(u16, u16)> {
|
||||
// Use `ESC [ 6 n` to and retrieve the cursor position.
|
||||
let mut stdout = io::stdout();
|
||||
stdout.write_all(b"\x1B[6n")?;
|
||||
stdout.flush()?;
|
||||
|
||||
loop {
|
||||
match poll_internal(Some(Duration::from_millis(2000)), &CursorPositionFilter) {
|
||||
Ok(true) => {
|
||||
if let Ok(InternalEvent::CursorPosition(x, y)) =
|
||||
read_internal(&CursorPositionFilter)
|
||||
{
|
||||
return Ok((x, y));
|
||||
}
|
||||
}
|
||||
Ok(false) => {
|
||||
return Err(Error::new(
|
||||
ErrorKind::Other,
|
||||
"The cursor position could not be read within a normal duration",
|
||||
));
|
||||
}
|
||||
Err(_) => {}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,341 +0,0 @@
|
||||
//! WinAPI related logic to cursor manipulation.
|
||||
|
||||
use std::convert::TryFrom;
|
||||
use std::io;
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
|
||||
use crossterm_winapi::{result, Coord, Handle, HandleType, ScreenBuffer};
|
||||
use winapi::{
|
||||
shared::minwindef::{FALSE, TRUE},
|
||||
um::wincon::{SetConsoleCursorInfo, SetConsoleCursorPosition, CONSOLE_CURSOR_INFO, COORD},
|
||||
};
|
||||
|
||||
/// The position of the cursor, written when you save the cursor's position.
|
||||
///
|
||||
/// This is `u64::MAX` initially. Otherwise, it stores the cursor's x position bit-shifted left 16
|
||||
/// times or-ed with the cursor's y position, where both are `i16`s.
|
||||
static SAVED_CURSOR_POS: AtomicU64 = AtomicU64::new(u64::MAX);
|
||||
|
||||
// The 'y' position of the cursor is not relative to the window but absolute to screen buffer.
|
||||
// We can calculate the relative cursor position by subtracting the top position of the terminal window from the y position.
|
||||
// This results in an 1-based coord zo subtract 1 to make cursor position 0-based.
|
||||
pub fn parse_relative_y(y: i16) -> std::io::Result<i16> {
|
||||
let window = ScreenBuffer::current()?.info()?;
|
||||
|
||||
let window_size = window.terminal_window();
|
||||
let screen_size = window.terminal_size();
|
||||
|
||||
if y <= screen_size.height {
|
||||
Ok(y)
|
||||
} else {
|
||||
Ok(y - window_size.top)
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the cursor position (column, row).
|
||||
///
|
||||
/// The top left cell is represented `0,0`.
|
||||
pub fn position() -> io::Result<(u16, u16)> {
|
||||
let cursor = ScreenBufferCursor::output()?;
|
||||
let mut position = cursor.position()?;
|
||||
// if position.y != 0 {
|
||||
position.y = parse_relative_y(position.y)?;
|
||||
// }
|
||||
Ok(position.into())
|
||||
}
|
||||
|
||||
pub(crate) fn show_cursor(show_cursor: bool) -> std::io::Result<()> {
|
||||
ScreenBufferCursor::from(Handle::current_out_handle()?).set_visibility(show_cursor)
|
||||
}
|
||||
|
||||
pub(crate) fn move_to(column: u16, row: u16) -> std::io::Result<()> {
|
||||
let cursor = ScreenBufferCursor::output()?;
|
||||
cursor.move_to(column as i16, row as i16)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn move_up(count: u16) -> std::io::Result<()> {
|
||||
let (column, row) = position()?;
|
||||
move_to(column, row - count)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn move_right(count: u16) -> std::io::Result<()> {
|
||||
let (column, row) = position()?;
|
||||
move_to(column + count, row)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn move_down(count: u16) -> std::io::Result<()> {
|
||||
let (column, row) = position()?;
|
||||
move_to(column, row + count)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn move_left(count: u16) -> std::io::Result<()> {
|
||||
let (column, row) = position()?;
|
||||
move_to(column - count, row)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn move_to_column(new_column: u16) -> std::io::Result<()> {
|
||||
let (_, row) = position()?;
|
||||
move_to(new_column, row)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn move_to_row(new_row: u16) -> std::io::Result<()> {
|
||||
let (col, _) = position()?;
|
||||
move_to(col, new_row)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn move_to_next_line(count: u16) -> std::io::Result<()> {
|
||||
let (_, row) = position()?;
|
||||
move_to(0, row + count)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn move_to_previous_line(count: u16) -> std::io::Result<()> {
|
||||
let (_, row) = position()?;
|
||||
move_to(0, row - count)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn save_position() -> std::io::Result<()> {
|
||||
ScreenBufferCursor::output()?.save_position()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn restore_position() -> std::io::Result<()> {
|
||||
ScreenBufferCursor::output()?.restore_position()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// WinAPI wrapper over terminal cursor behaviour.
|
||||
struct ScreenBufferCursor {
|
||||
screen_buffer: ScreenBuffer,
|
||||
}
|
||||
|
||||
impl ScreenBufferCursor {
|
||||
fn output() -> std::io::Result<ScreenBufferCursor> {
|
||||
Ok(ScreenBufferCursor {
|
||||
screen_buffer: ScreenBuffer::from(Handle::new(HandleType::CurrentOutputHandle)?),
|
||||
})
|
||||
}
|
||||
|
||||
fn position(&self) -> std::io::Result<Coord> {
|
||||
Ok(self.screen_buffer.info()?.cursor_pos())
|
||||
}
|
||||
|
||||
fn move_to(&self, x: i16, y: i16) -> std::io::Result<()> {
|
||||
if x < 0 {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
format!("Argument Out of Range Exception when setting cursor position to X: {x}"),
|
||||
));
|
||||
}
|
||||
|
||||
if y < 0 {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
format!("Argument Out of Range Exception when setting cursor position to Y: {y}"),
|
||||
));
|
||||
}
|
||||
|
||||
let position = COORD { X: x, Y: y };
|
||||
|
||||
unsafe {
|
||||
if result(SetConsoleCursorPosition(
|
||||
**self.screen_buffer.handle(),
|
||||
position,
|
||||
))
|
||||
.is_err()
|
||||
{
|
||||
return Err(io::Error::last_os_error());
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn set_visibility(&self, visible: bool) -> std::io::Result<()> {
|
||||
let cursor_info = CONSOLE_CURSOR_INFO {
|
||||
dwSize: 100,
|
||||
bVisible: if visible { TRUE } else { FALSE },
|
||||
};
|
||||
|
||||
unsafe {
|
||||
if result(SetConsoleCursorInfo(
|
||||
**self.screen_buffer.handle(),
|
||||
&cursor_info,
|
||||
))
|
||||
.is_err()
|
||||
{
|
||||
return Err(io::Error::last_os_error());
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn restore_position(&self) -> std::io::Result<()> {
|
||||
if let Ok(val) = u32::try_from(SAVED_CURSOR_POS.load(Ordering::Relaxed)) {
|
||||
let x = (val >> 16) as i16;
|
||||
let y = val as i16;
|
||||
self.move_to(x, y)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn save_position(&self) -> std::io::Result<()> {
|
||||
let position = self.position()?;
|
||||
|
||||
let bits = u64::from(u32::from(position.x as u16) << 16 | u32::from(position.y as u16));
|
||||
SAVED_CURSOR_POS.store(bits, Ordering::Relaxed);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Handle> for ScreenBufferCursor {
|
||||
fn from(handle: Handle) -> Self {
|
||||
ScreenBufferCursor {
|
||||
screen_buffer: ScreenBuffer::from(handle),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{
|
||||
move_down, move_left, move_right, move_to, move_to_column, move_to_next_line,
|
||||
move_to_previous_line, move_to_row, move_up, position, restore_position, save_position,
|
||||
};
|
||||
use crate::terminal::sys::temp_screen_buffer;
|
||||
use serial_test::serial;
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn test_move_to_winapi() {
|
||||
let _test_screen = temp_screen_buffer().unwrap();
|
||||
|
||||
let (saved_x, saved_y) = position().unwrap();
|
||||
|
||||
move_to(saved_x + 1, saved_y + 1).unwrap();
|
||||
assert_eq!(position().unwrap(), (saved_x + 1, saved_y + 1));
|
||||
|
||||
move_to(saved_x, saved_y).unwrap();
|
||||
assert_eq!(position().unwrap(), (saved_x, saved_y));
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn test_move_right_winapi() {
|
||||
let _test_screen = temp_screen_buffer().unwrap();
|
||||
|
||||
let (saved_x, saved_y) = position().unwrap();
|
||||
move_right(1).unwrap();
|
||||
assert_eq!(position().unwrap(), (saved_x + 1, saved_y));
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn test_move_left_winapi() {
|
||||
let _test_screen = temp_screen_buffer().unwrap();
|
||||
|
||||
move_to(2, 0).unwrap();
|
||||
|
||||
move_left(2).unwrap();
|
||||
|
||||
assert_eq!(position().unwrap(), (0, 0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn test_move_up_winapi() {
|
||||
let _test_screen = temp_screen_buffer().unwrap();
|
||||
|
||||
move_to(0, 2).unwrap();
|
||||
|
||||
move_up(2).unwrap();
|
||||
|
||||
assert_eq!(position().unwrap(), (0, 0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn test_move_to_next_line_winapi() {
|
||||
let _test_screen = temp_screen_buffer().unwrap();
|
||||
|
||||
move_to(0, 2).unwrap();
|
||||
|
||||
move_to_next_line(2).unwrap();
|
||||
|
||||
assert_eq!(position().unwrap(), (0, 4));
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn test_move_to_previous_line_winapi() {
|
||||
let _test_screen = temp_screen_buffer().unwrap();
|
||||
|
||||
move_to(0, 2).unwrap();
|
||||
|
||||
move_to_previous_line(2).unwrap();
|
||||
|
||||
assert_eq!(position().unwrap(), (0, 0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn test_move_to_column_winapi() {
|
||||
let _test_screen = temp_screen_buffer().unwrap();
|
||||
|
||||
move_to(0, 2).unwrap();
|
||||
|
||||
move_to_column(12).unwrap();
|
||||
|
||||
assert_eq!(position().unwrap(), (12, 2));
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn test_move_to_row_winapi() {
|
||||
let _test_screen = temp_screen_buffer().unwrap();
|
||||
|
||||
move_to(0, 2).unwrap();
|
||||
|
||||
move_to_row(5).unwrap();
|
||||
|
||||
assert_eq!(position().unwrap(), (0, 5));
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn test_move_down_winapi() {
|
||||
let _test_screen = temp_screen_buffer().unwrap();
|
||||
|
||||
move_to(0, 0).unwrap();
|
||||
|
||||
move_down(2).unwrap();
|
||||
|
||||
assert_eq!(position().unwrap(), (0, 2));
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn test_save_restore_position_winapi() {
|
||||
let _test_screen = temp_screen_buffer().unwrap();
|
||||
|
||||
let (saved_x, saved_y) = position().unwrap();
|
||||
|
||||
save_position().unwrap();
|
||||
move_to(saved_x + 1, saved_y + 1).unwrap();
|
||||
restore_position().unwrap();
|
||||
|
||||
let (x, y) = position().unwrap();
|
||||
|
||||
assert_eq!(x, saved_x);
|
||||
assert_eq!(y, saved_y);
|
||||
}
|
||||
}
|
349
src/event.rs
349
src/event.rs
@ -119,165 +119,18 @@
|
||||
//! them (`event-*`).
|
||||
|
||||
pub(crate) mod filter;
|
||||
pub(crate) mod read;
|
||||
pub(crate) mod source;
|
||||
#[cfg(feature = "event-stream")]
|
||||
pub(crate) mod stream;
|
||||
pub(crate) mod sys;
|
||||
pub(crate) mod timeout;
|
||||
|
||||
#[cfg(feature = "event-stream")]
|
||||
pub use stream::EventStream;
|
||||
|
||||
use crate::event::{
|
||||
filter::{EventFilter, Filter},
|
||||
read::InternalEventReader,
|
||||
timeout::PollTimeout,
|
||||
};
|
||||
use crate::{csi, Command};
|
||||
use parking_lot::{MappedMutexGuard, Mutex, MutexGuard};
|
||||
use std::fmt::{self, Display};
|
||||
use std::time::Duration;
|
||||
use std::{
|
||||
collections::VecDeque,
|
||||
fmt::{self, Display},
|
||||
};
|
||||
|
||||
use bitflags::bitflags;
|
||||
use std::hash::{Hash, Hasher};
|
||||
|
||||
/// Static instance of `InternalEventReader`.
|
||||
/// This needs to be static because there can be one event reader.
|
||||
static INTERNAL_EVENT_READER: Mutex<Option<InternalEventReader>> = parking_lot::const_mutex(None);
|
||||
|
||||
pub(crate) fn lock_internal_event_reader() -> MappedMutexGuard<'static, InternalEventReader> {
|
||||
MutexGuard::map(INTERNAL_EVENT_READER.lock(), |reader| {
|
||||
reader.get_or_insert_with(InternalEventReader::default)
|
||||
})
|
||||
}
|
||||
fn try_lock_internal_event_reader_for(
|
||||
duration: Duration,
|
||||
) -> Option<MappedMutexGuard<'static, InternalEventReader>> {
|
||||
Some(MutexGuard::map(
|
||||
INTERNAL_EVENT_READER.try_lock_for(duration)?,
|
||||
|reader| reader.get_or_insert_with(InternalEventReader::default),
|
||||
))
|
||||
}
|
||||
|
||||
/// Checks if there is an [`Event`](enum.Event.html) available.
|
||||
///
|
||||
/// Returns `Ok(true)` if an [`Event`](enum.Event.html) is available otherwise it returns `Ok(false)`.
|
||||
///
|
||||
/// `Ok(true)` guarantees that subsequent call to the [`read`](fn.read.html) function
|
||||
/// won't block.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `timeout` - maximum waiting time for event availability
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// Return immediately:
|
||||
///
|
||||
/// ```no_run
|
||||
/// use std::{time::Duration, io};
|
||||
/// use crossterm::{event::poll};
|
||||
///
|
||||
/// fn is_event_available() -> io::Result<bool> {
|
||||
/// // Zero duration says that the `poll` function must return immediately
|
||||
/// // with an `Event` availability information
|
||||
/// poll(Duration::from_secs(0))
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// Wait up to 100ms:
|
||||
///
|
||||
/// ```no_run
|
||||
/// use std::{time::Duration, io};
|
||||
///
|
||||
/// use crossterm::event::poll;
|
||||
///
|
||||
/// fn is_event_available() -> io::Result<bool> {
|
||||
/// // Wait for an `Event` availability for 100ms. It returns immediately
|
||||
/// // if an `Event` is/becomes available.
|
||||
/// poll(Duration::from_millis(100))
|
||||
/// }
|
||||
/// ```
|
||||
pub fn poll(timeout: Duration) -> std::io::Result<bool> {
|
||||
poll_internal(Some(timeout), &EventFilter)
|
||||
}
|
||||
|
||||
/// Reads a single [`Event`](enum.Event.html).
|
||||
///
|
||||
/// This function blocks until an [`Event`](enum.Event.html) is available. Combine it with the
|
||||
/// [`poll`](fn.poll.html) function to get non-blocking reads.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// Blocking read:
|
||||
///
|
||||
/// ```no_run
|
||||
/// use crossterm::event::read;
|
||||
/// use std::io;
|
||||
///
|
||||
/// fn print_events() -> io::Result<bool> {
|
||||
/// loop {
|
||||
/// // Blocks until an `Event` is available
|
||||
/// println!("{:?}", read()?);
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// Non-blocking read:
|
||||
///
|
||||
/// ```no_run
|
||||
/// use std::time::Duration;
|
||||
/// use std::io;
|
||||
///
|
||||
/// use crossterm::event::{read, poll};
|
||||
///
|
||||
/// fn print_events() -> io::Result<bool> {
|
||||
/// loop {
|
||||
/// if poll(Duration::from_millis(100))? {
|
||||
/// // It's guaranteed that `read` won't block, because `poll` returned
|
||||
/// // `Ok(true)`.
|
||||
/// println!("{:?}", read()?);
|
||||
/// } else {
|
||||
/// // Timeout expired, no `Event` is available
|
||||
/// }
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
pub fn read() -> std::io::Result<Event> {
|
||||
match read_internal(&EventFilter)? {
|
||||
InternalEvent::Event(event) => Ok(event),
|
||||
#[cfg(unix)]
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Polls to check if there are any `InternalEvent`s that can be read within the given duration.
|
||||
pub(crate) fn poll_internal<F>(timeout: Option<Duration>, filter: &F) -> std::io::Result<bool>
|
||||
where
|
||||
F: Filter,
|
||||
{
|
||||
let (mut reader, timeout) = if let Some(timeout) = timeout {
|
||||
let poll_timeout = PollTimeout::new(Some(timeout));
|
||||
if let Some(reader) = try_lock_internal_event_reader_for(timeout) {
|
||||
(reader, poll_timeout.leftover())
|
||||
} else {
|
||||
return Ok(false);
|
||||
}
|
||||
} else {
|
||||
(lock_internal_event_reader(), None)
|
||||
};
|
||||
reader.poll(timeout, filter)
|
||||
}
|
||||
|
||||
/// Reads a single `InternalEvent`.
|
||||
pub(crate) fn read_internal<F>(filter: &F) -> std::io::Result<InternalEvent>
|
||||
where
|
||||
F: Filter,
|
||||
{
|
||||
let mut reader = lock_internal_event_reader();
|
||||
reader.read(filter)
|
||||
}
|
||||
use self::sys::unix::parse::parse_event;
|
||||
|
||||
bitflags! {
|
||||
/// Represents special flags that tell compatible terminals to add extra information to keyboard events.
|
||||
@ -331,16 +184,6 @@ impl Command for EnableMouseCapture {
|
||||
csi!("?1006h"),
|
||||
))
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn execute_winapi(&self) -> std::io::Result<()> {
|
||||
sys::windows::enable_mouse_capture()
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn is_ansi_code_supported(&self) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// A command that disables mouse event capturing.
|
||||
@ -360,16 +203,6 @@ impl Command for DisableMouseCapture {
|
||||
csi!("?1000l"),
|
||||
))
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn execute_winapi(&self) -> std::io::Result<()> {
|
||||
sys::windows::disable_mouse_capture()
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn is_ansi_code_supported(&self) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// A command that enables focus event emission.
|
||||
@ -384,12 +217,6 @@ impl Command for EnableFocusChange {
|
||||
fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result {
|
||||
f.write_str(csi!("?1004h"))
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn execute_winapi(&self) -> std::io::Result<()> {
|
||||
// Focus events are always enabled on Windows
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// A command that disables focus event emission.
|
||||
@ -400,12 +227,6 @@ impl Command for DisableFocusChange {
|
||||
fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result {
|
||||
f.write_str(csi!("?1004l"))
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn execute_winapi(&self) -> std::io::Result<()> {
|
||||
// Focus events can't be disabled on Windows
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// A command that enables [bracketed paste mode](https://en.wikipedia.org/wiki/Bracketed-paste).
|
||||
@ -423,14 +244,6 @@ impl Command for EnableBracketedPaste {
|
||||
fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result {
|
||||
f.write_str(csi!("?2004h"))
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn execute_winapi(&self) -> std::io::Result<()> {
|
||||
Err(std::io::Error::new(
|
||||
std::io::ErrorKind::Unsupported,
|
||||
"Bracketed paste not implemented in the legacy Windows API.",
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
/// A command that disables bracketed paste mode.
|
||||
@ -443,11 +256,6 @@ impl Command for DisableBracketedPaste {
|
||||
fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result {
|
||||
f.write_str(csi!("?2004l"))
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn execute_winapi(&self) -> std::io::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// A command that enables the [kitty keyboard protocol](https://sw.kovidgoyal.net/kitty/keyboard-protocol/), which adds extra information to keyboard events and removes ambiguity for modifier keys.
|
||||
@ -494,21 +302,6 @@ impl Command for PushKeyboardEnhancementFlags {
|
||||
fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result {
|
||||
write!(f, "{}{}u", csi!(">"), self.0.bits())
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn execute_winapi(&self) -> std::io::Result<()> {
|
||||
use std::io;
|
||||
|
||||
Err(io::Error::new(
|
||||
io::ErrorKind::Unsupported,
|
||||
"Keyboard progressive enhancement not implemented for the legacy Windows API.",
|
||||
))
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn is_ansi_code_supported(&self) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// A command that disables extra kinds of keyboard events.
|
||||
@ -523,21 +316,6 @@ impl Command for PopKeyboardEnhancementFlags {
|
||||
fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result {
|
||||
f.write_str(csi!("<1u"))
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn execute_winapi(&self) -> std::io::Result<()> {
|
||||
use std::io;
|
||||
|
||||
Err(io::Error::new(
|
||||
io::ErrorKind::Unsupported,
|
||||
"Keyboard progressive enhancement not implemented for the legacy Windows API.",
|
||||
))
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn is_ansi_code_supported(&self) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents an event.
|
||||
@ -560,6 +338,12 @@ pub enum Event {
|
||||
/// An resize event with new dimensions after resize (columns, rows).
|
||||
/// **Note** that resize events can occur in batches.
|
||||
Resize(u16, u16),
|
||||
/// A cursor position (`col`, `row`).
|
||||
CursorPosition(u16, u16),
|
||||
/// The progressive keyboard enhancement flags enabled by the terminal.
|
||||
KeyboardEnhancementFlags(KeyboardEnhancementFlags),
|
||||
/// Attributes and architectural class of the terminal.
|
||||
PrimaryDeviceAttributes,
|
||||
}
|
||||
|
||||
/// Represents a mouse event.
|
||||
@ -677,19 +461,8 @@ impl Display for KeyModifiers {
|
||||
}
|
||||
match modifier {
|
||||
KeyModifiers::SHIFT => f.write_str("Shift")?,
|
||||
#[cfg(unix)]
|
||||
KeyModifiers::CONTROL => f.write_str("Control")?,
|
||||
#[cfg(windows)]
|
||||
KeyModifiers::CONTROL => f.write_str("Ctrl")?,
|
||||
#[cfg(target_os = "macos")]
|
||||
KeyModifiers::ALT => f.write_str("Option")?,
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
KeyModifiers::ALT => f.write_str("Alt")?,
|
||||
#[cfg(target_os = "macos")]
|
||||
KeyModifiers::SUPER => f.write_str("Command")?,
|
||||
#[cfg(target_os = "windows")]
|
||||
KeyModifiers::SUPER => f.write_str("Windows")?,
|
||||
#[cfg(not(any(target_os = "macos", target_os = "windows")))]
|
||||
KeyModifiers::SUPER => f.write_str("Super")?,
|
||||
KeyModifiers::HYPER => f.write_str("Hyper")?,
|
||||
KeyModifiers::META => f.write_str("Meta")?,
|
||||
@ -1119,20 +892,9 @@ impl Display for KeyCode {
|
||||
/// displayed as "Del", and the Enter key is displayed as "Enter".
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
// On macOS, the Backspace key is called "Delete" and the Delete key is called "Fwd Del".
|
||||
#[cfg(target_os = "macos")]
|
||||
KeyCode::Backspace => write!(f, "Delete"),
|
||||
#[cfg(target_os = "macos")]
|
||||
KeyCode::Delete => write!(f, "Fwd Del"),
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
KeyCode::Backspace => write!(f, "Backspace"),
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
KeyCode::Delete => write!(f, "Del"),
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
KeyCode::Enter => write!(f, "Return"),
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
KeyCode::Enter => write!(f, "Enter"),
|
||||
KeyCode::Left => write!(f, "Left"),
|
||||
KeyCode::Right => write!(f, "Right"),
|
||||
@ -1166,25 +928,6 @@ impl Display for KeyCode {
|
||||
}
|
||||
}
|
||||
|
||||
/// An internal event.
|
||||
///
|
||||
/// Encapsulates publicly available `Event` with additional internal
|
||||
/// events that shouldn't be publicly available to the crate users.
|
||||
#[derive(Debug, PartialOrd, PartialEq, Hash, Clone, Eq)]
|
||||
pub(crate) enum InternalEvent {
|
||||
/// An event.
|
||||
Event(Event),
|
||||
/// A cursor position (`col`, `row`).
|
||||
#[cfg(unix)]
|
||||
CursorPosition(u16, u16),
|
||||
/// The progressive keyboard enhancement flags enabled by the terminal.
|
||||
#[cfg(unix)]
|
||||
KeyboardEnhancementFlags(KeyboardEnhancementFlags),
|
||||
/// Attributes and architectural class of the terminal.
|
||||
#[cfg(unix)]
|
||||
PrimaryDeviceAttributes,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::collections::hash_map::DefaultHasher;
|
||||
@ -1227,18 +970,9 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn keycode_display() {
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
assert_eq!(format!("{}", Backspace), "Delete");
|
||||
assert_eq!(format!("{}", Delete), "Fwd Del");
|
||||
assert_eq!(format!("{}", Enter), "Return");
|
||||
}
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
{
|
||||
assert_eq!(format!("{}", Backspace), "Backspace");
|
||||
assert_eq!(format!("{}", Delete), "Del");
|
||||
assert_eq!(format!("{}", Enter), "Enter");
|
||||
}
|
||||
assert_eq!(format!("{}", Backspace), "Backspace");
|
||||
assert_eq!(format!("{}", Delete), "Del");
|
||||
assert_eq!(format!("{}", Enter), "Enter");
|
||||
assert_eq!(format!("{}", Left), "Left");
|
||||
assert_eq!(format!("{}", Right), "Right");
|
||||
assert_eq!(format!("{}", Up), "Up");
|
||||
@ -1292,29 +1026,6 @@ mod tests {
|
||||
assert_eq!(format!("{}", Modifier(IsoLevel5Shift)), "Iso Level 5 Shift");
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
#[test]
|
||||
fn modifier_keycode_display_macos() {
|
||||
assert_eq!(format!("{}", Modifier(LeftControl)), "Left Control");
|
||||
assert_eq!(format!("{}", Modifier(LeftAlt)), "Left Option");
|
||||
assert_eq!(format!("{}", Modifier(LeftSuper)), "Left Command");
|
||||
assert_eq!(format!("{}", Modifier(RightControl)), "Right Control");
|
||||
assert_eq!(format!("{}", Modifier(RightAlt)), "Right Option");
|
||||
assert_eq!(format!("{}", Modifier(RightSuper)), "Right Command");
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
#[test]
|
||||
fn modifier_keycode_display_windows() {
|
||||
assert_eq!(format!("{}", Modifier(LeftControl)), "Left Ctrl");
|
||||
assert_eq!(format!("{}", Modifier(LeftAlt)), "Left Alt");
|
||||
assert_eq!(format!("{}", Modifier(LeftSuper)), "Left Windows");
|
||||
assert_eq!(format!("{}", Modifier(RightControl)), "Right Ctrl");
|
||||
assert_eq!(format!("{}", Modifier(RightAlt)), "Right Alt");
|
||||
assert_eq!(format!("{}", Modifier(RightSuper)), "Right Windows");
|
||||
}
|
||||
|
||||
#[cfg(not(any(target_os = "macos", target_os = "windows")))]
|
||||
#[test]
|
||||
fn modifier_keycode_display_other() {
|
||||
assert_eq!(format!("{}", Modifier(LeftControl)), "Left Ctrl");
|
||||
@ -1325,3 +1036,35 @@ mod tests {
|
||||
assert_eq!(format!("{}", Modifier(RightSuper)), "Right Super");
|
||||
}
|
||||
}
|
||||
|
||||
pub struct TerminalState {
|
||||
char_buffer: VecDeque<u8>,
|
||||
}
|
||||
|
||||
impl TerminalState {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
char_buffer: VecDeque::<u8>::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn events_for_input(&mut self, input: &[u8]) -> std::io::Result<Vec<Event>> {
|
||||
let mut result: Vec<Event> = Vec::new();
|
||||
|
||||
self.char_buffer.extend(input);
|
||||
let mut buf: Vec<u8> = Vec::new();
|
||||
|
||||
loop {
|
||||
match self.char_buffer.pop_front() {
|
||||
None => return Ok(result),
|
||||
Some(c) => {
|
||||
buf.push(c);
|
||||
match parse_event(&buf, true)? {
|
||||
None => continue,
|
||||
Some(e) => result.push(e),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,48 +1,42 @@
|
||||
use crate::event::InternalEvent;
|
||||
use crate::event::Event;
|
||||
|
||||
/// Interface for filtering an `InternalEvent`.
|
||||
pub(crate) trait Filter: Send + Sync + 'static {
|
||||
/// Returns whether the given event fulfills the filter.
|
||||
fn eval(&self, event: &InternalEvent) -> bool;
|
||||
fn eval(&self, event: &Event) -> bool;
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct CursorPositionFilter;
|
||||
|
||||
#[cfg(unix)]
|
||||
impl Filter for CursorPositionFilter {
|
||||
fn eval(&self, event: &InternalEvent) -> bool {
|
||||
matches!(*event, InternalEvent::CursorPosition(_, _))
|
||||
fn eval(&self, event: &Event) -> bool {
|
||||
matches!(*event, Event::CursorPosition(_, _))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct KeyboardEnhancementFlagsFilter;
|
||||
|
||||
#[cfg(unix)]
|
||||
impl Filter for KeyboardEnhancementFlagsFilter {
|
||||
fn eval(&self, event: &InternalEvent) -> bool {
|
||||
fn eval(&self, event: &Event) -> bool {
|
||||
// This filter checks for either a KeyboardEnhancementFlags response or
|
||||
// a PrimaryDeviceAttributes response. If we receive the PrimaryDeviceAttributes
|
||||
// response but not KeyboardEnhancementFlags, the terminal does not support
|
||||
// progressive keyboard enhancement.
|
||||
matches!(
|
||||
*event,
|
||||
InternalEvent::KeyboardEnhancementFlags(_) | InternalEvent::PrimaryDeviceAttributes
|
||||
Event::KeyboardEnhancementFlags(_) | Event::PrimaryDeviceAttributes
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct PrimaryDeviceAttributesFilter;
|
||||
|
||||
#[cfg(unix)]
|
||||
impl Filter for PrimaryDeviceAttributesFilter {
|
||||
fn eval(&self, event: &InternalEvent) -> bool {
|
||||
matches!(*event, InternalEvent::PrimaryDeviceAttributes)
|
||||
fn eval(&self, event: &Event) -> bool {
|
||||
matches!(*event, Event::PrimaryDeviceAttributes)
|
||||
}
|
||||
}
|
||||
|
||||
@ -50,66 +44,50 @@ impl Filter for PrimaryDeviceAttributesFilter {
|
||||
pub(crate) struct EventFilter;
|
||||
|
||||
impl Filter for EventFilter {
|
||||
#[cfg(unix)]
|
||||
fn eval(&self, event: &InternalEvent) -> bool {
|
||||
matches!(*event, InternalEvent::Event(_))
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn eval(&self, _: &InternalEvent) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct InternalEventFilter;
|
||||
|
||||
impl Filter for InternalEventFilter {
|
||||
fn eval(&self, _: &InternalEvent) -> bool {
|
||||
fn eval(&self, _: &Event) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[cfg(unix)]
|
||||
mod tests {
|
||||
use super::{
|
||||
super::Event, CursorPositionFilter, EventFilter, Filter, InternalEvent,
|
||||
InternalEventFilter, KeyboardEnhancementFlagsFilter, PrimaryDeviceAttributesFilter,
|
||||
super::Event, CursorPositionFilter, Event, EventFilter, Filter, InternalEventFilter,
|
||||
KeyboardEnhancementFlagsFilter, PrimaryDeviceAttributesFilter,
|
||||
};
|
||||
|
||||
#[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)));
|
||||
assert!(!CursorPositionFilter.eval(&Event::Event(Event::Resize(10, 10))));
|
||||
assert!(CursorPositionFilter.eval(&Event::CursorPosition(0, 0)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_keyboard_enhancement_status_filter_filters_keyboard_enhancement_status() {
|
||||
assert!(!KeyboardEnhancementFlagsFilter.eval(&InternalEvent::Event(Event::Resize(10, 10))));
|
||||
assert!(!KeyboardEnhancementFlagsFilter.eval(&Event::Event(Event::Resize(10, 10))));
|
||||
assert!(
|
||||
KeyboardEnhancementFlagsFilter.eval(&InternalEvent::KeyboardEnhancementFlags(
|
||||
KeyboardEnhancementFlagsFilter.eval(&Event::KeyboardEnhancementFlags(
|
||||
crate::event::KeyboardEnhancementFlags::DISAMBIGUATE_ESCAPE_CODES
|
||||
))
|
||||
);
|
||||
assert!(KeyboardEnhancementFlagsFilter.eval(&InternalEvent::PrimaryDeviceAttributes));
|
||||
assert!(KeyboardEnhancementFlagsFilter.eval(&Event::PrimaryDeviceAttributes));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_primary_device_attributes_filter_filters_primary_device_attributes() {
|
||||
assert!(!PrimaryDeviceAttributesFilter.eval(&InternalEvent::Event(Event::Resize(10, 10))));
|
||||
assert!(PrimaryDeviceAttributesFilter.eval(&InternalEvent::PrimaryDeviceAttributes));
|
||||
assert!(!PrimaryDeviceAttributesFilter.eval(&Event::Event(Event::Resize(10, 10))));
|
||||
assert!(PrimaryDeviceAttributesFilter.eval(&Event::PrimaryDeviceAttributes));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_event_filter_filters_events() {
|
||||
assert!(EventFilter.eval(&InternalEvent::Event(Event::Resize(10, 10))));
|
||||
assert!(!EventFilter.eval(&InternalEvent::CursorPosition(0, 0)));
|
||||
assert!(EventFilter.eval(&Event::Event(Event::Resize(10, 10))));
|
||||
assert!(!EventFilter.eval(&Event::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)));
|
||||
assert!(InternalEventFilter.eval(&Event::Event(Event::Resize(10, 10))));
|
||||
assert!(InternalEventFilter.eval(&Event::CursorPosition(0, 0)));
|
||||
}
|
||||
}
|
||||
|
@ -1,430 +0,0 @@
|
||||
use std::{collections::vec_deque::VecDeque, io, time::Duration};
|
||||
|
||||
#[cfg(unix)]
|
||||
use crate::event::source::unix::UnixInternalEventSource;
|
||||
#[cfg(windows)]
|
||||
use crate::event::source::windows::WindowsEventSource;
|
||||
#[cfg(feature = "event-stream")]
|
||||
use crate::event::sys::Waker;
|
||||
use crate::event::{filter::Filter, source::EventSource, timeout::PollTimeout, InternalEvent};
|
||||
|
||||
/// Can be used to read `InternalEvent`s.
|
||||
pub(crate) struct InternalEventReader {
|
||||
events: VecDeque<InternalEvent>,
|
||||
source: Option<Box<dyn EventSource>>,
|
||||
skipped_events: Vec<InternalEvent>,
|
||||
}
|
||||
|
||||
impl Default for InternalEventReader {
|
||||
fn default() -> Self {
|
||||
#[cfg(windows)]
|
||||
let source = WindowsEventSource::new();
|
||||
#[cfg(unix)]
|
||||
let source = UnixInternalEventSource::new();
|
||||
|
||||
let source = source.ok().map(|x| Box::new(x) as Box<dyn EventSource>);
|
||||
|
||||
InternalEventReader {
|
||||
source,
|
||||
events: VecDeque::with_capacity(32),
|
||||
skipped_events: Vec::with_capacity(32),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl InternalEventReader {
|
||||
/// Returns a `Waker` allowing to wake/force the `poll` method to return `Ok(false)`.
|
||||
#[cfg(feature = "event-stream")]
|
||||
pub(crate) fn waker(&self) -> Waker {
|
||||
self.source.as_ref().expect("reader source not set").waker()
|
||||
}
|
||||
|
||||
pub(crate) fn poll<F>(&mut self, timeout: Option<Duration>, filter: &F) -> io::Result<bool>
|
||||
where
|
||||
F: Filter,
|
||||
{
|
||||
for event in &self.events {
|
||||
if filter.eval(event) {
|
||||
return Ok(true);
|
||||
}
|
||||
}
|
||||
|
||||
let event_source = match self.source.as_mut() {
|
||||
Some(source) => source,
|
||||
None => {
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::Other,
|
||||
"Failed to initialize input reader",
|
||||
))
|
||||
}
|
||||
};
|
||||
|
||||
let poll_timeout = PollTimeout::new(timeout);
|
||||
|
||||
loop {
|
||||
let maybe_event = match event_source.try_read(poll_timeout.leftover()) {
|
||||
Ok(None) => None,
|
||||
Ok(Some(event)) => {
|
||||
if filter.eval(&event) {
|
||||
Some(event)
|
||||
} else {
|
||||
self.skipped_events.push(event);
|
||||
None
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
if e.kind() == io::ErrorKind::Interrupted {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
return Err(e);
|
||||
}
|
||||
};
|
||||
|
||||
if poll_timeout.elapsed() || maybe_event.is_some() {
|
||||
self.events.extend(self.skipped_events.drain(..));
|
||||
|
||||
if let Some(event) = maybe_event {
|
||||
self.events.push_front(event);
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
return Ok(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn read<F>(&mut self, filter: &F) -> io::Result<InternalEvent>
|
||||
where
|
||||
F: Filter,
|
||||
{
|
||||
let mut skipped_events = VecDeque::new();
|
||||
|
||||
loop {
|
||||
while let Some(event) = self.events.pop_front() {
|
||||
if filter.eval(&event) {
|
||||
while let Some(event) = skipped_events.pop_front() {
|
||||
self.events.push_back(event);
|
||||
}
|
||||
|
||||
return Ok(event);
|
||||
} else {
|
||||
// We can not directly write events back to `self.events`.
|
||||
// If we did, we would put our self's into an endless loop
|
||||
// that would enqueue -> dequeue -> enqueue etc.
|
||||
// This happens because `poll` in this function will always return true if there are events in it's.
|
||||
// And because we just put the non-fulfilling event there this is going to be the case.
|
||||
// Instead we can store them into the temporary buffer,
|
||||
// and then when the filter is fulfilled write all events back in order.
|
||||
skipped_events.push_back(event);
|
||||
}
|
||||
}
|
||||
|
||||
let _ = self.poll(None, filter)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::io;
|
||||
use std::{collections::VecDeque, time::Duration};
|
||||
|
||||
#[cfg(unix)]
|
||||
use super::super::filter::CursorPositionFilter;
|
||||
use super::{
|
||||
super::{filter::InternalEventFilter, Event},
|
||||
EventSource, InternalEvent, InternalEventReader,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn test_poll_fails_without_event_source() {
|
||||
let mut reader = InternalEventReader {
|
||||
events: VecDeque::new(),
|
||||
source: None,
|
||||
skipped_events: Vec::with_capacity(32),
|
||||
};
|
||||
|
||||
assert!(reader.poll(None, &InternalEventFilter).is_err());
|
||||
assert!(reader
|
||||
.poll(Some(Duration::from_secs(0)), &InternalEventFilter)
|
||||
.is_err());
|
||||
assert!(reader
|
||||
.poll(Some(Duration::from_secs(10)), &InternalEventFilter)
|
||||
.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_poll_returns_true_for_matching_event_in_queue_at_front() {
|
||||
let mut reader = InternalEventReader {
|
||||
events: vec![InternalEvent::Event(Event::Resize(10, 10))].into(),
|
||||
source: None,
|
||||
skipped_events: Vec::with_capacity(32),
|
||||
};
|
||||
|
||||
assert!(reader.poll(None, &InternalEventFilter).unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(unix)]
|
||||
fn test_poll_returns_true_for_matching_event_in_queue_at_back() {
|
||||
let mut reader = InternalEventReader {
|
||||
events: vec![
|
||||
InternalEvent::Event(Event::Resize(10, 10)),
|
||||
InternalEvent::CursorPosition(10, 20),
|
||||
]
|
||||
.into(),
|
||||
source: None,
|
||||
skipped_events: Vec::with_capacity(32),
|
||||
};
|
||||
|
||||
assert!(reader.poll(None, &CursorPositionFilter).unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_read_returns_matching_event_in_queue_at_front() {
|
||||
const EVENT: InternalEvent = InternalEvent::Event(Event::Resize(10, 10));
|
||||
|
||||
let mut reader = InternalEventReader {
|
||||
events: vec![EVENT].into(),
|
||||
source: None,
|
||||
skipped_events: Vec::with_capacity(32),
|
||||
};
|
||||
|
||||
assert_eq!(reader.read(&InternalEventFilter).unwrap(), EVENT);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(unix)]
|
||||
fn test_read_returns_matching_event_in_queue_at_back() {
|
||||
const CURSOR_EVENT: InternalEvent = InternalEvent::CursorPosition(10, 20);
|
||||
|
||||
let mut reader = InternalEventReader {
|
||||
events: vec![InternalEvent::Event(Event::Resize(10, 10)), CURSOR_EVENT].into(),
|
||||
source: None,
|
||||
skipped_events: Vec::with_capacity(32),
|
||||
};
|
||||
|
||||
assert_eq!(reader.read(&CursorPositionFilter).unwrap(), CURSOR_EVENT);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(unix)]
|
||||
fn test_read_does_not_consume_skipped_event() {
|
||||
const SKIPPED_EVENT: InternalEvent = InternalEvent::Event(Event::Resize(10, 10));
|
||||
const CURSOR_EVENT: InternalEvent = InternalEvent::CursorPosition(10, 20);
|
||||
|
||||
let mut reader = InternalEventReader {
|
||||
events: vec![SKIPPED_EVENT, CURSOR_EVENT].into(),
|
||||
source: None,
|
||||
skipped_events: Vec::with_capacity(32),
|
||||
};
|
||||
|
||||
assert_eq!(reader.read(&CursorPositionFilter).unwrap(), CURSOR_EVENT);
|
||||
assert_eq!(reader.read(&InternalEventFilter).unwrap(), SKIPPED_EVENT);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_poll_timeouts_if_source_has_no_events() {
|
||||
let source = FakeSource::default();
|
||||
|
||||
let mut reader = InternalEventReader {
|
||||
events: VecDeque::new(),
|
||||
source: Some(Box::new(source)),
|
||||
skipped_events: Vec::with_capacity(32),
|
||||
};
|
||||
|
||||
assert!(!reader
|
||||
.poll(Some(Duration::from_secs(0)), &InternalEventFilter)
|
||||
.unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_poll_returns_true_if_source_has_at_least_one_event() {
|
||||
let source = FakeSource::with_events(&[InternalEvent::Event(Event::Resize(10, 10))]);
|
||||
|
||||
let mut reader = InternalEventReader {
|
||||
events: VecDeque::new(),
|
||||
source: Some(Box::new(source)),
|
||||
skipped_events: Vec::with_capacity(32),
|
||||
};
|
||||
|
||||
assert!(reader.poll(None, &InternalEventFilter).unwrap());
|
||||
assert!(reader
|
||||
.poll(Some(Duration::from_secs(0)), &InternalEventFilter)
|
||||
.unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_reads_returns_event_if_source_has_at_least_one_event() {
|
||||
const EVENT: InternalEvent = InternalEvent::Event(Event::Resize(10, 10));
|
||||
|
||||
let source = FakeSource::with_events(&[EVENT]);
|
||||
|
||||
let mut reader = InternalEventReader {
|
||||
events: VecDeque::new(),
|
||||
source: Some(Box::new(source)),
|
||||
skipped_events: Vec::with_capacity(32),
|
||||
};
|
||||
|
||||
assert_eq!(reader.read(&InternalEventFilter).unwrap(), EVENT);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_read_returns_events_if_source_has_events() {
|
||||
const EVENT: InternalEvent = InternalEvent::Event(Event::Resize(10, 10));
|
||||
|
||||
let source = FakeSource::with_events(&[EVENT, EVENT, EVENT]);
|
||||
|
||||
let mut reader = InternalEventReader {
|
||||
events: VecDeque::new(),
|
||||
source: Some(Box::new(source)),
|
||||
skipped_events: Vec::with_capacity(32),
|
||||
};
|
||||
|
||||
assert_eq!(reader.read(&InternalEventFilter).unwrap(), EVENT);
|
||||
assert_eq!(reader.read(&InternalEventFilter).unwrap(), EVENT);
|
||||
assert_eq!(reader.read(&InternalEventFilter).unwrap(), EVENT);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_poll_returns_false_after_all_source_events_are_consumed() {
|
||||
const EVENT: InternalEvent = InternalEvent::Event(Event::Resize(10, 10));
|
||||
|
||||
let source = FakeSource::with_events(&[EVENT, EVENT, EVENT]);
|
||||
|
||||
let mut reader = InternalEventReader {
|
||||
events: VecDeque::new(),
|
||||
source: Some(Box::new(source)),
|
||||
skipped_events: Vec::with_capacity(32),
|
||||
};
|
||||
|
||||
assert_eq!(reader.read(&InternalEventFilter).unwrap(), EVENT);
|
||||
assert_eq!(reader.read(&InternalEventFilter).unwrap(), EVENT);
|
||||
assert_eq!(reader.read(&InternalEventFilter).unwrap(), EVENT);
|
||||
assert!(!reader
|
||||
.poll(Some(Duration::from_secs(0)), &InternalEventFilter)
|
||||
.unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_poll_propagates_error() {
|
||||
let mut reader = InternalEventReader {
|
||||
events: VecDeque::new(),
|
||||
source: Some(Box::new(FakeSource::new(&[]))),
|
||||
skipped_events: Vec::with_capacity(32),
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
reader
|
||||
.poll(Some(Duration::from_secs(0)), &InternalEventFilter)
|
||||
.err()
|
||||
.map(|e| format!("{:?}", &e.kind())),
|
||||
Some(format!("{:?}", io::ErrorKind::Other))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_read_propagates_error() {
|
||||
let mut reader = InternalEventReader {
|
||||
events: VecDeque::new(),
|
||||
source: Some(Box::new(FakeSource::new(&[]))),
|
||||
skipped_events: Vec::with_capacity(32),
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
reader
|
||||
.read(&InternalEventFilter)
|
||||
.err()
|
||||
.map(|e| format!("{:?}", &e.kind())),
|
||||
Some(format!("{:?}", io::ErrorKind::Other))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_poll_continues_after_error() {
|
||||
const EVENT: InternalEvent = InternalEvent::Event(Event::Resize(10, 10));
|
||||
|
||||
let source = FakeSource::new(&[EVENT, EVENT]);
|
||||
|
||||
let mut reader = InternalEventReader {
|
||||
events: VecDeque::new(),
|
||||
source: Some(Box::new(source)),
|
||||
skipped_events: Vec::with_capacity(32),
|
||||
};
|
||||
|
||||
assert_eq!(reader.read(&InternalEventFilter).unwrap(), EVENT);
|
||||
assert!(reader.read(&InternalEventFilter).is_err());
|
||||
assert!(reader
|
||||
.poll(Some(Duration::from_secs(0)), &InternalEventFilter)
|
||||
.unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_read_continues_after_error() {
|
||||
const EVENT: InternalEvent = InternalEvent::Event(Event::Resize(10, 10));
|
||||
|
||||
let source = FakeSource::new(&[EVENT, EVENT]);
|
||||
|
||||
let mut reader = InternalEventReader {
|
||||
events: VecDeque::new(),
|
||||
source: Some(Box::new(source)),
|
||||
skipped_events: Vec::with_capacity(32),
|
||||
};
|
||||
|
||||
assert_eq!(reader.read(&InternalEventFilter).unwrap(), EVENT);
|
||||
assert!(reader.read(&InternalEventFilter).is_err());
|
||||
assert_eq!(reader.read(&InternalEventFilter).unwrap(), EVENT);
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct FakeSource {
|
||||
events: VecDeque<InternalEvent>,
|
||||
error: Option<io::Error>,
|
||||
}
|
||||
|
||||
impl FakeSource {
|
||||
fn new(events: &[InternalEvent]) -> FakeSource {
|
||||
FakeSource {
|
||||
events: events.to_vec().into(),
|
||||
error: Some(io::Error::new(io::ErrorKind::Other, "")),
|
||||
}
|
||||
}
|
||||
|
||||
fn with_events(events: &[InternalEvent]) -> FakeSource {
|
||||
FakeSource {
|
||||
events: events.to_vec().into(),
|
||||
error: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl EventSource for FakeSource {
|
||||
fn try_read(&mut self, _timeout: Option<Duration>) -> io::Result<Option<InternalEvent>> {
|
||||
// Return error if set in case there's just one remaining event
|
||||
if self.events.len() == 1 {
|
||||
if let Some(error) = self.error.take() {
|
||||
return Err(error);
|
||||
}
|
||||
}
|
||||
|
||||
// Return all events from the queue
|
||||
if let Some(event) = self.events.pop_front() {
|
||||
return Ok(Some(event));
|
||||
}
|
||||
|
||||
// Return error if there're no more events
|
||||
if let Some(error) = self.error.take() {
|
||||
return Err(error);
|
||||
}
|
||||
|
||||
// Timeout
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
#[cfg(feature = "event-stream")]
|
||||
fn waker(&self) -> super::super::sys::Waker {
|
||||
unimplemented!();
|
||||
}
|
||||
}
|
||||
}
|
@ -1,27 +0,0 @@
|
||||
use std::{io, time::Duration};
|
||||
|
||||
#[cfg(feature = "event-stream")]
|
||||
use super::sys::Waker;
|
||||
use super::InternalEvent;
|
||||
|
||||
#[cfg(unix)]
|
||||
pub(crate) mod unix;
|
||||
#[cfg(windows)]
|
||||
pub(crate) mod windows;
|
||||
|
||||
/// An interface for trying to read an `InternalEvent` within an optional `Duration`.
|
||||
pub(crate) trait EventSource: Sync + Send {
|
||||
/// Tries to read an `InternalEvent` within the given duration.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `timeout` - `None` block indefinitely until an event is available, `Some(duration)` blocks
|
||||
/// for the given timeout
|
||||
///
|
||||
/// Returns `Ok(None)` if there's no event available and timeout expires.
|
||||
fn try_read(&mut self, timeout: Option<Duration>) -> io::Result<Option<InternalEvent>>;
|
||||
|
||||
/// Returns a `Waker` allowing to wake/force the `try_read` method to return `Ok(None)`.
|
||||
#[cfg(feature = "event-stream")]
|
||||
fn waker(&self) -> Waker;
|
||||
}
|
@ -1,11 +0,0 @@
|
||||
#[cfg(feature = "use-dev-tty")]
|
||||
pub(crate) mod tty;
|
||||
|
||||
#[cfg(not(feature = "use-dev-tty"))]
|
||||
pub(crate) mod mio;
|
||||
|
||||
#[cfg(feature = "use-dev-tty")]
|
||||
pub(crate) use self::tty::UnixInternalEventSource;
|
||||
|
||||
#[cfg(not(feature = "use-dev-tty"))]
|
||||
pub(crate) use self::mio::UnixInternalEventSource;
|
@ -1,229 +0,0 @@
|
||||
use std::{collections::VecDeque, io, time::Duration};
|
||||
|
||||
use mio::{unix::SourceFd, Events, Interest, Poll, Token};
|
||||
use signal_hook_mio::v1_0::Signals;
|
||||
|
||||
#[cfg(feature = "event-stream")]
|
||||
use crate::event::sys::Waker;
|
||||
use crate::event::{
|
||||
source::EventSource, sys::unix::parse::parse_event, timeout::PollTimeout, Event, InternalEvent,
|
||||
};
|
||||
use crate::terminal::sys::file_descriptor::{tty_fd, FileDesc};
|
||||
|
||||
// Tokens to identify file descriptor
|
||||
const TTY_TOKEN: Token = Token(0);
|
||||
const SIGNAL_TOKEN: Token = Token(1);
|
||||
#[cfg(feature = "event-stream")]
|
||||
const WAKE_TOKEN: Token = Token(2);
|
||||
|
||||
// I (@zrzka) wasn't able to read more than 1_022 bytes when testing
|
||||
// reading on macOS/Linux -> we don't need bigger buffer and 1k of bytes
|
||||
// is enough.
|
||||
const TTY_BUFFER_SIZE: usize = 1_024;
|
||||
|
||||
pub(crate) struct UnixInternalEventSource {
|
||||
poll: Poll,
|
||||
events: Events,
|
||||
parser: Parser,
|
||||
tty_buffer: [u8; TTY_BUFFER_SIZE],
|
||||
tty_fd: FileDesc<'static>,
|
||||
signals: Signals,
|
||||
#[cfg(feature = "event-stream")]
|
||||
waker: Waker,
|
||||
}
|
||||
|
||||
impl UnixInternalEventSource {
|
||||
pub fn new() -> io::Result<Self> {
|
||||
UnixInternalEventSource::from_file_descriptor(tty_fd()?)
|
||||
}
|
||||
|
||||
pub(crate) fn from_file_descriptor(input_fd: FileDesc<'static>) -> io::Result<Self> {
|
||||
let poll = Poll::new()?;
|
||||
let registry = poll.registry();
|
||||
|
||||
let tty_raw_fd = input_fd.raw_fd();
|
||||
let mut tty_ev = SourceFd(&tty_raw_fd);
|
||||
registry.register(&mut tty_ev, TTY_TOKEN, Interest::READABLE)?;
|
||||
|
||||
let mut signals = Signals::new([signal_hook::consts::SIGWINCH])?;
|
||||
registry.register(&mut signals, SIGNAL_TOKEN, Interest::READABLE)?;
|
||||
|
||||
#[cfg(feature = "event-stream")]
|
||||
let waker = Waker::new(registry, WAKE_TOKEN)?;
|
||||
|
||||
Ok(UnixInternalEventSource {
|
||||
poll,
|
||||
events: Events::with_capacity(3),
|
||||
parser: Parser::default(),
|
||||
tty_buffer: [0u8; TTY_BUFFER_SIZE],
|
||||
tty_fd: input_fd,
|
||||
signals,
|
||||
#[cfg(feature = "event-stream")]
|
||||
waker,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl EventSource for UnixInternalEventSource {
|
||||
fn try_read(&mut self, timeout: Option<Duration>) -> io::Result<Option<InternalEvent>> {
|
||||
if let Some(event) = self.parser.next() {
|
||||
return Ok(Some(event));
|
||||
}
|
||||
|
||||
let timeout = PollTimeout::new(timeout);
|
||||
|
||||
loop {
|
||||
if let Err(e) = self.poll.poll(&mut self.events, timeout.leftover()) {
|
||||
// Mio will throw an interrupted error in case of cursor position retrieval. We need to retry until it succeeds.
|
||||
// Previous versions of Mio (< 0.7) would automatically retry the poll call if it was interrupted (if EINTR was returned).
|
||||
// https://docs.rs/mio/0.7.0/mio/struct.Poll.html#notes
|
||||
if e.kind() == io::ErrorKind::Interrupted {
|
||||
continue;
|
||||
} else {
|
||||
return Err(e);
|
||||
}
|
||||
};
|
||||
|
||||
if self.events.is_empty() {
|
||||
// No readiness events = timeout
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
for token in self.events.iter().map(|x| x.token()) {
|
||||
match token {
|
||||
TTY_TOKEN => {
|
||||
loop {
|
||||
match self.tty_fd.read(&mut self.tty_buffer) {
|
||||
Ok(read_count) => {
|
||||
if read_count > 0 {
|
||||
self.parser.advance(
|
||||
&self.tty_buffer[..read_count],
|
||||
read_count == TTY_BUFFER_SIZE,
|
||||
);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
// No more data to read at the moment. We will receive another event
|
||||
if e.kind() == io::ErrorKind::WouldBlock {
|
||||
break;
|
||||
}
|
||||
// once more data is available to read.
|
||||
else if e.kind() == io::ErrorKind::Interrupted {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(event) = self.parser.next() {
|
||||
return Ok(Some(event));
|
||||
}
|
||||
}
|
||||
}
|
||||
SIGNAL_TOKEN => {
|
||||
if self.signals.pending().next() == Some(signal_hook::consts::SIGWINCH) {
|
||||
// TODO Should we remove tput?
|
||||
//
|
||||
// This can take a really long time, because terminal::size can
|
||||
// launch new process (tput) and then it parses its output. It's
|
||||
// not a really long time from the absolute time point of view, but
|
||||
// it's a really long time from the mio, async-std/tokio executor, ...
|
||||
// point of view.
|
||||
let new_size = crate::terminal::size()?;
|
||||
return Ok(Some(InternalEvent::Event(Event::Resize(
|
||||
new_size.0, new_size.1,
|
||||
))));
|
||||
}
|
||||
}
|
||||
#[cfg(feature = "event-stream")]
|
||||
WAKE_TOKEN => {
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::Interrupted,
|
||||
"Poll operation was woken up by `Waker::wake`",
|
||||
));
|
||||
}
|
||||
_ => unreachable!("Synchronize Evented handle registration & token handling"),
|
||||
}
|
||||
}
|
||||
|
||||
// Processing above can take some time, check if timeout expired
|
||||
if timeout.elapsed() {
|
||||
return Ok(None);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "event-stream")]
|
||||
fn waker(&self) -> Waker {
|
||||
self.waker.clone()
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Following `Parser` structure exists for two reasons:
|
||||
//
|
||||
// * mimic anes Parser interface
|
||||
// * move the advancing, parsing, ... stuff out of the `try_read` method
|
||||
//
|
||||
#[derive(Debug)]
|
||||
struct Parser {
|
||||
buffer: Vec<u8>,
|
||||
internal_events: VecDeque<InternalEvent>,
|
||||
}
|
||||
|
||||
impl Default for Parser {
|
||||
fn default() -> Self {
|
||||
Parser {
|
||||
// This buffer is used for -> 1 <- ANSI escape sequence. Are we
|
||||
// aware of any ANSI escape sequence that is bigger? Can we make
|
||||
// it smaller?
|
||||
//
|
||||
// Probably not worth spending more time on this as "there's a plan"
|
||||
// to use the anes crate parser.
|
||||
buffer: Vec::with_capacity(256),
|
||||
// TTY_BUFFER_SIZE is 1_024 bytes. How many ANSI escape sequences can
|
||||
// fit? What is an average sequence length? Let's guess here
|
||||
// and say that the average ANSI escape sequence length is 8 bytes. Thus
|
||||
// the buffer size should be 1024/8=128 to avoid additional allocations
|
||||
// when processing large amounts of data.
|
||||
//
|
||||
// There's no need to make it bigger, because when you look at the `try_read`
|
||||
// method implementation, all events are consumed before the next TTY_BUFFER
|
||||
// is processed -> events pushed.
|
||||
internal_events: VecDeque::with_capacity(128),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Parser {
|
||||
fn advance(&mut self, buffer: &[u8], more: bool) {
|
||||
for (idx, byte) in buffer.iter().enumerate() {
|
||||
let more = idx + 1 < buffer.len() || more;
|
||||
|
||||
self.buffer.push(*byte);
|
||||
|
||||
match parse_event(&self.buffer, more) {
|
||||
Ok(Some(ie)) => {
|
||||
self.internal_events.push_back(ie);
|
||||
self.buffer.clear();
|
||||
}
|
||||
Ok(None) => {
|
||||
// Event can't be parsed, because we don't have enough bytes for
|
||||
// the current sequence. Keep the buffer and process next bytes.
|
||||
}
|
||||
Err(_) => {
|
||||
// Event can't be parsed (not enough parameters, parameter is not a number, ...).
|
||||
// Clear the buffer and continue with another sequence.
|
||||
self.buffer.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Iterator for Parser {
|
||||
type Item = InternalEvent;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
self.internal_events.pop_front()
|
||||
}
|
||||
}
|
@ -1,275 +0,0 @@
|
||||
#[cfg(feature = "libc")]
|
||||
use std::os::unix::prelude::AsRawFd;
|
||||
use std::{collections::VecDeque, io, os::unix::net::UnixStream, time::Duration};
|
||||
|
||||
#[cfg(not(feature = "libc"))]
|
||||
use rustix::fd::{AsFd, AsRawFd};
|
||||
|
||||
use signal_hook::low_level::pipe;
|
||||
|
||||
use crate::event::timeout::PollTimeout;
|
||||
use crate::event::Event;
|
||||
use filedescriptor::{poll, pollfd, POLLIN};
|
||||
|
||||
#[cfg(feature = "event-stream")]
|
||||
use crate::event::sys::Waker;
|
||||
use crate::event::{source::EventSource, sys::unix::parse::parse_event, InternalEvent};
|
||||
use crate::terminal::sys::file_descriptor::{tty_fd, FileDesc};
|
||||
|
||||
/// Holds a prototypical Waker and a receiver we can wait on when doing select().
|
||||
#[cfg(feature = "event-stream")]
|
||||
struct WakePipe {
|
||||
receiver: UnixStream,
|
||||
waker: Waker,
|
||||
}
|
||||
|
||||
#[cfg(feature = "event-stream")]
|
||||
impl WakePipe {
|
||||
fn new() -> io::Result<Self> {
|
||||
let (receiver, sender) = nonblocking_unix_pair()?;
|
||||
Ok(WakePipe {
|
||||
receiver,
|
||||
waker: Waker::new(sender),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// I (@zrzka) wasn't able to read more than 1_022 bytes when testing
|
||||
// reading on macOS/Linux -> we don't need bigger buffer and 1k of bytes
|
||||
// is enough.
|
||||
const TTY_BUFFER_SIZE: usize = 1_024;
|
||||
|
||||
pub(crate) struct UnixInternalEventSource {
|
||||
parser: Parser,
|
||||
tty_buffer: [u8; TTY_BUFFER_SIZE],
|
||||
tty: FileDesc<'static>,
|
||||
winch_signal_receiver: UnixStream,
|
||||
#[cfg(feature = "event-stream")]
|
||||
wake_pipe: WakePipe,
|
||||
}
|
||||
|
||||
fn nonblocking_unix_pair() -> io::Result<(UnixStream, UnixStream)> {
|
||||
let (receiver, sender) = UnixStream::pair()?;
|
||||
receiver.set_nonblocking(true)?;
|
||||
sender.set_nonblocking(true)?;
|
||||
Ok((receiver, sender))
|
||||
}
|
||||
|
||||
impl UnixInternalEventSource {
|
||||
pub fn new() -> io::Result<Self> {
|
||||
UnixInternalEventSource::from_file_descriptor(tty_fd()?)
|
||||
}
|
||||
|
||||
pub(crate) fn from_file_descriptor(input_fd: FileDesc<'static>) -> io::Result<Self> {
|
||||
Ok(UnixInternalEventSource {
|
||||
parser: Parser::default(),
|
||||
tty_buffer: [0u8; TTY_BUFFER_SIZE],
|
||||
tty: input_fd,
|
||||
winch_signal_receiver: {
|
||||
let (receiver, sender) = nonblocking_unix_pair()?;
|
||||
// Unregistering is unnecessary because EventSource is a singleton
|
||||
#[cfg(feature = "libc")]
|
||||
pipe::register(libc::SIGWINCH, sender)?;
|
||||
#[cfg(not(feature = "libc"))]
|
||||
pipe::register(rustix::process::Signal::Winch as i32, sender)?;
|
||||
receiver
|
||||
},
|
||||
#[cfg(feature = "event-stream")]
|
||||
wake_pipe: WakePipe::new()?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// read_complete reads from a non-blocking file descriptor
|
||||
/// until the buffer is full or it would block.
|
||||
///
|
||||
/// Similar to `std::io::Read::read_to_end`, except this function
|
||||
/// only fills the given buffer and does not read beyond that.
|
||||
fn read_complete(fd: &FileDesc, buf: &mut [u8]) -> io::Result<usize> {
|
||||
loop {
|
||||
match fd.read(buf) {
|
||||
Ok(x) => return Ok(x),
|
||||
Err(e) => match e.kind() {
|
||||
io::ErrorKind::WouldBlock => return Ok(0),
|
||||
io::ErrorKind::Interrupted => continue,
|
||||
_ => return Err(e),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl EventSource for UnixInternalEventSource {
|
||||
fn try_read(&mut self, timeout: Option<Duration>) -> io::Result<Option<InternalEvent>> {
|
||||
let timeout = PollTimeout::new(timeout);
|
||||
|
||||
fn make_pollfd<F: AsRawFd>(fd: &F) -> pollfd {
|
||||
pollfd {
|
||||
fd: fd.as_raw_fd(),
|
||||
events: POLLIN,
|
||||
revents: 0,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "event-stream"))]
|
||||
let mut fds = [
|
||||
make_pollfd(&self.tty),
|
||||
make_pollfd(&self.winch_signal_receiver),
|
||||
];
|
||||
|
||||
#[cfg(feature = "event-stream")]
|
||||
let mut fds = [
|
||||
make_pollfd(&self.tty),
|
||||
make_pollfd(&self.winch_signal_receiver),
|
||||
make_pollfd(&self.wake_pipe.receiver),
|
||||
];
|
||||
|
||||
while timeout.leftover().map_or(true, |t| !t.is_zero()) {
|
||||
// check if there are buffered events from the last read
|
||||
if let Some(event) = self.parser.next() {
|
||||
return Ok(Some(event));
|
||||
}
|
||||
match poll(&mut fds, timeout.leftover()) {
|
||||
Err(filedescriptor::Error::Poll(e)) | Err(filedescriptor::Error::Io(e)) => {
|
||||
match e.kind() {
|
||||
// retry on EINTR
|
||||
io::ErrorKind::Interrupted => continue,
|
||||
_ => return Err(e),
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::Other,
|
||||
format!("got unexpected error while polling: {:?}", e),
|
||||
))
|
||||
}
|
||||
Ok(_) => (),
|
||||
};
|
||||
if fds[0].revents & POLLIN != 0 {
|
||||
loop {
|
||||
let read_count = read_complete(&self.tty, &mut self.tty_buffer)?;
|
||||
if read_count > 0 {
|
||||
self.parser.advance(
|
||||
&self.tty_buffer[..read_count],
|
||||
read_count == TTY_BUFFER_SIZE,
|
||||
);
|
||||
}
|
||||
|
||||
if let Some(event) = self.parser.next() {
|
||||
return Ok(Some(event));
|
||||
}
|
||||
|
||||
if read_count == 0 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if fds[1].revents & POLLIN != 0 {
|
||||
#[cfg(feature = "libc")]
|
||||
let fd = FileDesc::new(self.winch_signal_receiver.as_raw_fd(), false);
|
||||
#[cfg(not(feature = "libc"))]
|
||||
let fd = FileDesc::Borrowed(self.winch_signal_receiver.as_fd());
|
||||
// drain the pipe
|
||||
while read_complete(&fd, &mut [0; 1024])? != 0 {}
|
||||
// TODO Should we remove tput?
|
||||
//
|
||||
// This can take a really long time, because terminal::size can
|
||||
// launch new process (tput) and then it parses its output. It's
|
||||
// not a really long time from the absolute time point of view, but
|
||||
// it's a really long time from the mio, async-std/tokio executor, ...
|
||||
// point of view.
|
||||
let new_size = crate::terminal::size()?;
|
||||
return Ok(Some(InternalEvent::Event(Event::Resize(
|
||||
new_size.0, new_size.1,
|
||||
))));
|
||||
}
|
||||
|
||||
#[cfg(feature = "event-stream")]
|
||||
if fds[2].revents & POLLIN != 0 {
|
||||
let fd = FileDesc::new(self.wake_pipe.receiver.as_raw_fd(), false);
|
||||
// drain the pipe
|
||||
while read_complete(&fd, &mut [0; 1024])? != 0 {}
|
||||
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::Interrupted,
|
||||
"Poll operation was woken up by `Waker::wake`",
|
||||
));
|
||||
}
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
#[cfg(feature = "event-stream")]
|
||||
fn waker(&self) -> Waker {
|
||||
self.wake_pipe.waker.clone()
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Following `Parser` structure exists for two reasons:
|
||||
//
|
||||
// * mimic anes Parser interface
|
||||
// * move the advancing, parsing, ... stuff out of the `try_read` method
|
||||
//
|
||||
#[derive(Debug)]
|
||||
struct Parser {
|
||||
buffer: Vec<u8>,
|
||||
internal_events: VecDeque<InternalEvent>,
|
||||
}
|
||||
|
||||
impl Default for Parser {
|
||||
fn default() -> Self {
|
||||
Parser {
|
||||
// This buffer is used for -> 1 <- ANSI escape sequence. Are we
|
||||
// aware of any ANSI escape sequence that is bigger? Can we make
|
||||
// it smaller?
|
||||
//
|
||||
// Probably not worth spending more time on this as "there's a plan"
|
||||
// to use the anes crate parser.
|
||||
buffer: Vec::with_capacity(256),
|
||||
// TTY_BUFFER_SIZE is 1_024 bytes. How many ANSI escape sequences can
|
||||
// fit? What is an average sequence length? Let's guess here
|
||||
// and say that the average ANSI escape sequence length is 8 bytes. Thus
|
||||
// the buffer size should be 1024/8=128 to avoid additional allocations
|
||||
// when processing large amounts of data.
|
||||
//
|
||||
// There's no need to make it bigger, because when you look at the `try_read`
|
||||
// method implementation, all events are consumed before the next TTY_BUFFER
|
||||
// is processed -> events pushed.
|
||||
internal_events: VecDeque::with_capacity(128),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Parser {
|
||||
fn advance(&mut self, buffer: &[u8], more: bool) {
|
||||
for (idx, byte) in buffer.iter().enumerate() {
|
||||
let more = idx + 1 < buffer.len() || more;
|
||||
|
||||
self.buffer.push(*byte);
|
||||
|
||||
match parse_event(&self.buffer, more) {
|
||||
Ok(Some(ie)) => {
|
||||
self.internal_events.push_back(ie);
|
||||
self.buffer.clear();
|
||||
}
|
||||
Ok(None) => {
|
||||
// Event can't be parsed, because we don't have enough bytes for
|
||||
// the current sequence. Keep the buffer and process next bytes.
|
||||
}
|
||||
Err(_) => {
|
||||
// Event can't be parsed (not enough parameters, parameter is not a number, ...).
|
||||
// Clear the buffer and continue with another sequence.
|
||||
self.buffer.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Iterator for Parser {
|
||||
type Item = InternalEvent;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
self.internal_events.pop_front()
|
||||
}
|
||||
}
|
@ -1,100 +0,0 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use crossterm_winapi::{Console, Handle, InputRecord};
|
||||
|
||||
use crate::event::{
|
||||
sys::windows::{parse::MouseButtonsPressed, poll::WinApiPoll},
|
||||
Event,
|
||||
};
|
||||
|
||||
#[cfg(feature = "event-stream")]
|
||||
use crate::event::sys::Waker;
|
||||
use crate::event::{
|
||||
source::EventSource,
|
||||
sys::windows::parse::{handle_key_event, handle_mouse_event},
|
||||
timeout::PollTimeout,
|
||||
InternalEvent,
|
||||
};
|
||||
|
||||
pub(crate) struct WindowsEventSource {
|
||||
console: Console,
|
||||
poll: WinApiPoll,
|
||||
surrogate_buffer: Option<u16>,
|
||||
mouse_buttons_pressed: MouseButtonsPressed,
|
||||
}
|
||||
|
||||
impl WindowsEventSource {
|
||||
pub(crate) fn new() -> std::io::Result<WindowsEventSource> {
|
||||
let console = Console::from(Handle::current_in_handle()?);
|
||||
Ok(WindowsEventSource {
|
||||
console,
|
||||
|
||||
#[cfg(not(feature = "event-stream"))]
|
||||
poll: WinApiPoll::new(),
|
||||
#[cfg(feature = "event-stream")]
|
||||
poll: WinApiPoll::new()?,
|
||||
|
||||
surrogate_buffer: None,
|
||||
mouse_buttons_pressed: MouseButtonsPressed::default(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl EventSource for WindowsEventSource {
|
||||
fn try_read(&mut self, timeout: Option<Duration>) -> std::io::Result<Option<InternalEvent>> {
|
||||
let poll_timeout = PollTimeout::new(timeout);
|
||||
|
||||
loop {
|
||||
if let Some(event_ready) = self.poll.poll(poll_timeout.leftover())? {
|
||||
let number = self.console.number_of_console_input_events()?;
|
||||
if event_ready && number != 0 {
|
||||
let event = match self.console.read_single_input_event()? {
|
||||
InputRecord::KeyEvent(record) => {
|
||||
handle_key_event(record, &mut self.surrogate_buffer)
|
||||
}
|
||||
InputRecord::MouseEvent(record) => {
|
||||
let mouse_event =
|
||||
handle_mouse_event(record, &self.mouse_buttons_pressed);
|
||||
self.mouse_buttons_pressed = MouseButtonsPressed {
|
||||
left: record.button_state.left_button(),
|
||||
right: record.button_state.right_button(),
|
||||
middle: record.button_state.middle_button(),
|
||||
};
|
||||
|
||||
mouse_event
|
||||
}
|
||||
InputRecord::WindowBufferSizeEvent(record) => {
|
||||
// windows starts counting at 0, unix at 1, add one to replicate unix behaviour.
|
||||
Some(Event::Resize(
|
||||
record.size.x as u16 + 1,
|
||||
record.size.y as u16 + 1,
|
||||
))
|
||||
}
|
||||
InputRecord::FocusEvent(record) => {
|
||||
let event = if record.set_focus {
|
||||
Event::FocusGained
|
||||
} else {
|
||||
Event::FocusLost
|
||||
};
|
||||
Some(event)
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
|
||||
if let Some(event) = event {
|
||||
return Ok(Some(InternalEvent::Event(event)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if poll_timeout.elapsed() {
|
||||
return Ok(None);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "event-stream")]
|
||||
fn waker(&self) -> Waker {
|
||||
self.poll.waker()
|
||||
}
|
||||
}
|
@ -1,9 +1 @@
|
||||
#[cfg(all(unix, feature = "event-stream"))]
|
||||
pub(crate) use unix::waker::Waker;
|
||||
#[cfg(all(windows, feature = "event-stream"))]
|
||||
pub(crate) use windows::waker::Waker;
|
||||
|
||||
#[cfg(unix)]
|
||||
pub(crate) mod unix;
|
||||
#[cfg(windows)]
|
||||
pub(crate) mod windows;
|
||||
|
@ -1,5 +1,2 @@
|
||||
#[cfg(feature = "event-stream")]
|
||||
pub(crate) mod waker;
|
||||
|
||||
#[cfg(feature = "events")]
|
||||
pub(crate) mod parse;
|
||||
|
@ -5,8 +5,6 @@ use crate::event::{
|
||||
MediaKeyCode, ModifierKeyCode, MouseButton, MouseEvent, MouseEventKind,
|
||||
};
|
||||
|
||||
use super::super::super::InternalEvent;
|
||||
|
||||
// Event parsing
|
||||
//
|
||||
// This code (& previous one) are kind of ugly. We have to think about this,
|
||||
@ -23,10 +21,7 @@ fn could_not_parse_event_error() -> io::Error {
|
||||
io::Error::new(io::ErrorKind::Other, "Could not parse an event.")
|
||||
}
|
||||
|
||||
pub(crate) fn parse_event(
|
||||
buffer: &[u8],
|
||||
input_available: bool,
|
||||
) -> io::Result<Option<InternalEvent>> {
|
||||
pub(crate) fn parse_event(buffer: &[u8], input_available: bool) -> io::Result<Option<Event>> {
|
||||
if buffer.is_empty() {
|
||||
return Ok(None);
|
||||
}
|
||||
@ -38,7 +33,7 @@ pub(crate) fn parse_event(
|
||||
// Possible Esc sequence
|
||||
Ok(None)
|
||||
} else {
|
||||
Ok(Some(InternalEvent::Event(Event::Key(KeyCode::Esc.into()))))
|
||||
Ok(Some(Event::Key(KeyCode::Esc.into())))
|
||||
}
|
||||
} else {
|
||||
match buffer[1] {
|
||||
@ -47,40 +42,28 @@ pub(crate) fn parse_event(
|
||||
Ok(None)
|
||||
} else {
|
||||
match buffer[2] {
|
||||
b'D' => {
|
||||
Ok(Some(InternalEvent::Event(Event::Key(KeyCode::Left.into()))))
|
||||
}
|
||||
b'C' => Ok(Some(InternalEvent::Event(Event::Key(
|
||||
KeyCode::Right.into(),
|
||||
)))),
|
||||
b'A' => {
|
||||
Ok(Some(InternalEvent::Event(Event::Key(KeyCode::Up.into()))))
|
||||
}
|
||||
b'B' => {
|
||||
Ok(Some(InternalEvent::Event(Event::Key(KeyCode::Down.into()))))
|
||||
}
|
||||
b'H' => {
|
||||
Ok(Some(InternalEvent::Event(Event::Key(KeyCode::Home.into()))))
|
||||
}
|
||||
b'F' => {
|
||||
Ok(Some(InternalEvent::Event(Event::Key(KeyCode::End.into()))))
|
||||
}
|
||||
b'D' => Ok(Some(Event::Key(KeyCode::Left.into()))),
|
||||
b'C' => Ok(Some(Event::Key(KeyCode::Right.into()))),
|
||||
b'A' => Ok(Some(Event::Key(KeyCode::Up.into()))),
|
||||
b'B' => Ok(Some(Event::Key(KeyCode::Down.into()))),
|
||||
b'H' => Ok(Some(Event::Key(KeyCode::Home.into()))),
|
||||
b'F' => Ok(Some(Event::Key(KeyCode::End.into()))),
|
||||
// F1-F4
|
||||
val @ b'P'..=b'S' => Ok(Some(InternalEvent::Event(Event::Key(
|
||||
KeyCode::F(1 + val - b'P').into(),
|
||||
)))),
|
||||
val @ b'P'..=b'S' => {
|
||||
Ok(Some(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())))),
|
||||
b'\x1B' => Ok(Some(Event::Key(KeyCode::Esc.into()))),
|
||||
_ => parse_event(&buffer[1..], input_available).map(|event_option| {
|
||||
event_option.map(|event| {
|
||||
if let InternalEvent::Event(Event::Key(key_event)) = event {
|
||||
if let Event::Key(key_event) = event {
|
||||
let mut alt_key_event = key_event;
|
||||
alt_key_event.modifiers |= KeyModifiers::ALT;
|
||||
InternalEvent::Event(Event::Key(alt_key_event))
|
||||
Event::Key(alt_key_event)
|
||||
} else {
|
||||
event
|
||||
}
|
||||
@ -89,38 +72,26 @@ pub(crate) fn parse_event(
|
||||
}
|
||||
}
|
||||
}
|
||||
b'\r' => Ok(Some(InternalEvent::Event(Event::Key(
|
||||
KeyCode::Enter.into(),
|
||||
)))),
|
||||
// Issue #371: \n = 0xA, which is also the keycode for Ctrl+J. The only reason we get
|
||||
// newlines as input is because the terminal converts \r into \n for us. When we
|
||||
// enter raw mode, we disable that, so \n no longer has any meaning - it's better to
|
||||
// use Ctrl+J. Waiting to handle it here means it gets picked up later
|
||||
b'\n' if !crate::terminal::sys::is_raw_mode_enabled() => Ok(Some(InternalEvent::Event(
|
||||
Event::Key(KeyCode::Enter.into()),
|
||||
))),
|
||||
b'\t' => Ok(Some(InternalEvent::Event(Event::Key(KeyCode::Tab.into())))),
|
||||
b'\x7F' => Ok(Some(InternalEvent::Event(Event::Key(
|
||||
KeyCode::Backspace.into(),
|
||||
)))),
|
||||
c @ b'\x01'..=b'\x1A' => Ok(Some(InternalEvent::Event(Event::Key(KeyEvent::new(
|
||||
b'\r' => Ok(Some(Event::Key(KeyCode::Enter.into()))),
|
||||
b'\t' => Ok(Some(Event::Key(KeyCode::Tab.into()))),
|
||||
b'\x7F' => Ok(Some(Event::Key(KeyCode::Backspace.into()))),
|
||||
c @ b'\x01'..=b'\x1A' => Ok(Some(Event::Key(KeyEvent::new(
|
||||
KeyCode::Char((c - 0x1 + b'a') as char),
|
||||
KeyModifiers::CONTROL,
|
||||
))))),
|
||||
c @ b'\x1C'..=b'\x1F' => Ok(Some(InternalEvent::Event(Event::Key(KeyEvent::new(
|
||||
)))),
|
||||
c @ b'\x1C'..=b'\x1F' => Ok(Some(Event::Key(KeyEvent::new(
|
||||
KeyCode::Char((c - 0x1C + b'4') as char),
|
||||
KeyModifiers::CONTROL,
|
||||
))))),
|
||||
b'\0' => Ok(Some(InternalEvent::Event(Event::Key(KeyEvent::new(
|
||||
)))),
|
||||
b'\0' => Ok(Some(Event::Key(KeyEvent::new(
|
||||
KeyCode::Char(' '),
|
||||
KeyModifiers::CONTROL,
|
||||
))))),
|
||||
)))),
|
||||
_ => parse_utf8_char(buffer).map(|maybe_char| {
|
||||
maybe_char
|
||||
.map(KeyCode::Char)
|
||||
.map(char_code_to_event)
|
||||
.map(Event::Key)
|
||||
.map(InternalEvent::Event)
|
||||
}),
|
||||
}
|
||||
}
|
||||
@ -134,7 +105,7 @@ fn char_code_to_event(code: KeyCode) -> KeyEvent {
|
||||
KeyEvent::new(code, modifiers)
|
||||
}
|
||||
|
||||
pub(crate) fn parse_csi(buffer: &[u8]) -> io::Result<Option<InternalEvent>> {
|
||||
pub(crate) fn parse_csi(buffer: &[u8]) -> io::Result<Option<Event>> {
|
||||
assert!(buffer.starts_with(&[b'\x1B', b'['])); // ESC [
|
||||
|
||||
if buffer.len() == 2 {
|
||||
@ -210,7 +181,7 @@ pub(crate) fn parse_csi(buffer: &[u8]) -> io::Result<Option<InternalEvent>> {
|
||||
_ => return Err(could_not_parse_event_error()),
|
||||
};
|
||||
|
||||
Ok(input_event.map(InternalEvent::Event))
|
||||
Ok(input_event)
|
||||
}
|
||||
|
||||
pub(crate) fn next_parsed<T>(iter: &mut dyn Iterator<Item = &str>) -> io::Result<T>
|
||||
@ -238,7 +209,7 @@ fn modifier_and_kind_parsed(iter: &mut dyn Iterator<Item = &str>) -> io::Result<
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn parse_csi_cursor_position(buffer: &[u8]) -> io::Result<Option<InternalEvent>> {
|
||||
pub(crate) fn parse_csi_cursor_position(buffer: &[u8]) -> io::Result<Option<Event>> {
|
||||
// ESC [ Cy ; Cx R
|
||||
// Cy - cursor row number (starting from 1)
|
||||
// Cx - cursor column number (starting from 1)
|
||||
@ -253,10 +224,10 @@ pub(crate) fn parse_csi_cursor_position(buffer: &[u8]) -> io::Result<Option<Inte
|
||||
let y = next_parsed::<u16>(&mut split)? - 1;
|
||||
let x = next_parsed::<u16>(&mut split)? - 1;
|
||||
|
||||
Ok(Some(InternalEvent::CursorPosition(x, y)))
|
||||
Ok(Some(Event::CursorPosition(x, y)))
|
||||
}
|
||||
|
||||
fn parse_csi_keyboard_enhancement_flags(buffer: &[u8]) -> io::Result<Option<InternalEvent>> {
|
||||
fn parse_csi_keyboard_enhancement_flags(buffer: &[u8]) -> io::Result<Option<Event>> {
|
||||
// ESC [ ? flags u
|
||||
assert!(buffer.starts_with(&[b'\x1B', b'[', b'?'])); // ESC [ ?
|
||||
assert!(buffer.ends_with(&[b'u']));
|
||||
@ -285,10 +256,10 @@ fn parse_csi_keyboard_enhancement_flags(buffer: &[u8]) -> io::Result<Option<Inte
|
||||
// flags |= KeyboardEnhancementFlags::REPORT_ASSOCIATED_TEXT;
|
||||
// }
|
||||
|
||||
Ok(Some(InternalEvent::KeyboardEnhancementFlags(flags)))
|
||||
Ok(Some(Event::KeyboardEnhancementFlags(flags)))
|
||||
}
|
||||
|
||||
fn parse_csi_primary_device_attributes(buffer: &[u8]) -> io::Result<Option<InternalEvent>> {
|
||||
fn parse_csi_primary_device_attributes(buffer: &[u8]) -> io::Result<Option<Event>> {
|
||||
// ESC [ 64 ; attr1 ; attr2 ; ... ; attrn ; c
|
||||
assert!(buffer.starts_with(&[b'\x1B', b'[', b'?']));
|
||||
assert!(buffer.ends_with(&[b'c']));
|
||||
@ -297,7 +268,7 @@ fn parse_csi_primary_device_attributes(buffer: &[u8]) -> io::Result<Option<Inter
|
||||
// exposed in the crossterm API so we don't need to parse the individual attributes yet.
|
||||
// See <https://vt100.net/docs/vt510-rm/DA1.html>
|
||||
|
||||
Ok(Some(InternalEvent::PrimaryDeviceAttributes))
|
||||
Ok(Some(Event::PrimaryDeviceAttributes))
|
||||
}
|
||||
|
||||
fn parse_modifiers(mask: u8) -> KeyModifiers {
|
||||
@ -345,7 +316,7 @@ fn parse_key_event_kind(kind: u8) -> KeyEventKind {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn parse_csi_modifier_key_code(buffer: &[u8]) -> io::Result<Option<InternalEvent>> {
|
||||
pub(crate) fn parse_csi_modifier_key_code(buffer: &[u8]) -> io::Result<Option<Event>> {
|
||||
assert!(buffer.starts_with(&[b'\x1B', b'['])); // ESC [
|
||||
//
|
||||
let s = std::str::from_utf8(&buffer[2..buffer.len() - 1])
|
||||
@ -390,7 +361,7 @@ pub(crate) fn parse_csi_modifier_key_code(buffer: &[u8]) -> io::Result<Option<In
|
||||
|
||||
let input_event = Event::Key(KeyEvent::new_with_kind(keycode, modifiers, kind));
|
||||
|
||||
Ok(Some(InternalEvent::Event(input_event)))
|
||||
Ok(Some(input_event))
|
||||
}
|
||||
|
||||
fn translate_functional_key_code(codepoint: u32) -> Option<(KeyCode, KeyEventState)> {
|
||||
@ -494,7 +465,7 @@ fn translate_functional_key_code(codepoint: u32) -> Option<(KeyCode, KeyEventSta
|
||||
None
|
||||
}
|
||||
|
||||
pub(crate) fn parse_csi_u_encoded_key_code(buffer: &[u8]) -> io::Result<Option<InternalEvent>> {
|
||||
pub(crate) fn parse_csi_u_encoded_key_code(buffer: &[u8]) -> io::Result<Option<Event>> {
|
||||
assert!(buffer.starts_with(&[b'\x1B', b'['])); // ESC [
|
||||
assert!(buffer.ends_with(&[b'u']));
|
||||
|
||||
@ -545,11 +516,6 @@ pub(crate) fn parse_csi_u_encoded_key_code(buffer: &[u8]) -> io::Result<Option<I
|
||||
match c {
|
||||
'\x1B' => KeyCode::Esc,
|
||||
'\r' => KeyCode::Enter,
|
||||
// Issue #371: \n = 0xA, which is also the keycode for Ctrl+J. The only reason we get
|
||||
// newlines as input is because the terminal converts \r into \n for us. When we
|
||||
// enter raw mode, we disable that, so \n no longer has any meaning - it's better to
|
||||
// use Ctrl+J. Waiting to handle it here means it gets picked up later
|
||||
'\n' if !crate::terminal::sys::is_raw_mode_enabled() => KeyCode::Enter,
|
||||
'\t' => {
|
||||
if modifiers.contains(KeyModifiers::SHIFT) {
|
||||
KeyCode::BackTab
|
||||
@ -613,10 +579,10 @@ pub(crate) fn parse_csi_u_encoded_key_code(buffer: &[u8]) -> io::Result<Option<I
|
||||
state_from_keycode | state_from_modifiers,
|
||||
));
|
||||
|
||||
Ok(Some(InternalEvent::Event(input_event)))
|
||||
Ok(Some(input_event))
|
||||
}
|
||||
|
||||
pub(crate) fn parse_csi_special_key_code(buffer: &[u8]) -> io::Result<Option<InternalEvent>> {
|
||||
pub(crate) fn parse_csi_special_key_code(buffer: &[u8]) -> io::Result<Option<Event>> {
|
||||
assert!(buffer.starts_with(&[b'\x1B', b'['])); // ESC [
|
||||
assert!(buffer.ends_with(&[b'~']));
|
||||
|
||||
@ -657,10 +623,10 @@ pub(crate) fn parse_csi_special_key_code(buffer: &[u8]) -> io::Result<Option<Int
|
||||
keycode, modifiers, kind, state,
|
||||
));
|
||||
|
||||
Ok(Some(InternalEvent::Event(input_event)))
|
||||
Ok(Some(input_event))
|
||||
}
|
||||
|
||||
pub(crate) fn parse_csi_rxvt_mouse(buffer: &[u8]) -> io::Result<Option<InternalEvent>> {
|
||||
pub(crate) fn parse_csi_rxvt_mouse(buffer: &[u8]) -> io::Result<Option<Event>> {
|
||||
// rxvt mouse encoding:
|
||||
// ESC [ Cb ; Cx ; Cy ; M
|
||||
|
||||
@ -679,15 +645,15 @@ pub(crate) fn parse_csi_rxvt_mouse(buffer: &[u8]) -> io::Result<Option<InternalE
|
||||
let cx = next_parsed::<u16>(&mut split)? - 1;
|
||||
let cy = next_parsed::<u16>(&mut split)? - 1;
|
||||
|
||||
Ok(Some(InternalEvent::Event(Event::Mouse(MouseEvent {
|
||||
Ok(Some(Event::Mouse(MouseEvent {
|
||||
kind,
|
||||
column: cx,
|
||||
row: cy,
|
||||
modifiers,
|
||||
}))))
|
||||
})))
|
||||
}
|
||||
|
||||
pub(crate) fn parse_csi_normal_mouse(buffer: &[u8]) -> io::Result<Option<InternalEvent>> {
|
||||
pub(crate) fn parse_csi_normal_mouse(buffer: &[u8]) -> io::Result<Option<Event>> {
|
||||
// Normal mouse encoding: ESC [ M CB Cx Cy (6 characters only).
|
||||
|
||||
assert!(buffer.starts_with(&[b'\x1B', b'[', b'M'])); // ESC [ M
|
||||
@ -707,15 +673,15 @@ pub(crate) fn parse_csi_normal_mouse(buffer: &[u8]) -> io::Result<Option<Interna
|
||||
let cx = u16::from(buffer[4].saturating_sub(32)) - 1;
|
||||
let cy = u16::from(buffer[5].saturating_sub(32)) - 1;
|
||||
|
||||
Ok(Some(InternalEvent::Event(Event::Mouse(MouseEvent {
|
||||
Ok(Some(Event::Mouse(MouseEvent {
|
||||
kind,
|
||||
column: cx,
|
||||
row: cy,
|
||||
modifiers,
|
||||
}))))
|
||||
})))
|
||||
}
|
||||
|
||||
pub(crate) fn parse_csi_sgr_mouse(buffer: &[u8]) -> io::Result<Option<InternalEvent>> {
|
||||
pub(crate) fn parse_csi_sgr_mouse(buffer: &[u8]) -> io::Result<Option<Event>> {
|
||||
// ESC [ < Cb ; Cx ; Cy (;) (M or m)
|
||||
|
||||
assert!(buffer.starts_with(&[b'\x1B', b'[', b'<'])); // ESC [ <
|
||||
@ -752,12 +718,12 @@ pub(crate) fn parse_csi_sgr_mouse(buffer: &[u8]) -> io::Result<Option<InternalEv
|
||||
kind
|
||||
};
|
||||
|
||||
Ok(Some(InternalEvent::Event(Event::Mouse(MouseEvent {
|
||||
Ok(Some(Event::Mouse(MouseEvent {
|
||||
kind,
|
||||
column: cx,
|
||||
row: cy,
|
||||
modifiers,
|
||||
}))))
|
||||
})))
|
||||
}
|
||||
|
||||
/// Cb is the byte of a mouse input that contains the button being used, the key modifiers being
|
||||
@ -810,7 +776,7 @@ fn parse_cb(cb: u8) -> io::Result<(MouseEventKind, KeyModifiers)> {
|
||||
}
|
||||
|
||||
#[cfg(feature = "bracketed-paste")]
|
||||
pub(crate) fn parse_csi_bracketed_paste(buffer: &[u8]) -> io::Result<Option<InternalEvent>> {
|
||||
pub(crate) fn parse_csi_bracketed_paste(buffer: &[u8]) -> io::Result<Option<Event>> {
|
||||
// ESC [ 2 0 0 ~ pasted text ESC 2 0 1 ~
|
||||
assert!(buffer.starts_with(b"\x1B[200~"));
|
||||
|
||||
@ -818,7 +784,7 @@ pub(crate) fn parse_csi_bracketed_paste(buffer: &[u8]) -> io::Result<Option<Inte
|
||||
Ok(None)
|
||||
} else {
|
||||
let paste = String::from_utf8_lossy(&buffer[6..buffer.len() - 6]).to_string();
|
||||
Ok(Some(InternalEvent::Event(Event::Paste(paste))))
|
||||
Ok(Some(Event::Paste(paste)))
|
||||
}
|
||||
}
|
||||
|
||||
@ -871,7 +837,7 @@ mod tests {
|
||||
fn test_esc_key() {
|
||||
assert_eq!(
|
||||
parse_event(b"\x1B", false).unwrap(),
|
||||
Some(InternalEvent::Event(Event::Key(KeyCode::Esc.into()))),
|
||||
Some(Event::Key(KeyCode::Esc.into())),
|
||||
);
|
||||
}
|
||||
|
||||
@ -884,10 +850,10 @@ mod tests {
|
||||
fn test_alt_key() {
|
||||
assert_eq!(
|
||||
parse_event(b"\x1Bc", false).unwrap(),
|
||||
Some(InternalEvent::Event(Event::Key(KeyEvent::new(
|
||||
Some(Event::Key(KeyEvent::new(
|
||||
KeyCode::Char('c'),
|
||||
KeyModifiers::ALT
|
||||
)))),
|
||||
))),
|
||||
);
|
||||
}
|
||||
|
||||
@ -895,10 +861,10 @@ mod tests {
|
||||
fn test_alt_shift() {
|
||||
assert_eq!(
|
||||
parse_event(b"\x1BH", false).unwrap(),
|
||||
Some(InternalEvent::Event(Event::Key(KeyEvent::new(
|
||||
Some(Event::Key(KeyEvent::new(
|
||||
KeyCode::Char('H'),
|
||||
KeyModifiers::ALT | KeyModifiers::SHIFT
|
||||
)))),
|
||||
))),
|
||||
);
|
||||
}
|
||||
|
||||
@ -906,10 +872,10 @@ mod tests {
|
||||
fn test_alt_ctrl() {
|
||||
assert_eq!(
|
||||
parse_event(b"\x1B\x14", false).unwrap(),
|
||||
Some(InternalEvent::Event(Event::Key(KeyEvent::new(
|
||||
Some(Event::Key(KeyEvent::new(
|
||||
KeyCode::Char('t'),
|
||||
KeyModifiers::ALT | KeyModifiers::CONTROL
|
||||
)))),
|
||||
))),
|
||||
);
|
||||
}
|
||||
|
||||
@ -921,79 +887,77 @@ mod tests {
|
||||
// parse_csi_cursor_position
|
||||
assert_eq!(
|
||||
parse_event(b"\x1B[20;10R", false).unwrap(),
|
||||
Some(InternalEvent::CursorPosition(9, 19))
|
||||
Some(Event::CursorPosition(9, 19))
|
||||
);
|
||||
|
||||
// parse_csi
|
||||
assert_eq!(
|
||||
parse_event(b"\x1B[D", false).unwrap(),
|
||||
Some(InternalEvent::Event(Event::Key(KeyCode::Left.into()))),
|
||||
Some(Event::Key(KeyCode::Left.into())),
|
||||
);
|
||||
|
||||
// parse_csi_modifier_key_code
|
||||
assert_eq!(
|
||||
parse_event(b"\x1B[2D", false).unwrap(),
|
||||
Some(InternalEvent::Event(Event::Key(KeyEvent::new(
|
||||
Some(Event::Key(KeyEvent::new(
|
||||
KeyCode::Left,
|
||||
KeyModifiers::SHIFT
|
||||
))))
|
||||
)))
|
||||
);
|
||||
|
||||
// parse_csi_special_key_code
|
||||
assert_eq!(
|
||||
parse_event(b"\x1B[3~", false).unwrap(),
|
||||
Some(InternalEvent::Event(Event::Key(KeyCode::Delete.into()))),
|
||||
Some(Event::Key(KeyCode::Delete.into())),
|
||||
);
|
||||
|
||||
// parse_csi_bracketed_paste
|
||||
#[cfg(feature = "bracketed-paste")]
|
||||
assert_eq!(
|
||||
parse_event(b"\x1B[200~on and on and on\x1B[201~", false).unwrap(),
|
||||
Some(InternalEvent::Event(Event::Paste(
|
||||
"on and on and on".to_string()
|
||||
))),
|
||||
Some(Event::Paste("on and on and on".to_string())),
|
||||
);
|
||||
|
||||
// parse_csi_rxvt_mouse
|
||||
assert_eq!(
|
||||
parse_event(b"\x1B[32;30;40;M", false).unwrap(),
|
||||
Some(InternalEvent::Event(Event::Mouse(MouseEvent {
|
||||
Some(Event::Mouse(MouseEvent {
|
||||
kind: MouseEventKind::Down(MouseButton::Left),
|
||||
column: 29,
|
||||
row: 39,
|
||||
modifiers: KeyModifiers::empty(),
|
||||
})))
|
||||
}))
|
||||
);
|
||||
|
||||
// parse_csi_normal_mouse
|
||||
assert_eq!(
|
||||
parse_event(b"\x1B[M0\x60\x70", false).unwrap(),
|
||||
Some(InternalEvent::Event(Event::Mouse(MouseEvent {
|
||||
Some(Event::Mouse(MouseEvent {
|
||||
kind: MouseEventKind::Down(MouseButton::Left),
|
||||
column: 63,
|
||||
row: 79,
|
||||
modifiers: KeyModifiers::CONTROL,
|
||||
})))
|
||||
}))
|
||||
);
|
||||
|
||||
// parse_csi_sgr_mouse
|
||||
assert_eq!(
|
||||
parse_event(b"\x1B[<0;20;10;M", false).unwrap(),
|
||||
Some(InternalEvent::Event(Event::Mouse(MouseEvent {
|
||||
Some(Event::Mouse(MouseEvent {
|
||||
kind: MouseEventKind::Down(MouseButton::Left),
|
||||
column: 19,
|
||||
row: 9,
|
||||
modifiers: KeyModifiers::empty(),
|
||||
})))
|
||||
}))
|
||||
);
|
||||
|
||||
// parse_utf8_char
|
||||
assert_eq!(
|
||||
parse_event("Ž".as_bytes(), false).unwrap(),
|
||||
Some(InternalEvent::Event(Event::Key(KeyEvent::new(
|
||||
Some(Event::Key(KeyEvent::new(
|
||||
KeyCode::Char('Ž'),
|
||||
KeyModifiers::SHIFT
|
||||
)))),
|
||||
))),
|
||||
);
|
||||
}
|
||||
|
||||
@ -1001,7 +965,7 @@ mod tests {
|
||||
fn test_parse_event() {
|
||||
assert_eq!(
|
||||
parse_event(b"\t", false).unwrap(),
|
||||
Some(InternalEvent::Event(Event::Key(KeyCode::Tab.into()))),
|
||||
Some(Event::Key(KeyCode::Tab.into())),
|
||||
);
|
||||
}
|
||||
|
||||
@ -1009,7 +973,7 @@ mod tests {
|
||||
fn test_parse_csi_cursor_position() {
|
||||
assert_eq!(
|
||||
parse_csi_cursor_position(b"\x1B[20;10R").unwrap(),
|
||||
Some(InternalEvent::CursorPosition(9, 19))
|
||||
Some(Event::CursorPosition(9, 19))
|
||||
);
|
||||
}
|
||||
|
||||
@ -1017,7 +981,7 @@ mod tests {
|
||||
fn test_parse_csi() {
|
||||
assert_eq!(
|
||||
parse_csi(b"\x1B[D").unwrap(),
|
||||
Some(InternalEvent::Event(Event::Key(KeyCode::Left.into()))),
|
||||
Some(Event::Key(KeyCode::Left.into())),
|
||||
);
|
||||
}
|
||||
|
||||
@ -1025,10 +989,10 @@ mod tests {
|
||||
fn test_parse_csi_modifier_key_code() {
|
||||
assert_eq!(
|
||||
parse_csi_modifier_key_code(b"\x1B[2D").unwrap(),
|
||||
Some(InternalEvent::Event(Event::Key(KeyEvent::new(
|
||||
Some(Event::Key(KeyEvent::new(
|
||||
KeyCode::Left,
|
||||
KeyModifiers::SHIFT
|
||||
)))),
|
||||
))),
|
||||
);
|
||||
}
|
||||
|
||||
@ -1036,7 +1000,7 @@ mod tests {
|
||||
fn test_parse_csi_special_key_code() {
|
||||
assert_eq!(
|
||||
parse_csi_special_key_code(b"\x1B[3~").unwrap(),
|
||||
Some(InternalEvent::Event(Event::Key(KeyCode::Delete.into()))),
|
||||
Some(Event::Key(KeyCode::Delete.into())),
|
||||
);
|
||||
}
|
||||
|
||||
@ -1044,10 +1008,10 @@ mod tests {
|
||||
fn test_parse_csi_special_key_code_multiple_values_not_supported() {
|
||||
assert_eq!(
|
||||
parse_csi_special_key_code(b"\x1B[3;2~").unwrap(),
|
||||
Some(InternalEvent::Event(Event::Key(KeyEvent::new(
|
||||
Some(Event::Key(KeyEvent::new(
|
||||
KeyCode::Delete,
|
||||
KeyModifiers::SHIFT
|
||||
)))),
|
||||
))),
|
||||
);
|
||||
}
|
||||
|
||||
@ -1067,28 +1031,25 @@ mod tests {
|
||||
);
|
||||
assert_eq!(
|
||||
parse_event(b"\x1B[200~o\x1B[2D\x1B[201~", false).unwrap(),
|
||||
Some(InternalEvent::Event(Event::Paste("o\x1B[2D".to_string())))
|
||||
Some(Event::Paste("o\x1B[2D".to_string()))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_csi_focus() {
|
||||
assert_eq!(
|
||||
parse_csi(b"\x1B[O").unwrap(),
|
||||
Some(InternalEvent::Event(Event::FocusLost))
|
||||
);
|
||||
assert_eq!(parse_csi(b"\x1B[O").unwrap(), Some(Event::FocusLost));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_csi_rxvt_mouse() {
|
||||
assert_eq!(
|
||||
parse_csi_rxvt_mouse(b"\x1B[32;30;40;M").unwrap(),
|
||||
Some(InternalEvent::Event(Event::Mouse(MouseEvent {
|
||||
Some(Event::Mouse(MouseEvent {
|
||||
kind: MouseEventKind::Down(MouseButton::Left),
|
||||
column: 29,
|
||||
row: 39,
|
||||
modifiers: KeyModifiers::empty(),
|
||||
})))
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
@ -1096,12 +1057,12 @@ mod tests {
|
||||
fn test_parse_csi_normal_mouse() {
|
||||
assert_eq!(
|
||||
parse_csi_normal_mouse(b"\x1B[M0\x60\x70").unwrap(),
|
||||
Some(InternalEvent::Event(Event::Mouse(MouseEvent {
|
||||
Some(Event::Mouse(MouseEvent {
|
||||
kind: MouseEventKind::Down(MouseButton::Left),
|
||||
column: 63,
|
||||
row: 79,
|
||||
modifiers: KeyModifiers::CONTROL,
|
||||
})))
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
@ -1109,39 +1070,39 @@ mod tests {
|
||||
fn test_parse_csi_sgr_mouse() {
|
||||
assert_eq!(
|
||||
parse_csi_sgr_mouse(b"\x1B[<0;20;10;M").unwrap(),
|
||||
Some(InternalEvent::Event(Event::Mouse(MouseEvent {
|
||||
Some(Event::Mouse(MouseEvent {
|
||||
kind: MouseEventKind::Down(MouseButton::Left),
|
||||
column: 19,
|
||||
row: 9,
|
||||
modifiers: KeyModifiers::empty(),
|
||||
})))
|
||||
}))
|
||||
);
|
||||
assert_eq!(
|
||||
parse_csi_sgr_mouse(b"\x1B[<0;20;10M").unwrap(),
|
||||
Some(InternalEvent::Event(Event::Mouse(MouseEvent {
|
||||
Some(Event::Mouse(MouseEvent {
|
||||
kind: MouseEventKind::Down(MouseButton::Left),
|
||||
column: 19,
|
||||
row: 9,
|
||||
modifiers: KeyModifiers::empty(),
|
||||
})))
|
||||
}))
|
||||
);
|
||||
assert_eq!(
|
||||
parse_csi_sgr_mouse(b"\x1B[<0;20;10;m").unwrap(),
|
||||
Some(InternalEvent::Event(Event::Mouse(MouseEvent {
|
||||
Some(Event::Mouse(MouseEvent {
|
||||
kind: MouseEventKind::Up(MouseButton::Left),
|
||||
column: 19,
|
||||
row: 9,
|
||||
modifiers: KeyModifiers::empty(),
|
||||
})))
|
||||
}))
|
||||
);
|
||||
assert_eq!(
|
||||
parse_csi_sgr_mouse(b"\x1B[<0;20;10m").unwrap(),
|
||||
Some(InternalEvent::Event(Event::Mouse(MouseEvent {
|
||||
Some(Event::Mouse(MouseEvent {
|
||||
kind: MouseEventKind::Up(MouseButton::Left),
|
||||
column: 19,
|
||||
row: 9,
|
||||
modifiers: KeyModifiers::empty(),
|
||||
})))
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
@ -1193,10 +1154,10 @@ mod tests {
|
||||
fn test_parse_char_event_lowercase() {
|
||||
assert_eq!(
|
||||
parse_event(b"c", false).unwrap(),
|
||||
Some(InternalEvent::Event(Event::Key(KeyEvent::new(
|
||||
Some(Event::Key(KeyEvent::new(
|
||||
KeyCode::Char('c'),
|
||||
KeyModifiers::empty()
|
||||
)))),
|
||||
))),
|
||||
);
|
||||
}
|
||||
|
||||
@ -1204,10 +1165,10 @@ mod tests {
|
||||
fn test_parse_char_event_uppercase() {
|
||||
assert_eq!(
|
||||
parse_event(b"C", false).unwrap(),
|
||||
Some(InternalEvent::Event(Event::Key(KeyEvent::new(
|
||||
Some(Event::Key(KeyEvent::new(
|
||||
KeyCode::Char('C'),
|
||||
KeyModifiers::SHIFT
|
||||
)))),
|
||||
))),
|
||||
);
|
||||
}
|
||||
|
||||
@ -1215,24 +1176,24 @@ mod tests {
|
||||
fn test_parse_basic_csi_u_encoded_key_code() {
|
||||
assert_eq!(
|
||||
parse_csi_u_encoded_key_code(b"\x1B[97u").unwrap(),
|
||||
Some(InternalEvent::Event(Event::Key(KeyEvent::new(
|
||||
Some(Event::Key(KeyEvent::new(
|
||||
KeyCode::Char('a'),
|
||||
KeyModifiers::empty()
|
||||
)))),
|
||||
))),
|
||||
);
|
||||
assert_eq!(
|
||||
parse_csi_u_encoded_key_code(b"\x1B[97;2u").unwrap(),
|
||||
Some(InternalEvent::Event(Event::Key(KeyEvent::new(
|
||||
Some(Event::Key(KeyEvent::new(
|
||||
KeyCode::Char('A'),
|
||||
KeyModifiers::SHIFT
|
||||
)))),
|
||||
))),
|
||||
);
|
||||
assert_eq!(
|
||||
parse_csi_u_encoded_key_code(b"\x1B[97;7u").unwrap(),
|
||||
Some(InternalEvent::Event(Event::Key(KeyEvent::new(
|
||||
Some(Event::Key(KeyEvent::new(
|
||||
KeyCode::Char('a'),
|
||||
KeyModifiers::ALT | KeyModifiers::CONTROL
|
||||
)))),
|
||||
))),
|
||||
);
|
||||
}
|
||||
|
||||
@ -1240,45 +1201,45 @@ mod tests {
|
||||
fn test_parse_basic_csi_u_encoded_key_code_special_keys() {
|
||||
assert_eq!(
|
||||
parse_csi_u_encoded_key_code(b"\x1B[13u").unwrap(),
|
||||
Some(InternalEvent::Event(Event::Key(KeyEvent::new(
|
||||
Some(Event::Key(KeyEvent::new(
|
||||
KeyCode::Enter,
|
||||
KeyModifiers::empty()
|
||||
)))),
|
||||
))),
|
||||
);
|
||||
assert_eq!(
|
||||
parse_csi_u_encoded_key_code(b"\x1B[27u").unwrap(),
|
||||
Some(InternalEvent::Event(Event::Key(KeyEvent::new(
|
||||
Some(Event::Key(KeyEvent::new(
|
||||
KeyCode::Esc,
|
||||
KeyModifiers::empty()
|
||||
)))),
|
||||
))),
|
||||
);
|
||||
assert_eq!(
|
||||
parse_csi_u_encoded_key_code(b"\x1B[57358u").unwrap(),
|
||||
Some(InternalEvent::Event(Event::Key(KeyEvent::new(
|
||||
Some(Event::Key(KeyEvent::new(
|
||||
KeyCode::CapsLock,
|
||||
KeyModifiers::empty()
|
||||
)))),
|
||||
))),
|
||||
);
|
||||
assert_eq!(
|
||||
parse_csi_u_encoded_key_code(b"\x1B[57376u").unwrap(),
|
||||
Some(InternalEvent::Event(Event::Key(KeyEvent::new(
|
||||
Some(Event::Key(KeyEvent::new(
|
||||
KeyCode::F(13),
|
||||
KeyModifiers::empty()
|
||||
)))),
|
||||
))),
|
||||
);
|
||||
assert_eq!(
|
||||
parse_csi_u_encoded_key_code(b"\x1B[57428u").unwrap(),
|
||||
Some(InternalEvent::Event(Event::Key(KeyEvent::new(
|
||||
Some(Event::Key(KeyEvent::new(
|
||||
KeyCode::Media(MediaKeyCode::Play),
|
||||
KeyModifiers::empty()
|
||||
)))),
|
||||
))),
|
||||
);
|
||||
assert_eq!(
|
||||
parse_csi_u_encoded_key_code(b"\x1B[57441u").unwrap(),
|
||||
Some(InternalEvent::Event(Event::Key(KeyEvent::new(
|
||||
Some(Event::Key(KeyEvent::new(
|
||||
KeyCode::Modifier(ModifierKeyCode::LeftShift),
|
||||
KeyModifiers::SHIFT,
|
||||
)))),
|
||||
))),
|
||||
);
|
||||
}
|
||||
|
||||
@ -1286,24 +1247,20 @@ mod tests {
|
||||
fn test_parse_csi_u_encoded_keypad_code() {
|
||||
assert_eq!(
|
||||
parse_csi_u_encoded_key_code(b"\x1B[57399u").unwrap(),
|
||||
Some(InternalEvent::Event(Event::Key(
|
||||
KeyEvent::new_with_kind_and_state(
|
||||
KeyCode::Char('0'),
|
||||
KeyModifiers::empty(),
|
||||
KeyEventKind::Press,
|
||||
KeyEventState::KEYPAD,
|
||||
)
|
||||
Some(Event::Key(KeyEvent::new_with_kind_and_state(
|
||||
KeyCode::Char('0'),
|
||||
KeyModifiers::empty(),
|
||||
KeyEventKind::Press,
|
||||
KeyEventState::KEYPAD,
|
||||
))),
|
||||
);
|
||||
assert_eq!(
|
||||
parse_csi_u_encoded_key_code(b"\x1B[57419u").unwrap(),
|
||||
Some(InternalEvent::Event(Event::Key(
|
||||
KeyEvent::new_with_kind_and_state(
|
||||
KeyCode::Up,
|
||||
KeyModifiers::empty(),
|
||||
KeyEventKind::Press,
|
||||
KeyEventState::KEYPAD,
|
||||
)
|
||||
Some(Event::Key(KeyEvent::new_with_kind_and_state(
|
||||
KeyCode::Up,
|
||||
KeyModifiers::empty(),
|
||||
KeyEventKind::Press,
|
||||
KeyEventState::KEYPAD,
|
||||
))),
|
||||
);
|
||||
}
|
||||
@ -1312,43 +1269,43 @@ mod tests {
|
||||
fn test_parse_csi_u_encoded_key_code_with_types() {
|
||||
assert_eq!(
|
||||
parse_csi_u_encoded_key_code(b"\x1B[97;1u").unwrap(),
|
||||
Some(InternalEvent::Event(Event::Key(KeyEvent::new_with_kind(
|
||||
Some(Event::Key(KeyEvent::new_with_kind(
|
||||
KeyCode::Char('a'),
|
||||
KeyModifiers::empty(),
|
||||
KeyEventKind::Press,
|
||||
)))),
|
||||
))),
|
||||
);
|
||||
assert_eq!(
|
||||
parse_csi_u_encoded_key_code(b"\x1B[97;1:1u").unwrap(),
|
||||
Some(InternalEvent::Event(Event::Key(KeyEvent::new_with_kind(
|
||||
Some(Event::Key(KeyEvent::new_with_kind(
|
||||
KeyCode::Char('a'),
|
||||
KeyModifiers::empty(),
|
||||
KeyEventKind::Press,
|
||||
)))),
|
||||
))),
|
||||
);
|
||||
assert_eq!(
|
||||
parse_csi_u_encoded_key_code(b"\x1B[97;5:1u").unwrap(),
|
||||
Some(InternalEvent::Event(Event::Key(KeyEvent::new_with_kind(
|
||||
Some(Event::Key(KeyEvent::new_with_kind(
|
||||
KeyCode::Char('a'),
|
||||
KeyModifiers::CONTROL,
|
||||
KeyEventKind::Press,
|
||||
)))),
|
||||
))),
|
||||
);
|
||||
assert_eq!(
|
||||
parse_csi_u_encoded_key_code(b"\x1B[97;1:2u").unwrap(),
|
||||
Some(InternalEvent::Event(Event::Key(KeyEvent::new_with_kind(
|
||||
Some(Event::Key(KeyEvent::new_with_kind(
|
||||
KeyCode::Char('a'),
|
||||
KeyModifiers::empty(),
|
||||
KeyEventKind::Repeat,
|
||||
)))),
|
||||
))),
|
||||
);
|
||||
assert_eq!(
|
||||
parse_csi_u_encoded_key_code(b"\x1B[97;1:3u").unwrap(),
|
||||
Some(InternalEvent::Event(Event::Key(KeyEvent::new_with_kind(
|
||||
Some(Event::Key(KeyEvent::new_with_kind(
|
||||
KeyCode::Char('a'),
|
||||
KeyModifiers::empty(),
|
||||
KeyEventKind::Release,
|
||||
)))),
|
||||
))),
|
||||
);
|
||||
}
|
||||
|
||||
@ -1356,40 +1313,40 @@ mod tests {
|
||||
fn test_parse_csi_u_encoded_key_code_has_modifier_on_modifier_press() {
|
||||
assert_eq!(
|
||||
parse_csi_u_encoded_key_code(b"\x1B[57449u").unwrap(),
|
||||
Some(InternalEvent::Event(Event::Key(KeyEvent::new_with_kind(
|
||||
Some(Event::Key(KeyEvent::new_with_kind(
|
||||
KeyCode::Modifier(ModifierKeyCode::RightAlt),
|
||||
KeyModifiers::ALT,
|
||||
KeyEventKind::Press,
|
||||
)))),
|
||||
))),
|
||||
);
|
||||
assert_eq!(
|
||||
parse_csi_u_encoded_key_code(b"\x1B[57449;3:3u").unwrap(),
|
||||
Some(InternalEvent::Event(Event::Key(KeyEvent::new_with_kind(
|
||||
Some(Event::Key(KeyEvent::new_with_kind(
|
||||
KeyCode::Modifier(ModifierKeyCode::RightAlt),
|
||||
KeyModifiers::ALT,
|
||||
KeyEventKind::Release,
|
||||
)))),
|
||||
))),
|
||||
);
|
||||
assert_eq!(
|
||||
parse_csi_u_encoded_key_code(b"\x1B[57450u").unwrap(),
|
||||
Some(InternalEvent::Event(Event::Key(KeyEvent::new(
|
||||
Some(Event::Key(KeyEvent::new(
|
||||
KeyCode::Modifier(ModifierKeyCode::RightSuper),
|
||||
KeyModifiers::SUPER,
|
||||
)))),
|
||||
))),
|
||||
);
|
||||
assert_eq!(
|
||||
parse_csi_u_encoded_key_code(b"\x1B[57451u").unwrap(),
|
||||
Some(InternalEvent::Event(Event::Key(KeyEvent::new(
|
||||
Some(Event::Key(KeyEvent::new(
|
||||
KeyCode::Modifier(ModifierKeyCode::RightHyper),
|
||||
KeyModifiers::HYPER,
|
||||
)))),
|
||||
))),
|
||||
);
|
||||
assert_eq!(
|
||||
parse_csi_u_encoded_key_code(b"\x1B[57452u").unwrap(),
|
||||
Some(InternalEvent::Event(Event::Key(KeyEvent::new(
|
||||
Some(Event::Key(KeyEvent::new(
|
||||
KeyCode::Modifier(ModifierKeyCode::RightMeta),
|
||||
KeyModifiers::META,
|
||||
)))),
|
||||
))),
|
||||
);
|
||||
}
|
||||
|
||||
@ -1397,24 +1354,24 @@ mod tests {
|
||||
fn test_parse_csi_u_encoded_key_code_with_extra_modifiers() {
|
||||
assert_eq!(
|
||||
parse_csi_u_encoded_key_code(b"\x1B[97;9u").unwrap(),
|
||||
Some(InternalEvent::Event(Event::Key(KeyEvent::new(
|
||||
Some(Event::Key(KeyEvent::new(
|
||||
KeyCode::Char('a'),
|
||||
KeyModifiers::SUPER
|
||||
)))),
|
||||
))),
|
||||
);
|
||||
assert_eq!(
|
||||
parse_csi_u_encoded_key_code(b"\x1B[97;17u").unwrap(),
|
||||
Some(InternalEvent::Event(Event::Key(KeyEvent::new(
|
||||
Some(Event::Key(KeyEvent::new(
|
||||
KeyCode::Char('a'),
|
||||
KeyModifiers::HYPER,
|
||||
)))),
|
||||
))),
|
||||
);
|
||||
assert_eq!(
|
||||
parse_csi_u_encoded_key_code(b"\x1B[97;33u").unwrap(),
|
||||
Some(InternalEvent::Event(Event::Key(KeyEvent::new(
|
||||
Some(Event::Key(KeyEvent::new(
|
||||
KeyCode::Char('a'),
|
||||
KeyModifiers::META,
|
||||
)))),
|
||||
))),
|
||||
);
|
||||
}
|
||||
|
||||
@ -1422,24 +1379,20 @@ mod tests {
|
||||
fn test_parse_csi_u_encoded_key_code_with_extra_state() {
|
||||
assert_eq!(
|
||||
parse_csi_u_encoded_key_code(b"\x1B[97;65u").unwrap(),
|
||||
Some(InternalEvent::Event(Event::Key(
|
||||
KeyEvent::new_with_kind_and_state(
|
||||
KeyCode::Char('a'),
|
||||
KeyModifiers::empty(),
|
||||
KeyEventKind::Press,
|
||||
KeyEventState::CAPS_LOCK,
|
||||
)
|
||||
Some(Event::Key(KeyEvent::new_with_kind_and_state(
|
||||
KeyCode::Char('a'),
|
||||
KeyModifiers::empty(),
|
||||
KeyEventKind::Press,
|
||||
KeyEventState::CAPS_LOCK,
|
||||
))),
|
||||
);
|
||||
assert_eq!(
|
||||
parse_csi_u_encoded_key_code(b"\x1B[49;129u").unwrap(),
|
||||
Some(InternalEvent::Event(Event::Key(
|
||||
KeyEvent::new_with_kind_and_state(
|
||||
KeyCode::Char('1'),
|
||||
KeyModifiers::empty(),
|
||||
KeyEventKind::Press,
|
||||
KeyEventState::NUM_LOCK,
|
||||
)
|
||||
Some(Event::Key(KeyEvent::new_with_kind_and_state(
|
||||
KeyCode::Char('1'),
|
||||
KeyModifiers::empty(),
|
||||
KeyEventKind::Press,
|
||||
KeyEventState::NUM_LOCK,
|
||||
))),
|
||||
);
|
||||
}
|
||||
@ -1449,18 +1402,18 @@ mod tests {
|
||||
assert_eq!(
|
||||
// A-S-9 is equivalent to A-(
|
||||
parse_event(b"\x1B[57:40;4u", false).unwrap(),
|
||||
Some(InternalEvent::Event(Event::Key(KeyEvent::new(
|
||||
Some(Event::Key(KeyEvent::new(
|
||||
KeyCode::Char('('),
|
||||
KeyModifiers::ALT,
|
||||
)))),
|
||||
))),
|
||||
);
|
||||
assert_eq!(
|
||||
// A-S-minus is equivalent to A-_
|
||||
parse_event(b"\x1B[45:95;4u", false).unwrap(),
|
||||
Some(InternalEvent::Event(Event::Key(KeyEvent::new(
|
||||
Some(Event::Key(KeyEvent::new(
|
||||
KeyCode::Char('_'),
|
||||
KeyModifiers::ALT,
|
||||
)))),
|
||||
))),
|
||||
);
|
||||
}
|
||||
|
||||
@ -1468,19 +1421,19 @@ mod tests {
|
||||
fn test_parse_csi_special_key_code_with_types() {
|
||||
assert_eq!(
|
||||
parse_event(b"\x1B[;1:3B", false).unwrap(),
|
||||
Some(InternalEvent::Event(Event::Key(KeyEvent::new_with_kind(
|
||||
Some(Event::Key(KeyEvent::new_with_kind(
|
||||
KeyCode::Down,
|
||||
KeyModifiers::empty(),
|
||||
KeyEventKind::Release,
|
||||
)))),
|
||||
))),
|
||||
);
|
||||
assert_eq!(
|
||||
parse_event(b"\x1B[1;1:3B", false).unwrap(),
|
||||
Some(InternalEvent::Event(Event::Key(KeyEvent::new_with_kind(
|
||||
Some(Event::Key(KeyEvent::new_with_kind(
|
||||
KeyCode::Down,
|
||||
KeyModifiers::empty(),
|
||||
KeyEventKind::Release,
|
||||
)))),
|
||||
))),
|
||||
);
|
||||
}
|
||||
|
||||
@ -1488,19 +1441,19 @@ mod tests {
|
||||
fn test_parse_csi_numbered_escape_code_with_types() {
|
||||
assert_eq!(
|
||||
parse_event(b"\x1B[5;1:3~", false).unwrap(),
|
||||
Some(InternalEvent::Event(Event::Key(KeyEvent::new_with_kind(
|
||||
Some(Event::Key(KeyEvent::new_with_kind(
|
||||
KeyCode::PageUp,
|
||||
KeyModifiers::empty(),
|
||||
KeyEventKind::Release,
|
||||
)))),
|
||||
))),
|
||||
);
|
||||
assert_eq!(
|
||||
parse_event(b"\x1B[6;5:3~", false).unwrap(),
|
||||
Some(InternalEvent::Event(Event::Key(KeyEvent::new_with_kind(
|
||||
Some(Event::Key(KeyEvent::new_with_kind(
|
||||
KeyCode::PageDown,
|
||||
KeyModifiers::CONTROL,
|
||||
KeyEventKind::Release,
|
||||
)))),
|
||||
))),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,11 +0,0 @@
|
||||
#[cfg(feature = "use-dev-tty")]
|
||||
pub(crate) mod tty;
|
||||
|
||||
#[cfg(not(feature = "use-dev-tty"))]
|
||||
pub(crate) mod mio;
|
||||
|
||||
#[cfg(feature = "use-dev-tty")]
|
||||
pub(crate) use self::tty::Waker;
|
||||
|
||||
#[cfg(not(feature = "use-dev-tty"))]
|
||||
pub(crate) use self::mio::Waker;
|
@ -1,34 +0,0 @@
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use ::mio::{Registry, Token};
|
||||
|
||||
/// Allows to wake up the `mio::Poll::poll()` method.
|
||||
/// This type wraps `mio::Waker`, for more information see its documentation.
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) struct Waker {
|
||||
inner: Arc<Mutex<::mio::Waker>>,
|
||||
}
|
||||
|
||||
impl Waker {
|
||||
/// Create a new `Waker`.
|
||||
pub(crate) fn new(registry: &Registry, waker_token: Token) -> std::io::Result<Self> {
|
||||
Ok(Self {
|
||||
inner: Arc::new(Mutex::new(mio::Waker::new(registry, waker_token)?)),
|
||||
})
|
||||
}
|
||||
|
||||
/// Wake up the [`Poll`] associated with this `Waker`.
|
||||
///
|
||||
/// Readiness is set to `Ready::readable()`.
|
||||
pub(crate) fn wake(&self) -> std::io::Result<()> {
|
||||
self.inner.lock().unwrap().wake()
|
||||
}
|
||||
|
||||
/// Resets the state so the same waker can be reused.
|
||||
///
|
||||
/// This function is not impl
|
||||
#[allow(dead_code, clippy::clippy::unnecessary_wraps)]
|
||||
pub(crate) fn reset(&self) -> std::io::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
@ -1,28 +0,0 @@
|
||||
use std::{
|
||||
io::{self, Write},
|
||||
os::unix::net::UnixStream,
|
||||
sync::{Arc, Mutex},
|
||||
};
|
||||
|
||||
/// Allows to wake up the EventSource::try_read() method.
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) struct Waker {
|
||||
inner: Arc<Mutex<UnixStream>>,
|
||||
}
|
||||
|
||||
impl Waker {
|
||||
/// Create a new `Waker`.
|
||||
pub(crate) fn new(writer: UnixStream) -> Self {
|
||||
Self {
|
||||
inner: Arc::new(Mutex::new(writer)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Wake up the [`Poll`] associated with this `Waker`.
|
||||
///
|
||||
/// Readiness is set to `Ready::readable()`.
|
||||
pub(crate) fn wake(&self) -> io::Result<()> {
|
||||
self.inner.lock().unwrap().write(&[0])?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
@ -1,48 +0,0 @@
|
||||
//! This is a WINDOWS specific implementation for input related action.
|
||||
|
||||
use std::convert::TryFrom;
|
||||
use std::io;
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
|
||||
use crossterm_winapi::{ConsoleMode, Handle};
|
||||
|
||||
pub(crate) mod parse;
|
||||
pub(crate) mod poll;
|
||||
#[cfg(feature = "event-stream")]
|
||||
pub(crate) mod waker;
|
||||
|
||||
const ENABLE_MOUSE_MODE: u32 = 0x0010 | 0x0080 | 0x0008;
|
||||
|
||||
/// This is a either `u64::MAX` if it's uninitialized or a valid `u32` that stores the original
|
||||
/// console mode if it's initialized.
|
||||
static ORIGINAL_CONSOLE_MODE: AtomicU64 = AtomicU64::new(u64::MAX);
|
||||
|
||||
/// Initializes the default console color. It will will be skipped if it has already been initialized.
|
||||
fn init_original_console_mode(original_mode: u32) {
|
||||
let _ = ORIGINAL_CONSOLE_MODE.compare_exchange(
|
||||
u64::MAX,
|
||||
u64::from(original_mode),
|
||||
Ordering::Relaxed,
|
||||
Ordering::Relaxed,
|
||||
);
|
||||
}
|
||||
|
||||
/// Returns the original console color, make sure to call `init_console_color` before calling this function. Otherwise this function will panic.
|
||||
fn original_console_mode() -> std::io::Result<u32> {
|
||||
u32::try_from(ORIGINAL_CONSOLE_MODE.load(Ordering::Relaxed))
|
||||
.map_err(|_| io::Error::new(io::ErrorKind::Other, "Initial console modes not set"))
|
||||
}
|
||||
|
||||
pub(crate) fn enable_mouse_capture() -> std::io::Result<()> {
|
||||
let mode = ConsoleMode::from(Handle::current_in_handle()?);
|
||||
init_original_console_mode(mode.mode()?);
|
||||
mode.set_mode(ENABLE_MOUSE_MODE)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn disable_mouse_capture() -> std::io::Result<()> {
|
||||
let mode = ConsoleMode::from(Handle::current_in_handle()?);
|
||||
mode.set_mode(original_console_mode()?)?;
|
||||
Ok(())
|
||||
}
|
@ -1,378 +0,0 @@
|
||||
use crossterm_winapi::{ControlKeyState, EventFlags, KeyEventRecord, ScreenBuffer};
|
||||
use winapi::um::{
|
||||
wincon::{
|
||||
CAPSLOCK_ON, LEFT_ALT_PRESSED, LEFT_CTRL_PRESSED, RIGHT_ALT_PRESSED, RIGHT_CTRL_PRESSED,
|
||||
SHIFT_PRESSED,
|
||||
},
|
||||
winuser::{
|
||||
GetForegroundWindow, GetKeyboardLayout, GetWindowThreadProcessId, ToUnicodeEx, VK_BACK,
|
||||
VK_CONTROL, VK_DELETE, VK_DOWN, VK_END, VK_ESCAPE, VK_F1, VK_F24, VK_HOME, VK_INSERT,
|
||||
VK_LEFT, VK_MENU, VK_NEXT, VK_NUMPAD0, VK_NUMPAD9, VK_PRIOR, VK_RETURN, VK_RIGHT, VK_SHIFT,
|
||||
VK_TAB, VK_UP,
|
||||
},
|
||||
};
|
||||
|
||||
use crate::event::{
|
||||
Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers, MouseButton, MouseEvent, MouseEventKind,
|
||||
};
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct MouseButtonsPressed {
|
||||
pub(crate) left: bool,
|
||||
pub(crate) right: bool,
|
||||
pub(crate) middle: bool,
|
||||
}
|
||||
|
||||
pub(crate) fn handle_mouse_event(
|
||||
mouse_event: crossterm_winapi::MouseEvent,
|
||||
buttons_pressed: &MouseButtonsPressed,
|
||||
) -> Option<Event> {
|
||||
if let Ok(Some(event)) = parse_mouse_event_record(&mouse_event, buttons_pressed) {
|
||||
return Some(Event::Mouse(event));
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
enum WindowsKeyEvent {
|
||||
KeyEvent(KeyEvent),
|
||||
Surrogate(u16),
|
||||
}
|
||||
|
||||
pub(crate) fn handle_key_event(
|
||||
key_event: KeyEventRecord,
|
||||
surrogate_buffer: &mut Option<u16>,
|
||||
) -> Option<Event> {
|
||||
let windows_key_event = parse_key_event_record(&key_event)?;
|
||||
match windows_key_event {
|
||||
WindowsKeyEvent::KeyEvent(key_event) => {
|
||||
// Discard any buffered surrogate value if another valid key event comes before the
|
||||
// next surrogate value.
|
||||
*surrogate_buffer = None;
|
||||
Some(Event::Key(key_event))
|
||||
}
|
||||
WindowsKeyEvent::Surrogate(new_surrogate) => {
|
||||
let ch = handle_surrogate(surrogate_buffer, new_surrogate)?;
|
||||
let modifiers = KeyModifiers::from(&key_event.control_key_state);
|
||||
let key_event = KeyEvent::new(KeyCode::Char(ch), modifiers);
|
||||
Some(Event::Key(key_event))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_surrogate(surrogate_buffer: &mut Option<u16>, new_surrogate: u16) -> Option<char> {
|
||||
match *surrogate_buffer {
|
||||
Some(buffered_surrogate) => {
|
||||
*surrogate_buffer = None;
|
||||
std::char::decode_utf16([buffered_surrogate, new_surrogate])
|
||||
.next()
|
||||
.unwrap()
|
||||
.ok()
|
||||
}
|
||||
None => {
|
||||
*surrogate_buffer = Some(new_surrogate);
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&ControlKeyState> for KeyModifiers {
|
||||
fn from(state: &ControlKeyState) -> Self {
|
||||
let shift = state.has_state(SHIFT_PRESSED);
|
||||
let alt = state.has_state(LEFT_ALT_PRESSED | RIGHT_ALT_PRESSED);
|
||||
let control = state.has_state(LEFT_CTRL_PRESSED | RIGHT_CTRL_PRESSED);
|
||||
|
||||
let mut modifier = KeyModifiers::empty();
|
||||
|
||||
if shift {
|
||||
modifier |= KeyModifiers::SHIFT;
|
||||
}
|
||||
if control {
|
||||
modifier |= KeyModifiers::CONTROL;
|
||||
}
|
||||
if alt {
|
||||
modifier |= KeyModifiers::ALT;
|
||||
}
|
||||
|
||||
modifier
|
||||
}
|
||||
}
|
||||
|
||||
enum CharCase {
|
||||
LowerCase,
|
||||
UpperCase,
|
||||
}
|
||||
|
||||
fn try_ensure_char_case(ch: char, desired_case: CharCase) -> char {
|
||||
match desired_case {
|
||||
CharCase::LowerCase if ch.is_uppercase() => {
|
||||
let mut iter = ch.to_lowercase();
|
||||
// Unwrap is safe; iterator yields one or more chars.
|
||||
let ch_lower = iter.next().unwrap();
|
||||
if iter.next().is_none() {
|
||||
ch_lower
|
||||
} else {
|
||||
ch
|
||||
}
|
||||
}
|
||||
CharCase::UpperCase if ch.is_lowercase() => {
|
||||
let mut iter = ch.to_uppercase();
|
||||
// Unwrap is safe; iterator yields one or more chars.
|
||||
let ch_upper = iter.next().unwrap();
|
||||
if iter.next().is_none() {
|
||||
ch_upper
|
||||
} else {
|
||||
ch
|
||||
}
|
||||
}
|
||||
_ => ch,
|
||||
}
|
||||
}
|
||||
|
||||
// Attempts to return the character for a key event accounting for the user's keyboard layout.
|
||||
// The returned character (if any) is capitalized (if applicable) based on shift and capslock state.
|
||||
// Returns None if the key doesn't map to a character or if it is a dead key.
|
||||
// We use the *currently* active keyboard layout (if it can be determined). This layout may not
|
||||
// correspond to the keyboard layout that was active when the user typed their input, since console
|
||||
// applications get their input asynchronously from the terminal. By the time a console application
|
||||
// can process a key input, the user may have changed the active layout. In this case, the character
|
||||
// returned might not correspond to what the user expects, but there is no way for a console
|
||||
// application to know what the keyboard layout actually was for a key event, so this is our best
|
||||
// effort. If a console application processes input in a timely fashion, then it is unlikely that a
|
||||
// user has time to change their keyboard layout before a key event is processed.
|
||||
fn get_char_for_key(key_event: &KeyEventRecord) -> Option<char> {
|
||||
let virtual_key_code = key_event.virtual_key_code as u32;
|
||||
let virtual_scan_code = key_event.virtual_scan_code as u32;
|
||||
let key_state = [0u8; 256];
|
||||
let mut utf16_buf = [0u16, 16];
|
||||
let dont_change_kernel_keyboard_state = 0x4;
|
||||
|
||||
// Best-effort attempt at determining the currently active keyboard layout.
|
||||
// At the time of writing, this works for a console application running in Windows Terminal, but
|
||||
// doesn't work under a Conhost terminal. For Conhost, the window handle returned by
|
||||
// GetForegroundWindow() does not appear to actually be the foreground window which has the
|
||||
// keyboard layout associated with it (or perhaps it is, but also has special protection that
|
||||
// doesn't allow us to query it).
|
||||
// When this determination fails, the returned keyboard layout handle will be null, which is an
|
||||
// acceptable input for ToUnicodeEx, as that argument is optional. In this case ToUnicodeEx
|
||||
// appears to use the keyboard layout associated with the current thread, which will be the
|
||||
// layout that was inherited when the console application started (or possibly when the current
|
||||
// thread was spawned). This is then unfortunately not updated when the user changes their
|
||||
// keyboard layout in the terminal, but it's what we get.
|
||||
let active_keyboard_layout = unsafe {
|
||||
let foreground_window = GetForegroundWindow();
|
||||
let foreground_thread = GetWindowThreadProcessId(foreground_window, std::ptr::null_mut());
|
||||
GetKeyboardLayout(foreground_thread)
|
||||
};
|
||||
|
||||
let ret = unsafe {
|
||||
ToUnicodeEx(
|
||||
virtual_key_code,
|
||||
virtual_scan_code,
|
||||
key_state.as_ptr(),
|
||||
utf16_buf.as_mut_ptr(),
|
||||
utf16_buf.len() as i32,
|
||||
dont_change_kernel_keyboard_state,
|
||||
active_keyboard_layout,
|
||||
)
|
||||
};
|
||||
|
||||
// -1 indicates a dead key.
|
||||
// 0 indicates no character for this key.
|
||||
if ret < 1 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut ch_iter = std::char::decode_utf16(utf16_buf.into_iter().take(ret as usize));
|
||||
let mut ch = ch_iter.next()?.ok()?;
|
||||
if ch_iter.next().is_some() {
|
||||
// Key doesn't map to a single char.
|
||||
return None;
|
||||
}
|
||||
|
||||
let is_shift_pressed = key_event.control_key_state.has_state(SHIFT_PRESSED);
|
||||
let is_capslock_on = key_event.control_key_state.has_state(CAPSLOCK_ON);
|
||||
let desired_case = if is_shift_pressed ^ is_capslock_on {
|
||||
CharCase::UpperCase
|
||||
} else {
|
||||
CharCase::LowerCase
|
||||
};
|
||||
ch = try_ensure_char_case(ch, desired_case);
|
||||
Some(ch)
|
||||
}
|
||||
|
||||
fn parse_key_event_record(key_event: &KeyEventRecord) -> Option<WindowsKeyEvent> {
|
||||
let modifiers = KeyModifiers::from(&key_event.control_key_state);
|
||||
let virtual_key_code = key_event.virtual_key_code as i32;
|
||||
|
||||
// We normally ignore all key release events, but we will make an exception for an Alt key
|
||||
// release if it carries a u_char value, as this indicates an Alt code.
|
||||
let is_alt_code = virtual_key_code == VK_MENU && !key_event.key_down && key_event.u_char != 0;
|
||||
if is_alt_code {
|
||||
let utf16 = key_event.u_char;
|
||||
match utf16 {
|
||||
surrogate @ 0xD800..=0xDFFF => {
|
||||
return Some(WindowsKeyEvent::Surrogate(surrogate));
|
||||
}
|
||||
unicode_scalar_value => {
|
||||
// Unwrap is safe: We tested for surrogate values above and those are the only
|
||||
// u16 values that are invalid when directly interpreted as unicode scalar
|
||||
// values.
|
||||
let ch = std::char::from_u32(unicode_scalar_value as u32).unwrap();
|
||||
let key_code = KeyCode::Char(ch);
|
||||
let kind = if key_event.key_down {
|
||||
KeyEventKind::Press
|
||||
} else {
|
||||
KeyEventKind::Release
|
||||
};
|
||||
let key_event = KeyEvent::new_with_kind(key_code, modifiers, kind);
|
||||
return Some(WindowsKeyEvent::KeyEvent(key_event));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Don't generate events for numpad key presses when they're producing Alt codes.
|
||||
let is_numpad_numeric_key = (VK_NUMPAD0..=VK_NUMPAD9).contains(&virtual_key_code);
|
||||
let is_only_alt_modifier = modifiers.contains(KeyModifiers::ALT)
|
||||
&& !modifiers.contains(KeyModifiers::SHIFT | KeyModifiers::CONTROL);
|
||||
if is_only_alt_modifier && is_numpad_numeric_key {
|
||||
return None;
|
||||
}
|
||||
|
||||
let parse_result = match virtual_key_code {
|
||||
VK_SHIFT | VK_CONTROL | VK_MENU => None,
|
||||
VK_BACK => Some(KeyCode::Backspace),
|
||||
VK_ESCAPE => Some(KeyCode::Esc),
|
||||
VK_RETURN => Some(KeyCode::Enter),
|
||||
VK_F1..=VK_F24 => Some(KeyCode::F((key_event.virtual_key_code - 111) as u8)),
|
||||
VK_LEFT => Some(KeyCode::Left),
|
||||
VK_UP => Some(KeyCode::Up),
|
||||
VK_RIGHT => Some(KeyCode::Right),
|
||||
VK_DOWN => Some(KeyCode::Down),
|
||||
VK_PRIOR => Some(KeyCode::PageUp),
|
||||
VK_NEXT => Some(KeyCode::PageDown),
|
||||
VK_HOME => Some(KeyCode::Home),
|
||||
VK_END => Some(KeyCode::End),
|
||||
VK_DELETE => Some(KeyCode::Delete),
|
||||
VK_INSERT => Some(KeyCode::Insert),
|
||||
VK_TAB if modifiers.contains(KeyModifiers::SHIFT) => Some(KeyCode::BackTab),
|
||||
VK_TAB => Some(KeyCode::Tab),
|
||||
_ => {
|
||||
let utf16 = key_event.u_char;
|
||||
match utf16 {
|
||||
0x00..=0x1f => {
|
||||
// Some key combinations generate either no u_char value or generate control
|
||||
// codes. To deliver back a KeyCode::Char(...) event we want to know which
|
||||
// character the key normally maps to on the user's keyboard layout.
|
||||
// The keys that intentionally generate control codes (ESC, ENTER, TAB, etc.)
|
||||
// are handled by their virtual key codes above.
|
||||
get_char_for_key(key_event).map(KeyCode::Char)
|
||||
}
|
||||
surrogate @ 0xD800..=0xDFFF => {
|
||||
return Some(WindowsKeyEvent::Surrogate(surrogate));
|
||||
}
|
||||
unicode_scalar_value => {
|
||||
// Unwrap is safe: We tested for surrogate values above and those are the only
|
||||
// u16 values that are invalid when directly interpreted as unicode scalar
|
||||
// values.
|
||||
let ch = std::char::from_u32(unicode_scalar_value as u32).unwrap();
|
||||
Some(KeyCode::Char(ch))
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(key_code) = parse_result {
|
||||
let kind = if key_event.key_down {
|
||||
KeyEventKind::Press
|
||||
} else {
|
||||
KeyEventKind::Release
|
||||
};
|
||||
let key_event = KeyEvent::new_with_kind(key_code, modifiers, kind);
|
||||
return Some(WindowsKeyEvent::KeyEvent(key_event));
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
// The 'y' position of a mouse event or resize event is not relative to the window but absolute to screen buffer.
|
||||
// This means that when the mouse cursor is at the top left it will be x: 0, y: 2295 (e.g. y = number of cells conting from the absolute buffer height) instead of relative x: 0, y: 0 to the window.
|
||||
pub fn parse_relative_y(y: i16) -> std::io::Result<i16> {
|
||||
let window_size = ScreenBuffer::current()?.info()?.terminal_window();
|
||||
Ok(y - window_size.top)
|
||||
}
|
||||
|
||||
fn parse_mouse_event_record(
|
||||
event: &crossterm_winapi::MouseEvent,
|
||||
buttons_pressed: &MouseButtonsPressed,
|
||||
) -> std::io::Result<Option<MouseEvent>> {
|
||||
let modifiers = KeyModifiers::from(&event.control_key_state);
|
||||
|
||||
let xpos = event.mouse_position.x as u16;
|
||||
let ypos = parse_relative_y(event.mouse_position.y)? as u16;
|
||||
|
||||
let button_state = event.button_state;
|
||||
|
||||
let kind = match event.event_flags {
|
||||
EventFlags::PressOrRelease | EventFlags::DoubleClick => {
|
||||
if button_state.left_button() && !buttons_pressed.left {
|
||||
Some(MouseEventKind::Down(MouseButton::Left))
|
||||
} else if !button_state.left_button() && buttons_pressed.left {
|
||||
Some(MouseEventKind::Up(MouseButton::Left))
|
||||
} else if button_state.right_button() && !buttons_pressed.right {
|
||||
Some(MouseEventKind::Down(MouseButton::Right))
|
||||
} else if !button_state.right_button() && buttons_pressed.right {
|
||||
Some(MouseEventKind::Up(MouseButton::Right))
|
||||
} else if button_state.middle_button() && !buttons_pressed.middle {
|
||||
Some(MouseEventKind::Down(MouseButton::Middle))
|
||||
} else if !button_state.middle_button() && buttons_pressed.middle {
|
||||
Some(MouseEventKind::Up(MouseButton::Middle))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
EventFlags::MouseMoved => {
|
||||
let button = if button_state.right_button() {
|
||||
MouseButton::Right
|
||||
} else if button_state.middle_button() {
|
||||
MouseButton::Middle
|
||||
} else {
|
||||
MouseButton::Left
|
||||
};
|
||||
if button_state.release_button() {
|
||||
Some(MouseEventKind::Moved)
|
||||
} else {
|
||||
Some(MouseEventKind::Drag(button))
|
||||
}
|
||||
}
|
||||
EventFlags::MouseWheeled => {
|
||||
// Vertical scroll
|
||||
// from https://docs.microsoft.com/en-us/windows/console/mouse-event-record-str
|
||||
// if `button_state` is negative then the wheel was rotated backward, toward the user.
|
||||
if button_state.scroll_down() {
|
||||
Some(MouseEventKind::ScrollDown)
|
||||
} else if button_state.scroll_up() {
|
||||
Some(MouseEventKind::ScrollUp)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
EventFlags::MouseHwheeled => {
|
||||
if button_state.scroll_left() {
|
||||
Some(MouseEventKind::ScrollLeft)
|
||||
} else if button_state.scroll_right() {
|
||||
Some(MouseEventKind::ScrollRight)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
|
||||
Ok(kind.map(|kind| MouseEvent {
|
||||
kind,
|
||||
column: xpos,
|
||||
row: ypos,
|
||||
modifiers,
|
||||
}))
|
||||
}
|
@ -1,86 +0,0 @@
|
||||
use std::io;
|
||||
use std::time::Duration;
|
||||
|
||||
use crossterm_winapi::Handle;
|
||||
use winapi::{
|
||||
shared::winerror::WAIT_TIMEOUT,
|
||||
um::{
|
||||
synchapi::WaitForMultipleObjects,
|
||||
winbase::{INFINITE, WAIT_ABANDONED_0, WAIT_FAILED, WAIT_OBJECT_0},
|
||||
},
|
||||
};
|
||||
|
||||
#[cfg(feature = "event-stream")]
|
||||
pub(crate) use super::waker::Waker;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct WinApiPoll {
|
||||
#[cfg(feature = "event-stream")]
|
||||
waker: Waker,
|
||||
}
|
||||
|
||||
impl WinApiPoll {
|
||||
#[cfg(not(feature = "event-stream"))]
|
||||
pub(crate) fn new() -> WinApiPoll {
|
||||
WinApiPoll {}
|
||||
}
|
||||
|
||||
#[cfg(feature = "event-stream")]
|
||||
pub(crate) fn new() -> std::io::Result<WinApiPoll> {
|
||||
Ok(WinApiPoll {
|
||||
waker: Waker::new()?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl WinApiPoll {
|
||||
pub fn poll(&mut self, timeout: Option<Duration>) -> std::io::Result<Option<bool>> {
|
||||
let dw_millis = if let Some(duration) = timeout {
|
||||
duration.as_millis() as u32
|
||||
} else {
|
||||
INFINITE
|
||||
};
|
||||
|
||||
let console_handle = Handle::current_in_handle()?;
|
||||
|
||||
#[cfg(feature = "event-stream")]
|
||||
let semaphore = self.waker.semaphore();
|
||||
#[cfg(feature = "event-stream")]
|
||||
let handles = &[*console_handle, **semaphore.handle()];
|
||||
#[cfg(not(feature = "event-stream"))]
|
||||
let handles = &[*console_handle];
|
||||
|
||||
let output =
|
||||
unsafe { WaitForMultipleObjects(handles.len() as u32, handles.as_ptr(), 0, dw_millis) };
|
||||
|
||||
match output {
|
||||
output if output == WAIT_OBJECT_0 => {
|
||||
// input handle triggered
|
||||
Ok(Some(true))
|
||||
}
|
||||
#[cfg(feature = "event-stream")]
|
||||
output if output == WAIT_OBJECT_0 + 1 => {
|
||||
// semaphore handle triggered
|
||||
let _ = self.waker.reset();
|
||||
Err(io::Error::new(
|
||||
io::ErrorKind::Interrupted,
|
||||
"Poll operation was woken up by `Waker::wake`",
|
||||
))
|
||||
}
|
||||
WAIT_TIMEOUT | WAIT_ABANDONED_0 => {
|
||||
// timeout elapsed
|
||||
Ok(None)
|
||||
}
|
||||
WAIT_FAILED => Err(io::Error::last_os_error()),
|
||||
_ => Err(io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
"WaitForMultipleObjects returned unexpected result.",
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "event-stream")]
|
||||
pub fn waker(&self) -> Waker {
|
||||
self.waker.clone()
|
||||
}
|
||||
}
|
@ -1,40 +0,0 @@
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use crossterm_winapi::Semaphore;
|
||||
|
||||
/// Allows to wake up the `WinApiPoll::poll()` method.
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) struct Waker {
|
||||
inner: Arc<Mutex<Semaphore>>,
|
||||
}
|
||||
|
||||
impl Waker {
|
||||
/// Creates a new waker.
|
||||
///
|
||||
/// `Waker` is based on the `Semaphore`. You have to use the semaphore
|
||||
/// handle along with the `WaitForMultipleObjects`.
|
||||
pub(crate) fn new() -> std::io::Result<Self> {
|
||||
let inner = Semaphore::new()?;
|
||||
|
||||
Ok(Self {
|
||||
inner: Arc::new(Mutex::new(inner)),
|
||||
})
|
||||
}
|
||||
|
||||
/// Wakes the `WaitForMultipleObjects`.
|
||||
pub(crate) fn wake(&self) -> std::io::Result<()> {
|
||||
self.inner.lock().unwrap().release()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Replaces the current semaphore with a new one allowing us to reuse the same `Waker`.
|
||||
pub(crate) fn reset(&self) -> std::io::Result<()> {
|
||||
*self.inner.lock().unwrap() = Semaphore::new()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Returns the semaphore associated with the waker.
|
||||
pub(crate) fn semaphore(&self) -> Semaphore {
|
||||
self.inner.lock().unwrap().clone()
|
||||
}
|
||||
}
|
@ -1,92 +0,0 @@
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
/// Keeps track of the elapsed time since the moment the polling started.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PollTimeout {
|
||||
timeout: Option<Duration>,
|
||||
start: Instant,
|
||||
}
|
||||
|
||||
impl PollTimeout {
|
||||
/// Constructs a new `PollTimeout` with the given optional `Duration`.
|
||||
pub fn new(timeout: Option<Duration>) -> PollTimeout {
|
||||
PollTimeout {
|
||||
timeout,
|
||||
start: Instant::now(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns whether the timeout has elapsed.
|
||||
///
|
||||
/// It always returns `false` if the initial timeout was set to `None`.
|
||||
pub fn elapsed(&self) -> bool {
|
||||
self.timeout
|
||||
.map(|timeout| self.start.elapsed() >= timeout)
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
/// Returns the timeout leftover (initial timeout duration - elapsed duration).
|
||||
pub fn leftover(&self) -> Option<Duration> {
|
||||
self.timeout.map(|timeout| {
|
||||
let elapsed = self.start.elapsed();
|
||||
|
||||
if elapsed >= timeout {
|
||||
Duration::from_secs(0)
|
||||
} else {
|
||||
timeout - elapsed
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use super::PollTimeout;
|
||||
|
||||
#[test]
|
||||
pub fn test_timeout_without_duration_does_not_have_leftover() {
|
||||
let timeout = PollTimeout::new(None);
|
||||
assert_eq!(timeout.leftover(), None)
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn test_timeout_without_duration_never_elapses() {
|
||||
let timeout = PollTimeout::new(None);
|
||||
assert!(!timeout.elapsed());
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn test_timeout_elapses() {
|
||||
const TIMEOUT_MILLIS: u64 = 100;
|
||||
|
||||
let timeout = PollTimeout {
|
||||
timeout: Some(Duration::from_millis(TIMEOUT_MILLIS)),
|
||||
start: Instant::now() - Duration::from_millis(2 * TIMEOUT_MILLIS),
|
||||
};
|
||||
|
||||
assert!(timeout.elapsed());
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn test_elapsed_timeout_has_zero_leftover() {
|
||||
const TIMEOUT_MILLIS: u64 = 100;
|
||||
|
||||
let timeout = PollTimeout {
|
||||
timeout: Some(Duration::from_millis(TIMEOUT_MILLIS)),
|
||||
start: Instant::now() - Duration::from_millis(2 * TIMEOUT_MILLIS),
|
||||
};
|
||||
|
||||
assert!(timeout.elapsed());
|
||||
assert_eq!(timeout.leftover(), Some(Duration::from_millis(0)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn test_not_elapsed_timeout_has_positive_leftover() {
|
||||
let timeout = PollTimeout::new(Some(Duration::from_secs(60)));
|
||||
|
||||
assert!(!timeout.elapsed());
|
||||
assert!(timeout.leftover().unwrap() > Duration::from_secs(0));
|
||||
}
|
||||
}
|
15
src/lib.rs
15
src/lib.rs
@ -241,20 +241,5 @@ pub mod style;
|
||||
/// A module to work with the terminal.
|
||||
pub mod terminal;
|
||||
|
||||
/// A module to query if the current instance is a tty.
|
||||
pub mod tty;
|
||||
|
||||
#[cfg(windows)]
|
||||
/// A module that exposes one function to check if the current terminal supports ANSI sequences.
|
||||
pub mod ansi_support;
|
||||
mod command;
|
||||
pub(crate) mod macros;
|
||||
|
||||
#[cfg(all(windows, not(feature = "windows")))]
|
||||
compile_error!("Compiling on Windows with \"windows\" feature disabled. Feature \"windows\" should only be disabled when project will never be compiled on Windows.");
|
||||
|
||||
#[cfg(all(winapi, not(feature = "winapi")))]
|
||||
compile_error!("Compiling on Windows with \"winapi\" feature disabled. Feature \"winapi\" should only be disabled when project will never be compiled on Windows.");
|
||||
|
||||
#[cfg(all(crossterm_winapi, not(feature = "crossterm_winapi")))]
|
||||
compile_error!("Compiling on Windows with \"crossterm_winapi\" feature disabled. Feature \"crossterm_winapi\" should only be disabled when project will never be compiled on Windows.");
|
||||
|
12
src/style.rs
12
src/style.rs
@ -180,18 +180,6 @@ pub fn available_color_count() -> u16 {
|
||||
})
|
||||
}
|
||||
|
||||
/// Forces colored output on or off globally, overriding NO_COLOR.
|
||||
///
|
||||
/// # Notes
|
||||
///
|
||||
/// crossterm supports NO_COLOR (https://no-color.org/) to disabled colored output.
|
||||
///
|
||||
/// This API allows applications to override that behavior and force colorized output
|
||||
/// even if NO_COLOR is set.
|
||||
pub fn force_color_output(enabled: bool) {
|
||||
Colored::set_ansi_color_disabled(!enabled)
|
||||
}
|
||||
|
||||
/// A command that sets the the foreground color.
|
||||
///
|
||||
/// See [`Color`](enum.Color.html) for more info.
|
||||
|
@ -1,6 +1,4 @@
|
||||
use parking_lot::Once;
|
||||
use std::fmt::{self, Formatter};
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
use serde::{Deserialize, Serialize};
|
||||
@ -23,9 +21,6 @@ pub enum Colored {
|
||||
UnderlineColor(Color),
|
||||
}
|
||||
|
||||
static ANSI_COLOR_DISABLED: AtomicBool = AtomicBool::new(false);
|
||||
static INITIALIZER: Once = Once::new();
|
||||
|
||||
impl Colored {
|
||||
/// Parse an ANSI foreground or background color.
|
||||
/// This is the string that would appear within an `ESC [ <str> m` escape sequence, as found in
|
||||
@ -69,38 +64,12 @@ impl Colored {
|
||||
|
||||
Some(output)
|
||||
}
|
||||
|
||||
/// Checks whether ansi color sequences are disabled by setting of NO_COLOR
|
||||
/// in environment as per https://no-color.org/
|
||||
pub fn ansi_color_disabled() -> bool {
|
||||
!std::env::var("NO_COLOR")
|
||||
.unwrap_or("".to_string())
|
||||
.is_empty()
|
||||
}
|
||||
|
||||
pub fn ansi_color_disabled_memoized() -> bool {
|
||||
INITIALIZER.call_once(|| {
|
||||
ANSI_COLOR_DISABLED.store(Self::ansi_color_disabled(), Ordering::SeqCst);
|
||||
});
|
||||
|
||||
ANSI_COLOR_DISABLED.load(Ordering::SeqCst)
|
||||
}
|
||||
|
||||
pub fn set_ansi_color_disabled(val: bool) {
|
||||
// Force the one-time initializer to run.
|
||||
_ = Self::ansi_color_disabled_memoized();
|
||||
ANSI_COLOR_DISABLED.store(val, Ordering::SeqCst);
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Colored {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
let color;
|
||||
|
||||
if Self::ansi_color_disabled_memoized() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
match *self {
|
||||
Colored::ForegroundColor(new_color) => {
|
||||
if new_color == Color::Reset {
|
||||
|
103
src/terminal.rs
103
src/terminal.rs
@ -83,60 +83,15 @@
|
||||
//!
|
||||
//! For manual execution control check out [crossterm::queue](../macro.queue.html).
|
||||
|
||||
use std::{fmt, io};
|
||||
use std::fmt;
|
||||
|
||||
#[cfg(windows)]
|
||||
use crossterm_winapi::{ConsoleMode, Handle, ScreenBuffer};
|
||||
#[cfg(feature = "serde")]
|
||||
use serde::{Deserialize, Serialize};
|
||||
#[cfg(windows)]
|
||||
use winapi::um::wincon::ENABLE_WRAP_AT_EOL_OUTPUT;
|
||||
|
||||
#[doc(no_inline)]
|
||||
use crate::Command;
|
||||
use crate::{csi, impl_display};
|
||||
|
||||
pub(crate) mod sys;
|
||||
|
||||
#[cfg(feature = "events")]
|
||||
pub use sys::supports_keyboard_enhancement;
|
||||
|
||||
/// Tells whether the raw mode is enabled.
|
||||
///
|
||||
/// Please have a look at the [raw mode](./index.html#raw-mode) section.
|
||||
pub fn is_raw_mode_enabled() -> io::Result<bool> {
|
||||
#[cfg(unix)]
|
||||
{
|
||||
Ok(sys::is_raw_mode_enabled())
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
{
|
||||
sys::is_raw_mode_enabled()
|
||||
}
|
||||
}
|
||||
|
||||
/// Enables raw mode.
|
||||
///
|
||||
/// Please have a look at the [raw mode](./index.html#raw-mode) section.
|
||||
pub fn enable_raw_mode() -> io::Result<()> {
|
||||
sys::enable_raw_mode()
|
||||
}
|
||||
|
||||
/// Disables raw mode.
|
||||
///
|
||||
/// Please have a look at the [raw mode](./index.html#raw-mode) section.
|
||||
pub fn disable_raw_mode() -> io::Result<()> {
|
||||
sys::disable_raw_mode()
|
||||
}
|
||||
|
||||
/// Returns the terminal size `(columns, rows)`.
|
||||
///
|
||||
/// The top left cell is represented `(1, 1)`.
|
||||
pub fn size() -> io::Result<(u16, u16)> {
|
||||
sys::size()
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct WindowSize {
|
||||
pub rows: u16,
|
||||
@ -145,15 +100,6 @@ pub struct WindowSize {
|
||||
pub height: u16,
|
||||
}
|
||||
|
||||
/// Returns the terminal size `[WindowSize]`.
|
||||
///
|
||||
/// The width and height in pixels may not be reliably implemented or default to 0.
|
||||
/// For unix, https://man7.org/linux/man-pages/man4/tty_ioctl.4.html documents them as "unused".
|
||||
/// For windows it is not implemented.
|
||||
pub fn window_size() -> io::Result<WindowSize> {
|
||||
sys::window_size()
|
||||
}
|
||||
|
||||
/// Disables line wrapping.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct DisableLineWrap;
|
||||
@ -301,11 +247,6 @@ impl Command for ScrollUp {
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn execute_winapi(&self) -> io::Result<()> {
|
||||
sys::scroll_up(self.0)
|
||||
}
|
||||
}
|
||||
|
||||
/// A command that scrolls the terminal screen a given number of rows down.
|
||||
@ -323,11 +264,6 @@ impl Command for ScrollDown {
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn execute_winapi(&self) -> io::Result<()> {
|
||||
sys::scroll_down(self.0)
|
||||
}
|
||||
}
|
||||
|
||||
/// A command that clears the terminal screen buffer.
|
||||
@ -351,11 +287,6 @@ impl Command for Clear {
|
||||
ClearType::UntilNewLine => csi!("K"),
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn execute_winapi(&self) -> io::Result<()> {
|
||||
sys::clear(self.0)
|
||||
}
|
||||
}
|
||||
|
||||
/// A command that sets the terminal buffer size `(columns, rows)`.
|
||||
@ -370,11 +301,6 @@ impl Command for SetSize {
|
||||
fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result {
|
||||
write!(f, csi!("8;{};{}t"), self.1, self.0)
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn execute_winapi(&self) -> io::Result<()> {
|
||||
sys::set_size(self.0, self.1)
|
||||
}
|
||||
}
|
||||
|
||||
/// A command that sets the terminal title
|
||||
@ -389,11 +315,6 @@ impl<T: fmt::Display> Command for SetTitle<T> {
|
||||
fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result {
|
||||
write!(f, "\x1B]0;{}\x07", &self.0)
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn execute_winapi(&self) -> io::Result<()> {
|
||||
sys::set_window_title(&self.0)
|
||||
}
|
||||
}
|
||||
|
||||
/// A command that instructs the terminal emulator to begin a synchronized frame.
|
||||
@ -436,17 +357,6 @@ impl Command for BeginSynchronizedUpdate {
|
||||
fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result {
|
||||
f.write_str(csi!("?2026h"))
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn execute_winapi(&self) -> io::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
#[inline]
|
||||
fn is_ansi_code_supported(&self) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
/// A command that instructs the terminal to end a synchronized frame.
|
||||
@ -489,17 +399,6 @@ impl Command for EndSynchronizedUpdate {
|
||||
fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result {
|
||||
f.write_str(csi!("?2026l"))
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn execute_winapi(&self) -> io::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
#[inline]
|
||||
fn is_ansi_code_supported(&self) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
impl_display!(for ScrollUp);
|
||||
|
@ -1,27 +0,0 @@
|
||||
//! This module provides platform related functions.
|
||||
|
||||
#[cfg(unix)]
|
||||
#[cfg(feature = "events")]
|
||||
pub use self::unix::supports_keyboard_enhancement;
|
||||
#[cfg(unix)]
|
||||
pub(crate) use self::unix::{
|
||||
disable_raw_mode, enable_raw_mode, is_raw_mode_enabled, size, window_size,
|
||||
};
|
||||
#[cfg(windows)]
|
||||
#[cfg(feature = "events")]
|
||||
pub use self::windows::supports_keyboard_enhancement;
|
||||
#[cfg(all(windows, test))]
|
||||
pub(crate) use self::windows::temp_screen_buffer;
|
||||
#[cfg(windows)]
|
||||
pub(crate) use self::windows::{
|
||||
clear, disable_raw_mode, enable_raw_mode, is_raw_mode_enabled, scroll_down, scroll_up,
|
||||
set_size, set_window_title, size, window_size,
|
||||
};
|
||||
|
||||
#[cfg(windows)]
|
||||
mod windows;
|
||||
|
||||
#[cfg(unix)]
|
||||
pub mod file_descriptor;
|
||||
#[cfg(unix)]
|
||||
mod unix;
|
@ -1,154 +0,0 @@
|
||||
use std::io;
|
||||
|
||||
#[cfg(feature = "libc")]
|
||||
use libc::size_t;
|
||||
#[cfg(not(feature = "libc"))]
|
||||
use rustix::fd::{AsFd, AsRawFd, BorrowedFd, OwnedFd, RawFd};
|
||||
#[cfg(feature = "libc")]
|
||||
use std::{
|
||||
fs,
|
||||
marker::PhantomData,
|
||||
os::unix::{
|
||||
io::{IntoRawFd, RawFd},
|
||||
prelude::AsRawFd,
|
||||
},
|
||||
};
|
||||
|
||||
/// A file descriptor wrapper.
|
||||
///
|
||||
/// It allows to retrieve raw file descriptor, write to the file descriptor and
|
||||
/// mainly it closes the file descriptor once dropped.
|
||||
#[derive(Debug)]
|
||||
#[cfg(feature = "libc")]
|
||||
pub struct FileDesc<'a> {
|
||||
fd: RawFd,
|
||||
close_on_drop: bool,
|
||||
phantom: PhantomData<&'a ()>,
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "libc"))]
|
||||
pub enum FileDesc<'a> {
|
||||
Owned(OwnedFd),
|
||||
Borrowed(BorrowedFd<'a>),
|
||||
}
|
||||
|
||||
#[cfg(feature = "libc")]
|
||||
impl FileDesc<'_> {
|
||||
/// Constructs a new `FileDesc` with the given `RawFd`.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `fd` - raw file descriptor
|
||||
/// * `close_on_drop` - specify if the raw file descriptor should be closed once the `FileDesc` is dropped
|
||||
pub fn new(fd: RawFd, close_on_drop: bool) -> FileDesc<'static> {
|
||||
FileDesc {
|
||||
fd,
|
||||
close_on_drop,
|
||||
phantom: PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn read(&self, buffer: &mut [u8]) -> io::Result<usize> {
|
||||
let result = unsafe {
|
||||
libc::read(
|
||||
self.fd,
|
||||
buffer.as_mut_ptr() as *mut libc::c_void,
|
||||
buffer.len() as size_t,
|
||||
)
|
||||
};
|
||||
|
||||
if result < 0 {
|
||||
Err(io::Error::last_os_error())
|
||||
} else {
|
||||
Ok(result as usize)
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the underlying file descriptor.
|
||||
pub fn raw_fd(&self) -> RawFd {
|
||||
self.fd
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "libc"))]
|
||||
impl FileDesc<'_> {
|
||||
pub fn read(&self, buffer: &mut [u8]) -> io::Result<usize> {
|
||||
let fd = match self {
|
||||
FileDesc::Owned(fd) => fd.as_fd(),
|
||||
FileDesc::Borrowed(fd) => fd.as_fd(),
|
||||
};
|
||||
let result = rustix::io::read(fd, buffer)?;
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
pub fn raw_fd(&self) -> RawFd {
|
||||
match self {
|
||||
FileDesc::Owned(fd) => fd.as_raw_fd(),
|
||||
FileDesc::Borrowed(fd) => fd.as_raw_fd(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "libc")]
|
||||
impl Drop for FileDesc<'_> {
|
||||
fn drop(&mut self) {
|
||||
if self.close_on_drop {
|
||||
// Note that errors are ignored when closing a file descriptor. The
|
||||
// reason for this is that if an error occurs we don't actually know if
|
||||
// the file descriptor was closed or not, and if we retried (for
|
||||
// something like EINTR), we might close another valid file descriptor
|
||||
// opened after we closed ours.
|
||||
let _ = unsafe { libc::close(self.fd) };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRawFd for FileDesc<'_> {
|
||||
fn as_raw_fd(&self) -> RawFd {
|
||||
self.raw_fd()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "libc"))]
|
||||
impl AsFd for FileDesc<'_> {
|
||||
fn as_fd(&self) -> BorrowedFd<'_> {
|
||||
match self {
|
||||
FileDesc::Owned(fd) => fd.as_fd(),
|
||||
FileDesc::Borrowed(fd) => fd.as_fd(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "libc")]
|
||||
/// Creates a file descriptor pointing to the standard input or `/dev/tty`.
|
||||
pub fn tty_fd() -> io::Result<FileDesc<'static>> {
|
||||
let (fd, close_on_drop) = if unsafe { libc::isatty(libc::STDIN_FILENO) == 1 } {
|
||||
(libc::STDIN_FILENO, false)
|
||||
} else {
|
||||
(
|
||||
fs::OpenOptions::new()
|
||||
.read(true)
|
||||
.write(true)
|
||||
.open("/dev/tty")?
|
||||
.into_raw_fd(),
|
||||
true,
|
||||
)
|
||||
};
|
||||
|
||||
Ok(FileDesc::new(fd, close_on_drop))
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "libc"))]
|
||||
/// Creates a file descriptor pointing to the standard input or `/dev/tty`.
|
||||
pub fn tty_fd() -> io::Result<FileDesc<'static>> {
|
||||
use std::fs::File;
|
||||
|
||||
let stdin = rustix::stdio::stdin();
|
||||
let fd = if rustix::termios::isatty(stdin) {
|
||||
FileDesc::Borrowed(stdin)
|
||||
} else {
|
||||
let dev_tty = File::options().read(true).write(true).open("/dev/tty")?;
|
||||
FileDesc::Owned(dev_tty.into())
|
||||
};
|
||||
Ok(fd)
|
||||
}
|
@ -1,315 +0,0 @@
|
||||
//! UNIX related logic for terminal manipulation.
|
||||
|
||||
use crate::terminal::{
|
||||
sys::file_descriptor::{tty_fd, FileDesc},
|
||||
WindowSize,
|
||||
};
|
||||
#[cfg(feature = "libc")]
|
||||
use libc::{
|
||||
cfmakeraw, ioctl, tcgetattr, tcsetattr, termios as Termios, winsize, STDOUT_FILENO, TCSANOW,
|
||||
TIOCGWINSZ,
|
||||
};
|
||||
use parking_lot::Mutex;
|
||||
#[cfg(not(feature = "libc"))]
|
||||
use rustix::{
|
||||
fd::AsFd,
|
||||
termios::{Termios, Winsize},
|
||||
};
|
||||
|
||||
use std::{fs::File, io, process};
|
||||
#[cfg(feature = "libc")]
|
||||
use std::{
|
||||
mem,
|
||||
os::unix::io::{IntoRawFd, RawFd},
|
||||
};
|
||||
|
||||
// Some(Termios) -> we're in the raw mode and this is the previous mode
|
||||
// None -> we're not in the raw mode
|
||||
static TERMINAL_MODE_PRIOR_RAW_MODE: Mutex<Option<Termios>> = parking_lot::const_mutex(None);
|
||||
|
||||
pub(crate) fn is_raw_mode_enabled() -> bool {
|
||||
TERMINAL_MODE_PRIOR_RAW_MODE.lock().is_some()
|
||||
}
|
||||
|
||||
#[cfg(feature = "libc")]
|
||||
impl From<winsize> for WindowSize {
|
||||
fn from(size: winsize) -> WindowSize {
|
||||
WindowSize {
|
||||
columns: size.ws_col,
|
||||
rows: size.ws_row,
|
||||
width: size.ws_xpixel,
|
||||
height: size.ws_ypixel,
|
||||
}
|
||||
}
|
||||
}
|
||||
#[cfg(not(feature = "libc"))]
|
||||
impl From<Winsize> for WindowSize {
|
||||
fn from(size: Winsize) -> WindowSize {
|
||||
WindowSize {
|
||||
columns: size.ws_col,
|
||||
rows: size.ws_row,
|
||||
width: size.ws_xpixel,
|
||||
height: size.ws_ypixel,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::useless_conversion)]
|
||||
#[cfg(feature = "libc")]
|
||||
pub(crate) fn window_size() -> io::Result<WindowSize> {
|
||||
// http://rosettacode.org/wiki/Terminal_control/Dimensions#Library:_BSD_libc
|
||||
let mut size = winsize {
|
||||
ws_row: 0,
|
||||
ws_col: 0,
|
||||
ws_xpixel: 0,
|
||||
ws_ypixel: 0,
|
||||
};
|
||||
|
||||
let file = File::open("/dev/tty").map(|file| (FileDesc::new(file.into_raw_fd(), true)));
|
||||
let fd = if let Ok(file) = &file {
|
||||
file.raw_fd()
|
||||
} else {
|
||||
// Fallback to libc::STDOUT_FILENO if /dev/tty is missing
|
||||
STDOUT_FILENO
|
||||
};
|
||||
|
||||
if wrap_with_result(unsafe { ioctl(fd, TIOCGWINSZ.into(), &mut size) }).is_ok() {
|
||||
return Ok(size.into());
|
||||
}
|
||||
|
||||
Err(std::io::Error::last_os_error().into())
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "libc"))]
|
||||
pub(crate) fn window_size() -> io::Result<WindowSize> {
|
||||
let file = File::open("/dev/tty").map(|file| (FileDesc::Owned(file.into())));
|
||||
let fd = if let Ok(file) = &file {
|
||||
file.as_fd()
|
||||
} else {
|
||||
// Fallback to libc::STDOUT_FILENO if /dev/tty is missing
|
||||
rustix::stdio::stdout()
|
||||
};
|
||||
let size = rustix::termios::tcgetwinsize(fd)?;
|
||||
Ok(size.into())
|
||||
}
|
||||
|
||||
#[allow(clippy::useless_conversion)]
|
||||
pub(crate) fn size() -> io::Result<(u16, u16)> {
|
||||
if let Ok(window_size) = window_size() {
|
||||
return Ok((window_size.columns, window_size.rows));
|
||||
}
|
||||
|
||||
tput_size().ok_or_else(|| std::io::Error::last_os_error().into())
|
||||
}
|
||||
|
||||
#[cfg(feature = "libc")]
|
||||
pub(crate) fn enable_raw_mode() -> io::Result<()> {
|
||||
let mut original_mode = TERMINAL_MODE_PRIOR_RAW_MODE.lock();
|
||||
if original_mode.is_some() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let tty = tty_fd()?;
|
||||
let fd = tty.raw_fd();
|
||||
let mut ios = get_terminal_attr(fd)?;
|
||||
let original_mode_ios = ios;
|
||||
raw_terminal_attr(&mut ios);
|
||||
set_terminal_attr(fd, &ios)?;
|
||||
// Keep it last - set the original mode only if we were able to switch to the raw mode
|
||||
*original_mode = Some(original_mode_ios);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "libc"))]
|
||||
pub(crate) fn enable_raw_mode() -> io::Result<()> {
|
||||
let mut original_mode = TERMINAL_MODE_PRIOR_RAW_MODE.lock();
|
||||
if original_mode.is_some() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let tty = tty_fd()?;
|
||||
let mut ios = get_terminal_attr(&tty)?;
|
||||
let original_mode_ios = ios.clone();
|
||||
ios.make_raw();
|
||||
set_terminal_attr(&tty, &ios)?;
|
||||
// Keep it last - set the original mode only if we were able to switch to the raw mode
|
||||
*original_mode = Some(original_mode_ios);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Reset the raw mode.
|
||||
///
|
||||
/// More precisely, reset the whole termios mode to what it was before the first call
|
||||
/// to [enable_raw_mode]. If you don't mess with termios outside of crossterm, it's
|
||||
/// effectively disabling the raw mode and doing nothing else.
|
||||
#[cfg(feature = "libc")]
|
||||
pub(crate) fn disable_raw_mode() -> io::Result<()> {
|
||||
let mut original_mode = TERMINAL_MODE_PRIOR_RAW_MODE.lock();
|
||||
if let Some(original_mode_ios) = original_mode.as_ref() {
|
||||
let tty = tty_fd()?;
|
||||
set_terminal_attr(tty.raw_fd(), original_mode_ios)?;
|
||||
// Keep it last - remove the original mode only if we were able to switch back
|
||||
*original_mode = None;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "libc"))]
|
||||
pub(crate) fn disable_raw_mode() -> io::Result<()> {
|
||||
let mut original_mode = TERMINAL_MODE_PRIOR_RAW_MODE.lock();
|
||||
if let Some(original_mode_ios) = original_mode.as_ref() {
|
||||
let tty = tty_fd()?;
|
||||
set_terminal_attr(&tty, original_mode_ios)?;
|
||||
// Keep it last - remove the original mode only if we were able to switch back
|
||||
*original_mode = None;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "libc"))]
|
||||
fn get_terminal_attr(fd: impl AsFd) -> io::Result<Termios> {
|
||||
let result = rustix::termios::tcgetattr(fd)?;
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "libc"))]
|
||||
fn set_terminal_attr(fd: impl AsFd, termios: &Termios) -> io::Result<()> {
|
||||
rustix::termios::tcsetattr(fd, rustix::termios::OptionalActions::Now, termios)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Queries the terminal's support for progressive keyboard enhancement.
|
||||
///
|
||||
/// On unix systems, this function will block and possibly time out while
|
||||
/// [`crossterm::event::read`](crate::event::read) or [`crossterm::event::poll`](crate::event::poll) are being called.
|
||||
#[cfg(feature = "events")]
|
||||
pub fn supports_keyboard_enhancement() -> io::Result<bool> {
|
||||
if is_raw_mode_enabled() {
|
||||
read_supports_keyboard_enhancement_raw()
|
||||
} else {
|
||||
read_supports_keyboard_enhancement_flags()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "events")]
|
||||
fn read_supports_keyboard_enhancement_flags() -> io::Result<bool> {
|
||||
enable_raw_mode()?;
|
||||
let flags = read_supports_keyboard_enhancement_raw();
|
||||
disable_raw_mode()?;
|
||||
flags
|
||||
}
|
||||
|
||||
#[cfg(feature = "events")]
|
||||
fn read_supports_keyboard_enhancement_raw() -> io::Result<bool> {
|
||||
use crate::event::{
|
||||
filter::{KeyboardEnhancementFlagsFilter, PrimaryDeviceAttributesFilter},
|
||||
poll_internal, read_internal, InternalEvent,
|
||||
};
|
||||
use std::io::Write;
|
||||
use std::time::Duration;
|
||||
|
||||
// This is the recommended method for testing support for the keyboard enhancement protocol.
|
||||
// We send a query for the flags supported by the terminal and then the primary device attributes
|
||||
// query. If we receive the primary device attributes response but not the keyboard enhancement
|
||||
// flags, none of the flags are supported.
|
||||
//
|
||||
// See <https://sw.kovidgoyal.net/kitty/keyboard-protocol/#detection-of-support-for-this-protocol>
|
||||
|
||||
// ESC [ ? u Query progressive keyboard enhancement flags (kitty protocol).
|
||||
// ESC [ c Query primary device attributes.
|
||||
const QUERY: &[u8] = b"\x1B[?u\x1B[c";
|
||||
|
||||
let result = File::open("/dev/tty").and_then(|mut file| {
|
||||
file.write_all(QUERY)?;
|
||||
file.flush()
|
||||
});
|
||||
if result.is_err() {
|
||||
let mut stdout = io::stdout();
|
||||
stdout.write_all(QUERY)?;
|
||||
stdout.flush()?;
|
||||
}
|
||||
|
||||
loop {
|
||||
match poll_internal(
|
||||
Some(Duration::from_millis(2000)),
|
||||
&KeyboardEnhancementFlagsFilter,
|
||||
) {
|
||||
Ok(true) => {
|
||||
match read_internal(&KeyboardEnhancementFlagsFilter) {
|
||||
Ok(InternalEvent::KeyboardEnhancementFlags(_current_flags)) => {
|
||||
// Flush the PrimaryDeviceAttributes out of the event queue.
|
||||
read_internal(&PrimaryDeviceAttributesFilter).ok();
|
||||
return Ok(true);
|
||||
}
|
||||
_ => return Ok(false),
|
||||
}
|
||||
}
|
||||
Ok(false) => {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
"The keyboard enhancement status could not be read within a normal duration",
|
||||
));
|
||||
}
|
||||
Err(_) => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// execute tput with the given argument and parse
|
||||
/// the output as a u16.
|
||||
///
|
||||
/// The arg should be "cols" or "lines"
|
||||
fn tput_value(arg: &str) -> Option<u16> {
|
||||
let output = process::Command::new("tput").arg(arg).output().ok()?;
|
||||
let value = output
|
||||
.stdout
|
||||
.into_iter()
|
||||
.filter_map(|b| char::from(b).to_digit(10))
|
||||
.fold(0, |v, n| v * 10 + n as u16);
|
||||
|
||||
if value > 0 {
|
||||
Some(value)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the size of the screen as determined by tput.
|
||||
///
|
||||
/// This alternate way of computing the size is useful
|
||||
/// when in a subshell.
|
||||
fn tput_size() -> Option<(u16, u16)> {
|
||||
match (tput_value("cols"), tput_value("lines")) {
|
||||
(Some(w), Some(h)) => Some((w, h)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "libc")]
|
||||
// Transform the given mode into an raw mode (non-canonical) mode.
|
||||
fn raw_terminal_attr(termios: &mut Termios) {
|
||||
unsafe { cfmakeraw(termios) }
|
||||
}
|
||||
|
||||
#[cfg(feature = "libc")]
|
||||
fn get_terminal_attr(fd: RawFd) -> io::Result<Termios> {
|
||||
unsafe {
|
||||
let mut termios = mem::zeroed();
|
||||
wrap_with_result(tcgetattr(fd, &mut termios))?;
|
||||
Ok(termios)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "libc")]
|
||||
fn set_terminal_attr(fd: RawFd, termios: &Termios) -> io::Result<()> {
|
||||
wrap_with_result(unsafe { tcsetattr(fd, TCSANOW, termios) })
|
||||
}
|
||||
|
||||
#[cfg(feature = "libc")]
|
||||
fn wrap_with_result(result: i32) -> io::Result<()> {
|
||||
if result == -1 {
|
||||
Err(io::Error::last_os_error())
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
@ -1,471 +0,0 @@
|
||||
//! WinAPI related logic for terminal manipulation.
|
||||
|
||||
use std::fmt::{self, Write};
|
||||
use std::io::{self};
|
||||
|
||||
use crossterm_winapi::{Console, ConsoleMode, Coord, Handle, ScreenBuffer, Size};
|
||||
use winapi::{
|
||||
shared::minwindef::DWORD,
|
||||
um::wincon::{SetConsoleTitleW, ENABLE_ECHO_INPUT, ENABLE_LINE_INPUT, ENABLE_PROCESSED_INPUT},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
cursor,
|
||||
terminal::{ClearType, WindowSize},
|
||||
};
|
||||
|
||||
/// bits which can't be set in raw mode
|
||||
const NOT_RAW_MODE_MASK: DWORD = ENABLE_LINE_INPUT | ENABLE_ECHO_INPUT | ENABLE_PROCESSED_INPUT;
|
||||
|
||||
pub(crate) fn is_raw_mode_enabled() -> std::io::Result<bool> {
|
||||
let console_mode = ConsoleMode::from(Handle::current_in_handle()?);
|
||||
|
||||
let dw_mode = console_mode.mode()?;
|
||||
|
||||
Ok(
|
||||
// check none of the "not raw" bits is set
|
||||
dw_mode & NOT_RAW_MODE_MASK == 0,
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn enable_raw_mode() -> std::io::Result<()> {
|
||||
let console_mode = ConsoleMode::from(Handle::current_in_handle()?);
|
||||
|
||||
let dw_mode = console_mode.mode()?;
|
||||
|
||||
let new_mode = dw_mode & !NOT_RAW_MODE_MASK;
|
||||
|
||||
console_mode.set_mode(new_mode)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn disable_raw_mode() -> std::io::Result<()> {
|
||||
let console_mode = ConsoleMode::from(Handle::current_in_handle()?);
|
||||
|
||||
let dw_mode = console_mode.mode()?;
|
||||
|
||||
let new_mode = dw_mode | NOT_RAW_MODE_MASK;
|
||||
|
||||
console_mode.set_mode(new_mode)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn size() -> io::Result<(u16, u16)> {
|
||||
let terminal_size = ScreenBuffer::current()?.info()?.terminal_size();
|
||||
// windows starts counting at 0, unix at 1, add one to replicated unix behaviour.
|
||||
Ok((
|
||||
(terminal_size.width + 1) as u16,
|
||||
(terminal_size.height + 1) as u16,
|
||||
))
|
||||
}
|
||||
|
||||
pub(crate) fn window_size() -> io::Result<WindowSize> {
|
||||
Err(io::Error::new(
|
||||
io::ErrorKind::Unsupported,
|
||||
"Window pixel size not implemented for the Windows API.",
|
||||
))
|
||||
}
|
||||
|
||||
/// Queries the terminal's support for progressive keyboard enhancement.
|
||||
///
|
||||
/// This always returns `Ok(false)` on Windows.
|
||||
#[cfg(feature = "events")]
|
||||
pub fn supports_keyboard_enhancement() -> std::io::Result<bool> {
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
pub(crate) fn clear(clear_type: ClearType) -> std::io::Result<()> {
|
||||
let screen_buffer = ScreenBuffer::current()?;
|
||||
let csbi = screen_buffer.info()?;
|
||||
|
||||
let pos = csbi.cursor_pos();
|
||||
let buffer_size = csbi.buffer_size();
|
||||
let current_attribute = csbi.attributes();
|
||||
|
||||
match clear_type {
|
||||
ClearType::All => {
|
||||
clear_entire_screen(buffer_size, current_attribute)?;
|
||||
}
|
||||
ClearType::FromCursorDown => clear_after_cursor(pos, buffer_size, current_attribute)?,
|
||||
ClearType::FromCursorUp => clear_before_cursor(pos, buffer_size, current_attribute)?,
|
||||
ClearType::CurrentLine => clear_current_line(pos, buffer_size, current_attribute)?,
|
||||
ClearType::UntilNewLine => clear_until_line(pos, buffer_size, current_attribute)?,
|
||||
_ => {
|
||||
clear_entire_screen(buffer_size, current_attribute)?;
|
||||
} //TODO: make purge flush the entire screen buffer not just the visible window.
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn scroll_up(row_count: u16) -> std::io::Result<()> {
|
||||
let csbi = ScreenBuffer::current()?;
|
||||
let mut window = csbi.info()?.terminal_window();
|
||||
|
||||
// check whether the window is too close to the screen buffer top
|
||||
let count = row_count as i16;
|
||||
if window.top >= count {
|
||||
window.top -= count; // move top down
|
||||
window.bottom -= count; // move bottom down
|
||||
|
||||
Console::output()?.set_console_info(true, window)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn scroll_down(row_count: u16) -> std::io::Result<()> {
|
||||
let screen_buffer = ScreenBuffer::current()?;
|
||||
let csbi = screen_buffer.info()?;
|
||||
let mut window = csbi.terminal_window();
|
||||
let buffer_size = csbi.buffer_size();
|
||||
|
||||
// check whether the window is too close to the screen buffer top
|
||||
let count = row_count as i16;
|
||||
if window.bottom < buffer_size.height - count {
|
||||
window.top += count; // move top down
|
||||
window.bottom += count; // move bottom down
|
||||
|
||||
Console::output()?.set_console_info(true, window)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn set_size(width: u16, height: u16) -> std::io::Result<()> {
|
||||
if width <= 1 {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
"terminal width must be at least 1",
|
||||
));
|
||||
}
|
||||
|
||||
if height <= 1 {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
"terminal height must be at least 1",
|
||||
));
|
||||
}
|
||||
|
||||
// get the position of the current console window
|
||||
let screen_buffer = ScreenBuffer::current()?;
|
||||
let console = Console::from(screen_buffer.handle().clone());
|
||||
let csbi = screen_buffer.info()?;
|
||||
|
||||
let current_size = csbi.buffer_size();
|
||||
let window = csbi.terminal_window();
|
||||
|
||||
let mut new_size = Size::new(current_size.width, current_size.height);
|
||||
|
||||
// If the buffer is smaller than this new window size, resize the
|
||||
// buffer to be large enough. Include window position.
|
||||
let mut resize_buffer = false;
|
||||
|
||||
let width = width as i16;
|
||||
if current_size.width < window.left + width {
|
||||
if window.left >= i16::MAX - width {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
"terminal width too large",
|
||||
));
|
||||
}
|
||||
|
||||
new_size.width = window.left + width;
|
||||
resize_buffer = true;
|
||||
}
|
||||
let height = height as i16;
|
||||
if current_size.height < window.top + height {
|
||||
if window.top >= i16::MAX - height {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
"terminal height too large",
|
||||
));
|
||||
}
|
||||
|
||||
new_size.height = window.top + height;
|
||||
resize_buffer = true;
|
||||
}
|
||||
|
||||
if resize_buffer {
|
||||
screen_buffer.set_size(new_size.width - 1, new_size.height - 1)?;
|
||||
}
|
||||
|
||||
let mut window = window;
|
||||
|
||||
// preserve the position, but change the size.
|
||||
window.bottom = window.top + height - 1;
|
||||
window.right = window.left + width - 1;
|
||||
console.set_console_info(true, window)?;
|
||||
|
||||
// if we resized the buffer, un-resize it.
|
||||
if resize_buffer {
|
||||
screen_buffer.set_size(current_size.width - 1, current_size.height - 1)?;
|
||||
}
|
||||
|
||||
let bounds = console.largest_window_size()?;
|
||||
|
||||
if width > bounds.x {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
format!("terminal width {width} too large"),
|
||||
));
|
||||
}
|
||||
if height > bounds.y {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidInput,
|
||||
format!("terminal height {height} too large"),
|
||||
));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn set_window_title(title: impl fmt::Display) -> std::io::Result<()> {
|
||||
struct Utf16Encoder(Vec<u16>);
|
||||
impl Write for Utf16Encoder {
|
||||
fn write_str(&mut self, s: &str) -> fmt::Result {
|
||||
self.0.extend(s.encode_utf16());
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
let mut title_utf16 = Utf16Encoder(Vec::new());
|
||||
write!(title_utf16, "{title}").expect("formatting failed");
|
||||
title_utf16.0.push(0);
|
||||
let title = title_utf16.0;
|
||||
|
||||
let result = unsafe { SetConsoleTitleW(title.as_ptr()) };
|
||||
if result != 0 {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(io::Error::last_os_error())
|
||||
}
|
||||
}
|
||||
|
||||
fn clear_after_cursor(
|
||||
location: Coord,
|
||||
buffer_size: Size,
|
||||
current_attribute: u16,
|
||||
) -> std::io::Result<()> {
|
||||
let (mut x, mut y) = (location.x, location.y);
|
||||
|
||||
// if cursor position is at the outer right position
|
||||
if x > buffer_size.width {
|
||||
y += 1;
|
||||
x = 0;
|
||||
}
|
||||
|
||||
// location where to start clearing
|
||||
let start_location = Coord::new(x, y);
|
||||
|
||||
// get sum cells before cursor
|
||||
let cells_to_write = buffer_size.width as u32 * buffer_size.height as u32;
|
||||
|
||||
clear_winapi(start_location, cells_to_write, current_attribute)
|
||||
}
|
||||
|
||||
fn clear_before_cursor(
|
||||
location: Coord,
|
||||
buffer_size: Size,
|
||||
current_attribute: u16,
|
||||
) -> std::io::Result<()> {
|
||||
let (xpos, ypos) = (location.x, location.y);
|
||||
|
||||
// one cell after cursor position
|
||||
let x = 0;
|
||||
// one at row of cursor position
|
||||
let y = 0;
|
||||
|
||||
// location where to start clearing
|
||||
let start_location = Coord::new(x, y);
|
||||
|
||||
// get sum cells before cursor
|
||||
let cells_to_write = (buffer_size.width as u32 * ypos as u32) + (xpos as u32 + 1);
|
||||
|
||||
// clear everything before cursor position
|
||||
clear_winapi(start_location, cells_to_write, current_attribute)
|
||||
}
|
||||
|
||||
fn clear_entire_screen(buffer_size: Size, current_attribute: u16) -> std::io::Result<()> {
|
||||
// get sum cells before cursor
|
||||
let cells_to_write = buffer_size.width as u32 * buffer_size.height as u32;
|
||||
|
||||
// location where to start clearing
|
||||
let start_location = Coord::new(0, 0);
|
||||
|
||||
// clear the entire screen
|
||||
clear_winapi(start_location, cells_to_write, current_attribute)?;
|
||||
|
||||
// put the cursor back at cell 0,0
|
||||
cursor::sys::move_to(0, 0)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn clear_current_line(
|
||||
location: Coord,
|
||||
buffer_size: Size,
|
||||
current_attribute: u16,
|
||||
) -> std::io::Result<()> {
|
||||
// location where to start clearing
|
||||
let start_location = Coord::new(0, location.y);
|
||||
|
||||
// get sum cells before cursor
|
||||
let cells_to_write = buffer_size.width as u32;
|
||||
|
||||
// clear the whole current line
|
||||
clear_winapi(start_location, cells_to_write, current_attribute)?;
|
||||
|
||||
// put the cursor back at cell 1 on current row
|
||||
cursor::sys::move_to(0, location.y as u16)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn clear_until_line(
|
||||
location: Coord,
|
||||
buffer_size: Size,
|
||||
current_attribute: u16,
|
||||
) -> std::io::Result<()> {
|
||||
let (x, y) = (location.x, location.y);
|
||||
|
||||
// location where to start clearing
|
||||
let start_location = Coord::new(x, y);
|
||||
|
||||
// get sum cells before cursor
|
||||
let cells_to_write = (buffer_size.width - x) as u32;
|
||||
|
||||
// clear until the current line
|
||||
clear_winapi(start_location, cells_to_write, current_attribute)?;
|
||||
|
||||
// put the cursor back at original cursor position before we did the clearing
|
||||
cursor::sys::move_to(x as u16, y as u16)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn clear_winapi(
|
||||
start_location: Coord,
|
||||
cells_to_write: u32,
|
||||
current_attribute: u16,
|
||||
) -> std::io::Result<()> {
|
||||
let console = Console::from(Handle::current_out_handle()?);
|
||||
console.fill_whit_character(start_location, cells_to_write, ' ')?;
|
||||
console.fill_whit_attribute(start_location, cells_to_write, current_attribute)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
// Create a new screen buffer to avoid changing the terminal the test
|
||||
// is running within.
|
||||
pub fn temp_screen_buffer() -> std::io::Result<ScreenBuffer> {
|
||||
let alternate_screen = ScreenBuffer::create()?;
|
||||
alternate_screen.show().unwrap();
|
||||
Ok(alternate_screen)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::{ffi::OsString, os::windows::ffi::OsStringExt};
|
||||
|
||||
use crossterm_winapi::ScreenBuffer;
|
||||
use serial_test::serial;
|
||||
use winapi::um::wincon::GetConsoleTitleW;
|
||||
|
||||
use super::{scroll_down, scroll_up, set_size, set_window_title, size, temp_screen_buffer};
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn test_resize_winapi_20_21() {
|
||||
let _test_screen = temp_screen_buffer().unwrap();
|
||||
|
||||
let (width, height) = size().unwrap();
|
||||
|
||||
// The values 20 and 21 are arbitrary and different from each other
|
||||
// just to see they're not crossed over.
|
||||
set_size(20, 21).unwrap();
|
||||
assert_eq!((20, 21), size().unwrap());
|
||||
|
||||
// reset to previous size
|
||||
set_size(width, height).unwrap();
|
||||
assert_eq!((width, height), size().unwrap());
|
||||
}
|
||||
|
||||
// This is similar to test_resize_winapi_20_21() above. This verifies that
|
||||
// another test of similar functionality runs independently (that a testing
|
||||
// race condition has been addressed).
|
||||
#[test]
|
||||
#[serial]
|
||||
#[ignore]
|
||||
fn test_resize_winapi_30_31() {
|
||||
let _test_screen = temp_screen_buffer().unwrap();
|
||||
|
||||
let (width, height) = size().unwrap();
|
||||
|
||||
set_size(30, 31).unwrap();
|
||||
assert_eq!((30, 31), size().unwrap());
|
||||
|
||||
// reset to previous size
|
||||
set_size(width, height).unwrap();
|
||||
assert_eq!((width, height), size().unwrap());
|
||||
}
|
||||
|
||||
// Test is disabled, because it's failing on Travis CI
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn test_scroll_down_winapi() {
|
||||
let current_window = ScreenBuffer::current()
|
||||
.unwrap()
|
||||
.info()
|
||||
.unwrap()
|
||||
.terminal_window();
|
||||
|
||||
scroll_down(2).unwrap();
|
||||
|
||||
let new_window = ScreenBuffer::current()
|
||||
.unwrap()
|
||||
.info()
|
||||
.unwrap()
|
||||
.terminal_window();
|
||||
|
||||
assert_eq!(new_window.top, current_window.top + 2);
|
||||
assert_eq!(new_window.bottom, current_window.bottom + 2);
|
||||
}
|
||||
|
||||
// Test is disabled, because it's failing on Travis CI
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn test_scroll_up_winapi() {
|
||||
// move the terminal buffer down before moving it up
|
||||
test_scroll_down_winapi();
|
||||
|
||||
let current_window = ScreenBuffer::current()
|
||||
.unwrap()
|
||||
.info()
|
||||
.unwrap()
|
||||
.terminal_window();
|
||||
|
||||
scroll_up(2).unwrap();
|
||||
|
||||
let new_window = ScreenBuffer::current()
|
||||
.unwrap()
|
||||
.info()
|
||||
.unwrap()
|
||||
.terminal_window();
|
||||
|
||||
assert_eq!(new_window.top, current_window.top - 2);
|
||||
assert_eq!(new_window.bottom, current_window.bottom - 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn test_set_title_winapi() {
|
||||
let _test_screen = temp_screen_buffer().unwrap();
|
||||
|
||||
let test_title = "this is a crossterm test title";
|
||||
set_window_title(test_title).unwrap();
|
||||
|
||||
let mut raw = [0_u16; 128];
|
||||
let length = unsafe { GetConsoleTitleW(raw.as_mut_ptr(), raw.len() as u32) } as usize;
|
||||
assert_ne!(0, length);
|
||||
|
||||
let console_title = OsString::from_wide(&raw[..length]).into_string().unwrap();
|
||||
assert_eq!(test_title, &console_title[..]);
|
||||
}
|
||||
}
|
54
src/tty.rs
54
src/tty.rs
@ -1,54 +0,0 @@
|
||||
//! Making it a little more convenient and safe to query whether
|
||||
//! something is a terminal teletype or not.
|
||||
//! This module defines the IsTty trait and the is_tty method to
|
||||
//! return true if the item represents a terminal.
|
||||
|
||||
#[cfg(unix)]
|
||||
use std::os::unix::io::AsRawFd;
|
||||
#[cfg(windows)]
|
||||
use std::os::windows::io::AsRawHandle;
|
||||
|
||||
#[cfg(windows)]
|
||||
use winapi::um::consoleapi::GetConsoleMode;
|
||||
|
||||
/// Adds the `is_tty` method to types that might represent a terminal
|
||||
///
|
||||
/// ```rust
|
||||
/// use std::io::stdout;
|
||||
/// use crossterm::tty::IsTty;
|
||||
///
|
||||
/// let is_tty: bool = stdout().is_tty();
|
||||
/// ```
|
||||
pub trait IsTty {
|
||||
/// Returns true when an instance is a terminal teletype, otherwise false.
|
||||
fn is_tty(&self) -> bool;
|
||||
}
|
||||
|
||||
/// On UNIX, the `isatty()` function returns true if a file
|
||||
/// descriptor is a terminal.
|
||||
#[cfg(all(unix, feature = "libc"))]
|
||||
impl<S: AsRawFd> IsTty for S {
|
||||
fn is_tty(&self) -> bool {
|
||||
let fd = self.as_raw_fd();
|
||||
unsafe { libc::isatty(fd) == 1 }
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(unix, not(feature = "libc")))]
|
||||
impl<S: AsRawFd> IsTty for S {
|
||||
fn is_tty(&self) -> bool {
|
||||
let fd = self.as_raw_fd();
|
||||
rustix::termios::isatty(unsafe { std::os::unix::io::BorrowedFd::borrow_raw(fd) })
|
||||
}
|
||||
}
|
||||
|
||||
/// On windows, `GetConsoleMode` will return true if we are in a terminal.
|
||||
/// Otherwise false.
|
||||
#[cfg(windows)]
|
||||
impl<S: AsRawHandle> IsTty for S {
|
||||
fn is_tty(&self) -> bool {
|
||||
let mut mode = 0;
|
||||
let ok = unsafe { GetConsoleMode(self.as_raw_handle() as *mut _, &mut mode) };
|
||||
ok == 1
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user